Gerrit Imsieke [ˈɡɛʁɪt.ˈɪmziːkə] or [ɪmˈziːkə] (@gimsieke),
le-tex publishing services [ɛl.ˈeː.tɛç] (@letexml)
Note: “Customization” in this presentation is understood as the idiomatic xsl:import
/override
approach, not so much as “using predefined customization parameters”
plus ad-hoc XSLT
<para role="foo">Para</para>
<xsl:template match="db:para | db:simpara">
<p>
<xsl:apply-templates select="@* | node()"/>
</p>
</xsl:template>
<xsl:template match="@role">
<xsl:attribute name="class" select="."/>
</xsl:template>
⇒
<p class="foo">Para</p>
@class
attribute tokens1. <para role="foo">Para</para>
2. <para>Para</para>
<xsl:template match="@role">
<xsl:attribute name="class" separator=" "
select="local-name(..), ." />
</xsl:template>
⇒
1. <p class="para foo">Para</p>
2. <p>Para</p> ⚡
If you always want the local-name()
to appear, hard-wire xsl:attribute
:
<xsl:template match="db:para | db:simpara">
<p>
<xsl:attribute name="class" separator=" ">
<xsl:sequence select="local-name()"/>
<xsl:apply-templates select="@role"/>
</xsl:attribute>
<xsl:apply-templates select="@* except @role | node()"/>
</p>
</xsl:template>
⇒
1. <p class="para foo">Para</p>
2. <p class="para">Para</p>
@condition
attribute@condition
like @role
, merge the resulting classes<xsl:template match="db:para | db:simpara">
<p>
<xsl:variable name="transformed-atts" as="attribute(class)*">
<xsl:apply-templates select="@role, @condition"/>
</xsl:variable>
<xsl:if test="exists($transformed-atts)">
<xsl:attribute name="class" separator=" ">
<xsl:sequence select="$transformed-atts"/>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates
select="@* except (@role | @condition) | node()"/>
</p>
</xsl:template>
<xsl:template match="@role | @condition">
<xsl:attribute name="class" select="." separator=" "/>
</xsl:template>
1. <para role="foo">Para</para>
2. <para>Para</para>
3. <para role="foo" condition="web hidden">Para</para>
4. <para condition="web">Para</para>
⇒
1. <p class="foo">Para</p>
2. <p>Para</p>
3. <p class="foo web hidden">Para</p>
4. <p class="web">Para</p>
A bit inefficient since the individually created class attributes are cast to strings before they become part of a compound class attribute again.
Also, duplicate tokens possible.
simpara
; @condition
, but filter out the token 'web'
1. <para role="foo">Para</para>
2. <para>Para</para>
3. <para role="foo" condition="web hidden">Para</para>
4. <para condition="web">Para</para>
5. <simpara>Simpara</simpara>
⇒
1. <p class="foo">Para</p>
2. <p>Para</p>
3. <p class="foo hidden">Para</p>
4. <p>Para</p>
5. <p class="simpara">Simpara</p>
<xsl:template match="@role" as="xs:string*">
<xsl:sequence select="tokenize(.)"/>
</xsl:template>
<xsl:template match="@condition">
<xsl:sequence select="tokenize(.)[not(. = 'web')]"/>
</xsl:template>
<xsl:template match="db:para | db:simpara">
<p>
<xsl:variable name="transformed-atts" as="xs:string*">
<xsl:apply-templates select="@role, @condition"/>
<xsl:sequence select="local-name()[self::simpara]"/>
</xsl:variable>
<xsl:if test="exists($transformed-atts)">
<xsl:attribute name="class" separator=" ">
<xsl:sequence select="$transformed-atts"/>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates
select="@* except (@role | @condition) | node()"/>
</p>
</xsl:template>
<gloss>
⇒ <span class="gloss">…
for TEI<simpara>
⇒ <p class="simpara">…
for DocBook xslTNG<p class="first">…
for JATS p
and license-p
elements
without predecessorHowever: If you use the function arguments for adding classes (DocBook, TEI) or transform p/@content-type
to a @class
attribute (JATS), you will overwrite these defaults. ⚡
<xsl:when>
s.<simpara>
, for example.<xsl:next-match>
to invoke a common node-processing template.Q: Can’t we use <xsl:next-match>
for the attributes, too, at least for the class attribute?
A: Yes, if we transform the element in a different mode that is for attribute creation only.
class-att
modeclass-att
<xsl:template match="db:para | db:simpara">
<p>
<xsl:apply-templates select="." mode="class-att"/>
<xsl:apply-templates select="@* | node()"/>
</p>
</xsl:template>
<xsl:template match="@role | @condition"/>
<xsl:template match="*" mode="class-att" as="attribute(class)?">
<xsl:call-template name="make-class">
<xsl:with-param name="tokens" as="xs:string*">
<xsl:apply-templates select="@role, @condition" mode="#current"/>
</xsl:with-param>
</xsl:call-template>
</xsl:template>
class-att
class-att
mode will make class attributes from elements
and tokens from attributes (default: tokenize them)
<xsl:template match="@*" mode="class-att" as="xs:string+">
<xsl:sequence select="tokenize(.)"/>
</xsl:template>
Auxiliary named template make-class
:
<xsl:template name="make-class" as="attribute(class)?">
<xsl:param name="tokens" as="xs:string*"/>
<xsl:if test="exists($tokens[normalize-space()])">
<xsl:attribute name="class" separator=" "
select="distinct-values($tokens[normalize-space()])"/>
</xsl:if>
</xsl:template>
Add the source element name only for simpara
:
<xsl:template match="db:simpara" mode="class-att" as="attribute(class)">
<xsl:attribute name="class" separator=" ">
<xsl:sequence select="local-name()"/>
<xsl:next-match/>
</xsl:attribute>
</xsl:template>
Remove 'web'
for @condition
:
<xsl:template match="@condition" mode="class-att" as="xs:string*">
<xsl:variable name="next-match" as="xs:string*">
<xsl:next-match/>
</xsl:variable>
<xsl:sequence select="$next-match[not(. = 'web')]"/>
</xsl:template>
Elegance is xsl:next-match’s second name. #xslt
— Gerrit Imsieke (@gimsieke) March 28, 2015
1. <para role="foo">Para</para>
2. <para>Para</para>
3. <para role="foo" condition="web hidden">Para</para>
4. <para condition="web">Para</para>
5. <simpara>Simpara</simpara>
⇒
1. <p class="foo">Para</p>
2. <p>Para</p>
3. <p class="foo hidden">Para</p>
4. <p>Para</p>
5. <p class="simpara">Simpara</p>
Same as the Requirement 3 results, as it should be.
class-att
approach@role
to @class
in default mode⇒ Document the chosen approach in the basic stylesheet, and warn people never to create class attributes in default mode.
m:attributes
as a breakout mode in which to create all
attributesxsl:apply-templates
that transforms the context item in a specific mode.tei:isInline()
function that contains 128 xsl:when
statementsxsl:apply-templates
in a specific modePeople try to avoid repeating similar modified identity templates for each source element, like this:
<xsl:template match="*">
<xsl:element name="{db:new-name(.)}">
<xsl:apply-templates select="." mode="class-att"/>
<xsl:apply-templates select="@* | node()"/>
</xsl:element>
</xsl:template>
with a mapping function like this:
<xsl:function name="db:new-name" as="xs:string">
<xsl:param name="elt" as="element(*)"/>
<xsl:choose>
<xsl:when test="local-name($elt) = ('para', 'simpara')">
<xsl:sequence select="'p'"/>
</xsl:when>
<xsl:when test="local-name($elt) = ('emphasis') and $elt/@role = 'bold'">
<xsl:sequence select="'b'"/>
</xsl:when>
<xsl:when test="local-name($elt) = ('emphasis')">
<xsl:sequence select="'i'"/>
</xsl:when>
<!-- … -->
<xsl:otherwise>
<xsl:sequence select="'div'"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
Problem: If you want to tweak the mapping, you’ll have to overwrite the whole function.
Solution: Refactor it, using a dedicated db:new-name
mode:
<xsl:function name="db:new-name" as="xs:string">
<xsl:param name="elt" as="element(*)"/>
<xsl:apply-templates select="$elt" mode="db:new-name"/>
</xsl:function>
with these string-producing templates in db:new-name
mode:
<xsl:template match="*" mode="db:new-name" as="xs:string">
<xsl:sequence select="'div'"/>
</xsl:template>
<xsl:template match="db:para | db:simpara" mode="db:new-name" as="xs:string">
<xsl:sequence select="'p'"/>
</xsl:template>
<xsl:template match="db:emphasis[@role = 'bold']" mode="db:new-name" as="xs:string">
<xsl:sequence select="'b'"/>
</xsl:template>
<xsl:template match="db:emphasis" mode="db:new-name" as="xs:string">
<xsl:sequence select="'i'"/>
</xsl:template>
<xsl:template match="db:emphasis[@role = 'bold']"
mode="db:new-name" as="xs:string">
<xsl:sequence select="'strong'"/>
</xsl:template>
<xsl:template match="db:emphasis"
mode="db:new-name" as="xs:string">
<xsl:sequence select="'em'"/>
</xsl:template>
1. <para role="foo">Para</para>
2. <para><emphasis role="bold">Para</emphasis></para>
3. <para role="foo" condition="web hidden">Para</para>
4. <para condition="web">Para</para>
5. <simpara><emphasis>Sim</emphasis>para</simpara>
⇒
1. <p class="foo">Para</p>
2. <p><strong class="bold">Para</strong></p>
3. <p class="foo hidden">Para</p>
4. <p>Para</p>
5. <p class="simpara"><em>Sim</em>para</p>
<strong class="bold">
???
class-att
mapping for @role='bold'
<xsl:template match="db:emphasis/@role[. = 'bold']"
mode="class-att" as="xs:string?"/>
⇒
1. <p class="foo">Para</p>
2. <p><strong>Para</strong></p>
3. <p class="foo hidden">Para</p>
4. <p>Para</p>
5. <p class="simpara"><em>Sim</em>para</p>
That’s what DocBook xslTNG does (in contrast to the other renderers in the paper)
<xsl:template match="db:para|db:simpara">
<p>
<xsl:apply-templates select="." mode="m:attributes"/>
<xsl:apply-templates/>
</p>
</xsl:template>
<xsl:template match="*" mode="m:attributes" as="attribute()*">
<xsl:variable name="attr" as="attribute()*">
<xsl:apply-templates select="@*"/>
<xsl:sequence select="f:chunk(.)"/>
</xsl:variable>
<xsl:sequence select="f:attributes(., $attr)"/>
</xsl:template>
'simpara'
token if no extra class is given.
But this token will disappear if any other token is produced.<xsl:function name="f:attributes" as="attribute()*">
<xsl:param name="node" as="element()"/>
<xsl:param name="attributes" as="attribute()*"/>
<xsl:sequence select="f:attributes($node, $attributes, local-name($node), ())"/>
</xsl:function>
<xsl:function name="f:attributes" as="attribute()*">
<xsl:param name="node" as="element()"/>
<xsl:param name="attributes" as="attribute()*"/>
<xsl:param name="extra-classes" as="xs:string*"/>
<xsl:param name="exclude-classes" as="xs:string*"/>
Problem: Not all kool-aid was consumed. For some contexts, f:attributes()
creates defaults,
for others it won’t. Overriding this may result in the known redundancies, unless…
In principle, you can just transform @role
, @condition
, …, in default mode, and
the resulting class tokens will be merged. Side effect: 'simpara'
token disappears.
Solution: A db:modify-class
template or function
<xsl:template match="db:para | db:simpara" mode="m:attributes">
<xsl:call-template name="db:modify-class">
<xsl:with-param name="atts" as="attribute()*">
<xsl:next-match/>
</xsl:with-param>
<xsl:with-param name="add-tokens" select="tokenize(@condition)"/>
<xsl:with-param name="remove-tokens" select="'web'"/>
</xsl:call-template>
</xsl:template>
This minimally invasive, black-box approach isn’t possible in DocBook XSLT 2.0 or TEI XSL.
db:modify-class
template<xsl:template name="db:modify-class" as="attribute()*">
<xsl:param name="atts" as="attribute()*"/>
<xsl:param name="add-tokens" as="xs:string*"/>
<xsl:param name="remove-tokens" as="xs:string*"/>
<xsl:variable name="existing-tokens" as="xs:string*"
select="$atts[name() = 'class'] ! tokenize(.)" />
<xsl:variable name="new-tokens" as="xs:string*"
select="distinct-values(($existing-tokens, $add-tokens)
[not(. = $remove-tokens)])"/>
<xsl:sequence select="$atts[not(name() = 'class')]"/>
<xsl:if test="exists($new-tokens)">
<xsl:attribute name="class" select="$new-tokens" separator=" "/>
</xsl:if>
</xsl:template>
f:modify-class()
function<xsl:function name="f:modify-class" as="attribute(class)?">
<xsl:param name="atts" as="attribute()*"/>
<xsl:param name="add-tokens" as="xs:string*"/>
<xsl:param name="remove-tokens" as="xs:string*"/>
<xsl:variable name="existing-tokens" as="xs:string*"
select="$atts[name() = 'class'] ! tokenize(.)" />
<xsl:variable name="new-tokens" as="xs:string*"
select="distinct-values(($existing-tokens, $add-tokens)
[not(. = $remove-tokens)])"/>
<xsl:sequence select="$atts[not(name() = 'class')]"/>
<xsl:if test="exists($new-tokens)">
<xsl:attribute name="class" select="$new-tokens" separator=" "/>
</xsl:if>
</xsl:function>
Unless meant to be used in XPath expressions, a named template should be preferred because it can accept tunnel parameters.
<xsl:function name="db:new-name" as="xs:string">
<xsl:param name="elt" as="element(*)"/>
<xsl:apply-templates select="$elt" mode="db:new-name"/>
</xsl:function>
The best of both worlds:
I call the xsl:apply-templates
above a “mode hook” and mode="db:new-name"
a “hook mode.”
Taken together, it’s the mhhm approach.
Questions?
tei:isInline()
, a function that accepts an element
as its argument and decides, using 128 xsl:when
branches, can be refactored
in a way so that people can more easily configure in which context a TEI note
is inline or not.