I am new to transformations between different formats. My goal is to transfer a notation from a toolkit which is in a plain text format to svg. An easy example would be that I have an orange ellipse and the notation would be like this (x and y is the coordinate system so 0 and 0 means the ellipse is in the middle):
GRAPHREP
PEN color:$000000 w:2pt
FILL color:$ff7f00
ELLIPSE x:0pt y:0pt rx:114pt ry:70pt
and my desired output would be an svg code something like this(the cx and cy coordinate are randomly selected for the example):
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g>
<ellipse fill="#ff7f00" stroke="#000000" stroke-width="2" stroke-dasharray="null" stroke-linejoin="null" stroke-linecap="null" cx="250" cy="250" id="svg_1" rx="114" ry="70"/>
</g>
</svg>
I found these two threads Parse text file with XSLT and XSL transform on text to XML with unparsed-text: need more depth
where they transform plain text to xml with XSLT 2.0 and the unparsed-text() function and regex. In my example how would it be possible to get the commands like ELLIPSE(is a
regex which recognizes the all uppercase words possible?) and the parameters(is it possible to get with Xpath from plain text anyhow?)? Is a good implementation doable in XSLT 2.0 or should I
look for another method? Any help would be appreciated!
Below is an example of how you can load the text file using unparsed-text(), and parse the content using xsl:analyze-text to produce an intermediate XML document, and then transform that XML using a "push"-style stylesheet.
It shows an example of how to support ELLIPSE, CIRCLE and RECTANGLE text conversion. You may need to customize it a bit, but should give you an idea of what is possible. With the addition of regex and unparsed-text(), XSLT 2.0 and 3.0 makes all sorts of text transformations possible that would have been extremely cumbersome or difficult in XSLT 1.0.
With a file called "drawing.txt" with the following content:
GRAPHREP
PEN color:$000000 w:2pt
FILL color:$ff7f00
ELLIPSE x:0pt y:0pt rx:114pt ry:70pt
GRAPHREP
PEN color:$000000 w:2pt
FILL color:$ff7f00
CIRCLE x:0pt y:0pt rx:114pt ry:70pt
GRAPHREP
PEN color:$000000 w:2pt
FILL color:$ff7f00
RECTANGLE x:0pt y:0pt width:114pt height:70pt
Executing the following XSLT in the same directory:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:local="local"
exclude-result-prefixes="xs"
version="2.0"
xmlns:svg="http://www.w3.org/2000/svg">
<xsl:output indent="yes"/>
<!--matches sequences of UPPER-CASE letters -->
<xsl:variable name="label-pattern" select="'[A-Z]+'"/>
<!--matches the "attributes" in the line i.e. w:2pt,
has two capture groups (1) => attribute name, (2) => attribute value -->
<xsl:variable name="attribute-pattern" select="'\s?(\S+):(\S+)'"/>
<!--matches a line of data for the drawing text,
has two capture groups (1) => label, (2) attribute data-->
<xsl:variable name="line-pattern" select="concat('(', $label-pattern, ')\s(.*)\n?')"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<svg width="400" height="400">
<g>
<!-- Find the text patterns indicating the shape -->
<xsl:analyze-string select="unparsed-text('drawing.txt')"
regex="{concat('(', $label-pattern, ')\n((', $line-pattern, ')+)\n?')}">
<xsl:matching-substring>
<!--Convert text to XML -->
<xsl:variable name="drawing-markup" as="element()">
<!--Create an element for this group, using first matched pattern as the element name
(i.e. GRAPHREP => <GRAPHREP>) -->
<xsl:element name="{regex-group(1)}">
<!--split the second matched group for this shape into lines by breaking on newline-->
<xsl:variable name="lines" select="tokenize(regex-group(2), '\n')"/>
<xsl:for-each select="$lines">
<!--for each line, run through this process to create an element with attributes
(e.g. FILL color:$frf7f00 => <FILL color=""/>
-->
<xsl:analyze-string select="." regex="{$line-pattern}">
<xsl:matching-substring>
<!--create an element using the UPPER-CASE label starting the line -->
<xsl:element name="{regex-group(1)}">
<!-- capture each of the attributes -->
<xsl:analyze-string select="regex-group(2)" regex="\s?(\S+):(\S+)">
<xsl:matching-substring>
<!--convert foo:bar into attribute foo="bar",
translate $ => #
and remove the letters 'p' and 't' by translating into nothing"-->
<xsl:attribute name="{regex-group(1)}" select="translate(regex-group(2), '$pt', '#')"/>
</xsl:matching-substring>
<xsl:non-matching-substring/>
</xsl:analyze-string>
</xsl:element>
</xsl:matching-substring>
<xsl:non-matching-substring/>
</xsl:analyze-string>
</xsl:for-each>
</xsl:element>
</xsl:variable>
<!--Uncomment the copy-of below if you want to see the intermediate XML $drawing-markup-->
<!--<xsl:copy-of select="$drawing-markup"/>-->
<!-- Transform XML into SVG -->
<xsl:apply-templates select="$drawing-markup"/>
</xsl:matching-substring>
<xsl:non-matching-substring/>
</xsl:analyze-string>
</g>
</svg>
</xsl:template>
<!--==========================================-->
<!-- Templates to convert the $drawing-markup -->
<!--==========================================-->
<!--for supported shapes, create the element using
lower-case value, and change rectangle to rect
for the svg element name-->
<xsl:template match="GRAPHREP[ELLIPSE | CIRCLE | RECTANGLE]">
<xsl:element name="{replace(lower-case(local-name(ELLIPSE | CIRCLE | RECTANGLE)), 'rectangle', 'rect', 'i')}">
<xsl:attribute name="id" select="concat('id_', generate-id())"/>
<xsl:apply-templates />
</xsl:element>
</xsl:template>
<xsl:template match="ELLIPSE | CIRCLE | RECTANGLE"/>
<!-- Just process the content of GRAPHREP.
If there are multiple shapes and you want a new
<svg><g></g></svg> for each shape,
then move it from the template for "/" into this template-->
<xsl:template match="GRAPHREP/*">
<xsl:apply-templates select="#*"/>
</xsl:template>
<xsl:template match="PEN" priority="1">
<!--TODO: test if these attributes exist, if they do, do not create these defaults.
Hard-coding for now, to match desired output, since I don't know what the text
attributes would be, but could wrap each with <xsl:if test="not(#dasharray)">-->
<xsl:attribute name="stroke-dasharray" select="'null'"/>
<xsl:attribute name="stroke-linjoin" select="'null'"/>
<xsl:attribute name="stroke-linecap" select="'null'"/>
<xsl:apply-templates select="#*"/>
</xsl:template>
<!-- conterts #color => #stroke -->
<xsl:template match="PEN/#color">
<xsl:attribute name="stroke" select="."/>
</xsl:template>
<!--converts #w => #stroke-width -->
<xsl:template match="PEN/#w">
<xsl:attribute name="stroke-width" select="."/>
</xsl:template>
<!--converts #color => #fill and replaces $ with # -->
<xsl:template match="FILL/#color">
<xsl:attribute name="fill" select="translate(., '$', '#')"/>
</xsl:template>
<!-- converts #x => #cx with hard-coded values.
May want to use value from text, but matching your example-->
<xsl:template match="ELLIPSE/#x | ELLIPSE/#y">
<!--not sure if there was a relationship between ELLIPSE x:0pt y:0pt, and why 0pt would be 250,
but just an example...-->
<xsl:attribute name="c{name()}" select="250"/>
</xsl:template>
</xsl:stylesheet>
Produces the following SVG output:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:local="local"
xmlns:svg="http://www.w3.org/2000/svg"
width="400"
height="400">
<g>
<ellipse id="id_d2e0"
stroke-dasharray="null"
stroke-linjoin="null"
stroke-linecap="null"
stroke="#000000"
stroke-width="2"
fill="#ff7f00"
cx="250"
cy="250"
rx="114"
ry="70"/>
<circle id="id_d3e0"
stroke-dasharray="null"
stroke-linjoin="null"
stroke-linecap="null"
stroke="#000000"
stroke-width="2"
fill="#ff7f00"
x="0"
y="0"
rx="114"
ry="70"/>
<rect id="id_d4e0"
stroke-dasharray="null"
stroke-linjoin="null"
stroke-linecap="null"
stroke="#000000"
stroke-width="2"
fill="#ff7f00"
x="0"
y="0"
width="114"
height="70"/>
</g>
</svg>
Related
I have a lot of XML files where an export from a database has added certain whitespace via indentation that now I wish to remove in an XSLT 3.0 transformation to a new xml output. I want to remove the whitespace introduced by the export around <lb> and <pb> (in the original files, before export, these two elements abutted other elements without whitespace - a hidden bug in the export indented them).
This is an example of the problem file to transform:
<p xml:id="MS609-0783-LA" xml:lang="LA">
<seg type="dep_event" xml:id="MS609-0783-1">
<pb n="58r"/>
<lb n="1"/>Item.
<date type="deposition_date" when="1245-07-07">Anno Domini M°CC°XL°V° Nonas Iulii</date><persName
nymRef="#ber_r_baz-hg" role="dep">Ber. R.</persName> testis juratus dixit quod vidit
<persName nymRef="#heretics_in_public" ref="her">hereticos</persName>.</seg>
</p>
Here, an example of the desired XML output:
<p xmlns="http://www.tei-c.org/ns/1.0" xml:id="MS609-0783-LA" xml:lang="LA">
<seg type="dep_event" xml:id="MS609-0783-1"><pb n="58r"/><lb n="1"/>Item.
<date type="deposition_date" when="1245-07-07">Anno Domini M°CC°XL°V° Nonas Iulii</date> <persName
nymRef="#ber_r_baz-hg" role="dep">Ber. R.</persName> testis juratus dixit quod vidit
<persName nymRef="#heretics_in_public" ref="her">hereticos</persName>.</seg>
</p>
I thought, naively, that I could target it so:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:tei="http://www.tei-c.org/ns/1.0"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="text()[normalize-space(.) = '']">
<xsl:choose>
<xsl:when test="./following-sibling::tei:pb">
<xsl:text/>
</xsl:when>
<xsl:when test="./following-sibling::tei:lb">
<xsl:text/>
</xsl:when>
<xsl:otherwise>
<xsl:text> </xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
But it does not produce the desired result: https://xsltfiddle.liberty-development.net/93nwMpi
Ideally, I am working towards a solution that strips out any blank white space out before or after <pb> and/or <lb> (when they are abutted by other elements), anywhere inside <seg> or its descendants.
Many thanks in advance for pointers.
I don't have a clear understanding on which text nodes you want to strip but
<xsl:template match="tei:seg//text()[not(normalize-space())][following-sibling::node()[1][self::tei:pb | self::tei:lb]]"/>
would strip white-space only ones followed by pb or lb inside of a seg.
Of course you can extend that to the ones preceded by those elements with e.g.
<xsl:template match="tei:seg//text()[not(normalize-space())][preceding-sibling::node()[1][self::tei:pb | self::tei:lb] or following-sibling::node()[1][self::tei:pb | self::tei:lb]]"/>
If the simple blocking based on match patterns doesn't suffice you might want to try whether your definition of "abutted" can be somehow expressed with group-adjacent and xsl:for-each-group and then drop any white space nodes in a group e.g.
<xsl:template match="tei:seg[tei:pb | tei:lb] | tei:seg//*[tei:pb | tei:lb]">
<xsl:copy>
<xsl:for-each-group select="node()" group-adjacent="boolean(self::text()[not(normalize-space())]|self::tei:pb|self::tei:lb)">
<xsl:choose>
<xsl:when test="current-grouping-key() and current-group()[self::tei:pb|self::tei:lb]">
<xsl:sequence select="current-group()[not(self::text())]"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
Kindly help me to wrap the img.inline element with the following sibling text comma (if comma exists):
text <img id="1" class="inline" src="1.jpg"/> another text.
text <img id="2" class="inline" src="2.jpg"/>, another text.
Should be changed to:
text <img id="1" class="inline" src="1.jpg"/> another text.
text <span class="img-wrap"><img id="2" class="inline" src="2.jpg"/>,</span> another text.
Currently, my XSLT will wrap the img.inline element and add comma inside the span, now I want to remove the following comma.
text <span class="img-wrap"><img id="2" class="inline" src="2.jpg"/>,</span>
, <!--remove this extra comma--> another text.
My XSLT:
<xsl:template match="//img[#class='inline']">
<xsl:copy>
<xsl:choose>
<xsl:when test="starts-with(following-sibling::text(), ',')">
<span class="img-wrap">
<xsl:apply-templates select="node()|#*"/>
<xsl:text>,</xsl:text>
</span>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="node()|#*"/>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
<!-- checking following-sibling::text() -->
<xsl:apply-templates select="following-sibling::text()" mode="commatext"/>
</xsl:template>
<!-- here I want to match the following text, if comma, then remove it -->
<xsl:template match="the following comma" mode="commatext">
<!-- remove comma -->
</xsl:template>
Is my approach is correct? or is this something should be handled differently? pls suggest?
Currently you are copying the img and the embedding the span within that. Also, you do <xsl:apply-templates select="node()|#*"/> which will select child nodes of img (or which there are none). And for the attributes it will end add them to the span.
You don't actually need the xsl:choose here as you can add the condition to the match attribute.
<xsl:template match="//img[#class='inline'][starts-with(following-sibling::node()[1][self::text()], ',')]">
Note I have changed the condition as following-sibling::text() selects ALL text elements that follow the img node. You only want to get the node immediately after the img node, but only if it is a text node.
Also, trying to select the following text node with xsl:apply-templates is probably not the right approach, assuming you have a template that matches the parent node which selects all child nodes (not just img ones). I am assuming you were using the identity template here.
Anyway, try this XSLT instead
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" indent="no" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="//img[#class='inline'][starts-with(following-sibling::node()[1][self::text()], ',')]">
<span class="img-wrap">
<xsl:copy-of select="." />
<xsl:text>,</xsl:text>
</span>
</xsl:template>
<xsl:template match="text()[starts-with(., ',')][preceding-sibling::node()[1][self::img]/#class='inline']">
<xsl:value-of select="substring(., 2)" />
</xsl:template>
</xsl:stylesheet>
My rather well-formed input (I don't want to copy all data):
StartThing
Size Big
Colour Blue
coords 42, 42
foo bar
EndThing
StartThing
Size Small
Colour Red
coords 29, 51
machin bidule
EndThing
<!-- repeat a few thousand times-->
I have the below XSL which I modified from Parse text file with XSLT
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="text-encoding" as="xs:string" select="'iso-8859-1'"/>
<xsl:param name="text-uri" as="xs:string" select="'unparsed-text.txt'"/>
<xsl:template name="text2xml">
<xsl:variable name="text" select="unparsed-text($text-uri, $text-encoding)"/>
<xsl:analyze-string select="$text" regex="(Size|Colour|coords) (.+)">
<xsl:matching-substring>
<xsl:element name="{(regex-group(1))}">
<xsl:value-of select="(regex-group(2))"/>
</xsl:element>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:template>
<xsl:template match="/">
<xsl:call-template name="text2xml"/>
</xsl:template>
</xsl:stylesheet>
and it works fine on parsing the pairs into elements and values. It gives me this output:
<?xml version="1.0" encoding="UTF-8"?>
<Size>Big</Size>
<Colour>Blue</Colour>
<coords>42, 42</coords>
But I'd also like to wrap the values in the Thing tag so that my output looks like this:
<Thing>
<Size>Big</Size>
<Colour>Blue</Colour>
<coords>42, 42</coords>
</Thing>
One solution might be a regex that matches each group of lines after each "thing". Then matches substrings as I'm already doing. Or is there some other way to parse the tree?
I would use two nested analyze-string levels, an outer one to extract everything between StartThing and EndThing, and then an inner one that operates on the strings matched by the outer one.
<xsl:template name="text2xml">
<xsl:variable name="text" select="unparsed-text($text-uri, $text-encoding)"/>
<!-- flags="s" allows .*? to match across newlines -->
<xsl:analyze-string select="$text" regex="StartThing.*?EndThing" flags="s">
<xsl:matching-substring>
<Thing>
<!-- "." here is the matching substring from the outer regex -->
<xsl:analyze-string select="." regex="(Size|Colour|coords) (.+)">
<xsl:matching-substring>
<xsl:element name="{(regex-group(1))}">
<xsl:value-of select="(regex-group(2))"/>
</xsl:element>
</xsl:matching-substring>
</xsl:analyze-string>
</Thing>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:template>
I am struggling with xslt from the past 2 days, owing to my starter status.My requirement is that given any input XML file ,I want the output to be a list of all the XPaths of all the tags in order in which they appear in the original XML document(parent, then parent,parents Attributes list/child, parent/child/childOFchild and so forth). THe XSLT should not be specific to any single XMl schema. It should work for any XML file, which is a valid one.
Ex:
If the Input XML Is :
<v1:Root>
<v1:UserID>test</v1:UserID>
<v1:Destination>test</v1:Destination>
<v1:entity name="entiTyName">
<v11:attribute name="entiTyName"/>
<v11:attribute name="entiTyName"/>
<v11:attribute name="entiTyName"/>
<v11:filter type="entiTyName">
<v11:condition attribute="entiTyName" operator="eq" value="{FB8D669E-D090-E011-8F43-0050568E222C}"/>
<v11:condition attribute="entiTyName" operator="eq" value="1"/>
</v11:filter>
<v11:filter type="or">
<v11:filter type="or">
<v11:filter type="and">
<v11:filter type="and">
<v11:condition attribute="cir_customerissuecode" operator="not-like" value="03%"/>
</v11:filter>
</v11:filter>
</v11:filter>
</v11:filter>
</v1:entity>
</v1:Root>
I want my output to be :
/v1:Root/v1:UserID
/v1:Root/v1:Destination
/v1:Root/v1:entity/#name
/v1:Root/v1:entity/v11:attribute
/v1:Root/v1:entity/v11:attribute/#name
/v1:Root/v1:entity/v11:attribute[2]
/v1:Root/v1:entity/v11:attribute[2]/#name
/v1:Root/v1:entity/v11:attribute[3]
/v1:Root/v1:entity/v11:attribute[3]/#name
/v1:Root/v1:entity/v11:filter/#type
/v1:Root/v1:entity/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter/v11:condition/#attribute
/v1:Root/v1:entity/v11:filter/v11:condition/#operator
/v1:Root/v1:entity/v11:filter/v11:condition/#value
/v1:Root/v1:entity/v11:filter/v11:condition[2]
/v1:Root/v1:entity/v11:filter/v11:condition[2]/#attribute
/v1:Root/v1:entity/v11:filter/v11:condition[2]/#operator
/v1:Root/v1:entity/v11:filter/v11:condition[2]/#value
/v1:Root/v1:entity/v11:filter[2]/v11:filter/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition/#attribute
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition/#operator
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition/#value
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/#type
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition/#attribute
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition/#operator
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition/#value
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]/#attribute
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]/#operator
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]/#value
So, it is basically all the XPath of each element ,then the Xpath of the elements Attributes.
I have an XSLT with me, which is like this:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no" />
<xsl:template match="*[not(child::*)]">
<xsl:for-each select="ancestor-or-self::*">
<xsl:value-of select="concat('/', name())" />
<xsl:if test="count(preceding-sibling::*[name() = name(current())]) != 0">
<xsl:value-of
select="concat('[', count(preceding-sibling::*[name() = name(current())]) + 1, ']')" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="*" />
</xsl:template>
<xsl:template match="/">
<xsl:apply-templates select="*" />
</xsl:template>
</xsl:stylesheet>
THe output which gets Produced does not cater to complex tags and also the tag's attributes in the resulting Xpath list :(.
Kindly help me in fixing this xslt to produce the output as mentioned above.
THe present output from the above XSLT is like this :
/v1:Root/v1:UserID
/v1:Root/v1:Destination
/v1:Root/v1:entity/v11:attribute
/v1:Root/v1:entity/v11:attribute[2]
/v1:Root/v1:entity/v11:attribute[3]
/v1:Root/v1:entity/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter/v11:filter/v11:filter[2]/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter[2]/v11:condition
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter[2]/v11:condition[2]
/v1:Root/v1:entity/v11:filter[2]/v11:filter[2]/v11:filter[2]/v11:condition[3]
I think there's a discrepancy between your sample input and output, in that the output describes a filter element with two conditions that's not in the source XML. At any rate, I believe this works:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no" />
<!-- Handle attributes -->
<xsl:template match="#*">
<xsl:apply-templates select="ancestor-or-self::*" mode="buildPath" />
<xsl:value-of select="concat('/#', name())"/>
<xsl:text>
</xsl:text>
</xsl:template>
<!-- Handle non-leaf elements (just pass processing downwards) -->
<xsl:template match="*[#* and *]">
<xsl:apply-templates select="#* | *" />
</xsl:template>
<!-- Handle leaf elements -->
<xsl:template match="*[not(*)]">
<xsl:apply-templates select="ancestor-or-self::*" mode="buildPath" />
<xsl:text>
</xsl:text>
<xsl:apply-templates select="#*" />
</xsl:template>
<!-- Outputs a path segment for the matched element: '/' + name() + [ordinalPredicate > 1] -->
<xsl:template match="*" mode="buildPath">
<xsl:value-of select="concat('/', name())" />
<xsl:variable name="sameNameSiblings" select="preceding-sibling::*[name() = name(current())]" />
<xsl:if test="$sameNameSiblings">
<xsl:value-of select="concat('[', count($sameNameSiblings) + 1, ']')" />
</xsl:if>
</xsl:template>
<!-- Ignore text -->
<xsl:template match="text()" />
</xsl:stylesheet>
How I can get first n characters with XSLT 1.0 from XHTML? I'm trying to create introduction text for news.
Everything is UTF-8
HTML entity aware ( &), one entity = one character
HTML tag aware (adds missing end tags)
Input HTML is always valid
If input text is over n chars add '...' to end output
Input tags are restricted to: a, img, p, div, span, b, strong
Example input HTML:
<img src="image.jpg" alt="">text link here
Example output with 9 characters:
<img src="image.jpg" alt="">text link...
Example input HTML:
<p>link here text</p>
Example output with 4 characters:
<p>link...</p>
Here is a starting point, although it currently doesn't contain any code to handle the requirement "Input tags are restricted to: a, img, p, div, span, b, strong"
It works by looping through the child nodes of a node, and totalling the length of the preceding siblings up to that point. Note that the code to get the length of the preceding siblings requires the use of the node-set function, which is an extension function to XSLT 1.0. In my example I am using Microsoft Extension function.
Where a node is not a text node, the total length of characters up to that point will be the sum of the lengths of the preceding siblings, put the sum of the preceding siblings of the parent node (which is passed as a parameter to the template).
Here is the XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:param name="MAXCHARS">9</xsl:param>
<xsl:template match="/body">
<xsl:apply-templates select="child::node()"/>
</xsl:template>
<xsl:template match="node()">
<xsl:param name="LengthToParent">0</xsl:param>
<!-- Get length of previous siblings -->
<xsl:variable name="previousSizes">
<xsl:for-each select="preceding-sibling::node()">
<length>
<xsl:value-of select="string-length(.)"/>
</length>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="LengthToNode" select="sum(msxsl:node-set($previousSizes)/length)"/>
<!-- Total amount of characters processed so far -->
<xsl:variable name="LengthSoFar" select="$LengthToNode + number($LengthToParent)"/>
<!-- Check limit is not exceeded -->
<xsl:if test="$LengthSoFar < number($MAXCHARS)">
<xsl:choose>
<xsl:when test="self::text()">
<!-- Output text nonde with ... if required -->
<xsl:value-of select="substring(., 1, number($MAXCHARS) - $LengthSoFar)"/>
<xsl:if test="string-length(.) > number($MAXCHARS) - $LengthSoFar">...</xsl:if>
</xsl:when>
<xsl:otherwise>
<!-- Output copy of node and recursively call template on its children -->
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates select="child::node()">
<xsl:with-param name="LengthToParent" select="$LengthSoFar"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When applied to this input
<body>
<img src="image.jpg" alt="" />text link here
</body>
The output is:
<body>
<img src="image.jpg" alt="" />text link...
</body>
When applied to this input (and changing the parameter to 4 in the XSLT)
<p>link here text</p>
The output is:
<p>link...</p>
This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="pMaxLength" select="4"/>
<xsl:template match="node()">
<xsl:param name="pPrecedingLength" select="0"/>
<xsl:variable name="vContent">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates select="node()[1]">
<xsl:with-param name="pPrecedingLength"
select="$pPrecedingLength"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:variable>
<xsl:variable name="vLength"
select="$pPrecedingLength + string-length($vContent)"/>
<xsl:if test="$pMaxLength + 3 >= $vLength and
(string-length($vContent) or not(node()))">
<xsl:copy-of select="$vContent"/>
<xsl:apply-templates select="following-sibling::node()[1]">
<xsl:with-param name="pPrecedingLength" select="$vLength"/>
</xsl:apply-templates>
</xsl:if>
</xsl:template>
<xsl:template match="text()" priority="1">
<xsl:param name="pPrecedingLength" select="0"/>
<xsl:variable name="vOutput"
select="substring(.,1,$pMaxLength - $pPrecedingLength)"/>
<xsl:variable name="vSumLength"
select="$pPrecedingLength + string-length($vOutput)"/>
<xsl:value-of select="concat($vOutput,
substring('...',
1 div ($pMaxLength
= $vSumLength)))"/>
<xsl:apply-templates select="following-sibling::node()[1]">
<xsl:with-param name="pPrecedingLength"
select="$vSumLength"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
With this input and 9 as pMaxLength:
<html><img src="image.jpg" alt=""/>text link here</html>
Output:
<html><img src="image.jpg" alt="">text link...</html>
And this input with 4 as pMaxLength:
<html><p>link here text</p></html>
Output:
<html><p>link...</p></html>
As indicated by many: this gets very messy very fast. So I just added another field to DB which has the introduction text.