Using XSLT to output Javascript (within a webpage)
Can you use XSLT to output Javascript to a webpage? — let’s find out
The simple answer is “Yes , of course” — Javascript is, in essence text, and XSLT is capable of outputting text strings, irrespective of what the text string actually signifies.
It’s fair to say that whilst you could indeed use XSLT to output Javascript— there are other ways to pull content from XML documents within a website context which you might be better placed to use, such as DOM or Javascript XPath, or potentially even adding data to HTML5 data-attributes.
Regardless, let’s put that point to one side and take a look at some options specifically for using XSLT to output Javascript.
The most useful thing you might want to do is to output a list, an array, of items.
If you’ve not yet seen my previous posts which explore revisiting XSLT for the Modern Web, check these out.
- Beginners XSLT patterns explained — simple key lookup
- What is XSLT and is it still relevant in a website context in 2024?
- XSLT in the web browser — simple transformations on the server
- Using jQuery with XSLT — .ajax(), .ajax() with Promises, .get()
- XSLT with Fetch() API — modern Javascript with/without Async and Await
Making a list of names
You might want to return a list, an array, of items from an XML document.
(Disclaimer — As covered above, there will be better ways to achieve this than XSLT but for the sake of exploring what you can do, lets try this).
Suppose you had an XML document with multiple <name> elements, each used to mark up a single mention of a persons name.
<?xml version="1.0" encoding="UTF-8"?>
...
<name type="pers">Herodotus</name>
...
(As an aside — some interesting Humanities XML files that can be used to experiment with XSLT can be found via the Perseus Digital Library website. A key text that is made available from the Perseus Digital Library is The Histories by Herodotus. Perseus makes this text available under a Creative Commons Attribution-ShareAlike 3.0 United States License. The edition of The Histories is jam packed full of names marked-up using the TEI <name> element so I will use this here.)
To generate a list of such names, you could apply a template like the below, (which would be called with <xsl:call-template>).
This template would firstly establish a <script> HTML element and then iterate through all instances of <name type=”pers”> using an <xsl:for-each>. Within the <xsl:for-each>, the <xsl:value-of select> element then outputs the string value of each element — for example “Herodotus”.
<xsl:template name="persons">
<script type="text/javascript" id="persons">
const personList = {
<xsl:for-each select="//p//name[@type='pers']">
"<xsl:value-of select="."/>",
</xsl:for-each>
};
</script>
</xsl:template>
When the transformation is applied this template then provides something like the below, with the value of each element. The same string value would be expected multiple times, just as persons are mentioned multiple times in a text.
const personList = { "Herodotus", "Io", "Inachus", "Io", "Io", "Europa", .....
That kind of approach could be helpful, depending on what you want to do, however there is an issue here.
However, depending on how the XSLT stylesheet is transformed, you might just end up with the array values being written and displayed to the HTML page as text (for example, if you are appending a XSLT transformation to document.body.appendChild() ) — the output will be presented as text and will be visible.
If you are performing a transformation of a text to HTML within the browser itself and then also output some content to be exported as Javascript, these are two different ways to approach
- Interception: Find a way to extract the specific part of the XSLT transformation which includes the exported data and store this into a variable before the full XSLT transformation is applied in the browser.
- Pre-exporting data to a Javascript file: Rather than applying the exporting stylesheet in the browser, perform the transformation offline first (to a file), then use that file in the website.
1. Interception
Interception: Find a way to extract the specific part of the transformation which includes the exported data before the full transformation is applied in the browser.
In my previous articles, I have used this short pattern (of sorts) to load a XSLT and XML file and apply a transformation. The function loadFile() is a async/await function using Fetch to load a file and return a Javascript promise.
const xsltProcessor = new XSLTProcessor();
const parser = new DOMParser();
loadFile("data/herodotus.xsl").then(data => {
const xsl = parser.parseFromString(data, "application/xml");
xsltProcessor.importStylesheet(xsl);
}).then(loadFile("data/Perseus_text_1999.01.0126.xml").then(data => {
const xml = parser.parseFromString(data, "application/xml");
const fragment = xsltProcessor.transformToFragment(xml, document);
document.body.appendChild(fragment);
}));
There is a clear opportunity here to be able to intercept the output of the transformation before it shows up on the webpage, through inspecting the variable fragment before it is applied to document.body.appendChild().
const xsltProcessor = new XSLTProcessor();
const parser = new DOMParser();
loadFile("data/intercepting.xsl").then(data => {
const xsl = parser.parseFromString(data, "application/xml");
xsltProcessor.importStylesheet(xsl);
}).then(loadFile("data/Perseus_text_1999.01.0126.xml").then(data => {
const xml = parser.parseFromString(data, "application/xml");
const fragment = xsltProcessor.transformToFragment(xml, document);
//intercept!!
const scriptFragment = fragment.getElementById("personsData");
const script = scriptFragment.firstChild.data;
console.log(script);
//now remove the script tag from the transformed output
// that you want on the website
scriptFragment.remove();
document.body.appendChild(fragment);
}));
The console.log() output shows a problem — lots of space and tab escape characters are included.
The simplest way to solve this is to remove the spaces and tabs from the XSLT template we are using to export the data. I reduced the XSLT further from this:
<xsl:template name="persons">
<script type="text/javascript" id="persons">
const personList = {
<xsl:for-each select="//p//name[@type='pers']">
"<xsl:value-of select="."/>",
</xsl:for-each>
};
</script>
</xsl:template>
To this:
<xsl:template name="persons">
<script type="text/javascript" id="personsData">const personList = {<xsl:for-each select="//p//name[@type='pers']">"<xsl:value-of select="."/>",</xsl:for-each>};</script>
</xsl:template>
Not quite as readable but gives a clearer output.
At this point I was treated with a further problem though — the variable ‘const personList’ is not treated within the browser as a Javascript variable — it is just character string.
It is not possible to use ‘const personList’ as a variable.
Is there another way we can get to the data? (At this point there are probably lots of different methods but what I came upon was this).
The simplest way would be to reduce the XSLT template further again to omitt the ‘const personList’ and only give us simply the values separated by a comma.
<xsl:template name="persons">
<script type="text/javascript" id="personsData"><xsl:for-each select="//p//name[@type='pers']"><xsl:value-of select="."/>,</xsl:for-each></script>
</xsl:template>
Then, in the Javascript of the webpage, we use the simple String.split(“,”) method to split the string based on the comma’s, and provide an array.
const xsltProcessor = new XSLTProcessor();
const parser = new DOMParser();
loadFile("data/intercepting.xsl").then(data => {
const xsl = parser.parseFromString(data, "application/xml");
xsltProcessor.importStylesheet(xsl);
}).then(loadFile("data/Perseus_text_1999.01.0126.xml").then(data => {
const xml = parser.parseFromString(data, "application/xml");
const fragment = xsltProcessor.transformToFragment(xml, document);
//intercept!!
const scriptFragment = fragment.getElementById("personsData");
const script = scriptFragment.firstChild.data;
//now split the string into an array, separated by commas
console.log(typeof script);
const personArray = script.split(",");
console.log(personArray);
//now remove the script tag from the transformed output
// that you want on the website
scriptFragment.remove();
document.body.appendChild(fragment);
}));
To note, due to the function scope, the personArray variable is not accessible to the console outside of the function scope.
This method is, of course, a little hacky in approach and may not perform well for values that are not basic text, so probably shouldn’t be relied upon without some degree of sanitising and probably only if you can be reasonably sure of the type of all the strings that would appear in the document.
As an aside, we can now also answer the question of how many name elements (mentions of persons names) are there in Herodotus’ The Histories — the answer is a staggering 6203! — Bear that point in mind as we look at another way of passing data to Javascript!
2. Pre-exporting data to a Javascript file
Pre-exporting: Rather than applying the exporting stylesheet in the browser, perform the transformation offline first, to a javascript file, then use that file in the website.
This method has massive performance improvements and uses XSLT as an offline tool to generate Javascript — but it could be useful in situations where the data is not going to change and can simply be ‘called up’ when needed.
This is also an approach that may suit a tool to export information based on a selection or predicate and where there is no need to generate the data again.
Here is an example, again we will extract <names> and instead of putting them into the webage itself
Pre-export — making the Javascript
js-export.xsl
We make a simple XSLT stylesheet such as the below, which prevents the output of elements within element <TEI.2>, outputs text and gets the value of all <name type=”pers”> elements with a trailing comma added.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns="http://www.tei-c.org/ns/1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8" indent="yes"/>
<xsl:template match="TEI.2"/>
<xsl:template match="/">
<xsl:call-template name="persons"/>
</xsl:template>
<xsl:template name="persons">
<xsl:text>const persons = [</xsl:text>
<xsl:for-each select="//p//name[@type='pers']">"<xsl:value-of select="."/>",</xsl:for-each>
<xsl:text>];</xsl:text>
</xsl:template>
</xsl:stylesheet>
We could call this XSLT not from the webpage but rather apply the transformation via a web server by adding the old-fashioned <?xml-stylesheet at the top of the XML document.
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="js-export.xsl"?>
...
Applying the transformation (I did this within the browser via local webserver) provides the following text as output.
const persons = ["Herodotus","Io","Inachus","Io","Io","Europa","Medea","Io","Alexandrus","Priam","Helen",
This isn’t yet Javascript, it’s text — so now copy and paste this text into a new document and name it js-export.js.
Using on the Webpage
Assuming we have saved the js-export.js file in a sensible place, we have to do very little to use the Javascript array in our webpage, we can simply load the .js file in a conventional way using the <script> element.
...
<script type="text/javascript" src="data/js-export.js"></script>
<script type="text/javascript">
(function(){
window.addEventListener("load", () => {console.log(persons)}, false);
})();
</script>
...
What can we do next?
Making a simple list like this helps us to think of other ways at looking at a text.
- The list of names is not a list of unique names — many persons are included multiple times. We might want to use Javascript Array methods to refine this array to unique values or use Javascript Symbols.
- There are lots of other names — names of places, ethnic groups — we might want to have a whole set of arrays for all these different types of names.
- We could look at understanding if there is a connection between named persons, given that some names may appear together in the text multiple times.
- We could use this list of names to populate a HTML search or select box, to explore the text in a non-linear fashion by selecting a name.
- We could use this list of names with XPath to extract all the paragraph text around each name and show to the user the context which surrounds the mention of each person in the book.
- We might want to make an associative array to record names with the number of times the names appear — doing this ‘on the fly’ for each user of a website could be very inefficient — preparing a array off-line would be far more performant.
- We might want to make Javascript Objects rather than Arrays — it would be possible to have an Object for each Book, with Arrays for all the different types of names encountered in each Book.
I hope you have found this article interesting and shows the workarounds that could be used when working with XSLT and Javascript.