xslt keys, indexes in xslt

Keys

1. How do keys work?
2. Keys, a general view.
3. How do I use keys
4. Unusual key usage
5. Keys across multilple input files
6. Multiple Keys example
7. Keys
8. Using keys
9. Understanding Keys
10. Keys. Some more explanation
11. Restricting keys to less than the entire document
12. Problem with keys
13. Understanding Keys with generate-id
14. Is it possible to use one key element for dual conditions
15. Keys example
16. Keys, finding contents
17. Keys for cross-references
18. Keys... again
19. Using Keys to group by position
20. Tabulation, using keys
21. Using keys to lookup from current stylesheet
22. performance enhancement in stylesheets by using keys!
23. Lookup XML file
24. Bibliographic references
25. Check id, idref pairs
26. How to select using multiple Keys
27. Using keys as a map/hashtable/dictionary

1.

How do keys work?

Michael Kay

Consider

<book id="1">
  <author>John</author>
  <author>Jane</author>
  <author>Mary</author>
</book>
<book id="2">
  <author>John</author>
</book>
<book id="3">
  <author>John</author>
  <author>Jane</author>
</book>

<xsl:key name="k" match="book" use="author"/>

For book id=1, the value of the use attribute is a node-set containing three author elements. Each element contributes one value to the key. The value that it contributes is the string-value of the author element. So the three key values for book id=1 are "John", "Jane", and "Mary".

So the value of key('k', 'Jane') is the set containing book id=1 and book id=3.

Is that clearer?

2.

Keys, a general view.

Wendell Piez

Actually, recursion isn't the easiest solution. It's to "prewire" the hierarchy you want with keys. It's easiest to think of the wiring from bottom up. It sounds from your description like your lowest level is C, which are directly inside their most-previous A (or possibly P, whichever is more recent). This relation can be established with

<xsl:key name="Records-by-parent" match="Record[F1='C']"
          use="generate-id(preceding-sibling::Record[F1='A' or
F1='P'][1])"/>

The way this key works is, if I give it the value of a string, it will return for me all C records that "belong to" (are intended to be children of) any P or A record whose generated unique ID equals my string.

Likewise,

<xsl:key name="Records-by-parent" match="Record[F1='A']"
          use="generate-id(preceding-sibling::Record[F1='P'][1])"/>

gets me back A records from the ID of a P record.

I build my hierarchy by pulling out all my P records, then inside each P record's element, getting all the A and C records that belong to it (using the ID of the P record as the key to retrieve them), then inside the As, getting all the C records that belong to them.

Sort of recursion inside out, if you like.

I hope between this synopsis and the FAQ (more code), you can see what you have to do. If not, ask again.

3.

How do I use keys

Michael Kay


> Being new to XSLT I'm a little unsure of exactly what the pattern in 
> the 'use' attribute value in the xsl:key element is doing.

If you want to index all employees by their works number, you do

<xsl:key name="k" match="employee" use="works-number"/>

and then to find an employee with works number 517541 you do

key('k', '517541')

The use attribute can be any XPath expression, so if you want to index employees on the number of events they have in their promotion history, you can do

<xsl:key name="k" match="employee" use="count(promotion-history/event)"/>

and then

key('k', 3)

will find all employees with three such events.

4.

Unusual key usage

J.Pietschmann

There is a general rule for using keys.

"The value used for looking up stuff via a key should be fiddled with the same way as in the key definition"

Some examples:

Normalizing space:

  <xsl:key name="n-stuff" match="stuff"
    use="normalize-space(.)"/>
 use
      select="key('n-stuff',normalize-space($val))"
 

Transforming characters:

  <xsl:key name="t-stuff" match="stuff"
    use="translate(.,$up,$lo)"/>
 use
      select="key('t-stuff',translate($keyval,$up,$lo))"
 

A composite key value:

  <xsl:key name="l" match="locale"
    use="concat(country,'_',language)"/>
 use
      select="key('l',concat($c,'_',$l))"
 

The rule is particularly important when using keys for Munchean grouping, because the values for lookup come from the source XML and cannot be supposed to be already in a proper form.

Unfortunately, this rule is not in any of the FAQs, and even if it were, those in need to be reminded would hardly look for it. :-)

5.

Keys across multilple input files

Jeni Tennison

I have managed to put together something that will probably work, though obviously I've simplified a few things rather than use the full complexity of what I know of what you're trying to do. I'm sure you can fill in the gaps.

There is a file named filelist.xml that holds the names of the XML class files. something like:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="test.xsl"?>
<documents>
  <doc href="test1.xml" />
  <doc href="test2.xml" />
  <doc href="test3.xml" />
</documents>

I'm also going to assume that it's the 'input file' for when you run the stylesheet. If it isn't, you can always refer to it using document('filelist.xml').

On to the stylesheet. First, we set up a variable that holds the contents of all those documents:

<xsl:variable name="documents" select="document(/documents/doc/@href)" />

Note that the way the document() function works, it actually goes and gets *all* those documents, not just the first one. Don't ask me why because I can't follow the definition of document(), but it works. Which is handy.

In this, $documents is assigned to *the result of the document() function being called on* the node list that holds the 'href' attributes of all the 'doc' elements that are children of the 'documents' document element in the input XML.

Next I define the key in the normal way:

<xsl:key name="classes" match="class" use="@name" />

Now, for lack of a better thing to do, I'm going to work through these classes one at a time according to the order the documents have been given in and the order the classes have been given in within those documents. You probably have some more sophisticated way of ordering your output. Slot it in here.

<xsl:template match="/">
  <xsl:for-each select="$documents">
    <xsl:apply-templates select="/classes/class" />
  </xsl:for-each>
</xsl:template>

For each of the classes, I'm going to have a bit of information about the class, and then generate the hierarchy that you (used to) want. You definitely have a more sophisticated output for each class. Slot it in here.

<xsl:template match="class">
  <h3><xsl:value-of select="@name" /></h3>
  <xsl:apply-templates select="." mode="hierarchy" />
</xsl:template>

And finally, the bit where we use the key() function to get the superclass node to build the hierarchy. Note that we have to define a variable for the name of the superclass outside the xsl:for-each. The key() function works in exactly the same way as normal, but the xsl:for-each defines the documents that the key is used within. You definitely have a more sophisticated output for the formatting and linking of the hierarchy. Slot it in here.

<xsl:template match="class" mode="hierarchy">
  <xsl:variable name="superclass" select="@superclass" />
  <xsl:for-each select="$documents">
    <xsl:apply-templates select="key('classes', $superclass)"
      mode="hierarchy" />
  </xsl:for-each>
  +- <xsl:value-of select="@name" />
</xsl:template>

6.

Multiple Keys example

DaveP


After a long struggle and assistance, I finally managed
to use the key idea of XSLT. This one uses an example
which produces multiple results.

XML file.


<?xml version="1.0"?>

