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

Documenting the Machine in XHTML

The machine's annotations were written for people as much as for checks: each state and each event has a paragraph of XHTML, each data item names its type, and each payload names an element of vane-data.xsd. A stylesheet can put all of it on one page, together with what only the schema knows, such as what a flag key may look like. The page is XHTML, so the paragraphs of the machine can go into it as they are.

The stylesheet

document.xsl:

<!-- An XHTML page about a Vane machine, from its annotations and from
     the schema of its data, vane-data.xsd. -->
<xsl:stylesheet version="4.0" expand-text="yes"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:html="http://www.w3.org/1999/xhtml"
    xmlns:sc="http://www.w3.org/2005/07/scxml"
    xmlns:vane="urn:example:vane"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:doc="urn:example:document"
    exclude-result-prefixes="html sc vane xs doc">
  <xsl:output method="xhtml" html-version="5" indent="yes"/>

  <!-- Where the published draft is, for links to its requirements. -->
  <xsl:param name="draft" select="'vane.html'"/>

  <xsl:variable name="schema" select="doc('vane-data.xsd')/xs:schema"/>

  <xsl:template match="/sc:scxml">
    <html>
      <head>
        <title>The {@name} machine</title>
      </head>
      <body>
        <h1>The {@name} machine</h1>
        <p>It starts in the state <a href="#{@initial}">{@initial}</a>.
          <xsl:apply-templates select="@vane:implements"/></p>
        <h2>States</h2>
        <xsl:apply-templates select="sc:state"/>
        <h2>Transitions</h2>
        <table>
          <tr><th>From</th><th>Event</th><th>Condition</th><th>To</th></tr>
          <xsl:apply-templates select="sc:state/sc:transition"/>
        </table>
        <h2>Events</h2>
        <dl>
          <xsl:apply-templates select="vane:events/vane:event"/>
        </dl>
        <h2>Data</h2>
        <table>
          <tr><th>Item</th><th>Initial value</th><th>Type</th></tr>
          <xsl:apply-templates select="sc:datamodel/sc:data"/>
        </table>
      </body>
    </html>
  </xsl:template>

  <!-- A state: its own XHTML, copied without the machine's namespaces. -->
  <xsl:template match="sc:state">
    <section id="{@id}">
      <h3>{@id}</h3>
      <xsl:copy-of select="html:div/node()" copy-namespaces="no"/>
      <xsl:apply-templates select="@vane:implements"/>
    </section>
  </xsl:template>

  <xsl:template match="sc:transition">
    <tr>
      <td><a href="#{../@id}">{../@id}</a></td>
      <td>{@event}</td>
      <td><xsl:if test="@cond"><code>{@cond}</code></xsl:if></td>
      <td>
        <xsl:choose>
          <xsl:when test="@target"><a href="#{@target}">{@target}</a></xsl:when>
          <xsl:otherwise>(stays)</xsl:otherwise>
        </xsl:choose>
      </td>
    </tr>
  </xsl:template>

  <!-- An event: its XHTML description, and what its payload carries. -->
  <xsl:template match="vane:event">
    <dt id="event-{@name}">{@name}</dt>
    <dd>
      <xsl:copy-of select="html:*" copy-namespaces="no"/>
      <xsl:apply-templates select="@vane:payload"/>
    </dd>
  </xsl:template>

  <xsl:template match="@vane:payload">
    <xsl:variable name="name" select="local-name-from-QName(resolve-QName(., ..))"/>
    <p>It carries a <code>{.}</code>:</p>
    <ul>
      <xsl:for-each select="$schema/xs:element[@name = $name]//xs:element">
        <li><code>{@name}</code>{if (@minOccurs = '0') then ' (optional)' else ''},
          {doc:describe(@type)}</li>
      </xsl:for-each>
    </ul>
  </xsl:template>

  <xsl:template match="sc:data">
    <tr>
      <td>{@id}</td>
      <td>{@expr}</td>
      <td><code>{@vane:type}</code>: {doc:describe(@vane:type)}</td>
    </tr>
  </xsl:template>

  <!-- Links to the requirements of the draft. -->
  <xsl:template match="@vane:implements">
    <span class="implements">Implements
      <xsl:for-each select="tokenize(.)">
        <a href="{$draft}#{.}">{.}</a>
        <xsl:if test="position() ne last()">, </xsl:if>
      </xsl:for-each>.</span>
  </xsl:template>

  <!-- A type of vane-data.xsd in words: its documentation if it has
       one, else its base type and facets. -->
  <xsl:function name="doc:describe" as="xs:string">
    <xsl:param name="type" as="attribute()"/>
    <xsl:variable name="name" select="resolve-QName($type, $type/..)"/>
    <xsl:variable name="definition" select="
      $schema/xs:simpleType[@name = local-name-from-QName($name)]
        [namespace-uri-from-QName($name) eq string($schema/@targetNamespace)]"/>
    <xsl:choose>
      <xsl:when test="empty($definition)">{
        if (namespace-uri-from-QName($name) eq 'http://www.w3.org/2001/XMLSchema')
        then 'xs:' || local-name-from-QName($name) else 'an unknown type'}</xsl:when>
      <xsl:when test="$definition/xs:annotation/xs:documentation">{
        normalize-space($definition/xs:annotation/xs:documentation)}</xsl:when>
      <xsl:otherwise>
        <xsl:variable name="restriction" select="$definition/xs:restriction"/>
        <xsl:sequence select="string-join((
          $restriction/@base,
          $restriction/xs:pattern/@value ! `matching {.}`,
          $restriction/xs:minInclusive/@value ! `at least {.}`,
          $restriction/xs:maxInclusive/@value ! `at most {.}`,
          if ($restriction/xs:enumeration)
          then 'one of ' || string-join($restriction/xs:enumeration/@value, ', ')
          else ()
        ), ', ')"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:function>
