Trippy XSL: Grouping nodes based on a rule set (StackOverflow)
This is an open discussion with 2 replies, filed under XSLT.
Search
I'm about to answer my own question on SO.
This is the solution I've found to work:
<!-- For each /item: - see it it's one of the starting points of a sequence: - Not "Standalone" - Left, Top-Left - if standalone, just return the element - if not standlone and not a starting point, skip it, since it means it being added by the previous item. - if not standolw and either of the starting points, kick into a recursion loop in a separate template: - store the item's current "score" (2 or 1 for single-quadrant images) - recur through the following-siblings with a counter for position(), checking if they are in the allowed list, and decreasing the "score" counter. - every time a match is found: - recreate the "allow" list, minus the current match, and pass the updated list to the next iteration - decrease the counter - if the iteration completes, reaching zero, return the position() of the last matched item - if during the iteration, while the score is still >0, a match is not found, return false(). Our sequence is broken, we have a user error. - the calling template (the one matching *every* item) checks whether the returned result is >0 or false() - if >0 returns a copy of every node up the number specified by >0 - if false() print out and error, suggesting possible sequences. --> <xsl:variable name="layouts"> <start handle="left" score="2"> <!-- The starting score which we'll subtract from on every iteration --> <allow handle="right" value="2"/> <!-- the acceptable position which we'll check against on every iteration --> <allow handle="top-right" value="1"/> <!-- the value for each position which we'll subtract from the <start> score --> <allow handle="bottom-right" value="1"/> </start> <start handle="top-left" score="3"> <allow handle="right" value="2"/> <allow handle="bottom-left" value="1"/> <allow handle="top-right" value="1"/> <allow handle="bottom-right" value="1"/> </start> <start handle="full" score="0"/> <!-- Position which are not acceptable as the start of a sequence are scored 0 --> <start handle="right" score="0"/> <start handle="top-right" score="0"/> <start handle="bottom-right" score="0"/> <start handle="bottom-left" score="0"/> </xsl:variable> <!-- Applied to every /item --> <xsl:template mode="imagewraps" match="item"> <xsl:param name="i" select="position()"/> <xsl:variable name="nodeName" select="name(.)"/> <xsl:variable name="layout" select="exsl:node-set($layouts)"/> <xsl:variable name="position" select="position/item/@handle"/> <xsl:variable name="score" select="$layout/start[@handle = $position]/@score"/> <xsl:variable name="allowList" select="$layout/start[@handle = $position]"/> <!-- This variable will store the final result of the recursion lanunched from within. The returned value will be a number, indication the position of the last node that is part of the sequence --> <xsl:variable name="sequenceFound"> <xsl:if test="$score > 0"> <xsl:apply-templates mode="test" select="parent::node()/*[name() = $nodeName][$i +1]"> <xsl:with-param name="i" select="$i +1"/> <xsl:with-param name="score" select="$score"/> <xsl:with-param name="allowList" select="$allowList"/> </xsl:apply-templates> </xsl:if> </xsl:variable> <div style="border: 1px solid red"> <xsl:choose> <!-- If the $score is 0 and the position is 'full' just return a copy if the current node --> <xsl:when test="$score = 0 and $position = 'full'"> <xsl:copy-of select="."/> </xsl:when> <!-- if the $score is greater than 0, return a copy of the current node and the siblings the follow, up to the value stored in $sequenceFound --> <xsl:when test="$score > 0"> <xsl:choose> <!-- Actually do the above only if $sequenceFound didn't end up being 0 (it currently never does, but good to have as an option to handle errors in here) --> <xsl:when test="$sequenceFound != 0"> <xsl:copy-of select="."/> <xsl:copy-of select="following-sibling::*[$sequenceFound - $i >= position()]"/> </xsl:when> </xsl:choose> </xsl:when> <!-- If the first item is wrong, let jsut say it --> <xsl:when test="$score = 0 and position() > 1"> <xsl:message>The first item should either be "full", "left", "top-left".</xsl:message> </xsl:when> </xsl:choose> </div> </xsl:template> <xsl:template mode="test" match="*"> <xsl:param name="i"/> <xsl:param name="score"/> <xsl:param name="allowList"/> <xsl:variable name="this" select="."/> <xsl:variable name="nodeName" select="name()"/> <xsl:variable name="position" select="position/item/@handle"/> <xsl:variable name="isInAllowList" select="count($allowList/allow[@handle = $position]) > 0"/> <xsl:variable name="value"> <xsl:if test="$isInAllowList"> <xsl:value-of select="$allowList/allow[@handle = $position]/@value"/> </xsl:if> </xsl:variable> <xsl:variable name="allowListMinusMatched"> <xsl:if test="$isInAllowList"> <xsl:copy-of select="$allowList/allow[@handle != $position]"/> </xsl:if> </xsl:variable> <xsl:choose> <xsl:when test="$isInAllowList"> <xsl:choose> <!-- if we've not ran out of loops, continue --> <xsl:when test="($score - $value) > 0"> <xsl:apply-templates mode="test" select="parent::node()/*[name() = $nodeName][$i +1]"> <xsl:with-param name="i" select="$i +1"/> <xsl:with-param name="allowList" select="$allowListMinusMatched"/> <xsl:with-param name="score" select="$score - $value"/> </xsl:apply-templates> </xsl:when> <xsl:when test="($score - $value) = 0"> <xsl:value-of select="$i"/> </xsl:when> </xsl:choose> </xsl:when> <xsl:otherwise> <xsl:variable name="layout" select="exsl:node-set($layouts)"/> <xsl:variable name="allowed" select="$layout/start[@handle = $position]"/> <xsl:message>Bombing out. Wrong Sequence.</xsl:message> <xsl:message> Items allowed after "<xsl:value-of select="$allowed/@handle"/>" are: <xsl:for-each select="$allowed/allow"> <xsl:value-of select="@handle"/> <xsl:if test="count($allowed/allow) > position()">, </xsl:if> <xsl:if test="count($allowed/allow) = position()">.</xsl:if> </xsl:for-each> </xsl:message> </xsl:otherwise> </xsl:choose> </xsl:template>
Hope someone can take some inspiration out of it.
And I am also immensely grateful to the dude who posted an answer just after I figured it out on my own. Pretty nice trick he used. Be sure to check it out http://stackoverflow.com/questions/7652982/grouping-nodes-based-on-a-rule-set
Create an account or sign in to comment.
This is a nasty one, for me at least. Is anyone up for helping me figure it out?
Jump in my SO question:
http://stackoverflow.com/questions/7652982/grouping-nodes-based-on-a-rule-set
Symphony-specific note
I have a variable layout situation for some images, and I would like to avoid tying the data structure to the layout by providing multiple upload fields on the same entry.
Each image upload has a multi-select option for
left
,right
,top-left
,top-right
,bottom-left
, etc.The data source gives these out as siblings, obviously, but I need to be able to group, say, a
left
image with aright
one (or with atop-right
and abottom-right
) if that makes sense.I also know it can be easily achieved via DOM manipulation, but I'm just getting in an XSL trip I'd like to solve. A nice utility could come out of this…