std/xml

Xml reads XML 1.0 with namespaces into a tree of XmlNode values and writes it back; it expands the entities and applies the attribute defaults of an internal subset, never fetches an external one, and an XmlError has a line and a column.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Die generierte Referenz von std/xml zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.

std/xml is XML 1.0 (fifth edition) with Namespaces in XML 1.0, written in TorbScript. Xml reads a document into a tree of XmlNode values and writes that tree back. XML is a document format, not a data model: an element has attributes, mixed content in order and a namespace, none of which fits a record, so there is no encode and decode here and no trait of its own, and the tree is the model. The reader checks well-formedness and the constraints of namespaces, never fetches or opens anything, and reads the internal subset of a document type declaration. The hand-written conformance cases of std/xml/tests cover each production of both Recommendations.

Import#

use Xml, XmlNode, XmlName, XmlAttribute, XmlDeclaration, XmlError from "std/xml"

The listing a bucket of an S3-compatible store answers is a document of this kind. The keys are read through the tree by the local names of the elements, whatever prefix the document uses for the namespace:

use Xml from "std/xml"

const listing = """
  <?xml version="1.0" encoding="UTF-8"?>
  <ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>photos</Name>
  <KeyCount>2</KeyCount>
  <Contents><Key>2026/cat.jpg</Key><Size>48213</Size></Contents>
  <Contents><Key>2026/dog.jpg</Key><Size>51877</Size></Contents>
  </ListBucketResult>
  """

match Xml().parse(listing) {
  Ok(tree) => {
    const result = tree.root() ?? tree
    print "{result.child("Name")?.text() ?? "?"}: {result.child("KeyCount")?.text() ?? "?"} keys"
    for entry in result.childrenNamed("Contents") {
      print "{entry.child("Key")?.text() ?? "?"} ({entry.child("Size")?.text() ?? "?"} bytes)"
    }
  }
  Fail(problem) => print "Rejected: {problem}"
}

root() is the root element of a document, child(local) the first child element with that local name and text() the text inside an element, with its CDATA sections and without its comments.

Declarations#

Xml#

type Xml {
  indent: Int = 0
  entityLimit: Int = 1000000
  fn parse(text: String): Result<XmlNode, XmlError>
  fn write(tree: XmlNode): Result<String, XmlError>
}

An Xml is the format with its options. parse reads one document into an XmlNode.Document, and write writes a tree as text; for every tree parse made, parse(write(tree)) is that tree. indent is 0 by default, which writes the tree as it is. Above 0 an element that holds no text puts each child on a line of its own, indent spaces deeper than the element; that adds white space, which a reader keeps as text, so the written tree is no longer equal to the one it came from. entityLimit is how many characters the entity references of one document may add to it, in all: the "billion laughs" document, nine levels of ten references each, is an XmlError that names entityLimit instead of filling the memory. The default is far beyond a document that is not an attack; raise it for a trusted one.

What the reader does. It reads the XML declaration, comments, processing instructions, CDATA sections, the five predefined entities, character references and the entities a document declares in its internal subset. Line breaks are read as \n, an attribute value is normalised as XML 1.0 section 3.3.3 says, and every element and attribute name is resolved against the namespaces in force: an element without a prefix is in the default namespace, an attribute without a prefix is in none. The encoding named in the declaration is kept in the tree and otherwise ignored, since the text it reads is already characters. A byte order mark in front of the document is skipped. XML 1.1 is not read, and elements nested more than 400 deep are refused instead of read until the stack is gone.

What the writer does. It escapes &, < and > in text and also ", tab and line feed in attribute values, and it declares a namespace the tree uses and does not declare, on the element that needs it. What XML cannot say is an XmlError with line 0 and no text is written: a name that is no name, a character XML does not allow, -- in a comment, ]]> in a CDATA section, ?> in a processing instruction, and a document without exactly one root element.

XmlNode#

type XmlNode with Show, Equals, Hash {
  case Document(declaration: XmlDeclaration?, children: List<Self>)
  case Doctype(name: String, publicId: String?, systemId: String?)
  case Element(name: XmlName, attributes: List<XmlAttribute>, children: List<Self>)
  case Text(value: String)
  case CData(value: String)
  case Comment(value: String)
  case Instruction(target: String, data: String)
  fn declaration(): XmlDeclaration?
  fn isElement(): Bool
  fn name(): XmlName?
  fn attributes(): List<XmlAttribute>
  fn children(): List<Self>
  fn elements(): List<Self>
  fn root(): Self?
  fn child(local: String): Self?
  fn childrenNamed(local: String): List<Self>
  fn attribute(local: String): String?
  fn text(): String
}

The tree. A document holds its optional declaration and its comments, instructions, document type declaration and root element in order. An element holds its attributes in document order and its children in order, text interleaved with elements as the document had it; Text and CData are separate nodes, since a writer can tell them apart, and text() joins them. The tree keeps no white space outside the root element and no entity references: &amp; is the text &. An entity that a document declares is expanded and does not appear in the tree either.

attributes(), children(), elements() and childrenNamed(local) are empty for a node that has none, child(local) and attribute(local) are None where nothing matches, and the lookups by local name ignore the namespace; compare the XmlName itself with isNamed where two namespaces use one local name.

use Xml, XmlNode from "std/xml"

