XSL: replacing <br><br/> with another character - xslt

I have an input xml document that I am transforming which has this content in an node:
<misc-item>22 mm<br></br><fraction>7/8</fraction> in.</misc-item>
When I create a variable by selecting 'misc-item', the br and fraction tags disappear. However, if I create a variable using 'misc-item/br' and test to see if it is finding the br, the test seems to work.
What I want to do is make the
'<br></br>'
into a space or semicolon or something, but I have had no luck. I tried getting the siblings of 'misc-item/br', but it has none. I checked the child count of 'misc-item' and it is one.
Any help greatly appreciated.
I looked at the post identified as possible dupe. I tried this to no avail:
<xsl:template match="#*|node()" mode='PageOutput'>
<xsl:copy>
<xsl:apply-templates select="#*|node()" mode="PageOutput" />
</xsl:copy>
</xsl:template>
<xsl:template match="br" mode='PageOutput'>
<xsl:value-of select="' '" />
</xsl:template>
Since I am not ignoring an element as in the suggested dupe, but rather substituting, this doesn't seem to be quite right.

When I create a variable by selecting 'misc-item', the br and fraction tags disappear. However, if I create a variable using 'misc-item/br' and test to see if it is finding the br, the test seems to work.
When you create a variable you're storing a reference to the misc-item node in the variable. If you ask for the value-of that node you'll get just the text, with elements stripped out, but the variable still holds the node itself.
This is probably something you need to tackle using apply-templates instead of value-of. A common theme is to have an "identity template" which essentially copies everything as-is but can be overridden with different behaviour for certain nodes by providing more specific templates.
<xsl:template match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()" /></xsl:copy>
</xsl:template>
<!-- replace any br element with a semicolon -->
<xsl:template match="br">;</xsl:template>
You can use a mode to restrict these templates for use in specific situations only
<xsl:template match="#*|node()" mode="strip-br">
<xsl:copy><xsl:apply-templates select="#*|node()" mode="strip-br" /></xsl:copy>
</xsl:template>
<!-- replace any br element with a semicolon -->
<xsl:template match="br" mode="strip-br">;</xsl:template>
and now you can use e.g.
<xsl:apply-templates select="$miscitem/node()" mode="strip-br" />
instead of <xsl:value-of select="$miscitem"/> to get the result you're after.

Related

Excluding a single element in XSLT when using document()

I'm using document() to copy the content of an XML file (tagged for DITA) into a variable. Here is the code I had used previously that worked:
<xsl:variable name="fileContent">
<xsl:copy-of select="document($fileSrc)"/>
</xsl:variable>
Now, I want to include everything except the <draft-comment> element. I've tried to change document() as follows:
<xsl:variable name="fileContent">
<xsl:copy-of select="document($fileSrc)/*[not(draft-comment)]"/>
</xsl:variable>
This doesn't seem to work. The text is still there. Any suggestions on how I can fix this? Thank you for your help.
Create two templates with the mode attribute; one to copy nodes unchanged, and one to ignore draft-comment
<xsl:template match="#*|node()" mode="document">
<xsl:copy>
<xsl:apply-templates select="#*|node()" mode="document" />
</xsl:copy>
</xsl:template>
<xsl:template match="draft-comment" mode="document" />
Then you can use xsl:apply-templates instead of xsl:copy-of
<xsl:variable name="fileContent">
<xsl:apply-templates select="document($fileSrc)" mode="document" />
</xsl:variable>
Note that xsl:copy-of does a deep-copy, and your existing statement will copy the root element and everything under it just as long as it doesn't have a child node called draft-comment

How can I access an xsl:variable from outside the "scope" of an xsl:for-each block

