DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

100 Languages Speedrun: Episode 22: XSLT

In the late 1990s and early 2000s there was an XML craze. People even wanted to replace HTML with some XML variant, and literally the only "advantage" it would have over HTML was that if you made any typos, the website would just refuse to display anything at all. Somehow that was supposed to be a huge selling point.

Eventually common sense prevailed, but back then XML craze was going so hot, people were asking questions like - what if I need to turn XML into XML? I know, I'l use XML! That's how XSLT came to be.

Hello, World!

We can't really do conventional Hello, World!, as the whole XSLT model is turning XML into XML, but let's do something simple anyway.

Here's hello.xml:

<?xml version="1.0" ?>
<persons>
  <person>
    <name>Alice</name>
  </person>
  <person>
    <name>Bob</name>
  </person>
</persons>
Enter fullscreen mode Exit fullscreen mode

And here's hello.xslt:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="/persons">
    <messages>
      <xsl:apply-templates select="person"/>
    </messages>
  </xsl:template>

  <xsl:template match="person">
    <message>Hello, <xsl:value-of select="name" />!</message>
  </xsl:template>
</xsl:stylesheet>
Enter fullscreen mode Exit fullscreen mode

We can then run it like this, xsltproc is even preinstalled on OSX:

$ xsltproc hello.xslt hello.xml
<?xml version="1.0"?>
<messages>
  <message>Hello, Alice!</message>
  <message>Hello, Bob!</message>
</messages>
Enter fullscreen mode Exit fullscreen mode

So what's going on:

  • first, the <?xml> boilerplate and some namespaces and versions. It's best to just copy paste paste it.
  • xsl:output specifies output mode, in this case we want to generate XML and indent it automatically for readability. Not every kind of XML should be indented like that.
  • Then we have two templates with xsl:template - top level one for /persons and then second one for each /person.

If this seems to you like a bit crazy way to code, then you're not wrong.

Text output

In addition to generating XML, XSLT can also generate HTML and plain text. Let's try some plain text. We need to be very careful to get all the spaces and newlines in the right places, so this look extremely verbose.

Here's text.xml:

<?xml version="1.0" ?>
<persons>
  <person>
    <name>Alice</name>
    <surname>Cooper</surname>
  </person>
  <person>
    <name>Bob</name>
    <surname>Smith</surname>
  </person>
</persons>
Enter fullscreen mode Exit fullscreen mode

And here's text.xslt:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>

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

  <xsl:template match="person">
    <xsl:text>Hello, </xsl:text>
    <xsl:value-of select="name" />
    <xsl:text> </xsl:text>
    <xsl:value-of select="surname" />
    <xsl:text>!&#10;</xsl:text>
  </xsl:template>
</xsl:stylesheet>
Enter fullscreen mode Exit fullscreen mode

And the output:

Hello, Alice Cooper!
Hello, Bob Smith!
Enter fullscreen mode Exit fullscreen mode

FizzBuzz

We could just generate the whole thing from scratch, but I think it's more true to the purpose of XSLT if we start with this fizzbuzz.xml:

<?xml version="1.0" encoding="UTF-8"?>
<fizzbuzz>
  <number>1</number>
  <number>2</number>
  <number>3</number>
  <number>4</number>
  ...
  <number>100</number>
</fizzbuzz>
Enter fullscreen mode Exit fullscreen mode

Then we could do this for fizzbuzz.xslt:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>

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

  <xsl:template match="number">
    <xsl:variable name="i" select="." />
    <xsl:choose>
      <xsl:when test="$i mod 15 = 0">FizzBuzz</xsl:when>
      <xsl:when test="$i mod 3 = 0">Fizz</xsl:when>
      <xsl:when test="$i mod 5 = 0">Buzz</xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$i" />
      </xsl:otherwise>
    </xsl:choose>
    <xsl:text>&#10;</xsl:text>
  </xsl:template>
</xsl:stylesheet>
Enter fullscreen mode Exit fullscreen mode

Which generates exactly the FizzBuzz sequence you're expecting.

There's things happenings here:

  • xsl:variable sets a local variable i
  • xsl:choose with xsl:whene and xsl:otherwise decide which FizzBuzz branch to take
  • there's also xsl:if we could use instead

Loops

XSLT went through many iterations. XSLT 2.0 would actually make this reasonably easy, thanks to more flexible xsl:for-each, but the XSLT processor that comes with OSX only supports XSLT 1.0, and it's not the only one - a lot of XSLT software never went past XSLT 1.0. So let's give it a go - we don't have loops, but we have recursion.

