Working with XHTML and XML tools, sometimes we need to invert the italics <i> blocks. Example:
<!-- original text: -->
<p id="p1"><i>Several more</i> Homo sapiens <i>fossils were discovered</i>.</p>
<!-- same text, swapping italics: -->
<p id="p2">Several more <i>Homo sapiens</i> fossils were discovered.</p>
So, looks like this,
Several more Homo sapiens fossils were discovered.
Several more Homo sapiens fossils were discovered.
There are many ways to transform a "mixed italics" text into a "inverted italics": see
What the correct algorithm to invert italics in a mixed text?...
... But I not see any way to do it with "pure XSLT" (without external processing dependences): do you see?
Something like this:
<xsl:template match="i" mode="invert-italic">
<xsl:apply-templates mode="invert-italic"/>
</xsl:template>
<xsl:template match="text()[not(ancestor::i)]" mode="invert-italic">
<i><xsl:copy-of select="."/></i>
</xsl:template>
<xsl:template match="node()" mode="invert-italic">
<xsl:copy>
<xsl:copy select="#*"/>
<xsl:apply-templates mode="invert-italic"/>
</xsl:copy>
</xsl:template>
I'm not sure if this would cover all cases, but you could do this:
XML Input
<html>
<!-- original text: -->
<p id="p1"><i>Several more</i> Homo sapiens <i>fossils were discovered</i>.</p>
<!-- same text, swapping italics: -->
<p id="p2">Several more <i>Homo sapiens</i> fossils were discovered.</p>
<p>Leave me alone!</p>
<p><b><i>O</i>RIGINAL <big><i>with italics</i> and </big> withOUT</b></p>
</html>
XSLT 1.0
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[i]">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:apply-templates select="node()" mode="swapItal"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()" mode="swapItal" priority="1">
<i><xsl:value-of select="."/></i>
</xsl:template>
<xsl:template match="i" mode="swapItal">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="#*|node()" mode="swapItal">
<xsl:copy>
<xsl:apply-templates select="#*|node()" mode="swapItal"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
XML Output
<html>
<!-- original text: -->
<p id="p1">Several more<i> Homo sapiens </i>fossils were discovered<i>.</i></p>
<!-- same text, swapping italics: -->
<p id="p2"><i>Several more </i>Homo sapiens<i> fossils were discovered.</i></p>
<p>Leave me alone!</p>
<p><b>O<i>RIGINAL </i><big>with italics<i> and </i></big><i> withOUT</i></b></p>
</html>
Input Rendered
Several more Homo sapiens fossils were discovered.
Several more Homo sapiens fossils were discovered.
Leave me alone!
ORIGINAL with italics and withOUT
Output Rendered
Several more Homo sapiens fossils were discovered.
Several more Homo sapiens fossils were discovered.
Leave me alone!
ORIGINAL with italics and withOUT
Hello? Someone else will edit? ... Ok, I need "100% solution", them I am adding one, only for finesh, but was not "my".
#MichaelKay and #DanielHaley show good clues and near to fineshed solutions (!).
XML Input
<html>
<p><i>Several more</i> Homo sapiens <i>fossils were discovered</i>.</p>
<p>Several more <i>Homo sapiens</i> fossils were discovered.</p>
<p>Leave me alone!</p>
<p><b><i>F</i>RAGMENT <big><i>with italics</i> and </big> withOUT</b></p>
<p><i><sup><sub><sup><sub>Deep tags</sub></sup></sub></sup> here</i>
<big><b><small>and here</small></b></big>!
</p>
</html>
XSLT 1.0 implemention
#DanielHaley show better results (only the <p>Leave me alone!</p> not inverted), but #MichaelKay's solution is more elegant: I merged both to produce this "100% solution". Now I am using this XSLT as "swap italics algorithm" on my system... No bugs until now (!).
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:preserve-space elements="p"/>
<xsl:template match="#*|node()"> <!-- copy all -->
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="i"> <!-- remove tag i -->
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="text()[not(ancestor::i)]"> <!-- inlcude tag i -->
<i><xsl:copy-of select="."/></i>
</xsl:template>
</xsl:stylesheet>
Outlining as an "drive by event algorithm" in a copy process:
remove i tags: copy any thing of "<i> thing </i>" as " thing ".
include i tags: copy any text as "<i> text </i>", when the text is not into a context of "italic parents (or another ancestor)". PS: text is a terminal node of the DOM tree.
Related
I have one XML file that I need to transform based on a mapping file with XSLT 2.0. I'm using the Saxon HE processor.
My mapping file:
<element root="TEST">
<childName condition="/TEST/MyElement/CHILD[text()='B']>/TEST/MyElement/CHILD</childName>
<childBez condition="/TEST/MyElement/CHILD[text()='B']>/TEST/MyElement/CHILDBEZ</childBez>
</element>
I have to copy the elements CHILD and CHILDBEZ plus the parent and the root elements when the text of CHILD equals B.
So with this Input:
<?xml version="1.0" encoding="UTF-8"?>
<TEST>
<MyElement>
<CHILD>A</CHILD>
<CHILDBEZ>ABEZ</CHILDBEZ>
<NotInteresting></NotInteresting>
</MyElement>
<MyElement>
<CHILD>B</CHILD>
<CHILDBEZ>BBEZ</CHILDBEZ>
<NotInteresting2></NotInteresting2>
</MyElement>
</TEST>
the desired output:
<TEST>
<MyElement>
<childName>B</childName>
<childBez>BBEZ</childBez>
</MyElement>
</TEST>
what I have so far (based on this solution XSLT 2.0 XPATH expression with variable):
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:param name="mapping" select="document('mapping.xml')"/>
<xsl:key name="map" match="*" use="."/>
<xsl:template match="/">
<xsl:variable name="first-pass">
<xsl:apply-templates mode="first-pass"/>
</xsl:variable>
<xsl:apply-templates select="$first-pass/*"/>
</xsl:template>
<xsl:template match="*" mode="first-pass">
<xsl:param name="parent-path" tunnel="yes"/>
<xsl:variable name="path" select="concat($parent-path, '/', name())"/>
<xsl:variable name="replacement" select="key('map', $path, $mapping)"/>
<xsl:variable name="condition" select="key('map', $path, $mapping)/#condition"/>
<xsl:choose>
<xsl:when test="$condition!= ''">
<!-- if there is a condition defined in the mapping file, check for it -->
</xsl:when>
<xsl:otherwise>
<xsl:element name="{if ($replacement) then name($replacement) else name()}">
<xsl:attribute name="original" select="not($replacement)"/>
<xsl:apply-templates mode="first-pass">
<xsl:with-param name="parent-path" select="$path" tunnel="yes"/>
</xsl:apply-templates>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="*">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[#original='true' and not(descendant::*/#original='false')]"/>
</xsl:stylesheet>
but the problem is that it's impossible to evaluate dynamic XPATH expressions with XSLT 2.0. Does anyone knows a workaround for that? Plus I have a problem with the mapping file. When there is only one element in it, it's not working at all.
If dynamic XPath evaluation isn't an option in your chosen processor, then generating an XSLT stylesheet is often a good alternative. In fact, it's often a good alternative anyway.
One way of thinking about this is that your mapping file is actually a program written in a very simple transformation language. There are two ways of executing this program: you can write an interpreter (dynamic XPath evaluation), or you can write a compiler (XSLT stylesheet generation). Both work well.
I've the below cases.
**Case1:**
<para>1A/66</para>
<para>1A/34S/4</para>
<para>1/66</para>
**Case 2:**
<para>A/66</para>
<para>A1/1</para>
Here, the explaination is if the para starts with a letter (here it is A, it can be any alphabet), it should print case 2 else it should print case 1.
please let me know how can i do this.
Here is a DEmo
With XSLT 2.0 you have regular expression support so using e.g.
<xsl:template match="para[matches(., '^[a-zA-Z]')]">case 2</xsl:template>
<xsl:template match="para[matches(., '^[^a-zA-Z]')]">case 1</xsl:template>
Of course you can use a different regular expression to match on non-ASCII letters as well if "can be any alphabet" is meant to indicate other letters.
Here you are :
<xsl:template match="/">
<hmtl>
<head>
<title>New Version!</title>
</head>
<body>
<xsl:call-template name="print-param" />
<xsl:apply-templates select="child::*"/>
</body>
</hmtl>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match = "body">
<!-- Match cases -->
</xsl:template>
<xsl:template name = "print-param">
<!-- Match cases -->
**Case1:**
<xsl:for-each select=".//para[matches(., '^[A-Za-z]\.*')]">
<para><xsl:value-of select="." /></para>
</xsl:for-each>
**Case 2:**
<xsl:for-each select=".//para[not(matches(., '^[A-Za-z]\.*'))]">
<para><xsl:value-of select="." /></para>
</xsl:for-each>
</xsl:template>
</xsl:transform>
Check this demo
I have a list of order lines with each one product on them. The products in may form a self-referencing hierarchy. I need to order the lines in such a way that all products that have no parent or whose parent is missing from the order are at the top, followed by their children. No child may be above its parent in the end result.
So how can i order the following xml:
<order>
<line><product code="3" parent="1"/></line>
<line><product code="2" parent="1"/></line>
<line><product code="6" parent="X"/></line>
<line><product code="1" /></line>
<line><product code="4" parent="2"/></line>
</order>
Into this:
<order>
<line><product code="6" parent="X"/></line>
<line><product code="1" /></line>
<line><product code="2" parent="1"/></line>
<line><product code="3" parent="1"/></line>
<line><product code="4" parent="2"/></line>
</order>
Note that the order within a specific level is not important, as long as the child node follows at some point after it's parent.
I have a solution which works for hierarchies that do not exceed a predefined depth:
<order>
<xsl:variable name="level-0"
select="/order/line[ not(product/#parent=../line/product/#code) ]"/>
<xsl:for-each select="$level-0">
<xsl:copy-of select="."/>
</xsl:for-each>
<xsl:variable name="level-1"
select="/order/line[ product/#parent=$level-0/product/#code ]"/>
<xsl:for-each select="$level-1">
<xsl:copy-of select="."/>
</xsl:for-each>
<xsl:variable name="level-2"
select="/order/line[ product/#parent=$level-1/product/#code ]"/>
<xsl:for-each select="$level-2">
<xsl:copy-of select="."/>
</xsl:for-each>
</order>
The above sample xslt will work for hierarchies with a maximum depth of 3 levels and is easily extended to more, but how can i generalize this and have the xslt sort arbitrary levels of depth correctly?
To start with, you could define a couple of keys to help you look up the line elements by either their code or parent attribute
<xsl:key name="products-by-parent" match="line" use="product/#parent" />
<xsl:key name="products-by-code" match="line" use="product/#code" />
You would start off by selecting the line elements with no parent, using a key to do this check:
<xsl:apply-templates select="line[not(key('products-by-code', product/#parent))]"/>
Then, within the template that matches the line element, you would just copy the element, and then select its "children" like so, using the other key
<xsl:apply-templates select="key('products-by-parent', product/#code)"/>
This would be a recursive call, so it would recursively look for its children until no more are found.
Try this XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="products-by-parent" match="line" use="product/#parent"/>
<xsl:key name="products-by-code" match="line" use="product/#code"/>
<xsl:template match="order">
<xsl:copy>
<xsl:apply-templates select="line[not(key('products-by-code', product/#parent))]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="line">
<xsl:call-template name="identity"/>
<xsl:apply-templates select="key('products-by-parent', product/#code)"/>
</xsl:template>
<xsl:template match="#*|node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Do note the use of the XSLT identity transform to copy the existing nodes in the XML.
Very interesting problem. I would do this in two passes: first, nest the elements according to their hierarchy. Then output the elements, sorted by the count of their ancestors.
XSLT 1.0 (+ EXSLT node-set() function):
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="product-by-code" match="product" use="#code" />
<!-- first pass -->
<xsl:variable name="nested">
<xsl:apply-templates select="/order/line/product[not(key('product-by-code', #parent))]" mode="nest"/>
</xsl:variable>
<xsl:template match="product" mode="nest">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates select="../../line/product[#parent=current()/#code]" mode="nest"/>
</xsl:copy>
</xsl:template>
<!-- output -->
<xsl:template match="/order">
<xsl:copy>
<xsl:for-each select="exsl:node-set($nested)//product">
<xsl:sort select="count(ancestor::*)" data-type="number" order="ascending"/>
<line><product><xsl:copy-of select="#*"/></product></line>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your input, the result is:
<?xml version="1.0" encoding="UTF-8"?>
<order>
<line>
<product code="6" parent="X"/>
</line>
<line>
<product code="1"/>
</line>
<line>
<product code="3" parent="1"/>
</line>
<line>
<product code="2" parent="1"/>
</line>
<line>
<product code="4" parent="2"/>
</line>
</order>
This still leaves the issue of the existing/missing parent X - I will try to address that later.
In a xsl:stylesheet I have this "identity like" transform, to eliminate comments, empty (terminal) tags and empty attributes... But the second xsl:when not works
<xsl:template match="node()">
<xsl:choose>
<xsl:when test="name()='p' and not(./*) and not(normalize-space(.))"></xsl:when>
<xsl:when test="not(name()='img') and not(name()='br') and not(./*) and not(text())"
></xsl:when> <!-- this line NOT WORKS -->
<xsl:otherwise><xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy></xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="#*">
<xsl:choose>
<xsl:when test="not(normalize-space(.))"></xsl:when>
<xsl:otherwise><xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy></xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="comment()"></xsl:template>
Whow to express condition to empty tags in this context?
PS: the "empty rules" are explained here, I try to use it, but not see why not working.
An empty element is an element with no child nodes.
Template match priority is your friend ... the following should be the kind of identity stylesheet that meets your description plus what I think you are doing with image and break elements.
<?xml version="1.0" encoding="US-ASCII"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<!--toss these-->
<xsl:template match="comment() |
*[not(node())] |
#*[not(normalize-space())]"/>
<!--preserve these-->
<xsl:template match="img|br" priority="1">
<xsl:call-template name="identity"/>
</xsl:template>
<!--preserve everything else-->
<xsl:template match="#*|node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
At some point in an XSLT program, I have the following:
<xsl:for-each select="tags/tag">
<xsl:apply-templates select="//shows/show[film=//films/film[tag=current()/#id]/#id]|//shows/show[group=//groups/group[film=//films/film[tag=current()/#id]/#id]/#id]">
<xsl:sort select="date" data-type="text" order="ascending"/>
<xsl:sort select="time" data-type="text" order="ascending"/>
</xsl:apply-templates>
</xsl:for-each>
It seems that the XPath expression //shows/show[film=//films/film[tag=current()/#id]/#id]|//shows/show[group=//groups/group[film=//films/film[tag=current()/#id]/#id]/#id], which is rather complex, considerably slows down the execution of the program (compared to the execution time before adding the quoted piece of code -- processing the same data, of course).
Do you think this is normal due to the relatively complex nature of the expression, and do you see how I could improve it so it performs better?
NB: in the XPath expression, film and //films/film, group and //groups/group refer to distinct elements.
See below a stripped-down sample of the XML input.
<program>
<tags>
<tag id="1">Tag1</tag>
<tag id="2">Tag2</tag>
<tag id="3">Tag3</tag>
</tags>
<films>
<film id="1">
Film1
<tag>2</tag><!-- References: /program/tags/tag/#id=2 -->
</film>
<film id="2">
Film2
<tag>1</tag><!-- References: /program/tags/tag/#id=1 -->
</film>
<film id="3">
Film3
<tag>3</tag><!-- References: /program/tags/tag/#id=3 -->
</film>
<film id="4">
Film4
<tag>3</tag><!-- References: /program/tags/tag/#id=3 -->
</film>
</film>
<groups>
<group id="1">
<film>3</film><!-- References: /program/films/film/#id=3 -->
<film>4</film><!-- References: /program/films/film/#id=4 -->
</group>
</groups>
<shows>
<show id="1"><!-- Show with film (=simple) -->
<film>1</film><!-- References: /program/films/film/#id=1 -->
<date>2011-12-12</date>
<time>12:00</time>
</show>
<show id="2"><!-- Show with group (=combined) -->
<group>1</group><!-- References: /program/groups/group/#id=1 -->
<date>2011-12-12</date>
<time>14:00</time>
</show>
</shows>
</program>
Explanations:
A tag is a property attached to a film (in fact, it's rather a category).
A group is an enumeration of films.
A show references either a film or a group.
What I want: for each tag, I'm looking for the shows referencing a film having the current tag and the shows referencing a group where at least one of the films has the current tag.
Double slashes in XPath are performance and CPU hogs when working with large documents (since every node in the document must be evaluated). If you can replace it with either an absolute or relative path you should have a noticeable improvement. If you can post the input schema and required output, we could be more specific?
e.g. With an absolute path
//shows/show[film=//films/film[tag=current()/#id]/#id]
becomes
/myroot/somepath/shows/show[film=/myroot/somepath/films/film[tag=current()/#id]/#id]
or if the shows and films are relative to the current node
./relativexpath/shows/show[film=./relativexpath/somepath/films/film[tag=current()/#id]/#id]
The answer by nonnb very likely points to the problem, however not really to an efficient solution ("cheaper" axis are better, but that alone doesn't make the speed such as when indexing data).
Note that the big problem is that the XPath expression predicate does another full traversal of the tree for each evaluation. You should use keys for stuff like this; this will (in most or even all XSLT implementations) make an indexed lookup possible, thereby reducing the runtime a lot.
Define keys for the films, groups and shows by id:
<xsl:key name="filmByTag" match="film" use="tag" />
<xsl:key name="groupsByFilm" match="group" use="tag" />
<xsl:key name="showsByFilm" match="show" use="film" />
<xsl:key name="showsByGroup" match="show" use="group" />
And then use it like this (not tested, but you should get the idea):
<xsl:variable name="films" select="key('filmByTag', #id)/#id" />
<xsl:apply-templates select="key('showsByFilm', $films)/#id|key('showsByGroups', key('groupsByFilm', $films)/#id)/#id">
Your XPath expression seems to be doing a three-way join so unless it's optimized the performance is likely to be O(n^3) in the size of the source document. Optimization involves replacing the serial searches of the document by indexed lookups. There are two ways of achieving this: you can hand-optimize it by replacing the filter expressions with calls on the key() function (as indicated by Dimitre), or you can use an optimizing XSLT processor such as Saxon-EE, which should do the same optimizations automatically.
Define a key with xsl:key and then use the key function for the cross reference instead of that comparison you currently have. Show us a sample of the XML so that we can understand its structure, then we can help with concrete code.
Here are two complete solutions that should exhibit better performance:
Do note: Better performance will be registered on sufficiently large input samples only. On small input samples it isn't worth it to optimize.
I. Not using // (but not using keys)
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vFilms" select="/*/films/film"/>
<xsl:variable name="vShows" select="/*/shows/show"/>
<xsl:variable name="vGroups" select="/*/groups/group"/>
<xsl:variable name="vTags" select="/*/tags/tag"/>
<xsl:template match="/*">
<xsl:for-each select="$vTags">
<xsl:apply-templates select=
"$vShows
[film
=
$vFilms
[tag=current()/#id]
/#id
or
group
=
$vGroups
[film
=
$vFilms
[tag=current()/#id]
/#id
]
/#id
]
">
<xsl:sort select="date" data-type="text" order="ascending"/>
<xsl:sort select="time" data-type="text" order="ascending"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="show">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
II. Using keys
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kShowByFilmId" match="show"
use="film"/>
<xsl:key name="kShowByGroupId" match="show"
use="group"/>
<xsl:key name="kGroupByFilmId" match="group"
use="film"/>
<xsl:key name="kFilmByTag" match="film"
use="tag"/>
<xsl:variable name="vTags" select="/*/tags/tag"/>
<xsl:template match="/*">
<xsl:for-each select="$vTags">
<xsl:apply-templates select=
"key('kShowByFilmId',
key('kFilmByTag', current()/#id)/#id
)
|
key('kShowByGroupId',
key('kGroupByFilmId',
key('kFilmByTag', current()/#id)/#id
)
/#id
)
">
<xsl:sort select="date" data-type="text" order="ascending"/>
<xsl:sort select="time" data-type="text" order="ascending"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="show">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>