XDV Reference

XDV Reference

XDV Reference

Introduction

(For more information about how Enfold Proxy can use XDV themes, see the XDV tutorial and Using Enfold Proxy with XDV Themes).

XDV is an implementation of the Deliverance concept using pure XSLT. In short, it is a way to apply a style/theme to a dynamic website.

Consider a scenario where you have some dynamic website, to which you want to apply a theme built by a web designer. The web designer is not familiar with the technology behind the dynamic website, and so has supplied a "static HTML" version of the site. This consists of an HTML file with more-or-less semantic markup, one or more style sheets, and perhaps some other resources like images or JavaScript files.

Using XDV, you could apply this theme to your dynamic website as follows:

  1. Identify the placeholders in the theme file that need to be replaced with dynamic elements. Ideally, these should be clearly identifiable, for example with a unique HTML id attribute.
  2. Identify the corresponding markup in the dynamic website. Then write a "replace" or "copy" rule using XDV's rules syntax that replaces the theme's static placeholder with the dynamic content.
  3. Identify markup in the dynamic website that should be copied wholesale into the theme. CSS and JavaScript links in the <head /> are often treated this way. Write an XDV "append" or "prepend" rule to copy these elements over.
  4. Identify parts of the theme and/or dynamic website that are superfluous. Write an XDV "drop" rule to remove these elements.

The rules file is written using a simple XML syntax. Elements in the theme and "content" (the dynamic website) can be identified using CSS3 or XPath selectors.

Once you have a theme HTML file and a rules XML file, you compile these using the XDV compiler into a single XSLT file. You can then deploy this XSLT file with your application. An XSLT processor (such as mod_transform in Apache) will then transform the dynamic content from your website into the themed content your end users see. The transformation takes place on-the-fly for each request.

Bear in mind that:

  • You never have to write, or even read, a line of XSLT (unless you want to).
  • The XSLT transformation that takes place for each request is very fast.
  • Static theme resources (like images, stylesheets or JavaScript files) can be served from a static webserver, which is normally much faster than serving them from a dynamic application.
  • You can leave the original theme HTML untouched, with makes it easier to re-use for other scenarios. For example, you can stitch two unrelated applications together by using a single theme file with separate rules files. This would result in two compiled XSLT files. You could use location match rules or similar techniques to choose which one to invoke for a given request.

Rules file syntax

The rules file, conventionally called rules.xml, is rooted in a tag called <rules />:

<?xml version="1.0" encoding="UTF-8"?>
<rules xmlns="http://namespaces.plone.org/xdv"
       xmlns:css="http://namespaces.plone.org/xdv+css">

       ...

</rules>

Here we have defined two namespaces: the default namespace is used for rules and XPath selectors. The css namespace is used for CSS3 selectors. These are functionally equivalent. In fact, CSS selectors are replaced by the equivalent XPath selector during the pre-processing step of the compiler. Thus, they have no performance impact.

XDV supports complex CSS3 and XPath selectors, including things like the nth-child pseudo-selector. You are advised to consult a good reference if you are new to XPath and/or CSS3.

The following elements are allowed inside the <rules /> element:

<replace />

Used to replace an element in the theme entirely with an element in the content. For example:

<replace theme="/html/head/title" content="/html/head/title"/>

The (near-)equivalent using CSS selectors would be:

<replace css:theme="title" css:content="title"/>

The result of either is that the <title /> element in the theme is replaced with the <title /> element in the (dynamic) content.

<copy />

Used to replace the contents of a placeholder tag with a tag from the theme. For example:

<copy css:theme="#main" css:content="#portal-content > *" />

This would replace any placeholder content inside the element with id main in the theme with all children of the element with id portal-content in the content. The usual reason for using <copy /> instead of <replace />, is that the theme has CSS styles or other behaviour attached to the target element (with id main in this case).

<append /> and <prepend />

Used to copy elements from the content into an element in the theme, leaving existing content in place. <append /> places the matched content directly before the closing tag in the theme; append places it directly after the opening tag. For example:

<append theme="/html/head" content="/html/head/link" />

This will copy all <link /> elements in the head of the content into the theme.

As a special case, you can copy individual attributes from a content element to an element in the theme using <prepend />:

<prepend theme="/html/body" content="/html/body/@class" />

This would copy the class attribute of the <body /> element in the content into the theme (replacing an existing attribute with the same name if there is one).

<before /> and <after />