<!DOCTYPE doc [
<!ELEMENT doc  (a*,b*,c*)* >
<!ELEMENT a (#PCDATA)>
<!ATTLIST a id ID #REQUIRED
            idref IDREFS #REQUIRED>
<!ELEMENT b (#PCDATA)>
<!ATTLIST b id ID #REQUIRED
            idref IDREF #REQUIRED>
<!ELEMENT c (#PCDATA)>
<!ATTLIST c id ID #REQUIRED
            idref IDREF #REQUIRED>

]>

<doc>
  <a id="ida"  idref="idb">Element A content</a>
  <a id="ida1" idref="ida2">Element A1 content</a>
  <a id="ida2" idref="idb3">Element A2 content</a>
  <a id="ida3" idref="idc">Element A3 content</a>

  <b id="idb"  idref="ida">Element B content</b>
  <b id="idb1" idref="ida1">Element B1 content</b>
  <b id="idb2" idref="ida3">Element B content</b>
  <b id="idb3" idref="idc1">element B3 content</b>

  <c id="idc"  idref="idb1">Element C content</c>
  <c id="idc1" idref="idb2">Element C1 content</c>
  <c id="idc2" idref="ida">element C2 content</c>
  <c id="idc3" idref="idb">Element C3 content</c>
</doc>
     <!--
id is called by

a       b, c2
a1      b1
a2      a1
a3      b2
b       a, c3
b1      c
b2      c1
b3      a2
c       a3
c1      b3
c2      none
c3      none
 -->


XSL File

<?xml version="1.0"?>

<!-- Purpose: (Apart from demonstrating use of keys :-0)
To search for all elements which have an idref attribute
pointing to the element I'm dealing with.  Look at the
comments at the end of the XML file to see what it 'should'
be.  Its effectively the inverse of the id() function.

 -->




<!DOCTYPE xsl:stylesheet [
<!ENTITY sp "<xsl:text> </xsl:text>">
<!ENTITY dot "<xsl:text>.</xsl:text>">
<!ENTITY nbsp "&#160;"> 
<!ENTITY nl "&#xa;"><!--new line-->


]>


<xsl:transform
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 version="1.0"

>


<xsl:output 
  method="text" 
  encoding="utf-8" 
 
indent="yes"/>

<!-- Create the key.

Give it some name.
Give it a pattern to search for - it searches
  the whole document but you don't need to specify
  a full path unless you want to.
Give it a pattern to use for comparison when using
the key() function. This specifies the idref 
attribute which is a child of elements a or b or c.

 -->
 <xsl:key name="caller" match="a|b|c" use="@idref"/>

 <!-- This form is better if you need to change the use
attribute for each element. It could be idref with element a,
forward-link with element b etc.
Remember that the use attribute is the content that is 
matched on, relative to the 'match' attribute which 
specifies the pattern to search for.
  <xsl:key name="caller" match="a" use="@idref"/>
  <xsl:key name="caller" match="b" use="@idref"/>
  <xsl:key name="caller" match="c" use="@idref"/>

 -->


 <xsl:template match="/doc">
    <xsl:apply-templates/>
  </xsl:template>


  <xsl:template match="a|b|c">
<!-- Excuse the layout, this to produce output that is clear.
Same version is below, in comments, with fuller explanation. -->
<xsl:variable name="here" select="@id"/>
<xsl:value-of select="@id"/> is called by (<
  xsl:choose><xsl:when test="(key('caller',$here))/@id"><
 xsl:for-each 
   select="key('caller',$here)/@id"> <xsl:value-of 
   select="."/> <xsl:if 
   test="not(position()=last())">,&sp;<
   /xsl:if></xsl:for-each></xsl:when>
   <xsl:otherwise> None </xsl:otherwise></xsl:choose>)
&sp;&sp;&sp; <xsl:apply-templates/>

  </xsl:template>
 
</xsl:transform>


<!-- 

Note that the processing is the same for all elements a b
and c

 Use a local variable to hold the pattern I want to search
  for.  Why? Because the context node changes within the
  for-each!
 
<xsl:variable name="here" select="@id"/>
 Standard output, show which ID value I'm dealing with. 
<xsl:value-of select="@id"/> is called by (

 The choose identifies any elements which don't have any
other elements pointing at them, the when clause deals with
those that do, the otherwise clause deals with those that
don't.  <xsl:choose>

 The test is for an empty node-list. If its empty, then use
the otherwise clause. The key function returns a node-list,
which contains all the elements whose idref value
(use="@idref" in the key definition) matches the value of
$here, which is the current elements ID value. I only want
those elements which have an ID attribute, so when the key
function returns a node-list (singleton or first of many),

  I use the result of that function with /@id appended.  
  <xsl:when test="(key('caller',$here))/@id">
 Now, there may be more than one, 
  so iterate over the list, using for-each.
  The select gives me each element with an id attribute
  returned by the key.

    <xsl:for-each select="key('caller',$here)/@id"> I
 need the value of the current node (not, as I did earlier,
 the value of the key function, note that the for-each has
 done this for me. Big duh moment when I realised that :-)
 <xsl:value-of select="."/> Now need to test if this
 element from the node list is the last, and put out a comma
 seperator if its not.  <xsl:if
 test="not(position()=last())">,&sp; </xsl:if>
 </xsl:for-each> </xsl:when> Otherwise the node list
 returned by the key function is empty say so.
 <xsl:otherwise> None </xsl:otherwise>
 </xsl:choose>) Space the output, to make it look pretty
 &sp;&sp;&sp;&nl; And apply
 templates. <grin> See what happens when you remove
 it</grin> <xsl:apply-templates/>



-->


Output;

 ida is called by (idb, idc2)
   Element A content
  ida1 is called by (idb1)
   Element A1 content
  ida2 is called by (ida1)
   Element A2 content
  ida3 is called by (idb2)
   Element A3 content

  idb is called by (ida, idc3)
   Element B content
  idb1 is called by (idc)
   Element B1 content
  idb2 is called by (idc1)
   Element B content
  idb3 is called by (ida2)
   element B3 content

  idc is called by (ida3)
   Element C content
  idc1 is called by (idb3)
   Element C1 content
  idc2 is called by ( None )
   element C2 content
  idc3 is called by ( None )
   Element C3 content



7.

Keys

>I would like to extract all paragraphs that have an attribute >of doc equal to contract.

Jeni Tennison

OK. First let's have a look at your input:

<Template>
 <Destination>
  <Target doc="contract"/>
     <Para>CONTRACT para destined for output to contract.xml</Para>
 </Destination>
</Template>
.... other para's all with different doc value according to DTD

Note here that the 'Target' element is an empty one: it does not contain the paragraph that it refers to. This contradicts your DTD, which says that the 'Target' element should *contain* a 'Content' element rather than be *followed by* a 'Para' element. I'll assume that your sample XML is what you want, rather than the DTD.

Now have a look at your key:

<xsl:key name="blueprint" match="Target" use="@doc"/>

Here you are making a key in which the 'Target' elements are indexed according to their 'doc' attribute. When you use it later:

<xsl:template match="Destination">
  <xsl:element name="{name()}">
    <xsl:copy-of select="key('blueprint', '$contractType')"/>
  </xsl:element>
</xsl:template>

you want to get the *Para* elements that follow that particular 'Target' element, but instead are getting the 'Target' elements themselves (or would be if you didn't quote the $contractType variable).

So, I'd change the way you define and use the key. Keys should generally match the elements that you're actually interested in, which in this case is the 'Para' elements. You can use anything reachable by an XPath expression from that element as a key, so:

<xsl:key name="blueprint" match="Para"  use="../Target/@doc" />

and

<xsl:template match="Destination">
  <xsl:element name="{name()}">
    <xsl:copy-of select="key('blueprint', $contractType)"/>
  </xsl:element>
</xsl:template>

Note that this will only get one paragraph - if there are multiple paragraphs with the same value for the 'doc' attribute, then you should iterate over them using xsl:for-each.

8.

Using keys

Jeni Tennison


>My aim is to extract paragraphs from the source tree
>(temp.xml) to be output to a result tree according to
>two criteria:
>
>      * Target doc="contract" and
>      * Target host="true"
>
>This is an extension of my original question.
>The output I receive is all paragraphs disregardless
>of attribute doc and host values.  I have attempted
>to achieve my goal using two methods.


>ATTEMPT 1: Using the Key() function
>=============================
>
>I realise that only one key value can be used and
>then the other key value can be used on the filtered
>result - unless the intersection saxon extension
>function is used.
    

That's true, but you can make keys that combine two values, using concat(). So, we can index on a string like 'contract-true' using:

  <xsl:key name="blueprint" match="Para"
           use="concat(ancestor::Target/@doc, '-', ancestor::Target/@host)" />

Note that now that you've changed the way your input is structured, what you're after are the 'doc' and 'host' attributes of the Target ancestors of the particular Para, not a sibling.

>However here I still receive
>all paragraphs, it seems without referencing
>the index created by my xsl:key?

Have a look at your template:

><xsl:template match="Para">
>  <xsl:element name="{name()}">
>       <xsl:copy-of select="key('blueprint', contract)"/>
>       <xsl:apply-templates/>
>   </xsl:element>
></xsl:template>

You're matching all 'Para' elements, and then (for all of them), creating an element called 'Para'. Within that element, you're putting a copy of the node set that you get when you retrieve the nodes from the 'blueprint' key, using the value of the 'contract' element as the key.

This is far from what you want to do. What you want to do is select all the Para elements that are in the list that is retrieved when you use the key 'contract-true' on the 'blueprint' key, and process only them, making a copy of them. When you only want to process a subset of nodes, then you should only apply templates to that subset, which means changing your root-node matching template:

<xsl:template match="/">
  <xsl:apply-templates select="key('blueprint', 'contract-true')" />
</xsl:template>

<xsl:template match="Para">
  <xsl:copy-of select="." />
</xsl:template>

I'm not sure what exactly you want your output to look like: this just copies the relevant 'Para' elements: you might want to wrap them in another element, in which case you should put it in the root-node matching template.

>I have also, since simply typed the value of contract
>in the key, quotes do not seem to make any difference
>either way.

They do make a difference in how it's interpreted, just not in your overall result because either way it wasn't working! :) When you put 'contract' in quotes, it interprets it as the string 'contract', which is what you want. When you don't put it in quotes, it interprets it as an XPath expression, and tries to find a child element of the current node that is called 'contract', and use its content as the value to index into the key.

>ATTEMPT 2: Using a template match
>============================
>
>*** the following template is never used ***
>*** and I did hope that this template would ***
>*** select paragraphs according to attrib values ***
>*** specified? ***
>
><xsl:template match='Para/Target[@doc="contract"
>                         and @host="true"]'>
>  <xsl:element name="{name()}">
>       <xsl:copy-of select="attribute::node()"/>
>       <xsl:apply-templates/>
>   </xsl:element>
></xsl:template>

You need to change the XPath expression that you're using here. Let me explain what the XPath you're using says:

  Para/Target[@doc="contract" and @host="true"]

Find an element called 'Target' that has a parent element called 'Para' and has a 'doc' attribute with a value of 'contract' and has a 'host' attribute with a value of 'true'

There are no such Paras in your input.

What you want to say is:

Find an element called 'Para' that
  has an ancestor element called 'Target' that
    has a 'doc' attribute with a value of 'contract' and
  has an ancestor element called 'Target' that
    has a 'host' attribute with a value of 'true'

This can be expressed as:

  Para[ancestor::Target[@doc="contract"] and
       ancestor::Target[@host="true"]]

or

  Target[@doc="contract"]//Para[ancestor::Target[@host="true"]]

or

  Target[@host="true"]//Para[ancestor::Target[@doc="contract"]]

Given your current input, a better version (because it involves less hunting around for matching nodes) is:

Find an element called 'Para' that
  has a parent element called 'Target' that
    has a 'host' attribute with a value of 'true' and
  has a grandparent element called 'Target' that
    has a 'doc' attribute with a value of 'contract'

This can be expressed as: Target[@doc="contract"]/Target[@host="true"]/Para

You should also make sure that you *only* process those 'Para' elements, because the XSLT Processor has built-in templates that match other elements and will output their textual content. Again, this means putting your XPath expression higher up in your templates: make sure that you then adapt it according to the current node within the template you put it in. For example:

  <xsl:template match="/">
    <xsl:apply-templates
      select="//Para[ancestor::Target[@doc='contract'] and
                     ancestor::Target[@host='true']]" />
  </xsl:template>

or

  <xsl:template match="Template">
    <xsl:apply-templates
      select="Target[@doc="contract"]/Target[@host="true"]/Para" />
  </xsl:template>

9.

Understanding Keys

Jeni Tennison

I've got trouble understanding the proper use of xsl:key and the key() function. Here's my XML-File:

<booklist>
 <book>
  <title>
   <name>Design Patterns</name>
  </title>
  <author>Erich Gamma</author>
  <author>Richard Helm</author>
  <author>Ralph Johnson</author>
  <author>John Vlissides</author>
 </book>
 <book>
  <title>
   <name>Pattern Hatching</name>
  </title>
  <author>John Vlissides</author>
 </book>
 <book>
  <title>
   <name>Building Applications</name>
  </title>
  <author>Mohamed Fayad</author>
  <author>Douglas C. Schmidt</author>
  <author>Ralph Johnson</author>
 </book>
</booklist>

Here's the XSL:

 <xsl:key name="test" match="title" use="name"/>

 <xsl:template match="booklist">
  <booklist>
   <xsl:apply-templates/>
  </booklist>
 </xsl:template>

 <xsl:template match="book">

  <xsl:if test="key('test','Pattern Hatching')">
   <node>
    <xsl:value-of select="."/>
    <xsl:text> -EndOfKeyValue-</xsl:text>
   </node>
  </xsl:if>

 </xsl:template>

Now, I'd expect the xsl:if to make sure only "name" elements that are children of "title" and have the content "Pattern Hatching" are shown. Instead, I get ALL contents, including the author names; only the various "book" contents are (naturally) placed into single "node" elements.

Now, to make a long question short: WHY??? :)

