# glexml
[](https://hex.pm/packages/glexml)
[](https://hexdocs.pm/glexml/)
A pure Gleam XML parser that runs on both the Erlang and JavaScript targets —
the same code works on the server and in the browser, with no FFI and no
native dependencies. It was built to be the core of an EPUB tool, so it
handles the documents found inside EPUB files well: package documents (OPF),
`container.xml`, NCX, and XHTML content documents.
```sh
gleam add glexml
```
## Quick start
```gleam
import glexml
pub fn main() {
let assert Ok(document) =
glexml.parse("<greeting lang=\"en\">Hello, Joe!</greeting>")
document.root.name
// -> "greeting"
glexml.attribute(document.root, "lang")
// -> Ok("en")
glexml.text_content(document.root)
// -> "Hello, Joe!"
}
```
## An EPUB example
Reading the title and manifest out of an EPUB package document:
```gleam
import gleam/list
import glexml
pub fn read_package(opf_source: String) {
let assert Ok(document) = glexml.parse(opf_source)
let package = document.root
// <metadata><dc:title>…</dc:title></metadata>
let assert Ok(title) = glexml.at(package, ["metadata", "dc:title"])
let title = glexml.text_content(title)
// Every <item> in the <manifest>
let assert Ok(manifest) = glexml.first_child_named(package, "manifest")
let hrefs =
glexml.children_named(manifest, "item")
|> list.filter_map(glexml.attribute(_, "href"))
#(title, hrefs)
}
```
## Features
- **Both targets.** Pure Gleam with only `gleam_stdlib` as a dependency; the
test suite runs on Erlang and on Node.js. Parsing is iterative where it
matters, so documents with very long runs of sibling nodes do not overflow
the JavaScript stack.
- **A complete tree.** Elements, attributes, text, CDATA sections, comments,
and processing instructions, with document order and whitespace preserved.
- **The XML you meet in practice.** XML declarations, byte order marks,
CRLF/CR line ending normalisation, the five predefined entities, and
decimal and hexadecimal character references.
- **Full DTD support.** DOCTYPE declarations are parsed, not skipped:
`<!ELEMENT>`, `<!ATTLIST>`, `<!ENTITY>` (general and parameter), and
`<!NOTATION>` declarations, entity expansion in content and attribute
values — including entities that contain markup, with recursion
detection — plus conditional sections and parameter entities in external
subsets via `parse_dtd`.
- **Validation.** The `glexml/dtd` module validates documents against a
DTD: content models, required/fixed/enumerated attributes, ID uniqueness
and IDREF integrity, notations, and unparsed entities, with readable
violation messages. `with_default_attributes` fills in attribute defaults
the way a validating processor would.
- **Namespace-friendly.** Qualified names like `dc:title` are kept as
written, with `local_name` and `namespace_prefix` helpers for splitting
them.
- **Navigation helpers.** `attribute`, `children_named`, `first_child_named`,
`descendants_named`, `at` (path lookup), and `text_content`.
- **CSS selectors.** The `glexml/selector` module implements the practical
selector subset over XML: type/attribute/class/ID selectors, all four
combinators, selector lists, and structural pseudo-classes including
`:nth-child()` and `:not()`. Matching is case-sensitive, and `dc|title` /
`|title` handle namespace prefixes.
- **Typed errors with positions.** Parse failures return an `ErrorKind` plus
line and column, and `error_to_string` renders a readable message.
- **Serialisation.** `document_to_string` and `element_to_string` write a
tree back out with correct escaping, for tools that also produce XML.
## CSS selectors
```gleam
import glexml/selector
// One-shot
selector.select(package, "manifest > item[properties~=nav]")
// -> Ok([the nav item])
// Parse once, query many times
let assert Ok(chapters) = selector.parse("item[media-type='application/xhtml+xml']")
selector.query(package, chapters)
selector.query_first(package, chapters)
selector.matches(item, chapters)
```
Because glexml keeps qualified names as literal strings, `dc|title` matches
the element written `<dc:title>`, and `|title` (or `*|title`) matches a
`title` local name under any prefix. `.x` and `#x` follow the usual XML
convention of meaning `[class~=x]` and `[id=x]`.
## Working with DTDs
Entities declared in a document's internal subset just work:
```gleam
let assert Ok(document) =
glexml.parse("<!DOCTYPE p [<!ENTITY nbsp \" \">]><p>a b</p>")
```
The parser never performs I/O, so external DTDs are not fetched (fetching
them is a classic security hole). To use one — say the XHTML DTD entities an
EPUB 2 content document relies on — load its text yourself and pass it in:
```gleam
let assert Ok(xhtml) = glexml.parse_dtd(xhtml_dtd_text)
let assert Ok(document) = glexml.parse_with_dtd(chapter_source, xhtml)
```
And to validate:
```gleam
import glexml/dtd
let assert Some(doctype) = document.doctype
case dtd.validate(document, doctype.declarations) {
[] -> Ok(document)
violations -> Error(list.map(violations, dtd.violation_to_string))
}
```
## Limitations
- External entities and external DTD subsets are never fetched; supply them
with `parse_dtd` + `parse_with_dtd` as shown above. Undeclared entities
are a parse error, except in documents whose DTD provably could not be
read completely (an external subset or parameter entity references, and
not standalone) — there the specification makes them a validity error, so
they are kept as `EntityReferenceNode`s and reported by
`glexml/dtd.validate`.
- `parse` takes a `String` (already-decoded text). For raw bytes,
`parse_bytes` detects the encoding the way the XML specification
describes and decodes UTF-8 and UTF-16 itself — everything an EPUB may
legally contain. UTF-32, EBCDIC, and legacy 8-bit encodings are
recognised but rejected; decode those yourself first.
- Namespaces are not resolved to URIs; names keep their prefixes as written.
- Serialisation does not re-render a DOCTYPE's internal subset, only its
name and external identifier.
Further documentation can be found at <https://hexdocs.pm/glexml>.
## Conformance
glexml is tested against the [W3C XML Conformance Test Suite]
(https://www.w3.org/XML/Test/) — the aggregated James Clark, Sun, IBM,
OASIS/NIST, Fuji Xerox, and University of Edinburgh suites. The harness
plays the role the library deliberately refuses: it loads external DTD
subsets and external entity files from disk and supplies them through
`parse_dtd_with` / `parse_bytes_with_dtd`, so tests requiring external
entities run too. glexml passes **100%** of the applicable XML 1.0
fifth-edition tests (1895 of 1895):
- **not well-formed** documents rejected: 986/986
- **valid** documents parsed and matching the canonical-form output
(including DTD attribute defaulting and normalisation): 699/699
- **invalid** documents caught by `glexml/dtd.validate` and
`glexml/dtd.standalone_violations`: 210/210
DTD subsets are parsed over a stack of entity segments rather than by
splicing replacement text, so entity boundaries stay visible: the Proper
Declaration/Group/Conditional Section PE Nesting validity constraints are
checked (`Dtd.pe_nesting_violations`), and external entity declarations
record which parameter entity's text declared them
(`ExternalEntity.declared_in`), the base for resolving their system
identifiers.
Run it yourself:
```sh
./scripts/fetch-xmlconf.sh # download the suite into ./xmlconf/ (not committed)
gleam dev # run and print the conformance report
```
## Development
```sh
gleam test # Run the tests on Erlang
gleam test --target javascript # Run the tests on Node.js
gleam dev # Run the W3C conformance suite (fetch first)
gleam format src test dev # Format the code
```