glexml

A pure Gleam XML parser that runs on both the Erlang and JavaScript targets, making it suitable for use on the server and in the browser.

glexml parses a whole XML document into an immutable tree of nodes and provides helper functions for navigating that tree. It was built to be the core of an EPUB tool, so it comfortably handles the documents found inside EPUB files: package documents (OPF), container.xml, NCX, and XHTML content documents.

Supported XML features:

The parser never performs I/O, so external entities and external DTD subsets are not fetched (fetching them is a classic security hole). Load an external DTD’s text yourself, parse it with parse_dtd, and pass it to parse_with_dtd to make its entities available.

parse takes a String (already-decoded text). For raw bytes use parse_bytes, which detects the encoding as the XML specification describes and decodes UTF-8 and UTF-16 itself.

Internally the parser scans a BitArray rather than a String: bit array patterns compile to binary pattern matching on Erlang and constant time buffer views on JavaScript, which keeps parsing linear on both targets. Every scanning delimiter in XML is ASCII, so byte-level scanning never splits a multi-byte UTF-8 character.

Types

A single attribute on an element. Entity and character references in the value have already been decoded.

pub type Attribute {
  Attribute(name: String, value: String)
}

Constructors

  • Attribute(name: String, value: String)

A single attribute declared in an <!ATTLIST>.

pub type AttributeDeclaration {
  AttributeDeclaration(
    name: String,
    kind: AttributeKind,
    default: AttributeDefault,
  )
}

Constructors

The default rule of a declared attribute.

pub type AttributeDefault {
  Required
  Implied
  Fixed(value: String)
  Default(value: String)
}

Constructors

  • Required
  • Implied
  • Fixed(value: String)
  • Default(value: String)

The declared type of an attribute.

pub type AttributeKind {
  CdataAttribute
  IdAttribute
  IdRefAttribute
  IdRefsAttribute
  EntityAttribute
  EntitiesAttribute
  NmtokenAttribute
  NmtokensAttribute
  NotationAttribute(allowed: List(String))
  EnumeratedAttribute(allowed: List(String))
}

Constructors

  • CdataAttribute
  • IdAttribute
  • IdRefAttribute
  • IdRefsAttribute
  • EntityAttribute
  • EntitiesAttribute
  • NmtokenAttribute
  • NmtokensAttribute
  • NotationAttribute(allowed: List(String))
  • EnumeratedAttribute(allowed: List(String))

The allowed content of an element, from <!ELEMENT>.

pub type ContentModel {
  EmptyContent
  AnyContent
  MixedContent(allowed: List(String))
  ElementContent(particle: Particle)
}

