Search code examples
xsltxslt-2.0cross-reference

Cross-referencing with AND condition in XSLT


I am trying to cross-reference from an external XML file, but instead of comparing just one key, I want to ask if one string AND other strings exist, and if yes reference from the external file:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:t="http://www.tei- 
c.org/ns/1.0"
xmlns="http://www.tei-c.org/ns/1.0" exclude-result-prefixes="xs t">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:param name="ids"
    select="document('instructions.xml')"/>

<xsl:key name="id" match="row" use="tokenize(normalize-space(elem[@name='Instruction']), ' ')"/>


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

<xsl:template match="instruction">
    <xsl:for-each select=".[contains(.,key('id', ., .))]">
    <xsl:copy>     
        <xsl:attribute name="norm">
            <xsl:value-of select="normalize-space(key('id', normalize-space(.), $ids)/elem[@name='Norm'])"/>
        </xsl:attribute>
        <xsl:apply-templates select="@* | node() | text() | *"/>   
    </xsl:copy>
    </xsl:for-each>
</xsl:template>

Input (External File):

<row>
  <elem name="instruction">pour out</elem>
  <elem name="norm">p1</elem>
</row>

Input (File to annotate):

<ab type="recipe">
Bla bla
  <instruction>pour the milk out</instruction> bla
</ab>

Desired Output:

<ab type="recipe">
Bla bla
  <instruction norm="p1">pour the milk out</instruction> bla
</ab>

In order words: Both of the tokens in the external XML file within the element <elem name="instruction"> "pour" AND "out" need to be contained within the <instruction>element in my XML file. If they are I want to set the norm attribute to the value of <elem name="norm"> in the external file.

Any help much appreciated!


Solution

  • I couldn't work out how to do it with a key, but I did come up with an alternate approach....

    <xsl:template match="instruction">
        <xsl:variable name="words" select="tokenize(normalize-space(.), ' ')" />
        <xsl:variable name="row" select="$ids//row[every $i in tokenize(normalize-space(elem[@name='instruction']), ' ') satisfies $i = $words]" />
        <xsl:copy>
            <xsl:if test="$row">
                <xsl:attribute name="norm" select="$row/elem[@name='norm']" />    
            </xsl:if>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    

    EDIT: In response to your comment, if you can have multiple rows matching, then to get the one with the most matching words, do this....

    <xsl:template match="instruction">
        <xsl:variable name="words" select="tokenize(normalize-space(.), ' ')" />
        <xsl:variable name="row" as="element()*">
            <xsl:perform-sort select="$ids//row[every $i in tokenize(normalize-space(elem[@name='instruction']), ' ') satisfies $i = $words]">
                <xsl:sort select="count(tokenize(normalize-space(elem[@name='instruction']), ' '))" order="descending" />
            </xsl:perform-sort>
        </xsl:variable>
        <xsl:copy>
            <xsl:if test="$row">
                <xsl:attribute name="norm" select="$row[1]/elem[@name='norm']" />    
            </xsl:if>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>