When you define the key:

> <xsl:key name="test" match="title" use="name"/>

Then you are setting up a number of associations between 'title' nodes and key values given to them through their 'name' child:

node		key value
title[1]	'Design Patterns'
title[2]	'Pattern Hatching'
title[3]	'Building Applications'

When you use:

  key('test', 'Pattern Hatching')

you are saying "What nodes within the 'test' key have the key value of 'Pattern Hatching'?"

The answer is always the second title (in your example), so that node is always returned as a result.

When you test on that node as in:

  <xsl:if test="key('test', 'Pattern Hatching')">
    ...
  </xsl:if>

then the test expression is evaluated and then converted to a boolean. When node sets are converted to a boolean value, they return true() if there is a node in the node set, and false() if not. In your case, since the key() does return a node, the test is always true, so the contents of xsl:if are always used, and you get information about all the books.

>Now, I'd expect the xsl:if to make sure only "name" elements that are 
>children of "title" and have the content "Pattern Hatching"  are 
>shown.

If you are matching all books, as you are here, then it's easy to test whether a book that you have has a 'title' child with a 'name' child that has a content of 'Pattern Hatching'. You don't need key() to do it:

  <xsl:if test="title/name = 'Pattern Hatching'">
    ...
  </xsl:if>

Keys are really useful for *selecting* nodes that you want to process. So if you only wanted to process the book with the title 'Pattern Hatching', then you could use something like:

<xsl:template match="booklist">
  <booklist>
    <xsl:apply-templates
        select="key('test', 'Pattern Hatching')" />
  </booklist>
</xsl:template>

<xsl:template match="title">
  <node>
    <xsl:value-of select="name" />
  </node>
</xsl:template>

If you want to give any information about the book aside from its title, you also may be better off changing your key so that it matches on *books* rather than on their *titles*. The match expression for a key should match the nodes that you're actually interested in, rather than one of their children, or an attribute. It's the 'use' expression that lets you select *what* is interesting about the node (e.g. the name child of the title child):

<xsl:key name="test" match="book" use="title/name" />

and then:

<xsl:template match="booklist">
  <booklist>
    <xsl:apply-templates select="key('test', 'Pattern Hatching')" />
  </booklist>
</xsl:template>

<xsl:template match="book">
  <node>
    <xsl:value-of select="title/name" />
  </node>
</xsl:template>

10.

Keys. Some more explanation

Jeni Tennison


>> <xsl:key name="test" match="title" use="substring-before(name, ' ')"/>
>> will create 
>> node		key value
>> title[1]	'Design'
>> title[2]	'Pattern'
>> title[3]	'Building'
>> 
>> I assume though I haven't tried it.
>
>I have done something similar, which was working fine. But I wanted 
>to know how to do it with the key() function, and that's what I don't 
>get working.

The key function is just a simple look up - you give it a key value, it tells you the nodes that are associated with that value. There isn't any functionality in keys that allow you to look at all the key values and find those that fulfill a certain pattern.

In your 'Pattern' example, say, to get the functionality that you want, the processor would have to go through every single key value in the key, check whether it contained the string 'Pattern', and then return those nodes that have a key value that fulfill that test. This would probably be a lot more complicated for the processor, certainly take a longer time.

Which isn't to say that it wouldn't be useful. In particular, we have to jump through quite a lot of hoops to find out what the unique key values are when doing grouping - a function that gave a list of all the key values would be really helpful in doing this quickly.

If you know that the only titles that you're going to be interested in are those containing the word 'Pattern', then you can hard code this into your stylesheet with:

  <xsl:key name="pattern-titles" match="title" use="contains(., 'Pattern')" />