These are equivalent to <append /> and <prepend />, but place the matched content before or after the matched theme element, rather than immediately inside it. For example:

<before css:theme="#content" css:content="#info-box" />

This would place the element with id info-box from the content immediately before the element with id content in the theme. If we wanted the box below the content instead, we could do:

<after css:theme="#content" css:content="#info-box" />

<drop />

Used to drop elements from the theme or the content. This is the only element that accepts either theme or content attributes (or their css: equivalents), but not both:

<drop css:content="#portal-content .about-box" />
<copy css:theme="#content" css:content="#portal-content > *" />

This would copy all children of the element with id portal-content in the theme into the element with id content in the theme, but only after removing any element with class about-box inside the content element first. Similarly:

<drop theme="/html/head/base" />

Would drop the <base /> tag from the head of the theme.

Order of rule execution

In most cases, you should not care too much about the inner workings of the XDV compiler. However, it can sometimes be useful to understand the order in which rules are applied.

  1. <before /> rules are always executed first.
  2. <drop /> rules are executed next.
  3. <replace /> rules are executed next, provided no <drop /> rule was applied to the same theme node.
  4. <prepend />, <copy /> and <append /> rules execute next, provided no <replace /> rule was applied to the same theme node.
  5. <after /> rules are executed last.

Behaviour if theme or content is not matched

If a rule does not match the theme (whether or not it matches the content), it is silently ignored.

If a <replace /> rule matches the theme, but not the content, the matched element will be dropped in the theme:

<replace css:theme="#header" content="#header-element" />

Here, if the element with id header-element is not found in the content, the placeholder with id header in the theme is removed.

Similarly, the contents of a theme node matched with a <copy /> rule will be dropped if there is no matching content. Another way to think of this is that if no content node is matched, XDV uses an empty nodeset when copying or replacing.

If you want the placeholder to stay put in the case of a missing content node, you can make this a conditional rule:

<replace css:theme="#header" content="#header-element" if-content="" />

See below for more details on conditional rules.

Advanced usage

The simple rules above should suffice for most use cases. However, there are a few more advanced tools at your disposal, should you need them.

Conditional rules

Sometimes, it is useful to apply a rule only if a given element appears or does not appear in the markup. The if-content attribute can be used with any rule to make it conditional.

if-content should be set an XPath expression. You can also use css:if-content with a CSS3 expression. If the expression matches a node in the content, the rule will be applied:

<copy css:theme="#portlets" css:content=".portlet"/>
<drop css:theme="#portlet-wrapper" if-content="not(//*[@class='portlet'])"/>

This will copy all elements with class portlet into the portlets element. If there are no matching elements in the content we drop the portlet-wrapper element, which is presumably superfluous.

Here is another example using CSS selectors:

<copy css:theme="#header" css:content="#header-box > *" css:if-content="#personal-bar"/>

This will copy the children of the element with id header-box in the content into the element with id header in the theme, so long as an element with id personal-bar also appears somewhere in the content.

Above, we also saw the special case of an empty if-content (which also works with an empty css:if-content). This is a shortcut that means "use the expression in the content or css:content` attribute as the condition". Hence the following two rules are equivalent:

<copy css:theme="#header" css:content="#header-box > *" css:if-content="#header-box > *"/>
<copy css:theme="#header" css:content="#header-box > *" css:if-content=""/>

If multiple rules of the same type match the same theme node but have different if-content expressions, they will be combined as an if..else if...else block:

<copy theme="/html/body/h1" content="/html/body/h1/text()" if-content="/html/body/h1"/>
<copy theme="/html/body/h1" content="//h1[@id='first-heading']/text()" if-content="//h1[@id='first-heading']"/>
<copy theme="/html/body/h1" content="/html/head/title/text()" />

These rules all attempt to fill the text in the <h1 /> inside the body. The first rule looks for a similar <h1 /> tag and uses its text. If that doesn't match, the second rule looks for any <h1 /> with id first-heading, and uses its text. If that doesn't match either, the final rule will be used as a fallback (since it has no if-content), taking the contents of the <title /> tag in the head of the content document.

Including external content

Normally, the content attribute of any rule selects nodes from the response being returned by the underlying dynamic web server. However, it is possible to include content from a different URL using the href attribute on any rule (other than <drop />). For example:

<append css:theme="#left-column" css:content="#portlet" href="/extra.html"/>

