xf:template working with full templating example, xf:snippet removed

xquery version "3.0";

(:~
 : Origami templating.
 :
 : Requires Basex 8.0 20141221 or later.
 :
 : @version 0.4
 : @author Marc van Grootel
 : @see https://github.com/xokomola/origami
 :)

module namespace xf = 'http://xokomola.com/xquery/origami';

(:~
 : Load an HTML resource.
 :)
declare function xf:html-resource($url-or-path) {
    if (starts-with($url-or-path,'http:/')) then
        xf:fetch-html($url-or-path)
    else
        xf:parse-html($url-or-path)
};

(:~
 : Load an XML resource.
 :)
declare function xf:xml-resource($url-or-path) {
    doc($url-or-path)
};

(:~
 : Fetch and parse HTML given a URL.
 :)
declare function xf:fetch-html($url) {
    html:parse(fetch:binary($url))
};

(:~
 : Parse HTML from a filesystem path.
 :)
declare function xf:parse-html($path) {
    html:parse(file:read-binary($path))
};

(:~
 : Returns a template.
 :)
declare function xf:template($template as node(), $selector as array(*)?, $model as item()*)
    as function(*) {
    let $at := if ($selector instance of array(*)) then xf:at($selector) else function($nodes) { $nodes }
    return
        typeswitch ($model)
        case function(*)
        return
            (: This is a bit of a hack to support returning a function with the correct arity :)
            switch (function-arity($model))
            case 0
            return
                function() as node()* {
                    $template => $at() => xf:transform($model)
                }
            case 1
            return
                function($arg1 as item()?) as node()* {
                    $template => $at() => xf:transform(apply($model, [$arg1]))
                }
            case 2
            return
                function($arg1 as item()?, $arg2 as item()?) as node()* {
                    $template => $at() => xf:transform(apply($model, [$arg1, $arg2]))
                }
            case 3
            return
                function($arg1 as item()?, $arg2 as item()?, $arg3 as item()?) as node()* {
                    $template => $at() => xf:transform(apply($model, [$arg1, $arg2, $arg3]))
                }
            default
            (: max arity-4 supported :)
            return
                function($arg1 as item()?, $arg2 as item()?, $arg3 as item()?, $arg4 as item()?) as node()* {
                    $template => $at() => xf:transform(apply($model, [$arg1, $arg2, $arg3, $arg4]))
                }
        case array(*)*
        return
            function() as node()* {
                $template => $at() => xf:transform($model)
            }
        default
        return
            function() as node()* {
                $template => $at()
            }
};

declare function xf:template($template as node(), $model as item()*)
    as function() as node()* {
    xf:template($template, (), $model)
};

(:~
 : Returns a Transformer function.
 :)
declare function xf:transform($templates as array(*)*)
    as function(node()*) as node()* {
    let $templates :=
        for $template in $templates
        return xf:match($template)
    return
        function($nodes as node()*) as node()* {
            xf:apply-nodes($nodes, (), $templates)
        }
};

(:~
 : Transform input, using the specified templates.
 :)
declare function xf:transform($nodes as node()*, $templates as array(*)*)
    as node()* {
    xf:transform($templates)($nodes)
};

(:~
 : Identity transformer.
 :)
declare function xf:transform() { xf:transform(()) };

(:~
 : Returns an extractor function that only returns selected nodes
 : only outermost, in document order and duplicates eliminated.
 :)
declare function xf:extract($selectors as array(*)*)
    as function(node()*) as node()* {
    xf:extract-outer($selectors)
};

(:~
 : Extracts nodes from input, using the specified selectors.
 :)
declare function xf:extract($nodes as node()*, $selectors as array(*)*)
    as node()* {
    xf:extract($selectors)($nodes)
};

(:~
 : Returns an extractor function that returns selected nodes,
 : only innermost, in document order and duplicates eliminitated.
 :)
declare function xf:extract-inner($selectors as array(*)*)
    as function(node()*) as node()* {
    let $selectors :=
        for $selector in $selectors
        return
            xf:at($selector)
    return
        function($nodes as node()*) as node()* {
            innermost(
                for $selector in $selectors
                return $selector($nodes)
            )
        }
};

declare function xf:extract-inner($nodes as node()*, $selectors as array(*)*)
    as node()* {
    xf:extract-inner($selectors)($nodes)
};

(:~
 : Returns an extractor function that returns selected nodes,
 : only outermost, in document order and duplicates eliminated.
 :)
declare function xf:extract-outer($selectors as array(*)*)
    as function(node()*) as node()* {
    let $selectors :=
        for $selector in $selectors
        return xf:at($selector)
    return
        function($nodes as node()*) as node()* {
            outermost(
                for $selector in $selectors
                return $selector($nodes)
            )
        }
};

declare function xf:extract-outer($nodes as node()*, $selectors as array(*)*)
    as node()* {
    xf:extract-outer($selectors)($nodes)
};

(:~
 : Defines a match template.
 :
 : A template takes a selector string or function and
 : a node transformation function or the items to return as
 : the template body.
 : Providing invalid matcher returns empty sequence.
 :)
declare function xf:match($template as array(*))
    as map(*)? {
    let $selector := xf:selector([array:head($template)], xf:select#1)
    let $body := xf:do(array:tail($template))
    where array:size($template) gt 0
    return
        map {
            'select': $selector,
            'fn': $body
        }
};

declare function xf:at($steps as array(*))
    as function(node()*) as node()* {
    xf:selector($steps, xf:select-all#1)
};

(:~
 : Execute a chain of node selectors.
 :)
declare function xf:at($nodes as node()*, $selector as item()*) {
    xf:at($selector)($nodes)
};

declare %private function xf:selector($steps as array(*))
    as function(node()*) as node()* {
    xf:selector($steps, xf:select-all#1)
};
(:~
 : Only nodes for which the chain of expressions evaluates to `true()`
 : are passed through.
 :)
declare function xf:if($conditions as item()*)
    as function(node()*) as node()* {
    let $conditions := xf:comp-expression($conditions)
    return
        function($nodes as node()*) as node()* {
            fold-left(
                $conditions,
                $nodes,
                function($result, $step) {
                    if ($step($result)) then
                       $result
                    else
                        ()
                }
            )
        }
};

(:~
 : Filter nodes based on evaluating a chain of expressions, letting only
 : those nodes through for which it returns `true()`.
 :)
declare function xf:if($nodes as node()*, $conditions as item()*)
    as node()* {
    xf:if($conditions)($nodes)
};

(:~
 : Compose a chain of node transformers.
 :)
declare function xf:do($fns as array(*))
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        array:fold-left(
            $fns,
            $nodes,
            function($nodes, $fn) {
                if ($fn instance of function(*)) then
                    $fn($nodes)
                else if ($fn instance of node()*) then
                    $fn
                else
                    ()
            }
        )
    }
};

(:~
 : Execute a chain of node transformers.
 :)
declare function xf:do($nodes as node()*, $fns as array(*))
    as node()* {
    xf:do($fns)($nodes)
};

(:~
 : Replace the content (child nodes) of the input node
 :)
declare function xf:content($content as node()*)
    as function(element()?) as element()? {
    function($element as element()?) as element()? {
        if (exists($element)) then
            element { node-name($element) } {
                $element/@*,
                $content
            }
        else
            $element
    }
};

declare function xf:content($element as element()?, $content as node()*)
    as element()? {
    xf:content($content)($element)
};

(:~
 : Only replace the content of the input element when
 : the content is not empty.
 :)
declare function xf:content-if($content as node()*)
    as function(element()?) as element()? {
    function($element as element()?) as element()? {
        if (exists($content) and exists($element)) then
            element { node-name($element) } {
                $element/@*,
                $content
            }
        else
            $element
    }
};

declare function xf:content-if($element as element()?, $content as node()*)
    as element()? {
    xf:content-if($content)($element)
};

(:~
 : Replace the current node.
 :)
declare function xf:replace($replacement as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        if (exists($nodes)) then
            $replacement
        else
            $nodes
    }
};

declare function xf:replace($nodes as node()*, $replacement as node()*)
    as node()* {
    xf:replace($replacement)($nodes)
};

(:~
 : Only replace the input nodes when the replacement
 : is not empty.
 :)
declare function xf:replace-if($replacement as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        if (exists($replacement) and exists($nodes)) then
            $replacement
        else
            $nodes
    }
};

declare function xf:replace-if($nodes as node()*, $replacement as node()*)
    as node()* {
    xf:replace-if($replacement)($nodes)
};

(:~
 : Inserts nodes before the current node.
 :)
declare function xf:before($before as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        ($before, $nodes)
    }
};

declare function xf:before($nodes as node()*, $before as node()*)
    as node()* {
    xf:before($before)($nodes)
};

(:~
 : Inserts nodes after the current node.
 :)
declare function xf:after($after as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        ($nodes, $after)
    }
};

declare function xf:after($nodes as node()*, $after as node()*)
    as node()* {
    xf:after($after)($nodes)
};

(:~
 : Inserts nodes as first child, before the current content.
 :)
declare function xf:append($append as node()*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as element() {
            element { node-name($node) } {
                ($node/(@*,node()), $append)
            }
        }
     (: HACK :)
    )(true())
};

(:~
 : Inserts nodes as last child, after the current content.
 :)
declare function xf:append($nodes as node()*, $append as node()*)
    as node()* {
    xf:append($append)($nodes)
};

(:~
 : Inserts nodes as last child, after the current content.
 :)
declare function xf:prepend($prepend as node()*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as element() {
            element { node-name($node) } {
                ($node/@*, $prepend, $node/node())
            }
        }
     (: HACK :)
    )(true())
};

(:~
 : Inserts nodes as last child, after the current content.
 :)
declare function xf:prepend($nodes as node()*, $prepend as node()*)
    as node()* {
    xf:prepend($prepend)($nodes)
};

(:~
 : Returns a node transformer that returns a text node with
 : the space normalized string value of a node.
 :)
declare function xf:text()
    as function(node()*) as node()* {
    function($nodes as node()*) as text()? {
        if (exists($nodes)) then
            text { normalize-space(string-join($nodes,'')) }
        else
            ()
    }
};

declare function xf:text($nodes as node()*)
    as text()? {
    xf:text()($nodes)
};

(:~
 : Set attributes using a map.
 :)
