Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call XSLT template and save all output to variable

Tags:

xslt-1.0

I want to call and template and save the output to a variable. I want to save ALL of the output including HTML markup, but that's not what happens.

For example, take this simple XSLT:

<xsl:call-template name="demonstration">

<xsl:template name="demonstration">
    <p>Just a test</p>
</xsl:template>

This simple template will output <p>Just a test</p> to the HTML page. If I view the page source, I see that (of course). Now take this example using the same template, but instead of just outputting to HTML I want to save the output to a variable:

<xsl:variable name="test">
    <xsl:call-template name="demonstration">
</xsl:variable>
<xsl:value-of select="$test"/>


<xsl:template name="demonstration">
    <p>Just a test</p>
</xsl:template>

Viewing the variable shows that the only output now is Just a test.

Where did the HTML markup go? I'd like to be able to save the output of a call-template to a variable, but I need the HTML markup too.

Is there a way of avoiding the loss of HTML tags when calling a template like this? Is there a different way of writing my template? Maybe a setting I'm missing? I've tried disable-escape-encoding, but that makes no difference (in Safari at least).

I prefer to use the template for both needs: I'd like to be able to just call it and have the output in an HTML page for viewing. I'd also like to be able to wrap the call in a variable, but it's important that both call methods including all of the HTML tags/markup as specificed in the template.

EDIT:

I've tried both of the posted answers so far, but copy-of gives me the same result as value-of. Actually, I'm not using value-of, I was only showing how to duplicate the problem. Here is a more thorough explanation of what I'm trying to do.

Have a stylesheet that is used to transform a rather large XML received from a REST response. The stylesheet has its output method set for html.

One of the templates in this stylesheet does a lot of decision-making on how to display as many as 4 rows of data in a table. There are 2 columns, one is for a label, the other is for data.

The decision-making that's done includes what the text of the label is and what class - sometimes the label is green, sometimes its red, etc. The data column can contain text and/or numbers. Some of the text might be bold, some might be colorized. There are a bunch of prerequisites that determine these attributes.

If I were displaying a details of a single item, I'd be done with this template, but one of these items can have a multitude of attributes chosen. For example, there might be a size and a color. Depending on which size or color a user chooses, the price can be different, the particular item might be out of stock, if it has a different price then it will have a different savings to the user. Different items might have free shipping, or might be available for preorder only. There are LOTS of attributes.

And so I have a template that anylizes all of these prerequisites and constructs a <tr><td></td><td></td></tr> filled with firly simple text and data derived by some not-so-simple logic.

I've modularized this template so that I can call it for any one of the items. It's paramertized so I can specify the label text, data, class, and a couple of other things.

When the web page loads, the default/main item is displayed along with general info - like the range of prices and a range of savings, etc.

But when a user selects a color or size, these table rows need to be updated with the correct data. I've already processed the data from the XML and it would be very costly to make another server request, so what I have done is created a JSON string array containing all of the data for all of the different kinds of items. I save this JSON in the value attribute of a hidden input control. There's other ways of doing it, yes, but this works and it seems manageable to me.

Since the logic to create these table rows is in the stylesheet on the server, I perform all this logic on all items and then pass the calculated strings to the client. Here is an example of what this looks like:

<input id="hfData" type="hidden" value="[
{"ProductID": "00001", "Color": "Beige", "Size": "14"},
{"ProductID": "00002", "Color": "Black", "Size": "14"},
{"ProductID": "00003", "Color": "Blue", "Size": "10"},
{"ProductID": "00004", "Color": "Pink", "Size": "10"},
{"ProductID": "00005", "Color": "Yellow", "Size": "10"}
]"
/>

I then have a small JQuery script that is triggered any time the user changes a dropdownlist selection, or changes an attribute. The script parses the above JSON and determines what the currently configured ProductID is.

Note: Since the value attribute of the input control would not be valid if I had double quotes scattered through it like this, they are actually all &quot;. I'm showing double quotes here to make it easy to look at.

Once the ProductID is determined, the page gets updated with lots of different details as described earlier in this edit. These details come from yet another JSON object I've created - again, since all of the product details are known in the XSLT Stylesheet, that is where I am creating the JSON string array.

<input id="hfDetails" type="hidden" value="[
{"ProductID": "00001", "ListPrice": "<tr class="collapsed"><td class="datalabel">List Price:</td><td class="datainfo"></td></tr>", "YourPrice": "<tr><td class="datalabel">Price:</td><td class="datainfo">$23.99 & is elegible for <span class="freeshipping">FREE</span> shipping <a href="#">Details</a></td></tr>"},

... etc, etc, etc ...

]"
/>

As you can see, the hidden input just above has a value attribute containing a JSON object which contains HTML markup. I've seen this work, but in order to get JSON to work correctly I have to escape all of the tags. Ampersands have to be &amp;, then there's &lt;, &gt; and also escaping all quotes with a backslash. I've coded it all that way and it works - albeit nasty to look at, it works and it prevents me from having to make round trips to the server - it prevents me from putting all the logic to create these strings on the client side.

None of this has anything to do with the problem I'm having, but I'm hoping to get rid of all the comments from JSON and XSL/XML professors who challenge me for having JSON and XSLT (or HTML) in a single sentence...

Now, I hope I can show (very simply) the precise problem I am having. To start, it has really VERY LITTLE to do with JSON. It has virtually nothing to do with using xsl:value-of over xsl:copy-of.

Here is where I create a hidden input field with the value attribute containing a JSON string:

<input>
  <xsl:attribute name="id">
    <xsl:value-of select="$id"/>
  </xsl:attribute>
  <xsl:attribute name="type">
    <xsl:text>hidden</xsl:text>
  </xsl:attribute>
  <xsl:attribute name="runat">
    <xsl:text>server</xsl:text>
  </xsl:attribute>
  <xsl:attribute name="value">
    <xsl:text>[</xsl:text>

    <xsl:for-each select="Items/Item">
          <xsl:call-template name="JSONItemDetails">
            <xsl:with-param name="offerNode" select="Offers"/>
            <xsl:with-param name="lastitem" select="position() = last()"/>
          </xsl:call-template>
    </xsl:for-each>

    <xsl:text>]</xsl:text>
  </xsl:attribute>
</input>


<xsl:template name="JSONItemDetails">
    <xsl:param name="offerNode" select="."/>
    <xsl:param name="attributesNode" select="ItemAttributes"/>
    <xsl:param name="listprice" select="0"/>
    <xsl:param name="lastitem" select="false()"/>

    <!-- Product ID -->
    <xsl:text>{</xsl:text>
    <xsl:text>&quot;ProductID&quot;: </xsl:text>
    <xsl:text>&quot;</xsl:text>
    <xsl:value-of select="./ProductID"/>
    <xsl:text>&quot;,</xsl:text>

    <xsl:for-each select="msxml:node-set($offerNode)">

        <!-- Title -->
        <xsl:text>&quot;Title&quot;: </xsl:text>
        <xsl:text>&quot;</xsl:text>
        <xsl:call-template name="escapeQuote">
            <xsl:with-param name="pText">
            <xsl:call-template name="title">
                <xsl:with-param name="node" select="$attributesNode" />
            </xsl:call-template>
            </xsl:with-param>
        </xsl:call-template>
        <xsl:text>&quot;,</xsl:text>

        <!-- List Price -->
        <xsl:text>&quot;ListPrice&quot;: </xsl:text>
        <xsl:text>&quot;</xsl:text>
        <xsl:call-template name="escapeQuote">
            <xsl:with-param name="pText">
            <xsl:call-template name="DataTableRow">
                <xsl:with-param name="label" select="'List Price:'" />
                <xsl:with-param name="data" select="./Price/FormattedPrice" />
                <xsl:with-param name="dataid" select="'listprice'" />
            </xsl:call-template>
            </xsl:with-param>
        </xsl:call-template>
        <xsl:text>&quot;</xsl:text>

    </xsl:for-each>

    <xsl:text>}</xsl:text>
    <xsl:if test="$lastitem != true()">
        <xsl:text>,</xsl:text>
    </xsl:if>

The DataTableRow template does a lot of things and which mostly provide consistency, but also clean up all the stray HTML tags used to create rows and columns. Here is that template.

