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

Drawing with XSLT

XQuery computed the layout and wrote the drawing in one go. The drawing half is also a natural job for XSLT: each kind of element in the input becomes a piece of SVG, which is exactly what template rules say. In this section a stylesheet draws the diagram from the layout that layout.xq writes, and it draws the edge labels last, on top of everything, which the XQuery drawing doesn't do.

The input: a laid-out diagram

The stylesheet does no arithmetic of its own beyond the label positions. It reads the description with coordinates added, which Computing a Layout made with layout.xq:

$ sxml xquery --context-item=diagram.xml --output=laid-out.xml layout.xq
$ sxml xpath --context-item=laid-out.xml '/diagram/@width/string(), //node[@id = "redis"]/@*!string()'
"360"
"redis"
"redis"
"store"
"200"
"420"

The stylesheet

draw.xsl:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Draws a laid-out diagram (the output of layout.xq) as SVG, in three
     layers: the edges, the nodes, and the edges' labels on top. -->
<xsl:stylesheet version="4.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns="http://www.w3.org/2000/svg"
    expand-text="yes">

  <xsl:output method="xml" indent="yes"/>

  <xsl:key name="node" match="node" use="@id"/>

  <xsl:template match="/diagram">
    <svg width="{@width}" height="{@height}" viewBox="0 0 {@width} {@height}">
      <title>{@title}</title>
      <style>
        .nodes rect {{ fill: #eef3fb; stroke: #3b5b92; stroke-width: 1.5 }}
        .nodes .store rect {{ fill: #f6efe3; stroke: #8a6a2f }}
        .nodes text {{ font: 14px sans-serif; fill: #1b1b1b }}
        .edges line, .edges path {{ fill: none; stroke: #555555; stroke-width: 1.5 }}
        .labels text {{ font: 12px sans-serif; fill: #555555 }}
      </style>
      <defs>
        <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5"
                markerWidth="8" markerHeight="8" orient="auto">
          <path d="M 0 0 L 10 5 L 0 10 z" fill="#555555"/>
        </marker>
      </defs>
      <g class="edges">
        <xsl:apply-templates select="edge" mode="edges"/>
      </g>
      <g class="nodes">
        <xsl:apply-templates select="node" mode="nodes"/>
      </g>
      <g class="labels">
        <xsl:apply-templates select="edge[@label]" mode="labels"/>
      </g>
    </svg>
  </xsl:template>

  <!-- A description without a layout: stop, and say what to do. -->
  <xsl:template match="/diagram[not(@width)]" priority="1">
    <xsl:message terminate="yes">{@title}: no layout; run layout.xq first</xsl:message>
  </xsl:template>

  <!-- The edges layer: a line, or the path of a loop. -->
  <xsl:template match="edge[@x1]" mode="edges">
    <line id="edge-{@from}-{@to}" x1="{@x1}" y1="{@y1}" x2="{@x2}" y2="{@y2}"
          marker-end="url(#arrow)"/>
  </xsl:template>

  <xsl:template match="edge" mode="edges">
    <path id="edge-{@from}-{@to}" d="{@d}" marker-end="url(#arrow)"/>
  </xsl:template>

  <!-- The nodes layer: a box and its label. Stores have rounder corners. -->
  <xsl:template match="node" mode="nodes">
    <xsl:param name="corner" select="6"/>
    <g id="node-{@id}" class="{(@kind, 'service')[1], @class}">
      <rect x="{@x}" y="{@y}" width="140" height="40" rx="{$corner}"/>
      <text x="{@x + 70}" y="{@y + 25}" text-anchor="middle">{@label}</text>
    </g>
  </xsl:template>

  <xsl:template match="node[@kind = 'store']" mode="nodes">
    <xsl:next-match>
      <xsl:with-param name="corner" select="20"/>
    </xsl:next-match>
  </xsl:template>

  <!-- The labels layer: at the middle of a line, or beside a loop. -->
  <xsl:template match="edge[@x1]" mode="labels">
    <text x="{(@x1 + @x2) idiv 2 + 6}" y="{(@y1 + @y2) idiv 2 + 4}">{@label}</text>
  </xsl:template>

  <xsl:template match="edge" mode="labels">
    <xsl:variable name="from" select="key('node', @from)"/>
    <text x="{$from/@x + 56}" y="{$from/@y + 60}">{@label}</text>
  </xsl:template>

</xsl:stylesheet>

Read it from the top.

  • xmlns="http://www.w3.org/2000/svg" on the stylesheet makes it the default namespace of the literal result elements: svg, g, rect and the others are written in the SVG namespace. It does not change what the patterns and paths mean: match="node" and select="edge" still name elements in no namespace, as in the input. (XSLT keeps the two apart; XQuery's declare default element namespace changes both.)
  • expand-text="yes" turns on text value templates: curly braces in text hold expressions, as {@title} and {@label} do. Braces in attributes are attribute value templates, which XSLT always has. In the style rules a brace that is CSS is doubled, the same rule as in XQuery. The text of style is copied as it is written, indentation included; CSS doesn't mind.
  • xsl:output indent="yes" asks for one element per line.

One mode per layer

The template for /diagram writes the frame (title, style, the arrow marker) and three groups, and it fills each group by applying templates in a different mode: edges, nodes and labels. A mode is a separate set of template rules, so the same edge elements are processed twice, once to draw their lines and once to write their labels, by different rules. The order of the groups is the order of drawing: lines first, then the boxes over the ends of the lines, then the labels over both, where no box can hide them.

Within a mode the most specific rule wins. edge[@x1] (an edge with a line) has a predicate, which gives it the default priority 0.5, and edge alone has priority 0; so lines are drawn by the first rule, and an edge without x1, a loop, falls to the second, which draws its path. In the labels mode the loop's label is placed next to its node, which the key node finds by id.

xsl:next-match for stores

A store is a node with rounder corners. Instead of a second copy of the node rule, the store rule hands the work back: xsl:next-match applies the next rule that matches, here the general node rule, and xsl:with-param gives it the corner radius. The general rule declares the parameter with its default, 6:

$ sxml transform draw.xsl laid-out.xml --output=drawn.svg
$ sxml xpath --context-item=drawn.svg '//*:g[@id = ("node-cache", "node-redis")] ! `{@id}: {@class}, rx {*:rect/@rx}`'
"node-cache: service, rx 6"
"node-redis: store, rx 20"

The cache module is a box like the other modules; Redis, where it keeps its data, is the store.

The class is an attribute value template with a sequence in it, {(@kind, 'service')[1], @class}: the values are joined with spaces, and a missing class adds nothing.

The result

$ sxml transform draw.xsl laid-out.xml
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="480" viewBox="0 0 360 480">
  <title>Vane</title>
  <style>
        .nodes rect { fill: #eef3fb; stroke: #3b5b92; stroke-width: 1.5 }
        .nodes .store rect { fill: #f6efe3; stroke: #8a6a2f }
        .nodes text { font: 14px sans-serif; fill: #1b1b1b }
        .edges line, .edges path { fill: none; stroke: #555555; stroke-width: 1.5 }
        .labels text { font: 12px sans-serif; fill: #555555 }
      </style>
  <defs>
    <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto">
      <path d="M 0 0 L 10 5 L 0 10 z" fill="#555555"/>
    </marker>
  </defs>
  <g class="edges">
    <line id="edge-browser-api" x1="90" y1="60" x2="180" y2="120" marker-end="url(#arrow)"/>
    <line id="edge-node-api" x1="270" y1="60" x2="180" y2="120" marker-end="url(#arrow)"/>
    <line id="edge-api-flags" x1="180" y1="160" x2="180" y2="220" marker-end="url(#arrow)"/>
    <line id="edge-flags-registry" x1="180" y1="260" x2="90" y2="320" marker-end="url(#arrow)"/>
    <line id="edge-flags-cache" x1="180" y1="260" x2="270" y2="320" marker-end="url(#arrow)"/>
    <line id="edge-registry-postgresql" x1="90" y1="360" x2="90" y2="420" marker-end="url(#arrow)"/>
    <line id="edge-cache-redis" x1="270" y1="360" x2="270" y2="420" marker-end="url(#arrow)"/>
  </g>
  <g class="nodes">
    <g id="node-browser" class="service">
      <rect x="20" y="20" width="140" height="40" rx="6"/>
      <text x="90" y="45" text-anchor="middle">@vane/browser</text>
    </g>
    <g id="node-node" class="service">
      <rect x="200" y="20" width="140" height="40" rx="6"/>
      <text x="270" y="45" text-anchor="middle">@vane/node</text>
    </g>
    <g id="node-api" class="service">
      <rect x="110" y="120" width="140" height="40" rx="6"/>
      <text x="180" y="145" text-anchor="middle">vane/api</text>
    </g>
    <g id="node-flags" class="service">
      <rect x="110" y="220" width="140" height="40" rx="6"/>
      <text x="180" y="245" text-anchor="middle">vane/flags</text>
    </g>
    <g id="node-registry" class="service">
      <rect x="20" y="320" width="140" height="40" rx="6"/>
      <text x="90" y="345" text-anchor="middle">vane/registry</text>
    </g>
    <g id="node-cache" class="service">
      <rect x="200" y="320" width="140" height="40" rx="6"/>
      <text x="270" y="345" text-anchor="middle">vane/cache</text>
    </g>
    <g id="node-postgresql" class="store">
      <rect x="20" y="420" width="140" height="40" rx="20"/>
      <text x="90" y="445" text-anchor="middle">postgresql</text>
    </g>
    <g id="node-redis" class="store">
      <rect x="200" y="420" width="140" height="40" rx="20"/>
      <text x="270" y="445" text-anchor="middle">redis</text>
    </g>
  </g>
  <g class="labels">
    <text x="141" y="94">HTTP</text>
    <text x="231" y="94">HTTP</text>
    <text x="96" y="394">SQL</text>
    <text x="276" y="394">Redis</text>
  </g>
</svg>

The labels layer has four texts, not seven: the stylesheet applies the labels mode only to edge[@label], and the three edges between modules have no label. The boxes, lines and labels are where the XQuery drawing has them; only the grouping and the style differ. Arithmetic on attributes works because an attribute of an untyped document is untyped: @x + 70 converts it to a number. The two steps also run as one pipeline, the layout's output read by the stylesheet from standard input (-):

$ sxml xquery --context-item=states.xml layout.xq | sxml transform draw.xsl - --output=states.svg
$ sxml xpath --context-item=states.svg '//*:g[@class = "labels"]/*:text ! `{.} at {@x},{@y}`'
"rollout.request at 141,94"
"check.passed at 186,194"
"check.failed at 171,94"
"step.done at 171,144"
"kill.request at 186,44"

A rule for input that is wrong

Given the description itself, without the layout, the rules above would write a drawing with empty sizes and no places, and say nothing. One more rule catches that: /diagram[not(@width)] matches a diagram without a layout, and xsl:message terminate="yes" stops the transformation with the message. Both patterns, /diagram and /diagram[not(@width)], have the default priority 0.5, so the rule gives itself priority="1" to win:

$ sxml transform draw.xsl diagram.xml
Vane: no layout; run layout.xq first
draw.xsl:43:5: err:XTMM9000: Vane: no layout; run layout.xq first

Matching SVG

So far the stylesheet wrote SVG and read elements in no namespace. A stylesheet that reads SVG must match SVG names, and the attribute xpath-default-namespace says which namespace unprefixed names in patterns and paths are in. This one lists the boxes of any drawing:

boxes.xsl:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Lists the boxes of an SVG drawing: where each one is, and its text. -->
<xsl:stylesheet version="4.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xpath-default-namespace="http://www.w3.org/2000/svg"
    expand-text="yes">

  <xsl:output method="text"/>

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

  <xsl:template match="rect">
    <xsl:text>{@x},{@y} {../text}&#10;</xsl:text>
  </xsl:template>

</xsl:stylesheet>

Run on both drawings, it shows that they agree:

$ sxml transform boxes.xsl diagram.svg
20,20 @vane/browser
200,20 @vane/node
110,120 vane/api
110,220 vane/flags
20,320 vane/registry
200,320 vane/cache
20,420 postgresql
200,420 redis
$ sxml transform boxes.xsl drawn.svg
20,20 @vane/browser
200,20 @vane/node
110,120 vane/api
110,220 vane/flags
20,320 vane/registry
200,320 vane/cache
20,420 postgresql
200,420 redis

Without xpath-default-namespace, //rect would look for rect in no namespace, find nothing, and the output would be empty. The next section is all about stylesheets like this one, which read drawings and write changed ones.