declare function xf:set-attr($attributes as item())
    as function(node()*) as node()* {
    let $attributes :=
        typeswitch ($attributes)
        case map(*)
        return
            map:for-each(
                $attributes,
                function($name,$value) {
                    attribute { $name } { $value }
                })
        case element()
        return $attributes/@*
        default
        return
            ()
    return
        xf:element-transformer(
            function($node as element()) as node()* {
                element { node-name($node) } {
                    $attributes,
                    for $att in $node/@*
                    where node-name($att) != $attributes/node-name()
                    return $att,
                    $node/node()
                }
            }
        )($attributes)
};

declare function xf:set-attr($nodes as node()*, $attributes as item())
    as node()* {
    xf:set-attr($attributes)($nodes)
};

(:~
 : Add one or more `$names` to a class attribute.
 :)
declare function xf:add-class($names as xs:string*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            element { node-name($node) } {
                $node/@*[not(name(.) = 'class')],
                attribute class {
                    string-join(
                        distinct-values(
                            tokenize(
                                string-join(($node/@class,$names),' '),
                                '\s+')),
                        ' ')
                },
                $node/node()
            }
        }
    )($names)
};

(:~
 : Add one or more `$names` to the class attribute of `$element`.
 : If it doesn't exist it is added.
 :)
declare function xf:add-class($nodes as node()*, $names as xs:string*)
    as node()* {
    xf:add-class($names)($nodes)
};

(:~
 : Remove one or more `$names` from the class attribute.
 : If the class attribute is empty after removing names it will be removed
 : from the element.
 :)
declare function xf:remove-class($names as xs:string*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            element { node-name($node) } {
                $node/@*[not(name(.) =  'class')],
                let $classes := distinct-values(
                    tokenize(($node/@class,'')[1],'\s+')[not(. = $names)])
                where exists($classes)
                return
                    attribute class { string-join($classes,' ') },
                $node/node()
            }
        }
    )($names)
};

(:~
 : Remove one or more `$names` from the class attribute of `$element`.
 : If the class attribute is empty after removing names it will be removed
 : from the element.
 :)
declare function xf:remove-class($nodes as node()*, $names as xs:string*)
    as node()* {
    xf:remove-class($names)($nodes)
};

(:~
 : Remove attributes.
 :)
declare function xf:remove-attr($attributes as item()*)
    as function(node()*) as node()* {
    let $attributes :=
        typeswitch ($attributes)
        case xs:string*
        return for $name in $attributes return attribute { $name } { '' }
        case element()
        return $attributes/@*
        case map(*)
        return
            map:for-each(
                $attributes,
                function($name,$value) {
                    attribute { $name } { '' }
            })
        default
        return ()
    return
        xf:element-transformer(
            function($node as element()) as node()* {
                element { node-name($node) } {
                    for $att in $node/@*
                    where not(node-name($att) = $attributes/node-name())
                    return $att
                }
            }
        )($attributes)
};

declare function xf:remove-attr($nodes as node()*, $names as item()*)
    as node()* {
    xf:remove-attr($names)($nodes)
};

(:~
 : Returns a selector step function that wraps nodes in
 : an element `$element`. An empty sequence will not be
 : wrapped.
 :)
declare function xf:wrap($element as element())
    as function(node()*) as element()? {
    function($nodes as node()*) as element()? {
        if (exists($nodes)) then
            element { node-name($element) } {
                $element/@*,
                $nodes
            }
        else
            ()
    }
};

(:~
 : Wraps `$nodes` in element `$node`. An empty sequence will
 : not be wrapped.
 :)
declare function xf:wrap($nodes as node()*, $element as element())
    as element()? {
    xf:wrap($element)($nodes)
};

(:~
 : Returns a node transformer that removes (unwraps) the
 : outer element of all nodes that are elements. Other nodes
 : are passed through unmodified.
 :
 : This function is safe for use as a node selector.
 :)
declare function xf:unwrap()
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            $node/node()
          (: FIXME: passing true() is a bit of a hack, because
             element-transformers check for empty arg even if they
             don't need one :)
        }
    )(true())
};

(:~
 : Unwraps all nodes that are elements.
 :)
declare function xf:unwrap($nodes as node()*)
    as node()* {
    xf:unwrap()($nodes)
};

declare function xf:xslt($stylesheet as item())
    as function(node()*) as node()* {
    xf:xslt($stylesheet, map {})
};

