xquery version "3.0"; module namespace templates="http://exist-db.org/xquery/templates"; (:~ : HTML templating module : : @version 2.0 : @author Wolfgang Meier :) import module namespace config="http://exist-db.org/xquery/apps/config" at "config.xqm"; declare variable $templates:CONFIG_STOP_ON_ERROR := "stop-on-error"; declare variable $templates:CONFIGURATION := QName("http://exist-db.org/xquery/templates", "configuration"); declare variable $templates:CONFIGURATION_ERROR := QName("http://exist-db.org/xquery/templates", "ConfigurationError"); declare variable $templates:NOT_FOUND := QName("http://exist-db.org/xquery/templates", "NotFound"); declare variable $templates:TOO_MANY_ARGS := QName("http://exist-db.org/xquery/templates", "TooManyArguments"); (:~ : Start processing the provided content. Template functions are looked up by calling the : provided function $resolver. The function should take a name as a string : and return the corresponding function item. The simplest implementation of this function could : look like this: : :
function($functionName as xs:string, $arity as xs:int) { function-lookup(xs:QName($functionName), $arity) }
:
: @param $content the sequence of nodes which will be processed
: @param $resolver a function which takes a name and returns a function with that name
: @param $model a sequence of items which will be passed to all called template functions. Use this to pass
: information between templating instructions.
:)
declare function templates:apply($content as node()+, $resolver as function(xs:string) as item()?, $model as map(*)?,
$configuration as map(*)?) {
let $model := if ($model) then $model else map:new()
let $configuration :=
if (exists($configuration)) then
map:new(($configuration, map { "resolve" := $resolver }))
else
map { "resolve" := $resolver }
let $model := map:new(($model, map:entry($templates:CONFIGURATION, $configuration)))
for $root in $content
return
templates:process($root, $model)
};
declare function templates:apply($content as node()+, $resolver as function(xs:string) as item()?, $model as map(*)?) {
templates:apply($content, $resolver, $model, ())
};
(:~
: Continue template processing on the given set of nodes. Call this function from
: within other template functions to enable recursive processing of templates.
:
: @param $nodes the nodes to process
: @param $model a sequence of items which will be passed to all called template functions. Use this to pass
: information between templating instructions.
:)
declare function templates:process($nodes as node()*, $model as map(*)) {
for $node in $nodes
return
typeswitch ($node)
case document-node() return
for $child in $node/node() return templates:process($child, $model)
case element() return
let $instructions := templates:get-instructions($node/@class)
return
if ($instructions) then
for $instruction in $instructions
return
templates:call($instruction, $node, $model)
else
element { node-name($node) } {
$node/@*, for $child in $node/node() return templates:process($child, $model)
}
default return
$node
};
declare %private function templates:get-instructions($class as xs:string?) as xs:string* {
for $name in tokenize($class, "\s+")
where templates:is-qname($name)
return
$name
};
declare %private function templates:call($class as xs:string, $node as element(), $model as map(*)) {
let $paramStr := substring-after($class, "?")
let $parameters := templates:parse-parameters($paramStr)
let $func := if ($paramStr) then substring-before($class, "?") else $class
let $call := templates:resolve(10, $func, $model($templates:CONFIGURATION)("resolve"))
return
if (exists($call)) then
templates:call-by-introspection($node, $parameters, $model, $call)
else if ($model($templates:CONFIGURATION)("stop-on-error")) then
error($templates:NOT_FOUND, "No template function found for call " || $func)
else
(: Templating function not found: just copy the element :)
element { node-name($node) } {
$node/@*, for $child in $node/node() return templates:process($child, $model)
}
};
declare %private function templates:call-by-introspection($node as element(), $parameters as element(parameters), $model as map(*),
$fn as function(*)) {
let $inspect := util:inspect-function($fn)
let $args := templates:map-arguments($inspect, $parameters)
return
templates:process-output(
$node,
$model,
templates:call-with-args($fn, $args, $node, $model),
$inspect
)
};
declare %private function templates:call-with-args($fn as function(*), $args as (function() as item()*)*,
$node as element(), $model as map(*)) {
switch (count($args))
case 0 return
$fn($node, $model)
case 1 return
$fn($node, $model, $args[1]())
case 2 return
$fn($node, $model, $args[1](), $args[2]())
case 3 return
$fn($node, $model, $args[1](), $args[2](), $args[3]())
case 4 return
$fn($node, $model, $args[1](), $args[2](), $args[3](), $args[4]())
case 5 return
$fn($node, $model, $args[1](), $args[2](), $args[3](), $args[4](), $args[5]())
case 6 return
$fn($node, $model, $args[1](), $args[2](), $args[3](), $args[4](), $args[5](), $args[6]())
case 7 return
$fn($node, $model, $args[1](), $args[2](), $args[3](), $args[4](), $args[5](), $args[6](), $args[7]())
case 8 return
$fn($node, $model, $args[1](), $args[2](), $args[3](), $args[4](), $args[5](), $args[6](), $args[7](), $args[8]())
default return
error($templates:TOO_MANY_ARGS, "Too many arguments to function " || function-name($fn))
};
declare %private function templates:process-output($node as element(), $model as map(*), $output as item()*,
$inspect as element(function)) {
let $wrap :=
$inspect/annotation[ends-with(@name, ":wrap")]
[@namespace = "http://exist-db.org/xquery/templates"]
return
if ($wrap) then
element { node-name($node) } {
$node/@*,
templates:process-output($node, $model, $output)
}
else
templates:process-output($node, $model, $output)
};
declare %private function templates:process-output($node as element(), $model as map(*), $output as item()*) {
typeswitch($output)
case map(*) return
templates:process($node/node(), map:new(($model, $output)))
default return
$output
};
declare %private function templates:map-arguments($inspect as element(function), $parameters as element(parameters)) {
let $args := $inspect/argument
return
if (count($args) > 2) then
for $arg in subsequence($inspect/argument, 3)
return
templates:map-argument($arg, $parameters)
else
()
};
declare %private function templates:map-argument($arg as element(argument), $parameters as element(parameters))
as function() as item()* {
let $var := $arg/@var
let $type := $arg/@type/string()
let $param :=
(
request:get-parameter($var, ()),
$parameters/param[@name = $var]/@value,
templates:arg-from-annotation($var, $arg)
)[1]
let $data :=
try {
templates:cast($param, $type)
} catch * {
error($templates:TYPE_ERROR, "Failed to cast parameter value '" || $param || "' to the required target type for " ||
"template function parameter $" || $name || " of function " || ($arg/../@name) || ". Required type was: " ||
$type || ". " || $err:description)
}
return
function() {
$data
}
};
declare %private function templates:arg-from-annotation($var as xs:string, $arg as element(argument)) {
let $anno :=
$arg/../annotation[ends-with(@name, ":default")]
[@namespace = "http://exist-db.org/xquery/templates"]
[value[1] = $var]
for $value in subsequence($anno/value, 2)
return
string($value)
};
declare %private function templates:resolve($arity as xs:int, $func as xs:string,
$resolver as function(xs:string, xs:int) as function(*)) {
if ($arity < 2) then
()
else
let $fn := $resolver($func, $arity)
return
if (exists($fn)) then
$fn
else
templates:resolve($arity - 1, $func, $resolver)
};
declare %private function templates:parse-parameters($paramStr as xs:string?) as element(parameters) {
{ $source }
Try it