Forum Moderators: open

Message Too Old, No Replies

Sorted Categories

Moving Along, Our Valiant n00b Hits a Cinder-Block Wall...

         

cmarshall

11:53 am on Mar 30, 2007 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Okay, now I'm getting fancy. I want to categorize the search results into sections.

For example, I have all my XML returned in sorted fashion. I have three categories, and the data is returned sorted into these categories:

wee_test.xml (The sorted XML data to be transformed):

<?xml version="1.0" encoding="ISO-8859-1"?>
<weetest xmlns="http://www.example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com file:/Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xsd">
<elementwrapper id="1">
<data_cat>1</data_cat>
<data_name>Test</data_name>

</elementwrapper>
<elementwrapper id="2">
<data_cat>1</data_cat>
<data_name>Test</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="3">
<data_cat>2</data_cat>
<data_name>Test</data_name>
</elementwrapper>
<elementwrapper id="4">
<data_cat>2</data_cat>
<data_name>Test</data_name>

</elementwrapper>
<elementwrapper id="5">
<data_cat>3</data_cat>
<data_name>Test</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="6">
<data_cat>3</data_cat>
<data_name>Test</data_name>
</elementwrapper>
</weetest>

I've adjusted the schema (wee_test.xsd) to accommodate the new data point:

<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.example.com"
elementFormDefault="qualified">
<xs:element name="weetest">
<xs:complexType>
<xs:sequence maxOccurs="unbounded">
<xs:element name="elementwrapper">
<xs:complexType>
<xs:sequence>
<xs:element name="data_cat" type="xs:integer"/>
<xs:element name="data_name" type="xs:string"/>
<xs:element minOccurs="0" name="data_uri" type="xs:anyURI"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

Here's the current XSL stylesheet (wee_test.xsl):

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wt="http://www.example.com" exclude-result-prefixes="wt">
<xsl:output indent="yes" method="html" version="4.01" doctype-system="http://www.w3c.org/tr/html4/strict.dtd" doctype-public="-//W3c//DTD HTML 4.01//EN"/>
<xsl:template match="/">
<html>
<head>
<title>XML Test</title>
</head>
<body>
<xsl:for-each select="wt:weetest/wt:elementwrapper">
<xsl:call-template name="render_one"/>
</xsl:for-each>
</body>
</html>
</xsl:template>
<xsl:template name="render_one">
<xsl:element name="div">
<xsl:attribute name="class">
<xsl:text>alt_</xsl:text>
<xsl:number value="position() mod 2" format="1"/>
</xsl:attribute>
<xsl:element name="div">
<xsl:attribute name="class">category</xsl:attribute>
<xsl:value-of select="wt:data_cat"/>
</xsl:element>

<div class="display">
<xsl:choose>
<xsl:when test="wt:data_uri!= ''">
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of disable-output-escaping="yes" select="wt:data_uri"/>
</xsl:attribute>
<xsl:value-of select="wt:data_name"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wt:data_name"/>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:element>
</xsl:template>
</xsl:stylesheet>

And it outputs the current HTML:

<!DOCTYPE html PUBLIC "-//W3c//DTD HTML 4.01//EN" "http://www.w3c.org/tr/html4/strict.dtd">
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>XML Test</title>
</head>
<body>
<div class="alt_1">
<div class="category">1</div>
<div class="display">
<a href="http://www.example.com/wee_test.html">Test</a>
</div>
</div>
<div class="alt_0">
<div class="category">1</div>
<div class="display">Test</div>
</div>
<div class="alt_1">
<div class="category">2</div>
<div class="display">Test</div>
</div>
<div class="alt_0">
<div class="category">2</div>
<div class="display">
<a href="http://www.example.com/wee_test.html">Test</a>
</div>
</div>
<div class="alt_1">
<div class="category">3</div>
<div class="display">Test</div>
</div>
<div class="alt_0">
<div class="category">3</div>
<div class="display">Test</div>
</div>
</body>
</html>

Now, what I want to output is:

<!DOCTYPE html PUBLIC "-//W3c//DTD HTML 4.01//EN" "http://www.w3c.org/tr/html4/strict.dtd">
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>XML Test</title>
</head>
<body>
<h1>Category 1</h1>
<div class="alt_1">
<div class="display">
<a href="http://www.example.com/wee_test.html">Test</a>
</div>
</div>
<div class="alt_0">
<div class="display">Test</div>
</div>
<h1>Category 2</h1>
<div class="alt_1">
<div class="display">Test</div>
</div>
<div class="alt_0">
<div class="display">
<a href="http://www.example.com/wee_test.html">Test</a>
</div>
</div>
<h1>Category 3</h1>
<div class="alt_1">
<div class="display">Test</div>
</div>
<div class="alt_0">
<div class="display">Test</div>
</div>
</body>
</html>

