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:debug:=false(); (: trace messages:)

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)
                            (:=>ore:trace("MODEL2:"):)
                        let $x:=ore:update($each,$opts,$model2)
                        return $x (:=>ore: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]=>ore:trace("£ target: ")
                 return (
                        replace value of node $target  with 
                        ore:expression($at, $model)=>ore: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)=>ore: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)=>ore: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=>ore: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("&amp;")
            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)      
};

(:~ trace:)
declare function ore:trace($value as item()*, $label as xs:string)
as item()*
{
 if($ore:debug) 
 then trace($value,$label)
 else $value
};
