Apply classes to selected XHTML elements based on content I wish to take an XHTML source document (e.g. demo.in.xhtml below) and add class attributes to every td element whose content is one of the following six strings: A1, A2, B1, B2, C1, or C2. The corresponding classes will be mytable-a1, mytable-a2, mytable-b1, mytable-b2, mytable-c1, mytable-c2. Any other td elements in the source document should be kept as they are.

demo.xsl performs this as desired, but is not DRY: it uses a six-line xsl:template element per class attribute. How can I refactor this so as to remove the repetition, or generally make demo.xsl more concise and readable?

I am using the following command on Debian Jessie GNU/Linux to perform the transformation:

saxonb-xslt -s:demo.in.xhtml -xsl:demo.xsl -o:demo.out.xhtml


demo.in.xhtml

&lt;?xml version="1.0" encoding="utf-8" ?&gt;
&lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
&lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB"&gt;
&lt;head&gt;
    &lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8" /&gt;
    &lt;title&gt;Title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;table class="mytable"&gt;
        &lt;tr&gt;
            &lt;td&gt;A1&lt;/td&gt;
            &lt;td&gt;A2&lt;/td&gt;
            &lt;td&gt;B1&lt;/td&gt;
            &lt;td&gt;B2&lt;/td&gt;
            &lt;td&gt;C1&lt;/td&gt;
            &lt;td&gt;C2&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/table&gt;
&lt;/body&gt;
&lt;/html&gt;


demo.xsl

&lt;?xml version="1.0"?&gt;
&lt;xsl:stylesheet
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        version="2.0"
        xpath-default-namespace="http://www.w3.org/1999/xhtml"&gt;
    &lt;xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/&gt;

    &lt;!--
    Inspiration
    URL: http://stackoverflow.com/a/2973585
    Author: Mads Hansen
    License: https://creativecommons.org/licenses/by-sa/3.0/
    --&gt;

    &lt;!--Custom behavior for matched nodes--&gt;
    &lt;xsl:template match="//td[.='A1']"&gt;
        &lt;xsl:copy&gt;
            &lt;xsl:attribute name="class"&gt;mytable-a1&lt;/xsl:attribute&gt;
            &lt;xsl:apply-templates select="@*|node()"/&gt;
        &lt;/xsl:copy&gt;
    &lt;/xsl:template&gt;

    &lt;xsl:template match="//td[.='A2']"&gt;
        &lt;xsl:copy&gt;
            &lt;xsl:attribute name="class"&gt;mytable-a2&lt;/xsl:attribute&gt;
            &lt;xsl:apply-templates select="@*|node()"/&gt;
        &lt;/xsl:copy&gt;
    &lt;/xsl:template&gt;

    &lt;xsl:template match="//td[.='B1']"&gt;
        &lt;xsl:copy&gt;
            &lt;xsl:attribute name="class"&gt;mytable-b1&lt;/xsl:attribute&gt;
            &lt;xsl:apply-templates select="@*|node()"/&gt;
        &lt;/xsl:copy&gt;
    &lt;/xsl:template&gt;

    &lt;xsl:template match="//td[.='B2']"&gt;
        &lt;xsl:copy&gt;
            &lt;xsl:attribute name="class"&gt;mytable-b2&lt;/xsl:attribute&gt;
            &lt;xsl:apply-templates select="@*|node()"/&gt;
        &lt;/xsl:copy&gt;
    &lt;/xsl:template&gt;

    &lt;xsl:template match="//td[.='C1']"&gt;
        &lt;xsl:copy&gt;
            &lt;xsl:attribute name="class"&gt;mytable-c1&lt;/xsl:attribute&gt;
            &lt;xsl:apply-templates select="@*|node()"/&gt;
        &lt;/xsl:copy&gt;
    &lt;/xsl:template&gt;

    &lt;xsl:template match="//td[.='C2']"&gt;
        &lt;xsl:copy&gt;
            &lt;xsl:attribute name="class"&gt;mytable-c2&lt;/xsl:attribute&gt;
            &lt;xsl:apply-templates select="@*|node()"/&gt;
        &lt;/xsl:copy&gt;
    &lt;/xsl:template&gt;

    &lt;!--Identity template (copies input into output) --&gt;
    &lt;xsl:template match="@*|node()"&gt;
        &lt;xsl:copy&gt;
            &lt;xsl:apply-templates select="@*|node()"/&gt;
        &lt;/xsl:copy&gt;
    &lt;/xsl:template&gt;