This will give you a key that has two possible key values, 'true' and 'false'. If you then do key('pattern-titles', 'true'), then you will get a list of those titles that contain 'Pattern'.

Obviously this technique is useless if the search string you're interested in changes each time you run the stylesheet.

The other thing that you can do (my thanks to Mike Kay's book for pointing this possibility out) is assign each title multiple key values within the same key space. So you can do:

  <xsl:key name="test" match="title" use="." />
  <xsl:key name="test" match="title" use="substring-before(., ' ')" />
  <xsl:key name="test" match="title" use="substring-after(., ' ')" />

If all your titles are less than two words long, then this will allow you to do:

  key('test', 'Pattern')

and get all the 'title' elements whose title is 'Pattern' or whose first word is 'Pattern' or whose last word (in a two-word title) is 'Pattern'.

You can add extra keys to your heart's content in order to break down the title further:

  <xsl:key name="test" match="title"
           use="substring-before(substring-after(., ' '), ' '))" />
  <xsl:key name="test" match="title"
           use="substring-before(substring-after(substring-after(., ' '), '
'), ' ')" />
  ...

but it is a bit laborious! :)

11.

Restricting keys to less than the entire document

David Carlisle

Given

<?xml version="1.0"?>

<doc>

    <itemlist>
      <item itemid="Z101" units="1"/>
      <item itemid="Z102" units="2"/>
      <item itemid="Z101" units="4"/>
    </itemlist>
    
  <itemlist>
      <item itemid="Z101" units="1"/>
      <item itemid="Z10x2" units="2"/>
      <item itemid="Z10x1" units="4"/>
    </itemlist>

  </doc>

The request was to sum items with identical itemid attributes e.g. Z101 gives 5, not 6.

DC provides us with

  <?xml version="1.0"?>
  
  
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>
  
  
    <xsl:template match="/|doc">**
      <xsl:apply-templates/>
    </xsl:template>
  
  
  <xsl:key name="items-by-itemid" match="item" use="concat(generate-id(..),@itemid)"
  />
  <xsl:template match="itemlist">
   <xsl:variable name="x" select="generate-id(.)"/>
    <xsl:for-each select
    ="item[count(. | key('items-by-itemid', concat($x,@itemid))[1]) =
  1]">
      <xsl:sort select="@itemid" />
        <tr>
          <td><xsl:value-of select="@itemid"/></td>
  	<td><xsl:value-of
  select="sum(key('items-by-itemid',concat($x,@itemid))/@units)"/></td>
        </tr>
    </xsl:for-each>
  </xsl:template>
  </xsl:stylesheet>

Jeni Tennison expands

However, if speed is more important and the itemlists are long, then the vital thing that you need to do is narrow down the list of items that the key gives you for a particular itemid in the context of a particular itemlist.

There are two general ways of doing this. The first is, as David Carlisle suggested, to include information about the itemlist that you're currently looking at in the key value that you use to retrieve the items you're interested in. At present, the key that you have looks like:

<xsl:key name="items" match="item" use="@itemid" />

The key value is the value of the itemid attribute on the particular item. You could include in that key value something about the itemlist that the item belongs to: its id, if it has one, or a name, or anything at all that distinguishes it from the other itemlists in the document. The most general thing you can use is its unique id as generated through the generate-id() function. You can include that in your key value by concatenating it with the @itemid value:

<xsl:key name="itemlist-items" match="item"
         use="concat(generate-id(parent::itemlist), '::', @itemid)" />

Then, in order to get the uniquely itemid'd items within a particular itemlist, you can use the XPath:

  item[generate-id() =
       generate-id(key('itemlist-items',
                       concat(generate-id(parent::itemlist), '::', @itemid)))]

In other words, get the items whose unique id is the same as the unique id of the (first) item returned from the 'items' key with a key value equal to a concatenation of the unique id of the item's parent itemlist, '::', and the item's itemid attribute.

You can also use:

  item[1 = count(. |
                 key('itemlist-items',
                     concat(generate-id(parent::itemlist),
                            '::', @itemid))[1])]

In other words, get the items such that there is only one node in the node set resulting from the union of that item and the first item returned from the 'items' key with a key value equal to a concatenation of the unique id of the item's parent itemlist, '::', and the item's itemid attribute.

The second general solution is to have the key() return *all* the items (no matter what itemlist they belong to), but filter that list to only those items that are in the itemlist for the item you're looking at. To use this method you have to have a context in which it is possible to get at the common node set that you have in mind. For example, in your case, you need to be in a context where the current node is the itemlist that you're interested in, such as within a template matching that itemlist.

Mike Brown suggested one way of doing this using the Kaysian Method for finding node set intersections, which is a general approach that is essential when all you know is the node set filter that you're interested in, not what all those nodes have in common. For example, if you had a global variable that held the node set that you were using as a filter, then Mike's approach would be perfect.

In your case, though, you know that the node set filter involves the identity of the itemlist for the items. If you have the unique identity for the current itemlist within a variable:

  <xsl:variable name="itemlist-id" select="generate-id()" />

then you can filter the node set returned by the key by testing which of them have a parent itemlist that has the same unique id as the current itemlist:

  key('items', @itemid)[generate-id(parent::itemlist) =
                        $itemlist-id]

You can insert this filtered key result into either of the Muenchian XPaths:

  item[generate-id() =
       generate-id(key('items', @itemid)[generate-id(parent::itemlist) =
                                         $itemlist-id])]

or:

  item[1 = count(. |
                 key('items', @itemid)[generate-id(parent::itemlist) =
                                       $itemlist-id][1])]

Ken Holman explains why the :: seperators are necessary

This is precisely the basis of a section of my instructor-led tutorial and exercise to do sub-tree subsetting of the xsl:key facility, but with one addition.

I teach 
use="concat(generate-id(subtree-root-expression),' ',value-expression)" 

because of the remote (but possible) synthesis of ambiguous use values. If the generated id of two nodes were "N1" and "N12", and the corresponding value expressions were coincidentally "23" and "3", then the values would be "N123" and "N123".

Since the generated id is always a name token, and the name token can never have a space character, the space is an effective delimiter to guarantee uniqueness.

12.

Problem with keys

Michael Kay

The key() function looks for nodes that are in the same document as the context node. Your xsl:for-each is changing the context node to be one in a different document.

13.

Understanding Keys with generate-id

Jeni Tennison

When you have a key that uses the generated ID of the node that it matches, the key value can only ever get that single node - none of the rest of the use expression matters, aside from as a test on the values that you use in them.

In more detail, when you do:

<xsl:key name="MyKey1" match="a/b"
         use="concat(generate-id(),':',@x,':',@y)"/>

<xsl:template match="a/b">
   <xsl:for-each select="key('MyKey1',concat(generate-id(),':','1:1'))">
      MyKey1:@id=<xsl:value-of select="@id"/><br/>
   </xsl:for-each>
   ...
</xsl:template>

In the template you're looking at a particular b element. This b element will be indexed in the key according to its generated ID, its x attribute and its y attribute. So given the b element:

  <b id="1" x="1" y="1" />

the key will be something like:

  randomID1:1:1

*Only* this particular b element will have this key because generate-id() produces a unique ID for a node. Thus each key value will only ever access one node.

When you select nodes using the key, the evaluation of the generate-id() for the b element is going to be exactly the same as it was for the key. So for the above b element, the call looks like:

  key('MyKey1', 'randomID1:1:1')

The only node this could possibly retrieve is the one identified by that particular unique ID (i.e. the b element above). It won't retrieve anything, though, if the x attribute and y attributes are not both equal to 1, as it cannot match any nodes.

So basically, you're either retrieving the node itself or nothing at all - it comes down to a test of whether the node's x and y attributes are both 1 or not. And that means that it's exactly equivalent to doing:

<xsl:template match="a/b">
   <xsl:if test="@x = 1 and @y = 1">
      MyKey1:@id=<xsl:value-of select="@id" /><br />
   </xsl:if>
</xsl:template>

Usually you use generate-id() in a use expression because you want to restrict the nodes that you retrieve from a key to a subtree of the document - you use the unique ID of the parent of the nodes that you're indexing, or one of its ancestors, or feasibly a descendant - something where *lots* of nodes could have the same node related to them.

[BTW, if you've got id attributes, it's more efficient to use them rather than generating unique IDs all the time.]

