简体   繁体   中英

Using XSLT to template a list within a paragraph, without losing the surrounding paragraph

I need to use XSLT to transform XML of the following form (I have no control over the format of the XML itself, so I couldn't tell you why the list is formatted the way it is):

<p id="p-0005" num="0004">Here is a statement and these are my reasons:
    <ul id="ul0001" list-style="none">
        <li id="ul0001-0001" num="0000">
            <ul id="ul0002" list-style="none">
                <li id="ul0002-0001" num="0005">a. Reason number 1.</li>
                <li id="ul0002-0002" num="0006">b. Reason number 2.</li>
                <li id="ul0002-0003" num="0007">c. Reason number 3.</li>
            </ul>
        </li>
    </ul>
</p>

I only have a few hours of XSLT experience, and so far I can extract the whole paragraph list and all:

<xsl:template match="p">
    <p>
        <xsl:value-of select="."/>
    </p>
</xsl:template>

or I've been able to extract only the list, without the sentence(s) preceding it:

<xsl:template match="p">
    <ul style="list-style: none;">
        <xsl:for-each select="./ul/li/ul/li">
            <li>
                <xsl:value-of select="."/>
            </li>
        </xsl:for-each>
    </ul>
</xsl:template> 

What I want to do is end up with HTML equivalent of:

<p> Here is a statement I am making and these are my reasons
    <ul>
        <li> Reason number 1. </li>
        <li> Reason number 2. </li>
        <li> Reason number 3. </li>
    </ul>
</p>

I've messed around with <xsl:apply-templates/> elements, but no matter what I try I end up with either missing or duplicate information. Any help is much appreciated!

The simplest way to do the kind of transformation you seem to be wanting is to write a template for each element type, and use apply-templates to handle flow of control:

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

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

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

A couple of things are worth noting here. First, all the templates produce elements of the same kind they match in the first place -- this is very close to an identity transform, and as a result many XSLT programmers would handle this specific case differently and more compactly. But the more compact solutions are harder to understand and don't illustrate this fundamental idiom as clearly.

Second, we're losing all the attributes, because by default <xsl:apply-templates/> doesn't select attributes.

Third, this just reproduces the input with the same structure, whereas you want, apparently, to lose the outermost ul element containing only a single li , and the single li within it. You'll need to decide exactly why you want to lose these elements, in order to express those conditions correctly in your code. Assuming that the rule is "Whenever an outer ul contains only a single li containing only a single ul, lose the outer ul and li,", then you can handle this by adding a single additional template to the stylesheet:

<xsl:template match="ul[count(*) = 1
                        and ./li 
                        and count(li/*) = 1 
                        and ./li/ul]">
  <xsl:apply-templates select="li/ul"/>
</xsl:template>

This does not handle the job of stripping the 'a.', 'b.', and 'c.' off the inner list items; I'll leave that as an exercise for you, if you really need to do it.

If you are having trouble learning how to use apply-templates properly, you might do worse than work at it until you understand how and why these templates work as they do. Until you understand how to use apply-templates, you will never be really fluent with XSLT.


Addendum: You ask "how can you avoid using the xsl:value-of element, while still getting its value into the output?"

The character data in the XML input is represented in the XSLT data model as text nodes; text nodes are processed like any other nodes. So you could, if you liked, write a template:

<xsl:template match="text()">
  <text-node>There was a text node here.</text-node>
</xsl:template>

Or, more usefully for this particular case:

<xsl:template match="text()">
  <xsl:value-of select="."/>
</xsl:template>

If you add this last template into the stylesheet, what you'll find is that it has no effect on the behavior . Why? Because the template just given is essentially the same as the default template which fires for any text node not matched by some user-supplied template. (For this reason, it is often unnecessary to write templates for text nodes -- the default behavior is often just fine.)

Using the value-of element on element nodes has the drawback that it gives you the string value of the element, which (as your example illustrates) normally involves losing all the structure of any internal markup in the element. For that reason, it's something many XSLT programmers will almost never use in writing XML-to-XML transforms. It does get used in handling text nodes, of course.

I've been able to extract only the list, without the sentence(s) preceding it:

Try adding <xsl:value-of select="text()"/> to before the ul element:

<xsl:template match="p">
    <xsl:value-of select="text()"/> <!-- here  -->
    <ul style="list-style: none;">
        <!-- <xsl:value-of select="text()"/>  NOT here  --> 
        <xsl:for-each select="./ul/li/ul/li">
            <li>
                <xsl:value-of select="."/>
            </li>
        </xsl:for-each>
    </ul>
</xsl:template> 

I would also suggest you learn about the identity transform template, as this could well be a more convenient starting point here (and certainly in other cases).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM