6
\$\begingroup\$

The CMS I use uses the Xalan XSLT processor which is a XSLT version 1.0 processor. Editors who use WYSIWYG fields within the CMS can save content that will look like either:

<story>
    <p>Story Text which may have <em>formatting</em>.</p>
</story>

or:

<story>Story Text which may have <em>formatting</em>.</story>

or even:

<story>
    <div>Story Text which may<br /> have <em>formatting</em>.</div>
</story>

In order to handle those varying cases I've been using:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/> 
<xsl:template match="/system-index-block">
    <xsl:for-each select="story">
        <xsl:choose>
            <!-- If wrapped in an element -->
            <xsl:when test="./p|./div">
                <!-- copy what is inside story, allow for text outside of elements -->
                <xsl:copy-of select="./text()|./*"/>
            </xsl:when>
            <!-- wrap the content in a paragraph tag -->
            <xsl:otherwise>
                <p>
                    <!-- copy text and tags from inside story -->
                    <xsl:copy-of select="./text()|./*"/>
                </p>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Is there a better way of producing well formed HTML from this data? Is there a way to simply this?

\$\endgroup\$
2
  • \$\begingroup\$ I don't know how complex the "real" XML will be, but if you only have XHTML wrapped in <story> elements you could simply replace those with <div>s. If it's more complicated it would be useful to see an example (for example, could there be <story>s within <story>s?) \$\endgroup\$
    – l0b0
    Feb 11, 2011 at 10:13
  • \$\begingroup\$ The real XML is more complex. There are never nested <story> elements. The <story> element contains the post processed output of a TinyMCE instance and in most cases the editors are restricted from using the "Edit HTML Source" button within that editor. \$\endgroup\$ Feb 11, 2011 at 16:17

1 Answer 1

5
\$\begingroup\$

You should change your mindset when working with XSLT: look at it as a functional language, where you declare rules for the transformation of elements. Think about it in terms of events: when a p element is found in input, I expect this and that in the output.

First, regarding the definition of your data. You should ask for (or create) a more formal description of the data. There are different formats available to describe the structure (grammar), DTD and XML Schema being the more common. There are more simple alternatives such as RELAX NG and Schematron.

My guess, from your stylesheet and your descriptions:

  • there is always a <system-index-block> at the top
  • there is a list of <story> elements in <system-index-block>
  • there is mixed HTML contents in <story>, with <p>,<div>,<br>,<em> and text allowed

I am not sure about the last part: is it possible to have both text and div or paragraph at the top level in a story?

<story>
  Text before a <p>Paragraph</p> in between or <div>Div</div> even after ?
</story>

You should break your unique template rule into several template rules, one or several for each element. Then to direct the flow from rule to rule, do not use for-each but rather apply-templates:

<xsl:template match="system-index-block">
  <!-- Note:
       You probably need to create the basic HTML structure Here: html, head, body
  -->
  <xsl:apply-templates /> <!-- you can specify select="story" but it is not required -->
</xsl:template>

<xsl:template match="story[p|div]"> <!-- If wrapped in an element -->
   <xsl:apply-templates mode="copy" />
</xsl:template>

<xsl:template match="story"> <!-- wrap the content in a paragraph tag -->
  <p>
    <xsl:apply-templates mode="copy" />
  </p>
</xsl:template>

Note that I used the mode "copy" above: you can now create different rules for the same nodes depending on the mode, for example:

<xsl:template match="text()" /> <!-- ignore text in normal mode, probably whitespace -->

<xsl:template match="text()" mode="copy">
    <xsl:copy />
</xsl:template>

Also, you will probably get into trouble because you only declared a namespace for the xsl prefix. You typically need a prefix/namespace declaration for the input and output formats. You will then need to use the prefix in match/select expressions and in the tag names for the output, for example, with input prefix bound to your input namespace and html bound to the html namespace:

<xsl:template match="input:story"> <!-- wrap the content in a paragraph tag -->
  <html:p>
    <xsl:apply-templates mode="copy" />
  </html:p>
</xsl:template>

To go further, here are pointers that you will probably find useful:

\$\endgroup\$
3
  • \$\begingroup\$ Good answer considering the degree to which I tried to simplify the example XML and XSL. I tried to boil the question down and see now that I left out useful information. I'll look at reworking things to include greater use of mode="". I'm going to review my use of <xsl:for-each throughout all of my code and see where I'm relying on it where it isn't needed. The namespace isn't an issue as the stylesheet is only generating a portion of a page. \$\endgroup\$ Feb 13, 2011 at 7:23
  • \$\begingroup\$ The only case this seems to break on the way it is layed out now is when <story> contains both unenclosed text and <p> tags. \$\endgroup\$ Feb 15, 2011 at 0:03
  • \$\begingroup\$ @Jason can you edit your answer to show your updated code? \$\endgroup\$ Feb 16, 2011 at 13:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.