XSLT

From HalfgeekKB
Revision as of 07:44, 5 October 2015 by Psmay (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Utilizing XSLT to its fullest will probably require some paradigm shifting into functional programming. Particularly smart transforms will require recursion.

Tools are listed below.

Identity transform / Strip matching elements

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
	<xsl:output method="xml" omit-xml-declaration="yes"/>
	<xsl:template match="node()|@*">
		<xsl:copy>
			<xsl:apply-templates select="node()|@*"/>
		</xsl:copy>
	</xsl:template>
	<xsl:template match="expression-returning-unwanted-elements"/>
</xsl:stylesheet>

Case transformation

XSLT has no built-in toUpper/toLower-style functions. The most commonly suggested solution seems to be to co-opt the built-in transform() function, making explicit your tables.

<xsl:transform ...>
	<xsl:variable name="upper">ABCDEFGHIJKLMNOPQRSTUVWXYZ</xsl:variable>
	<xsl:variable name="lower">abcdefghijklmnopqrstuvwxyz</xsl:variable>

	...
		<xsl:value-of select="translate(.,$upper,$lower)"/>
...

This obviously doesn't handle the occurrence of international characters. For that, try XSLT case transformation tables.

EXSLT minor examples

<xsl:stylesheet
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:_="urn:uuid:91da8805-6acd-42e1-ba6c-2650d0fca410"
  exclude-result-prefixes="_"
  xmlns:exsl="http://exslt.org/common"
  xmlns:func="http://exslt.org/functions"
  xmlns:str="http://exslt.org/strings"
  extension-element-prefixes="exsl func str"
  version="1.0">
  <xsl:output method="xml" encoding="utf-8" indent="yes"/>
  <!--
    All of these tests are known to work with libxml 20902, libxslt
    10128, libexslt 817.
  -->
  
  <!--
    A template used in the `exsl:node-set()` example.
  -->
  <xsl:template match="item" mode="second-pass">
    <foo>
      <xsl:value-of select=". * 2"/>
    </foo>
  </xsl:template>
  
  <!--
    `func:function` example.

    The name is required to have a prefix (specifically, the name must not
    exist in the null namespace). Transforms I write generally have a
    pseudo-private prefix named `_` or `lo` (as in local) pointing to a
    freshly generated UUID URN (briefly, `urn:uuid:` followed by a plain
    old UUID; see RFC 4122) in order to discourage its use, accidental or
    intentional, outside the current file. Add the prefix to
    `exclude-result-prefixes` attribute on the
    `xsl:stylesheet`/`xsl:transform`.

    func:result also supports a `select` attribute, which is preferable if
    the result is just an XPath expression.
  -->
  <func:function name="_:seq">
    <xsl:param name="count" select="0"/>
    <xsl:param name="start" select="1"/>
    <func:result>
      <xsl:if test="$start &lt;= $count">
        <item>
          <xsl:value-of select="$start"/>
        </item>
        <xsl:copy-of select="_:seq($count, $start + 1)"/>
      </xsl:if>
    </func:result>
  </func:function>
  
  <xsl:template match="/">
    <demos>
      
      <test name="exsl:object-type">
        <!--
          Possible results for exsl:object-type() are `string`,
          `number`, `boolean`, `node-set`, `RTF` (result tree
          fragment), or `external`.
        -->
        <expect>number</expect>
        <actual>
          <xsl:value-of select="exsl:object-type(12345)"/>
        </actual>
      </test>
      
      <test name="str:tokenize">
        <!--
          `str:tokenize()` splits a string using all individual
          characters of the second parameter as delimiters. If the
          second parameter is missing, splits on tab, newlines, and
          space. If present but empty, the result is each character
          of the string as a separate token.
        -->
        <expect>
          <token>ab</token>
          <token>cd</token>
          <token>ef</token>
        </expect>
        <actual>
          <xsl:copy-of select="str:tokenize('ab+cd*ef','*+')"/>
        </actual>
      </test>
      
      <test name="str:split">
        <!--
          `str:split()` splits a string using the entire second
          parameter as the only delimiter. If omitted, a single
          space is used. If present but empty, the result is each
          character of the string as a separate token.
        -->
        <expect>
          <token>melon</token>
          <token>enamel</token>
          <token>mole</token>
        </expect>
        <actual>
          <xsl:copy-of select="str:split('melonlemonenamellemonmole','lemon')"/>
        </actual>
      </test>
      
      <test name="str:replace">
        <!--
          `str:replace()` scans the first parameter to replace
          instances of the second with the third.
        -->
        <expect>A man, a plan, a canal, Panama</expect>
        <actual>
          <xsl:value-of select="str:replace('A mern, a plern, a cernal, Pernama', 'ern', 'an')"/>
        </actual>
      </test>
      
      <test name="str:replace 2">
        <!--
          If `str:replace()` is called with a node-set of search
          params and a node-set of replace params, all will be used
          by way of a one-to-one mapping from the first list to the
          second.

          Remember that the nodes in a variable element don't count
          as a node-set. Note also that the result of
          `exsl:node-set()` is not the sequence of nodes but a
          singular entity that contains them. The useful value can
          be located at `exsl:node-set(...)/*` (or, on the occasion
          that you want all the nodes and not just the elements,
          `exsl:node-set(...)/node()`).
        -->
        <!--
          When building up a variable whose only use will be as
          basis for a node-set, I give it an uglified name
          (`_rtf_searches`, `_rtf_values` below) and then give the
          non-ugly name (`searches`, `values` below) to the variable
          containing the converted node-set.
        -->
        <xsl:variable name="_rtf_searches">
          <!--
            Note that the element names aren't really important
            here.
          -->
          <foo>[LASTNAME]</foo>
          <foo>[FIRSTNAME]</foo>
          <foo>[MIDDLEINITIAL]</foo>
          <foo>[COUNTRY]</foo>
          <foo>[CITY]</foo>
        </xsl:variable>
        <xsl:variable name="searches" select="exsl:node-set($_rtf_searches)/*"/>
        <xsl:variable name="_rtf_values">
          <bar>Smith</bar>
          <bar>John</bar>
          <bar>Q</bar>
          <bar>USA</bar>
          <bar>Anytown</bar>
        </xsl:variable>
        <xsl:variable name="values" select="exsl:node-set($_rtf_values)/*"/>
        <xsl:variable name="template">
          <xsl:text>Mr. and Mrs. [FIRSTNAME] [MIDDLEINITIAL]</xsl:text>
          <xsl:text>. [LASTNAME], [CITY], [COUNTRY]</xsl:text>
        </xsl:variable>
        <expect>Mr. and Mrs. John Q. Smith, Anytown, USA</expect>
        <actual>
          <xsl:value-of select="str:replace($template, $searches, $values)"/>
        </actual>
      </test>
      
      <test name="str:padding">
        <!--
          `str:padding()` repeats a string until it is at least the
          desired number of characters long, then truncates (if
          necessary) to exactly the desired number.
        -->
        <expect>foodfoodfoodfoo</expect>
        <actual>
          <xsl:value-of select="str:padding(15, 'food')"/>
        </actual>
      </test>
      
      <test name="func:function">
        <!--
          Utilizes the `_:seq()` function defined above.

          Note that non-flat data can be returned from a function;
          `xsl:copy-of` is used here to insert the result into the
          output.
        -->
        <expect>
          <item>1</item>
          <item>2</item>
          <item>3</item>
          <item>4</item>
          <item>5</item>
        </expect>
        <actual>
          <xsl:copy-of select="_:seq(5)"/>
        </actual>
      </test>
      
      <test name="exsl:node-set">
        <!--
          Data originally constructed as a result tree fragment can
          be converted to a node-set using `exsl:node-set`. As noted
          above, this function returns a single entity containing
          everything that was converted. Some applications make more
          sense when trailed by e.g. `/*` or `/node()` to collect
          the node-set's contents.

          In this scenario, the entire node-set is passed as the
          select expression of `xsl:apply-templates`, which is one
          way to simulate multiple passes on one document. The
          `mode` attribute is used to separate this processing from
          the first pass.
        -->
        <expect>
          <foo>2</foo>
          <foo>4</foo>
          <foo>6</foo>
          <foo>8</foo>
          <foo>10</foo>
        </expect>
        <actual>
          <xsl:apply-templates mode="second-pass" select="exsl:node-set(_:seq(5))"/>
        </actual>
      </test>

    </demos>
  </xsl:template>
</xsl:stylesheet>

Text indentation toy

This transform handles a document with top, levels, and lines (root = top, top ( (level|line)* ), level ( (level|line)* )) and indents lines to the appropriate depth for the level. It also has an optional abs attribute for a level to interrupt the normal scheme (for example, abs="0" would make sense for embedded POD doc).

The indent and indent-depth templates demonstrate the use of templates in terms of the current state rather than parameters.

<?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:variable name="TAB" select="'&#9;'"/>
	<xsl:variable name="ENDL" select="'&#10;'"/>
	<xsl:variable name="BL" select="'&#10;&#10;'"/>
	<xsl:variable name="APOS">'</xsl:variable>
	<xsl:strip-space elements="*"/>
	<xsl:preserve-space elements="line"/>

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

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

	<xsl:template match="line">
		<xsl:call-template name="indent"/>
		<xsl:apply-templates/>
		<xsl:value-of select="$ENDL"/>
	</xsl:template>

	<xsl:template name="repeat">
		<xsl:param name="str"/>
		<xsl:param name="count" select="0"/>
		<xsl:if test="$count &gt; 0">
			<xsl:value-of select="$str"/>
			<xsl:call-template name="repeat">
				<xsl:with-param name="str" select="$str"/>
				<xsl:with-param name="count" select="$count - 1"/>
			</xsl:call-template>
		</xsl:if>
	</xsl:template>

	<xsl:template name="indent">
		<xsl:variable name="depth">
			<xsl:call-template name="indent-depth"/>
		</xsl:variable>
		<xsl:call-template name="repeat">
			<xsl:with-param name="str" select="$TAB"/>
			<xsl:with-param name="count" select="number($depth)"/>
		</xsl:call-template>
	</xsl:template>

	<xsl:template name="indent-depth">
		<xsl:variable name="own-value">
			<xsl:choose>
				<xsl:when test="self::level">1</xsl:when>
				<xsl:otherwise>0</xsl:otherwise>
			</xsl:choose>
		</xsl:variable>
		<xsl:variable name="parent-value">
			<xsl:choose>
				<xsl:when test="parent::top">0</xsl:when>
				<xsl:when test="parent::level[@abs]"><xsl:value-of select="parent::level/@abs"/></xsl:when>
				<xsl:when test="parent::*">
					<xsl:for-each select="parent::*">
						<xsl:call-template name="indent-depth"/>
					</xsl:for-each>
				</xsl:when>
				<xsl:otherwise>0</xsl:otherwise>
			</xsl:choose>
		</xsl:variable>
		<xsl:value-of select="number($own-value) + number($parent-value)"/>
	</xsl:template>

</xsl:stylesheet>

Unorganized notes

Inline link

INLINE LINK
<?xml-stylesheet type="text/xsl" href="sheetFoo.xsl"?>


Boilerplate

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


value-of

VALUE
XPath context is from <xsl:template/> or <xsl:for-each/>.
XPath for current context is "."
<xsl:value-of select="path/to/node"/>


for-each

FOREACH
XPath filters apply (eg "node[subnode='value']").
<xsl:sort/> is optional.
<xsl:for-each select="xpath/expression">
	<!-- executes this for every <expression/> child of a <xpath/> -->
	<xsl:sort select="foo"/><!-- sort by the <foo/> subnode -->
	<li>
		<!-- The value of the node <foo/> under this <expression/> -->
		<xsl:value-of select="foo"/>
	</li>
</xsl:for-each>


if

IF
<xsl:if test="condition">
	condition was true
</xsl:if>


choose/when/otherwise (If-Then-Else)

IF-THEN-ELSE
<xsl:choose>
	<xsl:when test="ifThis">
		ifThis was true
	</xsl:when>
	<xsl:when test="elseifThis">
		ifThis was not true
		elseifThis was true
	</xsl:when>
	<xsl:otherwise>
		ifThis and elseifThis were both not true
	</xsl:otherwise>
</xsl:choose>
Conditions like
childname < 0
childname > 0


template (subroutines)

SUBROUTINES
It's hard to tell how the scoping works, but so far it seems that the template of match="/" is the one that is rendered outmost.

<xsl:template match="/">
	<html>
		<body>
			<h1>Welcome to my Nightmare</h1>
			<ul><xsl:apply-templates/></ul>
		</body>
	</html>
</xsl:template>
<xsl:template match="cd">
	<li>
		<xsl:apply-templates select="title"/>
		<xsl:apply-templates select="artist"/>
	</li>
</xsl:template>
<xsl:template match="title">
	<b><xsl:value-of select="."/></b>
</xsl:template>
<xsl:template match="artist">
	(<xsl:value-of select="."/>)
</xsl:template>

output (output format control)

OUTPUT CONTROL
<xsl:output method="text" indent="no" media-type="application/json"/>

method is:

  • xml
  • html
  • text

media-type is the MIME type of the output.

  • SVG : image/svg+xml

See also http://www.w3schools.com/xsl/el_output.asp


more

<xsl:template name="foo">
	<xsl:param name="s"/>
	<!-- ... -->
</xsl:template>

<xsl:call-template name="foo">
	<xsl:with-param name="s" select="XPathReturnValueForS"/>
</xsl:call-template>

A post-process-based means of preventing certain empty tags from collapsing

<?xml version="1.0" encoding="UTF-8"?>
<!--
NONCE=`uuidgen` &&
	xsltproc -%-stringparam nonce "$NONCE" ex.xsl test.xml |
	perl -n -e 's{<!-%-'$NONCE'-%->}{}sg; print'
-->
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
	<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
	<xsl:param name="nonce">REMOVE-ME</xsl:param>
	<xsl:template match="@*|node()">
		<xsl:copy>
			<xsl:apply-templates select="@*|node()"/>
		</xsl:copy>
	</xsl:template>
	<xsl:template match="style">
		<style>
			<xsl:if test="not(@type)">
				<xsl:attribute name="type">text/css</xsl:attribute>
			</xsl:if>
			<xsl:apply-templates select="@*|node()"/>
		</style>
	</xsl:template>
	<xsl:template match="script">
		<script>
			<xsl:if test="not(@type)">
				<xsl:attribute name="type">text/javascript</xsl:attribute>
			</xsl:if>
			<xsl:apply-templates select="@*|node()"/>
			<xsl:if test="not(node())">
				<xsl:comment><xsl:value-of select="$nonce"/></xsl:comment>
			</xsl:if>
		</script>
	</xsl:template>
</xsl:transform>

Tools

XSLT Processors

Xalan-J

java org.apache.xalan.xslt.Process -XSL sheet.xsl -IN input.xml

For XSLT 1.0.

See also [1].

xsltproc (LibXSLT)

xsltproc sheet.xsl input.xml

For XSLT 1.0.

See also xsltproc.

Saxon

java net.sf.saxon.Transform -xsl:sheet.xsl -s:input.xml [ -o:output.xml ]

For XSLT 2.0. The open-source variant, Saxon-HE, is intentionally broken to leave out a lot of the XSLT 2.0 functionality, but there is enough left for it to remain significantly more useful than XSLT 1.0.

Don't try to use Saxon on an XSLT 1.0 sheet; it does complain and support for EXSLT is not compiled in by default. Stick with Xalan for 1.0.

XSLT Documentation

See also