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:
- Elements, attributes, and text content
- Namespaced (qualified) names such as
dc:title, kept as strings - The five predefined entities (
&<>"') and decimal/hexadecimal character references (©,😀) - CDATA sections, comments, and processing instructions
- The XML declaration, byte order marks, and CRLF/CR line ending normalisation
- DOCTYPE declarations and DTDs:
<!ELEMENT>,<!ATTLIST>,<!ENTITY>(general and parameter), and<!NOTATION>declarations, entity expansion in content and attribute values (including entities containing markup, with recursion detection), and conditional sections in external subsets. Validation lives in theglexml/dtdmodule.
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
-
AttributeDeclaration( name: String, kind: AttributeKind, default: AttributeDefault, )
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
-
EmptyContentEMPTY: no children at all. -
AnyContentANY: 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
SYSTEMorPUBLIC, if any. The parser never fetches it; load its text yourself and parse it withparse_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
Stringbefore 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_violationschecks 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 inelements; declaring an element twice is a validity error thatglexml/dtd.validatereports. - 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
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
notationmarks an unparsed (binary) entity. External entities cannot be expanded during parsing; supply their content asInternalEntityvalues in a DTD passed toparse_with_dtdif you need it.declared_innames 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
-
UnexpectedEndOfInputThe document ended before parsing finished, e.g. an unclosed element or a comment with no
-->. -
MissingRootElementNo root element was found.
-
ContentAfterRootElementThere was more than whitespace, comments, or processing instructions after the root element was closed.
-
InvalidNameA 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.
-
MalformedEntityAn
&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.
. XML (unlike HTML) only definesamp,lt,gt,quot, andapos. -
InvalidCharacterReference(reference: String)A numeric character reference that is not a valid XML character, such as
�. -
MalformedDoctypeA 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
NDATAentity. -
MarkupInAttributeValue(entity: String)An entity used in an attribute value expands to text containing
<, which XML forbids. -
InvalidCharacterA character that XML does not allow in documents, such as most control characters, or a
<inside an attribute value. -
MalformedCommentA comment containing
--, which XML forbids. -
CdataEndInContentThe 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.
-
MissingWhitespaceWhitespace was required, for example between two attributes.
-
UnsupportedEncoding(encoding: String)parse_bytescould 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.
literalis 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
UnknownEntityparse 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
-
NameParticle(name: String, occurrence: Occurrence) -
Sequence(particles: List(Particle), occurrence: Occurrence) -
Choice(particles: List(Particle), occurrence: Occurrence)
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 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 \" \">")
glexml.parse_with_dtd("<p>a 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.