I need to replace standalone attributes width="<>" and height="<>" with style="width:<>; height:<>;.
A typical input is the following
<img width="32pt" alt="PIC" class="graphics" height="32pt" src="images/about.png"/>
Thus the result should be
<img style="width:32pt; height:32pt;" alt="PIC" class="graphics" src="images/about.png"/>
The problem is that I've never worked with xsl before.
What I know, as far, is that
this can catch the img element
<xsl:template match="xh:img">
this can catch the width attribute
<xsl:template match="#width">
and I know how to add an attribute or element.
But I don't know how to store the value of the width and height and write both in a single line.
Any help would be appreciated.
You could use:
<xsl:template match="img">
<img style="width:{#width}; height:{#height};">
<xsl:copy-of select="#*[not(name()='width' or name()='height')]"/>
</img>
</xsl:template>
This is assuming every img has both width and height attributes, and no child nodes. If your source XML places the img elements in a namespace (not shown in your question), add the appropriate prefix to the template's match pattern.
For reference, see: https://www.w3.org/TR/xslt/#attribute-value-templates
You (or other readers) may be interested in this more general (XSLT 2.0) solution taken from a production stylesheet:
<xsl:template name="style-attributes">
<xsl:choose>
<xsl:when test="#style | #class">
<xsl:copy-of select="#style | #class"/>
</xsl:when>
<xsl:when test="#*[f:is-style-attribute(.)]">
<xsl:attribute name="style">
<xsl:for-each select="#*[f:is-style-attribute(.)]">
<xsl:if test="position() gt 1">; </xsl:if>
<xsl:apply-templates select="." mode="style-attribute"/>
</xsl:for-each>
</xsl:attribute>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:function name="f:is-style-attribute" as="xs:boolean">
<xsl:param name="att" as="attribute(*)"/>
<xsl:variable name="n" select="local-name($att)"/>
<xsl:sequence select="namespace-uri($att) = '' and $n = ('class', 'style', 'border', 'width', 'cellspacing', 'padding', 'cellpadding', 'align', 'valign')"/>
</xsl:function>
<xsl:function name="f:units" as="xs:string">
<xsl:param name="value" as="xs:string"/>
<xsl:sequence select="if ($value castable as xs:integer) then concat($value, 'px') else $value"/>
</xsl:function>
<xsl:template match="#border" mode="style-attribute">border:<xsl:value-of select="f:units(.)"/> solid</xsl:template>
<xsl:template match="#width" mode="style-attribute">width:<xsl:value-of select="f:units(.)"/></xsl:template>
<xsl:template match="#align" mode="style-attribute">text-align:<xsl:value-of select="."/></xsl:template>
<xsl:template match="#valign" mode="style-attribute">vertical-align:<xsl:value-of select="."/></xsl:template>
<xsl:template match="#cellspacing" mode="style-attribute">border-spacing:<xsl:value-of select="."/></xsl:template>
<xsl:template match="#padding" mode="style-attribute">padding:<xsl:value-of select="f:units(.)"/></xsl:template>
<xsl:template match="#cellpadding" mode="style-attribute">padding:<xsl:value-of select="f:units(.)"/></xsl:template>
... etc (as required)
Related
In first template, I am intentionally excluding an element ('milk') because the parsed data map is relatively flat and I would like to use XSLT to categorize and structure the data. The aim is to process the excluded element ('milk') in the second template. The both templates works running them one at a time. Running the templates together will not show the result of the excluded element ('milk') which should set another attribute name and attribute value.
JSON:
<data>
{
"storage": {
"pencils": 12,
"milk": 8,
"rulers": 4
}
}
</data>
XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform
version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:storage="http://www.exammple.com/1"
xmlns:office="http://www.exammple.com/2"
xmlns:item="http://www.exammple.com/3"
expand-text="yes">
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- Parse JSON to XML -->
<xsl:template match="data">
<storage:one>
<xsl:apply-templates select="json-to-xml(.)"/>
</storage:one>
</xsl:template>
<!-- Print map -->
<!-- <xsl:template match="*[#key = 'storage']"> <xsl:copy-of select=".."/> </xsl:template> -->
<xsl:template match="*[#key='storage']">
<xsl:for-each select="*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
<xsl:template match="*[#key='milk']">
<xsl:for-each select=".">
<xsl:element name="item:{#key}">
<xsl:attribute name="beverage">plant-2</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:transform>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<storage:one xmlns:item="http://www.exammple.com/3"
xmlns:office="http://www.exammple.com/2"
xmlns:storage="http://www.exammple.com/1">
<item:pencils office="plant-1">12</item:pencils>
<item:rulers office="plant-1">4</item:rulers>
</storage:one>
Wanted result:
<?xml version="1.0" encoding="UTF-8"?>
<storage:one xmlns:item="http://www.exammple.com/3"
xmlns:office="http://www.exammple.com/2"
xmlns:storage="http://www.exammple.com/1">
<item:pencils office="plant-1">12</item:pencils>
<item:rulers office="plant-1">4</item:rulers>
<item:milk beverage="plant-2">8</item:milk>
</storage:one>
Your second template is never matched, because it is never reached. All elements are processed by <xsl:template match="*[#key='storage']"> - which doesn't have an <xsl:apply-templates ...> to reach further templates.
Your first template does not recurse into its children. So add an <xsl:apply-templates select="*" /> to the end of the first template:
<xsl:template match="*[#key='storage']">
<xsl:for-each select="*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
<xsl:apply-templates select="*" />
</xsl:template>
This will try to apply further templates at the level of "storage" and therefore match the second template.
I would write templates for each different output type and if the order of the output is different from the order of the input throw in an xsl:sort or an XPath 3.1 sort call to change the order:
<xsl:template match="data">
<storage:one>
<xsl:apply-templates select="json-to-xml(.)"/>
</storage:one>
</xsl:template>
<xsl:template match="*[#key = 'storage']">
<xsl:apply-templates select="sort(*, (), function($el) { $el/#key = 'milk' })"/>
</xsl:template>
<xsl:template match="*[#key='storage']/*[not(#key='milk')]">
<xsl:element name="item:{#key}">
<xsl:attribute name="office">plant-1</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
<xsl:template match="*[#key='storage']/*[#key='milk']">
<xsl:element name="item:{#key}">
<xsl:attribute name="beverage">plant-2</xsl:attribute>
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
I got a question, I try to convert an XML to JSON using XSLT version 1.0.
As far as the name goes I get it right but the value is another story.
<datapost>
<fields>
<field>
<id>emailId</id>
<name>emailName</name>
<values>
<value>info#example.com</value>
</values>
</field>
</fields>
AS IT CURRENTLY IS:
At the moment I get only the "name" correct but the "value" (emailIdName & emailId & info#example.com) is all the values squashed together what I obviously don't want.
{
"emailName":{
"emailIdemailNameinfo#example.com"
}
}
EXPECTED TO BE:
I want to get only the "name" and the "value" in values (info#example.com)
This is the result that I WANT to get:
{
"emailName":{
"info#example.com"
}
}
This is the code I use:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="no" encoding="UTF-16" omit-xml-declaration="yes"/>
<xsl:template match="/">
<body>{
<xsl:call-template name="fieldsName"></xsl:call-template>
<xsl:text>
</xsl:text>
</body>
</xsl:template>
<xsl:template name="fieldsName">
<xsl:for-each select="//datapost[position()=1]/fields/field">
"
<xsl:value-of select="name"/>" :
<xsl:call-template name="fieldsValue"/>}
</xsl:for-each>
</xsl:template>
<!-- Array Element -->
<xsl:template match="*" mode="ArrayElement">
<xsl:call-template name="fieldsValue"/>
</xsl:template>
<!-- Object Properties -->
<xsl:template name="fieldsValue">
<xsl:variable name="childName" select="//datapost[position()=1]/fields/field/values/value"/>
<xsl:choose>
<xsl:when test="not(*|#*)">"
<xsl:value-of select="."/>"
</xsl:when>
<xsl:when test="count(*[name()=$childName]) > 1">{ "
<xsl:value-of select="$childName"/>" :[
<xsl:apply-templates select="*" mode="ArrayElement"/>] }
</xsl:when>
<xsl:otherwise>{
<xsl:apply-templates select="#*"/>
<xsl:apply-templates select="*"/>
}
</xsl:otherwise>
</xsl:choose>
<xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>
<!-- Attribute Property -->
<xsl:template match="#*">"
<xsl:value-of select="name"/>" : "
<xsl:value-of select="."/>",
</xsl:template>
</xsl:stylesheet>
The element names in your XML are poorly chosen, and this has probably confused you (it certainly confused me). One would expect "field" to contain a single field, but it actually contains all of them. So change
<xsl:for-each select="//datapost[position()=1]/fields/field">
to
<xsl:for-each select="//datapost[position()=1]/fields/field/*">
or better still,
<xsl:for-each select="datapost/fields/field/*">
since the other parts of the expression are redundant verbiage.
Then you need to look at the template containing the variable
<xsl:variable name="childName" select="//datapost[position()=1]/fields/field/values/value"/>
This is selecting all the values in the document, not just the values of the current element. I'm not entirely sure what you're trying to achieve here, and it doesn't seem to be covered by your test data, but I suspect you're trying to do some kind of grouping of elements that have the same name. For that you need to use grouping facilities: xsl:for-each-group in XSLT 2.0+, Muenchian grouping in XSLT 1.0. You haven't said which XSLT version you're using, but all of this would be an awful lot easier if you used XSLT 2.0+.
I need of display BIG on planets with diameter higher than average diameter or SMALL without using avg function (XSLT 1.0)
I have tried to use xsl:when with a condition like diameter > sum(....) div count(nom), but it doesn't work :
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="/systeme_solaire">
<html lang="fr">
<head>
<title>Les planètes</title>
</head>
<body>
<xsl:apply-templates select="planete[nom!='Terre']">
<xsl:sort select ="diametre" order="descending" data-type="number" />
</xsl:apply-templates>
</body>
</html>
</xsl:template>
<xsl:template match="planete" >
<ul>
<p><b><xsl:value-of select="nom"/> : </b></p>
<li>Distance au soleil: <xsl:value-of select="distance"/><xsl:value-of select="distance/#unit"/></li>
<li>Masse: <xsl:value-of select="masse"/> <xsl:value-of select="masse/#unit"/></li>
<li>
<xsl:choose>
<xsl:when test="diametre > ((sum(diametre[unit='diamètre terrestre']*sum(diametre[unit='km']))+sum(diametre[unit='km'])) div count(nom))">
BIG
</xsl:when>
<xsl:otherwise>
SMALL
</xsl:otherwise>
</xsl:choose> Diamètre: <xsl:value-of select="diametre"/> <xsl:value-of select="diametre/#unit"/></li>
<xsl:if test="satellite>0"><li>Nombre de satellites: <xsl:value-of select="satellite"/></li></xsl:if>
</ul>
</xsl:template>
</xsl:stylesheet>
XML file used (diameter of planets differents from earth are defined according to earth diameter ratio) :
<?xml version="1.0" encoding="ISO-8859-1" ?>
<systeme_solaire>
<planete type="tellurique">
<nom>Vénus</nom>
<distance unit="UA" >0.7</distance>
<masse unit="masse terrestre">0.8</masse>
<diametre unit="diamètre terrestre">0.9</diametre>
</planete>
<planete type="tellurique">
<nom>Terre</nom>
<distance unit="km" >149600000</distance>
<masse unit="kg">5.98e24</masse>
<diametre unit="km">12756</diametre>
<satellite>1</satellite>
</planete>
<planete type="tellurique">
<nom>Mars</nom>
<distance unit="UA" >1.5</distance>
<masse unit="masse terrestre">0.1</masse>
<diametre unit="diamètre terrestre">0.5</diametre>
<satellite>2</satellite>
</planete>
</systeme_solaire>
You have two problems :
First you need to calculate you average diameter outside the
template planete in order to reach all planets then pass this
average to you template
Then you xpath is incorrect : wrong parenthesis, unit is an attribute so you need to use #. You need something like this :
((sum(//diametre[#unit='diamètre
terrestre'])*//diametre[#unit='km'])+//diametre[#unit='km']) div
count(//nom)
Edit : You also need to calculate the actual diameter of your current planet base on earth diameter, you can do this by adding another parameter <xsl:with-param name="terre" select="//diametre[#unit='km']"/> and using it <xsl:when test="diametre*$terre > $avg">
I've updated you XSLT like so :
Solution 1
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="/systeme_solaire">
<html lang="fr">
<head>
<title>Les planètes</title>
</head>
<body>
<xsl:apply-templates select="planete[nom!='Terre']">
<xsl:sort select ="diametre" order="descending" data-type="number" />
<xsl:with-param name="avg" select="((sum(//diametre[#unit='diamètre terrestre'])*//diametre[#unit='km'])+//diametre[#unit='km']) div count(//nom)"/>
<xsl:with-param name="terre" select="//diametre[#unit='km']"/>
</xsl:apply-templates>
</body>
</html>
</xsl:template>
<xsl:template match="planete" >
<xsl:param name="avg"/>
<xsl:param name="terre"/>
<ul>
<p><b><xsl:value-of select="nom"/> : </b></p>
<li>Distance au soleil: <xsl:value-of select="distance"/><xsl:value-of select="distance/#unit"/></li>
<li>Masse: <xsl:value-of select="masse"/> <xsl:value-of select="masse/#unit"/></li>
<li>
<xsl:choose>
<xsl:when test="diametre*$terre > $avg">
BIG
</xsl:when>
<xsl:otherwise>
SMALL
</xsl:otherwise>
</xsl:choose> Diamètre: <xsl:value-of select="diametre"/> <xsl:value-of select="diametre/#unit"/></li>
<xsl:if test="satellite>0"><li>Nombre de satellites: <xsl:value-of select="satellite"/></li></xsl:if>
</ul>
</xsl:template>
</xsl:stylesheet>
Edit to add michael suggestion (calculate the average diameter as the ratio) :
Solution 2
<xsl:apply-templates select="planete[nom!='Terre']">
<xsl:sort select ="diametre" order="descending" data-type="number" />
<xsl:with-param name="avg" select="(sum(//diametre[#unit='diamètre terrestre'])+1) div count(//nom)"/>
</xsl:apply-templates>
...
<xsl:template match="planete" >
<xsl:param name="avg"/>
...
<xsl:when test="diametre > $avg">
...
I have a node like this in an XML file that I transform with XSLT 2.0:
<h2><span class='sun'>☼☼</span> my text (G1.2)</h2>
which includes some HTML special characters, as you see.
Now I need to generate XHTML like this:
my text
So I have to strip out the span and the stuff between () and use the rest to generate the h2 header.
To strip the (), I have this:
<xsl:value-of select="normalize-space(replace( . ,'\([^\)]*\)' ,''))"/>
which works OK. But to strip the span, I cannot use
<xsl:template match="span[#class='sun']"/>
Because I do not apply templates after the xsl:value-of anymore. So the span-template is never applied.
Can I strip the span in the same line? If not, how can I strip the span also?
Or can I replace the special characters in the same replace function somehow? Then I would be left with an empty span element, but that's not a problem.
You can match the child::text() contents of <h1> separately from the child::span elements. This should work:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="span[#class='sun']"/>
<xsl:template match="h2/text()">
<a href="">
<xsl:value-of select="normalize-space(replace( . ,'\([^\)]*\)' ,''))"/>
</a>
</xsl:template>
</xsl:stylesheet>
Here is the template thus far. It (toct) generates a table of contents from a HTML h1/h2/h3 structure recursively.
It is applied like this:
<xsl:call-template name="toct">
<xsl:with-param name="nodes" select="document(file)//(h1|h2|h3)"/>
<xsl:with-param name="file" select="replace (file,'xml', 'xhtml')"/>
</xsl:call-template>
<xsl:template name="toct">
<xsl:param name="nodes"/>
<xsl:param name="file"/>
<xsl:if test="count($nodes) > 0">
<xsl:for-each-group select="$nodes" group-starting-with="*[local-name() = name($nodes[1])]">
<li>
<!-- do not include empty header tags in the TOC -->
<a>
<xsl:choose>
<xsl:when test="text()">
<xsl:if test="name($nodes[1])='h1'">
<xsl:attribute name="id"><xsl:value-of select="$nodes[1]/#id"/></xsl:attribute>
</xsl:if>
<xsl:attribute name="href"><xsl:value-of select="$file"/>#<xsl:value-of select="#id"/></xsl:attribute>
<xsl:value-of select="normalize-space(replace(replace( string-join(node(),'') , '[☼*]' ,'') ,'\([^\)]*\)' ,''))"/>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="class">invisible</xsl:attribute>
<xsl:text>-</xsl:text>
</xsl:otherwise>
</xsl:choose>
</a>
<xsl:if test="current-group()[2]">
<ol>
<xsl:call-template name="toct">
<!-- strip the first node in the list. This is why you can use $nodes[1] to find out
which level of <h_> tags you are at -->
<xsl:with-param name="nodes" select="current-group() except ."/>
<xsl:with-param name="file" select="$file"/>
</xsl:call-template>
</ol>
</xsl:if>
</li>
</xsl:for-each-group>
</xsl:if>
</xsl:template>
How can I transform the xml in format1 to a string as mentioned in format2 using XSLT?
format 1
<children>
<child data="test1">
<content><name>test1</name>
<child>
<content><name>test1child</name>
</child>
</child>
</children>
format 2
"<root>"
+"<item id='test1'>"
+"<content><name>test1</name></content>"
+"<item parent_id='test1'>"
+"<content><name>test1child</name>"
+"</content>"
+"</item>"
+</item>
+"<root>"
so children should replace with root, child should replaced with item and child of child should be replaced with *item parent_id of parent id*. Is it possible to do with xslt?
Assuming XSLT 2.0, here is some suggestion on how to approach that, assuming you really want some string output with quotes and plus symbols (to get Javascript or similar code to construct XML as a string?):
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:output method="text"/>
<xsl:template match="*">
<xsl:param name="name" select="name()"/>
<xsl:text><</xsl:text>
<xsl:value-of select="$name"/>
<xsl:apply-templates select="#*"/>
<xsl:text>></xsl:text>
<xsl:apply-templates/>
<xsl:text></</xsl:text>
<xsl:value-of select="$name"/>
<xsl:text>></xsl:text>
</xsl:template>
<xsl:template match="#*">
<xsl:param name="name" select="name()"/>
<xsl:text> </xsl:text>
<xsl:value-of select="$name"/>
<xsl:text>='</xsl:text>
<xsl:value-of select="."/>
<xsl:text>'</xsl:text>
</xsl:template>
<xsl:template match="text()[matches(., '^\s+$')]">
<xsl:text>"
+"</xsl:text>
</xsl:template>
<xsl:template match="/children">
<xsl:text>"</xsl:text>
<xsl:next-match>
<xsl:with-param name="name" select="'root'"/>
</xsl:next-match>
<xsl:text>"</xsl:text>
</xsl:template>
<xsl:template match="child">
<xsl:next-match>
<xsl:with-param name="name" select="'item'"/>
</xsl:next-match>
</xsl:template>
<xsl:template match="child//child">
<xsl:variable name="copy" as="element()">
<xsl:copy>
<xsl:attribute name="parent_id" select="ancestor::child[1]/#data"/>
<xsl:copy-of select="#* , node()"/>
</xsl:copy>
</xsl:variable>
<xsl:apply-templates select="$copy"/>
</xsl:template>
</xsl:stylesheet>
That transforms the input
<children>
<child data="test1">
<content><name>test1</name></content>
<child>
<content><name>test1child</name></content>
</child>
</child>
</children>
into the result
"<root>"
+"<item data='test1'>"
+"<content><name>test1</name></content>"
+"<item parent_id='test1'>"
+"<content><name>test1child</name></content>"
+"</item>"
+"</item>"
+"</root>"
The code so far has many shortcomings however as it would not construct well-formed XML with properly escaped attribute values and also does not care to output namespace declarations. You could however adapt more sophisticated solutions like http://lenzconsulting.com/xml-to-string/.