Slightly different to the standard Meunchain grouping as I'm dealing with a null(?) attribute for some of the tags that I'm transforming. I'd like the null groupings to be treated as their own individual group with the transformed output adding the grouped strings. Also if there is a grouping to do a count of how many there were.
<root>
<section>
<subsection>
<module>
<comp>111</comp>
</module>
<module group='group01'>
<comp>222</comp>
</module>
<module group='group01'>
<comp>333</comp>
</module>
<module>
<comp>444</comp>
</module>
<module>
<comp>555</comp>
</module>
</subsection>
</section>
<section>
<subsection>
<module group ="group02">
<comp>666</comp>
</module>
<module group ="group02">
<comp>777</comp>
</module>
<module>
<comp>888</comp>
</module>
<module group ="group03">
<comp>999</comp>
</module>
<module group ="group03">
<comp>101010</comp>
</module>
</subsection>
<subsection>
<module group ="group04">
<comp>11111</comp>
</module>
<module group ="group04">
<comp>121212</comp>
</module>
<module group ="group05">
<comp>131313</comp>
</module>
<module group ="group05">
<comp>141414</comp>
</module>
<module group ="group06">
<comp>151515</comp>
</module>
<module group ="group06">
<comp>161616</comp>
</module>
<module>
<comp>171717</comp>
</module>
</subsection>
</section>
Wanted output:
<AllSections>
<section>
<subsection>
<page>
<content>111</content>
</page>
<page>
<content>222333</content>
<count>2</count>
</page>
<page>
<content>444</content>
</page>
<page>
<content>555</content>
</page>
</subsection>
</section>
<section>
<subsection>
<page>
<content>666777</content>
<count>2</count>
</page>
<page>
<content>888</content>
</page>
<page>
<content>999101010</content>
<count>2</count>
</page>
</subsection>
<subsection>
<page>
<content>111111121212</content>
<count>2</count>
</page>
<page>
<content>131313141414161616</content>
<count>3</count>
</page>
<page>
<content>151515</content>
</page>
<page>
<content>171717</content>
</page>
</subsection>
</section>
Thanks!
For the elements with a group attribute, you are grouping by that attribute but also within the parent subsection element. Therefore you could start off by defining a key to group them this was
<xsl:key name="modules" match="module[#group]" use="concat(generate-id(..), '|', #group)" />
Next, you would need templates to match the various cases for the module elements. Firstly, you could have a template to match module elements with no group attribute, where you could format the output as required.
<xsl:template match="module[not(#group)]">
<page>
<content>
<xsl:value-of select="comp"/>
</content>
</page>
</xsl:template>
For modules, with group attributes you would need a match that checked that this particular module element occurred first in the group for the key defined above.
<xsl:template
match="module
[#group]
[generate-id() = generate-id(key('modules', concat(generate-id(..), '|', #group))[1])]">
Within this template, you could then easily define a variable to hold the elements in the group, using the key, and then either output the child comp elements, or count them
<xsl:variable name="modules" select="key('modules', concat(generate-id(..), '|', #group))"/>
<page>
<content>
<xsl:apply-templates select="$modules/comp/text()"/>
</content>
<count>
<xsl:value-of select="count($modules)" />
</count>
</page>
Finally, you would need a third template to match all other module elements (i.e. elements with a group attribute, but not first in the group) to ignore them, to ensure they don't get output twice. (The XSLT processor should always match more specific templates before this more generic one)
<xsl:template match="module"/>
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="modules" match="module[#group]" use="concat(generate-id(..), '|', #group)"/>
<xsl:template match="root">
<AllSections>
<xsl:apply-templates />
</AllSections>
</xsl:template>
<xsl:template match="module[not(#group)]">
<page>
<content>
<xsl:value-of select="comp"/>
</content>
</page>
</xsl:template>
<xsl:template match="module[#group][generate-id() = generate-id(key('modules', concat(generate-id(..), '|', #group))[1])]">
<xsl:variable name="modules" select="key('modules', concat(generate-id(..), '|', #group))"/>
<page>
<content>
<xsl:apply-templates select="$modules/comp/text()"/>
</content>
<count>
<xsl:value-of select="count($modules)" />
</count>
</page>
</xsl:template>
<xsl:template match="module"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your sample XML, the following is output
<AllSections>
<section>
<subsection>
<page>
<content>111</content>
</page>
<page>
<content>222333</content>
<count>2</count>
</page>
<page>
<content>444</content>
</page>
<page>
<content>555</content>
</page>
</subsection>
</section>
<section>
<subsection>
<page>
<content>666777</content>
<count>2</count>
</page>
<page>
<content>888</content>
</page>
<page>
<content>999101010</content>
<count>2</count>
</page>
</subsection>
<subsection>
<page>
<content>11111121212</content>
<count>2</count>
</page>
<page>
<content>131313141414</content>
<count>2</count>
</page>
<page>
<content>151515161616</content>
<count>2</count>
</page>
<page>
<content>171717</content>
</page>
</subsection>
</section>
</AllSections>
Related
I want to transform the following XML-data to the desired output shown below - without success until now. So I hope of an input from an expert relating the xsl. Thank you in advance.
Input XML-Data
<?xml version="1.0" encoding="utf-8"?>
<pages>
<page number="1">
<pageArticles>
<articleID id="100"/>
<articleID id="200"/>
<articleID id="300"/>
</pageArticles>
</page>
<page number="5">
<pageArticles/>
</page>
<page number="9">
<pageArticles>
<articleID id="400"/>
<articleID id="500"/>
</pageArticles>
</page>
</pages>
Desired Ouput
<?xml version="1.0" encoding="utf-8"?>
<pages>
<page>
<number>1</number>
<articleID>100</articleID>
</page>
<page>
<number>1</number>
<articleID>200</articleID>
</page>
<page>
<number>1</number>
<articleID>300</articleID>
</page>
<page>
<number>5</number>
<articleID></articleID>
</page>
<page>
<number>9</number>
<articleID>400</articleID>
</page>
<page>
<number>9</number>
<articleID>500</articleID>
</page>
</pages>
Many thanks in advance for any assistance!
Urs
This XSLT-1.0 stylesheet does the job:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="text()" />
<xsl:template match="/pages">
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
<xsl:template match="//articleID | pageArticles[not(*)]">
<page>
<number><xsl:value-of select="ancestor::page/#number[1]" /></number>
<articleID><xsl:value-of select="#id" /></articleID>
</page>
</xsl:template>
</xsl:stylesheet>
Output is:
<pages>
<page>
<number>1</number>
<articleID>100</articleID>
</page>
<page>
<number>1</number>
<articleID>200</articleID>
</page>
<page>
<number>1</number>
<articleID>300</articleID>
</page>
<page>
<number>5</number>
<articleID/>
</page>
<page>
<number>9</number>
<articleID>400</articleID>
</page>
<page>
<number>9</number>
<articleID>500</articleID>
</page>
</pages>
Write a template for those articleID elements to collect the number from the grandparent and add a template for empty pageArticles:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
expand-text="yes"
version="3.0">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="pages">
<xsl:copy>
<xsl:apply-templates select="page/pageArticles"/>
</xsl:copy>
</xsl:template>
<xsl:template match="pageArticles[not(has-children())]">
<page>
<number>{../#number}</number>
<articleID/>
</page>
</xsl:template>
<xsl:template match="articleID">
<page>
<number>{../../#number}</number>
<articleID>{#id}</articleID>
</page>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/bFDb2BT and the above is XSLT 3. In earlier version of XSLT you have to replace the text value templates (e.g. {../#number}) with xsl:value-of (e.g. <xsl:value-of select="../#number"/>) and the predicate [not(has-children())] with [not(node()].
I have the following html structure:
<document>
<ol>a question</ol>
<div>answer</div>
<div>answer</div>
<ol>another question</ol>
<div>answer</div>
<ol>question #3</ol>
...
</document>
I would like to take the <ol> nodes and the following <div> nodes until the next <ol> node, so I can group them in an xml like
<vce>
<topic>
<question> ... </question>
<answer> ... </answer>
</topic>
...
</vce>
So far I have the following
<xsl:for-each select="//body/ol">
<document>
<content name="question">
<xsl:value-of select="." />
</content>
<content name="answer">
<xsl:for-each
select="./following-sibling::div !!! need code here !!!>
<xsl:value-of select="." />
</xsl:for-each>
</content>
</document>
</xsl:for-each>
I get the questions just fine but I'm having trouble with the answers. I have tried working with following, preceding, not, for-each-group, ... . There are many similar questions but not quit like this with this format because I don't really have a child-parent structure in my html file.
Try it this way:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="answers" match="div" use="generate-id(preceding-sibling::ol[1])" />
<xsl:template match="/document">
<vce>
<xsl:for-each select="ol">
<topic>
<question>
<xsl:value-of select="." />
</question>
<xsl:for-each select="key('answers', generate-id())">
<answer>
<xsl:value-of select="." />
</answer>
</xsl:for-each>
</topic>
</xsl:for-each>
</vce>
</xsl:template>
</xsl:stylesheet>
when applied to the following test input:
XML
<document>
<ol>question A</ol>
<div>answer A1</div>
<div>answer A2</div>
<ol>question B</ol>
<div>answer B1</div>
<ol>question C</ol>
<div>answer C1</div>
<div>answer C2</div>
</document>
the result will be:
<?xml version="1.0" encoding="UTF-8"?>
<vce>
<topic>
<question>question A</question>
<answer>answer A1</answer>
<answer>answer A2</answer>
</topic>
<topic>
<question>question B</question>
<answer>answer B1</answer>
</topic>
<topic>
<question>question C</question>
<answer>answer C1</answer>
<answer>answer C2</answer>
</topic>
</vce>
I have a source XML that contains address elements which could have the same values (please note that Contact/id=1 and Contact/id=3 have the same address:
<?xml version="1.0" encoding="utf-8"?>
<Contacts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Contact>
<id>1</id>
<Address>
<City City="Wien" />
<Postcode Postcode="LSP-123" />
</Address>
</Contact>
<Contact>
<id>2</id>
<Address>
<City City="Toronto" />
<Postcode Postcode="LKT-947" />
</Address>
</Contact>
<Contact>
<id>3</id>
<Address>
<City City="Wien" />
<Postcode Postcode="LSP-123" />
</Address>
</Contact>
</Contacts>
Desired output with XSLT 1.0:
<?xml version="1.0" encoding="utf-8"?>
<Contacts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Contact>
<id>1</id>
<Address>SomeId_1</Address>
</Contact>
<Contact>
<id>2</id>
<Address>SomeId_2</Address>
</Contact>
<Contact>
<id>3</id>
<Address>SomeId_1</Address>
</Contact>
</Contacts>
When I used function generate-id(Address) I got different id for addresses in Contact 1 and Contact 3. What other way to generate unique id for node based on its value only?
Thank you for the help.
I would advise building a key of values as a lookup table and then just orienting from the first entry of the lookup table for the unique number:
t:\ftemp>type ivan.xml
<?xml version="1.0" encoding="utf-8"?>
<Contacts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Contact>
<id>1</id>
<Address>
<City City="Wien" />
<Postcode Postcode="LSP-123" />
</Address>
</Contact>
<Contact>
<id>2</id>
<Address>
<City City="Toronto" />
<Postcode Postcode="LKT-947" />
</Address>
</Contact>
<Contact>
<id>3</id>
<Address>
<City City="Wien" />
<Postcode Postcode="LSP-123" />
</Address>
</Contact>
</Contacts>
t:\ftemp>call xslt ivan.xml ivan.xsl
<?xml version="1.0" encoding="utf-8"?><Contacts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Contact>
<id>1</id>
<Address>SomeId_1</Address>
</Contact>
<Contact>
<id>2</id>
<Address>SomeId_2</Address>
</Contact>
<Contact>
<id>3</id>
<Address>SomeId_1</Address>
</Contact>
</Contacts>
t:\ftemp>type ivan.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:key name="city-pc-pair" match="Address"
use="concat(City/#City,'
',Postcode/#PostCode)"/>
<xsl:template match="Address">
<xsl:for-each select="key('city-pc-pair',
concat(City/#City,'
',Postcode/#PostCode))[1]">
<Address>SomeId_<xsl:number level="any"/></Address>
</xsl:for-each>
</xsl:template>
<xsl:template match="#*|node()"><!--identity for all other nodes-->
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
t:\ftemp>rem Done!
As for the concatenation that I'm using, I tell my students the technique of using a carriage return as a field delimiter reduces the likelihood of an unintended value collision to an infinitesimal size since there are very few hard carriage returns in XML content (those carriage returns that are parts of end-of-line sequences are normalized to a line-feed and so do not appear in the data).
Edited to add the following entity technique that may improve maintenance since it focuses the lookup expression to a single declaration in the stylesheet, so as not to be accidentally written differently in two different parts of the stylesheet:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet
[
<!ENTITY lookup "concat(City/#City,'
',Postcode/#PostCode)">
]>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:key name="city-pc-pair" match="Address" use="&lookup;"/>
<xsl:template match="Address">
<xsl:for-each select="key('city-pc-pair',&lookup;)[1]">
<Address>SomeId_<xsl:number level="any"/></Address>
</xsl:for-each>
</xsl:template>
<xsl:template match="#*|node()"><!--identity for all other nodes-->
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
XSLT's generate-id() is intended for generating #xml:id's, which are generally attributes meant to uniquely identify a node in the document. So everytime you call generate-id(), you
should be getting a unique value.
The identifier's you want to generate are just data, and have nothing to do with what generate-id() does.
If you want an identifier whose value is based on the value of some other data, then you
should just generate it from that data. Concat those values together, for example:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:template match="*|#*">
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
<xsl:template match="Address">
<Address>
<xsl:value-of select="concat(City/#City, '+', Postcode/#Postcode)"/>
</Address>
</xsl:template>
Will produce:
<?xml version="1.0" encoding="UTF-8"?>
<Contacts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Contact>
<id>1</id>
<Address>Wien+LSP-123</Address>
</Contact>
<Contact>
<id>2</id>
<Address>Toronto+LKT-947</Address>
</Contact>
<Contact>
<id>3</id>
<Address>Wien+LSP-123</Address>
</Contact>
</Contacts>
If you have some other requirements for the identifier, then you can write a function
or use a lookup table to map from those keys to some other identifiers.
A transform (http://stackoverflow.com/questions/12862902/how-to-do-xslt-muenchian-grouping-with-some-null-attributes/12871809#12871809) groups based on an attribute (#group) and that attribute being null. Initially this was for just concat-ing strings but I now need to extend it so it uses the string as a file location and concats the docs if they are grouped - if not grouped it should use the string as a file it gets but doesn't need to join to anything else. The Transform:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="modules" match="module[#group]" use="concat(generate-id(..), '|', #group)"/>
<xsl:template match="root">
<AllSections>
<xsl:apply-templates />
</AllSections>
</xsl:template>
<!-- NON GROUPED PART -->
<xsl:template match="module[not(#group)]">
<page>
<content>
<xsl:value-of select="comp"/>
</content>
</page>
</xsl:template>
<!--GROUPED PART -->
<xsl:template match="module[#group][generate-id() = generate-id(key('modules', concat(generate-id(..), '|', #group))[1])]">
<xsl:variable name="modules" select="key('modules', concat(generate-id(..), '|', #group))"/>
<page>
<content>
<xsl:apply-templates select="$modules/comp/text()"/>
</content>
<count>
<xsl:value-of select="count($modules)" />
</count>
</page>
</xsl:template>
<xsl:template match="module"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
And Sample Input:
<root>
<section>
<subsection>
<module>
<comp>111</comp>
</module>
<module group='group01'>
<comp>222</comp>
</module>
<module group='group01'>
<comp>333</comp>
</module>
<module>
<comp>444</comp>
</module>
<module>
<comp>555</comp>
</module>
</subsection>
</section>
<section>
<subsection>
<module group ="group02">
<comp>666</comp>
</module>
<module group ="group02">
<comp>777</comp>
</module>
<module>
<comp>888</comp>
</module>
<module group ="group03">
<comp>999</comp>
</module>
<module group ="group03">
<comp>101010</comp>
</module>
</subsection>
<subsection>
<module group ="group04">
<comp>11111</comp>
</module>
<module group ="group04">
<comp>121212</comp>
</module>
<module group ="group05">
<comp>131313</comp>
</module>
<module group ="group05">
<comp>141414</comp>
</module>
<module group ="group06">
<comp>151515</comp>
</module>
<module group ="group06">
<comp>161616</comp>
</module>
<module>
<comp>171717</comp>
</module>
</subsection>
The transform at the moment concats the comp strings if they are of the same group... doing this for the non-grouped parts so the string is used as a file location:
<!-- NON GROUPED PART -->
<xsl:template match="module[not(#group)]">
<page>
<content>
<xsl:variable name="var">
<xsl:value-of select="comp"/>
</xsl:variable>
<xsl:copy-of select="document(concat('../myfile/', string($var)))"/>
</content>
</page>
</xsl:template>
At the moment the GROUPED PART will output:
...
<page>
<content>strgin1string2</content>
<count>2</count>
</page>
...
I need:
...
<page>
<content>
TEXT FROM FILE CALLED string1
TEXT FROM FILE CALLED string2
</content>
<count>2</count>
</page>
...
Thanks!
Assuming you simply want to pull in complete XML documents you can change your code to
<xsl:template match="module[#group][generate-id() = generate-id(key('modules', concat(generate-id(..), '|', #group))[1])]">
<xsl:variable name="modules" select="key('modules', concat(generate-id(..), '|', #group))"/>
<page>
<content>
<xsl:copy-of select="document($modules/comp)"/>
</content>
<count>
<xsl:value-of select="count($modules)" />
</count>
</page>
</xsl:template>
[edit] I think my suggestion above works if the comp elements contain the complete URL. Your comment however suggests you want to concatenate a constant with each comp, in that case with XSLT 1.0 you need
<xsl:template match="module[#group][generate-id() = generate-id(key('modules', concat(generate-id(..), '|', #group))[1])]">
<xsl:variable name="modules" select="key('modules', concat(generate-id(..), '|', #group))"/>
<page>
<content>
<xsl:for-each select="$modules/comp">
<xsl:copy-of select="document(concat('../myfile/', .))"/>
</xsl:for-each>
</content>
<count>
<xsl:value-of select="count($modules)" />
</count>
</page>
</xsl:template>
With XSLT 2.0 you could write a compact line like <xsl:copy-of select="document($modules/comp/(concat('../line/', .))"/> but with XSLT 1.0 the for-each should do.
I have one XML request which I need to modify (to XML) and then send it further. I have no prior knowledge of XSLT.
Say I have
<Combined>
<Profile>
<Fullname>John Doe</Fullname>
<OtherData>
<Birthdate>1996</Birthdate>
<FavoriteBooks>
<Book>
<id>1</id>
<description>Libre1</description>
</Book>
<Book>
<id>2</id>
<description>Libre2</description>
</Book>
<Book>
<id>3</id>
<description></description>
</Book>
<Book>
<id>4</id>
<description>Libre4</description>
</Book>
</FavoriteBooks>
</OtherData>
</Profile>
<LoadedData>
<NewBirthdate>1998</NewBirthdate>
<BooksUpdate>
<Book id="1">
<BookText>Book1</BookText>
</Book>
<Book id="2">
<BookText>Book2</BookText>
</Book>
<Book id="3">
<BookText>Book3</BookText>
</Book>
<Book id="4">
<BookText>Book4</BookText>
</Book>
<Book id="5">
<BookText>Book5</BookText>
</Book>
</BooksUpdate>
</LoadedData>
And want to get
<Profile>
<Fullname>John Doe</Fullname>
<OtherData>
<Birthdate>1998</Birthdate>
<FavoriteBooks>
<Book>
<id>1</id>
<description>Libre1Book1</description>
</Book>
<Book>
<id>2</id>
<description>Libre2Book2</description>
</Book>
<Book>
<id>3</id>
<description>empty</description>
</Book>
<Book>
<id>4</id>
<description>Libre4Book4</description>
</Book>
<Book>
<id>5</id>
<description>new Book5</description>
</Book>
</FavoriteBooks>
</OtherData>
I did a pretty pathetic attempt, which obviously does not work.
<?xml version="1.0" encoding="utf-8"?>
<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:output method="xml" indent="yes"/>
<xsl:template match="/">
<Profile>
<xsl:apply-templates select="Combined/Profile/Fullname" />
</Profile>
<Otherdata>
<Birthdate>
<xsl:apply-templates select="Combined/LoadedData/NewBirthdate"/>
</Birthdate>
<FavoriteBooks>
<xsl:for-each select="/Combined/Profile/OtherData/FavoriteBooks/Book">
<Book>
<id>
<xsl:value-of select="id"/>
</id>
<description>
<xsl:value-of select="description"/>
<xsl:apply-templates select="/Combined/LoadedData/BooksUpdate/Book[#id='']" />
</description>
</Book>
</xsl:for-each>
</FavoriteBooks>
</Otherdata>
</xsl:template>
</xsl:stylesheet>
How can I get closer to what I want to get? Could you also advise me some book to jump start, because w3schools tutorials are useless :(
How's this?
According to the logic you described, the description would not be "empty" but rather "Book3" (empty string merged with "Book3").
<!-- root and static content -->
<xsl:template match="/">
<xsl:apply-templates select='Combined/Profile' />
</xsl:template>
<!-- identity/copy, with some tweaks -->
<xsl:template match='node()|#*'>
<!-- copy node -->
<xsl:copy>
<!-- add in its attributes -->
<xsl:apply-templates select='#*' />
<!-- now either apply same treatment to child nodes, or something special -->
<xsl:choose>
<!-- use updated birthdate -->
<xsl:when test='name() = "Birthdate"'>
<xsl:value-of select='/Combined/LoadedData/NewBirthdate' />
</xsl:when>
<!-- merge book descriptions -->
<xsl:when test='name() = "description"'>
<xsl:value-of select='concat(., /Combined/LoadedData/BooksUpdate/Book[#id = current()/../id]/BookText)' />
</xsl:when>
<!-- or just keep recursing -->
<xsl:otherwise>
<xsl:apply-templates select='node()' />
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
<!-- if we've done all books, add in any in the loaded data but not the original data -->
<xsl:if test='name() = "Book" and not(count(following-sibling::Book))'>
<xsl:variable name='orig_book_ids'>
<xsl:for-each select='../Book'>
<xsl:value-of select='concat("-",id,"-")' />
</xsl:for-each>
</xsl:variable>
<xsl:apply-templates select='/Combined/LoadedData/BooksUpdate/Book[not(contains($orig_book_ids, concat("-",#id,"-")))]' mode='new_books' />
</xsl:if>
</xsl:template>
<!-- new books -->
<xsl:template match='Book' mode='new_books'>
<Book>
<id><xsl:value-of select='#id' /></id>
<description>new <xsl:value-of select='BookText' /></description>
</Book>
</xsl:template>
You can run it at this XMLPlayground session (see output source).