Constructors

  • EmptyContent

    EMPTY: no children at all.

  • AnyContent

    ANY: anything goes.

  • MixedContent(allowed: List(String))

    (#PCDATA | a | b)*: text freely mixed with the listed elements.

  • ElementContent(particle: Particle)

    A children content model such as (head, body) or (a | b)+.

A <!DOCTYPE ...> declaration.

pub type Doctype {
  Doctype(
    root_name: String,
    external_id: option.Option(ExternalId),
    declarations: Dtd,
  )
}

Constructors

  • Doctype(
      root_name: String,
      external_id: option.Option(ExternalId),
      declarations: Dtd,
    )

    Arguments

    root_name

    The name the root element is declared to have.

    external_id

    The external DTD referenced with SYSTEM or PUBLIC, if any. The parser never fetches it; load its text yourself and parse it with parse_dtd.

    declarations

    The declarations from the internal subset ([ ... ]).

A parsed XML document.

pub type Document {
  Document(
    version: String,
    encoding: option.Option(String),
    standalone: Bool,
    doctype: option.Option(Doctype),
    prolog: List(Node),
    root: Element,
    epilogue: List(Node),
  )
}

Constructors

  • Document(
      version: String,
      encoding: option.Option(String),
      standalone: Bool,
      doctype: option.Option(Doctype),
      prolog: List(Node),
      root: Element,
      epilogue: List(Node),
    )

    Arguments

    version

    The version from the XML declaration, or "1.0" if there was none.

    encoding

    The encoding named by the XML declaration, if any. Note that the input has already been decoded to a String before parsing, so this is informational only.

    standalone

    Whether the declaration said standalone="yes". Standalone documents promise that nothing outside the document affects its content; glexml/dtd.standalone_violations checks that promise.

    doctype

    The DOCTYPE declaration, if the document had one.

    prolog

    Comments and processing instructions (such as <?xml-stylesheet?>) that appeared before the root element, in order.

    root

    The root element of the document.

    epilogue

    Comments and processing instructions that appeared after the root element, in order.

The declarations of a document type definition. Build one by parsing a DOCTYPE (see Document.doctype) or an external subset with parse_dtd, or start from empty_dtd.

pub type Dtd {
  Dtd(
    elements: dict.Dict(String, ContentModel),
    attribute_lists: dict.Dict(String, List(AttributeDeclaration)),
    entities: dict.Dict(String, Entity),
    parameter_entities: dict.Dict(String, String),
    notations: List(String),
    duplicate_elements: List(String),
    pe_nesting_violations: List(String),
  )
}

Constructors

  • Dtd(
      elements: dict.Dict(String, ContentModel),
      attribute_lists: dict.Dict(String, List(AttributeDeclaration)),
      entities: dict.Dict(String, Entity),
      parameter_entities: dict.Dict(String, String),
      notations: List(String),
      duplicate_elements: List(String),
      pe_nesting_violations: List(String),
    )

    Arguments

    elements

    Content models from <!ELEMENT> declarations, by element name.

    attribute_lists

    Attribute declarations from <!ATTLIST>, by element name.

    entities

    General entities from <!ENTITY name ...>.

    parameter_entities

    Parameter entities from <!ENTITY % name ...>, with their replacement text.

    notations

    Names declared with <!NOTATION>.

    duplicate_elements

    Element names that were declared with <!ELEMENT> more than once. The first declaration is the one kept in elements; declaring an element twice is a validity error that glexml/dtd.validate reports.

    pe_nesting_violations

    Violations of the Proper Declaration/Group/Conditional Section PE Nesting validity constraints found while parsing: constructs that open in one parameter entity’s replacement text and close in another. Reported by glexml/dtd.validate.

An XML element: a name, attributes, and child nodes.

Names are kept exactly as written, including any namespace prefix, so <dc:title> has the name "dc:title". Use local_name and namespace_prefix to split qualified names.

pub type Element {
  Element(
    name: String,
    attributes: List(Attribute),
    children: List(Node),
  )
}

Constructors

  • Element(
      name: String,
      attributes: List(Attribute),
      children: List(Node),
    )

A general entity declared in a DTD.

pub type Entity {
  InternalEntity(replacement: String)
  ExternalEntity(
    id: ExternalId,
    notation: option.Option(String),
    declared_in: option.Option(String),
  )
}

Constructors

  • InternalEntity(replacement: String)

    An entity whose replacement text was given inline. Parameter and character references have already been expanded in it.

  • ExternalEntity(
      id: ExternalId,
      notation: option.Option(String),
      declared_in: option.Option(String),
    )

    An entity whose content lives in another resource. A notation marks an unparsed (binary) entity. External entities cannot be expanded during parsing; supply their content as InternalEntity values in a DTD passed to parse_with_dtd if you need it.

    declared_in names the external parameter entity whose replacement text contained this declaration, if any: the entity’s system identifier resolves relative to that entity’s own location rather than the document’s.

The specific problem found while parsing.

pub type ErrorKind {
  UnexpectedEndOfInput
  MissingRootElement
  ContentAfterRootElement
  InvalidName
  MalformedAttribute(attribute: String)
  DuplicateAttribute(attribute: String)
  MismatchedClosingTag(opening: String, closing: String)
  MalformedEntity
  UnknownEntity(entity: String)
  InvalidCharacterReference(reference: String)
  MalformedDoctype
  RecursiveEntity(entity: String)
  UnresolvableEntity(entity: String)
  MarkupInAttributeValue(entity: String)
  InvalidCharacter
  MalformedComment
  CdataEndInContent
  UnbalancedEntity(entity: String)
  MissingWhitespace
  UnsupportedEncoding(encoding: String)
  DeclaredEncodingMismatch(declared: String, detected: String)
}

Constructors

  • UnexpectedEndOfInput

    The document ended before parsing finished, e.g. an unclosed element or a comment with no -->.

  • MissingRootElement

    No root element was found.

  • ContentAfterRootElement

    There was more than whitespace, comments, or processing instructions after the root element was closed.

  • InvalidName

    A name was expected (for an element, attribute, or closing tag) but not found.

  • MalformedAttribute(attribute: String)

    An attribute was not followed by ="value" or ='value'.

  • DuplicateAttribute(attribute: String)

    The same attribute appeared twice on one element.

  • MismatchedClosingTag(opening: String, closing: String)

    A closing tag did not match the currently open element.

  • MalformedEntity

    An & was not followed by a terminated entity reference.

  • UnknownEntity(entity: String)

    An entity reference other than the five predefined XML entities or a character reference, e.g. &nbsp;. XML (unlike HTML) only defines amp, lt, gt, quot, and apos.

  • InvalidCharacterReference(reference: String)

    A numeric character reference that is not a valid XML character, such as &#xD800;.

  • MalformedDoctype

    A DOCTYPE or a declaration inside a DTD could not be parsed.

  • RecursiveEntity(entity: String)

    An entity expands to itself, directly or indirectly.

  • UnresolvableEntity(entity: String)

    A reference to an entity whose content is not available: an external entity (which this parser never fetches) or an unparsed NDATA entity.

  • MarkupInAttributeValue(entity: String)

    An entity used in an attribute value expands to text containing <, which XML forbids.

  • InvalidCharacter

    A character that XML does not allow in documents, such as most control characters, or a < inside an attribute value.

  • MalformedComment

    A comment containing --, which XML forbids.

  • CdataEndInContent

    The literal sequence ]]> in character data, which XML forbids.

  • UnbalancedEntity(entity: String)

    An entity whose content is not balanced: it opens elements it does not close, or closes elements it did not open.

  • MissingWhitespace

    Whitespace was required, for example between two attributes.

  • UnsupportedEncoding(encoding: String)

    parse_bytes could not decode the input: an encoding this library does not support (such as UTF-32 or EBCDIC), or bytes that are not valid for the encoding they claim to be.

  • DeclaredEncodingMismatch(declared: String, detected: String)

    The encoding named in the XML declaration contradicts the encoding the document is actually written in, e.g. encoding="UTF-16" on a UTF-8 document.

