Insert image on first occurence - xslt

I have a xml document which looks something like this:
<chapter>
<para>Just a random text<cross-ref refid="1234">Abb. 1.0</cross-ref>Some more text</para>
<section-title>Title</section-title>
<para>and more text text ext<cross-ref refif="1234">Abb 1.0</cross-ref>more more more</para>
</chapter>
As you can see there are two cross-ref elements inside paragraphs. They can occur basically everywhere and are somewhat identified by their refid(but not uniquely). What i am currently trying to do is inserting an image (based on the refid) at the position of the first occurence while keeping the text as a caption. Every other occurence (which are not the first) should just be inline texts containing an internal basic-link to that inserted image.
My current solution is:
<xsl:template match="cross-ref">
<xsl:choose>
<xsl:when test="position() = 1">
<fo:block text-align="center" id="{#refid}">
<xsl:variable name="refVar" select="#refid"/>
<xsl:variable name="imageName" select="/chapter/floats/figure[#id=$refVar]/link/#locator" />
<fo:external-graphic src="url({concat($imageName, '.jpg')})" />
<fo:block text-align="center" xsl:use-attribute-sets="lit-para">
<xsl:value-of select="current()" />
</fo:block>
</fo:block>
</xsl:when>
<xsl:otherwise>
<fo:basic-link internal-destination="{#refid}">
<xsl:value-of select="current()" />
</fo:basic-link>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
It does work on some cases, but since position() is not always 1 some images are not getting inserted correctly. What are my options?
Thank you!
EDIT: I should clarify. The image should get inserted at the first occurence of a "new" refid. Thus. Each refid only has one image and every other cross-refelement with the same refid points to that image

You have to change the test in your xsl:when, so that it is true only for the first occurrence of each #ref-id value; in other words, you have to check that no preceding cross-ref element has the same #ref-id:
<xsl:when test="not(preceding::cross-ref[#ref-id = current()/#ref_id])">
...

