XQuery/Transformation idioms
Motivation
editDocument transformation using the basic typeswitch statement applies the same transformation to an element independent of where it occurs in the document. The transformation also preserves document order since it processes elements in document order. In comparison with XSLT, XQuery lacks some mechanisms such as modes, priority and numbering. This article addresses some of these limitations.
Example
editThe example uses a custom XML schema to markup the contents of the book "Search: The Graphics Web Guide", Ken Coupland, a compendium of websites. This document is formatted with a site-specific schema. The document contains site elements which are tagged with a category, and also category elements which provide a commentary on the category. For comparison this dataset is used in a student case study which uses XSLT for transformations.
Identity Transformation
editmodule 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())
}
};
Customising the identity transformation
editThe module code is only a basic skeleton which we would edit to customize the transformation. In this example we will transform the document to HTML. This will require editing a number of the element converters.
Default action
editChange the convert-default
function to provide a different default action. For example:
declare function coupland:convert-default ($node) {
if ($node instance of element())
then coupland:convert($node/node())
else $node
};
would include the content of the node but remove the tag and its attributes.
Change element name
editSite descriptions will be rendered as divs:
declare function coupland:description($node as element(description)) as item()* {
element div{
$node/@*,
coupland:convert($node/node())
}
};
Ignore element
editThe 'class' element is not needed:
declare function coupland:class($node as element(class)) as item()* {
()
};
Define transformation
editThe image element should be transformed to an html img
element using the uri as the source:
declare function coupland:image($node as element(image)) as item()* {
element div {
element img {
attribute src { $node}
}
}
};
Transformation depends on context
editBy default all elements with the same name anywhere in the document are transformed in the same way. Often this is not what is required:
declare function coupland:name($node as element(name)) as item()* {
if ($node/parent::node() instance of element(site))
then
element h3{
$node/@*,
coupland:convert($node/node())
}
else
element h1{
$node/@*,
coupland:convert($node/node())
}
};
Reordering elements
editEach site is to be rendered in the order name, uri and then the rest of the sub-elements:
declare function coupland:site($node as element(site)) as item()* {
element div{
element div {
coupland:convert($node/name),
coupland:convert($node/uri)
} ,
coupland:convert($node/(node() except (uri,name)))
}
};
Numbering categories
editThe xsl:number instruction provides a mechanism to generate hierarchical section numbers. This instruction is very powerful. In specific cases we can generate numbers using functions.
For example to number the categories we can use this function to create a number for a node in a sequence of siblings. Note that the number is based on the order of nodes in the original document, not the transformed document (as does xsl:number) .
declare function coupland:number($node) as xs:string {
concat(count($node/preceding-sibling::node()[name(.) = name($node)]) + 1,". ")
};
and call this function when transforming category names:
element h2{
$node/@*,
coupland:number($node/parent::node()),
coupland:convert($node/node())
Parameterisation
editThe transformation can clearly be applied to different documents, but often the same transformation is to be used in different contexts. XSLT provides parameters and variables which are global to all templates.
In XQuery we can either declare global variables in the module or pass one or more parameters around the functions ( module generation is helpful here).
....
Generating an index
editXSLT uses the mode mechanism to allow the same template to be processed in multiple ways. A common use case is where the same transformation must generate both an index and the content.
Several approaches suggest themselves. We could mimic the XSLT approach by passing an additional mode parameter in the calls and choose which transformation to apply in each function. Alternatively we append the mode to the function name. It is more difficult to use context (either global or passed) because the mode will need to be updated.
The simplest approach is to use use two typeswitch transformation and combine the results at a higher level. This clearly separates the two modes of transformation. The technique of module generation is helpful here.
Complex transformation
editThe overall HTML document can be structured in the transformer for the root element. The page uses the blueprint stylesheets. Each category of site is rendered, with the sites which are classified in that category.
declare function coupland:websites($node as element(websites)) as item()* {
(: the root element so convert to html :)
<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">
{
for $category in $node/category
order by $category/class
return
<div>
<div class="span-10">
{coupland:convert($category)}
</div>
<div class="span-14 last">
{for $site in $node/sites/site[category=$category/class]
order by ($site/sortkey,$site/name)[1]
return
coupland:convert($site)
}
</div>
<hr />
</div>
}
</div>
</body>
</html>
};
Completed transformation
editThe full XQuery module now 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 node()?) as item()* {
for $node in $nodes
return
typeswitch ($node)
case element(category) return coupland:category($node)
case element(class) return coupland:class($node)
case element(description) return coupland:description($node)
case element(em) return coupland:em($node)
case element(hub) return coupland:hub($node)
case element(image) return coupland:image($node)
case element(name) return coupland:name($node)
case element(p) return coupland:p($node)
case element(q) return coupland:q($node)
case element(site) return coupland:site($node)
case element(sites) return coupland:sites($node)
case element(sortkey) return coupland:sortkey($node)
case element(subtitle) return coupland:subtitle($node)
case element(uri) return coupland:uri($node)
case element(websites) return coupland:websites($node)
default return
coupland:convert-default($node)
};
declare function coupland:convert-default($node as node() as node()?) as item()* {
$node
};
declare function coupland:category($node as element(category) as node()?) as item()* {
if ($node/parent::node() instance of element(site))
then ()
else
element div{
$node/@*,
coupland:convert($node/node())
}
};
declare function coupland:class($node as element(class) as node()?) as item()* {
()
};
declare function coupland:description($node as element(description) as node()?) as item()* {
element div{
$node/@*,
coupland:convert($node/node())
}
};
declare function coupland:em($node as element(em) as node()?) as item()* {
element em{
$node/@*,
coupland:convert($node/node())
}
};
declare function coupland:hub($node as element(hub) as node()?) as item()* {
element hub{
$node/@*,
coupland:convert($node/node())
}
};
declare function coupland:image($node as element(image) as node()?) as item()* {
element div {
element img {
attribute src { $node}
}
}
};
declare function coupland:name($node as element(name) as node()?) as item()* {
if ($node/parent::node() instance of element(site))
then
element span {
attribute style {"font-size: 16pt"},
$node/@*,
coupland:convert($node/node())
}
else
element h1{
$node/@*,
coupland:number($node/parent::node()),
coupland:convert($node/node())
}
};
declare function coupland:p($node as element(p) as node()?) as item()* {
element p{
$node/@*,
coupland:convert($node/node())
}
};
declare function coupland:q($node as element(q) as node()?) as item()* {
element q{
$node/@*,
coupland:convert($node/node())
}
};
declare function coupland:site($node as element(site) as node()?) as item()* {
element div{
element div {
coupland:convert($node/name),
coupland:convert($node/uri)
} ,
coupland:convert($node/(node() except (uri,name)))
}
};
declare function coupland:sites($node as element(sites) as node()?) as item()* {
for $site in $node/site
order by $node/sortkey
return
coupland:convert($node/site)
};
declare function coupland:sortkey($node as element(sortkey) as node()?) as item()* {
()
};
declare function coupland:subtitle($node as element(subtitle) as node()?) as item()* {
element div{
$node/@*,
coupland:convert($node/node())
}
};
declare function coupland:uri($node as element(uri) as node()?) as item()* {
<span>
{element a{
attribute href {$node },
"Link"
}
}
</span>
};
declare function coupland:websites($node as element(websites) as node()?) as item()* {
(: the rot element so convert to html :)
<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">
{
for $category in $node/category
order by $category/class
return
<div>
<div class="span-10">
{coupland:convert($category)}
</div>
<div class="span-14 last">
{for $site in $node/sites/site[category=$category/class]
order by ($site/sortkey,$site/name)[1]
return
coupland:convert($site)
}
</div>
<hr />
</div>
}
</div>
</body>
</html>
};