<xsl:template name="DataTableRow">
    <xsl:param name="label" select="''"/>
    <xsl:param name="data" select="''"/>
    <xsl:param name="dataid" select="''"/>
    <xsl:param name="concat" select="''"/>
    <xsl:param name="class" select="''"/>

    <tr>
        <xsl:choose>
        <xsl:when test="$data = not(string(.))">
          <xsl:attribute name="style">
            <xsl:text>display:none</xsl:text>
          </xsl:attribute>
        </xsl:when>
        <xsl:otherwise>
           <xsl:attribute name="style">
           <xsl:text>display:block</xsl:text>
        </xsl:attribute>
        </xsl:otherwise>
        </xsl:choose>

        <!-- Label Column -->
        <td>
            <div>
                <xsl:choose>
                <xsl:when test="$class = 'bigmaroon'">
                    <xsl:attribute name="class">
                        <xsl:text>datalabel maroon</xsl:text>
                    </xsl:attribute>
               </xsl:when>
               <xsl:otherwise>
                    <xsl:attribute name="class">
                       <xsl:text>datalabel</xsl:text>
                    </xsl:attribute>
               </xsl:otherwise>
               </xsl:choose>
               <xsl:value-of select="$label"/>
           </div>
        </td>

        <!-- Data Column -->
        <td class="datainfo">
            <xsl:attribute name="id">
                <xsl:value-of select="$dataid"/>
            </xsl:attribute>

            <xsl:choose>
            <xsl:when test="$class = 'strike'">
                <strike>
                    <xsl:value-of select="$data" />
                </strike>
                <xsl:value-of select="$concat"/>
            </xsl:when>
            <xsl:when test="$class = 'maroon'">
                <span class="maroon">
                    <xsl:value-of select="$data" />
                </span>
                <xsl:value-of select="$concat"/>
            </xsl:when>
            <xsl:when test="$class = 'bigmaroon'">
                <span class="bigmaroon">
                    <xsl:value-of select="$data" />
                </span>
                <xsl:value-of select="$concat"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$data" />
                <xsl:value-of select="$concat" />
            </xsl:otherwise>
            </xsl:choose>
        </td>
    </tr>

The DataTableRow template has a bit of logic that would be messy to do everytime I needed to write one of these rows. First off, All of these rows have to exist in the HTML. If there is no data, then they are set with a style of display:none. This is important since I won't be able to fill them in with data from my JQuery script unless there is a valid selector...

Problem is, after all of this, DataTableRow works just fine when it is called directly like this:

<xsl:call-template name="DataTableRow"/>

In the above call, the HTML tags are all output to my web page. They look fine, their classes and styles are correct. I can call it like this from anywhere in my template and it works just fine. I'm sure there is a bug here or there, and I might see the light on a better/more efficient way of coding this, but it basically works and works well for HTML.

THE PROBLEM: I am unable to get any of the HTML tags when I build my hidden input fields above. The only value that gets stored in my JSON strings are the innerhtml. DataTableRow is called from JSONItemDetails template and instead of getting a result of:

<tr><td>Some Label</td><td>Some Data</td></tr>

I get a result of

Some LabelSome Data

This was an awfult lot of information to ask such a simple question, but I've been getting responses that seem to hint I'm not doing things right if I'm dealing with JSON within XSLT/HTML.

Can anyone help with my problem? Why are the HTML tags being stripped from the output of DataTableRow when I call it in JSONItemDetails to create my JSON string?

EDIT #2:

I've got several things going on with my code that have been causing this problem of HTML tags being stripped. Am finally getting a handle on what's been causing the problem and now I've been trying to figure out how to resolve the issue. Here are additional notes.

Am able to confirm, xsl:value-of is stripping my HTML tags from the output of a template call. xsl:copy-of shows me the tags I expect. This is a big help for me in understanding where the problem is at, but it also allows me to identify other problems.

  1. xsl-copy cannot be used to output under the tag's value= attribute. I understand why, though I'm clueless of how to handle this problem if I proceed in the direction I was going.
  2. Even if I figure out a solution to #1, I have to figure out how to escape the double quotes in my HTML markup. Ie: A double quote isn't valid within the value= attribute. I understand that an apostrophe would work, but these double quotes are created from using xsl:attribute under certain tags to specify a class or occassional style. I have a template that escapes double quotes with a backslash, but calling it also strips out my HTML tags and I can't see why - so I don't know how to fix it. I'll post the code for this template below.

If I had a workaround for the above 2 problems I could proceed in the direction I've been going, but I'm open to hearing advice on why I shouldn't be mixing data with display in my JSON.

I mix the two because I don't like the idea of putting the display logic in my JQuery script. I'd like my script to remain ignorant of this logic. It's simple in JQuery using these JSON objects to drill down to a ProductID and replace a table row like this:

        var details = $.parseJSON($("#[id*='hfDetails']").val());

        var filteredDetails = $.grep(details, function (n) {
            return n.ProductID == ProductID;
        });

        if (filteredDetails.length > 0) {
            var product = filteredDetails[0];

            $("div#title").html(product.Title);

            $("td#listprice").parent().html(product.ListPrice);
            $("td#price").parent().html(product.Price);
            $("td#yousave").parent().html(product.YouSave);
        }