14.

Is it possible to use one key element for dual conditions

Michael Kay



> I  use two keys for following conditions:

> when  FacilityID exists, use prodCode key
> &lt;xsl:key name="prodCode" match="z:row"
> use="concat(@FacilityID,':',@ProductCode)"/>

> when no FacilityID exists, then use prodCode2 key
> &lt;xsl:key name="prodCode2" match="z:row" use="@ProductCode" />

> The above key element is applied to get a set of unique node-set
> and the related information.

> I am wondering is it possible to combine these two keys into 
> one key

Use two xsl:key declarations with the same name:


&lt;xsl:key name="prodCode" match="z:row[@FacilityId]"
  use="concat(@FacilityID,':',@ProductCode)"/>
&lt;xsl:key name="prodCode" match="z:row[not(@FacilityId)]" 
  use="@ProductCode" />

15.

Keys example

Jeni Tennison

>I want to write a stylesheet such that after parsing this XML file, it 
>counts the number
>of those elements which have the same partNum and report that in some way, 

This is a grouping problem: you want to group the parts together, and count the number of items in each group. Grouping problems are currently (and using basic XSLT without any processor extensions) best solved using the Muenchian method, which involves defining a key that does the grouping quickly for you.

When you design a key to help you group things together, you have three variables to set:

* name - a name for the key, anything you like: 'parts' in this case
* match - the things that you want to group: elements with 'partNum' attributes in this case
* use - the thing that defines the groups: the number of the part in this case

So, the key that you want looks like:

<xsl:key name="parts" match="*[@partNum]" use="@partNum" />

This element is a top-level element: it goes right underneath the xsl:stylesheet element. Note that I have assumed within the 'match' expression that different elements, with different names, might all have 'partNum' attributes, so as well as:

<CPU partNum="1345" />

you might have:

<HDD partNum="5437" />

If the elements that you're interested in are *all* CPUs, then you could use:

<xsl:key name="CPUs" match="CPU" use="@partNum" />

instead. Indeed, you could define several different keys for each of the elements that have partNums, if you know those in advance.

Retrieving the groups involves using the key() function. The first argument is the name of the key (so 'parts' in this case) and the second argument is the value of the thing that was used to group the things you're grouping, so a part number in this case, something like:

  key('parts', '1345')

You can dynamically decide what the part number (or even key name) is.

Getting a list of the part numbers is the slightly tricky bit. You need to identify one of the group for each of the groups that you've defined in the key. You do this by comparing a node (a 'CPU' element) with the first node that you get when you use the key value for that node to index into the key. So, if the first element that you get when you use the value '1345' to index into your 'parts' key is the same as the current element, then you know that it's the first one to appear in the list. To compare nodes, you use the generate-id() function. So, a template matching on the parent of the CPU elements should look something like:

<xsl:template match="parts">
  <table>
    <tr><th>partNum</th><th>Qty</th></tr>
    <xsl:apply-templates
      select="*[@partNum and
                generate-id(.)=generate-id(key('parts', @partNum))]" />
  </table>
</xsl:template>

This guarantees that the only parts that have templates applied to them are those that occur first in the list of parts with that particular part number. You can then have a template that matches on them and outputs the rows of your table. To count the number of parts with that particular part number, you count the number of elements that are retrieved when you use that part number to index into the key that you've used to group your elements:

<xsl:template match="*[@partNum]">
  <tr>
    <!-- first column is the value of the partNum attribute -->
    <td><xsl:value-of select="@partNum" /></td>
    <!-- second column is the number of parts with that partNum -->
    <td><xsl:value-of select="count(key('parts', @partNum))" /></td>
  </tr>
</xsl:template>

This is tested and works in SAXON. The important things to take out of this example are how to use the key to group the elements that you're interested in:

<xsl:key name="parts" match="*[@partNum]" use="@partNum" />

how to retrieve them and count them:

  count(key('parts', @partNum))

and how to retrieve the unique elements so that you can identify what part numbers are used in your file:

  *[@partNum and
    generate-id(.)=generate-id(key('parts', @partNum))]

>The difficulty that I have is the following: I have different XML files 
>(with similar structure)
>in which, these partNum are different , and may even change in the future, 

I'm not sure whether this means you want to summarise the content of all these different files at the same time. If you do, you should take note that the key() function only indexes nodes in the documents that you are currently working on. You can set the current documents using the document() function, but that's another question :)

From your XSLT, I'm guessing that your input XML looks like:

<PROJECTS>
  <PROROW>
    <id>1</id><name>Customer 1</name><project_name>Project 1</project_name>
  </PROROW>
  <PROROW>
    <id>2</id><name>Customer 1</name><project_name>Project 2</project_name>
  </PROROW>
  <PROROW>
    <id>3</id><name>Customer 2</name><project_name>Project 1</project_name>
  </PROROW>
</PROJECTS>

You are trying to group the rows according to the customer name. One of the ways of dealing with grouping problems like this is to use the Muenchian method.

First, define a key to group the rows according to the customer. You define a key using the xsl:key element, which has to go at the top level in the stylesheet (right under xsl:stylesheet). xsl:key takes three attributes: * name - the name of the key, anything you like, but usually the thing you're grouping: 'rows' in this case * match - the nodes that you want to match - in your case, the PROROW elements * use - the key that you want to use to group the nodes expressed as an XPath from the thing you're matching to the key that you want to use to access those nodes - in your case, the value of the 'name' element child of the PROROW

So, your xsl:key element should look something like:

<xsl:key name="rows" match="PROROW" use="name" />

The key indexes each of the rows according to the customer. You can use the key() function to get the list of rows relating to a customer, so key('rows', 'Customer 1') gives you a list of the PROROW elements with ids 1 and 2, whereas key('rows', 'Customer 2') gives you the PROROW element with id 3. This makes it easy to get to your rows once you know the name of the customer.

Next you have to arrange it so that you can get a list of the customers, in which each customer only appears once. You've grouped the rows within the key, and you know each group relates to one particular customer - it's a matter of identifying one row from each group - the first, say. The way you do this is through an XPath expression that identifies all the rows that appear first in their group:

  PROROW[generate-id(.) = generate-id(key('rows', name)[1])]

This says:
Find a PROROW element such that
  A unique ID generated for that element is the same as
  A unique ID generated for
    The first node in the node list returned by
      Indexing the 'rows' key on the value of
        The 'name' child element of that element

If you apply templates only to those PROROW elements, then you know that you are applying templates to one PROROW per customer:

<xsl:template match="PROJECTS">
  <xsl:apply-templates
    select="PROROW[generate-id(.) = generate-id(key('rows', name)[1])]" />
</xsl:template>

Your PROROW-matching template should now output information about all the rows relating to the customer named in that PROROW. You can access them through the key() function as described earlier, and cycle through them using xsl:for-each (or through calling xsl:apply-templates, if you want).

<xsl:template match="PROROW">
  <b><xsl:value-of select="name" /></b>
  <ul>
    <xsl:for-each select="key('rows', name)">
      <li>
        <a href="projects_results.xml?project={id}">
          <xsl:value-of select="project_name" />
        </a>
      </li>
    </xsl:for-each>
  </ul>
</xsl:template>

[Notes: I've changed your HTML elements into lower case to make them XHTML compatible. The links go to the id of the *row* rather than of the project - I don't know if this is what is desired. Tested and works in SAXON.]

So, you can index the structs based on the field/type/refs that they have:

<xsl:key name="structs" match="struct" use="field/type/ref" />

Now, if I do key('structs', 'A') then I'll get all the structs that have a field/type/ref with a value of 'A'. More importantly if I do:

  key('structs', {'A', 'B', 'C'})

where {'A', 'B', 'C'} is a node set of nodes with values 'A', 'B' and 'C', then I'll get all the structs that have field/type/refs with values of 'A', 'B' or 'C'.

So, if I do:

  key('structs', $nodes/name)

then I'll get all those structs who have field/type/refs with that name.

What this boils down to is: try adding the key definition to your stylesheet:

<xsl:key name="structs" match="struct" use="field/type/ref" />

Then, use the following to define your $nextnodes variable.

  <xsl:variable name="nextnodes"
    select="key('structs', $nodes/name)
             [not($processed/name=name) and
              not(field/type/ref[not(. = $processed/name)])]" />

