XEXPR module – handling XML as scheme s-expressions

Writing and searching x-expressions

Scheme s-expressions are a convenient way of wrangling XML, known as x-expressions. The functions in this module will search the content of, and write out, such x-expressions. There are a couple of ways that x-expressions have been defined, in the Scheme world, in the past. I have followed Racket.

The following grammar describes expressions that create X-expressions:

xexpr = string
  | (list symbol (list (list symbol string) ...) xexpr ...)
  | (cons symbol (list xexpr ...))
  | symbol
  | valid-char?
  | cdata
  | misc

For example:

(xexpr-write/xml!
   '(p "Here is a " (a ((href "http://foo")) "link")
       " with " (br ((clear "right")))
       ". This" amp "that" 33))

produces

<p>Here is a <a href="http://foo">link</a> with <br clear="right" />. This&amp;that&#x21;</p>

Above, a ‘string’ is literal data, either as a "string" or #[ustring]. When converted to an XML stream, the characters of the data will be escaped as necessary.

A pair represents an element, optionally with attributes. Each attribute’s name is represented by a symbol, and its value is represented by a string.

A symbol represents an entity reference. For example, the symbol 'nbsp represents &nbsp;. These are serialised as such, unless :expand-entities? is #t, in which case a subset of entities are expanded into the nominated character. It is unspecified just what entities are included in this ‘subset’, but this list is reasonable.

A valid-char? represents a numeric entity. For example, #x20 represents &#x20;.

Right now, I don't implement cdata or misc.

There is not at present any support for parsing XML into xexprs.

Writing

The functions xexpr-write/md!, xexpr-write/python!, xexpr-write/sexp!, xexpr-write/xhtml!, xexpr-write/xml! will write out an xexpr in one or other format (the function xexpr-write/xhtml! is generally the most useful).

Examining

The function xexpr-path-search implements a basic path searcher.

Given an xexpr, functions xexpr-disassemble, xexpr-get-attribute and xexpr-text make it convenient to inspect the contents.

Functions

Index:

xexpr-disassemble

(xexpr-disassemble xe) : Given an xexpr representing an element, return its name, attributes and contents as multiple values. If there are no attributes in the xexpr, then we return a null list for that part of the result.

xexpr-get-attribute

(xexpr-get-attribute xe att) : Given an xexpr xe, return the attribute att (a symbol) if it's present, or #f if not.

xexpr-path-search

(xexpr-path-search search-path xexpr) : search for a path in an xexpr.

The search-path is a list of element names as symbols, predicates, or one final keyword.

Returns a list comprised of the lists within xexpr which match. A symbol matches an element name, a predicate is true if it returns true when applied to an xexpr, and a trailing keyword matches an attribute name.

Thus '(p em) would find all em elements contained within a p element, '(p class:) would find the class attribute of p elements (ie, XPath p/@class), and (list 'p (lambda (e) (xexpr-get-attribute e 'a1)) 'q) would match p elements which have an a1 attribute and contain a q element (ie, XPath p[@a1]/q).

xexpr-search-path?

(xexpr-search-path? x) : The search path specification for XEXPR-PATH-SEARCH.

A sequence of symbols followed by an optional keyword. The prefix of symbols specifies a path of tags from the leaves with an implicit any sequence to the root. The final, optional keyword specifies an attribute.

xexpr-text

(xexpr-text xe) -> string? : Given an xexpr, extract all and only the text content. Any entity references (ie, symbols) are deemed to be 'text' in this context, and expanded to the corresponding string.

xexpr-write/md!

(xexpr-write/md! xexprs) : Convert xexprs to Markdown. The argument XEXPRS is a list of xexpr?. Writes the result to the current output port. This is preliminary/best-efforts, rather than being at all thorough.

xexpr-write/python!

(xexpr-write/python! body) : Write the xexpr expression in a form which is readable by Python. Specifically, it's the format wrangled by https://pypi.org/project/listxml/

xexpr-write/sexp!

(xexpr-write/sexp! el :dumb?) : write an xexpr to the stdout as a sexpr (ie, this is just a very basic prettyprinter).

If the keyword argument :dumb? is #t, then this uses an alternative writer which is less pretty but more robust.

xexpr-write/xhtml!

(xexpr-write/xhtml! body :metadata) : Write an xexpr to the stdout as XHTML (ie, adding head and body elements). If METADATA is present, then it is taken to be a list of lists, '(("key" "value") ("key" "value") ...) (specifically, the result from parse-markdown/metadata), which is searched for an item with key "title".

xexpr-write/xml!

(xexpr-write/xml! el :expand-entities?) : write an xexpr to the stdout as XML.

If keyword :prettyprint? is present and #t, then prettyprint the XML output. At present, this is very basic, and consists only of adding newlines after end-tags. For the keyword :expand-entities?, see below.

An xexpr is defined as follows; this is copying the specification in Racket.

The following grammar describes expressions that create X-expressions:

xexpr = string
  | (list symbol (list (list symbol string) ...) xexpr ...)
  | (cons symbol (list xexpr ...))
  | symbol
  | valid-char?
  | cdata
  | misc

For example:

(xexpr-write/xml!
   '(p "Here is a " (a ((href "http://foo")) "link")
       " with " (br ((clear "right")))
       ". This" amp "that" 33))

produces

<p>Here is a <a href="http://foo">link</a> with <br clear="right" />. This&amp;that&#x21;</p>

Above, a string is literal data, either as a "string" or #"ustring". When converted to an XML stream, the characters of the data will be escaped as necessary.

A pair represents an element, optionally with attributes. Each attribute’s name is represented by a symbol, and its value is represented by a string.

A symbol represents an entity reference. For example, the symbol 'nbsp represents &nbsp;. These are serialised as such, unless :expand-entities? is #t, in which case a subset of entities are expanded into the nominated character. It is unspecified just what entities are included in this ‘subset’, but this list is reasonable.

A valid-char? represents a numeric entity. For example, #x20 represents &#x20;.

Right now, I don't implement cdata or misc.

Norman
2026 August 02