xml:id of ancestor element

75 Views Asked by At

My stylesheet used version="3.0". My engine version Saxon PEE 9.9.1.7.

The xml:id attribute of w is built from xml:id of l + '_' + the text() value of w. It seems to me that the xpath of xml:id of l is correct, but this doesn't work. Example of xml before transformation:

<div3 xml:id="ktu1-1_ii">
   <!-- tei elements div4 and lg -->
         <l n="1a">
            <!-- other children -->
            <w>l</w>
         </l>

Processing instructions:

   <xsl:template match="div3/div4/lg/l">
      <xsl:copy>
         <xsl:apply-templates select="@*"/>
         <xsl:if test="not(@xml:id)">
            <xsl:attribute name="xml:id">
               <xsl:value-of select="concat(ancestor::div3/@xml:id, '_l', @n)"/>
            </xsl:attribute>
         </xsl:if>
         <xsl:apply-templates/>
      </xsl:copy>
   </xsl:template>
   
 
   <xsl:template match="l/w">
      <xsl:variable name="xmlid">
         <xsl:value-of select="ancestor::l/@xml:id"/>
      </xsl:variable>
      <xsl:message select="$xmlid"/> <!-- test @xml:id, nothing -->
      <xsl:copy>
         <xsl:apply-templates select="@*"/>
         <xsl:attribute name="pos"></xsl:attribute>
         <!-- Calculate xml:id based on parent 'l' xml:id -->
         <xsl:attribute name="xml:id">
            <xsl:value-of select="normalize-space(replace(ancestor::l/@xml:id, ' ', '')) || '_' || replace(., '\s|̊', '')"/> 
         </xsl:attribute>
         <xsl:apply-templates/>
      </xsl:copy>
   </xsl:template> 

The result is correct for l: <l n='1a' xml:id='ktu1-1_ii_l1a'>, but only one part works for w <w xml:id="_l">l</w> when the result should be <w xml:id="ktu1-1_ii_l1a_l">l</w>.

Any idea where the error might have come from?

3

There are 3 best solutions below

3
michael.hor257k On BEST ANSWER

Consider the following (simplified) example:

XML

<div3 xml:id="ktu1-1_ii">
    <!-- tei elements div4 and lg -->
    <l n="1a">
        <!-- other children -->
        <w>l</w>
        <w>l</w>
    </l>
</div3>

XSLT 3.0

<xsl:stylesheet version="3.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:mode on-no-match="shallow-copy"/>
  
<xsl:template match="l[not(@xml:id)]">
    <xsl:variable name="l_id" select="concat(ancestor::div3/@xml:id, '_l', @n)"/>
    <xsl:copy>
        <xsl:apply-templates select="@*"/>
        <xsl:attribute name="xml:id" select="$l_id"/>   
        <xsl:apply-templates>
            <xsl:with-param name="l_id" select="$l_id"/>
        </xsl:apply-templates>
    </xsl:copy>
</xsl:template>

<xsl:template match="w">
    <xsl:param name="l_id"/>
    <xsl:copy>
        <xsl:apply-templates select="@*"/>
        <xsl:attribute name="xml:id" select="concat($l_id, '_', position())"/>  
        <xsl:apply-templates/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Result

<?xml version="1.0" encoding="UTF-8"?>
<div3 xml:id="ktu1-1_ii"><!-- tei elements div4 and lg -->
   <l n="1a" xml:id="ktu1-1_ii_l1a"><!-- other children -->
      <w xml:id="ktu1-1_ii_l1a_2">l</w>
      <w xml:id="ktu1-1_ii_l1a_3">l</w>
   </l>
</div3>

Here the value of the l parent's xml:id is calculated once and passed down to the children as a parameter.


P.S. The purpose of this exercise is not entirely clear. If you need to have a unique ID for each node, you could simply have your stylesheet generate them instead of trying to assemble them piecemeal from their ancestor parts.

5
Michael Kay On

I think the mistake you are making is to imagine that ancestor::l/@xml:id will access the value of @xml:id on the l element in the result tree, whereas it will actually access the value of @xml:id on the l element in the source tree.

To access the new value of ancestor::l/@xml:id, some of the options are:

  1. Introduce a second phase of transformation, where the result of the first phase is captured in a variable and then subjected to a second transformation.

  2. Recompute the value when needed, by moving the computation into a function and calling it from multiple places.

  3. Make that function into a memo function so when you call it more than once on the same node, the previous value is remembered.

2
Vanessa On

Thanks to Martin, here is the correct xsl:

<xsl:template match="div3/div4/lg/l">
      <xsl:copy>
         <xsl:apply-templates select="@*"/>
         <xsl:if test="not(@xml:id)">
            <xsl:attribute name="xml:id">
               <xsl:value-of select="concat(ancestor::div3/@xml:id, '_l', @n)"/>
            </xsl:attribute>
         </xsl:if>
         <xsl:apply-templates/>
      </xsl:copy>
   </xsl:template>
   
   
   <xsl:template match="l/w">
      <xsl:variable name="xmlid">
         <xsl:value-of select="ancestor::l/@xml:id"/>
      </xsl:variable>
      <xsl:copy>
         <xsl:apply-templates select="@*"/>
         <xsl:attribute name="pos"></xsl:attribute>
         <!-- Calculate xml:id based on parent 'l' xml:id, @n, and text() value of the current element -->
         <xsl:attribute name="xml:id">
            <xsl:value-of select="concat(ancestor::div3/@xml:id, '_l', ancestor::l/@n, '_', normalize-space(replace(., '\s|̊', '')))"/>
         </xsl:attribute>
         <xsl:apply-templates/>
      </xsl:copy>
   </xsl:template> 

Result:

<l n='1a' xml:id='ktu1-1_ii_l1a'>
<!-- other children -->
<w xml:id="ktu1-1_ii_l1a_l">l</w>
</l>

Additional clarification added after receiving Michael's suggestion:

The current solution--which was my previous choice--is, from my point of view, likely more easily understandable, especially for xsl neophytes while Michael's suggestion is more concise and avoids repetition of the xpath with the addition of the variable <xsl:variable name="l_id" select="concat(ancestor::div3/@xml:id, '_l', @n)"/> in l and the parameter <xsl:with-param name="l_id" select="$l_id"/>--although I made a few adjustments considering my entire transformation file.