This will resolve the URL /extra.html, look for an element with id portlet and then append to to the element with id left-column in the theme.

The inclusion can happen in one of three ways:

  • Using the XSLT document() function. This is the default, but it can be explicitly specified by adding an attribute method="document" to the rule element. Whether this is able to resolve the URL depends on how and where the compiled XSLT is being executed:

    <append css:theme="#left-column" css:content="#portlet" href="/extra.html" method="document" />
    
  • Via a Server Side Include directive. This can be specified by setting the method attribute to ssi:

    <append css:theme="#left-column" css:content="#portlet" href="/extra.html" method="ssi"/>
    

    The output will look something like this:

    <!--# include  virtual="/extra.html?;filter_xpath=//*[@id%20=%20'portlet']" wait="yes" -->
    

    This SSI instruction would need to be processed by a fronting web server such as Apache or nginx. Also note the ;filter_xpath query string parameter. Since we are deferring resolution of the referenced document until SSI processing takes place (i.e. after the compiled XDV XSLT transform has executed), we need to ask the SSI processor to filter out elements in the included file that we are not interested in. This requires specific configuration. An example for nginx is included below.

  • Via an Edge Side Includes directive. This can be specified by setting the method attribute to esi:

    <append css:theme="#left-column" css:content="#portlet" href="/extra.html" method="esi"/>
    

    The output is similar to that for the SSI mode:

    <esi:include src="/extra.html?;filter_xpath=//*[@id%20=%20'portlet']"></esi:include>
    

    Again, the directive would need to be processed by a fronting server, such as Varnish. Chances are an ESI-aware cache server would not support arbitrary XPath filtering. If the referenced file is served by a dynamic web server, it may be able to inspect the ;filter_xpath parameter and return a tailored response. Otherwise, if a server that can be made aware of this is placed in-between the cache server and the underlying web server, that server can perform the necessary filtering.

Modifying the theme on the fly

Sometimes, the theme is almost perfect, but cannot be modified, for example because it is being served from a remote location that you do not have access to, or because it is shared with other applications.

XDV allows you to modify the theme using "inline" markup in the rules file. You can think of this as a rule where the matched content is explicitly stated in the rules file, rather than pulled from the response being styled.

For example:

<xdv:rules
    xmlns:xdv="http://namespaces.plone.org/xdv"
    xmlns:css="http://namespaces.plone.org/xdv+css"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    >

    <xdv:append theme="/html/head">
        <style type="text/css">
            /* From the rules */
            body > h1 { color: red; }
        </style>
    </xdv:append>

</xdv:rules>

Notice how we have placed the rules in an explicit xdv namespace, so that we can write the "inline" HTML without a namespace prefix.

In the example above, the <append /> rule will copy the <style /> attribute and its contents into the <head /> of the theme. Similar rules can be constructed for <copy />, <replace />, <prepend />, <before /> or <after />.

It is even possible to insert XSLT instructions into the compiled theme in this manner. Having declared the xsl namespace as shown above, we can do something like this:

<xdv:replace css:theme="#details">
    <dl id="details">
        <xsl:for-each css:select="table#details > tr">
            <dt><xsl:copy-of select="td[1]/text()"/></dt>
            <dd><xsl:copy-of select="td[2]/node()"/></dd>
        </xsl:for-each>
    </dl>
</xdv:replace>

Note that css expressions are converted to "placeless" XPath expressions, so css:select="table#details > tr" converts to select="//table[@id='details]/tr". This means it would not be possible to use css:select="td:first-child > *" as you want a relative selector here. You can, of course, just use a manual XPath in a select attribute instead.

XInclude

You may wish to re-use elements of your rules file across multiple themes. This is particularly useful if you have multiple variations on the same theme used to style different pages on a particular website.

Inclusions use standard XInclude syntax. For example:

<rules
   xmlns="http://namespaces.plone.org/xdv"
   xmlns:css="http://namespaces.plone.org/xdv+css"
   xmlns:xi="http://www.w3.org/2001/XInclude">

   <xi:include href="standard-rules.xml#xpointer(/*/node())" />

</rules>

An xpointer is used so as not to pull in the outer <rules> element. In Windows OS you should use shashes instead of back slashes for file path. For example:

<xi:include href="C:\theme\standard-rules.xml#xpointer(/*/node())" />

Should be:

<xi:include href="C:/theme/standard-rules.xml#xpointer(/*/node())" />

You can use xincludes only in rules.xml file.