Another thought is rather than keeping track of which nodes *have* been processed, you could keep track of which nodes *haven't* been processed.

  <xsl:template match="structs">
    <xsl:call-template name="process">
      <xsl:with-param name="nodes" select="struct[not(field/type/ref)]"/>
      <xsl:with-param name="todo" select="struct[field/type/ref]"/>
    </xsl:call-template>
  </xsl:template>

  <xsl:template name="process">
    <xsl:param name="nodes"/>
    <xsl:param name="todo"/>
    <xsl:for-each select="$nodes">
      <xsl:value-of select="name"/>
    </xsl:for-each>
    <xsl:if test="$todo">
      <xsl:variable name="nextnodes"
         select="$todo[not(field/type/ref[. = $todo/name])]"/>
      <xsl:if test="$nextnodes">
        <xsl:call-template name="process">
          <xsl:with-param name="nodes" select="$nextnodes"/>
          <xsl:with-param name="todo"
                          select="$todo[not(name = $nextnodes/name)]"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:if>
  </xsl:template>

With this solution it might just be more efficient to define a key to split the structs into those with field/type/refs and those without:

<xsl:key name="referencing-structs"
         match="struct"
         use="boolean(field/type/ref)" />

<xsl:template match="structs">
  <xsl:call-template name="process">
    <xsl:with-param name="nodes"
                    select="key('referencing-structs', false())" />
    <xsl:with-param name="todo"
                    select="key('referencing-structs', true())" />
  </xsl:call-template>
</xsl:template>
         

16.

Keys, finding contents

W. Eliot Kimber

This should have been obvious, but it took me a while to figure it out and I know a lot of people are struggling with key-based processing:

There's no direct way to see what the entries in a table are (e.g., the equivalent of Hastable.getKeySet() in Java). However, you can easily replicate what the key table is doing and sanity check what's happening. All you do is a for-each from the root that uses the same select rule as the key.

For example, if your key declaration is:

<xsl:key name="primary" 
  match="index.marker" 
  use="normalize-space(concat(primary/@sortas,
primary[not(@sortas)]))"/>

Then to see what is actually going into the table, just do this:

<xsl:for-each select="//index.marker">
 <xsl:message>key='<xsl:value-of 
  select="normalize-space(concat(primary/@sortas, 
                                
primary[not(@sortas)]))"/>'</xsl:message>
</xsl:for-each>

This should print out the equivalent of all the entries in the table. You can also capture the key value and do a lookup in the table to see, for example, how many of that entry are in the table under that key.

<xsl:for-each select="//index.marker">
 <xsl:variable name="tempkey">
  <xsl:value-of
    select="normalize-space(concat(primary/@sortas, 
                                 primary[not(@sortas)]))"/>
 <xsl:variable>
 <xsl:message>key='<xsl:value-of select="$tempkey"/>'</xsl:message>
 <xsl:message>  <xsl:value-of select="count(key('primary', $tempkey))/>
items found
  </xsl:message>
</xsl:for-each>  

Jeni adds

In some cases, the key declaration may actually be adding several entries to the table for each matched node. For example:

<xsl:key name="keywords" match="article" use="keywords/keyword" />

If the use attribute evaluates to a node set, then you get one entry per node in that node set (all pointing to the same matched article). So this key indexes each article against each of its keywords.

Therefore in these cases, the xsl:for-each you need is:

  <xsl:for-each select="//article/keywords/keyword">
    <xsl:message>key='<xsl:value-of select="." />'</xsl:message>
  </xsl:for-each>

17.

Keys for cross-references

Jeni Tennison



> I want to pass in a collection/@name and get a list of all the
> corresponding part elements (cross refence). Then I want to sort the
> data first by part/@type, then by part/@name. 

Whenever you're dealing with cross references, the first thing you should do is set up a key that indexes the things that you want to get access to by the value through which you want to access them.

In this case, you want to access the part elements by their id attribute:

<xsl:key name="parts" match="part" use="@id" />

You can then get a part with a particular id through the key function. For example, the following gets the part with id '3':

  key('parts', '3')

One of the useful features of the key function is that if you pass in a node set as the second argument, the XSLT processor goes through each of the nodes, using their string value to retrieve nodes from the key, and then unions the results together. So for example, if you're on a collection element then you can get all the parts referenced by all the ref element children (through their refid attributes) with:

  key('parts', ref/@refid)

This is really powerful in your case because it immediately gives you the list of the parts that you want to sort. Your code becomes a lot simpler:

<xsl:template match="/large-collection/collection">
  <xsl:if test="@name = $MyGroup">
    <xsl:for-each select="key('parts', ref/@refid)">
      <xsl:sort select="@type" />
      <xsl:sort select="@name" />
      <xsl:value-of select="concat('type: ', @type,
                                   '  name: ', @name,
                                   '  id: ', @id, '&#xA;')" />
    </xsl:for-each>
  </xsl:if>
</xsl:template>

By the way, I'd usually only apply templates to the nodes that I was interested in; in your example, you could apply templates directly to the collection element whose name is equal to $MyGroup, from within a template matching the large-collection element:

<xsl:template match="large-collection">
  <xsl:apply-templates select="collection[@name = $MyGroup]" />
</xsl:template>

Then you wouldn't need the xsl:if within the template matching collection elements.

18.

Keys... again

Stuart Brown & David Carlisle


> <xsl:key match="*" use="name(.)" name="all-nodes"/>
> ..
> ...
> ....
> .....
> ......
> <xsl:if test="count(.| key('all-nodes', name(.))[1]) = 1">
> It works fine, i however do not understand what goes on in the above
line..

The xsl:key statement has set up an index of all nodes (match="*"), indexed by their name (use="name(.)"). If you then supply the key() function with a value it returns the relevant nodes as a nodeset. So, key('all-nodes','foo') would return a nodeset of all the foo nodes in the document. To extend this, key('all-nodes', name(.)) returns all the nodes in the document with the same name as your current node. The addition of the [1] predicate filters this down to the first node in the nodeset, which must be the first in document order. Thus, key('all-nodes', name(.))[1] says "give me the first element in document order with the same name as the current element". The count function says "count the number of nodes which match the condition of being EITHER my current node (.) OR the first node with the same name in the document (obtained by the key() function)". So the only possible condition in which a value of 1 could be returned for this is if the current node IS the first node of that name in the document.

Much the same with the generate-id() version: you are using key() to obtain what you know is the first node of that name in the document, and then comparing it with the current node to see if they are one and the same.

Took me a while to get my head round this, too!

David Carlisle offers:

This is Steve Meunch's devious trick.

To see how it works it helps to lie down in a darkened rule and repeat the secret XSLT chant until you are in the correct frame of mind. Then just look at the code again and it all becomes clear.

Basically the problem is that you have a node "." and a set of nodes returned by the key() function. What you want to know is if the node you have is the first element in the set. so.. key('contacts-by-surname', surname)[1] is the first element (in document order) in the set. and you want to know if this is the same node as . you can't do . = key('contacts-by-surname', surname)[1] because that will test the string values of the nodes, it does not test whether the nodes are the same. (So for example all empty elements would be equal if tested with = as they all have string value "")

However the set

(. | key('contacts-by-surname', surname)[1])

is the union of the set set consisting of . and the set consisting of the first element returned by key(). Either this union has 1 element or two. If it has two it means the key() function has returned a node other than the current one.

19.

Using Keys to group by position

David Carlisle

Asked as, change each newline to a div element

div.xml - source file

<page>
    <bodytext>This is the <link url="zzz">link</link> xxx
    This is another line
    and a <link url="zzz">link2</link> 2nd
    and a third</bodytext>
</page>

Stylesheet div.xsl

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
                 version="1.0">

<xsl:output indent="no"/>

<xsl:key name="x" match="bodytext/node()"
use="generate-id((..|preceding-sibling::text()
            [contains(.,'&#10;')][1])[last
()])"/>

<xsl:template match="page">
<html>
<head>
<title>testing...</title>
</head>
<xsl:apply-templates/>
</html>
</xsl:template>

<xsl:template match="link">
<a href="{@url}">
<xsl:apply-templates/>
</a>
</xsl:template>