If you are using XSLT 2.0 or XSLT 3.0, if you add an xsl:key as a top-level element:
<xsl:key name="cross-ref" match="cross-ref" use="#refid" />
then you can change your xsl:when to:
<xsl:when test=". is key('cross-ref', #refid)[1]">
This works because key() returns nodes in document order (https://www.w3.org/TR/xslt20/#keys). This is potentially quicker (on large documents) than using the preceding axis, but to be sure you'd have to test it by running on your documents with your XSLT processor.
If you're using XSLT 1.0, you'd have to do it using a Meunchian Grouping-like trick:
<xsl:when test="count(. | key('cross-ref', #refid)[1]) = 1">
but that is much less readable than the XSLT 2.0 version.

Related

Duplicates in a map

I currently have an XSLT function that loads key=value pairs from a text file into a map.
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
The function works fine unless the user tries to load a file containing duplicates, in which case the XSLT transform fails with an error (expected behaviour):
Error evaluating (map:merge(...)) on line xyz column xy of xyz.xsl:
XTDE3365: Duplicate key in constructed map: {keyInError}
Is there a way I could catch this case and keep the transformation from aborting, something like this :
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:choose>
<xsl:when test="...map contains key...">
<xsl:message>Map already contains key. Please check input file.</xsl:message>
</xsl:when>
<xsl:otherwise>
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
I see that there is something implemented for a future XSLT 4.0 release (Saxon - Controlling duplicates on xsl:map) but I would like to stick to XSLT 3.0 for the time being.
Thanks.
To add to Martin Honnen's suggestions, you could use xsl:iterate instead of xsl:for-each, passing the map as a parameter, which would allow you to inspect the map before adding another entry to it.
<xsl:iterate select="...">
<xsl:param name="map" select="map{}"/>
<xsl:choose>
<xsl:when test="map:contains($map, ...)">...</xsl:when>
<xsl:otherwise>
<xsl:next-iteration>
<xsl:with-param name="map" select="map:put($map, ..., ...)"/>
Well, both map:merge in XPath 3.1 or of course grouping with e.g.
<xsl:for-each-group select="unparsed-text-lines($inputFile,$fileEncoding)[contains(.,'=') and not(starts-with(.,'#'))]" group-by="substring-before(., '=')">
<xsl:map-entry key="current-grouping-key()" select="substring-after(., '=')"/>
<xsl:if test="current-group()[2]">
<xsl:message>..</xsl:message>
</xsl:if>
</xsl:for-each-group>
allow you more control than your approach without having to wait for XSLT 4 or trying to use experimental extensions.

Create empty Column space when <xsl:otherwise> is true

I am transforming an XML into PDF Document using XSLT 1.0 and Groovy. When my table is rendered in PDF, XSLT is inserting Doc Link in every column, whereas my requirement is to render empty white space in the column when no link is found in the XML
<fo:table-cell border="solid 1px black">
<xsl:variable name="link"><xsl:value-of select="link/text()"/</xsl:variable>
<xsl:variable name="space" select="'     '"/>
<fo:block>
<xsl:choose>
<xsl:when test="$link">
<fo:basic-link external-destination="url({$link})" color="blue" text-decoration="underline">Doc Link</fo:basic-link></xsl:when>
<xsl:otherwise><xsl:value-of select="$space"/></xsl:otherwise>
</xsl:choose>
</fo:block>
Could it be because of Groovy? If yes how can I fix it?
As #dave pointed out, you've fallen into the trap I described earlier today in Can we have multiple script function in for <xsl:value-of /> element
You need to replace
<xsl:variable name="link"><xsl:value-of select="link/text()"/</xsl:variable>
with
<xsl:variable name="link" select="link/text()"/>
I don't know why this mistake is so common, since the correct code is much shorter and simpler than the incorrect code.

XSLT Choose - testing attribute value

I'm strugling with a Choose statement and the corresponding Test-Clause.
If have the following XML (just an extract) which represents an datamodel exportet from Enterprise Architect as XMI:
<xmi:XMI xmi:version="2.1" xmlns:uml="http://schema.omg.org/spec/UML/2.1" xmlns:xmi="http://schema.omg.org/spec/XMI/2.1" xmlns:thecustomprofile="http://www.sparxsystems.com/profiles/thecustomprofile/1.0" xmlns:EAUML="http://www.sparxsystems.com/profiles/EAUML/1.0"> <xmi:Documentation exporter="Enterprise Architect" exporterVersion="6.5"/> <uml:Model xmi:type="uml:Model" name="EA_Model" visibility="public"> <packagedElement xmi:type="uml:Package" xmi:id="EAPK_F3388CFE_57A7_4d84_8866_3FB3AADE565A" name="Data Model - SQLServer2012" visibility="public">
<packagedElement xmi:type="uml:Artifact" xmi:id="EAID_B62341D4_41C6_4c83_A60A_4CA65C2E185E" name="Database SQLServer2012" visibility="public"/>
<packagedElement xmi:type="uml:Package" xmi:id="EAPK_BA7676C5_40BC_4bd9_A0F5_F6B15E534E8E" name="Logical Model" visibility="public">
<packagedElement xmi:type="uml:Class" xmi:id="EAID_2DC36189_CCFB_40bf_A1CB_CD4FB08FE8B5" name="AnamneseStatus" visibility="public">
<ownedAttribute xmi:type="uml:Property" xmi:id="EAID_9BBF5184_37F8_4729_9DC1_7ED3B4D8FC98" name="RCHIUNET05_ContextKey" visibility="public" isStatic="false" isReadOnly="false" isDerived="false" isOrdered="true" isUnique="false" isDerivedUnion="false">
<lowerValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000001_37F8_4729_9DC1_7ED3B4D8FC98" value="1"/>
<upperValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000002_37F8_4729_9DC1_7ED3B4D8FC98" value="1"/>
<type xmi:idref="EASQL_Server_2012_nvarchar"/>
</ownedAttribute>
<ownedAttribute xmi:type="uml:Property" xmi:id="EAID_BC1F93D0_A7F4_474c_A27E_26D3ABCCFB7B" name="MRNCmpdId" visibility="public" isStatic="false" isReadOnly="false" isDerived="false" isOrdered="false" isUnique="true" isDerivedUnion="false">
<lowerValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000003_A7F4_474c_A27E_26D3ABCCFB7B" value="1"/>
<upperValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000004_A7F4_474c_A27E_26D3ABCCFB7B" value="1"/>
<type xmi:idref="EASQL_Server_2012_nvarchar"/>
</ownedAttribute>
..........
So with my XSL I will loop throug the relevant nodes and extract the tablenames and attributes. this works without problem. Now I need to translate the EA datatype into another datatype definition.
Lets say: EASQL_Server_2012_nvarchar needs to become System.String
The XSL doing this looks like this (since there are other datatypes the Choose Statement will be longer than showed here):
<xsl:for-each select="ownedAttribute[#xmi:type='uml:Property']">
<xsl:text disable-output-escaping="yes"><</xsl:text>
<xsl:value-of select="'Element Name="'"/>
<xsl:value-of select="#name"/>
<xsl:value-of select="'" '"/>
<xsl:choose>
<xsl:when test="#xmi:idref = 'EASQL_Server_2012_nvarchar'">
<xsl:value-of select="'Type="'"/>
<xsl:value-of select="'System.String'"/>
<xsl:value-of select="'" '"/>
<xsl:value-of select="'MaxLength="'"/>
<xsl:value-of select="'400'"/>
<xsl:value-of select="'" '"/>
</xsl:when>
<xsl:when test="#xmi:idref = 'EASQL_Server_2012_int'">
<xsl:value-of select="'Type="'"/>
<xsl:value-of select="'System.Int32'"/>
<xsl:value-of select="'" '"/>
</xsl:when>
......
Now my problem is, that it will not hit the test conditions and always run into the "otherwise" statement.
Does somebody see why the test condition is not working?
Thank you for any help on this.
Cheers
Sandro
I suspect that instead of:
<xsl:when test="#xmi:idref = 'EASQL_Server_2012_nvarchar'">
you want to do:
<xsl:when test="type/#xmi:idref = 'EASQL_Server_2012_nvarchar'">
since in the partial example you have posted, ownedAttribute (which is your context node when you run this test) has no xmi:idref attribute, but its child element type does.
P.S. I don't know what you're doing overall, but I cringe whenever I see:
<xsl:text disable-output-escaping="yes"><</xsl:text>
This should never be necessary. If - as it seems - you're not outputting XML, set the output method to text. Then it's not necessary to disable output escaping.

How to generate empty space in a fo:block if there is no valued in the extracted element?

I am using XSL-FO and FOP .95, whenever i write a code in xsl-fo i have to use this statement to generate an empty space:
<fo:block>
<xsl:choose>
<xsl:when test="normalize-space(Seller_Name)!=''">
<xsl:value-of select="normalize-space(Seller_Name)"/>
</xsl:when>
<xsl:otherwise><xsl:text> </xsl:text></xsl:otherwise>
</xsl:choose>
</fo:block>
I dont want to use these choose when conditions to generate an empty space to save the block collapse. is there any function or property which can be used here? I have tried line-feed-treatment and white-space-collapse but it didnt work. Please advise something.
IF you are happy with what you have above, why not template it. This would reduce the call to three lines:
<xsl:template name="blockwithblank">
<xsl:param name="field"/>
<fo:block>
<xsl:choose>
<xsl:when test="normalize-space($field)!=''">
<xsl:value-of select="$field"/>
</xsl:when>
<xsl:otherwise><xsl:text> </xsl:text></xsl:otherwise>
</xsl:choose>
</fo:block>
</xsl:template>
That above is once in the whole stylesheet, then each of the calls is only three lines:
<xsl:call-template name="blockwithblank">
<xsl:with-param name="field" select="Seller_Name"/>
</xsl:call-template>
I am not sure you can shorten it more than three lines each call.
Use two templates: one for the regular case and the other, with a higher priority, for the empty case:
<xsl:template match="Seller_Name">
<fo:block>
<xsl:value-of select="normalize-space()"/>
</fo:block>
</xsl:template>
<xsl:template match="Seller_Name[normalize-space() = '']" priority="5">
<fo:block> </fo:block>
</xsl:template>
Your XSLT would need to have a xsl:apply-templates at the appropriate point, but it would make the current template shorter.
If you're doing this a lot with multiple elements, you could match on multiple elements in each of these templates and save a lot of repetition.

Inserting a line break in a PDF generated from XSL FO using <xsl:value-of>

I am using XSL FO to generate a PDF file containing a table with information. One of these columns is a "Description" column. An example of a string that I am populating one of these Description fields with is as follows:
This is an example Description.<br/>List item 1<br/>List item 2<br/>List item 3<br/>List item 4
Inside the table cell that corresponds to this Description, I would like the output to display as such:
This is an example Description.
List item 1
List item 2
List item 3
List item 4
I've learned from searching elsewhere that you can make line breaks in XSL FO using an <fo:block></fo:block> within another <fo:block> element. Therefore, even before I parse the XML with my XSL stylesheet, I replace all occurrences of <br/> with <fo:block/>, so that the literal value of the string now looks like:
This is an example Description.<fo:block/>List item 1<fo:block/>List item 2<fo:block/>List item 3<fo:block/>List item 4
The problem arises when the Description string I am using is obtained using <xsl:value-of>, example as follows:
<fo:block>
<xsl:value-of select="descriptionStr"/>
</fo:block>
In which case, the value that gets output to my PDF document is the literal value, so it looks exactly like the previous example with all the <fo:block/> literals. I've tried manually hard-coding the <fo:block/> in the middle of another string, and it displays correctly. E.g. if I write inside my stylesheet:
<fo:block>Te<fo:block/>st</fo:block>
It will display correctly as:
Te
st
But this does not seem to happen when the <fo:block/> is inside the value of an <xsl:value-of select=""/> statement. I've tried searching for this on SO as well as Google, etc. to no avail. Any advice or help will be greatly appreciated. Thank you!
You could also replace <br/> with
and add a linefeed-treatment="preserve" attribute to your <fo:block>.
Something like:
<fo:block linefeed-treatment="preserve">This is an example Description.
List item 1
List item 2
List item 3
List item 4</fo:block>
Edit
Some users may need to use \n instead of
depending on how they are creating the XML. See Retain the
during xml marshalling for more details.
This helped me and should be simplest solution (working with Apache FOP 1.1):
Why not replace your <br/> with Unicode character called line separator.
<xsl:template match="br">
<xsl:value-of select="'
'"/>
</xsl:template>
See https://en.wikipedia.org/wiki/Newline#Unicode
The following code worked:
<fo:block white-space-collapse="false"
white-space-treatment="preserve"
font-size="0pt" line-height="15px">.</fo:block>
It makes the xsl processor thinks this block contains a line of text, which actually has a 0pt font size.
You can customize line height by providing your own value.
You shouldn't use xsl:value-of instruction but xsl:apply-templates instead: for built-in rule for text node will just output their string value, and for empty br element you could declare a rule matching descriptionStr/br or descriptionStr//br (depending your input) in order to transform to empty fo:block.
Generating strings containing escaped XML markup is seldom the right answer, but if that's what you have to work with, then for input like this:
<Description><![CDATA[This is an example Description.<br/>List item 1<br/>List item 2<br/>List item 3<br/>List item 4]]></Description>
if you're using XSLT 2.0, you can use xsl:analyze-string to get the empty fo:block that you originally wanted:
<xsl:template match="Description">
<fo:block>
<xsl:analyze-string select="." regex="<br/>">
<xsl:matching-substring>
<fo:block />
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="." />
</xsl:non-matching-substring>
</xsl:analyze-string>
</fo:block>
</xsl:template>
but if you are using XSLT 2.0, you can more concisely use linefeed-treatment="preserve" as per #Daniel Haley and use replace() to insert the linefeeds:
<xsl:template match="Description">
<fo:block linefeed-treatment="preserve">
<xsl:value-of select="replace(., '<br/>', '
')" />
</fo:block>
</xsl:template>
If you are using XSLT 1.0, you can recurse your way through the string:
<xsl:template match="Description">
<fo:block linefeed-treatment="preserve">
<xsl:call-template name="replace-br" />
</fo:block>
</xsl:template>
<xsl:template name="replace-br">
<xsl:param name="text" select="." />
<xsl:choose>
<xsl:when test="not(contains($text, '<br/>'))">
<xsl:value-of select="$text" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring-before($text, '<br/>')"/>
<xsl:text>
</xsl:text> <!-- or <fo:block /> -->
<xsl:call-template name="replace-br">
<xsl:with-param name="text" select="substring-after($text, '<br/>')"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Try this:
<fo:block><fo:inline color="transparent">x</fo:inline></fo:block>
This code adds a block which contains transparent text, making it look like a new line.
Try using linefeed-treatment="preserve" and \n instead of <br> for a new line.
<fo:block linefeed-treatment="preserve" >
<xsl:value-of select="Description" />
</fo:block>
For XSLT 1.0 I'm using my XSLT Line-Break Template on GitHub.
For XSL-FO it supports
Line breaks
Line delimiters (vs Line breaks)
Series of pointers in a row
Ignore Pointer Repetitions (disable the Series of pointers in a row)
Any string as a pointer to insert a break or a delimiter ("\n" is default)
Line delimiters' height
Default Line delimiter height from a current font size.
Auto ignoring of the "\r" char when searching a break place.
Added support for XSLT 2.0 for a seamless migration.
something else...
For XSLT 2.0 and later consider to use approaches like
XSLT 2.0 xsl:analyze-string (RegEx)
XPath 2.0 tokenize + XSLT (RegEx)
passing sequences as a template parameter (XSLT 2.0)
and so on
I usually use an empty block with a height that can be changed if I need more or less space:
<fo:block padding-top="5mm" />
I know this isn't the best looking solution but it's funtional.
I had a text block that looks like this
<fo:table-cell display-align="right">
<fo:block font-size="40pt" text-align="right">
<xsl:text> Text 1 </xsl:text>
<fo:block> </fo:block>
<xsl:text> Text2 </xsl:text>
<fo:block> </fo:block>
<xsl:text> Text 3</xsl:text>
</fo:block>
NB: note the empty
</fo:block> on it's own is not a direct substitute for <br/> <br/> is an html unpaired abberation that has no direct equivalent in xsl:fo
</fo:block> just means end of block. If you scatter them through your text you wont have valid xml, and your xsl processor will sick up errors.
For the line break formatting you want, each block will occur on a new line. You need a <fo:block> start block and </fo:block> end block pair for each line.