XQuery/Generating Skeleton Typeswitch Transformation Modules

Motivation

edit

For document types which contain multiple tags, such as TEI or DocBook, it is tedious and error-prone to write this conversion code by hand. We can use XQuery to generate the basic text for an XQuery module.

This article uses the same example as used in Transformation idioms.

Example

edit

Starting with a simple list of the tags in this document, we can generate a module which performs an identity transform on a document containing these tags.

import module namespace gen =  "http://www.cems.uwe.ac.uk/xmlwiki/gen" at "gen.xqm";

let $tags := ("websites","sites","site","uri","name","description")

let $config :=  
 <config>
   <modulename>coupland</modulename>
   <namespace>http://www.cems.uwe.ac.uk/xmlwiki/coupland</namespace>
 </config>
return 
  gen:create-module($tags, $config)

Here is the XML output and the text XQuery file created by adding the line

 declare option exist:serialize "method=text media-type=text/text";

to the script.


If we save this script as, say coupid.xqm, we can use it to generate the transformed document:

import module namespace coupland = "http://www.cems.uwe.ac.uk/xmlwiki/coupland" at "coupid.xqm";

let $doc := doc("/db/Wiki/eXist/transformation/Coupland1.xml")/*
return 
   coupland:convert($doc)

Generate

We can also check if the identity transformation has retained the full structure of the document:

import module namespace coupland = "http://www.cems.uwe.ac.uk/xmlwiki/coupland" at "coupid.xqm";

let $doc := doc("/db/Wiki/eXist/transformation/Coupland1.xml")/*
return 
   <compare>{deep-equal($doc,coupland:convert($doc))}</compare>

Compare

Module design

edit

The generated module looks like this:

module namespace coupland = "http://www.cems.uwe.ac.uk/xmlwiki/coupland";
(: conversion module generated from a set of tags 

:)

declare function coupland:convert($nodes as node()*) as item()* {
  for $node in $nodes
  return 
     typeswitch ($node)
       case element(websites) return coupland:websites($node)
           case element(sites) return coupland:sites($node)
           case element(site) return coupland:site($node)
           case element(uri) return coupland:uri($node)
           case element(name) return coupland:name($node)
           case element(description) return coupland:description($node)
           
       default return 
         coupland:convert-default($node)
  };

declare function coupland:convert-default($node as node()) as item()* {
  $node
  };

declare function coupland:websites($node as element(websites)) as item()* {
  element websites{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
   
declare function coupland:sites($node as element(sites)) as item()* {
  element sites{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
   
declare function coupland:site($node as element(site)) as item()* {
  element site{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
   
declare function coupland:uri($node as element(uri)) as item()* {
  element uri{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
   
declare function coupland:name($node as element(name)) as item()* {
  element name{
     $node/@*,
     coupland:convert($node/node()) 
     }
};
   
declare function coupland:description($node as element(description)) as item()* {
  element description{
     $node/@*,
     coupland:convert($node/node()) 
     }
};

The function convert($nodes) contains the typeswitch statement to dispatch the node to one of the tag functions. Each tag function creates an element of that name, copies the attributes and then recursively calls the convert function passing the child nodes. The default action defined in the function convert-default merely copies the node.

Generation Function

edit

This function generates the code for an XQuery module which performs an identity transformation.

There are two parameters

  • tags - a sequence of tags
  • config - an XML node containing definitions of the module name, module prefix and module namespace.


declare variable $gen:cr := "&#13;";

declare function gen:create-module($tags as xs:string*, $config as element(config) ) as element(module) {
let $modulename := $config/modulename/text()
let $prefix := $config/prefix/text()
let $pre:= concat($modulename,":",$prefix)
let $namespace := ($config/namespace,"http://mysite/module")[1]/text()
return
<module>
module namespace {$modulename} = "{$namespace}";
(: conversion module generated from a set of tags 

:)
<function>
declare function {$pre}convert($nodes as node()*) as item()* {{ {$gen:cr}
  for $node in $nodes
  return 
     typeswitch ($node)
       {for $tag in $tags
        return 
           <s>case element({$tag}) return {$pre}{replace($tag,":","-")}($node)
           </s>
       }
       default return 
         {$pre}convert-default($node)
  }};
</function>

<function>
declare function {$pre}convert-default($node as node()) as item()* {{ {$gen:cr}
  $node
  }};
</function>

{for $tag in $tags
 return 
   <function>
declare function {$pre}{replace($tag,":","-")}($node as element({$tag})) as item()* {{ {$gen:cr}
  element {$tag} {{
     $node/@*,
     {$pre}convert($node/node()) 
     }}{$gen:cr}
}};
   </function>
}

</module>
};

Generating the tags

edit

All tags in the document or corpus need to be handled by the identity transformation, so it would be better to generate the list of tags from the document or corpus itself. The following function returns a sequence of tags in alphabetically order.

declare function gen:tags($docs as node()*) as xs:string * {
   for $tag in distinct-values ($docs//*/name(.))
   order by $tag
   return $tag
};

and we can modify the calling script:

let $doc := doc("/db/Wiki/eXist/transformation/Coupland1.xml")
let $tags := gen:tags($doc)

let $config :=  
 <config>
   <modulename>coupland</modulename>
   <namespace>http://www.cems.uwe.ac.uk/xmlwiki/coupland</namespace>
 </config>
return 
  gen:create-module($tags, $config)

Generate


User-defined function template

edit

The module generator function generates a fixed code pattern for each tag. We can allow the user to customize this pattern by using a callback function to generate the code pattern as an alternative to modifying the generator code itself.

The modified function code has the following modifications:

Function signature ;

declare function gen:create-module($tags as xs:string*, $callback as function, $config as element(config) ) as element(module) {

generating each tag function:

 <function>
declare function {$pre}{replace($tag,":","-")}($node as element({$tag})) as item()* {{ {$gen:cr}
  {util:call($callback,$tag,$pre)}{$gen:cr}
}};
 </function>

To generate a basic transformation to HTML, with HTML elements being copied while non-HTML elements are converted to div elements with an additional class attribute, we define the function to create the code body, create the function reference and call the convert function:

import module namespace gen =  "http://www.cems.uwe.ac.uk/xmlwiki/gen" at "gen.xqm";
declare namespace fx = "http://www.cems.uwe.ac.uk/xmlwiki/fx";

declare variable $fx:html-tags := 
  ("p","a","em","q");

declare function fx:tag-code ($tag as xs:string, $pre as xs:string) {

if ($tag = $x:html-tags)
   then 
<code>
  element {$tag} {{
     $node/@*,
     {$pre}convert($node/node()) 
  }}
</code>
   else
<code>   
  element div {{
     attribute class {{"{$tag}" }},
     $node/(@* except class),
     {$pre}convert($node/node()) 
  }}
</code>
};

declare option exist:serialize "method=text media-type=text/text";

let $doc := doc("/db/Wiki/eXist/transformation/Coupland1.xml")
let $tags := gen:tags($doc)
let $callback := util:function(QName("http://www.cems.uwe.ac.uk/xmlwiki/x","fx:tag-code"),2)

let $config :=  
 <config>
   <modulename>coupland</modulename>
   <namespace>http://www.cems.uwe.ac.uk/xmlwiki/coupland</namespace>
 </config>
return 
   gen:create-module($tags, $callback,  $config)

Generate


Customising the generator

edit

Another customization of the generator which may be required is to add an additional $options parameter to all signatures and calls. This provides a mechanism for passing configuration parameter around the functions to control the transformation.

Transforming with XSLT

edit

When the conversion is complex, requiring restructuring, context-dependent transformations and reordering, it is not clear that the XQuery typeswitch approach is better or worse than the XSLT equivalent. For comparison here is the equivalent XSLT.


<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    <xsl:template match="/websites">
         <html>
            <head>
                <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
                <title>Web Sites by Coupland</title>
                <link rel="stylesheet" href="../../css/blueprint/screen.css" type="text/css" media="screen, projection"/>
                <link rel="stylesheet" href="../../css/blueprint/print.css" type="text/css" media="print"/>
                <!--[if IE ]><link rel="stylesheet" href="../../css/blueprint/ie.css" type="text/css" media="screen, projection" /><![endif]-->
                <link rel="stylesheet" href="screen.css" type="text/css" media="screen"/>
            </head>
            <body>
                <div class="container">
                  <h1>Design web sites by Ken Coupland</h1>
                
                   <xsl:apply-templates select="category">
                        <xsl:sort select="name"/>
                   </xsl:apply-templates>              
                </div>
            </body>
        </html>
    </xsl:template>
    
    <xsl:template match="websites/category">
        <div>
            <div class="span-10">
                <h3>
                    <xsl:value-of select="name"/>
                </h3>
                <h4>
                    <xsl:value-of select="subtitle"/>
                </h4>
                <xsl:copy-of select="description/node()"/>
            </div>
            <div class="span-14 last">
               <xsl:apply-templates select="../sites/site">
                  <xsl:sort select="(sortkey,name)[1]" order="ascending"/>
               </xsl:apply-templates>
            </div>
            <hr />
        </div>
    </xsl:template>
    <xsl:template match="site/category">
        
    </xsl:template>
    <xsl:template match="site">
        <h3>
                <xsl:value-of select="name"/>
        </h3>
        <span><a href="{uri}">Link</a></span>
        
        <div class="site">
            <xsl:apply-templates select="* except (uri,name,sortkey)"/>
        </div>
    </xsl:template>
    
    <xsl:template match="description">
        <p>
            <xsl:copy-of select="node()"/>
        </p>
    </xsl:template>
    
    <xsl:template match="image">
            <img  src="{uri}"/>
    </xsl:template>
    
</xsl:stylesheet>

and XQuery to apply this server-side:

declare option exist:serialize "method=xhtml media-type=text/html";

let $doc := doc("/db/Wiki/eXist/transformation/Coupland1.xml")
let $ss := doc("/db/Wiki/eXist/transformation/tohtml.xsl")
return 
   transform:transform($doc, $ss,())

Transform to HTML via XSLT