Stylesheet infrastructure

What are stylesheets for?

Using Converters, we can open any input format, even if they're not XML files. So instead of a XML document viewer, we get a XML-based document viewer. However, the resulting XML might not be what we really want to see. It would be nice if we could add custom icons and labels to the nodes and hide or delete some of those nodes. We can do that with preprocessing stylesheets.

Even after that, we'll only get a (prettier) tree. What if we want to see a single node in detail, with context-dependant information and links to other nodes of interest? That's where view stylesheets come into play. Currently, XHTML node views are supported, but other formats could be implemented in the future. JavaFX would be nice, for instance.

All these stylesheets are integrated into a TransformationPipeline, which takes the input file and produces tree and node views.

What are those stylesheets, then?

Basically, they're XSLT 1.0 stylesheets, slightly extended in some ways to allow for:
  • Runtime tab-specific view and preprocessing stylesheet switching by the user
  • Easier creation of links between documents and/or nodes
  • Special URIs which allow for both localization and inheritance at the same time
  • Easier installation: unpack some files in the right place and restart XMLEye

We'll get more into detail below. Depending on the context, "stylesheet" might mean a coherent group of XSLT 1.0 stylesheets (such as "the xml view stylesheet"), or an individual XSLT 1.0 stylesheet file.

What about performance? XMLEye uses Xalan as its XSLT engine, and stylesheets are lazily compiled into Java bytecode. The first node might take a second or two to show up, but after that it's blazing fast. Or so I think. :-D

Installation

Installing a new stylesheet is as easy as placing the XSLT files under a subdirectory of the right section of the stylesheet repository (see below for details). Its location varies depending on what installation method you used. Preprocessing stylesheets go under the preproc subdirectory, and view stylesheets go under the view subdirectory. For example, the xml view stylesheet installed through the XMLEye Debian package has all the required XSLT files under /usr/share/xmleye/xslt/view/xml.

As usual, you don't have to worry about these details if you just use the Debian packages in the XMLEye private repository.

Implementation details

You'll probably only be interested in this if you want to define new stylesheets.

Stylesheet repositories

All stylesheets used in XMLEye are installed in a so-called stylesheet repository. It is located under the xslt subdirectory of wherever the xmleye.jar was run from. Debian packages use /usr/share/xmleye, but it could be pretty much anywhere. See the InstallationInstructions for more details.

It will always contain at least these files:

  • preproc.xsl: all preprocessing transformations start from here. It defines some useful variables and imports two stylesheets: util.xsl and the stylesheet referred to by the special URI current_preproc. This is an XMLEye-specific extension: that URI is resolved into the entry point of the preprocessing stylesheet selected currently by the user. We'll talk more about entry points later.
  • view.xsl: this is the starting point for all view transformations. It uses current_view as we used current_preproc before, but it needs to take care of one more thing: switching the context node to that selected currently by the user. Its identifier is passed to the stylesheet through the selectedUID parameter, which contains the unique ID of this node. Where does this UID come from? It is generated in the preprocessing stage, actually. It's one of the few things which must always be done. So long as your preprocessing stylesheet inherits from the xml base stylesheet, you won't have anything to worry about. Oh, and looking for the node with a specific UID is done through an index, and certainly not using a full document scan.
  • util.xsl: this stylesheet includes some common useful functions. I try to use pure XPath functions whenever possible. There's util:to-lower, util:to-upper, and util:substring-after-last, with their expected meanings. Maybe after I switch to XSLT 2.0 these won't be necessary, but they don't hurt either, so feel free to use them.
  • preproc: this subdirectory contains one directory for each preprocessing stylesheet, which consists of one or more XSLT 1.0 stylesheet files.
  • view: same as the previous directory, but for view stylesheets.

Localization

One of XMLEye's main design concerns has always been internationalization. Everything can be easily localized: that includes all help files, the Swing UI and of course, the stylesheets.

There's a small problem, though: how do we localize a stylesheet and avoid code duplication at the same time? Well, it's actually pretty simple. We saw before that all preprocessing and view transformations use the preproc.xsl and view.xsl XSLT stylesheets as entry points. Actually, those import in turn our stylesheet's entry point through current_preproc and current_view. Finally, these stylesheet-specific entry points import all the required logic.

The trick here is that there can be more than one entry point. XMLEye follows the usual practice in most localization frameworks, such as gettext. Let L be the current locale's ISO 639 language code and P be the ISO 3166 country code. Should the H preprocessing stylesheet be selected, XMLEye would try to resolve the special current_preproc URI into these actual XSLT stylesheet file paths, in the same order:

  1. xslt/preproc/H/H_L_P.xsl
  2. xslt/preproc/H/H_L.xsl
  3. xslt/preproc/H/H.xsl