&lt;/xsl:stylesheet&gt;
You could just create the class based on the value of the td.

Input (I added a td that should not be modified by the stylesheet.)

&lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB"&gt;
    &lt;head&gt;
        &lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8" /&gt;
        &lt;title&gt;Title&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;table class="mytable"&gt;
            &lt;tr&gt;
                &lt;td&gt;A1&lt;/td&gt;
                &lt;td&gt;A2&lt;/td&gt;
                &lt;td&gt;B1&lt;/td&gt;
                &lt;td&gt;B2&lt;/td&gt;
                &lt;td&gt;C1&lt;/td&gt;
                &lt;td&gt;C2&lt;/td&gt;
                &lt;td&gt;ignore me&lt;/td&gt;
            &lt;/tr&gt;
        &lt;/table&gt;
    &lt;/body&gt;
&lt;/html&gt;


XSLT 2.0

&lt;xsl:stylesheet version="2.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xpath-default-namespace="http://www.w3.org/1999/xhtml"&gt;
  &lt;xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/&gt;
  &lt;xsl:strip-space elements="*"/&gt;

  &lt;xsl:param name="tds" select="('A2','B1','B2','C1','C2')"/&gt;

  &lt;xsl:template match="@*|node()"&gt;
    &lt;xsl:copy&gt;
      &lt;xsl:apply-templates select="@*|node()"/&gt;
    &lt;/xsl:copy&gt;
  &lt;/xsl:template&gt;

  &lt;xsl:template match="td[normalize-space()=$tds]"&gt;
    &lt;xsl:copy&gt;
      &lt;xsl:attribute name="class" 
        select="concat('mytable-',lower-case(normalize-space()))"/&gt;
      &lt;xsl:apply-templates select="@*|node()"/&gt;
    &lt;/xsl:copy&gt;
  &lt;/xsl:template&gt;

&lt;/xsl:stylesheet&gt;


Output

&lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB"&gt;
   &lt;head&gt;
      &lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8"/&gt;
      &lt;title&gt;Title&lt;/title&gt;
   &lt;/head&gt;
   &lt;body&gt;
      &lt;table class="mytable"&gt;
         &lt;tr&gt;
            &lt;td class="foo"&gt;A1&lt;/td&gt;
            &lt;td class="mytable-a2"&gt;A2&lt;/td&gt;
            &lt;td class="mytable-b1"&gt;B1&lt;/td&gt;
            &lt;td class="mytable-b2"&gt;B2&lt;/td&gt;
            &lt;td class="mytable-c1"&gt;C1&lt;/td&gt;
            &lt;td class="mytable-c2"&gt;C2&lt;/td&gt;
            &lt;td&gt;ignore me&lt;/td&gt;
         &lt;/tr&gt;
      &lt;/table&gt;
   &lt;/body&gt;
&lt;/html&gt;


Notes:


In my match, I didn't use the // abbreviated syntax (match="//td"); I just used match="td".
I used xsl:param instead of using the sequence ('A2','B1','B2','C1','C2') directly in the match so that it could be passed in at runtime. This would allow you to change which td elements were modified without changing the actual code.
Try to use a select attribute on xsl:attribute, even when outputting plain text (&lt;xsl:attribute name="class" select="'mytable-c2'"/&gt;). This prevents creating unnecessary RTF's (result tree fragments).
I use normalize-space() when creating the class to strip any leading/trailing whitespace in the text() node.
After creating the class attribute, we do an xsl:apply-templates on @*. If there is an existing class attribute, it will overwrite what we just created. If this is a possibility, I'd do an xsl:apply-templates on @*, then create the new class attribute (overwrites the existing class or you can concat the new and old), and then finally do the xsl:apply-templates on node().