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) };