Of course, the stylesheet author must take this into account. Normally, the stylesheet would be split into 2 parts:

  1. Entry points would be limited to declaring variables and named templates which will produce the localized text, importing all parent stylesheets and including its own XSLT stylesheet files which implement the actual functionality.
  2. Included XSLT stylesheet files wouldn't contain any text strings of their own. They'll always refer to the previously defined variables in the entry point.

Oh, and one last thing: when including specific XSLT files, relative paths from the stylesheet repository root directory should always be used. xml.xsl includes main.xsl like this:

<xsl:include href="view/xml/main.xsl"/>

Inheritance

XSLT already includes support for inheritance: the xsl:import element brings all the templates from another XSLT file using a lower precedence, so we can replace and refine some of its rules with our own. However, we can only import files in specific locations in this way. This means that we wouldn't be able to use the locale-aware entry point resolution we described above.

We'll need to use special URIs again: preproc_X is resolved to the X preprocessing stylesheet's entry point. view_X does the same thing, but for view stylesheets.

For instance, the ppACL2 preprocessing stylesheet inherits from the xml stylesheet like this:

<xsl:import href="preproc_xml"/>

We'd get an import/include tree like this:

No restrictions are imposed on the number of inheritance levels. Take as an example the summaries and reverse preprocessing stylesheets, which inherit from the ppACL2 stylesheet.

Preprocessing specifics

As said above, every preprocessing stylesheet must add UIDs to every node, so the user can select them and obtain node views. The xml stylesheet takes care of this, as long as we follow some rules in our own stylesheet.

The xml stylesheet immediately changes to the process-node mode, which contains a default rule that follows these steps:

  1. The current element is copied, along with its attributes.
  2. A new UID attribute is added with an identifier guaranteed to be unique to that node.
  3. Applies the XSLT templates available in the default mode. Nothing is done by default, but we can add whatever we need.
  4. Copies all text children.
  5. Applies the XSLT templates available in the process-children mode. All children are processed by default, but we can redefine this so some nodes can be entirely removed from the tree.

This is just an implementation of the Template Method pattern. In short, what this means is that:

  1. If you want to add new attributes or content, define new templates in the default mode.
  2. If you want to filter out some nodes, define new templates in the process-children mode.

But that's not all. There's a few special attributes in the default namespace (though they will be moved to a namespace of its own in a later release) that XMLEye understands:

  • hidden: when set to "1", the node and its children will be hidden in the tree view. It won't be deleted, though, so you will be able to access its information to generate views for other nodes. This is used in the yaxml stylesheet to hide all _key nodes, like this:
<!-- Hide all yaml:_key elements -->
<xsl:template match="*[local-name(.)='_key']">
  <xsl:attribute name="hidden">1</xsl:attribute>
</xsl:template>
  • leaf: when set to "1", it will be treated as a leaf node, and all of its children will be hidden.
  • nodeicon: contains the relative path from the stylesheet repository root directory to the 16x16 pixel icon to be used for the node. For instance, the ppACL2 stylesheet uses this to indicate whether a specific element's proof was successful or not.
  • nodelabel: contains the label to be used for the node. This helps alleviate the usual restrinctions on XML node names, which are a problem when viewing YAML/JSON documents, for instance. See this code:
<!-- Label map values using their keys -->
<xsl:template match="*[local-name(.)='_value']">
  <xsl:attribute name="nodelabel">
    <xsl:value-of select="preceding-sibling::*[local-name(.)='_key'][1]"/>
  </xsl:attribute>
</xsl:template>

View specifics

Most of the time, we'll need all views to follow a basic structure. This is most easily done through a named template, which will receive the result tree fragment with the XHTML code to be included in the body of the document.

We can use the skeleton named template defined in the xml view stylesheet. It produces links to all direct children and ancestors of the current node. This is all optional, of course: we could use our own skeleton if we wanted, or none at all.

What's more important is how to create hyperlinks between different nodes. We can use standard XHTML links to external sites and anchors, but we'll need to do some extra work to create links between nodes and between documents.

These special links follow the syntax #xpointer(path), where path can be any path which uniquely identifies the node. We can use the getPath XPath extension function from the xalan://es.uca.xmleye.xpath.XPathPathManager namespace to generate it, like this:

<xsl:template match="encapsulate|local" mode="ppacl2_printnode">
  <h2 class="section">
    <a href="#xpointer({ext:getPath(.,/)})">
      <xsl:value-of select="name(.)"/>
    </a>
  </h2>
  <pre>
    <xsl:value-of select="@formula"/>
  </pre>
</xsl:template>

As we can see, the function takes two arguments: the node to which we want to link, and the root node. In this case, we have linked to the current context node.

Links to nodes in other documents can be defined as path_to_the_file#xpointer(path). There's no XPath function to help us in this case: we'll have to build a XPath path on our own, using what we know about the XML format used.

stylesheet_hierarchy.png (14.5 KB) Antonio García Domínguez, 05/02/2012 10:28 PM