xslt and xpath: match preceding comments
given this xml:
<root>
<list>
<!-- foo's comment -->
<item name="foo" />
<item name="bar" />
<!-- another foo's comment -->
<item name="another foo" />
开发者_StackOverflow中文版 </list>
</root>
I'd like to use a XPath to select all item-nodes that have a comment immediately preceding them, that is I like to select the "foo" and "another foo" items, but not the "bar" item.
I already fiddled about the preceding-sibling axis and the comment() function but to no avail.
This seems to work:
//comment()/following-sibling::*[1]/self::item
It looks for immediately following siblings of comments which are also <item>
elements. I don't know a better way to express the ::*[1]/self::item
part, which is ugly; note that if it were written ::item[1]
then it would also find <item>
s not immediately proceded by a comment.
The currently selected solution:
//comment()/following-sibling::*[1]/self::item
doesn't work in the case where there is a procesing instruction (or a whole group of processing instructions) between the comment and the element -- as noticed in a comment by Martin Honnen.
The solution below doesn't have such a problem.
The following XPath expression selects only elements nodes that are either immediately preceded by a comment node, or are immediately preceded by a white-space-only text node, which is immediately preceded by a comment node:
(//comment()
/following-sibling::node()
[1]
[self::text()
and
not(normalize-space())
]
/following-sibling::node()
[1] [self::item]
)
|
(//comment()
/following-sibling::node()
[1]
[self::item]
)
Here is a complete test:
We use this XML document:
<root>
<list>
<!-- foo's comment -->
<item name="foo" />
<item name="bar" />
<!-- another foo's comment -->
<item name="another foo" />
<!-- comment 3 --><item name="immed.after 3"/>
<!-- comment 4 --><?PI ?><item name="after PI"/>
</list>
</root>
When the following transformation is applied on the above XML document:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:copy-of select=
"
(//comment()
/following-sibling::node()
[1]
[self::text()
and
not(normalize-space())
]
/following-sibling::node()
[1] [self::item]
)
|
(//comment()
/following-sibling::node()
[1]
[self::item]
)
"/>
</xsl:template>
</xsl:stylesheet>
the wanted, correct result is produced:
<item name="foo"/>
<item name="another foo"/>
<item name="immed.after 3"/>
As mentioned in this thread, introducing a test (<xsl:if test="..."></xsl:if>
) like:
preceding-sibling::comment()
would only tests whether the node has a preceding sibling that's a comment.
If you want to know, of the preceding siblings that are elements or comments, whether the nearest one is a comment, you could try:
(preceding-sibling::*|preceding-sibling::comment())[1][self::comment()] # WRONG
BUT: that won't work, because though "
[1]
" means first in the backwards direction forpreceding-sibling
, it doesn't mean that for a parenthesized expression - it means first in document order
You can try:
(preceding-sibling::*|preceding-sibling::comment())[last()][self::comment()]
or
preceding-sibling::node()[self::*|self::comment()][1][self::comment()]
For instance:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output omit-xml-declaration="no" indent="no"/>
<xsl:template match="//item">
<xsl:if test="preceding-sibling::node()[self::*|self::comment()][1][self::comment()]">
<xsl:value-of select="./@name" />
</xsl:if>
</xsl:template>
</xsl:stylesheet>
would only display:
foo
another foo
when typing:
C:\Prog\xslt\preceding-sibling_comment>
java -cp ..\saxonhe9-2-0-6j\saxon9he.jar net.sf.saxon.Transform -s:test.xml -xsl:t.xslt -o:res.xml
with:
test.xml
: your file displayed in your questiont.xslt
: the xslt file aboveres.xml
: the resulting transformed file
Edit: since it doesn't take into account processing instructions, I left that answer as Community Wiki.
精彩评论