Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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}&#10;</xsl:text>
    <xsl:apply-templates select="rfc/front/title, rfc/middle/section, rfc/back/references"/>
  </xsl:template>

  <xsl:template match="title">
    <xsl:text>&#10;{.}&#10;&#10;</xsl:text>
  </xsl:template>

  <xsl:template match="section">
    <xsl:text>{spec:number(.)}.  {name}&#10;&#10;</xsl:text>
    <xsl:apply-templates select="* except name"/>
  </xsl:template>

  <xsl:template match="references">
    <xsl:text>{name}&#10;&#10;</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}]&#10;</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}, &lt;{@target}&gt;.</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&#10;')"/>
    <xsl:text>&#10;</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>   {.}&#10;</xsl:text>
  </xsl:template>

  <xsl:template match="sourcecode">
    <xsl:value-of select="tokenize(., '&#10;')[normalize-space()] ! ('     ' || . || '&#10;')"
                  separator=""/>
    <xsl:text>&#10;</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.