There doesn't seem to be an obvious way to do this. I do it now in PHP. What I do is keep track of the current category, and, when it changes, I output a section marker. With XSLT refusing to support persistent data storage, I'm not sure how to do this. As daveVk pointed out [webmasterworld.com], the alternating rows was really just a "trick" that easily breaks.

I should mention that a reason for this is to maximize real estate usage for cellphones. I want to eliminate columns, if possible.

cmarshall

2:48 pm on Mar 30, 2007 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Hmm... Here's a conversation I saw that might help me to cast some light on this:

Stefano Pogliani wrote:
> I am looking for some very very simple examples of use
> of XSLT/XPath in which some variables are used (something like
> processing an XML document and producing another XML with the
> grandTotal of some numeric repeated field in the original XML
> document.

XML output is the default. Elements (within templates) that are not
associated with the XSL namespace (i.e., they don't have the xsl: prefix)
are going to be in your output.

If the field is numeric, use the sum() function -- variables aren't
necessary. The argument to the function is a node-set containing the numbers
to be totaled. Indicate the node-set by using an XPath expression that
identifies the appropriate nodes in the original XML. Here is an example
where this expression is very explicit: '/doc/num' matches element nodes
named 'num' that are children of elements named 'doc' that are children of
the root node.

Given this XML:

<?xml version="1.0"?>
<doc>
<num>1</num>
<num>7</num>
<notnum>hello world</notnum>
<num>4</num>
</doc>

Here is the simplest XSL that gives you the total of all 'num' element
children of 'doc':

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/XSL/Transform/1.0";>
<xsl:template match="/">
<xsl:element name="grandTotal" select="sum(/doc/num)"/>
</xsl:template>
</xsl:stylesheet>

...This is exactly the same as:

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/XSL/Transform/1.0";>
<xsl:template match="/">
<grandTotal><xsl:value-of select="sum(/doc/num)"/></grandTotal>
</xsl:template>
</xsl:stylesheet>

...And if you really need to use variables:

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/XSL/Transform/1.0";>
<xsl:template match="/">
<xsl:variable name="total" select="sum(/doc/num)"/>
<grandTotal><xsl:value-of select="$total"/></grandTotal>
</xsl:template>
</xsl:stylesheet>

I may be able to use an XPath function to check the value of this element to the previous one. After all, what I'm looking for is a change, not necessarily a state.

cmarshall

8:32 pm on Mar 30, 2007 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Well, I'll be looking at XPath this weekend. I haven't paid as much attention to it as I have the basic XSLT elements. We'll see what comes up.

The literature out there for XPath is even spottier than the rest of XML/XSLT, but I may have found a decent SAMS book to use. We'll see...

I'm going to see if it is practical to count up all the occurrences of a particular category, then display them all together. This would be more of a filter than anything else.

This would require a way of iterating through all he categories, then using each category as a filter for elements that are in that category. I'm wondering if it might be best to add the keys as attributes of the elements in question, or if leaving them as sibling/child elements would be sufficient.

cmarshall

2:28 pm on Mar 31, 2007 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Okay, got that solved. I'm pretty sure it's the proper way of doing things, too. I hope this works in PHP's implementation of XPath.

The XML file (I modified it to indicate the category of each item):

<?xml version="1.0" encoding="ISO-8859-1"?>
<weetest xmlns="http://www.example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com file:/Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xsd">
<elementwrapper id="1">
<data_cat>1</data_cat>
<data_name>Test(cat 1)</data_name>

</elementwrapper>
<elementwrapper id="2">
<data_cat>1</data_cat>
<data_name>Test(cat 1)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="3">
<data_cat>2</data_cat>
<data_name>Test(cat 2)</data_name>
</elementwrapper>
<elementwrapper id="4">
<data_cat>2</data_cat>
<data_name>Test(cat 2)</data_name>

</elementwrapper>
<elementwrapper id="5">
<data_cat>3</data_cat>
<data_name>Test(cat 3)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="6">
<data_cat>3</data_cat>
<data_name>Test(cat 3)</data_name>
</elementwrapper>
</weetest>

Now, here's the modified XSLT file. The entire structure has changed, but I'll highlight the important XPath expressions that I use to filter the result node sets:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wt="http://www.example.com" exclude-result-prefixes="wt">
<xsl:output indent="yes" method="html" version="4.01" doctype-system="http://www.w3c.org/tr/html4/strict.dtd" doctype-public="-//W3c//DTD HTML 4.01//EN"/>

<xsl:template match="/">
<html>
<head>
<title>XML Test</title>
</head>
<body>
<h1>Category 1</h1>
<xsl:for-each select="wt:weetest/wt:elementwrapper[wt:data_cat=1]">
<xsl:call-template name="render_one"/>
</xsl:for-each>
<h1>Category 2</h1>
<xsl:for-each select="wt:weetest/wt:elementwrapper[wt:data_cat=2]">
<xsl:call-template name="render_one"/>
</xsl:for-each>
<h1>Category 3</h1>
<xsl:for-each select="wt:weetest/wt:elementwrapper[wt:data_cat=3]">
<xsl:call-template name="render_one"/>
</xsl:for-each>
</body>
</html>
</xsl:template>

<xsl:template name="render_one">
<xsl:element name="div">
<xsl:attribute name="class">
<xsl:text>alt_</xsl:text>
<xsl:number value="position() mod 2" format="1"/>
</xsl:attribute>
<div class="display">
<xsl:choose>
<xsl:when test="wt:data_uri!= ''">
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of disable-output-escaping="yes" select="wt:data_uri"/>
</xsl:attribute>
<xsl:value-of select="wt:data_name"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wt:data_name"/>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:element>
</xsl:template>

</xsl:stylesheet>

Now, what happens here is that each loop filters its node set to just nodes that fit a particular criteria. In this case, nodes that have a

<data_cat>
element with a particular value. Let's look at the XPath statement:

wt:weetest/wt:elementwrapper[wt:data_cat=1]

Now, there's two parts to this statement: the part that says where to position the result, and the part that specifies the condition of the result.

This is the part that says where to position the results. It also says where the filtering begins:

wt:weetest/wt:elementwrapper

This is the actual filtering part:

[wt:data_cat=1]

The first part says "look at all the

wt:weetest/wt:elementwrapper
elements. If any of them fit the following criteria, add this node, at this position, to the result node set."

The second part says "starting from the current node, anything inside of it is fodder for this operation. Now, any direct child, with an element name of 'wt:data_cat', and having a value of '1' will pass the test, and allow the node to be added to the result."

Applying the above test to our XML file results in a node set that looks like this:

SystemID: /Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xml
Location: 3:2
Description: /weetest[1]/elementwrapper[1] - id="1"

SystemID: /Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xml
Location: 8:2
Description: /weetest[1]/elementwrapper[2] - id="2"

Now, if we had specified the XPath filter differently:

/wt:weetest/wt:elementwrapper[wt:data_uri!='']

We would have gotten a different result:

SystemID: /Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xml
Location: 3:2
Description: /weetest[1]/elementwrapper[1] - id="1"

SystemID: /Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xml
Location: 17:2
Description: /weetest[1]/elementwrapper[4] - id="4"

By the way, these are results returned by <oXygen>'s XPath Builder tool. I highly recommend <oXygen> if you are learning XML. I may get XMLSpy eventually, but <oXygen> is 64 dollars, and XMLSpy is a wee bit more expensive (plus, it's Windows-only).

Okay, the long and the short of it is that the stylesheet above will create the following output:

<!DOCTYPE html PUBLIC "-//W3c//DTD HTML 4.01//EN" "http://www.w3c.org/tr/html4/strict.dtd">
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>XML Test</title>
</head>
<body>
<h1>Category 1</h1>
<div class="alt_1">
<div class="display">
<a href="http://www.example.com/wee_test.html">Test (cat 1)</a>
</div>
</div>
<div class="alt_0">
<div class="display">Test (cat 1)</div>
</div>
<h1>Category 2</h1>
<div class="alt_1">
<div class="display">Test (cat 2)</div>
</div>
<div class="alt_0">
<div class="display">
<a href="http://www.example.com/wee_test.html">Test (cat 2)</a>
</div>
</div>
<h1>Category 3</h1>
<div class="alt_1">
<div class="display">Test (cat 3)</div>
</div>
<div class="alt_0">
<div class="display">Test (cat 3)</div>
</div>
</body>
</html>

The nice thing about this, is that the alternating rows work just fine. This answers daveVk's concern [webmasterworld.com] about what if we didn't want to deal with "holes" in the node set.

The whole deal with using XPath in a foreach loop is to create a node set that can then be iterated sequentially. It also explains why there are so few choices for loop counters.

I suspect a lot of people just use XSLT elements and ignore XPath. Judging from the murky docs and explanations I've seen out there, I can understand why.