Many Outputs, Functions and Packages
Writing many files
A transformation has one principal result, the one that goes to
standard output, and can make any number of result documents with
xsl:result-document. This stylesheet makes a page for each top-level
section of the draft and an index that links to them:
pages.xsl:
<xsl:stylesheet version="4.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<ul>
<xsl:for-each select="rfc/middle/section">
<xsl:variable name="page" select="'sections/' || @anchor || '.html'"/>
<!-- One page for each top-level section... -->
<xsl:result-document href="{$page}">
<h1>{name}</h1>
<p>{count(.//t[@anchor])} requirements, {count(section)} subsections</p>
</xsl:result-document>
<!-- ...and a link to it in the index. -->
<li><a href="{$page}">{name}</a></li>
</xsl:for-each>
</ul>
</xsl:template>
</xsl:stylesheet>
sxml writes files only where you allow it. Without
--result-directory, it writes the principal result and tells you
about each result document it did not write:
$ sxml transform pages.xsl vane.xml
sxml transform: result document 'sections/intro.html' not written; give --result-directory
sxml transform: result document 'sections/states.html' not written; give --result-directory
sxml transform: result document 'sections/messages.html' not written; give --result-directory
sxml transform: result document 'sections/pause.html' not written; give --result-directory
sxml transform: result document 'sections/security.html' not written; give --result-directory
sxml transform: result document 'sections/iana.html' not written; give --result-directory
<ul>
<li><a href="sections/intro.html">Introduction</a></li>
<li><a href="sections/states.html">Flag States</a></li>
<li><a href="sections/messages.html">Messages</a></li>
<li><a href="sections/pause.html">Pause</a></li>
<li><a href="sections/security.html">Security Considerations</a></li>
<li><a href="sections/iana.html">IANA Considerations</a></li>
</ul>
With --result-directory, the result documents are written below that
directory, and --output puts the principal result into a file too:
$ mkdir site
$ sxml transform --result-directory=site --output=site/index.html pages.xsl vane.xml
$ find site -type f | sort
site/index.html
site/sections/iana.html
site/sections/intro.html
site/sections/messages.html
site/sections/pause.html
site/sections/security.html
site/sections/states.html
$ cat site/sections/messages.html
<h1>Messages</h1>
<p>5 requirements, 2 subsections</p>
All the files are written only when the whole transformation succeeds,
and a result document that would land outside the directory is an
error. Run the same command again, and sxml refuses to replace the
files without --force:
$ sxml transform --result-directory=site --output=site/index.html pages.xsl vane.xml; echo $?
sxml transform: 'site/index.html' exists; give --force to replace it
3
Functions and packages
The HTML page and the plain text of Templates and
Modes need the same things: the number of a section
or a requirement, and the text of an xref. Written once, they belong
in functions that every stylesheet of the draft can call.
xsl:function declares a function that XPath expressions in the
stylesheet can call, with typed parameters and result. Its name must
have a namespace prefix of your own; here spec, for
urn:example:spec.
A package is a stylesheet module with a name and a version, whose components (functions, templates, variables, keys, modes) are private unless it says otherwise. Other stylesheets use a package and see only what it makes public. Packages let a team share a library of functions without the risk of names clashing or of rules leaking into each other. This one holds the draft's functions:
spec.xsl:
<xsl:package name="spec.xsl" package-version="1.0" version="4.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:spec="urn:example:spec">
<!-- Private: every element with an anchor, found by its anchor. -->
<xsl:key name="anchor" match="*[@anchor]" use="@anchor"/>
<!-- Private: the strength of each BCP 14 key word. -->
<xsl:variable name="spec:strengths" as="map(xs:string, xs:string)" select="{
'MUST': 'absolute', 'MUST NOT': 'absolute', 'REQUIRED': 'absolute',
'SHALL': 'absolute', 'SHALL NOT': 'absolute',
'SHOULD': 'recommended', 'SHOULD NOT': 'recommended',
'RECOMMENDED': 'recommended', 'NOT RECOMMENDED': 'recommended',
'MAY': 'optional', 'OPTIONAL': 'optional' }"/>
<xsl:function name="spec:strength" as="xs:string" visibility="public">
<xsl:param name="keyword" as="element(bcp14)"/>
<xsl:sequence select="$spec:strengths(normalize-space($keyword))"/>
</xsl:function>
<!-- "3.1" for a section, "R6" for a requirement: numbers by position. -->
<xsl:function name="spec:number" as="xs:string" visibility="public">
<xsl:param name="node" as="element()"/>
<!-- One text node, whose string is the result. -->
<xsl:value-of>
<xsl:choose>
<xsl:when test="$node/self::section">
<xsl:number select="$node" level="multiple" count="section" format="1.1"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>R</xsl:text>
<xsl:number select="$node" level="any" count="t[@anchor]"/>
</xsl:otherwise>
</xsl:choose>
</xsl:value-of>
</xsl:function>
<!-- The text of an xref: its own, or one made from what it points to. -->
<xsl:function name="spec:xref-text" as="xs:string" visibility="public">
<xsl:param name="xref" as="element(xref)"/>
<xsl:variable name="target" select="key('anchor', $xref/@target, root($xref))"/>
<xsl:sequence select="
if ($xref/node()) then string($xref)
else if ($target/self::section) then 'Section ' || spec:number($target)
else if ($target/self::t) then 'Requirement ' || spec:number($target)
else '[' || $xref/@target || ']'"/>
</xsl:function>
</xsl:package>
The three functions are public. The key anchor and the variable
spec:strengths are private: only the package's own functions can see
them. spec:number puts its xsl:choose inside xsl:value-of, which
makes one text node of what xsl:text and xsl:number write, and the
function's as="xs:string" turns that node into its string. Without
the xsl:value-of, a requirement's number would be two text nodes, "R"
and "6", and two strings are not the one string the function promises
(XTTE0780).
A stylesheet uses the package with xsl:use-package. It can call the
public functions, and nothing else:
private.xsl:
<xsl:stylesheet version="4.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:spec="urn:example:spec">
<xsl:use-package name="spec.xsl" package-version="1.0"/>
<xsl:output method="text"/>
<xsl:template match="/">{$spec:strengths?MUST}</xsl:template>
</xsl:stylesheet>
$ sxml transform private.xsl vane.xml
private.xsl:7:29: err:XPST0008: the variable is not declared
The sxml command finds a package by reading its name as a file name,
relative to the stylesheet. A program that embeds sxml can find
packages any way it likes: by name and version, in a database, or built
in (Embedding sxml in C).
Importing stylesheets
Some template rules are shared too: an xref becomes the text that
spec:xref-text makes, and a cref becomes nothing, in every output.
They go in a module of their own, with a function that gives the text
of a paragraph as a reader sees it:
common.xsl:
<xsl:stylesheet version="4.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:spec="urn:example:spec"
exclude-result-prefixes="xs spec">
<!-- What the HTML, the text and the JSON output share. Each stylesheet
that imports this module uses the package spec.xsl. -->
<xsl:template match="xref">{spec:xref-text(.)}</xsl:template>
<xsl:template match="cref"/>
<!-- The text of a paragraph as a reader sees it. -->
<xsl:function name="spec:text" as="xs:string">
<xsl:param name="paragraph" as="element()"/>
<xsl:variable name="text">
<xsl:apply-templates select="$paragraph/node()"/>
</xsl:variable>
<xsl:sequence select="normalize-space($text)"/>
</xsl:function>
</xsl:stylesheet>
xsl:import brings in the rules of another stylesheet, with a lower
import precedence: a rule in the importing stylesheet beats any rule
it imports, whatever their priorities. That is how you extend a
stylesheet without changing it. The HTML stylesheet imports
common.xsl, uses the package, and replaces the xref rule with one
that puts a link around the imported rule's text, which
xsl:apply-imports calls:
html.xsl:
<xsl:stylesheet version="4.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:spec="urn:example:spec"
exclude-result-prefixes="spec">
<!-- The shared rules, with a lower import precedence than these. -->
<xsl:import href="common.xsl"/>
<xsl:use-package name="spec.xsl" package-version="1.0"/>
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<html>
<head>
<title>{rfc/front/title}</title>
</head>
<body>
<h1>{rfc/front/title}</h1>
<p class="draft">{rfc/@docName}</p>
<nav>
<ol class="toc">
<xsl:apply-templates select="rfc/middle/section" mode="toc"/>
</ol>
</nav>
<xsl:apply-templates select="rfc/middle/section, rfc/back/references"/>
<section id="requirements">
<h2>Index of Requirements</h2>
<table>
<xsl:apply-templates select="//t[@anchor]" mode="index"/>
</table>
</section>
</body>
</html>
</xsl:template>
<xsl:template match="section">
<section id="{@anchor}">
<h2>{spec:number(.)}. {name}</h2>
<xsl:apply-templates select="* except name"/>
</section>
</xsl:template>
<xsl:template match="t">
<p><xsl:apply-templates/></p>
</xsl:template>
<xsl:template match="t[@anchor]">
<p id="{@anchor}" class="requirement">
<span class="number">{spec:number(.)}</span>
<xsl:text> </xsl:text>
<xsl:apply-templates/>
</p>
</xsl:template>
<!-- A link around the text that common.xsl makes. -->
<xsl:template match="xref">
<a href="#{@target}"><xsl:apply-imports/></a>
</xsl:template>
<xsl:template match="bcp14">
<span class="bcp14 {spec:strength(.)}">{.}</span>
</xsl:template>
<xsl:template match="dl | dd">
<xsl:copy><xsl:apply-templates/></xsl:copy>
</xsl:template>
<xsl:template match="dt">
<dt id="{@anchor}"><xsl:apply-templates/></dt>
</xsl:template>
<xsl:template match="sourcecode">
<pre>{.}</pre>
</xsl:template>
<xsl:template match="references">
<section id="{@anchor}">
<h2>{name}</h2>
<xsl:apply-templates select="references"/>
<xsl:if test="reference">
<dl>
<xsl:apply-templates select="reference"/>
</dl>
</xsl:if>
</section>
</xsl:template>
<xsl:template match="reference">
<dt id="{@anchor}">[{@anchor}]</dt>
<dd>{string-join(front/author/@fullname, ', ')}, "{front/title}",
{front/date/@month} {front/date/@year}.</dd>
</xsl:template>
<xsl:template match="section" mode="toc">
<li>
<a href="#{@anchor}">{spec:number(.)}. {name}</a>
<xsl:if test="section">
<ol>
<xsl:apply-templates select="section" mode="toc"/>
</ol>
</xsl:if>
</li>
</xsl:template>
<xsl:template match="t" mode="index">
<tr>
<td><a href="#{@anchor}">{spec:number(.)}</a></td>
<td>{bcp14}</td>
<td>{ancestor::section[1] ! (spec:number(.) || '. ' || name)}</td>
</tr>
</xsl:template>
</xsl:stylesheet>
$ sxml transform html.xsl vane.xml | grep -B1 -A1 'Section 4'
while it is in a <a href="#term-pause">pause</a>
(<a href="#pause">Section 4</a>).</p>
<p id="req-9" class="requirement"><span class="number">R6</span> A service <span class="bcp14 optional">MAY</span> ask an owner for a
An xsl:use-package must be in the principal stylesheet module, not
in a module it imports (XTSE3008), so each stylesheet that imports
common.xsl uses the package itself. xsl:include is the other way to
combine modules: the included rules count as if they were written in
the including stylesheet, with the same precedence.
The plain-text stylesheet imports the same module and keeps its
xref rule. Its rules are those of the text mode of draft.xsl,
now in the default mode of a stylesheet of their own, with an RFC's
first line and the references at the end:
text.xsl:
<xsl:stylesheet version="4.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:spec="urn:example:spec"
exclude-result-prefixes="xs spec">
<xsl:import href="common.xsl"/>
<xsl:use-package name="spec.xsl" package-version="1.0"/>
<xsl:output method="text"/>
<xsl:template match="/">
<!-- The first line: the series on the left, the date on the right. -->
<xsl:variable name="date" select="rfc/front/date ! (@month || ' ' || @year)"/>
<xsl:text>Internet-Draft{spec:spaces(72 - 14 - string-length($date))}{$date} </xsl:text>
<xsl:apply-templates select="rfc/front/title, rfc/middle/section, rfc/back/references"/>
</xsl:template>
<xsl:template match="title">
<xsl:text> {.} </xsl:text>
</xsl:template>
<xsl:template match="section">
<xsl:text>{spec:number(.)}. {name} </xsl:text>
<xsl:apply-templates select="* except name"/>
</xsl:template>
<xsl:template match="references">
<xsl:text>{name} </xsl:text>
<xsl:apply-templates select="* except name"/>
</xsl:template>
<xsl:template match="t | dd">
<xsl:call-template name="paragraph">
<xsl:with-param name="indent" select="if (self::dd) then 6 else 3"/>
<xsl:with-param name="text">
<xsl:apply-templates/>
</xsl:with-param>
</xsl:call-template>
</xsl:template>
<xsl:template match="reference">
<xsl:text> [{@anchor}] </xsl:text>
<xsl:call-template name="paragraph">
<xsl:with-param name="indent" select="6"/>
<xsl:with-param name="text">{string-join(front/author/@fullname, ', ')}, "{front/title}", {front/date/@month} {front/date/@year}, <{@target}>.</xsl:with-param>
</xsl:call-template>
</xsl:template>
<!-- Lines of at most 72 characters, indented. -->
<xsl:template name="paragraph">
<xsl:param name="indent" as="xs:integer"/>
<xsl:param name="text" as="xs:string"/>
<xsl:variable name="spaces" select="spec:spaces($indent)"/>
<xsl:value-of select="replace(normalize-space($text),
'(.{1,' || 72 - $indent || '})( |$)',
$spaces || '$1 ')"/>
<xsl:text> </xsl:text>
</xsl:template>
<xsl:function name="spec:spaces" as="xs:string">
<xsl:param name="count" as="xs:integer"/>
<xsl:sequence select="string-join(replicate(' ', $count))"/>
</xsl:function>
<xsl:template match="dl">
<xsl:apply-templates select="*"/>
</xsl:template>
<xsl:template match="dt">
<xsl:text> {.} </xsl:text>
</xsl:template>
<xsl:template match="sourcecode">
<xsl:value-of select="tokenize(., ' ')[normalize-space()] ! (' ' || . || ' ')"
separator=""/>
<xsl:text> </xsl:text>
</xsl:template>
</xsl:stylesheet>
$ sxml transform text.xsl vane.xml | head -3
Internet-Draft October 2026
The Vane Protocol for Feature Flag Rollouts
$ sxml transform text.xsl vane.xml | grep -A1 '(Section'
A service MUST NOT roll out a flag while it is in a pause (Section
4).
$ sxml transform text.xsl vane.xml | grep -A3 '^ \[RFC6238\]$'
[RFC6238]
D. M'Raihi, S. Machani, M. Pei, J. Rydell, "TOTP: Time-Based
One-Time Password Algorithm", May 2011,
<https://www.rfc-editor.org/info/rfc6238>.
spec:spaces is a function of the stylesheet itself, not of the
package: a function that only one stylesheet needs stays there. It has
no visibility, and in a stylesheet that is not a package that makes no
difference.
Transformations from queries
fn:transform runs a stylesheet from XPath or XQuery and returns its
results in a map; ?output is the principal result. With the sxml
command, it reads the stylesheet as fn:doc reads files. A query can
check what a stylesheet makes, such as the line lengths of the
plain-text draft:
$ sxml xpath --context-item=vane.xml 'let $lines := transform({ "stylesheet-location": "text.xsl", "source-node": . })?output => tokenize("\n") return (count($lines), max($lines ! string-length()))'
122
72
The plain text has 122 lines, counting the empty ones, and none is longer than 72 characters.
Large inputs are a subject of their own: a stylesheet whose mode is streamable reads its input once, without building a tree of it. Streaming Large Trace Files shows how, on files of traces.