I'm trying to access a variable from within a for-each. I've done a lot of reading today to try to figure this out, but nothing quite fits my scenario. Eventually, I will have multiple series like seen below and I will use the variables that I'm pulling out and make different condition. The for-each that I have below is bringing back data from 400 records. I'm sorry, I cannot provide an XML. I'm not able to expose GUIDs and such.
UPDATE
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output media-type="xml" indent="yes"/>
<xsl:template match="/">
<Records>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
</xsl:for-each>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
<xsl:variable name ="findingName" select="Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
</xsl:for-each>
<xsl:if test="$findingName1 = $rocName1">
<Records>
<findingID>
<xsl:for-each select="Records/Record/Record[#levelGuid = '123']">
<xsl:value-of select ="Field[#guid = '123']"/>
</xsl:for-each>
</findingID>
</Records>
</xsl:if>
</Records>
</xsl:template>
</xsl:stylesheet>
The desired output is any findingID that has a $findingName1 that equals $rocName1. The GUIDS only appear once, but each level has hundreds of records.
I'm trying to access a variable from within a for-each.
The variable $rocRecord is in scope inside the for-each. You can simply reference it and use it.
But I think you are trying to do something else. I.e., defining a variable inside for-each and wanting to use it outside it.
Variables are scoped within their focus-setting containing block. So the short answer is: you cannot do it. The long answer however...
Use templates. The only reason to do what you seem to want to be doing is to need to access the data elsewhere:
<xsl:template match="/">
<!-- in fact, you don't need for-each at all, but I leave it in for clarity -->
<xsl:for-each select="records/record">
<!--
apply-templates means: call the declared xsl:template that
matches this node, it is somewhat similar to a function call in other
languages, except that it works the other way around, the processor will
magically find the "function" (i.e., template) for you
-->
<xsl:apply-templates select="Field[#guid='123']" />
</xsl:for-each>
</xsl:template>
<xsl:template match="Field">
<!-- the focus here is what is the contents of your variable $rocName1 -->
<rocName>
<xsl:value-of select="substring=-before(., ' - ')" />
</rocName>
</xsl:template>
XSLT is a declarative, template-oriented, functional language with concepts that are quite unique compared to most other languages. It can take a few hours to get used to it.
You said you did a lot of reading, but perhaps it is time to check a little XSLT course? There are a few online, search for "XSLT fundamentals course". It will save you hours / days of frustration.
This is a good, short read to catch up on variables in XSLT.
Update
On second read, I think it looks like you are troubled by the fact that the loop goes on for 400 items and that you only want to output the value of $rocName1. The example I showed above, does exactly that, because apply-templates does nothing if the selection is empty, which is what happens if the guid is not found.
If the guid appears once, the code above will output it once. If it appears multiple times and you only want the first, append [1] to the select statement.
Update #2 (after your update with an example)
You have two loops:
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
and
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
You then want to do something (created a findingId) when a record in the first loop matches a record in the second loop.
While you can solve this using (nested) loops, it is not necessary to do so, in fact, it is discouraged as it will make your code hard to read. As I explained in my original answer, apply-templates is usually the easier way to do get this to work.
Since the Record elements are siblings of one another, I would tackle this as follows:
<xsl:template match="/">
<Records>
<xsl:apply-templates select="Records/Records/Record[#levelGuid = 'level1']" />
</Records>
</xsl:template>
<xsl:template match="Record">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
<xsl:variable name="findingNameBase" select="../Record[#levelGuid = 'levelA']" />
<xsl:variable name ="findingName" select="$findingNameBase/Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
<findingId rocName="{$rocName1}">
<xsl:value-of select="$findingName" />
</findingId>
</xsl:template>
While this can be simplified further, it is a good start to learn about applying templates, which is at the core of anything you do with XSLT. Learn about applying templates, because without it, XSLT will be very hard to understand.

xml:space='preserve' doesn't seem to get on with xsl:apply-templates select="node()"

Doing some work with xsl - first time I've done anything serious, and I've hit something which I can't explain. Easiest way to show it is with the identity transform:
This works:
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
This doesn't (says "Unable to apply transformation on current source"):
<xsl:template match="#*|node()" xml:space='preserve'>
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
This does:
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:apply-templates select="node()" xml:space='preserve'/>
</xsl:copy>
</xsl:template>
OK, I can see what's happening. But I don't understand why. Why does xml:space not want to play nicely with attributes? Just curious.
BTW, this is using the xsl translator that's built into Notepad++. Perhaps I shouldn't trust it?
What are you trying to accomplish? xml:space="preserve" tells XML-consuming applications that you want to preserve whitespace-only text nodes that are descendants of the element that xml:space is an attribute of. In this example, you have xml:space as an attribute of <xsl:apply-templates>, but <xsl:apply-templates> has no whitespace-only text node descendants, so xml:space has no possible effect.
I think you wanted to preserve whitespace-only text nodes from the input XML document (not from the XSLT stylesheet). In that case, you need xml:space to be in the input XML document, not in the XSLT stylesheet. The stylesheet can have xsl:preserve-space-elements="*", but that's already the default, unless you have xsl:strip-space-elements set.
Yes, I would be inclined to wonder whether the XSLT processor used by Notepad++ (libxml) is doing something illegit. As a good diagnostic, try a respected processor like Saxon and see if you get any errors.
Either that, or just remove xml:space from your stylesheet, since it won't do you any good even if the processor doesn't throw an error.
Suggestion:
Just use
<xsl:output method="html" indent="yes"/>
as the first child of <xsl:stylesheet>.
The indent="yes" will prevent all the output elements from being crammed together on one line, so you can read the results.
Whitespace is not preserved for attributes according to specification - it is highlighted in this posting. Preserving attribute whitespace in XSLT

XSLT: Using a different way of processing within the current way of processing

