xml – how to fill text templates using xslt – Education Career Blog

I have an XML file with information, for example:

<letter>
  <name>Test</name>
  <age>20</age>
  <me>Me</me>
</letter>

And then I have an text template like:

Dear $name,

some text with other variables like $age or $name again

greatings $me

When using xslt to transform the XML to the plain text letter I can use something like:

<xsl:text>Dear </xsl:text><xsl:value-of select="name"/><xsl:text>

some text with other variables like </xsl:text>
<xsl:value-of select="age"/><xsl:text> or </xsl:text>
<xsl:value-of select="name"/><xsl:text> again

greatings </xsl:text><xsl:value-of select="me"/>

But when I get more and more variables and more text this becomes a nightmare to enter and to maintain.

Is there some way to do this in a cleaner way using xslt? I would prefer if I could just use the text template I used as an example above and have $name and $age replaced with the correct values.

,

This stylesheet:

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:my="my">
    <xsl:output method="text"/>
    <xsl:preserve-space elements="my:layout"/>
    <my:layout>Dear <name/>,

some text with other variables like <age/> or <name/> again

greatings <me/></my:layout>
    <xsl:variable name="vData" select="/"/>
    <xsl:template match="/">
        <xsl:apply-templates select="document('')/*/my:layout/node()"/>
    </xsl:template>
    <xsl:template match="*/*">
        <xsl:value-of select="$vData//*name()=name(current())"/>
    </xsl:template>
</xsl:stylesheet>

Output:

Dear Test,

some text with other variables like 20 or Test again

greatings Me

Note: For more complex population pattern (i.e. iteration), check this posts: Sitemesh like functionality with XSLT? and XSLT Layouts With Dynamic Content Region

,

One possible solution would be to change your template file to be an XML configuration, like this:

<?xml version="1.0" encoding="UTF-8"?>
<template>
    Dear <name/>,

    some text with other variables like <age/> or <name/> again

    greatings <me/>
</template>

Assuming that the above XML template is named template.xml and in the same directory as the XSLT below:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl"
    exclude-result-prefixes="xs xd"
    version="1.0">
<xsl:output method="text"/>

<!--Load the template document as a variable -->
<xsl:variable name="templateFile" select="document('template.xml')" />
<!--Load the current document in a variable, so that we can reference this file from within template matches on the template.xml content-->    
<xsl:variable name="letter" select="/*" />    

<!--When this stylesheet is invoked, apply templates for the templateFile content (everything inside the template element) -->    
<xsl:template match="/">
    <xsl:apply-templates select="$templateFile/*/node()" />
</xsl:template>

 <!--Match on any of the template placeholder elements and replace with the value from it's corresponding letter document-->
<xsl:template match="template/*">
    <!--set the local-name of the current element as a variable, so that we can use it in the expression below -->
    <xsl:variable name="templateElementName" select="local-name(.)" />
    <!--Find the corresponding letter element that matches this template element placeholder-->
    <xsl:value-of select="$letter/*local-name()=$templateElementName" />
</xsl:template>

<!--standard identity template that copies content forward-->
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()" />
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

When the XSLT is run against the sample XML file, it produces the following output:

Dear Test,

some text with other variables like 20 or Test again

greatings Me

As @Tomalak pointed out, unmatched placeholder elements would be removed from the output. If you wanted to preserve them, to make it apparent that the XML file did not have a match for placeholder items in the template, you could change the template that matches on the template placeholder elements like this:

 <!--Match on any of the template placeholder elements and replace with the value from it's corresponding letter document-->
<xsl:template match="template/*">
    <!--set the local-name of the current element as a variable, so that we can use it in the expression below -->
    <xsl:variable name="templateElementName" select="local-name(.)" />
    <!--Find the corresponding letter element that matches this template element placeholder-->
    <xsl:variable name="replacementValue" select="$letter/*local-name()=$templateElementName" />
    <xsl:choose>
        <xsl:when test="$replacementValue">
            <xsl:value-of select="$replacementValue" />
        </xsl:when>
        <xsl:otherwise>
            <xsl:text>$</xsl:text>
            <xsl:value-of select="local-name()"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

If there was an unmatched placeholder element, <foo/> for example, then it would appear in the text output as $foo.

,

You can do this:

<xsl:stylesheet 
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
  <xsl:output method="text" />

  <xsl:variable name="placeholderText"
>Dear $name,

some text with other variables like $age or $name again,
the $undefined will not be replaced.

greatings $me</xsl:variable>

  <xsl:template match="letter">
    <xsl:call-template name="expand-placeholders">
      <xsl:with-param name="text"   select="$placeholderText" /> 
      <xsl:with-param name="values" select="*" /> 
    </xsl:call-template>
  </xsl:template>

  <xsl:template name="expand-placeholders">
    <xsl:param name="text"   select="''" />
    <xsl:param name="values" select="false()" />
    <xsl:choose>
      <xsl:when test="contains($text, '$') and $values">
        <xsl:variable name="head" select="substring-before($text, '$')" />
        <xsl:variable name="curr" select="substring-after($text, '$')" />
        <!-- find the longest matching value name... -->
        <xsl:variable name="valName">
          <xsl:for-each select="$valuesstarts-with($curr, name())">
            <xsl:sort select="string-length(name())" data-type="number" order="descending" />
            <xsl:if test="position() = 1">
              <xsl:value-of select="name()" />
            </xsl:if>
          </xsl:for-each>
        </xsl:variable>
        <!-- ... and select the appropriate placeholder element -->
        <xsl:variable name="val" select="$valuesname() = $valName1" />
        <xsl:variable name="tail">
          <xsl:choose>
            <xsl:when test="$val">
              <xsl:value-of select="substring-after($curr, name($val))" />
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$curr" />
            </xsl:otherwise>
          </xsl:choose>
        </xsl:variable>

        <xsl:value-of select="$head" />
        <xsl:if test="not($val)">$</xsl:if>
        <xsl:value-of select="$val" />  

        <xsl:call-template name="expand-placeholders">
          <xsl:with-param name="text"   select="$tail" /> 
          <xsl:with-param name="values" select="$values" /> 
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$text" />  
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Output against your XML sample:

Dear Test,

some text with other variables like 20 or Test again,
the $undefined will not be replaced.

greatings Me

,

As @Mads Hansen suggests, if you can use a XML based template this can be solved in an easier, and imo better way.

Here’s a solution with a XML template as input.

XML template as input:

<?xml version="1.0" encoding="UTF-8"?>
<letter>Dear <name/>,

some text with other variables like <age/> or <name/> again

greatings <me/></letter>

XSLT:

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

  <xsl:output method="text"/>
  <xsl:preserve-space elements="letter"/>

  <vars:letter>
    <name>Test</name>
    <age>20</age>
    <me>Me</me>
  </vars:letter>

  <xsl:template match="name|age|me">
    <xsl:variable name="name" select="local-name()"/>
    <xsl:value-of select="document('')/*/vars:letter/*local-name() = $name"/>
  </xsl:template>

</xsl:stylesheet>

Output:

Dear Test,

some text with other variables like 20 or Test again

greatings Me

If you use the XML template as input, this is one possible way to solve it. The different values to fill you template with could of course be fetched from an external XML file with fn:document() aswell:

<xsl:value-of select="document('path/to/file.xml')/letter/*local-name() = $name"/>

Update: As @Tomlak commented the above solution isn’t very flexible so here is an updated one:

XSLT:

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

  <xsl:output method="text"/>
  <xsl:preserve-space elements="letter"/>

  <vars:letter>
    <name>Test</name>
    <age>20</age>
    <me>Me</me>
  </vars:letter>

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

  <xsl:template match="letter/*">
    <xsl:variable name="name" select="local-name()"/>
    <xsl:variable name="content" select="document('')/*/vars:letter/*local-name() = $name"/>
    <xsl:value-of select="$content"/>
    <xsl:if test="not($content)">
      <xsl:message>
        <xsl:text>Found unknown variable in template: </xsl:text>
        <xsl:value-of select="concat('&lt;', local-name(), '/&gt;')" disable-output-escaping="yes"/>
      </xsl:message>
      <xsl:value-of select="concat('&lt;', local-name(), '/&gt;')"/>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

Note that this is a different approach since it uses the template as input document and not the list of variables. Why? To me it makes more sense to use the letter template as input since it’s actully that document you transform and get as output.

Leave a Comment