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&that!</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 . 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  .
Right now, I don't implement cdata or misc.
There is not at present any support for parsing XML into xexprs.
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).
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.
Index:
(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 xe att) : Given an xexpr xe, return the attribute att (a symbol) if it's present, or #f if not.
(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? 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 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! 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! 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! 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! 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! 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&that!</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 . 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  .
Right now, I don't implement cdata or misc.