If I remove display from my JSON strings as Dimitre is advising, all the sudden I have to put a lot of logic in my jquery script to not only provide <tr> and <td> formatting (classes), but also logic to wrap the actual data, like <strike, <strong>, color specifications and font sizes or even whether a particular table row is a display:block or a display:none. I absolutely do not want to code any of that logic in JQuery script on the client.

It's a very simple xsl:for-each in my XSLT template to create these strings on the server and to stuff the results in my JSON object so that I can use the above script. Admittingly, it's ugly data to look at.

Another note, no matter if I seperate display from data or not, the current XSLT template I use to process my XML will still be needed in the event there is only one item with a single possibility of display. In the event there is only a single product, JSON doesn't come into the picture since there are no controls displayed on the page to change the product's attributes (color, size, etc).

I definitely want to code this "right", so I certainly appreciate hearing from experience. I just don't think anyone can effectively advise me for or against the end result without knowing all of the considerations made that got me there

Here is (was) the template I used to escape double quotes:

<!--
Escape quotes with \ for JSON strings
-->
<xsl:template name="escapeQuote">
  <xsl:param name="pText" select="."/>

  <xsl:if test="string-length($pText) >0">
    <xsl:copy-of select="substring-before(concat($pText, '&quot;'), '&quot;')"/>

    <xsl:if test="contains($pText, '&quot;')">
      <xsl:text>\"</xsl:text>

      <xsl:call-template name="escapeQuote">
        <xsl:with-param name="pText" select="substring-after($pText, '&quot;')"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:if>

</xsl:template>

This template strips HTML tags from the input string. I call it like this:

<xsl:variable name="output3">
    <xsl:call-template name="escapeQuote">
        <xsl:with-param name="pText">
        <xsl:call-template name="DataTableRow">
            ... with-params ...
        </xsl:call-template>
        </xsl:with-param>
    </xsl:call-template>
</xsl:variable>

Immediately doing:

<xsl:copy-of select="$output3"/>

Shows that HTML tags no longer exist and therefore, no double quotes exist either. However, doing this reveals all of my HTML tags:

<xsl:variable name="output3">
    <xsl:call-template name="DataTableRow">
        ... with-params ...
    </xsl:call-template>
</xsl:variable>
<xsl:copy-of select="$output3"/>

Thanks for reading all of this - and btw, I see other posts where XSL code is formatted nicely and colorized. It's a LOT easier to read, but I can't get it to work with the code I'm posting. Everything is black text and left-justified. I've tried correcting that, but it's not working for me.

EDIT #3:

Dimitre commented that a quote could be used literally in the value attribute of a hidden input field, so I did some experimenting with that in mind. There seems to be a gotcha everywhere I look.

<input value='something containing " literally'/>

To get my value= contents wrapped by apostrophes instead of double quotes, I tried creating my hidden <input> like this:

<input>
  <xsl:attribute name="id">
    <xsl:value-of select="$id"/>
  </xsl:attribute>
  <xsl:attribute name="type">
    <xsl:text>hidden</xsl:text>
  </xsl:attribute>
  <xsl:attribute name="runat">
    <xsl:text>server</xsl:text>
  </xsl:attribute>
  <xsl:text> value='[</xsl:text>
  <xsl:copy-of select="$output"/>
  <xsl:text>]'</xsl:text>
</input>    

This doesn't work because the <input...> tag gets rendered with the value= attribute (and it's contents) AFTER the closing </input>.

