<<Date(2012-09-27T15:55:48Z)>>
Using External XML Data in xsl Customizations
Written by: Marc Betz, Rogue Wave Software, Inc. Contact: Marc.Betz@roguewave.com
The Problem
We have user guides generated by WebWorks that need to link to class descriptions in reference guides generated from source code comments by doxygen, a Javadocs-like application. Doxygen generates a "tagfile", an XML file that describes all of the generated descriptions and contains all of the data needed to create accurate links.
In the user guides, links are of the form:
<classname>ClassName</classname> <methodname>ClassName::methodName</methodname>
where <classname> and <methodname> represent FrameMaker character styles.
The Customization
Customization is in the file .../Transforms/contents.xsl. First we make the data in the tagfile available to this process. We do that by reading the data into a variable:
<!-- Define the path to the tagfile --> <xsl:variable name="VarTagPath" select="wwfilesystem:Combine(wwprojext:GetProjectFormatDirectoryPath(), 'Transforms', 'combinedTagfiles.xml')" /> <!-- Use the path to load the document data into a variable --> <xsl:variable name="GlobalTagFile" select="wwexsldoc:LoadXMLWithoutResolver($VarTagPath)" />
Now the variable $GlobalTagFile contains our XML data, which we can access through XPath Expressions. Note that the $VarTagPath definition says the tagfile is located in the Transforms directory of the WebWorks project, and that the file name is 'combinedTagfiles.xml'.
Because we are dealing with data in character tags, the template we need is <xsl:template name="TextRun-Normal">. This template has generalized code for handling character tags, organized as:
<xsl:choose> <xsl:when test="..."> ... </xsl:when> <xsl:otherwise> ... </xsl:otherwise> </xsl:choose>
We co-opt this template to handle our classname and methodname character styles in a special way by putting our own <xsl:when ...> clause at the beginning of the sequence:
<xsl:choose> <!-- Beginning of class link customization --> <xsl:when test="($ParamTextRun/@stylename = 'classname') or ($ParamTextRun/@stylename = 'methodname')"> <xsl:variable name="VarClassOrMethodName"> <xsl:call-template name="TextRunChildren"> <xsl:with-param name="ParamSplits" select="$ParamSplits" /> <xsl:with-param name="ParamCargo" select="$ParamCargo" /> <xsl:with-param name="ParamLinks" select="$ParamLinks" /> <xsl:with-param name="ParamSplit" select="$ParamSplit" /> <xsl:with-param name="ParamParagraphID" select="$ParamParagraphID" /> <xsl:with-param name="ParamTextRun" select="$ParamTextRun" /> <xsl:with-param name="ParamEmitAnchorName" select="$ParamEmitAnchorName" /> </xsl:call-template> </xsl:variable>
The variable $ParamTextRun apparently contains an XML element with an attribute that specifies the name of the character style being processed. This is an educated guess, mind you, based on examining other code in this file; it is certainly not documented anywhere that I can find. It allows us, in the test clause of the <xsl:when ...> element, to request processing when the character tag name is either "classname" or "methodname". The variable $VarClassOrMethodName gets its value by calling a named template. I have no idea what the template does; I just borrowed it from the usual processing that follows the customization.
Note: In Reverb, the call to TextRunChildren takes two fewer parameters. You would need to remove the ParamParagraphID and ParamEmitAnchorName parameters from the above call, otherwise the generation will fail with errors.
Obtaining the Needed Data
The next set of code shows how we go about obtaining the data we need. There are some inline comments, and a fuller explanation follows.
<xsl:choose> <!-- If character style text contains '::', it is a qualified method name --> <xsl:when test="contains($VarGoToRefText,'::')"> <!-- Output log message for development/debugging purposes --> <xsl:variable name="VarFound" select="wwlog:Warning('FOUND: ', $VarGoToRefText, ', STYLE = ',$ParamTextRun/@stylename)" /> <!-- Capture the class name --> <xsl:variable name="VarGoToRefClassName"> <xsl:value-of select="substring-before($VarGoToRefText,'::')" /> </xsl:variable> <!-- Capture the method name --> <xsl:variable name="VarGoToRefMethodName"> <xsl:value-of select="substring-after($VarGoToRefText,'::')" /> </xsl:variable> ... <!-- Use an XPath expression to obtain the name of the class reference that this method appears in --> <xsl:variable name="VarClassReferenceName"> <xsl:value-of select="$GlobalTagFile/tagfiles/tagfile/compound[name/text()= $VarGoToRefClassName]/ancestor::tagfile/@refguide" /> </xsl:variable> <!-- Use an XPath expression to obtain the name of the doxygen-generated anchor string for the target method, allowing direct links to the method description --> <xsl:variable name="VarMethodAnchorName"> <xsl:value-of select="$GlobalTagFile/tagfiles/tagfile/compound[name/text()= $VarGoToRefClassName]/member[name/text()=$VarGoToRefMethodName]/anchor/text()" /> </xsl:variable> ...
In XSLT, you define a variable with the expression <xsl:variable name="whatever"> and populate it with the expression <xsl:value-of select="something"> (and a variety of other ways). For the first three variables above, the value-of expression uses the built-in XSLT functions contains(), substring-before(), and substring-after(). These all act on some previously defined variable and do what would expect: tests whether the variable contains a given string; returns the data in the variable that occurs before a given string; and returns the data in the variable the occurs after a given string.
The last two variables get there data from XPath expressions that extract data from the imported file. XPath is a powerful tool for navigating around an XML file to obtain particular data. A good starting point for learning more about it is here: http://www.w3schools.com/xpath/default.asp.
Here is what the data in the tagfile looks like:
<tagfiles> <tagfile refguide="refcppannotext"> <compound kind="class"> <name>IlvAnnoText</name> <filename>classIlvAnnoText.html</filename> <member kind="function"> <name>IlvAnnoText</name> <anchorfile>classIlvAnnoText.html</anchorfile> <anchor>a76f480e50275517e59d3f9859e73fef0</anchor> </member> ... </compound> </tagfile> <tagfiles>
The first XPath expression
$GlobalTagFile/tagfiles/tagfile/compound[name/text()=$VarGoToRefClassName]/ancestor::tagfile/@refguide
navigates the XML tree as follows:
Root of the imported data the <tagfiles> element any <tagfile> element a <compound> element that has a child <name> element whose text matches our class name an ancestor element of that <compound> element with the name 'tagfile' the value of the <tagfile> element's 'refguide' attribute
The reason we have to first go down to the <compound> element and then back up to its ancestor <tagfile> element is because the file contains multiple <tagfile> elements, corresponding to the several reference guides for this product. If our XPath just said "give me the value of the 'refguide' attribute for some <tagfile> element, we would always get the first one in the file, 'reccppannotext' in the example above. The XSLT text() function returns the data in the element specified just before its use.
The second XPath expression, which obtains the anchor tag string for the method description, is very similar, and a bit easier since it goes directly to the data we need without backtracking.
Producing Output Data
All the stuff in the <xsl:when ...> element that we have looked at so far does not produce any output. Now that we have the data we need to create a link to our method description, we want to output the character tag text wrapped in a link element. But first we check to make sure we really do have all the data we need:
<xsl:when test="($VarClassDescFile != '') and ($VarClassReferenceName != '') and ($VarMethodAnchorName != '')">
This is pretty straightforward: look at our three variables to be sure they actually got populated. If they did, produce output:
<xsl:element name="span" namespace="{$GlobalDefaultNamespace}"> <xsl:attribute name="class"> <xsl:value-of select="'MethodName'" /> </xsl:attribute> <xsl:element name="a" namespace="{$GlobalDefaultNamespace}"> <xsl:attribute name="href"> <xsl:value-of select="concat('../RefMan/',$VarClassReferenceName,'/', $VarClassDescFile,'#',$VarMethodAnchorName)" /> </xsl:attribute> <xsl:value-of select="$VarGoToRefText" /> </xsl:element> </xsl:element>
The XSLT expressions <xsl:element ...> and <xsl:attribute ...> create actual XML/HTML tags and send them to the output. We are actually creating two elements here. First we produce a wrapping 'span' element with a class attribute whose value is 'methodname'. This allows us to attach specific formatting to the text through a css file.
<span class='methodname'> ... </span>
The internal element is a standard HTML <a href ...> element with the link we have gone to such trouble to create. The XSLT concat() function allows you to string together literal values and variable data. The complete output data would look something like this:
<span class='methodname'><a href="../RefMan/SomeReferenceGuide/classSomeClass.html#a76f480e50275517e59d3f9859e73fef0">SomeClass</a></span>
There is an <xsl:otherwise> alternative to our <xsl:when> code above that deals with the case when for some reason our variables failed to all get populated. It outputs the data wrapped only in the <span> element, and a warning message. There is also an alternative to our original <xsl:when ...> test that handles the simpler case of a link to a class description based on just a class name.
Summary
One might wonder whether this is worth all the trouble. Before we created this customization, though, we relied on a mechanism that associated a FrameMaker marker with each use of these character tags. The marker contained the name of the reference guide and the class description file. In WebWorks, we used the Pass Through option to dump the content of the marker in with the character tag, and then used an even more complex customization to parse the ugly text we ended up with.
In addition, we had no way to link directly to method descriptions because there was no way we could know the value of the arcane ~33-character anchor string generated internally by doxygen.
So yes, it is worth it, to us at least.
But even more worth it is understanding the general mechanism. I can imagine a lot of uses for this in other customizations I might do. For example, I could define whatever data I might like in an external XML file and incorporate it directly into the WebWorks generation process, a more maintainable and cleaner alternative to post-processing of the WebWorks output, or hard-coding data into a WebWorks xsl file.