A reference to an external resource in a DOCTYPE, entity, or notation declaration.

pub type ExternalId {
  System(system: String)
  Public(public: String, system: option.Option(String))
}

Constructors

  • System(system: String)
  • Public(public: String, system: option.Option(String))

A node in the document tree.

pub type Node {
  ElementNode(Element)
  TextNode(text: String, literal: Bool)
  CommentNode(String)
  ProcessingInstructionNode(target: String, content: String)
  EntityReferenceNode(entity: String)
}

Constructors

  • ElementNode(Element)
  • TextNode(text: String, literal: Bool)

    Character data. Entity and character references have been decoded, and CDATA sections become text nodes with their contents kept verbatim. Whitespace is preserved.

    literal is False when any of the text came from a CDATA section or from a character reference recognised in content. The distinction matters for one validity rule: in element-only content models, whitespace between children only counts as separator whitespace when it is literal. Entity expansions still count as literal, unless the replacement text itself contained CDATA sections or character references.

  • CommentNode(String)

    A comment, with the <!-- and --> delimiters removed.

  • ProcessingInstructionNode(target: String, content: String)

    A processing instruction such as <?xml-stylesheet href="a.xsl"?>, split into its target and content. The whitespace separating the two is removed; the rest of the content is kept verbatim.

  • EntityReferenceNode(entity: String)

    A reference to an entity that was not declared, in a document whose DTD could not have been read completely (it has an external subset or uses parameter entity references, and is not standalone). In that situation the XML specification makes an undeclared entity a validity error rather than a well-formedness error, so the reference is kept unexpanded instead of failing the parse. In all other documents an undeclared entity is still an UnknownEntity parse error.

The ?, *, or + suffix of a content particle.

pub type Occurrence {
  ExactlyOne
  ZeroOrOne
  ZeroOrMore
  OneOrMore
}

Constructors

  • ExactlyOne
  • ZeroOrOne
  • ZeroOrMore
  • OneOrMore

An error encountered while parsing, along with where it happened.

Lines and columns are 1-indexed and counted in graphemes, and offset is the 0-indexed grapheme offset into the (line ending normalised) input.

pub type ParseError {
  ParseError(
    kind: ErrorKind,
    line: Int,
    column: Int,
    offset: Int,
  )
}

Constructors

  • ParseError(kind: ErrorKind, line: Int, column: Int, offset: Int)

One step of a children content model.

pub type Particle {
  NameParticle(name: String, occurrence: Occurrence)
  Sequence(particles: List(Particle), occurrence: Occurrence)
  Choice(particles: List(Particle), occurrence: Occurrence)
}

Constructors

Values

pub fn at(
  element: Element,
  path: List(String),
) -> Result(Element, Nil)

Follow a path of element names from an element, taking the first matching child at each step.

glexml.at(package, ["metadata", "dc:title"])
// -> Ok(the <dc:title> element inside <metadata>)
pub fn attribute(
  element: Element,
  name: String,
) -> Result(String, Nil)

Get the value of the named attribute of an element.

pub fn child_elements(element: Element) -> List(Element)