declare function xf:xslt($stylesheet as item(), $params as item())
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            xslt:transform($node, $stylesheet, $params)/*
        }
    )($params)
};

declare function xf:xslt($nodes as node()*, $stylesheet as item(), $params as item())
    as node()* {
    xf:xslt($stylesheet, $params)($nodes)
};

(:
 : Creates a generic element node transformer function.
 :)
declare function xf:element-transformer($transform as function(element()) as node()*)
    as function(*) {
    function($args as item()*) as function(*) {
        function($nodes as node()*) as node()* {
            if (exists($args)) then
                for $node in $nodes
                return
                    if ($node instance of element()) then
                        $transform($node)
                    else
                        $node
            else
                $nodes
        }
    }
};

(:~
 : Sets up a default environment which can be customized.
 : Represents the default bindings for selecting nodes.
 : The context is set to $nodes and all it's descendant elements.
 : It also sets up a helper function $in to enable proper checks on tokenized
 : (space-delimited) attribute values such as @class.
 :)
declare function xf:environment() {
    map {
        'bindings': function($nodes as node()*) as map(*) {
            map {
                '': $nodes/descendant-or-self::node(),
                xs:QName('in'): function($att, $token) as xs:boolean {
                    $token = tokenize(string($att),'\s+')
                }
            }
        },
        'query': function($selector as xs:string) {
            'declare variable $in external; ' || $selector
        }
    }
};

declare function xf:expr-environment() {
    map {
        'bindings': function($nodes as node()*) as map(*) {
            map {
                '': $nodes,
                xs:QName('in'): function($att, $token) as xs:boolean {
                    $token = tokenize(string($att),'\s+')
                }
            }
        },
        'query': function($selector as xs:string) {
            'declare variable $in external; ' || $selector
        }
    }
};

(:~
 : Find matches for XPath expression string applied to passed in nodes.
 :)
declare function xf:select-all($selector as xs:string)
    as function(node()*) as node()* {
    xf:select-with-env($selector, xf:environment())
};

declare function xf:select-all($nodes as node()*, $selector as xs:string)
    as function(node()*) as node()* {
    xf:select-all($selector)($nodes)
};

declare function xf:select($selector as xs:string)
    as function(node()*) as node()* {
    xf:select-with-env($selector, xf:expr-environment())
};

declare function xf:select($nodes as node()*, $selector as xs:string)
    as function(node()*) as node()* {
    xf:select($selector)($nodes)
};

declare %private function xf:select-with-env($selector as xs:string, $env as map(*))
    as function(node()*) as node()* {
    let $query := $env('query')($selector)
    let $bindings := $env('bindings')
    return
        function($nodes as node()*) as node()* {
            xquery:eval($query, $bindings($nodes))
        }
};

(:~
 : Copies nodes to output, and calls apply for
 : nodes that are wrapped inside <xf:apply>.
 :)
declare %private function xf:copy-nodes($nodes as node()*, $xform as map(*)*)
    as node()* {
    for $node in $nodes
    return
        if ($node/self::xf:apply) then
            xf:apply-nodes($node, (), $xform)
        else if ($node instance of element()) then
            element { node-name($node) } {
                $node/@*,
                xf:copy-nodes($node/node(), $xform)
            }
        else if ($node instance of document-node()) then
            document {
                xf:copy-nodes($node/node(), $xform)
            }
        else
            $node
};

(:~
 : Applies nodes to output, but runs the template node transformer when it
 : encounters a node that was matched.
 :)
declare %private function xf:apply-nodes($nodes as node()*, $context as map(*)*, $xform as map(*)*)
    as node()* {
    for $node in $nodes
    let $context := (xf:match-templates($node, $xform), $context)
    let $match := xf:matched-template($node, $context)
    return
        if ($node/self::xf:apply) then
            xf:apply-nodes($node/(@*,node()), $context, $xform)
        else if ($match instance of map(*)) then
            xf:copy-nodes($match('fn')($node), $xform)
        else if ($node instance of element()) then
            element { node-name($node) } {
                xf:apply-nodes($node/(@*, node()), $context, $xform)
            }
        else if ($node instance of document-node()) then
            document {
                xf:apply-nodes($node/node(), $context, $xform)
            }
        else
            $node
};

(:~
 : Apply to be used from within templates.
 :)
declare function xf:apply($nodes as node()*)
    as element(xf:apply) {
    <xf:apply>{ $nodes }</xf:apply>
};

(:~
 : Find all templates matched by this node and adds the matched nodes
 : to the templates.
 :)
declare %private function xf:match-templates($node as node(), $xform as map(*)*)
    as map(*)* {
    fold-left(
        $xform,
        (),
        function($matched-templates as map(*)*, $template as map(*)) {
            let $matched-nodes := $template('select')($node)
            return
                if (exists($matched-nodes)) then
                    ($matched-templates, map:new(($template, map { 'nodes': $matched-nodes })))
                else
                    $matched-templates
        }
    )
};

(:~
 : Looks in the $context to find a template that was matched by this
 : node. First one found (most-specific) wins.
 :)
declare %private function xf:matched-template($node as node(), $context as map(*)*)
    as map(*)? {
    if (count($context) gt 0) then
        hof:until(
            function($context as map(*)*) {
                empty($context) or
                xf:is-node-in-sequence($node, head($context)('nodes')) },
            function($context as map(*)*) {
                tail($context) },
            $context
        )[1]
    else
        ()
};

(:~
 : Define a transformation that starts with selecting nodes with an
 : XPath expression.
 :)
declare %private function xf:selector($steps as array(*), $selector-fn as function(*))
    as function(node()*) as node()* {
    (: compile the steps :)
    let $steps :=
        array:fold-left(
            $steps,
            (),
            function($result, $step) {
                if ($step instance of xs:string) then
                    ($result, $selector-fn($step))
                else if ($step instance of function(*)) then
                    ($result, $step)
                else if ($step instance of node()*) then
                    ($result, function($node as node()*) as node()* { $step })
                else if ($step instance of empty-sequence()) then
                    function($node as node()*) as node()* { () }
                else
                    ($result, text { $step })
            }
        )
    return
        function($nodes as node()*) as node()* {
            for $selected in head($steps)($nodes)
            return
                fold-left(
                    tail($steps),
                    $selected,
                    function($result, $step) {
                        $step($result)
                    }
                )
        }
};

declare %private function xf:comp-expression($expressions as item()*)
    as (function(node()*) as node()*)* {
    for $expression in $expressions
    return
        if ($expression instance of xs:string) then
            xf:select($expression)
        else
            $expression
};

(:~
 : Returns `true()` if the node `$node` is also in sequence `$seq`.
 :
 : @see http://www.xqueryfunctions.com/xq/functx_is-node-in-sequence.html
 :)
declare %private function xf:is-node-in-sequence($node as node()?, $seq as node()*)
    as xs:boolean {
    some $nodeInSeq in $seq satisfies $nodeInSeq is $node
};

xquery version "3.0";

(:~
 : Origami templating.
 :
 : Requires Basex 8.0 20141221 or later.
 :
 : @version 0.4
 : @author Marc van Grootel
 : @see https://github.com/xokomola/origami
 :)

module namespace xf = 'http://xokomola.com/xquery/origami';

(:~
 : Load an HTML resource.
 :)
declare function xf:html-resource($url-or-path) {
    if (starts-with($url-or-path,'http:/')) then
        xf:fetch-html($url-or-path)
    else
        xf:parse-html($url-or-path)
};

(:~
 : Load an XML resource.
 :)
declare function xf:xml-resource($url-or-path) {
    doc($url-or-path)
};

(:~
 : Fetch and parse HTML given a URL.
 :)
declare function xf:fetch-html($url) {
    html:parse(fetch:binary($url))
};

(:~
 : Parse HTML from a filesystem path.
 :)
declare function xf:parse-html($path) {
    html:parse(file:read-binary($path))
};

(:~
 : Returns a template.
 :)
declare function xf:template($template as node(), $selector as array(*), $slots as array(*)*)
    as function(item()?) as node()* {
    function($args as item()?) as node()* {
        $template => xf:extract($selector) => xf:transform($slots)
    }
};

declare function xf:template($template as node(), $slots as array(*)*)
    as function(item()?) as node()* {
    function($args as item()?) as node()* {
        $template => xf:transform($slots)
    }
};

(:~
 : Returns a template.
 :)
declare function xf:snippet($template as node(), $selector as array(*))
    as function(array(*)*) as node()* {
    function($args as array(*)*) as node()* {
        if (exists($args)) then
            $template => xf:at($selector) => xf:transform($args)
        else
            $template => xf:at($selector)
    }
};

(:~
 : Returns a Transformer function.
 :)
declare function xf:transform($templates as array(*)*)
    as function(node()*) as node()* {
    let $templates :=
        for $template in $templates
        return xf:match($template)
    return
        function($nodes as node()*) as node()* {
            xf:apply-nodes($nodes, (), $templates)
        }
};

(:~
 : Transform input, using the specified templates.
 :)
declare function xf:transform($nodes as node()*, $templates as array(*)*)
    as node()* {
    xf:transform($templates)($nodes)
};

(:~
 : Identity transformer.
 :)
declare function xf:transform() { xf:transform(()) };

(:~
 : Returns an extractor function that only returns selected nodes
 : only outermost, in document order and duplicates eliminated.
 :)
declare function xf:extract($selectors as array(*)*)
    as function(node()*) as node()* {
    xf:extract-outer($selectors)
};

(:~
 : Extracts nodes from input, using the specified selectors.
 :)
declare function xf:extract($nodes as node()*, $selectors as array(*)*)
    as node()* {
    xf:extract($selectors)($nodes)
};

(:~
 : Returns an extractor function that returns selected nodes,
 : only innermost, in document order and duplicates eliminitated.
 :)
declare function xf:extract-inner($selectors as array(*)*)
    as function(node()*) as node()* {
    let $selectors :=
        for $selector in $selectors
        return
            xf:at($selector)
    return
        function($nodes as node()*) as node()* {
            innermost(
                for $selector in $selectors
                return $selector($nodes)
            )
        }
};

declare function xf:extract-inner($nodes as node()*, $selectors as array(*)*)
    as node()* {
    xf:extract-inner($selectors)($nodes)
};

(:~
 : Returns an extractor function that returns selected nodes,
 : only outermost, in document order and duplicates eliminated.
 :)
declare function xf:extract-outer($selectors as array(*)*)
    as function(node()*) as node()* {
    let $selectors :=
        for $selector in $selectors
        return xf:at($selector)
    return
        function($nodes as node()*) as node()* {
            outermost(
                for $selector in $selectors
                return $selector($nodes)
            )
        }
};

declare function xf:extract-outer($nodes as node()*, $selectors as array(*)*)
    as node()* {
    xf:extract-outer($selectors)($nodes)
};

(:~
 : Defines a match template.
 :
 : A template takes a selector string or function and
 : a node transformation function or the items to return as
 : the template body.
 : Providing invalid matcher returns empty sequence.
 :)
declare function xf:match($template as array(*))
    as map(*)? {
    let $selector := xf:selector([array:head($template)], xf:select#1)
    let $body := xf:do(array:tail($template))
    where array:size($template) gt 0
    return
        map {
            'select': $selector,
            'fn': $body
        }
};

declare function xf:at($steps as array(*))
    as function(node()*) as node()* {
    xf:selector($steps, xf:select-all#1)
};

(:~
 : Execute a chain of node selectors.
 :)
declare function xf:at($nodes as node()*, $selector as item()*) {
    xf:at($selector)($nodes)
};

declare %private function xf:selector($steps as array(*))
    as function(node()*) as node()* {
    xf:selector($steps, xf:select-all#1)
};
(:~
 : Only nodes for which the chain of expressions evaluates to `true()`
 : are passed through.
 :)
declare function xf:if($conditions as item()*)
    as function(node()*) as node()* {
    let $conditions := xf:comp-expression($conditions)
    return
        function($nodes as node()*) as node()* {
            fold-left(
                $conditions,
                $nodes,
                function($result, $step) {
                    if ($step($result)) then
                       $result
                    else
                        ()
                }
            )
        }
};

(:~
 : Filter nodes based on evaluating a chain of expressions, letting only
 : those nodes through for which it returns `true()`.
 :)
declare function xf:if($nodes as node()*, $conditions as item()*)
    as node()* {
    xf:if($conditions)($nodes)
};

(:~
 : Compose a chain of node transformers.
 :)
declare function xf:do($fns as array(*))
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        array:fold-left(
            $fns,
            $nodes,
            function($nodes, $fn) {
                if ($fn instance of function(*)) then
                    $fn($nodes)
                else if ($fn instance of node()*) then
                    $fn
                else
                    ()
            }
        )
    }
};

(:~
 : Execute a chain of node transformers.
 :)
declare function xf:do($nodes as node()*, $fns as array(*))
    as node()* {
    xf:do($fns)($nodes)
};

(:~
 : Replace the content (child nodes) of the input node
 :)
declare function xf:content($content as node()*)
    as function(element()?) as element()? {
    function($element as element()?) as element()? {
        if (exists($element)) then
            element { node-name($element) } {
                $element/@*,
                $content
            }
        else
            $element
    }
};

declare function xf:content($element as element()?, $content as node()*)
    as element()? {
    xf:content($content)($element)
};

(:~
 : Only replace the content of the input element when
 : the content is not empty.
 :)
declare function xf:content-if($content as node()*)
    as function(element()?) as element()? {
    function($element as element()?) as element()? {
        if (exists($content) and exists($element)) then
            element { node-name($element) } {
                $element/@*,
                $content
            }
        else
            $element
    }
};

declare function xf:content-if($element as element()?, $content as node()*)
    as element()? {
    xf:content-if($content)($element)
};

(:~
 : Replace the current node.
 :)
declare function xf:replace($replacement as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        if (exists($nodes)) then
            $replacement
        else
            $nodes
    }
};

declare function xf:replace($nodes as node()*, $replacement as node()*)
    as node()* {
    xf:replace($replacement)($nodes)
};

(:~
 : Only replace the input nodes when the replacement
 : is not empty.
 :)
declare function xf:replace-if($replacement as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        if (exists($replacement) and exists($nodes)) then
            $replacement
        else
            $nodes
    }
};

declare function xf:replace-if($nodes as node()*, $replacement as node()*)
    as node()* {
    xf:replace-if($replacement)($nodes)
};

(:~
 : Inserts nodes before the current node.
 :)
declare function xf:before($before as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        ($before, $nodes)
    }
};

declare function xf:before($nodes as node()*, $before as node()*)
    as node()* {
    xf:before($before)($nodes)
};

(:~
 : Inserts nodes after the current node.
 :)
declare function xf:after($after as node()*)
    as function(node()*) as node()* {
    function($nodes as node()*) as node()* {
        ($nodes, $after)
    }
};

declare function xf:after($nodes as node()*, $after as node()*)
    as node()* {
    xf:after($after)($nodes)
};

(:~
 : Inserts nodes as first child, before the current content.
 :)
declare function xf:append($append as node()*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as element() {
            element { node-name($node) } {
                ($node/(@*,node()), $append)
            }
        }
     (: HACK :)
    )(true())
};

(:~
 : Inserts nodes as last child, after the current content.
 :)
declare function xf:append($nodes as node()*, $append as node()*)
    as node()* {
    xf:append($append)($nodes)
};

(:~
 : Inserts nodes as last child, after the current content.
 :)
declare function xf:prepend($prepend as node()*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as element() {
            element { node-name($node) } {
                ($node/@*, $prepend, $node/node())
            }
        }
     (: HACK :)
    )(true())
};

(:~
 : Inserts nodes as last child, after the current content.
 :)
declare function xf:prepend($nodes as node()*, $prepend as node()*)
    as node()* {
    xf:prepend($prepend)($nodes)
};

(:~
 : Returns a node transformer that returns a text node with
 : the space normalized string value of a node.
 :)
declare function xf:text()
    as function(node()*) as node()* {
    function($nodes as node()*) as text()? {
        if (exists($nodes)) then
            text { normalize-space(string-join($nodes,'')) }
        else
            ()
    }
};

declare function xf:text($nodes as node()*)
    as text()? {
    xf:text()($nodes)
};

(:~
 : Set attributes using a map.
 :)
declare function xf:set-attr($attributes as item())
    as function(node()*) as node()* {
    let $attributes :=
        typeswitch ($attributes)
        case map(*)
        return
            map:for-each(
                $attributes,
                function($name,$value) {
                    attribute { $name } { $value }
                })
        case element()
        return $attributes/@*
        default
        return
            ()
    return
        xf:element-transformer(
            function($node as element()) as node()* {
                element { node-name($node) } {
                    $attributes,
                    for $att in $node/@*
                    where node-name($att) != $attributes/node-name()
                    return $att,
                    $node/node()
                }
            }
        )($attributes)
};

declare function xf:set-attr($nodes as node()*, $attributes as item())
    as node()* {
    xf:set-attr($attributes)($nodes)
};

(:~
 : Add one or more `$names` to a class attribute.
 :)
declare function xf:add-class($names as xs:string*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            element { node-name($node) } {
                $node/@*[not(name(.) = 'class')],
                attribute class {
                    string-join(
                        distinct-values(
                            tokenize(
                                string-join(($node/@class,$names),' '),
                                '\s+')),
                        ' ')
                },
                $node/node()
            }
        }
    )($names)
};

(:~
 : Add one or more `$names` to the class attribute of `$element`.
 : If it doesn't exist it is added.
 :)
declare function xf:add-class($nodes as node()*, $names as xs:string*)
    as node()* {
    xf:add-class($names)($nodes)
};

(:~
 : Remove one or more `$names` from the class attribute.
 : If the class attribute is empty after removing names it will be removed
 : from the element.
 :)
declare function xf:remove-class($names as xs:string*)
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            element { node-name($node) } {
                $node/@*[not(name(.) =  'class')],
                let $classes := distinct-values(
                    tokenize(($node/@class,'')[1],'\s+')[not(. = $names)])
                where exists($classes)
                return
                    attribute class { string-join($classes,' ') },
                $node/node()
            }
        }
    )($names)
};

(:~
 : Remove one or more `$names` from the class attribute of `$element`.
 : If the class attribute is empty after removing names it will be removed
 : from the element.
 :)
declare function xf:remove-class($nodes as node()*, $names as xs:string*)
    as node()* {
    xf:remove-class($names)($nodes)
};

(:~
 : Remove attributes.
 :)
declare function xf:remove-attr($attributes as item()*)
    as function(node()*) as node()* {
    let $attributes :=
        typeswitch ($attributes)
        case xs:string*
        return for $name in $attributes return attribute { $name } { '' }
        case element()
        return $attributes/@*
        case map(*)
        return
            map:for-each(
                $attributes,
                function($name,$value) {
                    attribute { $name } { '' }
            })
        default
        return ()
    return
        xf:element-transformer(
            function($node as element()) as node()* {
                element { node-name($node) } {
                    for $att in $node/@*
                    where not(node-name($att) = $attributes/node-name())
                    return $att
                }
            }
        )($attributes)
};

declare function xf:remove-attr($nodes as node()*, $names as item()*)
    as node()* {
    xf:remove-attr($names)($nodes)
};

(:~
 : Returns a selector step function that wraps nodes in
 : an element `$element`. An empty sequence will not be
 : wrapped.
 :)
declare function xf:wrap($element as element())
    as function(node()*) as element()? {
    function($nodes as node()*) as element()? {
        if (exists($nodes)) then
            element { node-name($element) } {
                $element/@*,
                $nodes
            }
        else
            ()
    }
};

(:~
 : Wraps `$nodes` in element `$node`. An empty sequence will
 : not be wrapped.
 :)
declare function xf:wrap($nodes as node()*, $element as element())
    as element()? {
    xf:wrap($element)($nodes)
};

(:~
 : Returns a node transformer that removes (unwraps) the
 : outer element of all nodes that are elements. Other nodes
 : are passed through unmodified.
 :
 : This function is safe for use as a node selector.
 :)
declare function xf:unwrap()
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            $node/node()
          (: FIXME: passing true() is a bit of a hack, because
             element-transformers check for empty arg even if they
             don't need one :)
        }
    )(true())
};

(:~
 : Unwraps all nodes that are elements.
 :)
declare function xf:unwrap($nodes as node()*)
    as node()* {
    xf:unwrap()($nodes)
};

declare function xf:xslt($stylesheet as item())
    as function(node()*) as node()* {
    xf:xslt($stylesheet, map {})
};

declare function xf:xslt($stylesheet as item(), $params as item())
    as function(node()*) as node()* {
    xf:element-transformer(
        function($node as element()) as node()* {
            xslt:transform($node, $stylesheet, $params)/*
        }
    )($params)
};

declare function xf:xslt($nodes as node()*, $stylesheet as item(), $params as item())
    as node()* {
    xf:xslt($stylesheet, $params)($nodes)
};

(:
 : Creates a generic element node transformer function.
 :)
declare function xf:element-transformer($transform as function(element()) as node()*)
    as function(*) {
    function($args as item()*) as function(*) {
        function($nodes as node()*) as node()* {
            if (exists($args)) then
                for $node in $nodes
                return
                    if ($node instance of element()) then
                        $transform($node)
                    else
                        $node
            else
                $nodes
        }
    }
};

(:~
 : Sets up a default environment which can be customized.
 : Represents the default bindings for selecting nodes.
 : The context is set to $nodes and all it's descendant elements.
 : It also sets up a helper function $in to enable proper checks on tokenized
 : (space-delimited) attribute values such as @class.
 :)
declare function xf:environment() {
    map {
        'bindings': function($nodes as node()*) as map(*) {
            map {
                '': $nodes/descendant-or-self::node(),
                xs:QName('in'): function($att, $token) as xs:boolean {
                    $token = tokenize(string($att),'\s+')
                }
            }
        },
        'query': function($selector as xs:string) {
            'declare variable $in external; ' || $selector
        }
    }
};

declare function xf:expr-environment() {
    map {
        'bindings': function($nodes as node()*) as map(*) {
            map {
                '': $nodes,
                xs:QName('in'): function($att, $token) as xs:boolean {
                    $token = tokenize(string($att),'\s+')
                }
            }
        },
        'query': function($selector as xs:string) {
            'declare variable $in external; ' || $selector
        }
    }
};

(:~
 : Find matches for XPath expression string applied to passed in nodes.
 :)
declare function xf:select-all($selector as xs:string)
    as function(node()*) as node()* {
    xf:select-with-env($selector, xf:environment())
};

declare function xf:select-all($nodes as node()*, $selector as xs:string)
    as function(node()*) as node()* {
    xf:select-all($selector)($nodes)
};

declare function xf:select($selector as xs:string)
    as function(node()*) as node()* {
    xf:select-with-env($selector, xf:expr-environment())
};

declare function xf:select($nodes as node()*, $selector as xs:string)
    as function(node()*) as node()* {
    xf:select($selector)($nodes)
};

declare %private function xf:select-with-env($selector as xs:string, $env as map(*))
    as function(node()*) as node()* {
    let $query := $env('query')($selector)
    let $bindings := $env('bindings')
    return
        function($nodes as node()*) as node()* {
            xquery:eval($query, $bindings($nodes))
        }
};

(:~
 : Copies nodes to output, and calls apply for
 : nodes that are wrapped inside <xf:apply>.
 :)
declare %private function xf:copy-nodes($nodes as node()*, $xform as map(*)*)
    as node()* {
    for $node in $nodes
    return
        if ($node/self::xf:apply) then
            xf:apply-nodes($node, (), $xform)
        else if ($node instance of element()) then
            element { node-name($node) } {
                $node/@*,
                xf:copy-nodes($node/node(), $xform)
            }
        else if ($node instance of document-node()) then
            document {
                xf:copy-nodes($node/node(), $xform)
            }
        else
            $node
};

(:~
 : Applies nodes to output, but runs the template node transformer when it
 : encounters a node that was matched.
 :)
declare %private function xf:apply-nodes($nodes as node()*, $context as map(*)*, $xform as map(*)*)
    as node()* {
    for $node in $nodes
    let $context := (xf:match-templates($node, $xform), $context)
    let $match := xf:matched-template($node, $context)
    return
        if ($node/self::xf:apply) then
            xf:apply-nodes($node/(@*,node()), $context, $xform)
        else if ($match instance of map(*)) then
            xf:copy-nodes($match('fn')($node), $xform)
        else if ($node instance of element()) then
            element { node-name($node) } {
                xf:apply-nodes($node/(@*, node()), $context, $xform)
            }
        else if ($node instance of document-node()) then
            document {
                xf:apply-nodes($node/node(), $context, $xform)
            }
        else
            $node
};

(:~
 : Apply to be used from within templates.
 :)
declare function xf:apply($nodes as node()*)
    as element(xf:apply) {
    <xf:apply>{ $nodes }</xf:apply>
};

(:~
 : Find all templates matched by this node and adds the matched nodes
 : to the templates.
 :)
declare %private function xf:match-templates($node as node(), $xform as map(*)*)
    as map(*)* {
    fold-left(
        $xform,
        (),
        function($matched-templates as map(*)*, $template as map(*)) {
            let $matched-nodes := $template('select')($node)
            return
                if (exists($matched-nodes)) then
                    ($matched-templates, map:new(($template, map { 'nodes': $matched-nodes })))
                else
                    $matched-templates
        }
    )
};

(:~
 : Looks in the $context to find a template that was matched by this
 : node. First one found (most-specific) wins.
 :)
declare %private function xf:matched-template($node as node(), $context as map(*)*)
    as map(*)? {
    if (count($context) gt 0) then
        hof:until(
            function($context as map(*)*) {
                empty($context) or
                xf:is-node-in-sequence($node, head($context)('nodes')) },
            function($context as map(*)*) {
                tail($context) },
            $context
        )[1]
    else
        ()
};

(:~
 : Define a transformation that starts with selecting nodes with an
 : XPath expression.
 :)
declare %private function xf:selector($steps as array(*), $selector-fn as function(*))
    as function(node()*) as node()* {
    (: compile the steps :)
    let $steps :=
        array:fold-left(
            $steps,
            (),
            function($result, $step) {
                if ($step instance of xs:string) then
                    ($result, $selector-fn($step))
                else if ($step instance of function(*)) then
                    ($result, $step)
                else if ($step instance of node()*) then
                    ($result, function($node as node()*) as node()* { $step })
                else if ($step instance of empty-sequence()) then
                    function($node as node()*) as node()* { () }
                else
                    ($result, text { $step })
            }
        )
    return
        function($nodes as node()*) as node()* {
            for $selected in head($steps)($nodes)
            return
                fold-left(
                    tail($steps),
                    $selected,
                    function($result, $step) {
                        $step($result)
                    }
                )
        }
};

declare %private function xf:comp-expression($expressions as item()*)
    as (function(node()*) as node()*)* {
    for $expression in $expressions
    return
        if ($expression instance of xs:string) then
            xf:select($expression)
        else
            $expression
};

(:~
 : Returns `true()` if the node `$node` is also in sequence `$seq`.
 :
 : @see http://www.xqueryfunctions.com/xq/functx_is-node-in-sequence.html
 :)
declare %private function xf:is-node-in-sequence($node as node()?, $seq as node()*)
    as xs:boolean {
    some $nodeInSeq in $seq satisfies $nodeInSeq is $node
};


propagate fix commited earlier today.

propagate fix commited earlier today.

module namespace admin = 'http://monetdb.cwi.nl/XQuery/admin/';

(: ADMIN MODULE, by Peter Boncz (boncz@cwi.nl)
 :
 : - makes administrative functionality available over SOAP
 : - standard part of the MonetDB/XQuery distribution
 : - can only be called from IPs listed in xrpc_admin (MonetDB.conf option)
 :)

declare namespace pf = 'http://www.pathfinder-xquery.org/';

(: =================== document management =================== :)

declare function admin:collections()
{ pf:collections-unsafe() };

declare function admin:documents()
{ pf:documents-unsafe() };

declare function admin:documents($collection as xs:string)
{ pf:documents-unsafe($collection) };

declare document management function admin:add-doc($uri as xs:string, $document as xs:string, $collection as xs:string, $percentage as xs:integer)
{ pf:add-doc($uri, $document, $collection, $percentage) };

declare document management function admin:del-doc($document as xs:string)
{ pf:del-doc($document) };

declare document management function admin:del-col($collection as xs:string)
{ let $docs := pf:documents-unsafe($collection)
  return if (count($docs) = 0)
    then error("collection not found")
    else for $document in $docs
         return pf:del-doc($document)
};

(: =================== backup & restore =================== :)

declare updating function admin:backup-col($collection as xs:string, $bakname as xs:string)
{
  let $path  := concat('backup/', $bakname, '/', $collection, '/')
  let $index :=
   element documents {
     for $document at $pos in pf:documents($collection)
     let $uri := concat($path, string(10000 + (($pos div 10000)) cast as xs:integer), '/',  string($pos) , '.xml')
     return (text { "
  " }, element document { attribute uri {$uri}, $document/text() }),
     text { "
" } }
  return
   (put($index, concat($path,'documents.xml')),
    for $document in $index/document
    return put(exactly-one(doc(string($document))), exactly-one($document/@uri)))
};

declare updating function admin:backup($bakname as xs:string)
{
  let $path  := concat('backup/', $bakname, '/')
  let $index :=
    element collections {
      for $collection in pf:collections()
      return (text { "
  " }, element collection { attribute updatable {$collection/@updatable}, $collection/text() }),
      text { "
" } }
  return
    (put($index, concat($path,'collections.xml')),
     for $collection in pf:collections()
     return admin:backup-col($collection, $bakname))
};

declare document management function admin:restore-col($collection as xs:string, $bakname as xs:string, $percentage as xs:integer)
{
  for $document in doc(concat('backup/', $bakname, '/', $collection, '/documents.xml'))//document
  return pf:add-doc(exactly-one($document/@uri), exactly-one($document/text()), $collection, $percentage)
};

declare document management function
admin:restore($bakname as xs:string, $percentage as xs:integer)
{
  for $collection in doc(concat('backup/', $bakname, '/collections.xml'))//collection
  let $perc := if ($collection/@updatable = 'true') then $percentage else 0
  return admin:restore-col(exactly-one($collection/text()), $bakname, $perc)
};

(: =================== server management =================== :)

declare function admin:db-stats()
{ pf:mil('lock_set(pf_short); var ret := bat(str,str); var tot := 0LL; colname_runtime@batloop() tot :+= sum([batsize]($t)); ret.insert("xquery_index_curMB", str(tot/1048576LL)); tot := doc_timestamp.select(timestamp_nil,timestamp_nil).reverse(); ret.insert("xquery_cache_curdocs", str(count(tot))); tot := tot.join(doc_collection).tunique().reverse().join(collection_size).sum(); ret.insert("xquery_cache_curMB", str(tot/1048576LL)); ret.insert("xquery_log_curMB", str((logger_changes(pf_logger) - logger_base)/131072)); ret.insert("gdk_vm_cursize", str(vm_cursize())); ret.insert("gdk_mem_cursize", str(mem_cursize())); lock_unset(pf_short); return ret;') };

declare function admin:db-env()
{ pf:mil('return environment().copy().delete("exec_prefix").delete("prefix").delete("gdk_debug").delete("gdk_embedded").delete("gdk_vm_minsize").delete("mapi_debug").delete("mapi_noheaders").delete("monet_daemon").delete("monet_mod_path").delete("monet_pid").delete("monet_prompt").delete("monet_welcome").delete("sql_debug").delete("sql_logdir").insert("gdk_mem_maxsize", str(mem_maxsize())).sort();') };

declare function admin:db-restart()
{ pf:mil('fork({ sleep(5); exit();}); return "Mserver will restart in 5 seconds..";') };

(: =================== HTTP =================== :)

declare function admin:GET($uri as xs:string)
{ doc($uri) };

declare updating function admin:PUT($uri as xs:string, $doc as element())
{
  if (substring($uri, 1,4) = 'tmp/')
  then put($doc, $uri)
  else error('PUT: only relative URIs starting with tmp/ allowed')
};

declare document management function admin:DELETE($uri as xs:string)
{
  if (substring($uri, 1,4) = 'tmp/')
  then pf:del-doc($uri)
  else error('DELETE: only relative URIs starting with tmp/ allowed')
};

module namespace admin = 'http://monetdb.cwi.nl/XQuery/admin/';

(: ADMIN MODULE, by Peter Boncz (boncz@cwi.nl)
 :
 : - makes administrative functionality available over SOAP
 : - standard part of the MonetDB/XQuery distribution
 : - can only be called from IPs listed in xrpc_admin (MonetDB.conf option)
 :)

declare namespace pf = 'http://www.pathfinder-xquery.org/';

(: =================== document management =================== :)

declare function admin:collections()
{ pf:collections-unsafe() };

declare function admin:documents()
{ pf:documents-unsafe() };

declare function admin:documents($collection as xs:string)
{ pf:documents-unsafe($collection) };

declare document management function admin:add-doc($uri as xs:string, $document as xs:string, $collection as xs:string, $percentage as xs:integer)
{ pf:add-doc($uri, $document, $collection, $percentage) };

declare document management function admin:del-doc($document as xs:string)
{ pf:del-doc($document) };

declare document management function admin:del-col($collection as xs:string)
{ let $docs := pf:documents-unsafe($collection)
  return if (count($docs) = 0)
    then error("collection not found")
    else for $document in $docs
         return pf:del-doc($document)
};

(: =================== backup & restore =================== :)

declare updating function admin:backup-col($collection as xs:string, $bakname as xs:string)
{
  let $path  := concat('backup/', $bakname, '/', $collection, '/')
  let $index :=
   element documents {
     for $document at $pos in pf:documents($collection)
     let $uri := concat($path, string(10000 + (($pos div 10000)) cast as xs:integer), '/',  string($pos) , '.xml')
     return (text { "
  " }, element document { attribute uri {$uri}, $document/text() }),
     text { "
" } }
  return
   (put($index, concat($path,'documents.xml')),
    for $document in $index/document
    return put(exactly-one(doc(string($document))), exactly-one($document/@uri)))
};

declare updating function admin:backup($bakname as xs:string)
{
  let $path  := concat('backup/', $bakname, '/')
  let $index :=
    element collections {
      for $collection in pf:collections()
      return (text { "
  " }, element collection { attribute updatable {$collection/@updatable}, $collection/text() }),
      text { "
" } }
  return
    (put($index, concat($path,'collections.xml')),
     for $collection in pf:collections()
     return admin:backup-col($collection, $bakname))
};

declare document management function admin:restore-col($collection as xs:string, $bakname as xs:string, $percentage as xs:integer)
{
  for $document in doc(concat('backup/', $bakname, '/', $collection, '/documents.xml'))//document
  return pf:add-doc(exactly-one($document/@uri), exactly-one($document/text()), $collection, $percentage)
};

declare document management function
admin:restore($bakname as xs:string, $percentage as xs:integer)
{
  for $collection in doc(concat('backup/', $bakname, '/collections.xml'))//collection
  let $perc := if ($collection/@updatable = 'true') then $percentage else 0
  return admin:restore-col(exactly-one($collection/text()), $bakname, $perc)
};

(: =================== server management =================== :)

declare function admin:db-stats()
{ pf:mil('lock_set(pf_short); var ret := bat(str,str); var tot := 0LL; colname_runtime@batloop() tot :+= sum([batsize]($t)); ret.insert("xquery_index_curMB", str(tot/1048576LL)); tot := doc_timestamp.select(timestamp_nil,timestamp_nil).reverse(); ret.insert("xquery_cache_curdocs", str(count(tot))); tot := tot.join(doc_collection).tunique().reverse().join(collection_size).sum(); ret.insert("xquery_cache_curMB", str(tot/1048576LL)); ret.insert("xquery_log_curMB", str((logger_changes(pf_logger) - logger_base)/131072)); ret.insert("gdk_vm_cursize", str(vm_cursize())); ret.insert("gdk_mem_cursize", str(mem_cursize())); lock_unset(pf_short); return ret;') };

declare function admin:db-env()
{ pf:mil('return environment().access(BAT_WRITE).delete("exec_prefix").delete("prefix").delete("gdk_debug").delete("gdk_embedded").delete("gdk_vm_minsize").delete("mapi_debug").delete("mapi_noheaders").delete("monet_daemon").delete("monet_mod_path").delete("monet_pid").delete("monet_prompt").delete("monet_welcome").delete("sql_debug").delete("sql_logdir").insert("gdk_mem_maxsize", str(mem_maxsize())).sort();') };

declare function admin:db-restart()
{ pf:mil('fork({ sleep(5); exit();}); return "Mserver will restart in 5 seconds..";') };

(: =================== HTTP =================== :)

declare function admin:GET($uri as xs:string)
{ doc($uri) };

declare updating function admin:PUT($uri as xs:string, $doc as element())
{
  if (substring($uri, 1,4) = 'tmp/')
  then put($doc, $uri)
  else error('PUT: only relative URIs starting with tmp/ allowed')
};

declare document management function admin:DELETE($uri as xs:string)
{
  if (substring($uri, 1,4) = 'tmp/')
  then pf:del-doc($uri)
  else error('DELETE: only relative URIs starting with tmp/ allowed')
};


Test data changes for Git 166 and 355. Rich argument marshalling.

(:
  Copyright 2014-2015 MarkLogic Corporation

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
:)

declare namespace test='http://marklogic.com/test';
declare variable $test:myString as xs:string external;
declare variable $myXmlNode external;
(:declare variable $myJsonNode as xs:string external;:)
declare variable $myBool as xs:boolean external;
declare variable $myInteger as xs:integer external;
declare variable $myDecimal as xs:decimal external;
declare variable $myDouble as xs:double external;
declare variable $myFloat as xs:float external;
declare variable $myNull  external;

(
    $test:myString,
    $myBool,
    document{ $myXmlNode },
    $myXmlNode//comment(),
    $myXmlNode//text(),
    $myXmlNode//*,
    $myXmlNode/@attr,
    $myXmlNode//processing-instruction(),
    $myInteger,
    $myDecimal,
    $myDouble,
    $myFloat
    )

(:
  Copyright 2014-2015 MarkLogic Corporation

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
:)

declare namespace test='http://marklogic.com/test';
declare variable $test:myString as xs:string external;
declare variable $myXmlNode as xs:string external;
(:declare variable $myJsonNode as xs:string external;:)
declare variable $myBool as xs:boolean external;
declare variable $myInteger as xs:integer external;
declare variable $myDecimal as xs:decimal external;
declare variable $myDouble as xs:double external;
declare variable $myFloat as xs:float external;
declare variable $myNull  external;

(
    $test:myString,
      $myBool,
     document{ xdmp:unquote($myXmlNode) },
     xdmp:unquote($myXmlNode)//comment(),
     xdmp:unquote($myXmlNode)//text(),
     xdmp:unquote($myXmlNode)//*,
     xdmp:unquote($myXmlNode)/@attr,
    xdmp:unquote($myXmlNode)//processing-instruction(),
     (:$myNull,
     xdmp:unquote($myJsonNode)/a,
     xdmp:unquote($myJsonNode)/b,
     xdmp:unquote($myJsonNode)/c1,
     xdmp:unquote($myJsonNode)/d,
     xdmp:unquote($myJsonNode)/f,
     xdmp:unquote($myJsonNode)/g, :)
     $myInteger,
     $myDecimal,
     $myDouble,
     $myFloat
    )


Continued fix for http://sourceforge.net/tracker/index.php?func=detail&aid=1535514&group_id=17691&atid=317691 ([ 1535514 ] decoding resources names in webadmin interface)
Removal of binary doc now ok......

svn path=/trunk/eXist/; revision=5085

(:
    Module: display and browse collections.
:)
module namespace browse="http://exist-db.org/xquery/admin-interface/browse";

declare namespace request="http://exist-db.org/xquery/request";
declare namespace xdb="http://exist-db.org/xquery/xmldb";
declare namespace util="http://exist-db.org/xquery/util";

import module namespace date="http://exist-db.org/xquery/admin-interface/date" at "dates.xqm";

(:
    Main function: outputs the page.
:)
declare function browse:main($user as xs:string, $passwd as xs:string) as element() {
    let $colName := request:get-parameter("collection", "/db"),
        $collection := xdb:collection($colName, $user, $passwd)
    return

            { browse:process-action($collection) }

Browsing Collection: {xdb:decode-uri(xs:anyURI($colName))}

            <form method="POST" enctype="multipart/form-data">
                {
                    browse:display-collection($collection)
                }

<input type="submit" name="action" value="Remove Selected"/>

                        <input type="submit" name="action" value="Create Collection"/>
                        New collection:

                        <input type="text" name="create" size="40"/>

                        <input type="submit" name="action" value="Upload"/>
                        <input type="file" size="30" name="upload"/>
                        Store as:

                        <input type="text" name="name" size="20"/>

                <input type="hidden" name="collection" value="{$colName}"/>
                <input type="hidden" name="panel" value="browse"/>
            </form>

};

(:
    Process an action.
:)
declare function browse:process-action($collection as object) as element()* {
    let $action := request:get-parameter("action", ())
    return
        util:catch("java.lang.Exception",
            if($action eq "Remove Selected") then
                browse:remove()
            else if($action eq "Create Collection") then
                browse:create-collection($collection)
            else if($action eq "Store") then
                browse:store($collection)
            else if($action eq "Upload") then
                browse:upload($collection)
            else
                (),

                An error occurred while processing the action:

                {$util:exception-message}

        )
};

(:
    Store uploaded content.
:)
declare function browse:upload($collection as object) as element() {
    let $name := request:get-parameter("name", ()),
        $docName :=
            if($name) then $name
            else request:get-uploaded-file-name("upload"),
        $file := request:get-uploaded-file("upload")
    return

Actions:

Storing uploaded content to: {$docName}

                {
                    xdb:store($collection, $docName, $file)
                }

};

(:
    Store files from an URI.

    Allowing this opens a security whole as a user can
    upload arbitrary files on the server if the process is running
    as root.
:)
declare function browse:store($collection as object) as element() {
    let $uri := request:get-parameter("uri", ()),
        $path := if(starts-with($uri, "file:")) then $uri
            else concat("file:", $uri),
        $docName := request:get-parameter("name", ())
    return

Actions:

Storing resources from URI: {$path}

                {
                    xdb:store($collection, $docName, xs:anyURI($path))
                }

};

(:
    Remove a set of resources.
:)
declare function browse:remove() as element() {
    let $resources := request:get-parameter("resource", ())
    return

Actions:

                {
                    for $resource in $resources
                    return
                        browse:remove-resource($resource)
                }

};

(:
    Remove a resource.
:)
declare function browse:remove-resource($resource as xs:string) as element()* {
    let $isBinary := util:binary-doc-available($resource)
    let $doc := if ($isBinary) then $resource else doc($resource)
    return
        if($doc) then (

Removing document: {xdb:decode-uri(xs:anyURI($resource))} ...
,
            xdb:remove(util:collection-name($doc), util:document-name($doc))
        ) else (

Removing collection: {xdb:decode-uri(xs:anyURI($resource))} ...
,
            xdb:remove($resource)
        )
};

(:
    Create a collection.
:)
declare function browse:create-collection($parent as object) as element() {
    let $newcol := request:get-parameter("create", ())
    return

Actions:

            {
                if($newcol) then
                    let $col := xdb:create-collection($parent, $newcol)
                    return

Created collection: {util:collection-name($col)}.

                else

No name specified for new collection!

            }

};

(:
    Display the contents of a collection in a table view.
:)
declare function browse:display-collection($collection as object)
as element() {
    let $colName := util:collection-name($collection)
    return

                Name
                Permissions
                Owner
                Group
                Created
                Modified
                Size (KB)

                Up

            {
                browse:display-child-collections($collection),
                browse:display-child-resources($collection)
            }

};

declare function browse:display-child-collections($collection as object)
as element()* {
    let $parent := util:collection-name($collection)
    for $child in xdb:get-child-collections($collection)
    let $path := concat($parent, '/', $child),
        $created := xdb:created($path)
    order by $child
    return

            <input type="checkbox" name="resource" value="{$path}"/>
            {xdb:decode-uri(xs:anyURI($child))}
            {xdb:permissions-to-string(xdb:get-permissions($path))}
            {xdb:get-owner($path)}
            {xdb:get-group($path)}
            {date:format-dateTime($created)}

};

declare function browse:display-child-resources($collection as object)
as element()* {
    let $parent := util:collection-name($collection)
    for $child in xdb:get-child-resources($collection)
    order by $child
    return

            <input type="checkbox" name="resource" value="{$parent}/{$child}"/>
            {xdb:decode-uri(xs:anyURI($child))}
            {xdb:permissions-to-string(xdb:get-permissions($collection, $child))}
            {xdb:get-owner($collection, $child)}
            {xdb:get-group($collection, $child)}
            {date:format-dateTime(xdb:created($collection, $child))}
            {date:format-dateTime(xdb:last-modified($collection, $child))}
            {fn:ceiling(xdb:size($collection, $child) div 1024)}

};

(:
    Get the name of the parent collection from a specified collection path.
:)
declare function browse:get-parent-collection($path as xs:string) as xs:string {
    if($path eq "/db") then
        $path
    else
        replace($path, "/[^/]*$", "")
};

(:
    Module: display and browse collections.
:)
module namespace browse="http://exist-db.org/xquery/admin-interface/browse";

declare namespace request="http://exist-db.org/xquery/request";
declare namespace xdb="http://exist-db.org/xquery/xmldb";
declare namespace util="http://exist-db.org/xquery/util";

import module namespace date="http://exist-db.org/xquery/admin-interface/date" at "dates.xqm";

(:
    Main function: outputs the page.
:)
declare function browse:main($user as xs:string, $passwd as xs:string) as element() {
    let $colName := request:get-parameter("collection", "/db"),
        $collection := xdb:collection($colName, $user, $passwd)
    return

            { browse:process-action($collection) }

Browsing Collection: {xdb:decode-uri(xs:anyURI($colName))}

            <form method="POST" enctype="multipart/form-data">
                {
                    browse:display-collection($collection)
                }

<input type="submit" name="action" value="Remove Selected"/>

                        <input type="submit" name="action" value="Create Collection"/>
                        New collection:

                        <input type="text" name="create" size="40"/>

                        <input type="submit" name="action" value="Upload"/>
                        <input type="file" size="30" name="upload"/>
                        Store as:

                        <input type="text" name="name" size="20"/>

                <input type="hidden" name="collection" value="{$colName}"/>
                <input type="hidden" name="panel" value="browse"/>
            </form>

};

(:
    Process an action.
:)
declare function browse:process-action($collection as object) as element()* {
    let $action := request:get-parameter("action", ())
    return
        util:catch("java.lang.Exception",
            if($action eq "Remove Selected") then
                browse:remove()
            else if($action eq "Create Collection") then
                browse:create-collection($collection)
            else if($action eq "Store") then
                browse:store($collection)
            else if($action eq "Upload") then
                browse:upload($collection)
            else
                (),

                An error occurred while processing the action:

                {$util:exception-message}

        )
};

(:
    Store uploaded content.
:)
declare function browse:upload($collection as object) as element() {
    let $name := request:get-parameter("name", ()),
        $docName :=
            if($name) then $name
            else request:get-uploaded-file-name("upload"),
        $file := request:get-uploaded-file("upload")
    return

Actions:

Storing uploaded content to: {$docName}

                {
                    xdb:store($collection, $docName, $file)
                }

};

(:
    Store files from an URI.

    Allowing this opens a security whole as a user can
    upload arbitrary files on the server if the process is running
    as root.
:)
declare function browse:store($collection as object) as element() {
    let $uri := request:get-parameter("uri", ()),
        $path := if(starts-with($uri, "file:")) then $uri
            else concat("file:", $uri),
        $docName := request:get-parameter("name", ())
    return

Actions:

Storing resources from URI: {$path}

                {
                    xdb:store($collection, $docName, xs:anyURI($path))
                }

};

(:
    Remove a set of resources.
:)
declare function browse:remove() as element() {
    let $resources := request:get-parameter("resource", ())
    return

Actions:

                {
                    for $resource in $resources
                    return
                        browse:remove-resource($resource)
                }

};

(:
    Remove a resource.
:)
declare function browse:remove-resource($resource as xs:string) as element()* {
    let $isBinary := util:binary-doc-available($resource)
    let $doc := if ($isBinary) then $resource else doc($resource)
    return
        if($doc) then (

Removing document: {$resource} ...
,
            xdb:remove(util:collection-name($doc), util:document-name($doc))
        ) else (

Removing collection: {xdb:decode-uri(xs:anyURI($resource))} ...
,
            xdb:remove($resource)
        )
};

(:
    Create a collection.
:)
declare function browse:create-collection($parent as object) as element() {
    let $newcol := request:get-parameter("create", ())
    return

Actions:

            {
                if($newcol) then
                    let $col := xdb:create-collection($parent, $newcol)
                    return

Created collection: {util:collection-name($col)}.

                else

No name specified for new collection!

            }

};

(:
    Display the contents of a collection in a table view.
:)
declare function browse:display-collection($collection as object)
as element() {
    let $colName := util:collection-name($collection)
    return

                Name
                Permissions
                Owner
                Group
                Created
                Modified
                Size (KB)

                Up

            {
                browse:display-child-collections($collection),
                browse:display-child-resources($collection)
            }

};

declare function browse:display-child-collections($collection as object)
as element()* {
    let $parent := util:collection-name($collection)
    for $child in xdb:get-child-collections($collection)
    let $path := concat($parent, '/', $child),
        $created := xdb:created($path)
    order by $child
    return

            <input type="checkbox" name="resource" value="{$path}"/>
            {xdb:decode-uri(xs:anyURI($child))}
            {xdb:permissions-to-string(xdb:get-permissions($path))}
            {xdb:get-owner($path)}
            {xdb:get-group($path)}
            {date:format-dateTime($created)}

};

declare function browse:display-child-resources($collection as object)
as element()* {
    let $parent := util:collection-name($collection)
    for $child in xdb:get-child-resources($collection)
    order by $child
    return

            <input type="checkbox" name="resource" value="{$parent}/{$child}"/>
            {xdb:decode-uri(xs:anyURI($child))}
            {xdb:permissions-to-string(xdb:get-permissions($collection, $child))}
            {xdb:get-owner($collection, $child)}
            {xdb:get-group($collection, $child)}
            {date:format-dateTime(xdb:created($collection, $child))}
            {date:format-dateTime(xdb:last-modified($collection, $child))}
            {fn:ceiling(xdb:size($collection, $child) div 1024)}

};

(:
    Get the name of the parent collection from a specified collection path.
:)
declare function browse:get-parent-collection($path as xs:string) as xs:string {
    if($path eq "/db") then
        $path
    else
        replace($path, "/[^/]*$", "")
};


More documentation.

More documentation.

(:
 : Copyright 2006-2009 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
 :)

xquery version "3.0";

 (:~
  : Using this module, you can parse JSON data into XML, manipulate it like any
  : other XML data using XQuery, and serialize the result back as JSON.
  :
  : There are many ways to represent JSON data in XML, some loss-less ("round
  : tripable") and some lossy ("one way").  Loss-less representations preserve
  : the JSON data types boolean, number, and null; lossy
  : representations convert all data to strings.
  :

  : For a loss-less representation, Zorba implements that proposed by
  : John Snelson.
  : For example:
  :

  :   {
  :     "firstName" : "John",
  :     "lastName" : "Smith",
  :     "address" : {
  :       "streetAddress" : "21 2nd Street",
  :       "city" : "New York",
  :       "state" : "NY",
  :       "postalCode" : 10021
  :     },
  :     "phoneNumbers" : [ "212 732-1234", "646 123-4567" ]
  :   }
  :

  : would be represented as:
  :

  :   <json type="object">
  :     <pair name="firstName" type="string">John</pair>
  :     <pair name="lastName" type="string">Smith</pair>
  :     <pair name="address" type="object">
  :       <pair name="streetAddress" type="string">21 2nd Street</pair>
  :       <pair name="city" type="string">New York</pair>
  :       <pair name="state" type="string">NY</pair>
  :       <pair name="postalCode" type="number">10021</pair>
  :     </pair>
  :     <pair name="phoneNumbers" type="array">
  :       <item type="string">212 732-1234</item>
  :       <item type="string">646 123-4567</item>
  :     </pair>
  :   </json>
  :

  : For a lossy representation, Zorba implements
  : JsonML (the array form).
  : For example:
  :

  :   [ "person",
  :     { "created" : "2006-11-11T19:23",
  :       "modified" : "2006-12-31T23:59" },
  :     [ "firstName", "Robert" ],
  :     [ "lastName", "Smith" ],
  :     [ "address",
  :       { "type" : "home" },
  :       [ "street", "12345 Sixth Ave" ],
  :       [ "city", "Anytown" ],
  :       [ "state", "CA" ],
  :       [ "postalCode", "98765-4321" ]
  :     ]
  :   ]
  :

  : would be represented as:
  :

  :   <person created="2006-11-11T19:23" modified="2006-12-31T23:59">
  :     <firstName>Robert</firstName>
  :     <lastName>Smith</lastName>
  :     <address type="home">
  :       <street>12345 Sixth Ave</street>
  :       <city>Anytown</city>
  :       <state>CA</state>
  :       <postalCode>98765-4321</postalCode>
  :     </address>
  :   </person>
  :

  :
  : @author Paul J. Lucas
  : @project data processing/data converters
  :)
module namespace json = "http://www.zorba-xquery.com/modules/converters/json";

import module namespace schema = "http://www.zorba-xquery.com/modules/schema";

import schema namespace json-options =
  "http://www.zorba-xquery.com/modules/converters/json-options";

declare namespace ann = "http://www.zorba-xquery.com/annotations";
declare namespace err = "http://www.w3.org/2005/xqt-errors";
declare namespace zerr = "http://www.zorba-xquery.com/errors";

declare namespace ver = "http://www.zorba-xquery.com/options/versioning";
declare option ver:module-version "2.0";

(:~
 : Parses JSON data from a string and returns an XDM instance using one of the
 : representations described above.
 :
 : @param $json The JSON data to parse.
 : @param $options The parsing options.
 : @return said XDM instance.
 : @error err:XQDY0027 if $options can not be validated against the
 : json-options schema.
 : @error zerr:ZJPE0001 if $json contains an illegal JSON character.
 : @error zerr:ZJPE0002 if $json contains an illegal Unicode code-point.
 : @error zerr:ZJPE0003 if $json contains an illegal JSON character escape.
 : @error zerr:ZJPE0004 if $json contains an illegal JSON literal.
 : @error zerr:ZJPE0005 if $json contains an illegal JSON number.
 : @error zerr:ZJPE0006 if $json is not a valid JSON string.
 : @error zerr:ZJPE0007 if $json contains an unterminated string.
 : @error zerr:ZJPE0008 if $json contains an illegal QName.
 : @example test/rbkt/Queries/zorba/json/json-jsonml_array-parse-01.xq
 :)
declare function json:parse(
  $json as xs:string?,
  $options as element(json-options:options)
) as element(*,xs:untyped)*
{
  let $validated-options := if ( schema:is-validated( $options ) ) then
                              $options
                            else
                              validate { $options }
  return json:parse-internal( $json, $validated-options )
};

(:~
 : Parses JSON data from a string and returns an XDM instance using the Snelson
 : representation described above.
 :
 : @param $json The JSON data to parse.
 : @return said XDM instance.
 : @error zerr:ZJPE0001 if $json contains an illegal JSON character.
 : @error zerr:ZJPE0002 if $json contains an illegal Unicode code-point.
 : @error zerr:ZJPE0003 if $json contains an illegal JSON character escape.
 : @error zerr:ZJPE0004 if $json contains an illegal JSON literal.
 : @error zerr:ZJPE0005 if $json contains an illegal JSON number.
 : @error zerr:ZJPE0006 if $json is not a valid JSON string.
 : @error zerr:ZJPE0007 if $json contains an unterminated string.
 : @error zerr:ZJPE0008 if $json contains an illegal QName.
 : @example test/rbkt/Queries/zorba/json/json-snelson-parse-array-01.xq
 :)
declare function json:parse(
  $json as xs:string?
) as element(*,xs:untyped)*
{
  json:parse-internal(
    $json,
    validate {
      <options xmlns="http://www.zorba-xquery.com/modules/converters/json-options" >
        <json-format value="Snelson"/>
      </options>
    }
  )
};

(:~
 : Serializes an XDM into JSON using one of the representations described
 : above.
 :
 : @param $xml The XDM to serialize.
 : @param $options The serializing options.
 : @return a JSON string.
 : @error err:XQDY0027 if $options can not be validated against the
 : json-options schema.
 : @error zerr:ZJSE0001 if $xml is not a document or element node.
 : @error zerr:ZJSE0002 if $xml contains an element that is missing a required
 : attribute.
 : @error zerr:ZJSE0003 if $xml contains an attribute having an illegal value.
 : @error zerr:ZJSE0004 if $xml contains an illegal element.
 : @error zerr:ZJSE0005 if $xml contains an illegal child element for a JSON
 : type.
 : @error zerr:ZJSE0006 if $xml contains an illegal child element.
 : @error zerr:ZJSE0007 if $xml contains an illegal text node.
 : @error zerr:ZJSE0008 if $xml contains an illegal value for a JSON type.
 : @example test/rbkt/Queries/zorba/json/json-jsonml_array-serialize-01.xq
 :)
declare function json:serialize(
  $xml as item()*,
  $options as element(json-options:options)
) as xs:string
{
  let $validated-options := if ( schema:is-validated( $options ) ) then
                              $options
                            else
                              validate { $options }
  return json:serialize-internal( $xml, $validated-options )
};

(:~
 : Serializes an XDM into JSON using one of the representations described
 : above.
 :
 : @param $xml The XDM to serialize.
 : @return a JSON string.
 : @error zerr:ZJSE0001 if $xml is not a document or element node.
 : @error zerr:ZJSE0002 if $xml contains an element that is missing a required
 : attribute.
 : @error zerr:ZJSE0003 if $xml contains an attribute having an illegal value.
 : @error zerr:ZJSE0004 if $xml contains an illegal element.
 : @error zerr:ZJSE0005 if $xml contains an illegal child element for a JSON
 : type.
 : @error zerr:ZJSE0006 if $xml contains an illegal child element.
 : @error zerr:ZJSE0007 if $xml contains an illegal text node.
 : @error zerr:ZJSE0008 if $xml contains an illegal value for a JSON type.
 : @example test/rbkt/Queries/zorba/json/json-snelson-serialize-array-01.xq
 :)
declare function json:serialize(
  $xml as item()*
) as xs:string
{
  json:serialize-internal($xml,
    validate {
      <options xmlns="http://www.zorba-xquery.com/modules/converters/json-options" >
        <json-format value="Snelson"/>
      </options>
   }
 )
};

(:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::)

declare %private function json:parse-internal(
  $json as xs:string?,
  $options as item()?
) as element()* external;

declare %ann:streamable %private function json:serialize-internal(
  $xml as item()*,
  $options as item()?
) as xs:string external;

(: vim:set et sw=2 ts=2: :)

(:
 : Copyright 2006-2009 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
 :)

xquery version "3.0";

 (:~
  : Using this module, you can parse JSON data into XML, manipulate it like any
  : other XML data using XQuery, and serialize the result back as JSON.
  :
  : There are many ways to represent JSON data in XML, some loss-less ("round
  : tripable") and some lossy ("one way").  Loss-less representations preserve
  : the JSON data types boolean, number, and null; lossy
  : representations convert all data to strings.
  :
  : For a loss-less representation, Zorba implements that proposed by
  : John Snelson.
  : For example:
  :

  :   <json type="object">
  :     <pair name="firstName" type="string">John</pair>
  :     <pair name="lastName" type="string">Smith</pair>
  :     <pair name="address" type="object">
  :       <pair name="streetAddress" type="string">21 2nd Street</pair>
  :       <pair name="city" type="string">New York</pair>
  :       <pair name="state" type="string">NY</pair>
  :       <pair name="postalCode" type="number">10021</pair>
  :     </pair>
  :     <pair name="phoneNumbers" type="array">
  :       <item type="string">212 732-1234</item>
  :       <item type="string">646 123-4567</item>
  :     </pair>
  :   </json>
  :

  : For a lossy representation, Zorba implements
  : JsonML (the array form).
  : For example:
  :

  :   <person created="2006-11-11T19:23" modified="2006-12-31T23:59">
  :     <firstName>Robert</firstName>
  :     <lastName>Smith</lastName>
  :     <address type="home">
  :       <street>12345 Sixth Ave</street>
  :       <city>Anytown</city>
  :       <state>CA</state>
  :       <postalCode>98765-4321</postalCode>
  :     </address>
  :   </person>
  :

  :
  : @author Paul J. Lucas
  : @project data processing/data converters
  :)
module namespace json = "http://www.zorba-xquery.com/modules/converters/json";

import module namespace schema = "http://www.zorba-xquery.com/modules/schema";

import schema namespace json-options =
  "http://www.zorba-xquery.com/modules/converters/json-options";

declare namespace ann = "http://www.zorba-xquery.com/annotations";
declare namespace err = "http://www.w3.org/2005/xqt-errors";
declare namespace zerr = "http://www.zorba-xquery.com/errors";

declare namespace ver = "http://www.zorba-xquery.com/options/versioning";
declare option ver:module-version "2.0";

(:~
 : Parses JSON data from a string and returns an XDM instance using one of the
 : representations described above.
 :
 : @param $json The JSON data to parse.
 : @param $options The parsing options.
 : @return said XDM instance.
 : @error err:XQDY0027 if $options can not be validated against the
 : json-options schema.
 : @error zerr:ZJPE0001 if $json contains an illegal JSON character.
 : @error zerr:ZJPE0002 if $json contains an illegal Unicode code-point.
 : @error zerr:ZJPE0003 if $json contains an illegal JSON character escape.
 : @error zerr:ZJPE0004 if $json contains an illegal JSON literal.
 : @error zerr:ZJPE0005 if $json contains an illegal JSON number.
 : @error zerr:ZJPE0006 if $json is not a valid JSON string.
 : @error zerr:ZJPE0007 if $json contains an unterminated string.
 : @error zerr:ZJPE0008 if $json contains an illegal QName.
 : @example test/rbkt/Queries/zorba/json/json-jsonml_array-parse-01.xq
 :)
declare function json:parse(
  $json as xs:string?,
  $options as element(json-options:options)
) as element(*,xs:untyped)*
{
  let $validated-options := if ( schema:is-validated( $options ) ) then
                              $options
                            else
                              validate { $options }
  return json:parse-internal( $json, $validated-options )
};

(:~
 : Parses JSON data from a string and returns an XDM instance using the Snelson
 : representation described above.
 :
 : @param $json The JSON data to parse.
 : @return said XDM instance.
 : @error zerr:ZJPE0001 if $json contains an illegal JSON character.
 : @error zerr:ZJPE0002 if $json contains an illegal Unicode code-point.
 : @error zerr:ZJPE0003 if $json contains an illegal JSON character escape.
 : @error zerr:ZJPE0004 if $json contains an illegal JSON literal.
 : @error zerr:ZJPE0005 if $json contains an illegal JSON number.
 : @error zerr:ZJPE0006 if $json is not a valid JSON string.
 : @error zerr:ZJPE0007 if $json contains an unterminated string.
 : @error zerr:ZJPE0008 if $json contains an illegal QName.
 : @example test/rbkt/Queries/zorba/json/json-snelson-parse-array-01.xq
 :)
declare function json:parse(
  $json as xs:string?
) as element(*,xs:untyped)*
{
  json:parse-internal(
    $json,
    validate {
      <options xmlns="http://www.zorba-xquery.com/modules/converters/json-options" >
        <json-format value="Snelson"/>
      </options>
    }
  )
};

(:~
 : Serializes an XDM into JSON using one of the representations described
 : above.
 :
 : @param $xml The XDM to serialize.
 : @param $options The serializing options.
 : @return a JSON string.
 : @error err:XQDY0027 if $options can not be validated against the
 : json-options schema.
 : @error zerr:ZJSE0001 if $xml is not a document or element node.
 : @error zerr:ZJSE0002 if $xml contains an element that is missing a required
 : attribute.
 : @error zerr:ZJSE0003 if $xml contains an attribute having an illegal value.
 : @error zerr:ZJSE0004 if $xml contains an illegal element.
 : @error zerr:ZJSE0005 if $xml contains an illegal child element for a JSON
 : type.
 : @error zerr:ZJSE0006 if $xml contains an illegal child element.
 : @error zerr:ZJSE0007 if $xml contains an illegal text node.
 : @error zerr:ZJSE0008 if $xml contains an illegal value for a JSON type.
 : @example test/rbkt/Queries/zorba/json/json-jsonml_array-serialize-01.xq
 :)
declare function json:serialize(
  $xml as item()*,
  $options as element(json-options:options)
) as xs:string
{
  let $validated-options := if ( schema:is-validated( $options ) ) then
                              $options
                            else
                              validate { $options }
  return json:serialize-internal( $xml, $validated-options )
};

(:~
 : Serializes an XDM into JSON using one of the representations described
 : above.
 :
 : @param $xml The XDM to serialize.
 : @return a JSON string.
 : @error zerr:ZJSE0001 if $xml is not a document or element node.
 : @error zerr:ZJSE0002 if $xml contains an element that is missing a required
 : attribute.
 : @error zerr:ZJSE0003 if $xml contains an attribute having an illegal value.
 : @error zerr:ZJSE0004 if $xml contains an illegal element.
 : @error zerr:ZJSE0005 if $xml contains an illegal child element for a JSON
 : type.
 : @error zerr:ZJSE0006 if $xml contains an illegal child element.
 : @error zerr:ZJSE0007 if $xml contains an illegal text node.
 : @error zerr:ZJSE0008 if $xml contains an illegal value for a JSON type.
 : @example test/rbkt/Queries/zorba/json/json-snelson-serialize-array-01.xq
 :)
declare function json:serialize(
  $xml as item()*
) as xs:string
{
  json:serialize-internal($xml,
    validate {
      <options xmlns="http://www.zorba-xquery.com/modules/converters/json-options" >
        <json-format value="Snelson"/>
      </options>
   }
 )
};

(:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::)

declare %private function json:parse-internal(
  $json as xs:string?,
  $options as item()?
) as element()* external;

declare %ann:streamable %private function json:serialize-internal(
  $xml as item()*,
  $options as item()?
) as xs:string external;

(: vim:set et sw=2 ts=2: :)