Basically we first figure out how many iterations we want, then call iteration(1, 20). It will then check if current index reached max - if yes, that will be the end of it, otherwise it will call iteration(2, 20), which will call iteration(3, 20) and so on until iteration(20, 20) eventually stops.

Here's loop.xml:

<?xml version="1.0" ?>
<loop>20</loop>
Enter fullscreen mode Exit fullscreen mode

And loop.xslt:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>

  <xsl:template name="iteration">
    <xsl:param name="i" />
    <xsl:param name="max" />

    <xsl:text>Iteration </xsl:text>
    <xsl:value-of select="$i"/>
    <xsl:text>&#10;</xsl:text>
    <xsl:if test="$max > $i">
      <xsl:call-template name="iteration">
        <xsl:with-param name="i" select="$i + 1"/>
        <xsl:with-param name="max" select="$max"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template match="/loop">
    <xsl:call-template name="iteration">
      <xsl:with-param name="i" select="1"/>
      <xsl:with-param name="max" select="."/>
    </xsl:call-template>
  </xsl:template>
</xsl:stylesheet>
Enter fullscreen mode Exit fullscreen mode

Which generates:

$ xsltproc loop.xslt loop.xml
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
Iteration 6
Iteration 7
Iteration 8
Iteration 9
Iteration 10
Iteration 11
Iteration 12
Iteration 13
Iteration 14
Iteration 15
Iteration 16
Iteration 17
Iteration 18
Iteration 19
Iteration 20
Enter fullscreen mode Exit fullscreen mode

Fibonacci

And now that we can loop, we can generate the Fibonacci sequence.

fib.xml is just the max value:

<?xml version="1.0" ?>
<fib-sequence>20</fib-sequence>
Enter fullscreen mode Exit fullscreen mode

And let's do fib.xslt:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>

  <xsl:template name="fib">
    <xsl:param name="n"/>
     <xsl:choose>
        <xsl:when test="2 >= $n">1</xsl:when>
        <xsl:otherwise>
          <xsl:variable name="a">
            <xsl:call-template name="fib">
              <xsl:with-param name="n" select="$n - 1"/>
            </xsl:call-template>
          </xsl:variable>
          <xsl:variable name="b">
            <xsl:call-template name="fib">
              <xsl:with-param name="n" select="$n - 2"/>
            </xsl:call-template>
          </xsl:variable>
        <xsl:value-of select="$a + $b"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <xsl:template name="iteration">
    <xsl:param name="i" />
    <xsl:param name="max" />

    <xsl:text>fib(</xsl:text>
    <xsl:value-of select="$i"/>
    <xsl:text>) = </xsl:text>
    <xsl:call-template name="fib">
      <xsl:with-param name="n" select="$i"/>
    </xsl:call-template>
    <xsl:text>&#10;</xsl:text>
    <xsl:if test="$max > $i">
      <xsl:call-template name="iteration">
        <xsl:with-param name="i" select="$i + 1"/>
        <xsl:with-param name="max" select="$max"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template match="/fib-sequence">
    <xsl:call-template name="iteration">
      <xsl:with-param name="i" select="1"/>
      <xsl:with-param name="max" select="."/>
    </xsl:call-template>
  </xsl:template>
</xsl:stylesheet>
Enter fullscreen mode Exit fullscreen mode

In order:

  • we define recursive function fib(n) for calculating the Fibonacci value
  • we define iteration(i, max) which will do our looping
  • we call iteration(1, max) at top level /fib-sequence

And the output is as expected:

fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
fib(11) = 89
fib(12) = 144
fib(13) = 233
fib(14) = 377
fib(15) = 610
fib(16) = 987
fib(17) = 1597
fib(18) = 2584
fib(19) = 4181
fib(20) = 6765
Enter fullscreen mode Exit fullscreen mode

XSLT 2.0 would make it slightly more readable as we wouldn't ned recursive looping, but in the end it would still be quite dreadful.

Should you use XSLT?

Absolutely not.

XSLT is basically a joke language, except unlike with Emojicode, Befunge, Brainfuck, and such, people who created it weren't in on the joke.

Just about every real language does XML processing better than XSLT. Just pick your favorite.

Usually Ruby or Python is a close call, but in this case the first choice is very clearly Ruby. Ruby's Nokogiri is nearly perfect, and for some reason all Python's XML libraries I've tried (and I've tried a lot of them), had a lot of issues. Of course that's just relatively speaking, any of Python's libraries is still far better than using XSLT.

There are no excuses to use XSLT. It's unsuitable for any purpose, in any version.

Code

All code examples for the series will be in this repository.

Code for the XSLT episode is available here.

Latest comments (0)