<xsl:template match="bodytext">
<body>
<xsl:for-each select=".|text()[contains(.,'&#10;')]">
 <xsl:call-template name="d1">
 <xsl:with-param name="s" 
         select="substring-after(self::text(),'&#10;')"/>
 </xsl:call-template>
</xsl:for-each>
</body>
</xsl:template>

<xsl:template name="d1">
 <xsl:param name="s"/>
  <xsl:text>&#10;</xsl:text>
<xsl:choose>
 <xsl:when test="contains($s,'&#10;')">
  <div><xsl:value-of 
       select="substring-before($s,'&#10;')"/></div>
 <xsl:call-template name="d1">
 <xsl:with-param name="s" select="substring-after($s,'&#10;')"/>
 </xsl:call-template>
 </xsl:when>
 <xsl:otherwise>
  <div>
  <xsl:value-of select="$s"/>
  <xsl:apply-templates
select="key('x',generate-id(.))[position()&lt;last()]"/>
<xsl:value-of
select="substring-before(key('x',generate-id(.))[last()],'&#10;')"/>
  </div>
 </xsl:otherwise>
 </xsl:choose>
</xsl:template>


</xsl:stylesheet>

$ saxon div.xml div.xsl

<html><head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
   <title>testing...</title></head>
    <body>
<div>This is the <a href="zzz">link</a> xxx</div>
<div>    This is another line</div>
<div>    and a <a href="zzz">link2</a> 2nd</div>
<div>    and a third</div></body>
</html>

20.

Tabulation, using keys

Mukul Gandhi



> I have a set of elements
> (recovered by SQL), each containing four child
> elements: property, prvalue, user_id and acct_id.

> I want to display it as:
> +--------+------------------+
> |property1 - prvalue1       |
> +--------+------------------+
> |user_id1|acct_id1, acct_id2|
> +--------+------------------+
> |user_id2|acct_id1 ...      |
> +--------+------------------+

We need to use composite keys as suggested by J.Pietschmann

With this input data

<?xml version="1.0" encoding="UTF-8"?>
<recordset>
  <record>
    <property>property2</property>
    <prvalue>prvalue2</prvalue>
    <user_id>user_id4</user_id>
    <acct_id>acct_id6</acct_id>
  </record>
  <record>
    <property>property1</property>
    <prvalue>prvalue1</prvalue>
    <user_id>user_id1</user_id>
    <acct_id>acct_id1</acct_id>
  </record>
  <record>
    <property>property2</property>
    <prvalue>prvalue2</prvalue>
    <user_id>user_id3</user_id>
    <acct_id>acct_id3</acct_id>
  </record>
  <record>
    <property>property1</property>
    <prvalue>prvalue1</prvalue>
    <user_id>user_id1</user_id>
    <acct_id>acct_id2</acct_id>
  </record>
  <record>
    <property>property2</property>
    <prvalue>prvalue2</prvalue>
    <user_id>user_id4</user_id>
    <acct_id>acct_id5</acct_id>
  </record>
  <record>
    <property>property1</property>
    <prvalue>prvalue1</prvalue>
    <user_id>user_id2</user_id>
    <acct_id>acct_id1</acct_id>
  </record>
  <record>
    <property>property2</property>
    <prvalue>prvalue2</prvalue>
    <user_id>user_id3</user_id>
    <acct_id>acct_id4</acct_id>
  </record>
</recordset>

The complete XSL is

<?xml version="1.0" encoding="UTF-8"?>
<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="x" match="recordset/record"
use="property"/>
 <xsl:key name="y" match="recordset/record"
use="concat(property,':',user_id)"/>

 <xsl:template match="recordset">
   <html>
     <head>
	<title>Grouping</title>
     </head>
     <body>
       <table border="1">
	<xsl:apply-templates select="record"/>
       </table>
     </body>
   </html>
 </xsl:template>
 
 <xsl:template match="record">
   <xsl:if test="generate-id(.) =
generate-id(key('x',property)[1])">
   <xsl:for-each select="key('x',property)">
     <xsl:if test="position() = 1">
       <tr>
	<td>
  	 <xsl:value-of select="property"/>
 	</td>
	<td>
	- <xsl:value-of select="prvalue"/>
	</td>
      </tr>
    </xsl:if>
    <xsl:if test="generate-id(.)=generate-id(key('y',
concat(property,':',user_id))[1])">
      <xsl:for-each
select="key('y',concat(property,':',user_id))">
        <xsl:if test="position() = 1">
	<tr>
	 <td>
	  <xsl:value-of select="user_id"/>
	 </td>
	 <td>
	   <xsl:for-each select="key('y',
concat(property,':',user_id))">				   
<xsl:value-of
select="acct_id"/>
	<xsl:if test="position()!=last()">,</xsl:if>
	</xsl:for-each>
	</td>
    </tr>				
  </xsl:if>
  </xsl:for-each>
 </xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:template>

</xsl:stylesheet>

21.

Using keys to lookup from current stylesheet

Justin Makeig


<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:a="http://cde.berkeley.edu/docbook/constant/acronym"
    exclude-result-prefixes="a">
    
    <xsl:key name="AcronymKey" match="a:acronymItem" use="a:acronym"/>
    
    <xsl:template name="AcronymnStandsFor">
        <xsl:param name="acronym"/>
        <!-- change context to current document so the key will work -->
        <xsl:for-each select="document('')">
            <xsl:value-of select="key('AcronymKey',$acronym)/a:standsFor"/>
        </xsl:for-each>
    </xsl:template>
    
    <!-- acronym lookups -->
    <a:acronymList>
        <a:acronymItem>
            <a:acronym>Ant</a:acronym>
            <a:standsFor>Another Neat Tool</a:standsFor>
        </a:acronymItem>
        ...
    </a:acronymList>
</xsl:stylesheet>

22.

performance enhancement in stylesheets by using keys!

Roger L. Costello

I did a crude test of how much keys can speed up a stylesheet. Here's my results.

I have a stylesheet which processes a 50 x 50 grid (a Vineyard with 50 tracts, each tract having 50 lots). I have 400 pickers that move around on the Vineyard. Processing the Vineyard and pickers requires accessing the lots thousands of times.

Here's the basic form of my stylesheet without keys:

<xsl:for-each select="1 to 50">
    <xsl:variable name="tract-num" select="."/>
    <xsl:for-each select="1 to 50">
        <xsl:variable name="lot-num" select="."/>
        <xsl:variable name="current-lot" 
                      select="/vineyard/lot[@tract-num eq
$tract-num][$lot-num eq $lot-num]"/>
        ... get all neighboring lots ...
        ... process current and neighboring lots ...
    </xsl:for-each>
</xsl:for-each>

Time required to process using this approach: 1 minute, 35 seconds (95 seconds).

Here's the basic form of my stylesheet using keys:

<xsl:for-each select="1 to 50">
    <xsl:variable name="tract-num" select="."/>
    <xsl:variable name="tract">
       <xsl:sequence select="/vineyard/key('tracts', $tract-num)"/> 
    </xsl:variable>  
    <xsl:for-each select="1 to 50">
        <xsl:variable name="lot-num" select="."/>
        <xsl:variable name="current-lot"  
                      select="$tract/key('lots', $lot-num)"/>
        ... get all neighboring lots ...
        ... process current and neighboring lots ...
    </xsl:for-each>
</xsl:for-each>

Time required to process using this approach: 27 seconds

Without using keys it took more than 3 times longer than when I used keys!

23.

Lookup XML file

Michael Kay



> The reason for a lookup xml source is to act as a cross-reference to 
> data in the main xml source.  So, I need to reference all elements 
> who's ID = 123 but my match is expressed as a code value "abc".  The 
> code "abc" maps to 123 in my lookup.

> So, I'm still scratching my head wondering how I might effectively put 
> the external file to use with keys, but thinking it won't work, the 
> context would strictly be of the main xml source.

lookup.xml

<lookup>
 <entry code="abc" value="123"/>
 <entry code="xyz" value="987"/>
</lookup>

main.xml

<data>
  <reading code="abc"/>
</data>

required output

<data>
  <reading value="123"/>
</data>

stylesheet

<xsl:transform ....


<xsl:key name="k" match="entry" use="@code"/>

