How to put CDATA into script tag in XSLT
You have an XSL template for the website you are working on and you would like to embed some JavaScript in the markup. You care so you would like to keep the XHTML output valid. Easy enough - all it takes is wrapping the actual JavaScript code with CDATA. To make it safe you would also add JS comments around CDATA and move on. But is it really that easy with XSL? Let's have a look.
Short story: JavaScript template
Here is the working solution which allows for safe embedding JavaScript on XHTML pages created with XSL templates. This is the fruit of rather lengthy trial and error session.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output
method="xml"
encoding="utf-8"
doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
indent="yes"
cdata-section-elements=""
omit-xml-declaration="yes" />
<xsl:template name="javascript">
<xsl:param name="code"/>
<xsl:text disable-output-escaping="yes">
<script type="text/javascript">
/* <![CDATA[ */
</xsl:text>
<xsl:value-of select="$code" disable-output-escaping="yes"/>
<xsl:text disable-output-escaping="yes">
/* ]]> */
</script>
</xsl:text>
</xsl:template>
<xsl:template match="/">
<html>
<body>
<xsl:call-template name="javascript">
<xsl:with-param name="code">
<![CDATA[
if (1 > 2) {}
]]>
</xsl:with-param>
</xsl:call-template>
</body>
</html>
</xsl:template>
</xsl:transform>
The above example will output:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<script type="text/javascript">
/* <![CDATA[ */
if (1 > 2) {}
/* ]]> */
</script>
</body>
</html>
Long story: can this be simpler?
Well, let's have a look at a few different ways this problem can be approached and the results. For brevity I am showing only the script
element.
Add CDATA directly in the script tag
Example 1
<script type="text/javascript">
/* <![CDATA[ */
if (1 < 2) {}
/* ]]> */
</script>
Output 1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<script type="text/javascript">
/* */
if (1 < 2) {}
/* */
</script>
</body>
</html>
What happened?
CDATA got stripped off and the <
sign got replaced with <
. Of course! CDATA as an XML specific construct is interpreted in XSL as well. And that helps - try to remove CDATA from the following example and you will see an error. Good old <
is the source of trouble here - without CDATA around, it makes XML invalid. The interpreted CDATA should stay then but another CDATA is required in the output. Let's try another approach.
cdata-section-elements
xls:output
has an option which allows to wrap content of certain tags with CDATA. However even if you set it up to wrap up content of script tags with CDATA (the rest of the code like in example 1) you will not get what you need. Let's have a look.
Example 2
<xsl:output
<!-- all the other attributes we have seen already -->
cdata-section-elements="script"
/>
Output 2
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<script type="text/javascript">
<![CDATA[
/* */
if (1 < 2) {}
/* */
]]>
</script>
</body>
</html>
What happened?
So... the script tag got its CDATA, but of course not safely escaped. And the CDATA that we have put into the XSL template is gone anyway. Similarly to what we have concluded above - as a XML construct it is interpreted in XSL as well. No good.
CDATA as text
How about creating CDATA explicitly as a text? Maybe with disabled output escaping that would work? Let's see.
Example 3
<script type="text/javascript">
<xsl:text disable-output-escaping="yes">
/* <![CDATA[ */
</xsl:text>
<![CDATA[
if (1 < 2) {}
]]>
<xsl:text disable-output-escaping="yes">
/* ]]> */
</xsl:text>
</script>
Output 3
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<script type="text/javascript">
/* <![CDATA[ */
if (1 < 2) {}
/* ]]> */
</script>
</body>
</html>
What happened?
That's definitely some progress, CDATA is in place, neatly commented out. Now the problem is <
in the JavaScript code. We would prefer that to be <
again.
JavaScript code as xsl:text
Why not to put the whole JavaScript code into another xsl:text
tag with disable escaping?
Example 4
<script type="text/javascript">
<xsl:text disable-output-escaping="yes">
/* <![CDATA[ */
</xsl:text>
<xsl:text disable-output-escaping="yes">
<![CDATA[
if (1 < 2) {}
]]>
</xsl:text>
<xsl:text disable-output-escaping="yes">
/* ]]> */
</xsl:text>
</script>
Output 4
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<script type="text/javascript">
/* <![CDATA[ */
if (1 < 2) {}
/* ]]> */
</script>
</body>
</html>
What happened?
Is it a dead end or maybe xsl:text
element is not the best choice after all?
JavaScript code as xsl:value-of
We can see we are on to somthing. It has to be something with disable-output-escaping attribute. Another such element is xsl:value-of
. Assuming that the JavaScript code exists only in the template and not in the processed XML structure, xsl:value-of
needs an xsl:variable
to read value from.
Example 5
<xsl:variable name="s1">
<![CDATA[
if (1 < 2) {}
]]>
</xsl:variable>
<script type="text/javascript">
<xsl:text disable-output-escaping="yes">
/* <![CDATA[ */
</xsl:text>
<xsl:value-of select="$s1" disable-output-escaping="yes"/>
<xsl:text disable-output-escaping="yes">
/* ]]> */
</xsl:text>
</script>
Output 5
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<script type="text/javascript">
/* <![CDATA[ */
if (1 < 2) {}
/* ]]> */
</script>
</body>
</html>
What happened?
It worked! It is not very flexible solution, though. Defining variable for every JavaScript in the template seems to be acceptable only if necessary. Luckily xsl:value-of
can also read value of a parameter passed to an xsl:template
. That's how I got to the template you can find at the top of the page.
Thank you for reading. I hope you found it useful.
Have fun,
Jacek