const text = "<feed xmlns=\"urn:feed\" xmlns:m=\"urn:media\"><entry m:kind=\"photo\">First</entry></feed>"
match Xml().parse(text) {
  Ok(tree) => {
    const entry = tree.root()?.child("entry")
    print entry?.text()
    for attribute in entry?.attributes() ?? [] {
      print "{attribute.name.qualified()} in {attribute.name.namespace ?? "no namespace"} = {attribute.value}"
    }
  }
  Fail(problem) => print "Rejected: {problem}"
}

XmlName, XmlAttribute and XmlDeclaration#

type XmlName with Show, Equals, Hash {
  local: String
  prefix: String? = None
  namespace: String? = None
  fn qualified(): String
  fn isNamed(local: String, namespace: String? = None): Bool
  fn show(): String
}
type XmlAttribute with Show, Equals, Hash {
  name: XmlName
  value: String
}
type XmlDeclaration with Show, Equals, Hash {
  version: String = "1.0"
  encoding: String? = None
  standalone: Bool? = None
}

An XmlName keeps the local name, the prefix as it was written, and the namespace it stands for; isNamed(local, namespace) compares the local name and the namespace and not the prefix. A declaration of a namespace, xmlns="uri" or xmlns:p="uri", is an attribute like any other, in the namespace http://www.w3.org/2000/xmlns/, so that a tree written back declares what the document declared. The prefix xml is bound to http://www.w3.org/XML/1998/namespace without a declaration.

XmlError#

type XmlError with Show, Error {
  line: Int
  column: Int
  message: String
  fn show(): String
}

An XmlError has the line and the column of the problem, counted from 1 and in characters, and a message. It is raised for every syntax error and every broken constraint of namespaces:

line 3, column 1: the end tag `</a>` does not match the start tag `<b>`
line 1, column 10: the attribute `x` is given twice
line 1, column 2: the prefix `p` of `p:a` is not declared
line 1, column 6: a bare `&` is not allowed in text: write `&amp;`

A mismatched end tag is named where the end tag stands, an element that is not closed where its start tag stands, and a second root element where it starts. A problem with a tree that is written has no place: its line is 0 and show() is the message alone.

The document type declaration#

A document type declaration without an internal subset is accepted: <!DOCTYPE html>, <!DOCTYPE a SYSTEM "a.dtd"> and <!DOCTYPE a PUBLIC "-//X//Y//EN" "a.dtd"> read into an XmlNode.Doctype that keeps the name and both identifiers, and the writer writes it back. Nothing the identifiers name is ever fetched or opened: the reader has no way to read a file or a URL, and an external subset is not read, so a SYSTEM identifier that points at file:///etc/passwd is a string in the tree.

An internal subset, the [...] after the name, is read and checked. The tree keeps nothing of it: the Doctype node holds the name and the identifiers, and the writer writes the doctype without a subset, so parse(write(tree)) is the tree, with the entities already expanded in it.

  • General entities. <!ENTITY e "text"> is expanded where &e; stands, in content and in an attribute value. The replacement text is parsed as content and has to be well-balanced, so <b> without </b> in it is an error. A character reference in the value is resolved at the declaration, so &#38;#60; is the text &#60; that stands for < after the expansion. The first declaration of a name wins, and the five predefined entities may be declared again, which changes nothing: the predefined meaning stays.
  • Parameter entities. <!ENTITY % p "..."> and %p; between declarations are expanded. A reference inside a declaration is an error (WFC: PEs in Internal Subset) and so is a conditional section, which belongs to an external subset.
  • External and unparsed entities. SYSTEM, PUBLIC and NDATA declarations are recorded and an error where the entity is referenced (WFC: No External Entity References, WFC: Parsed Entity). A document that declares <!ENTITY xxe SYSTEM "file:///etc/passwd"> and never uses it is read, and one that uses it is refused, and nothing is opened either way.
  • Element, notation, comment and processing instruction declarations are checked and skipped.
  • Attribute lists. <!ATTLIST e a CDATA "v"> gives every element e that lacks a the attribute a with the value v, after the attributes it was written with, so the tree has it as if it had been written; #REQUIRED and #IMPLIED add nothing, and #FIXED adds its value. The first declaration of an attribute of an element wins. The default is processed where it is used, like a written value: references are resolved, entities expanded, white space normalised. An error in it is reported at the start tag of the element, naming the attribute, and a default that is never used is never read. A value of a type other than CDATA, written or defaulted, loses the spaces at either end and has each run of spaces inside one space. A defaulted xmlns or xmlns:p takes part in the namespaces of the element, and a default that breaks the rules of namespaces is an error. A default counts against entityLimit. The declarations of an external subset are not read, so they apply nothing.
  • Attribute values from an entity are normalised as XML 1.0 section 3.3.3 says, also the white space a character reference made in the declaration, and a < that an entity brings in is an error.
  • Recursion is the error WFC: No Recursion, found where the entity is used: an entity that contains itself or another that contains it again. A recursive entity that no reference reaches is not an error.
  • The limit. The replacement text of each reference counts against entityLimit, and so does each parameter entity. A document past it is an XmlError at the reference that crossed it, which names the option. Entities nested more than 400 deep are refused as well.
  • Places. An error inside an expansion has the place of the reference in the document, and its message starts with the entity it happened in: in the entity e: the element b is not closed.

An entity that the five predefined ones and the internal subset do not cover is an error (WFC: Entity Declared). When the document type declaration names an external subset, which this reader does not read, the message says that the entity may be declared there.

Esc