<xsl:template match="*">
  <xsl:copy><xsl:copy-of 
      select="@*"/><xsl:apply-templates/>
  </xsl:copy>
</xsl:template>

<xsl:template match="reading">
  <reading>
    <xsl:variable name="code" select="@code"/>
    <xsl:for-each select="document('lookup.xml')">
<xsl:attribute name="value">
  <xsl:value-of select="key('k', $code)/@value"/> </xsl:attribute>

    </xsl:for-each>
  </reading>
</xsl:template>

or in 2.0

<xsl:template match="reading">
  <reading value="{key('k', @code, 
   document('lookup.xml'))}"/> 
  </xsl:template>

24.

Bibliographic references

Mike Kay




> I am trying to output the following bibliography references
> (<bibref...>) in following format.
>
> ***This is the output that I want:***
>
> Refer to References (1, 4, 5) for guidelines on performing
> precision testing.
>
> ***The source file looks like this:***
>
> <para>Refer to References (<bibref
> xref="bib98861831"/>,<bibref
> xref="bib98861816"/>,<bibref xref="bib988618273"/>) for
> guidelines on performing precision testing.</para>
>
> <bibliography><title>REFERENCES</title>
>   <bibliomixed id="bib98861831">National
> Committee...<booktitle>Fundamentals...</booktitle>...</bibliomixed>
>
>   <bibliomixed id="bib98861814">National Committee...</bibliomixed>
>   <bibliomixed id="bib98861870">National Committee...</bibliomixed>
>   <bibliomixed id="bib98861816">National Committee...</bibliomixed>
>   <bibliomixed id="bib988618273">National Committee for
> Clinical...</bibliomixed>  </bibliography>
>

In 2.0 I would be inclined to do:

<xsl:key name="b" match="bibliomixed" use="@id"/>
<xsl:template match="bibref">
 <xsl:number select="key('b', @xref)"/>
</xsl:template>

Mark Shellenberger offers an XSLT 1.0 solution.

   <xsl:template match="bibliography"/>
   <xsl:template match="bibref">
       <xsl:variable name="xref" select="@xref"/>
       <xsl:for-each select="//bibliography/bibliomixed">
         <xsl:if test="@id = $xref">
           <xsl:value-of select="count(preceding-sibling::bibliomixed)+1"/>
         </xsl:if>
      </xsl:for-each>
   </xsl:template>
</xsl:stylesheet>

DC elaborates with a more general approach

Always in any cross referencing in xslt the trick is to generate the number on the _referenced_ node, often just using <xsl:number/> does the job, sometimes you need to use count() but whatever it is just code it once.

Then to make a cross reference (whether the reference text is a number or a section heading or short id etc) the method is the same _go to the referenced node_ and generate the text.

so in this case

<xsl:key name="bib" match="bibliomixed" use="@id"/>

<!-- make a numbered listing at the end -->
<xsl:template match="bibliomixed">
[<xsl:number/>] <xsl:apply-templates/>
</xsl:template>

<!--make xref-->

<xsl:template match="xref">
see [<xsl:for-each
select="key('bib',@xref)"><xsl:number/></xsl:for-each>]
</xsl:template>

see xsl:number is evaluated in both cases on the bibliomixed element,so it makes the same number on both cases. If instead of [3] you wanted [National Committee...]or [NatCom2006] or any other format you'd use the smae basic idea, go the the bibliomixed node then (instead of xsl:number) generate the same text for both the xref and the listing.

25.

Check id, idref pairs

Mike Kay



> > Our schema has numerous elements with "id" attributes (target 
> > IDs) 

<xsl:key name="kid" match="*[@id]" use="@id"/>

> > and several with "refID" attributes (reference IDs).  
> > Unfortunately, these are not defined as ID and IDREF 
> > respectively or I wouldn't be having this issue.  I need to 
> > compare all of the "refID" attributes in a document against 
> > all of the "id" attributes and generate a report showing the 
> > "refID" attributes that do not have a valid target. 

<xsl:for-each select="//*[@refID][not(key('kid', @refID)])">
  <if test="not(.)">
  <xsl:message>No key</xsl:message>
  </xsl:if>
</xsl:for-each>

26.

How to select using multiple Keys

David Carlisle, Mike Kay



How do I use multiple key values?

> Declaration:
> <xsl:key name="keyname" match="subroot" use="ccc"/>

> During the usage, I want to specify multiple values:

> <xsl:variable name="keyname" select="key('keyname', '11' or 
> '22')"/> ==>  Here I want to use multiple values 11 and 22.

> For the following xml, result should be:
> aaaaa bbbbb ccccc ddddd ==> note that both values of  ccc= 
> '22' and ccc= '11' 


> <?xml version="1.0" encoding="UTF-8"?>
> <root>    
>     <subroot id="11111">
>         <ccc>11</ccc> ==> needs to be picked
>         <ddd>2005-08-26</ddd>
>         <eee>aaaaa</eee>
>     </subroot>
>     <subroot id="11111">
>         <ccc>22</ccc> ==> needs to be picked
>         <ddd>2005-08-26</ddd>
>         <eee>bbbbb</eee>
>     </subroot>
>     <subroot id="11111">
>         <ccc>11</ccc>
>         <ddd>2005-08-26</ddd>
>         <eee>ccccc</eee>
>     </subroot>
>     <subroot id="11111">
>         <ccc>11</ccc>
>         <ddd>2005-08-26</ddd>
>         <eee>ddddd</eee>
>     </subroot>    
>     <subroot id="11111">
>         <ccc>33</ccc>
>         <ddd>2005-08-26</ddd>
>         <eee>eeeee</eee>
>     </subroot>     
> </root> 

In XSLT 2.0, you can supply a sequence:

key('keyname', ('111', '222'))

DC offers

<xsl:variable name="keyname" 
   select="key('keyname', '22')|key('keyname', '11')"/>

27.

Using keys as a map/hashtable/dictionary

Dimitre Novatchev


I'm having some problems with constructs like hash tables or
indexes, which are quick in other languages but presently I
am using some brute force with.  E.G.


<xsl:template name="month-of">
 <xsl:param name="mon"/>
   <xsl:choose>
     <xsl:when test="lower-case($mon) = 'jan'">
       <xsl:value-of select="'01'"/>
     </xsl:when>
     <xsl:when test="lower-case($mon) = 'feb'">
       <xsl:value-of select="'02'"/>
     </xsl:when>

Whenever you need a "HashTable" or "Dictionary" or "Map", try to use keys:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:m="my:Months">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <m:Months>
  <m name="jan" mid="1"/>
  <m name="feb" mid="2"/>
  <m name="mar" mid="3"/>
  <m name="apr" mid="4"/>
  <m name="may" mid="5"/>
  <m name="jun" mid="6"/>
  <m name="jul" mid="7"/>
  <m name="aug" mid="8"/>
  <m name="sep" mid="9"/>
  <m name="oct" mid="10"/>
  <m name="nov" mid="11"/>
  <m name="dec" mid="12"/>
 </m:Months>

 <xsl:key name="kIdFromName" match="@mid"
          use="../@name"/>

 <xsl:variable name="vUpper" select=
 "'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>

 <xsl:variable name="vLower" select=
 "'abcdefghijklmnopqrstuvwxyz'"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="m/text()">
  <xsl:variable name="vCur" select="."/>
  <xsl:for-each select="document('')">
  <xsl:value-of select=
   "key('kIdFromName',
        translate($vCur,$vUpper,$vLower)
        )"/>
  </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

when this XSLT 1.0 transformation ()as you indicated you are limited
to XSLT 1.0)  is applied on the following document:

<t>
 <m>Mar</m>
 <m>Jun</m>
 <m>Aug</m>
 <m>Oct</m>
 <m>May</m>
 <m>Jan</m>
 <m>Dec</m>
 <m>Nov</m>
 <m>Sep</m>
 <m>Jul</m>
 <m>Feb</m>
 <m>Apr</m>
</t>

the wanted, correct result is produced:

<t>
   <m>3</m>
   <m>6</m>
   <m>8</m>
   <m>10</m>
   <m>5</m>
   <m>1</m>
   <m>12</m>
   <m>11</m>
   <m>9</m>
   <m>7</m>
   <m>2</m>
   <m>4</m>
</t>