</xsl:stylesheet>

It reads two documents: the machine, its source, and the schema of the annotations, with doc('vane-data.xsd'). A schema is an XML document like any other, so a stylesheet can read the declarations in it, just as check.xq read the names of the payloads.

The XHTML namespace is the default namespace of the stylesheet, so the literal result elements, html, section, table, are XHTML elements. The same namespace is bound to the prefix html too, for the patterns and paths that select the machine's XHTML: in a path, an unprefixed name would mean no namespace, not the default namespace of literal results. exclude-result-prefixes keeps the namespaces the page doesn't use out of it.

xsl:output method="xhtml" writes the page as XHTML: an XML document that browsers can also read as HTML, with the <!DOCTYPE html> of html-version="5" and a meta element for the encoding.

Writing the page

The machine is the one Checking What the Schema Cannot fixed, flag-fixed.scxml:

$ sxml transform --output=flag.html document.xsl flag-fixed.scxml
$ sed -n '1,12p' flag.html
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta charset="UTF-8"/>
    <title>The feature_flag machine</title>
  </head>
  <body>
    <h1>The feature_flag machine</h1>
    <p>It starts in the state <a href="#stable">stable</a>.
          <span class="implements">Implements
      <a href="vane.html#req-1">req-1</a>.</span></p>

The machine's own XHTML

Each state's section has its heading, then the content of the state's div, copied with xsl:copy-of:

$ sed -n '/<section id="paused">/,/<\/section>/p' flag.html
    <section id="paused"><h3>paused</h3>
      <p>The service refuses every rollout for <var>pauseSeconds</var> seconds.</p>
    <span class="implements">Implements
      <a href="vane.html#req-5">req-5</a>, <a href="vane.html#req-8">req-8</a>.</span></section>

The p and the var inside it are copied as they are: they were XHTML in the machine, and they are XHTML on the page. A copied element takes along the namespace bindings in scope where it was, and in the machine the namespace urn:example:vane was in scope everywhere. copy-namespaces="no" leaves the bindings out that the copy doesn't use. Without it, each paragraph carries the vane namespace into the page:

$ sed 's|<xsl:copy-of select="html:div/node()" copy-namespaces="no"/>|<xsl:copy-of select="html:div/node()"/>|' document.xsl > keep.xsl
$ sxml transform keep.xsl flag-fixed.scxml | sed -n '/<section id="paused">/,/<\/section>/p'
    <section id="paused"><h3>paused</h3>
      <p xmlns:vane="urn:example:vane">The service refuses every rollout for <var>pauseSeconds</var> seconds.</p>
    <span class="implements">Implements
      <a href="vane.html#req-5">req-5</a>, <a href="vane.html#req-8">req-8</a>.</span></section>

