glexml

Package Version Hex Docs

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.

gleam add glexml

Quick start

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:

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

CSS selectors

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:

let assert Ok(document) =
  glexml.parse("<!DOCTYPE p [<!ENTITY nbsp \"&#160;\">]><p>a&nbsp;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:

let assert Ok(xhtml) = glexml.parse_dtd(xhtml_dtd_text)
let assert Ok(document) = glexml.parse_with_dtd(chapter_source, xhtml)

And to validate:

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

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):

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:

./scripts/fetch-xmlconf.sh  # download the suite into ./xmlconf/ (not committed)
gleam dev                   # run and print the conformance report

Development

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
Search Document