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

Keys, Numbers and Grouping

Keys

An xref without text, such as <xref target="pause"/>, asks the renderer for the text: "Section 4" when it points to a section, "[RFC5234]" when it points to a reference. To write it, a stylesheet must find the element whose anchor is the target. A key is an index that the processor builds for that. You declare what to index and by which value, and the key function looks nodes up:

xrefs.xsl:

<xsl:stylesheet version="4.0" expand-text="yes"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>

  <!-- Every element with an anchor, found by its anchor. -->
  <xsl:key name="anchor" match="*[@anchor]" use="@anchor"/>

  <xsl:template match="/">
    <xsl:apply-templates select="//xref"/>
  </xsl:template>

  <xsl:template match="xref">
    <xsl:text>{@target} -> </xsl:text>
    <xsl:apply-templates select="key('anchor', @target)" mode="xref-text"/>
    <xsl:text>&#10;</xsl:text>
  </xsl:template>

  <!-- The text of an xref depends on what it points to. -->
  <xsl:template match="section" mode="xref-text">
    <xsl:text>Section </xsl:text>
    <xsl:number level="multiple" count="section" format="1.1"/>
  </xsl:template>

  <xsl:template match="reference" mode="xref-text">[{@anchor}]</xsl:template>

  <xsl:template match="dt" mode="xref-text">the term "{.}"</xsl:template>
</xsl:stylesheet>
$ sxml transform xrefs.xsl vane.xml
term-sdk -> the term "SDK"
RFC2119 -> [RFC2119]
RFC8174 -> [RFC8174]
term-flag -> the term "Flag"
term-flag -> the term "Flag"
RFC5234 -> [RFC5234]
term-percentage -> the term "Percentage"
term-pause -> the term "Pause"
pause -> Section 4
RFC6238 -> [RFC6238]
term-pause -> the term "Pause"

key('anchor', @target) finds the elements whose @anchor equals the xref's target, in the document of the context node; a third argument names another document. A predicate such as //*[@anchor = current()/@target] would give the same result, but the key is built once and each lookup is fast, however large the draft is. The rules of the mode xref-text then decide the text by the kind of element the key found: one rule for a section, one for a reference, one for a term.

Numbers

The draft has no section numbers in it, and it should not: when the authors insert a section, every number after it changes, and every "Section 4" in the text with it. The renderer numbers by position, and xsl:number is the instruction that does it:

numbers.xsl:

<xsl:stylesheet version="4.0" expand-text="yes"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:apply-templates select="//middle//(section | t[@anchor])"/>
  </xsl:template>

  <!-- 1, 1.1, 2, ...: the position among the sections at each level. -->
  <xsl:template match="section">
    <xsl:number level="multiple" count="section" format="1.1"/>
    <xsl:text>  {name}&#10;</xsl:text>
  </xsl:template>

  <!-- R1, R2, ...: the position among all requirements of the draft. -->
  <xsl:template match="t">
    <xsl:text>    R</xsl:text>
    <xsl:number level="any" count="t[@anchor]" format="1"/>
    <xsl:text> {@anchor}: {bcp14}&#10;</xsl:text>
  </xsl:template>
</xsl:stylesheet>
$ sxml transform numbers.xsl vane.xml
1  Introduction
1.1  Terminology
2  Flag States
    R1 req-1: MUST
    R2 req-2: MUST
    R3 req-3: SHOULD
3  Messages
3.1  Rollout
    R4 req-4: MUST
    R5 req-5: MUST NOT
    R6 req-9: MAY
3.2  Status
    R7 req-6: MUST
    R8 req-10: SHOULD NOT
4  Pause
    R9 req-7: MUST
    R10 req-8: SHOULD
    R11 req-11: MAY
5  Security Considerations
6  IANA Considerations

level="multiple" counts the node and each of its ancestors that match count among their siblings, and format="1.1" writes the numbers with dots between them. level="any" counts every matching node before it in the document, at any depth: that numbers the requirements from R1 to R11 in reading order, while their anchors keep the order in which they were written (req-9 was added in the second draft and is the sixth requirement). Other formats give a, A, i, I or 01, as in format="a)" or format="i.". A format token that xsl:number does not know falls back to decimal digits, so R1 as a format would write 1; the R is written as text before the number.

Grouping

xsl:for-each-group puts the selected items into groups and processes each group once. Inside it, current-grouping-key() is the key of the group, and current-group() its items.

group-by groups by a computed value. An index of the draft's terms is grouped by first letter, and lists the sections that use each term, found with a second key:

term-index.xsl:

<xsl:stylesheet version="4.0" expand-text="yes"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>

  <!-- The xrefs, found by the anchor they point to. -->
  <xsl:key name="uses" match="xref" use="@target"/>

  <xsl:template match="/">
    <xsl:for-each-group select="//dt" group-by="substring(., 1, 1)">
      <xsl:sort select="current-grouping-key()"/>
      <xsl:text>{current-grouping-key()}&#10;</xsl:text>
      <xsl:for-each select="current-group()">
        <xsl:sort select="."/>
        <xsl:text>  {.}: </xsl:text>
        <!-- The sections that use the term, each once. -->
        <xsl:for-each select="key('uses', @anchor)/ancestor::section[1]">
          <xsl:if test="position() > 1">, </xsl:if>
          <xsl:number level="multiple" count="section" format="1.1"/>
        </xsl:for-each>
        <xsl:text>&#10;</xsl:text>
      </xsl:for-each>
    </xsl:for-each-group>
  </xsl:template>
</xsl:stylesheet>
$ sxml transform term-index.xsl vane.xml
F
  Flag: 1.1, 2
P
  Pause: 3.1, 4
  Percentage: 3.1
S
  SDK: 1

key('uses', @anchor) gives every xref to the term, and /ancestor::section[1] the section around each one. A path removes duplicates and returns nodes in document order, so a section that uses a term twice would still be listed once, and the sections come in the order of the draft.

The references are grouped by the references element they are in, normative or informative:

references.xsl:

<xsl:stylesheet version="4.0" expand-text="yes"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:for-each-group select="//reference" group-by="../name">
      <xsl:text>{current-grouping-key()}&#10;</xsl:text>
      <xsl:for-each select="current-group()">
        <!-- The newest first; references of the same year by anchor. -->
        <xsl:sort select="front/date/@year" data-type="number" order="descending"/>
        <xsl:sort select="@anchor"/>
        <xsl:text>  [{@anchor}] {front/date/@year} {front/title}&#10;</xsl:text>
      </xsl:for-each>
    </xsl:for-each-group>
  </xsl:template>
</xsl:stylesheet>
$ sxml transform references.xsl vane.xml
Normative References
  [RFC8174] 2017 Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words
  [RFC5234] 2008 Augmented BNF for Syntax Specifications: ABNF
  [RFC2119] 1997 Key words for use in RFCs to Indicate Requirement Levels
Informative References
  [RFC6238] 2011 TOTP: Time-Based One-Time Password Algorithm

Grouping by position

Grouping is not only for values. Documents often have structure that is only implied by the order of elements. The terms of the draft are a dl, a flat list in which each dt is followed by its dd. group-starting-with starts a new group at each item that matches a pattern, which turns the flat list into entries:

glossary.xsl:

<xsl:stylesheet version="4.0" expand-text="yes"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>

  <!-- A dl is a flat list: dt, dd, dt, dd. Each dt starts an entry. -->
  <xsl:template match="/">
    <glossary>
      <xsl:for-each-group select="//dl/*" group-starting-with="dt">
        <entry term="{current-group()[1]}">
          <xsl:value-of select="normalize-space(current-group()[2])"/>
        </entry>
      </xsl:for-each-group>
    </glossary>
  </xsl:template>
</xsl:stylesheet>
$ sxml transform glossary.xsl vane.xml
<?xml version="1.0" encoding="UTF-8"?>
<glossary>
  <entry term="Flag">A named switch in an application that turns a feature on for a share of its users.</entry>
  <entry term="SDK">The library in an application that evaluates flags from its cache and keeps the cache current with Vane messages.</entry>
  <entry term="Percentage">A whole number from 0 to 100 that an owner gives to roll a flag out to that share of users. An SDK puts a user in the share by a hash of the flag and the user, so the user stays in it while the percentage grows.</entry>
  <entry term="Pause">A state in which a flag refuses every rollout for a time.</entry>
</glossary>

There are two more ways to form groups. group-adjacent puts neighbouring items with the same key together, such as runs of paragraphs of the same kind, and group-ending-with ends a group at each item that matches a pattern.

Sorting

xsl:sort sorts the items of xsl:apply-templates, xsl:for-each and xsl:for-each-group. In term-index.xsl it sorts the groups by their key and the terms in each group by name. In references.xsl, two sorts make two sort keys: the year, newest first, as a number (data-type="number", order="descending"), and the anchor for references of the same year.

Strings sort by their Unicode code points unless a sort names a collation, with collation or lang: lang="de" sorts by the rules of German. Working with Text sorts the authors of the references, M'Raihi among them, and the anchors req-1 to req-11 in the order people expect.