Below I'm trying to match certain nodes.
<xsl:template match="nodes">
<element>
<xsl:apply-templates select="nodes" mode="different" />
</element>
</xsl:template>
Now, there are multiple ways of processing for the same nodes. I want to use this different way of processing within the current way of processing. That's why I perform apply-templates on the same selection, which is nodes, however the mode is different now.
Here's how the different mode could look like:
<xsl:template match="nodes" mode="different">
<!-- another way of processing these nodes -->
</xsl:template>
Now, this does not work. Only the first type of processing is processed and the apply-templates call is simply not applied.
To be a bit more specific:
<xsl:template match="Foundation.Core.Association.connection">
<xsl:for-each select="Foundation.Core.AssociationEnd">
<someElement>
<xsl:apply-templates select="Foundation.Core.Association.connection" mode="different" />
</someElement>
</xsl:for-each>
</xsl:template>
As you can see, I select Foundation.Core.Association.connection. Of course this is wrong, but how do I refer to this element given the current element and position? Given Derek his comment, that should do it.
What am I doing wrong, how can I get what I want using XSLT? What could be another approach to solve this problem?
Thanks.
if "nodes" is referring to the same exact set of nodes in the containing match, try:
<xsl:template match="nodes">
<element>
<xsl:apply-templates select="." mode="different" />
</element>
</xsl:template>
<xsl:template match="Foundation.Core.Association.connection">
<xsl:for-each select="Foundation.Core.AssociationEnd">
<someElement>
<xsl:apply-templates
select="Foundation.Core.Association.connection"
mode="different" />
As you can see, I select
Foundation.Core.Association.connection.
Of course this is wrong, but how do I
refer to this element given the
current element and position?
Use:
<xsl:apply-templates select=".." mode="different" />
The element you want to process differently is the parent of the current node.
Of course, most likely this convoluted processing is not necessary at all, which would be confirmed, had you been able to show more of the XML document and to formulate the problem in a more succint way.

XSLT to Select Desired Elements When Nested In Not-Desired Elements

What XSLT would I use to extract some nodes to output, ignoring others, when the nodes to be be extracted are some times nested nodes to be ignored?
Consider:
<alpha_top>This prints.
<beta>This doesn't.
<alpha_bottom>This too prints.</alpha_bottom>
</beta>
</alpha_top>
I want a transform that produces:
<alpha_top>This prints.
<alpha_bottom>This too prints.</alpha_bottom>
</alpha_top>
This answer shows how to select nodes based on the presence of a string in the element tag name.
Ok, here is a better way
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="beta">
<xsl:apply-templates select="*"></xsl:apply-templates>
</xsl:template>
<xsl:template match="/|*|text()">
<xsl:copy>
<xsl:apply-templates select="*|text()"></xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
This basically does an identity transform, but for the element you don't want to include I removed the xsl:copy and only applied templates on the child elements.
The following stylesheet works on your particular case, but I suspect you are looking for something a bit more generic. I'm also sure there is a simpler way.
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:apply-templates select="alpha_top"></xsl:apply-templates>
</xsl:template>
<xsl:template match="alpha_top">
<xsl:copy>
<xsl:apply-templates select="beta/alpha_bottom|text()"></xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="*|text()">
<xsl:copy>
<xsl:apply-templates select="*|text()"></xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
I think, that once you have a reasonable understand of how XSLT traversal works (hopefully I answered that in your other question) this becomes quite simple.
You have several choices on how to do this. Darrell Miller's answer shows you have to process a whole document and strip out the elements you're not interested in. That's one approach.
Before I go further, I get the impression that you might not entirely 'get' the concept of context in XSLT. This is important and will make your life simpler. At any time in XSLT there is one and only context node. This is the node (element, attribute, comment, etc) currently being 'processed'. Inside a template called via xsl:select the node that has been selected is the context node. So, given your xml:
<alpha_top>This prints.
<beta>This doesn't.
<alpha_bottom>This too prints.</alpha_bottom>
</beta>
</alpha_top>
and the following:
<xsl:apply-templates select='beta'/>
and
<xsl:template match='beta'>...</xsl:template>
the beta node will be the context node inside the template. There's a bit more to it than that but not much.
So, when you start your stylesheet with something like:
<xsl:template match='/'>
<xsl:apply-templates select='alpha_top'/>
</xsl:apply-templates>
you are selecting the children of the document node (the only child element is the alpha_top element). Your xpath statement inside there is relative to the context node.
Now, in that top level template you might decide that you only want to process your alpha_bottom nodes. Then you could put in a statement like:
<xsl:template match='/>
<xsl:apply-templates select='//alpha_top'/>
</xsl:template>
This would walk down the tree and select all alpha_top elements and nothing else.
Alternatively you could process all your elements and simply ignore the content of the beta node:
<xsl:template match='beta'>
<xsl:apply-templates/>
</xsl:template>
(as I mentioned in my other reply to you xsl:apply-templates with no select attribute is the same as using select=''*).
This will ignore the content of the beta node but process all of it's children (assuming you have templates).
So, ignoring elements in your output is basically a matter of using the correct xpath statements in your select attributes. Of course, you might want a good xpath tutorial :)
The probably simplest solution to your problem is this:
<xsl:template match="alpha_top|alpha_bottom">
<xsl:copy>
<xsl:value-of select="text()" />
<xsl:apply-templates />
</xsl:copy>
</xs:template>
<xsl:template match="text()" />
This does not exhibit the same white-space behavior you have in your example, but this is probably irrelevant.