1

Here is some example XML from which I only need to select the closest ancestor to an element that contains a given string, the ancestor must have the @isDoc attribute.

In my below example, I would expect to only get the <example> node in the resulting nodeset if the value of $macroAlias was match.

<container isDoc="">
    <site isDoc="">
        <home isDoc="">
            <page>Some text</page>
            <page>Some text</page>
            <example isDoc="">
                <title>A title</title>
                <body><![CDATA[Some text and a page containing a <p>string to match</p>]]></body>
            </example>
            <page>Some text</page>
        </home>
    </site>
</container>

My current query can be found below. The problem with it is that it selects not only the closest ancestor but all other ancestors above that too. I really only want the ancestor node (with the @isDoc attribute) of the node containing the given string ($macroAlias).

<xsl:if test="$macroAlias != ''">
    <xsl:variable name="nodes"
                  select="//node()[contains(., $macroAlias)][ancestor::*[@isDoc][1]]/parent::*[@isDoc]"/>

        <ul>
            <li>
                <xsl:for-each select="$nodes">
                    <xsl:value-of select="name()"/>
                </xsl:for-each>
            </li>
        </ul>
</xsl:if>

I've tried many different ways to achieve this and either end up with the same result or no results in my output.

ProNotion
  • 3,662
  • 3
  • 21
  • 30
  • Your question is not clear (not to say confusing). The `example` element in your example is not ancestor of anything other than its text child node and the `isDoc` attribute . In general, the expression `ancestor::*[some condition][1]` will select the nearest ancestor that satisfies the condition. Please reduce the example to the minimum necessary to demonstrate the problem - see: [mcve]. – michael.hor257k May 05 '23 at 06:40
  • I am afraid this still doesn't make sense. If the searchString is "match", then both `body` and `contact` have it in their child text nodes. For `contact`, the nearest ancestor with an `isDoc` attribute is `home`. Why should it not be included in the output? – michael.hor257k May 06 '23 at 14:33

1 Answers1

1

The problem with the . in node()[contains(., $macroAlias)] is that it will find any node() (also text()-nodes) that contains (all descendants included) this $macroAlias. See for a explanation the differnce between text() and . i.e. this question.

So that is true for:

  • /container[@isDoc] and
  • /container[@isDoc]/site[@isDoc] and
  • /container[@isDoc]/site[@isDoc]/home[@isDoc] and
  • /container[@isDoc]/site[@isDoc]/home[@isDoc]/example[@isDoc] and
  • /container[@isDoc]/site[@isDoc]/home[@isDoc]/example[@isDoc]/example[@isDoc]/text()

If the $macroAlias is in a child-text() of a element with *[@isDoc] (as in your example), I would try this:

<xsl:variable name="nodes"
              select="//*[@isDoc][contains(text(), $macroAlias)]"/>

If it $macroAlias is not a direct child of a element with a @isDoc-attribute you could use:

<xsl:variable name="nodes"
              select="//text()[contains(., $macroAlias)]/ancestor::*[@isDoc][1]"/>

Base on this xml:

<container isDoc="">
  <site isDoc="">
    <home isDoc="">
      <page>Some text</page>
      <page>Some text</page>
      <example isDoc="">
        <title>A title</title>
        <body><![CDATA[Some text and a page containing a <p>string to match</p>]]></body>
      </example>
      <page>Some text</page>
      <contact><![CDATA[Some text and a page containing a <p>string to not match</p>]]></contact>
    </home>
  </site>
</container>

using this xslt:

<?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:strip-space elements="*"/>
  <xsl:param name="macroAlias" select="'A title'"></xsl:param>
  
  <xsl:template match="/">
    <xsl:if test="$macroAlias != ''">
      <xsl:variable name="nodes" select="//text()[contains(., $macroAlias)]/ancestor-or-self::*[@isDoc][1]"/>
      <ul>
        <li>
          <xsl:for-each select="$nodes">
            <xsl:value-of select="name()"/>
          </xsl:for-each>
        </li>
      </ul>
    </xsl:if>  
  </xsl:template>
  
</xsl:stylesheet>

Will give this result:

<?xml version="1.0" encoding="UTF-8"?>
<ul>
   <li>example</li>
</ul>
Siebe Jongebloed
  • 3,906
  • 2
  • 14
  • 19
  • This gets me much closer. As per another request I have tried to simplify my question and example with the expected result. The 2nd query you have given did not seem to be valid unless I put square brackets around the ancestor part `//*[contains(text(), $macroAlias)][ancestor::*[@isDoc][1]]`. Based on my example this returns `` and `` nodes but should only return `` – ProNotion May 06 '23 at 06:00
  • 1
    If changed ancestor::* to ancestor-or-self::* and added example code/result – Siebe Jongebloed May 06 '23 at 10:49
  • 2
    I suspect you could simplify this by looking for `//text()[contains(., $macroAlias)]/ancestor::*[@isDoc][1]`. If that's really the desired logic (I am still not entirely sure). I would also add this reference to your explanation: https://www.w3.org/TR/1999/REC-xpath-19991116/#dt-string-value – michael.hor257k May 06 '23 at 12:03
  • @michael.hor257k: thanks, that is simplifies it. I have changed the code. The advised link does not take me to something I understand – Siebe Jongebloed May 06 '23 at 13:10
  • 1
    Well, the thing is that the arguments of the `contains()` function are *strings*. When the supplied argument is a *node*, it is converted to a string by returning the **string-value** of the node. Each type of node has its own string-value definition. The string-value of an element node is the concatenation of the string-values of all text node descendants of the element node in document order. The string-value of a text node is the character data. – michael.hor257k May 06 '23 at 13:34
  • @SiebeJongebloed This gets me the results I needed, thank you for your perseverance and for providing the solution. – ProNotion May 06 '23 at 16:46