basex-lsp/webapp/lsp-manager/lib.xq/oregano.xqm
2025-10-13 23:07:01 +01:00

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