235 lines
8.1 KiB
Text
235 lines
8.1 KiB
Text
xquery version '4.0';
|
|
(: oregano: A thymeleaf style templating library - INCOMPLETE
|
|
Aims to be a compatable implementation of a subset https://www.thymeleaf.org/
|
|
for use as a HTML5 templating engine with BaseX 10+
|
|
supports: th:text, layout:decorate, layout:fragment
|
|
@author Andy Bunce
|
|
@status INCOMPLETE alpha
|
|
@licence Apache 2
|
|
:)
|
|
module namespace ore = 'urn:quodatum:template:oregano';
|
|
import module namespace p = 'Thyme' at "Thyme.xqm";
|
|
|
|
declare namespace th="http://www.thymeleaf.org";
|
|
declare namespace layout="http://www.ultraq.net.nz/thymeleaf/layout";
|
|
|
|
declare variable $ore:default-options:=map{"base": file:base-dir(),
|
|
"layout": true(),
|
|
"indent": "no"
|
|
};
|
|
|
|
(:~ updated html doc :)
|
|
declare function ore:render($view as xs:string,$opts as map(*),$model as map(*))
|
|
as document-node(){
|
|
let $opts:=map:merge(($opts,$ore:default-options))
|
|
let $doc:=(ore:doc($view,$opts)
|
|
,message($model,"£ RENDER MODEL(" || $view ||"): "))
|
|
let $a:= ore:update($doc,$opts,$model)
|
|
return ore:decorate($a,$opts)
|
|
};
|
|
|
|
(:~ update doc by evaluating th:each and th:text :)
|
|
declare function ore:update($doc ,$opts as map(*),$model as map(*))
|
|
{
|
|
$doc update { (
|
|
for $each in outermost(*//*[@th:each])
|
|
let $p:=ore:parse-each($each/@th:each)
|
|
return (
|
|
replace node $each with (
|
|
for $item in map:get($model,substring-after($p,","))
|
|
let $model2:=map:put($model,substring-before($p,","),$item)
|
|
(:=>trace("MODEL2:"):)
|
|
let $x:=ore:update($each,$opts,$model2)
|
|
return $x (:=>trace("DD: "):)
|
|
))
|
|
,delete node @th:each
|
|
)
|
|
}update{
|
|
for $text in .//*[@th:text]
|
|
where not($text/ancestor::*/@th:each)
|
|
return (
|
|
replace value of node $text with ore:expression($text/@th:text,$model)
|
|
,delete node $text/@th:text
|
|
)
|
|
, ore:update-attrib(., "value", $model)=>prof:time("£ ore:update-attrib value: ")
|
|
, ore:update-attrib(., "href", $model)=>prof:time("£ ore:update-attrib href: ")
|
|
, ore:update-attrib(., "src", $model)=>prof:time("£ ore:update-attrib src: ")
|
|
}
|
|
};
|
|
|
|
(:~ update-attrib: use value of expression @th:name to update @name :)
|
|
declare %updating function ore:update-attrib($context as node(),$name as xs:string,$model as map(*))
|
|
as empty-sequence(){
|
|
for $at in $context//*/@th:*[local-name(.) eq $name]
|
|
where not($at/ancestor::*/@th:each)
|
|
let $target:=$at/../@*[name() eq $name]=>trace("£ target: ")
|
|
return (
|
|
replace value of node $target with
|
|
ore:expression($at, $model)=>trace("£ ore:expression: ")
|
|
,delete node $at
|
|
)
|
|
};
|
|
|
|
(: load doc from base folder:)
|
|
declare function ore:doc($file as xs:string,$opts as map(*))
|
|
as document-node(){
|
|
let $f:=file:resolve-path($file,$opts?base)=>trace("ore:doc")
|
|
return doc($f)
|
|
};
|
|
|
|
(:~ wrap with template named in root attribute @layout:decorate :)
|
|
declare function ore:decorate($doc as document-node(),$opts as map(*))
|
|
as document-node(){
|
|
if(not($opts?layout and $doc/*/@layout:decorate))
|
|
then $doc
|
|
else
|
|
let $wrap:=$doc/*/@layout:decorate=>replace(".+\{(.+)\}","$1")
|
|
let $wdoc:=ore:doc($wrap,$opts)
|
|
return $wdoc update{
|
|
|
|
replace node html/head/title
|
|
with $doc/html/head/title,
|
|
|
|
insert node $doc/html/head/*[name() ne 'title']
|
|
as last into html/head,
|
|
|
|
(: update matching fragments :)
|
|
for $frag in //*/@layout:fragment
|
|
where $doc//*/@layout:fragment=$frag
|
|
return replace node //*[@layout:fragment=$frag]
|
|
with ($doc//*[@layout:fragment=$frag] update {delete node @layout:fragment})
|
|
|
|
}
|
|
};
|
|
|
|
(:~ serialize as HTML5 :)
|
|
declare function ore:serialize($doc as document-node(),$opts as map(*))
|
|
as xs:string{
|
|
serialize($doc ,
|
|
map { 'method': 'html', 'version': '5.0'}
|
|
)
|
|
};
|
|
|
|
(:~ Return value of $atrib from model :)
|
|
declare function ore:expression($atrb as xs:string,$model as map(*))
|
|
as xs:string{
|
|
let $p:=ore:parse-text($atrb)=>trace("ore:expression: ")
|
|
let $exp:=substring-after($p,",")
|
|
return switch(substring-before($p,","))
|
|
|
|
case "$" case "#" return
|
|
(: @TODO assumes simple dotted :)
|
|
ore:expression-value($exp,$model)
|
|
|
|
case "@" return
|
|
let $x:=ore:expression-link($exp)=>ore:parse-params()
|
|
return ore:template1($x,$model)
|
|
|
|
case "*" return
|
|
error(xs:QName('ore:expression'),"*" || $exp)
|
|
|
|
default return
|
|
error(xs:QName('ore:expression'),"bad exp start: " || $atrb)
|
|
};
|
|
|
|
(:~ evaluate link $exp is body from "@{ body }" :)
|
|
declare function ore:expression-link($exp as xs:string)
|
|
as xs:string {
|
|
switch (true())
|
|
case starts-with($exp=>trace("EEEE: "), "http")
|
|
case starts-with($exp, "//")
|
|
return $exp
|
|
case starts-with($exp, "/")
|
|
return concat("/{_ctx}",$exp) (: prepend app:)
|
|
case starts-with($exp, "~/")
|
|
return substring($exp, 2)
|
|
|
|
default
|
|
return $exp
|
|
};
|
|
|
|
(:~ $exp is name or dotted name :)
|
|
declare function ore:expression-value($exp as xs:string, $model as map (*))
|
|
as xs:string {
|
|
(: @TODO assumes simple dotted :)
|
|
try {
|
|
normalize-space($exp)=> tokenize("\.")=>fold-left( $model, map:get#2)=>string()
|
|
} catch *{
|
|
error(xs:QName('expression-value'),"bad: " || $exp)
|
|
}
|
|
};
|
|
|
|
(: extract param map if present in link
|
|
@result 1st item string updated link , optional 2nd item is map of params
|
|
:)
|
|
declare function ore:parse-params($link as xs:string)
|
|
as item()*
|
|
{
|
|
let $rex:="[^(]+\((.+)\)$"
|
|
let $p:=replace($link,$rex,"$1")
|
|
return if($p ne $link)
|
|
then (: params present :)
|
|
let $params:=(for $i in tokenize($p,",")
|
|
let $v:=substring-after($i,"=")=>replace("['""](.*)['""]","$1") (: remove quotes :)
|
|
return map:entry(substring-before($i,"="),$v)
|
|
)=>map:merge()
|
|
|
|
let $ps:=map:keys($params)!concat(.,"=",$params(.))=>string-join("&")
|
|
let $u:=substring-before($link,"(")
|
|
let $anchor:=if(contains($u,"#"))
|
|
then "#" || substring-after($u,"#")
|
|
|
|
return (concat(substring-before($u || "#","#"),"?",$ps,$anchor),$params)
|
|
|
|
else $link
|
|
};
|
|
|
|
(:~ return "name,collection" from @th:each="name: ${collection}" or error :)
|
|
declare function ore:parse-each($atrb as xs:string)
|
|
as xs:string{
|
|
let $rex:="^(\w+):\s*\$\{\s*(\w+)\s*\}\s*$"
|
|
return if(matches($atrb,$rex))
|
|
then replace($atrb,$rex,"$1,$2")
|
|
else error(xs:QName('ore:parse-each'),"bad th:each: " || $atrb)
|
|
};
|
|
|
|
(:~ return "type,expression" from @th:text or error
|
|
Variable Expressions: ${...}
|
|
Selection Variable Expressions: *{...}
|
|
Message Expressions: #{...}
|
|
Link URL Expressions: @{...}
|
|
:)
|
|
declare function ore:parse-text($atrb as xs:string)
|
|
as xs:string{
|
|
let $rex:="^([$*#@])\{\s*([^\s]+)\s*\}$"
|
|
return if(matches($atrb,$rex))
|
|
then replace($atrb,$rex,"$1,$2")
|
|
else error(xs:QName('ore:parse-text'),"bad th:text: " || $atrb)
|
|
};
|
|
|
|
|
|
|
|
(:~ REx parse :)
|
|
declare function ore:parse($exp as xs:string)
|
|
as element(*)
|
|
{
|
|
let $x:=p:parse-ThymeLeaf($exp)
|
|
return if($x/self::ERROR)
|
|
then error(xs:QName('ore:parse'),"bad: " || $exp)
|
|
else $x
|
|
};
|
|
|
|
|
|
(:~ subst {key} strings in $tmp from model dotted values :)
|
|
declare function ore:template1($tmp as xs:string, $model as map(*))
|
|
as xs:string{
|
|
(: @todo xquery 4 replace :)
|
|
let $s:=analyze-string($tmp, "\{([^}]+)\}")
|
|
update{
|
|
for $m in fn:match
|
|
|
|
let $g:= normalize-space($m/fn:group)=> tokenize("\.")=>fold-left( $model, map:get#2)=>string()
|
|
return replace value of node $m with $g
|
|
}
|
|
return string($s)
|
|
};
|