Reputation: 7568
Given the following Xml:
<record>
<category>Sport/Racket Sports/Tennis</category>
<category>Sport/Racket Sports/Badminton</category>
</record>
I'm trying to break up the categories so that the following Xml is produced:
<add>
<doc>
<field name="category_0">Sport</field>
<field name="category_1">Sport/Racket Sports</field>
<field name="category_2">Sport/Racket Sports/Tennis</field>
<field name="category_2">Sport/Racket Sports/Badminton</field>
</doc>
</add>
I've managed to produce something which is nearly there.. I now need a way of removing the duplicates? Any ideas?
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="record">
<add>
<doc>
<xsl:for-each select="category[. != '']">
<xsl:call-template name="split-cats">
<xsl:with-param name="prefix" select="''"/>
<xsl:with-param name="text" select="."/>
<xsl:with-param name="level" select="number(0)"/>
</xsl:call-template>
</xsl:for-each>
</doc>
</add>
</xsl:template>
<xsl:template name="split-cats">
<xsl:param name="text" select="."/>
<xsl:param name="prefix"/>
<xsl:param name="level" select="0"/>
<xsl:choose>
<xsl:when test="contains($text, '/')">
<field>
<xsl:attribute name="name">
<xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
</xsl:attribute>
<xsl:value-of select="concat($prefix, substring-before($text, '/'))"/>
</field>
<xsl:call-template name="split-cats">
<xsl:with-param name="prefix" select="concat($prefix, concat(substring-before($text, '/'), '/'))"/>
<xsl:with-param name="text" select="substring-after($text, '/')"/>
<xsl:with-param name="level" select="$level + 1"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<field>
<xsl:attribute name="name">
<xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
</xsl:attribute>
<xsl:value-of select="concat($prefix, $text)"/>
</field>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
This template produces:
<add>
<doc>
<field name="category_0">Sport</field>
<field name="category_1">Sport/Racket Sports</field>
<field name="category_2">Sport/Racket Sports/Tennis</field>
<field name="category_0">Sport</field>
<field name="category_1">Sport/Racket Sports</field>
<field name="category_2">Sport/Racket Sports/Badminton</field>
</doc>
</add>
Which as you can see has Sport
and Sport/Racket Sports
in there twice :(
FYI: I need to be able to do this using XSLT 1.0
Thanks
Dave
Upvotes: 0
Views: 976
Reputation:
Without extension functions, this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:template match="record">
<add>
<doc>
<xsl:apply-templates select="category[1]"/>
</doc>
</add>
</xsl:template>
<xsl:template match="category" name="category">
<xsl:param name="pOutput"/>
<xsl:param name="pPrefix"/>
<xsl:param name="pLevel" select="0"/>
<xsl:param name="pSequence" select="concat(.,'/')"/>
<xsl:choose>
<xsl:when test="$pSequence">
<xsl:variable name="vItem"
select="concat($pPrefix,
substring-before($pSequence,
'/'))"/>
<xsl:variable name="vOutput"
select="concat('|',$vItem,'|')"/>
<xsl:if test="not(contains($pOutput,$vOutput))">
<field name="category_{$pLevel}">
<xsl:value-of select="$vItem"/>
</field>
</xsl:if>
<xsl:call-template name="category">
<xsl:with-param name="pOutput"
select="concat($pOutput,$vOutput)"/>
<xsl:with-param name="pPrefix"
select="concat($vItem,'/')"/>
<xsl:with-param name="pLevel" select="$pLevel + 1"/>
<xsl:with-param name="pSequence"
select="substring-after($pSequence,'/')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="following-sibling::category[1]">
<xsl:with-param name="pOutput" select="$pOutput"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Output:
<add>
<doc>
<field name="category_0">Sport</field>
<field name="category_1">Sport/Racket Sports</field>
<field name="category_2">Sport/Racket Sports/Tennis</field>
<field name="category_2">Sport/Racket Sports/Badminton</field>
</doc>
</add>
Upvotes: 1
Reputation: 70598
Another way to do this without making use of extension function (but which will not necessarily be as efficient asusing Muenchian grouping) is to add a check to compare previous category records to see if they begin with the string you are about to about
<xsl:if test="not(/record/category
[. != '']
[position() < $pos]
[substring(., 1, string-length($field-text)) = $field-text])">
In this bit of code $pos is a parameter containing the position of the current category element you are currently matching, and $field-text is a variable containing the text you are about to output.
Here is the complete XSLT stylesheet, which should also give you the output you desire
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="record">
<add>
<doc>
<xsl:for-each select="category[. != '']">
<xsl:call-template name="split-cats">
<xsl:with-param name="prefix" select="''"/>
<xsl:with-param name="text" select="."/>
<xsl:with-param name="level" select="number(0)"/>
<!-- Position of the current category -->
<xsl:with-param name="pos" select="position()"/>
</xsl:call-template>
</xsl:for-each>
</doc>
</add>
</xsl:template>
<xsl:template name="split-cats">
<xsl:param name="text" select="."/>
<xsl:param name="prefix"/>
<xsl:param name="level" select="0"/>
<xsl:param name="pos"/>
<xsl:choose>
<xsl:when test="contains($text, '/')">
<xsl:variable name="field-text" select="concat($prefix, substring-before($text, '/'))"/>
<!-- Test no previous category begins with the text we are about to output -->
<xsl:if test="not(/record/category
[. != '']
[position() < $pos]
[substring(., 1, string-length($field-text)) = $field-text])">
<field>
<xsl:attribute name="name">
<xsl:text>category_</xsl:text>
<xsl:value-of select="$level"/>
</xsl:attribute>
<xsl:value-of select="$field-text"/>
</field>
</xsl:if>
<xsl:call-template name="split-cats">
<xsl:with-param name="prefix" select="concat($prefix, concat(substring-before($text, '/'), '/'))"/>
<xsl:with-param name="text" select="substring-after($text, '/')"/>
<xsl:with-param name="level" select="$level + 1"/>
<xsl:with-param name="pos" select="$pos"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="field-text" select="concat($prefix, $text)"/>
<!-- Test no previous category begins with the text we are about to output -->
<xsl:if test="not(/record/category
[. != '']
[position() < $pos]
[substring(., 1, string-length($field-text)) = $field-text])">
<field>
<xsl:attribute name="name">
<xsl:text>category_</xsl:text>
<xsl:value-of select="$level"/>
</xsl:attribute>
<xsl:value-of select="$field-text"/>
</field>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Upvotes: 1
Reputation: 167436
Here is a straight-forward approach that simply inserts a second transformation step using Muenchian grouping on the result your code creates:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<xsl:output indent="yes"/>
<xsl:key name="k1" match="cats/field" use="."/>
<xsl:template match="record">
<xsl:variable name="cats">
<cats>
<xsl:for-each select="category[. != '']">
<xsl:call-template name="split-cats">
<xsl:with-param name="prefix" select="''"/>
<xsl:with-param name="text" select="."/>
<xsl:with-param name="level" select="0"/>
</xsl:call-template>
</xsl:for-each>
</cats>
</xsl:variable>
<add>
<doc>
<xsl:copy-of select="exsl:node-set($cats)/cats/field[generate-id() = generate-id(key('k1', .)[1])]"/>
</doc>
</add>
</xsl:template>
<xsl:template name="split-cats">
<xsl:param name="text" select="."/>
<xsl:param name="prefix"/>
<xsl:param name="level" select="0"/>
<xsl:choose>
<xsl:when test="contains($text, '/')">
<field>
<xsl:attribute name="name">
<xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
</xsl:attribute>
<xsl:value-of select="concat($prefix, substring-before($text, '/'))"/>
</field>
<xsl:call-template name="split-cats">
<xsl:with-param name="prefix" select="concat($prefix, concat(substring-before($text, '/'), '/'))"/>
<xsl:with-param name="text" select="substring-after($text, '/')"/>
<xsl:with-param name="level" select="$level + 1"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<field>
<xsl:attribute name="name">
<xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
</xsl:attribute>
<xsl:value-of select="concat($prefix, $text)"/>
</field>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
That makes use of exsl:node-set
however, if you target XSLT 1.0 in the browsers then for IE/MSXML you need to fix it with the script shown in http://dpcarlisle.blogspot.com/2007/05/exslt-node-set-function.html.
Upvotes: 1