0

I can not figure out how to make this work using two files with xsltproc. cooking.xml is opened using document() and menu.xml is passed in on the command line. I can select the recipes without issue, what I can not figure out is how to get a unique list of ingredients. When I use the preceding-sibling function on my list of ingredients it behaves like this {[shell, beef, lettuce, tomato, cheese], [eggs, cheese]}. Why does a select like "cooking/recipe[@name = $menu]/ingredients" create a disjoint set that I can't use preceding-sibling on?

This is a contrived example from a larger system.

File cooking.xml

<?xml version="1.0" encoding="UTF-8"?>
<cooking  xmlns="https://cooking.com/2022/cooking">
    <recipe name="tacos">
    <ingredient name="shell"/> 
    <ingredient name="beef"/> 
    <ingredient name="lettuce"/>
    <ingredient name="tomato"/>
    <ingredient name="cheese"/>
    </recipe>
    <recipe name="hamburger">
    <ingredient name="bun"/> 
    <ingredient name="beef"/> 
    <ingredient name="lettuce"/>
    <ingredient name="tomato"/>
    </recipe>
    <recipe name="omelet">
    <ingredient name="eggs"/> 
    <ingredient name="cheese"/>
    </recipe>
    <recipe name="soup">
    <ingredient name="chicken"/> 
    <ingredient name="stock"/>
    </recipe>
</cooking>

File menu.xml

<?xml version="1.0" encoding="UTF-8"?>
<cooking xmlns="https://cooking.com/2022/cooking">
    <recipe name="tacos"/>
    <recipe name="omelet"/>
</cooking>

File shop.xsl

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:set="http://exslt.org/sets"
xmlns:cook="https://cooking.com/2022/cooking"
extension-element-prefixes="set">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

<xsl:key name="rcp" match="recipe" use="@name" />

<xsl:template match="cooking">
    <output>
    <xsl:variable name="menu" select="recipe/@name" />
    <!-- switch context to target document in order to use key -->
    <xsl:for-each select="document('cooking.xml')">
        <xsl:for-each select="set:distinct(key('rcp', $menu)/ingredient/@name)">
            <ingredient name="{.}"/>
        </xsl:for-each>
    </xsl:for-each>
    </output>
</xsl:template>
  
</xsl:stylesheet>

xsltproc shop.xsl menu.xml >ingredients.xml

<?xml version="1.0" encoding="UTF-8"?>
<output xmlns:cook="https://cooking.com/2022/cooking"/>
 

Desired output:

<?xml version="1.0" encoding="UTF-8"?>
<cooking xmlns:cook="https://cooking.com/2022/cooking">
    <ingredient name="shell"/> 
    <ingredient name="beef"/> 
    <ingredient name="lettuce"/>
    <ingredient name="tomato"/>
    <ingredient name="cheese"/>
    <ingredient name="eggs"/> 
</cooking>
Jon Smirl
  • 349
  • 2
  • 10
  • So where is the minimal but complete XSLT code together with the exact unwanted result sample and the wanted result sample? – Martin Honnen Nov 28 '21 at 19:11

1 Answers1

1

Why does a select like "cooking/recipe[@name = $menu]/ingredients" create a disjoint set that I can't use preceding-sibling on?

Because the preceding-sibling axis is evaluated in the context of the input tree, not in the context of your selection. It would be different if you had copied your selection to a variable.

Either way, using the preceding-sibling axis is not a good method to select unique values. If you're using xsltproc (i.e. libxslt) then you could do something like:

XSLT 1.0 + EXSLT set:distinct-values()

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:set="http://exslt.org/sets"
extension-element-prefixes="set">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
  
<xsl:template match="/cooking">
    <output>
        <xsl:for-each select="set:distinct(document('cooking.xml')/cooking/recipe[@name = current()/recipe/@name]/ingredient/@name)">
            <ingredient name="{.}"/>
        </xsl:for-each>
    </output>
</xsl:template>
  
</xsl:stylesheet>

or perhaps a bit more efficiently:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:set="http://exslt.org/sets"
extension-element-prefixes="set">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

<xsl:key name="rcp" match="recipe" use="@name" />

<xsl:template match="/cooking">
    <output>
        <xsl:variable name="menu" select="recipe/@name" />
        <!-- switch context to target document in order to use key -->
        <xsl:for-each select="document('cooking.xml')">
            <xsl:for-each select="set:distinct(key('rcp', $menu)/ingredient/@name)">
                <ingredient name="{.}"/>
            </xsl:for-each>
        </xsl:for-each>
    </output>
</xsl:template>
  
</xsl:stylesheet>

to get:

Result

<?xml version="1.0" encoding="UTF-8"?>
<output>
  <ingredient name="shell"/>
  <ingredient name="beef"/>
  <ingredient name="lettuce"/>
  <ingredient name="tomato"/>
  <ingredient name="cheese"/>
  <ingredient name="eggs"/>
</output>

But for this cooking.xml must a well-formed XML document.

michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Thanks, this is what I was looking for. I had played with set:district() but I was not using it properly. I will work this into my production system and let you know how it goes. – Jon Smirl Nov 28 '21 at 20:07
  • The production data has a namespace on it. I have updated the question to include namespaces. – Jon Smirl Nov 28 '21 at 20:35
  • See: https://stackoverflow.com/a/34762628/3016153 – michael.hor257k Nov 28 '21 at 20:39
  • I finally understand why I could not get this to work. One of my input files is in a namespace, the other wasn't but it was using the same tags. To human me they looked the same. When I added the namespace to the second file I was able to get the production system working. – Jon Smirl Nov 28 '21 at 21:16
  • You do not need to modify your input. You can simply use a prefix to address the elements in one input file and unprefixed names to address the elements in the other file. – michael.hor257k Nov 28 '21 at 21:51
  • Did you declare `xmlns:set="http://exslt.org/sets"`? – michael.hor257k Nov 29 '21 at 00:57
  • Just figured that out, that is a terrible error message. – Jon Smirl Nov 29 '21 at 01:00