Get the element children of an element, skipping text, comments, and processing instructions.

pub fn children_named(
  element: Element,
  name: String,
) -> List(Element)

Get the element children of an element that have the given name.

pub fn descendants_named(
  element: Element,
  name: String,
) -> List(Element)

Get every descendant element with the given name, in document order.

glexml.descendants_named(package, "item")
// -> every <item> anywhere below <package>
pub fn doctype_to_string(doctype: Doctype) -> String

Serialise a DOCTYPE declaration. The internal subset is not rendered, only the root name and any external identifier.

pub fn document_to_string(document: Document) -> String

Serialise a document to a string, including an XML declaration.

Text and attribute values are escaped as needed. Elements with no children are rendered self-closing.

pub fn element_to_string(element: Element) -> String

Serialise an element and its children to a string.

pub fn empty_dtd() -> Dtd

A DTD with no declarations at all.

pub fn error_to_string(error: ParseError) -> String

Convert a ParseError into a human readable message, suitable for showing to users of your tool.

pub fn first_child_named(
  element: Element,
  name: String,
) -> Result(Element, Nil)

Get the first element child of an element with the given name.

pub fn local_name(name: String) -> String

The part of a qualified name after any namespace prefix.

glexml.local_name("dc:title")
// -> "title"
glexml.local_name("title")
// -> "title"
pub fn merge_dtds(preferred: Dtd, other: Dtd) -> Dtd

Combine two DTDs. Where both declare the same name the preferred DTD wins, matching the XML rule that the first declaration of an entity is binding. Use this to merge a document’s internal subset with an external DTD: merge_dtds(doctype.declarations, external).

One exception to preference: when the preferred DTD declares an entity as an external parsed entity (whose content this library cannot fetch) and the other DTD supplies an InternalEntity under the same name, the supplied content is used. This is how callers provide the content of external entities: load the file yourself and pass it as an InternalEntity in the DTD given to parse_with_dtd.

pub fn namespace_prefix(name: String) -> option.Option(String)

The namespace prefix of a qualified name, if it has one.

glexml.namespace_prefix("dc:title")
// -> Some("dc")
pub fn parse(input: String) -> Result(Document, ParseError)

Parse a string of XML into a Document.

let assert Ok(document) = glexml.parse("<greeting lang=\"en\">Hello</greeting>")
document.root.name
// -> "greeting"
pub fn parse_bytes(
  input: BitArray,
) -> Result(Document, ParseError)

Parse raw bytes of XML, detecting the character encoding first.

Detection follows Appendix F of the XML specification: a byte order mark, or the byte pattern of <?xml at the start of the input. UTF-8 (the default) and UTF-16 in either byte order are decoded; UTF-32 and EBCDIC are recognised but unsupported, and reported as UnsupportedEncoding. An encoding declaration that contradicts the detected encoding is an error, as the specification requires.

Use this instead of parse when reading files whose encoding is not known to be UTF-8, such as XML inside EPUB 2 publications.

pub fn parse_bytes_with_dtd(
  input: BitArray,
  dtd: Dtd,
) -> Result(Document, ParseError)

parse_bytes with extra DTD declarations available, as with parse_with_dtd.

pub fn parse_dtd(input: String) -> Result(Dtd, ParseError)

Parse the text of an external DTD subset (a .dtd file) into a Dtd.

Parameter entities, conditional sections (<![INCLUDE[/<![IGNORE[), and all four declaration kinds are supported. The result can be passed to parse_with_dtd so its entities resolve during parsing, and to glexml/dtd.validate to validate a document against it.

pub fn parse_dtd_with(
  input: String,
  seed: Dtd,
) -> Result(Dtd, ParseError)

Like parse_dtd, but with declarations already in scope: the given DTD’s declarations bind first, exactly as if they had been read before this subset. Use this when an external subset refers to parameter entities declared in a document’s internal subset, which is read first.

pub fn parse_with_dtd(
  input: String,
  dtd: Dtd,
) -> Result(Document, ParseError)

Parse a string of XML with extra DTD declarations available, typically an external DTD subset loaded separately and parsed with parse_dtd.

Entities declared in the document’s own internal subset take precedence over the ones given here, mirroring how XML processors read the internal subset first.

let assert Ok(xhtml_dtd) = glexml.parse_dtd("<!ENTITY nbsp \"&#160;\">")
glexml.parse_with_dtd("<p>a&nbsp;b</p>", xhtml_dtd)
// -> Ok(...)
pub fn text_content(element: Element) -> String

Concatenate all text in an element and its descendants, in document order. Comments and processing instructions contribute nothing.

Search Document