So I removed the xsl:attribute elements and formed it with xsl:text. Unfortunately, using a < inside a xsl:text isn't valid and generates an error. So I changed the < to &lt; as well as the corresponding >. This didn't generate any errors, but causes the entire <input> and its contents to be displayed as a string when the page is rendered. (I'm viewing the page in Firefox).

I haven't tried coding this with CDATA because I'm pretty sure that's not going to work either and I'll get the same results as the latter - everything being displayed as a string.

shrug

EDIT #4:

OJay recommended dropping the idea of putting my JSON in the value= attribute of a hidden field. I've considered this all along, but wasn't sure how much I'd have to change my code. When he showed examples of how little I'd have to change, I decided to go for it.

Unfortunately, there's another gotcha.

<script type="text/javascript">

var hfDetails = [{ProductID: '000001',Title: '<h3>American Apparel Sheer Jersey Chemise X-Small-Asphalt</h3>',Condition: 'New',ListPrice: '<tr style="display:block">
    <td>
      <div class="datalabel">0List Price:</div>
    </td>
    <td class="datainfo" id="listprice">$24.99</td>
  </tr>',Price: '<tr style="display:block">
    <td>
      <div class="datalabel maroon">Sale:</div>
    </td>
    <td class="datainfo" id="price"><span class="bigmaroon">$15.49</span></td>
  </tr>',YouSave: '<tr style="display:block">
    <td>
      <div class="datalabel">You Save:</div>
    </td>
    <td class="datainfo" id="yousave"><span class="maroon">$9.50 (38%)</span></td>
  </tr>'},

 ....

]
</script>

This is a paste from the rendered web page. I'm getting an error of "Unterminated string literal" and it's pointing to the apostrophe right before the very first <tr> - on the second line.

I'm assuming this is because the string has whitespace spreading it over several lines. The whitespace is as-is from making the call-template. I'm not sure how to disable this whitespacing or if that'll even make the difference. Am going to do some googling on this, but does anyone know if there is a way to turn whitespace off for a call-template?

like image 848
rwkiii Avatar asked Jun 25 '12 03:06

rwkiii


2 Answers

Try <xsl:copy-of select="$test"> instead of <xsl:value-of ... />

Also note, <xsl:value-of /> (and <xsl:copy-of /> for that matter), should be inside a template, not at the root level of the <xsl:stylesheet >....</xsl:stylesheet> - throws an error in my XSLT debugger

so test that worked for me was

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">    
  <xsl:output method="xml" indent="yes"/>

  <xsl:variable name="test">
    <xsl:call-template name="demonstration" />
  </xsl:variable>

  <xsl:template match="/">
    <xsl:copy-of select="$test"/>
  </xsl:template>

  <xsl:template name="demonstration">
    <p>Just a test</p>
  </xsl:template>

</xsl:stylesheet>

Value-of would be getting the value of the variable, and becuase we are in XML the value would be the same as the value of a node, i.e the data between the tags. Copy of would output exactly that a complete copy , tags and all


EDIT As per my started comment, what I was meaning was something like this:

out from the XSLT

<script type="text/javascript">
var hfDetails = [{"ProductID": "00001", "ListPrice": '<tr class="collapsed"><td class="datalabel">List Price:</td><td class="datainfo"></td></tr>', "YourPrice": '<tr><td class="datalabel">Price:</td><td class="datainfo">$23.99 & is elegible for <span class="freeshipping">FREE</span> shipping <a href="#">Details</a></td></tr>'},

... etc, etc, etc ...

];
</script>

instead of the hidden input field. What this will do is create a global javascript array object. in essences jumps the step required by

var details = $.parseJSON($("#[id*='hfDetails']").val());

you could then just do

var details = hfDetails;

and no more code needs to change.

If you are concerned about the escaping of apostrophe and quotes. Either can be used to define a string in javascript, and then the other can safely be used in the string i.e.

"ListPrice" : '<tr class="collapsed"><td class="datalabel">List Price:</td><td class="datainfo"></td></tr>'

is valid and so is

"ListPrice" : "<tr class='collapsed'><td class='datalabel'>List Price:</td><td class='datainfo'></td></tr>"

also note, you don't have to enclose the properties of the object in quotes i.e.

"ProductID" : "00001"

can just be

ProductID : "00001"

as long as you don't have spaces in your property names

like image 115
OJay Avatar answered Jan 04 '23 15:01

OJay


<xsl:value-of select="$test"/>

You must use xsl:copy-of (or xsl:sequence is recommended in XSLT 2.0) -- instead of xsl:value-of.

By definition, xsl:value-of outputs the string value of the result of evaluating the expression in its select attribute.

<xsl:copy-of> outputs (in document order) all the nodes of the node-set specified in its select attribute. Or, to quote the W3C XSLT 1.0 specification again:

"The xsl:copy-of element can be used to copy a node-set over to the result tree without converting it to a string"

In XSLT 2.0 xsl:sequence may be used to output the nodes of a node-set in any desired order.

like image 22
Dimitre Novatchev Avatar answered Jan 04 '23 15:01

Dimitre Novatchev