It is not wrong, but it is noise in a page meant for browsers. The section of a state is written on fewer lines than the rest: an element with text of its own is mixed content, and the serializer doesn't indent inside it, because new whitespace there would change the text.

A table of transitions

The transitions are a table, one row for each, in document order, with links to the states' sections:

$ sed -n '/<h2>Transitions/,/<\/table>/p' flag.html | head -26
    <h2>Transitions</h2>
    <table>
      <tr>
        <th>From</th>
        <th>Event</th>
        <th>Condition</th>
        <th>To</th>
      </tr>
      <tr>
        <td><a href="#stable">stable</a></td>
        <td>flag.archived</td>
        <td></td>
        <td><a href="#archived">archived</a></td>
      </tr>
      <tr>
        <td><a href="#stable">stable</a></td>
        <td>rollout.request</td>
        <td></td>
        <td><a href="#checking">checking</a></td>
      </tr>
      <tr>
        <td><a href="#stable">stable</a></td>
        <td>status.request</td>
        <td></td>
        <td>(stays)</td>
      </tr>

Events and their payloads

Each event is a term of a definition list, with its XHTML paragraph. For an event with a payload, the stylesheet finds the payload's declaration in the schema and lists its children:

$ sed -n '/<dt id="event-status.response">/,/<\/dd>/p' flag.html
      <dt id="event-status.response">status.response</dt>
      <dd>
        <p>The service answers with the state.</p>
        <p>It carries a <code>vane:status</code>:</p>
        <ul>
          <li><code>flag</code>,
          The key of a flag: the flag-key rule of the protocol's ABNF.</li>
          <li><code>state</code>,
          xs:string, one of stable, rolling-out, paused</li>
          <li><code>failed-checks</code>,
          xs:nonNegativeInteger</li>
          <li><code>retry-after</code> (optional),
          A duration in whole seconds, at most one hour.</li>
        </ul>
      </dd>

@vane:payload is a QName in the machine, written with the machine's prefix. resolve-QName resolves it with the namespace bindings of its element, and the stylesheet compares only the local name with the schema's declarations; Checking What the Schema Cannot made sure that the payloads name elements of this schema.

The types are described by the function doc:describe: the type's xs:documentation when it has one, else its base type and its facets in words. vane:flag-key and vane:seconds are documented in vane-data.xsd, so the page shows the schema's own sentences for flag and retry-after; vane:state and vane:failure-count are not, so one of stable, rolling-out, paused is the enumeration in words, and xs:nonNegativeInteger the base type. Without its documentation, vane:flag-key would read xs:string, matching [A-Za-z0-9\-]{1,32}, and vane:seconds xs:positiveInteger, at most 3600. The data items are described the same way:

$ sed -n '/<h2>Data/,/<\/table>/p' flag.html | grep 'code>'
        <td><code>vane:failure-count</code>: xs:nonNegativeInteger</td>
        <td><code>vane:failure-limit</code>: xs:positiveInteger, at most 10</td>
        <td><code>vane:seconds</code>: A duration in whole seconds, at most one hour.</td>
        <td><code>vane:seconds</code>: A duration in whole seconds, at most one hour.</td>

vane:implements becomes links to the requirements of the draft, in the form vane.html#req-8: a link into an HTML version of the draft, such as Project: Publishing the Specification makes, in which each requirement's anchor is the id of its paragraph. The parameter draft says where the published draft is, relative to the page:

$ sxml transform --param=draft=../spec/vane.html document.xsl flag-fixed.scxml | grep -m 2 'href="../spec'
      <a href="../spec/vane.html#req-1">req-1</a>.</span></p>
      <a href="../spec/vane.html#req-2">req-2</a>.</span></section>

The page is generated from the machine, so it can't fall behind it: when the machine changes, the page is made again, and when the machine names a requirement, the page links to its text. Drawing the Machine adds a picture.