XQuery/Generating Skeleton Typeswitch Transformation Modules
Motivation
editFor 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
editStarting 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)
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>
Module design
editThe 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
editThis 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 := " ";
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
editAll 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)
User-defined function template
editThe 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)
Customising the generator
editAnother 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
editWhen 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,())