[apidoc] Filter the xquery/javascript descriptions.

(:
 : Copyright (C) 2020 Reece H. Dunn
 :
 : 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.1";

declare namespace apidoc = "http://marklogic.com/xdmp/apidoc";
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare namespace xhtml = "http://www.w3.org/1999/xhtml";

declare variable $marklogic-version as xs:string external;

declare variable $language as xs:string external := "xquery"; (: xquery | javascript | rest :)
declare variable $namespace as xs:string external;
declare variable $local-name as xs:string external;

declare variable $apidoc as element(apidoc:apidoc) :=
    typeswitch (.)
    case xs:string return fn:parse-xml(.)/element()
    case document-node() return ./element()
    default return .;

(: documentation helpers --------------------------------------------------- :)

declare function local:apidoc-language($doc as element()) as xs:string {
    ($doc/@class, "xquery")[1]
};

declare function local:prefix($namespaces as map(*), $namespace as xs:string) as xs:string {
    for $prefix in map:keys($namespaces)
    where map:get($namespaces, $prefix) = $namespace
    return $prefix
};

declare function local:documentation-html($doc as node()) as node()* {
    if ($doc/namespace-uri() = "http://marklogic.com/xdmp/apidoc") then
        let $first :=
            let $node := $doc/node()[position() = 1]
            return if ($node instance of text() and string-length($node/normalize-space()) != 0) then
                ()
            else
                $node
        return if (exists($first) and (some $child in $doc/node() satisfies $child instance of element())) then
            if (local:apidoc-language($doc) = $language) then
                for $child in $doc/node()
                return local:documentation-html($child)
            else ()
        else

{$doc/node()}

    else if ($doc instance of text()) then
        let $text := $doc/normalize-space()
        return if ($text != "") then
            $doc
        else ()
    else if ($doc instance of element() and local:apidoc-language($doc) = $language) then
        element { $doc/local-name() } {
            for $child in $doc/node()
            return local:documentation-html($child)
        }
    else ()
};

(: namespaces -------------------------------------------------------------- :)

declare variable $module-namespaces := map:merge(
    for $import in $apidoc//apidoc:module/apidoc:summary//(*:p|*:pre)
    let $match := fn:analyze-string(($import/*:code, $import)[1], "import module (namespace\s*([a-z0-9]+)\s*=\s*)?""([^""]+)""")/fn:match[1]
    let $prefix := $import/ancestor::apidoc:module/@lib
    let $namespace := $match/fn:group[@nr = "3"]/string()
    where exists($namespace)
    return map:entry($prefix, $namespace)
);

declare variable $xquery-namespaces := map {
    "local": "http://www.w3.org/2005/xquery-local-functions",
    "fn": "http://www.w3.org/2005/xpath-functions",
    "xml": "http://www.w3.org/XML/1998/namespace",
    "xs": "http://www.w3.org/2001/XMLSchema",
    "xsi": "http://www.w3.org/2001/XMLSchema-instance"
};

declare variable $marklogic-5-builtin := map {
    "cts": "http://marklogic.com/cts",
    "dbg": "http://marklogic.com/xdmp/dbg",
    "dir": "http://marklogic.com/xdmp/directory",
    "error": "http://marklogic.com/xdmp/error",
    "map": "http://marklogic.com/xdmp/map",
    "math": "http://marklogic.com/xdmp/math",
    "prof": "http://marklogic.com/xdmp/profile",
    "sec": "http://marklogic.com/security",
    "spell": "http://marklogic.com/xdmp/spell",
    "xdmp": "http://marklogic.com/xdmp"
};

declare variable $marklogic-6-builtin := map {
    "err": "http://www.w3.org/2005/xqt-error"
};

declare variable $marklogic-7-builtin := map {
    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
    "sc": "http://marklogic.com/xdmp/schema-components",
    "sql": "http://marklogic.com/xdmp/sql"
};

declare variable $marklogic-10-builtin := map {
    "cntk": "http://marklogic.com/cntk",
    "ort": "http://marklogic.com/onnxruntime"
};

declare variable $marklogic-additional := map {
    "as": "http://marklogic.com/xdmp/assignments",
    "db": "http://marklogic.com/xdmp/database",
    "dg": "xdmp:document-get",
    "cl": "http://marklogic.com/xdmp/clusters",
    "exsl": "http://exslt.org/common",
    "extusr": "http://marklogic.com/xdmp/external/user",
    "gr": "http://marklogic.com/xdmp/group",
    "lang": "http://marklogic.com/xdmp/language",
    "list": "http://marklogic.com/manage/package/list",
    "md": "urn:oasis:names:tc:SAML:2.0:metadata",
    "mt": "http://marklogic.com/xdmp/mimetypes",
    "pkg": "http://marklogic.com/manage/package",
    "s": "http://www.w3.org/2009/xpath-functions/analyze-string",
    "sch": "http://purl.oclc.org/dsdl/schematron",
    "x509": "http://marklogic.com/xdmp/x509",
    "xdt": "http://www.w3.org/2004/10/xpath-datatypes",
    "xi": "http://www.w3.org/2001/XInclude"
};

declare variable $namespaces := map:merge((
    $module-namespaces,
    $xquery-namespaces,
    $marklogic-5-builtin,
    $marklogic-6-builtin,
    $marklogic-7-builtin,
    $marklogic-10-builtin,
    $marklogic-additional,
    ()
));

(: functions --------------------------------------------------------------- :)

(: Several functions in the apidocs have duplicate parameter names in some versions, so fix these. :)
declare variable $param-fixes := map {
    "fn:subtract-dateTimes-yielding-dayTimeDuration": ("srcval1", "srcval2"),
    "fn:subtract-dateTimes-yielding-yearMonthDuration": ("srcval1", "srcval2"),
    "geo:region-contains": ("target", "region", "options"),
    "math:fmod": ("x", "y"),
    "math:rank": ("arg1", "arg2", "options")
};

declare variable $functions as element(apidoc:function)* := $apidoc//apidoc:function[
    local:function-language(.) = $language and not(@name = "")
];

declare function local:function-uri($function as element(apidoc:function)) as xs:string {
    ``[http://docs.marklogic.com/`{$marklogic-version}`/`{local:function-name($function)}`]``
};

declare function local:function-language($function as element(apidoc:function)) as xs:string {
    let $bucket := $function/@bucket
    return switch ($bucket)
    case "REST Resources API" return "rest"
    default return local:apidoc-language($function)
};

declare function local:function-name($function as element(apidoc:function)) as xs:string {
    ``[`{$function/@lib}`:`{$function/@name}`]``
};

declare function local:function-parameters($function as element(apidoc:function)) as xs:string* {
    let $names as xs:string* :=
        let $names := map:get($param-fixes, local:function-name($function))
        return if (exists($names)) then
            $names
        else
            $function/apidoc:params/apidoc:param[local:apidoc-language(.) = $language]/@name/string()
    return if (count($names) != count(distinct-values($names))) then
        fn:error(xs:QName("err:XQST0039"),``[Duplicate parameter name in `{local:function-name($function)}`]``)
    else
        $names
};

declare function local:function-arity($function as element(apidoc:function)) as map(*) {
    let $params := $function/apidoc:params/apidoc:param[local:apidoc-language(.) = $language]
    return map {
        "min": count($params[not(@optional = "true")]),
        "max": count($params)
    }
};

declare function local:function-parameter($function as element(apidoc:function), $name as xs:string) as element(apidoc:param) {
    let $names := map:get($param-fixes, local:function-name($function))
    return if (exists($names)) then
        let $i := fn:index-of($names, $name)
        return $function/apidoc:params/apidoc:param[$i]
    else
        $function/apidoc:params/apidoc:param[@name = $name and local:apidoc-language(.) = $language]
};

declare function local:function-parameter-type($function as element(apidoc:function), $name as xs:string) as xs:string {
    let $type := local:function-parameter($function, $name)/@type/string()
    return switch ($type)
    case "atomic type" return "item()*"
    case "xs:string)" return "xs:string"
    case "xs:dateTime? | xs:date? | xs:string?" return "(xs:dateTime?|xs:date?|xs:string?)"
    case "binary())" return "binary()"
    case "element(sec:query-rolesets)*)" return "element(sec:query-rolesets)*"
    case "function()" return "function(*)"
    case "item()* | map:map" return "(item()*|map:map)"
    case "(binary()|node())*" return "(binary()*|node()*)"
    case "(binary()|node())?" return "(binary()?|node()?)"
    case "(element()|map:map)?" case "element()?|map:map?" return "(element()?|map:map?)"
    case "(node()? | map:map)?" return "(node()?|map:map?)"
    case "(cts:order|xs:string)*" return "(cts:order*|xs:string*)"
    case "[xs:string?]" return "xs:string?"
    default return replace($type, ",\.\.\.", "...")
};

declare function local:function-return-type($function as element(apidoc:function)) as xs:string {
    let $type := normalize-space($function/apidoc:return[local:apidoc-language(.) = $language])
    return switch ($type)
    case "An XML representation of the certificate." return "element()?"
    case "element())" case "Element()" return "element()"
    case "element()|map:map" return "(element()|map:map)"
    case "element(xsd:schema)*" return "element(xs:schema)*"
    case "empty-sequence" case "empty sequence" return "empty-sequence()"
    case "xs:string)" return "xs:string"
    case "xs:unsignedLong)" return "xs:unsignedLong"
    case "pkgins:install($pkgname)" return "element(pkg:install-status)"
    case "pkgins:revert($ticket)" return "element(pkg:revert-status)"
    case "query-results-serialize($results)" return "item()*"
    case "The return data type is the data type of the date argument" return "item()"
    case "" return
        switch (local:function-name($function))
        case "admin:appserver-set-output-indent-tabs" return "element(configuration)"
        default return ""
    default return $type
};

declare function local:function-xquery-signatures($function as element(apidoc:function)) as xs:string* {
    let $name := local:function-name($function)
    let $params :=
        for $name in local:function-parameters($function)
        let $type := local:function-parameter-type($function, $name)
        return ``[$`{$name}` as `{$type}`]``
    let $type := local:function-return-type($function)
    let $arity-range := local:function-arity($function)
    for $arity in $arity-range?min to $arity-range?max
    let $params := $params[position() = 1 to $arity]
    return if ($type = "") then
        ``[declare function `{$name}`(`{string-join($params, ", ")}`) external;]``
    else
        ``[declare function `{$name}`(`{string-join($params, ", ")}`) as `{$type}` external;]``
};

declare function local:function-html-signatures($function as element(apidoc:function)) as element(div)+ {
    let $name := local:function-name($function)
    let $params :=
        for $name in local:function-parameters($function)
        let $type := local:function-parameter-type($function, $name)
        return

            ${$name},
             as ,
            {$type}

    let $type := local:function-return-type($function)
    let $arity-range := local:function-arity($function)
    for $arity in $arity-range?min to $arity-range?max
    let $params := $params[position() = 1 to $arity]
    return if (count($params) <= 2) then

            {$name}({
                for $param at $i in $params
                return (
                    if ($i > 1) then ", " else (),
                    $param/*
                )
            }){
                if ($type != "") then (
                     as ,
                    xs:double
                ) else ()
            }

    else

{
                (: JEditorPanel does not support vertical-align on tr/td elements, so use valign instead. :)

                    {$name}(
                    {$params[1]/code[@class="arg"]}
                    {$params[1]/code[not(@class="arg")]},

,
                for $param at $i in $params[position() > 1]
                return

                        {$param/code[@class="arg"]}
                        {
                            $param/code[not(@class="arg")],
                            if ($i = count($params) - 1) then ")" else ","
                        }

            }

};

declare function local:function-html-parameters($function as element(apidoc:function)) as element(table)? {
    let $params := local:function-parameters($function)
    return if (exists($params)) then

{
            for $name in local:function-parameters($function)
            let $type := local:function-parameter-type($function, $name)
            let $summary := local:function-parameter($function, $name)
            return

                    ${$name}
                    {$summary/node()}

        }

    else ()
};

(: query ------------------------------------------------------------------- :)

let $prefix := local:prefix($namespaces, $namespace)
for $function in $functions
where $function/@lib = $prefix and $function/@name = $local-name
return (
    local:function-uri($function),
    fn:serialize(local:documentation-html($function/apidoc:summary)),
    string-join(local:function-html-signatures($function) ! fn:serialize(.), ""),
    fn:serialize(local:function-html-parameters($function)),
    fn:serialize($function/apidoc:privilege ! local:documentation-html(.)),
    fn:serialize($function/apidoc:usage ! local:documentation-html(.)),
    fn:serialize($function/apidoc:example ! local:documentation-html(.)),
    ()
)

(:
 : Copyright (C) 2020 Reece H. Dunn
 :
 : 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.1";

declare namespace apidoc = "http://marklogic.com/xdmp/apidoc";
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
declare namespace xhtml = "http://www.w3.org/1999/xhtml";

declare variable $marklogic-version as xs:string external;

declare variable $language as xs:string external := "xquery"; (: xquery | javascript | rest :)
declare variable $namespace as xs:string external;
declare variable $local-name as xs:string external;

declare variable $apidoc as element(apidoc:apidoc) :=
    typeswitch (.)
    case xs:string return fn:parse-xml(.)/element()
    case document-node() return ./element()
    default return .;

(: documentation helpers --------------------------------------------------- :)

declare function local:apidoc-language($doc as element()) as xs:string {
    ($doc/@class, "xquery")[1]
};

declare function local:prefix($namespaces as map(*), $namespace as xs:string) as xs:string {
    for $prefix in map:keys($namespaces)
    where map:get($namespaces, $prefix) = $namespace
    return $prefix
};

declare function local:documentation-html($doc as node()) as node()* {
    if ($doc/namespace-uri() = "http://marklogic.com/xdmp/apidoc") then
        let $first :=
            let $node := $doc/node()[position() = 1]
            return if ($node instance of text() and string-length($node/normalize-space()) != 0) then
                ()
            else
                $node
        return if (exists($first) and (some $child in $doc/node() satisfies $child instance of element())) then
            for $child in $doc/node()
            return local:documentation-html($child)
        else

{$doc/node()}

    else if ($doc instance of text()) then
        let $text := $doc/normalize-space()
        return if ($text != "") then
            $doc
        else ()
    else if ($doc instance of element()) then
        element { $doc/local-name() } {
            for $child in $doc/node()
            return local:documentation-html($child)
        }
    else ()
};

(: namespaces -------------------------------------------------------------- :)

declare variable $module-namespaces := map:merge(
    for $import in $apidoc//apidoc:module/apidoc:summary//(*:p|*:pre)
    let $match := fn:analyze-string(($import/*:code, $import)[1], "import module (namespace\s*([a-z0-9]+)\s*=\s*)?""([^""]+)""")/fn:match[1]
    let $prefix := $import/ancestor::apidoc:module/@lib
    let $namespace := $match/fn:group[@nr = "3"]/string()
    where exists($namespace)
    return map:entry($prefix, $namespace)
);

declare variable $xquery-namespaces := map {
    "local": "http://www.w3.org/2005/xquery-local-functions",
    "fn": "http://www.w3.org/2005/xpath-functions",
    "xml": "http://www.w3.org/XML/1998/namespace",
    "xs": "http://www.w3.org/2001/XMLSchema",
    "xsi": "http://www.w3.org/2001/XMLSchema-instance"
};

declare variable $marklogic-5-builtin := map {
    "cts": "http://marklogic.com/cts",
    "dbg": "http://marklogic.com/xdmp/dbg",
    "dir": "http://marklogic.com/xdmp/directory",
    "error": "http://marklogic.com/xdmp/error",
    "map": "http://marklogic.com/xdmp/map",
    "math": "http://marklogic.com/xdmp/math",
    "prof": "http://marklogic.com/xdmp/profile",
    "sec": "http://marklogic.com/security",
    "spell": "http://marklogic.com/xdmp/spell",
    "xdmp": "http://marklogic.com/xdmp"
};

declare variable $marklogic-6-builtin := map {
    "err": "http://www.w3.org/2005/xqt-error"
};

declare variable $marklogic-7-builtin := map {
    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
    "sc": "http://marklogic.com/xdmp/schema-components",
    "sql": "http://marklogic.com/xdmp/sql"
};

declare variable $marklogic-10-builtin := map {
    "cntk": "http://marklogic.com/cntk",
    "ort": "http://marklogic.com/onnxruntime"
};

declare variable $marklogic-additional := map {
    "as": "http://marklogic.com/xdmp/assignments",
    "db": "http://marklogic.com/xdmp/database",
    "dg": "xdmp:document-get",
    "cl": "http://marklogic.com/xdmp/clusters",
    "exsl": "http://exslt.org/common",
    "extusr": "http://marklogic.com/xdmp/external/user",
    "gr": "http://marklogic.com/xdmp/group",
    "lang": "http://marklogic.com/xdmp/language",
    "list": "http://marklogic.com/manage/package/list",
    "md": "urn:oasis:names:tc:SAML:2.0:metadata",
    "mt": "http://marklogic.com/xdmp/mimetypes",
    "pkg": "http://marklogic.com/manage/package",
    "s": "http://www.w3.org/2009/xpath-functions/analyze-string",
    "sch": "http://purl.oclc.org/dsdl/schematron",
    "x509": "http://marklogic.com/xdmp/x509",
    "xdt": "http://www.w3.org/2004/10/xpath-datatypes",
    "xi": "http://www.w3.org/2001/XInclude"
};

declare variable $namespaces := map:merge((
    $module-namespaces,
    $xquery-namespaces,
    $marklogic-5-builtin,
    $marklogic-6-builtin,
    $marklogic-7-builtin,
    $marklogic-10-builtin,
    $marklogic-additional,
    ()
));

(: functions --------------------------------------------------------------- :)

(: Several functions in the apidocs have duplicate parameter names in some versions, so fix these. :)
declare variable $param-fixes := map {
    "fn:subtract-dateTimes-yielding-dayTimeDuration": ("srcval1", "srcval2"),
    "fn:subtract-dateTimes-yielding-yearMonthDuration": ("srcval1", "srcval2"),
    "geo:region-contains": ("target", "region", "options"),
    "math:fmod": ("x", "y"),
    "math:rank": ("arg1", "arg2", "options")
};

declare variable $functions as element(apidoc:function)* := $apidoc//apidoc:function[
    local:function-language(.) = $language and not(@name = "")
];

declare function local:function-uri($function as element(apidoc:function)) as xs:string {
    ``[http://docs.marklogic.com/`{$marklogic-version}`/`{local:function-name($function)}`]``
};

declare function local:function-language($function as element(apidoc:function)) as xs:string {
    let $bucket := $function/@bucket
    return switch ($bucket)
    case "REST Resources API" return "rest"
    default return local:apidoc-language($function)
};

declare function local:function-name($function as element(apidoc:function)) as xs:string {
    ``[`{$function/@lib}`:`{$function/@name}`]``
};

declare function local:function-parameters($function as element(apidoc:function)) as xs:string* {
    let $names as xs:string* :=
        let $names := map:get($param-fixes, local:function-name($function))
        return if (exists($names)) then
            $names
        else
            $function/apidoc:params/apidoc:param[local:apidoc-language(.) = $language]/@name/string()
    return if (count($names) != count(distinct-values($names))) then
        fn:error(xs:QName("err:XQST0039"),``[Duplicate parameter name in `{local:function-name($function)}`]``)
    else
        $names
};

declare function local:function-arity($function as element(apidoc:function)) as map(*) {
    let $params := $function/apidoc:params/apidoc:param[local:apidoc-language(.) = $language]
    return map {
        "min": count($params[not(@optional = "true")]),
        "max": count($params)
    }
};

declare function local:function-parameter($function as element(apidoc:function), $name as xs:string) as element(apidoc:param) {
    let $names := map:get($param-fixes, local:function-name($function))
    return if (exists($names)) then
        let $i := fn:index-of($names, $name)
        return $function/apidoc:params/apidoc:param[$i]
    else
        $function/apidoc:params/apidoc:param[@name = $name and local:apidoc-language(.) = $language]
};

declare function local:function-parameter-type($function as element(apidoc:function), $name as xs:string) as xs:string {
    let $type := local:function-parameter($function, $name)/@type/string()
    return switch ($type)
    case "atomic type" return "item()*"
    case "xs:string)" return "xs:string"
    case "xs:dateTime? | xs:date? | xs:string?" return "(xs:dateTime?|xs:date?|xs:string?)"
    case "binary())" return "binary()"
    case "element(sec:query-rolesets)*)" return "element(sec:query-rolesets)*"
    case "function()" return "function(*)"
    case "item()* | map:map" return "(item()*|map:map)"
    case "(binary()|node())*" return "(binary()*|node()*)"
    case "(binary()|node())?" return "(binary()?|node()?)"
    case "(element()|map:map)?" case "element()?|map:map?" return "(element()?|map:map?)"
    case "(node()? | map:map)?" return "(node()?|map:map?)"
    case "(cts:order|xs:string)*" return "(cts:order*|xs:string*)"
    case "[xs:string?]" return "xs:string?"
    default return replace($type, ",\.\.\.", "...")
};

declare function local:function-return-type($function as element(apidoc:function)) as xs:string {
    let $type := normalize-space($function/apidoc:return[local:apidoc-language(.) = $language])
    return switch ($type)
    case "An XML representation of the certificate." return "element()?"
    case "element())" case "Element()" return "element()"
    case "element()|map:map" return "(element()|map:map)"
    case "element(xsd:schema)*" return "element(xs:schema)*"
    case "empty-sequence" case "empty sequence" return "empty-sequence()"
    case "xs:string)" return "xs:string"
    case "xs:unsignedLong)" return "xs:unsignedLong"
    case "pkgins:install($pkgname)" return "element(pkg:install-status)"
    case "pkgins:revert($ticket)" return "element(pkg:revert-status)"
    case "query-results-serialize($results)" return "item()*"
    case "The return data type is the data type of the date argument" return "item()"
    case "" return
        switch (local:function-name($function))
        case "admin:appserver-set-output-indent-tabs" return "element(configuration)"
        default return ""
    default return $type
};

declare function local:function-xquery-signatures($function as element(apidoc:function)) as xs:string* {
    let $name := local:function-name($function)
    let $params :=
        for $name in local:function-parameters($function)
        let $type := local:function-parameter-type($function, $name)
        return ``[$`{$name}` as `{$type}`]``
    let $type := local:function-return-type($function)
    let $arity-range := local:function-arity($function)
    for $arity in $arity-range?min to $arity-range?max
    let $params := $params[position() = 1 to $arity]
    return if ($type = "") then
        ``[declare function `{$name}`(`{string-join($params, ", ")}`) external;]``
    else
        ``[declare function `{$name}`(`{string-join($params, ", ")}`) as `{$type}` external;]``
};

declare function local:function-html-signatures($function as element(apidoc:function)) as element(div)+ {
    let $name := local:function-name($function)
    let $params :=
        for $name in local:function-parameters($function)
        let $type := local:function-parameter-type($function, $name)
        return

            ${$name},
             as ,
            {$type}

    let $type := local:function-return-type($function)
    let $arity-range := local:function-arity($function)
    for $arity in $arity-range?min to $arity-range?max
    let $params := $params[position() = 1 to $arity]
    return if (count($params) <= 2) then

            {$name}({
                for $param at $i in $params
                return (
                    if ($i > 1) then ", " else (),
                    $param/*
                )
            }){
                if ($type != "") then (
                     as ,
                    xs:double
                ) else ()
            }

    else

{
                (: JEditorPanel does not support vertical-align on tr/td elements, so use valign instead. :)

                    {$name}(
                    {$params[1]/code[@class="arg"]}
                    {$params[1]/code[not(@class="arg")]},

,
                for $param at $i in $params[position() > 1]
                return

                        {$param/code[@class="arg"]}
                        {
                            $param/code[not(@class="arg")],
                            if ($i = count($params) - 1) then ")" else ","
                        }

            }

};

declare function local:function-html-parameters($function as element(apidoc:function)) as element(table)? {
    let $params := local:function-parameters($function)
    return if (exists($params)) then

{
            for $name in local:function-parameters($function)
            let $type := local:function-parameter-type($function, $name)
            let $summary := local:function-parameter($function, $name)
            return

                    ${$name}
                    {$summary/node()}

        }

    else ()
};

(: query ------------------------------------------------------------------- :)

let $prefix := local:prefix($namespaces, $namespace)
for $function in $functions
where $function/@lib = $prefix and $function/@name = $local-name
return (
    local:function-uri($function),
    fn:serialize(local:documentation-html($function/apidoc:summary)),
    string-join(local:function-html-signatures($function) ! fn:serialize(.), ""),
    fn:serialize(local:function-html-parameters($function)),
    fn:serialize($function/apidoc:privilege ! local:documentation-html(.)),
    fn:serialize($function/apidoc:usage ! local:documentation-html(.)),
    fn:serialize($function/apidoc:example ! local:documentation-html(.)),
    ()
)


[ignore] Admin webapp index browser: always showed "default analyzer" for all indexes.

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

xquery version "1.0";

(:
    Module: Browse Indexes: See an overview of .xconf files stored in /db/system/config,
    and browse the associated index keys.
:)

module namespace indexes="http://exist-db.org/xquery/admin-interface/indexes";

declare namespace cc="http://exist-db.org/collection-config/1.0";

declare option exist:serialize "method=xhtml media-type=text/html omit-xml-declaration=no enforce-xhtml=yes";

(:
    Global variables - derived from URL parameters
:)
declare variable $indexes:start-value := request:get-parameter('start-value', '');
declare variable $indexes:callback := util:function(xs:QName("indexes:term-callback"), 2);
declare variable $indexes:max-number-returned := xs:integer(request:get-parameter('max', 100));
declare variable $indexes:index := request:get-parameter('index', '');
declare variable $indexes:sortby := request:get-parameter('sortby', 'term');
declare variable $indexes:sortorder := request:get-parameter('sortorder', 'ascending');
declare variable $indexes:node-name := request:get-parameter('node-name', '');
declare variable $indexes:match := request:get-parameter('match', '');
declare variable $indexes:collection := request:get-parameter('collection', '');
declare variable $indexes:node-set :=
    if ($indexes:node-name ne '' and $indexes:collection ne '') then
        indexes:get-nodeset-from-qname($indexes:collection, $indexes:node-name)
    else if ($indexes:match ne '' and $indexes:collection ne '') then
        indexes:get-nodeset-from-match($indexes:collection, $indexes:match)
    else
        ();
declare variable $indexes:qname :=
    if ($indexes:node-name ne '') then
        if (not(matches($indexes:node-name, 'xml:')) and contains($indexes:node-name, ':')) then
            QName(indexes:get-namespace-uri-from-node-name($indexes:node-name, $indexes:collection), $indexes:node-name)
        else
            xs:QName($indexes:node-name)
    else ();
declare variable $indexes:show-keys-by := request:get-parameter('show-keys-by', '');
declare variable $indexes:index-names :=

        <name>Index Types</name>
        <description>This is the list of known index types and their labels.</description>
        <items>
            <item>
                Lucene
                <value>lucene-index</value>
            </item>
            <item>
                NGram
                <value>ngram-index</value>
            </item>
            <item>
                Range
                <value>range-index</value>
            </item>
            <item>
                Legacy Fulltext
                <value>legacy-fulltext-index</value>
            </item>
        </items>
    ;

(:
    Main function: outputs the page.
:)
declare function indexes:main() {
    let $xconfs :=
        for $xconf in collection('/db/system/config/')/cc:collection
        order by util:collection-name($xconf)
        return $xconf
    return
        if (empty($xconfs)) then

Browse Indexes

No Index Configurations were found in the /db/system/config collection.
                    Consider loading the example files and returning
                    to browse the indexes created.

        else if (empty($indexes:node-set)) then

            indexes:list-indexes($xconfs)

        else

            indexes:show-index-keys()
};

(:
    Lists indexes.
:)
declare function indexes:list-indexes($xconfs) {

Browse Indexes

This is an overview of the index definitions applied in the database,
            as well as the "index keys" (the actual items stored in the index).

Browsing {count($xconfs)} indexes:

        { indexes:xconf-to-table($xconfs) }

};

(:
    Transforms an index definition into an HTML table.
:)
declare function indexes:xconf-to-table($xconfs as node()*) as item()* {

{
        for $xconf at $count in $xconfs
        let $xconf-collection-name := util:collection-name($xconf)
        let $data-collection-name := substring-after(util:collection-name($xconf), '/db/system/config')
        order by $xconf-collection-name
        return

                {$count}. {$data-collection-name} {if (xmldb:collection-available($data-collection-name)) then () else ' (no data)'}
                    (View xconf file)

                        Item Indexed
                        Index
                        Instances
                        Show Index Keys By

                    {
                    for $entry in ( indexes:analyze-legacy-fulltext-indexes($xconf),
                        indexes:analyze-lucene-indexes($xconf),
                        indexes:analyze-range-indexes($xconf),
                        indexes:analyze-ngram-indexes($xconf) )
                    let $item := $entry/td[1]
                    let $index := $entry/td[2]
                    (: order by $index, $item :)
                    return $entry
                    }

    }

};

(:
    Shows the index keys on a given nodeset or QName
:)
declare function indexes:show-index-keys() {
    let $query-start-time := util:system-time()

    let $keys :=
        (: legacy fulltext index use the text:index-terms() function :)
        if ($indexes:index eq 'legacy-fulltext-index') then
            text:index-terms($indexes:node-set, $indexes:start-value, $indexes:callback, $indexes:max-number-returned)
        (: range indexes can use util:index-keys() without specifying the index :)
        else if ($indexes:index eq 'range-index') then
            if (util:index-type($indexes:node-set) eq 'xs:string') then
                util:index-keys($indexes:node-set, $indexes:start-value, $indexes:callback, $indexes:max-number-returned)
            else (: if (util:index-type($indexes:node-set) eq 'xs:integer') then :)
                let $start-value := if ($indexes:start-value castable as xs:integer) then xs:integer($indexes:start-value) else 0
                return
                    util:index-keys($indexes:node-set, $start-value, $indexes:callback, $indexes:max-number-returned)
            (: TODO: add more conditionals for xs:double etc.?  or is there a better solution:)
        (: all other indexes need to specify the index in the 5th parameter of util:index-keys() :)
        else
            if ($indexes:show-keys-by eq 'node') then
                util:index-keys($indexes:node-set, $indexes:start-value, $indexes:callback, $indexes:max-number-returned, $indexes:index)
            else
                util:index-keys-by-qname($indexes:qname, $indexes:start-value, $indexes:callback, $indexes:max-number-returned, $indexes:index)

    let $log := util:log("DEBUG", concat("INDEXES index type:    ", $indexes:index))
    let $log := util:log("DEBUG", concat("INDEXES qname     :    ", $indexes:qname))

    (:  Reminder for sorting:
           term = $key/td[1]
           frequency := xs:integer($key/td[2])
           documents := xs:integer($key/td[3])
           position := xs:integer($key/td[4])
    :)

    let $sorted-keys :=
        if ($indexes:sortby eq 'term') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by $key/td[1] ascending return $key
            else
                for $key in $keys order by $key/td[1] descending return $key
        else if ($indexes:sortby eq 'frequency') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by xs:integer($key/td[2]) ascending, $key/td[1] ascending return $key
            else
                for $key in $keys order by xs:integer($key/td[2]) descending, $key/td[1] ascending return $key
        else if ($indexes:sortby eq 'documents') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by xs:integer($key/td[3]) ascending, $key/td[1] ascending return $key
            else
                for $key in $keys order by xs:integer($key/td[3]) descending, $key/td[1] ascending return $key
        else if ($indexes:sortby eq 'position') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by xs:integer($key/td[4]) ascending return $key
            else
                for $key in $keys order by xs:integer($key/td[4]) descending return $key
        else $keys

    let $query-end-time := util:system-time()
    let $query-duration := ($query-end-time - $query-start-time) div xs:dayTimeDuration('PT1S')

    return

View Index Keys

Select a different index

{count($keys)} keys returned in {$query-duration}s

Keys for the {indexes:index-name-to-label($indexes:index)} index defined on "{string-join(($indexes:node-name, $indexes:match), '')}" in the {$indexes:collection} collection, by {$indexes:show-keys-by}.

            <form method="get" action="{indexes:remove-parameter-names('start-value')}">

                    Max number returned: <select name="max">{
                        for $number in (10, 100, 1000, 10000)
                        return
                            <option value="{$number}">{if ($number eq $indexes:max-number-returned) then attribute selected {'selected'} else ()}{$number}</option>
                    }</select>

                    Search for terms starting with: <input name="start-value" type="text" value="{$indexes:start-value}"/>
                    {if ($indexes:start-value ne '') then Clear search else ()}
                    {
                    for $param in request:get-parameter-names()[not(. = ('max', 'start-value'))]
                    return
                        <input type="hidden" name="{$param}" value="{request:get-parameter($param, '')}"/>
                    }

                    <input type="submit"/>

            </form>

{
                    for $column in ('term', 'frequency', 'documents', 'position')
                    return
                        {$column} {indexes:sort-direction-indicator($column)}
                }

                { $sorted-keys }

};

(:
    Helper function: Callback function called used in indexes:show-index-keys() for util:index-keys()
:)
declare function indexes:term-callback($term as xs:string, $data as xs:int+) as element() {

        {$term}
        {$data[1]}
        {$data[2]}
        {$data[3]}

};

(:
    Analyzes the Lucene indexes in an index definition
:)
declare function indexes:analyze-lucene-indexes($xconf) {
    let $index-label := indexes:index-name-to-label('lucene-index')
    let $lucene := $xconf/cc:index/cc:lucene
    return if (not($lucene) or not($lucene/cc:text)) then () else
        (
        (: TODO: complete report of default/other Lucene analyzers :)
        (:let $analyzers := $lucene/cc:analyzer
        return
            if ($analyzers) then
                (

Lucene Analyzers

                ,
                for $node in $analyzers
                return
                    if (not($node/@id)) then

default: {concat(' ', $node/@class/string())}

                    else

{$node/@id/string()}: {concat(' ', $node/@class/string())}

                )
            else ()
        ,:)
        let $texts := $lucene//cc:text
        return
            (
            for $text in $texts
            let $qname := if ($text/@qname) then $text/@qname/string() else ()
            let $match := if ($text/@match) then $text/@match/string() else ()
            let $analyzer := if ($text/@analyzer) then $text/@analyzer/string() else ()
            let $collection := substring-after(util:collection-name($text), '/db/system/config')
            let $nodeset := if ($qname) then indexes:get-nodeset-from-qname($collection, $qname) else indexes:get-nodeset-from-match($collection, $match)
            return

                        {if ($qname) then $qname else $match}
                        {if ($text/@boost) then concat(' (boost: ', $text/@boost/string(), ')') else ()}
                        {if ($text/cc:ignore) then (
, concat('(ignore: ', string-join(for $ignore in $text/cc:ignore return $ignore/@qname/string(), ', '), ')')) else ()}
                    {$index-label} {if ($qname) then ' QName' else ' Match'} {if ($analyzer) then concat(' (', $analyzer, ')') else ' (default analyzer)' (: TODO: complete report of default/other Lucene analyzers :)}
                    {count($nodeset)}
                    {
                        if (empty($nodeset)) then ()
                        else
                            (
                            QName
                            ,
                            ', '
                            ,
                            Node
                            )
                    }

            )
        )
};

(:
    Analyzes the legacy fulltext indexes in an index definition
:)
declare function indexes:analyze-legacy-fulltext-indexes($xconf) {
    let $collection := substring-after(util:collection-name($xconf), '/db/system/config')
    let $index := 'legacy-fulltext-index'
    let $index-label := indexes:index-name-to-label($index)
    let $fulltext := $xconf/cc:index/cc:fulltext
    let $creates := $fulltext/cc:create
    let $default-none := $fulltext/@default eq 'none'
    let $attributes-none := $fulltext/@attributes eq 'false'

    let $no-fulltext := if ( not($fulltext) or ($default-none and $attributes-none and not($creates)) ) then '(disabled)' else ()
    return
        if ($no-fulltext) then ()
        else if (not($default-none) and not($attributes-none)) then

                All Elements and Attributes!
                {$index-label}
                {count(collection($collection)//*)} elements and {count(collection($collection)//@*)} attributes
                (Too many to display)

(: is it feasible to display all qnames/nodes here? :)
        else (: if ($creates) then :)
            (
            for $create in $creates
            let $qname := $create/@qname/string()
            let $mixed := $create/@mixed/string()
            return

                    {$qname}
                    {$index-label} {if ($mixed) then '(mixed)' else ()}
                    {count(util:eval(concat('collection(', $collection, ')//', $qname)))}
                    Node

            ,
            let $only-elements-disabled := if ($fulltext/@default eq 'none' and $fulltext/@attributes ne 'no') then '(elements disabled)' else ()
            let $only-attribs-disabled := if ($fulltext/@default ne 'none' and $fulltext/@attributes eq 'no') then '(attributes disabled)' else ()
            return
            if ($only-elements-disabled) then

                    All Attributes!
                    {$index-label}
                    {count(collection($collection)//@*)} attributes
                    (Too many to display)

(: is it feasible to display all qnames/nodes here? :)
            else if ($only-attribs-disabled) then

                    All Elements!
                    {$index-label}
                    {count(collection($collection)//*)} elements
                    (Too many to display)

(: is it feasible to display all qnames/nodes here? :)
            else ()
            )
};

(:
    Analyzes the range indexes in an index definition
:)
declare function indexes:analyze-range-indexes($xconf) {
    let $index-label := indexes:index-name-to-label('range-index')
    let $ranges := $xconf/cc:index/cc:create
    return if (not($ranges)) then () else
        for $range in $ranges
        let $qname := $range/@qname/string()
        let $match := $range/@path/string()
        let $type := $range/@type/string()
        let $collection := substring-after(util:collection-name($range), '/db/system/config')
        let $nodeset := if ($qname) then indexes:get-nodeset-from-qname($collection, $qname) else indexes:get-nodeset-from-match($collection, $match)
        return

                {if ($qname) then $qname else $match}
                {$index-label} {if ($qname) then ' QName' else ' Path'} ({$type})
                {count($nodeset)}
                {
                    if (empty($nodeset)) then ()
                    else
                        Node
                }

};

(:
    Analyzes the NGram indexes in an index definition
:)
declare function indexes:analyze-ngram-indexes($xconf) {
    let $index-label := indexes:index-name-to-label('ngram-index')
    let $ngrams := $xconf/cc:index/cc:ngram
    return if (not($ngrams)) then () else
        for $ngram in $ngrams
        let $qname := $ngram/@qname/string()
        let $collection := substring-after(util:collection-name($ngram), '/db/system/config')
        let $nodeset := indexes:get-nodeset-from-qname($collection, $qname)
        return

                {$qname}
                {$index-label} QName
                {count($nodeset)}
                {
                    if (not(empty($nodeset))) then
                        (
                        QName,
                        ', '
                        ,
                        Node
                        )
                     else ()
                 }

};

(:
    Helper function: Returns a nodeset of instances of a node-name in a collection
:)
declare function indexes:get-nodeset-from-qname($collection as xs:string, $node-name as xs:string) as node()* {
    let $nodeset-expression :=
        concat(
            indexes:get-namespace-decaration-from-node-name($node-name, $collection)
            ,
            'collection("', $collection, '")//', $node-name
        )
    let $log := util:log("DEBUG", concat("INDEXES get-nodeset:          ", $nodeset-expression))
    return
        util:eval($nodeset-expression)
};

(:
    Helper function: Returns a nodeset of instances of a match expression in a collection
:)
declare function indexes:get-nodeset-from-match($collection as xs:string, $match as xs:string) as node()* {
    let $nodeset-expression :=
        concat(
            string-join(
                let $node-names := tokenize(replace($match, '//', '/'), '/')
                return
                    for $node-name in $node-names
                    return
                        indexes:get-namespace-decaration-from-node-name($node-name, $collection)
                , ' ')
            ,
            'collection("', $collection, '")', $match, if (contains($match, '@')) then '/string()' else ()
        )
    return
        util:eval($nodeset-expression)
};

(:
    Helper function: Returns the index definition for a given collection
:)
declare function indexes:get-xconf($collection as xs:string) as document-node() {
    let $config-root := '/db/system/config'
    let $xconf-collection := concat($config-root, $collection)
    let $xconf-filename := xmldb:get-child-resources($xconf-collection)[ends-with(., '.xconf')]
    let $xconf := doc(concat($xconf-collection, '/', $xconf-filename))
    return $xconf
};

(:
    Helper function: Looks in the collection.xconf's collection and index elements for namespace URIs for a given node name
:)
declare function indexes:get-namespace-uri-from-node-name($node-name, $collection) {
    let $name := substring-before($node-name, ':')
    let $xconf := indexes:get-xconf($collection)
    let $uri := (namespace-uri-for-prefix($name, $xconf/cc:collection), namespace-uri-for-prefix($name, $xconf//cc:index))
    return
        $uri
};

(:
    Helper function: Constructs a namespace declaration for use in util:eval()
:)
declare function indexes:get-namespace-decaration-from-node-name($node-name as xs:string, $collection as xs:string) as xs:string* {
    if (not(matches($node-name, 'xml:')) and contains($node-name, ':')) then
        let $name := substring-before($node-name, ':')
        let $uri := indexes:get-namespace-uri-from-node-name($node-name, $collection)
        return
            concat('declare namespace ', $name, '="', $uri, '"; ')
    else ()
};

(:
    Helper function: gets the label for a given index-name
:)
declare function indexes:index-name-to-label($index-name as xs:string) as xs:string {
    $indexes:index-names//item[value eq $index-name]/label/text()
};

(:
    ====
    Helper functions for modifying the sort order used in indexes:show-index-keys()
    ====
:)
declare function indexes:toggle-sortorder($current-sortorder) {
    indexes:toggle-sortorder($current-sortorder, ())
};

declare function indexes:toggle-sortorder($current-sortorder, $other-new-parameters) {
    let $neworder :=
        if ($current-sortorder eq 'ascending') then
            'sortorder=descending'
        else
            'sortorder=ascending'
    let $new-parameters := ($neworder, $other-new-parameters)
    return
        indexes:replace-parameters($new-parameters)
};

declare function indexes:set-sortorder($current-sortorder, $current-sortby, $new-sortby) {
    if ($current-sortby eq $new-sortby) then
        indexes:toggle-sortorder($current-sortorder)
    else
        indexes:strip-param-from-param-string(indexes:replace-parameters(concat('sortby=', $new-sortby)), 'sortorder')
};

declare function indexes:set-sortorder($new-sortby) {
    indexes:set-sortorder($indexes:sortorder, $indexes:sortby, $new-sortby)
};

declare function indexes:sort-direction-indicator($sortby as xs:string) {
    if ($sortby eq $indexes:sortby) then
        if ($indexes:sortorder eq 'ascending') then
            ' ↓'
        else
            ' ↑'
    else ()
};

(:
    ====
    Helper functions for handling parameters
    ====
:)
declare function indexes:remove-parameter-names($parameter-names-to-remove) {
    let $current-parameter-names := request:get-parameter-names()
    let $remaining-parameters :=
        indexes:remove-parameter-names(
            for $current-parameter-name in $current-parameter-names
            return
                concat($current-parameter-name, '=', request:get-parameter( $current-parameter-name, () ))
            ,
            $parameter-names-to-remove
            )
    return
        if (exists($remaining-parameters)) then
            concat('?', string-join($remaining-parameters, '&'))
        else
            '?'
};

declare function indexes:remove-parameter-names($current-parameters, $parameter-names-to-remove) {
    for $current-parameter in $current-parameters
    return
        if (substring-before($current-parameter, '=') = $parameter-names-to-remove) then
            ()
        else
            $current-parameter
};

declare function indexes:remove-parameter-names-except($parameter-names-to-keep) {
    let $current-parameter-names := request:get-parameter-names()
    return
        indexes:remove-parameter-names($current-parameter-names[not(. = $parameter-names-to-keep)])
};

declare function indexes:replace-parameters($new-parameters) {
    let $current-parameter-names := request:get-parameter-names()
    let $current-parameters :=
        for $name in $current-parameter-names
        return concat($name, '=', request:get-parameter($name, ()))
    return
        indexes:replace-parameters($current-parameters, $new-parameters)
};

declare function indexes:replace-parameters($current-parameters, $new-parameters) {
    let $new-parameter-names := for $new-parameter in $new-parameters return substring-before($new-parameter, '=')
    let $remaining-parameters := indexes:remove-parameter-names($current-parameters, $new-parameter-names)
    let $result-parameters := for $param in ($remaining-parameters, $new-parameters) order by $param return $param
    return
        concat('?', string-join($result-parameters, '&'))
};

declare function indexes:strip-param-from-param-string($param-string, $param) {
    replace($param-string, concat('&?', $param, '=[^&]*?&?.*$'), '')
};

xquery version "1.0";

(:
    Module: Browse Indexes: See an overview of .xconf files stored in /db/system/config,
    and browse the associated index keys.
:)

module namespace indexes="http://exist-db.org/xquery/admin-interface/indexes";

declare namespace cc="http://exist-db.org/collection-config/1.0";

declare option exist:serialize "method=xhtml media-type=text/html omit-xml-declaration=no enforce-xhtml=yes";

(:
    Global variables - derived from URL parameters
:)
declare variable $indexes:start-value := request:get-parameter('start-value', '');
declare variable $indexes:callback := util:function(xs:QName("indexes:term-callback"), 2);
declare variable $indexes:max-number-returned := xs:integer(request:get-parameter('max', 100));
declare variable $indexes:index := request:get-parameter('index', '');
declare variable $indexes:sortby := request:get-parameter('sortby', 'term');
declare variable $indexes:sortorder := request:get-parameter('sortorder', 'ascending');
declare variable $indexes:node-name := request:get-parameter('node-name', '');
declare variable $indexes:match := request:get-parameter('match', '');
declare variable $indexes:collection := request:get-parameter('collection', '');
declare variable $indexes:node-set :=
    if ($indexes:node-name ne '' and $indexes:collection ne '') then
        indexes:get-nodeset-from-qname($indexes:collection, $indexes:node-name)
    else if ($indexes:match ne '' and $indexes:collection ne '') then
        indexes:get-nodeset-from-match($indexes:collection, $indexes:match)
    else
        ();
declare variable $indexes:qname :=
    if ($indexes:node-name ne '') then
        if (not(matches($indexes:node-name, 'xml:')) and contains($indexes:node-name, ':')) then
            QName(indexes:get-namespace-uri-from-node-name($indexes:node-name, $indexes:collection), $indexes:node-name)
        else
            xs:QName($indexes:node-name)
    else ();
declare variable $indexes:show-keys-by := request:get-parameter('show-keys-by', '');
declare variable $indexes:index-names :=

        <name>Index Types</name>
        <description>This is the list of known index types and their labels.</description>
        <items>
            <item>
                Lucene
                <value>lucene-index</value>
            </item>
            <item>
                NGram
                <value>ngram-index</value>
            </item>
            <item>
                Range
                <value>range-index</value>
            </item>
            <item>
                Legacy Fulltext
                <value>legacy-fulltext-index</value>
            </item>
        </items>
    ;

(:
    Main function: outputs the page.
:)
declare function indexes:main() {
    let $xconfs :=
        for $xconf in collection('/db/system/config/')/cc:collection
        order by util:collection-name($xconf)
        return $xconf
    return
        if (empty($xconfs)) then

Browse Indexes

No Index Configurations were found in the /db/system/config collection.
                    Consider loading the example files and returning
                    to browse the indexes created.

        else if (empty($indexes:node-set)) then

            indexes:list-indexes($xconfs)

        else

            indexes:show-index-keys()
};

(:
    Lists indexes.
:)
declare function indexes:list-indexes($xconfs) {

Browse Indexes

This is an overview of the index definitions applied in the database,
            as well as the "index keys" (the actual items stored in the index).

Browsing {count($xconfs)} indexes:

        { indexes:xconf-to-table($xconfs) }

};

(:
    Transforms an index definition into an HTML table.
:)
declare function indexes:xconf-to-table($xconfs as node()*) as item()* {

{
        for $xconf at $count in $xconfs
        let $xconf-collection-name := util:collection-name($xconf)
        let $data-collection-name := substring-after(util:collection-name($xconf), '/db/system/config')
        order by $xconf-collection-name
        return

                {$count}. {$data-collection-name} {if (xmldb:collection-available($data-collection-name)) then () else ' (no data)'}
                    (View xconf file)

                        Item Indexed
                        Index
                        Instances
                        Show Index Keys By

                    {
                    for $entry in ( indexes:analyze-legacy-fulltext-indexes($xconf),
                        indexes:analyze-lucene-indexes($xconf),
                        indexes:analyze-range-indexes($xconf),
                        indexes:analyze-ngram-indexes($xconf) )
                    let $item := $entry/td[1]
                    let $index := $entry/td[2]
                    (: order by $index, $item :)
                    return $entry
                    }

    }

};

(:
    Shows the index keys on a given nodeset or QName
:)
declare function indexes:show-index-keys() {
    let $query-start-time := util:system-time()

    let $keys :=
        (: legacy fulltext index use the text:index-terms() function :)
        if ($indexes:index eq 'legacy-fulltext-index') then
            text:index-terms($indexes:node-set, $indexes:start-value, $indexes:callback, $indexes:max-number-returned)
        (: range indexes can use util:index-keys() without specifying the index :)
        else if ($indexes:index eq 'range-index') then
            if (util:index-type($indexes:node-set) eq 'xs:string') then
                util:index-keys($indexes:node-set, $indexes:start-value, $indexes:callback, $indexes:max-number-returned)
            else (: if (util:index-type($indexes:node-set) eq 'xs:integer') then :)
                let $start-value := if ($indexes:start-value castable as xs:integer) then xs:integer($indexes:start-value) else 0
                return
                    util:index-keys($indexes:node-set, $start-value, $indexes:callback, $indexes:max-number-returned)
            (: TODO: add more conditionals for xs:double etc.?  or is there a better solution:)
        (: all other indexes need to specify the index in the 5th parameter of util:index-keys() :)
        else
            if ($indexes:show-keys-by eq 'node') then
                util:index-keys($indexes:node-set, $indexes:start-value, $indexes:callback, $indexes:max-number-returned, $indexes:index)
            else
                util:index-keys-by-qname($indexes:qname, $indexes:start-value, $indexes:callback, $indexes:max-number-returned, $indexes:index)

    let $log := util:log("DEBUG", concat("INDEXES index type:    ", $indexes:index))
    let $log := util:log("DEBUG", concat("INDEXES qname     :    ", $indexes:qname))

    (:  Reminder for sorting:
           term = $key/td[1]
           frequency := xs:integer($key/td[2])
           documents := xs:integer($key/td[3])
           position := xs:integer($key/td[4])
    :)

    let $sorted-keys :=
        if ($indexes:sortby eq 'term') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by $key/td[1] ascending return $key
            else
                for $key in $keys order by $key/td[1] descending return $key
        else if ($indexes:sortby eq 'frequency') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by xs:integer($key/td[2]) ascending, $key/td[1] ascending return $key
            else
                for $key in $keys order by xs:integer($key/td[2]) descending, $key/td[1] ascending return $key
        else if ($indexes:sortby eq 'documents') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by xs:integer($key/td[3]) ascending, $key/td[1] ascending return $key
            else
                for $key in $keys order by xs:integer($key/td[3]) descending, $key/td[1] ascending return $key
        else if ($indexes:sortby eq 'position') then
            if ($indexes:sortorder eq 'ascending') then
                for $key in $keys order by xs:integer($key/td[4]) ascending return $key
            else
                for $key in $keys order by xs:integer($key/td[4]) descending return $key
        else $keys

    let $query-end-time := util:system-time()
    let $query-duration := ($query-end-time - $query-start-time) div xs:dayTimeDuration('PT1S')

    return

View Index Keys

Select a different index

{count($keys)} keys returned in {$query-duration}s

Keys for the {indexes:index-name-to-label($indexes:index)} index defined on "{string-join(($indexes:node-name, $indexes:match), '')}" in the {$indexes:collection} collection, by {$indexes:show-keys-by}.

            <form method="get" action="{indexes:remove-parameter-names('start-value')}">

                    Max number returned: <select name="max">{
                        for $number in (10, 100, 1000, 10000)
                        return
                            <option value="{$number}">{if ($number eq $indexes:max-number-returned) then attribute selected {'selected'} else ()}{$number}</option>
                    }</select>

                    Search for terms starting with: <input name="start-value" type="text" value="{$indexes:start-value}"/>
                    {if ($indexes:start-value ne '') then Clear search else ()}
                    {
                    for $param in request:get-parameter-names()[not(. = ('max', 'start-value'))]
                    return
                        <input type="hidden" name="{$param}" value="{request:get-parameter($param, '')}"/>
                    }

                    <input type="submit"/>

            </form>

{
                    for $column in ('term', 'frequency', 'documents', 'position')
                    return
                        {$column} {indexes:sort-direction-indicator($column)}
                }

                { $sorted-keys }

};

(:
    Helper function: Callback function called used in indexes:show-index-keys() for util:index-keys()
:)
declare function indexes:term-callback($term as xs:string, $data as xs:int+) as element() {

        {$term}
        {$data[1]}
        {$data[2]}
        {$data[3]}

};

(:
    Analyzes the Lucene indexes in an index definition
:)
declare function indexes:analyze-lucene-indexes($xconf) {
    let $index-label := indexes:index-name-to-label('lucene-index')
    let $lucene := $xconf/cc:index/cc:lucene
    return if (not($lucene) or not($lucene/cc:text)) then () else
        (
        (: TODO: complete report of default/other Lucene analyzers :)
        (:let $analyzers := $lucene/cc:analyzer
        return
            if ($analyzers) then
                (

Lucene Analyzers

                ,
                for $node in $analyzers
                return
                    if (not($node/@id)) then

default: {concat(' ', $node/@class/string())}

                    else

{$node/@id/string()}: {concat(' ', $node/@class/string())}

                )
            else ()
        ,:)
        let $texts := $lucene//cc:text
        return
            (
            for $text in $texts
            let $qname := if ($text/@qname) then $text/@qname/string() else ()
            let $match := if ($text/@match) then $text/@match/string() else ()
            let $id := if ($text/@id) then $text/@id/string() else ()
            let $collection := substring-after(util:collection-name($text), '/db/system/config')
            let $nodeset := if ($qname) then indexes:get-nodeset-from-qname($collection, $qname) else indexes:get-nodeset-from-match($collection, $match)
            return

                        {if ($qname) then $qname else $match}
                        {if ($text/@boost) then concat(' (boost: ', $text/@boost/string(), ')') else ()}
                        {if ($text/cc:ignore) then (
, concat('(ignore: ', string-join(for $ignore in $text/cc:ignore return $ignore/@qname/string(), ', '), ')')) else ()}
                    {$index-label} {if ($qname) then ' QName' else ' Match'} {if ($id) then concat(' (', $id, ')') else ' (default analyzer)' (: TODO: complete report of default/other Lucene analyzers :)}
                    {count($nodeset)}
                    {
                        if (empty($nodeset)) then ()
                        else
                            (
                            QName
                            ,
                            ', '
                            ,
                            Node
                            )
                    }

            )
        )
};

(:
    Analyzes the legacy fulltext indexes in an index definition
:)
declare function indexes:analyze-legacy-fulltext-indexes($xconf) {
    let $collection := substring-after(util:collection-name($xconf), '/db/system/config')
    let $index := 'legacy-fulltext-index'
    let $index-label := indexes:index-name-to-label($index)
    let $fulltext := $xconf/cc:index/cc:fulltext
    let $creates := $fulltext/cc:create
    let $default-none := $fulltext/@default eq 'none'
    let $attributes-none := $fulltext/@attributes eq 'false'

    let $no-fulltext := if ( not($fulltext) or ($default-none and $attributes-none and not($creates)) ) then '(disabled)' else ()
    return
        if ($no-fulltext) then ()
        else if (not($default-none) and not($attributes-none)) then

                All Elements and Attributes!
                {$index-label}
                {count(collection($collection)//*)} elements and {count(collection($collection)//@*)} attributes
                (Too many to display)

(: is it feasible to display all qnames/nodes here? :)
        else (: if ($creates) then :)
            (
            for $create in $creates
            let $qname := $create/@qname/string()
            let $mixed := $create/@mixed/string()
            return

                    {$qname}
                    {$index-label} {if ($mixed) then '(mixed)' else ()}
                    {count(util:eval(concat('collection(', $collection, ')//', $qname)))}
                    Node

            ,
            let $only-elements-disabled := if ($fulltext/@default eq 'none' and $fulltext/@attributes ne 'no') then '(elements disabled)' else ()
            let $only-attribs-disabled := if ($fulltext/@default ne 'none' and $fulltext/@attributes eq 'no') then '(attributes disabled)' else ()
            return
            if ($only-elements-disabled) then

                    All Attributes!
                    {$index-label}
                    {count(collection($collection)//@*)} attributes
                    (Too many to display)

(: is it feasible to display all qnames/nodes here? :)
            else if ($only-attribs-disabled) then

                    All Elements!
                    {$index-label}
                    {count(collection($collection)//*)} elements
                    (Too many to display)

(: is it feasible to display all qnames/nodes here? :)
            else ()
            )
};

(:
    Analyzes the range indexes in an index definition
:)
declare function indexes:analyze-range-indexes($xconf) {
    let $index-label := indexes:index-name-to-label('range-index')
    let $ranges := $xconf/cc:index/cc:create
    return if (not($ranges)) then () else
        for $range in $ranges
        let $qname := $range/@qname/string()
        let $match := $range/@path/string()
        let $type := $range/@type/string()
        let $collection := substring-after(util:collection-name($range), '/db/system/config')
        let $nodeset := if ($qname) then indexes:get-nodeset-from-qname($collection, $qname) else indexes:get-nodeset-from-match($collection, $match)
        return

                {if ($qname) then $qname else $match}
                {$index-label} {if ($qname) then ' QName' else ' Path'} ({$type})
                {count($nodeset)}
                {
                    if (empty($nodeset)) then ()
                    else
                        Node
                }

};

(:
    Analyzes the NGram indexes in an index definition
:)
declare function indexes:analyze-ngram-indexes($xconf) {
    let $index-label := indexes:index-name-to-label('ngram-index')
    let $ngrams := $xconf/cc:index/cc:ngram
    return if (not($ngrams)) then () else
        for $ngram in $ngrams
        let $qname := $ngram/@qname/string()
        let $collection := substring-after(util:collection-name($ngram), '/db/system/config')
        let $nodeset := indexes:get-nodeset-from-qname($collection, $qname)
        return

                {$qname}
                {$index-label} QName
                {count($nodeset)}
                {
                    if (not(empty($nodeset))) then
                        (
                        QName,
                        ', '
                        ,
                        Node
                        )
                     else ()
                 }

};

(:
    Helper function: Returns a nodeset of instances of a node-name in a collection
:)
declare function indexes:get-nodeset-from-qname($collection as xs:string, $node-name as xs:string) as node()* {
    let $nodeset-expression :=
        concat(
            indexes:get-namespace-decaration-from-node-name($node-name, $collection)
            ,
            'collection("', $collection, '")//', $node-name
        )
    let $log := util:log("DEBUG", concat("INDEXES get-nodeset:          ", $nodeset-expression))
    return
        util:eval($nodeset-expression)
};

(:
    Helper function: Returns a nodeset of instances of a match expression in a collection
:)
declare function indexes:get-nodeset-from-match($collection as xs:string, $match as xs:string) as node()* {
    let $nodeset-expression :=
        concat(
            string-join(
                let $node-names := tokenize(replace($match, '//', '/'), '/')
                return
                    for $node-name in $node-names
                    return
                        indexes:get-namespace-decaration-from-node-name($node-name, $collection)
                , ' ')
            ,
            'collection("', $collection, '")', $match, if (contains($match, '@')) then '/string()' else ()
        )
    return
        util:eval($nodeset-expression)
};

(:
    Helper function: Returns the index definition for a given collection
:)
declare function indexes:get-xconf($collection as xs:string) as document-node() {
    let $config-root := '/db/system/config'
    let $xconf-collection := concat($config-root, $collection)
    let $xconf-filename := xmldb:get-child-resources($xconf-collection)[ends-with(., '.xconf')]
    let $xconf := doc(concat($xconf-collection, '/', $xconf-filename))
    return $xconf
};

(:
    Helper function: Looks in the collection.xconf's collection and index elements for namespace URIs for a given node name
:)
declare function indexes:get-namespace-uri-from-node-name($node-name, $collection) {
    let $name := substring-before($node-name, ':')
    let $xconf := indexes:get-xconf($collection)
    let $uri := (namespace-uri-for-prefix($name, $xconf/cc:collection), namespace-uri-for-prefix($name, $xconf//cc:index))
    return
        $uri
};

(:
    Helper function: Constructs a namespace declaration for use in util:eval()
:)
declare function indexes:get-namespace-decaration-from-node-name($node-name as xs:string, $collection as xs:string) as xs:string* {
    if (not(matches($node-name, 'xml:')) and contains($node-name, ':')) then
        let $name := substring-before($node-name, ':')
        let $uri := indexes:get-namespace-uri-from-node-name($node-name, $collection)
        return
            concat('declare namespace ', $name, '="', $uri, '"; ')
    else ()
};

(:
    Helper function: gets the label for a given index-name
:)
declare function indexes:index-name-to-label($index-name as xs:string) as xs:string {
    $indexes:index-names//item[value eq $index-name]/label/text()
};

(:
    ====
    Helper functions for modifying the sort order used in indexes:show-index-keys()
    ====
:)
declare function indexes:toggle-sortorder($current-sortorder) {
    indexes:toggle-sortorder($current-sortorder, ())
};

declare function indexes:toggle-sortorder($current-sortorder, $other-new-parameters) {
    let $neworder :=
        if ($current-sortorder eq 'ascending') then
            'sortorder=descending'
        else
            'sortorder=ascending'
    let $new-parameters := ($neworder, $other-new-parameters)
    return
        indexes:replace-parameters($new-parameters)
};

declare function indexes:set-sortorder($current-sortorder, $current-sortby, $new-sortby) {
    if ($current-sortby eq $new-sortby) then
        indexes:toggle-sortorder($current-sortorder)
    else
        indexes:strip-param-from-param-string(indexes:replace-parameters(concat('sortby=', $new-sortby)), 'sortorder')
};

declare function indexes:set-sortorder($new-sortby) {
    indexes:set-sortorder($indexes:sortorder, $indexes:sortby, $new-sortby)
};

declare function indexes:sort-direction-indicator($sortby as xs:string) {
    if ($sortby eq $indexes:sortby) then
        if ($indexes:sortorder eq 'ascending') then
            ' ↓'
        else
            ' ↑'
    else ()
};

(:
    ====
    Helper functions for handling parameters
    ====
:)
declare function indexes:remove-parameter-names($parameter-names-to-remove) {
    let $current-parameter-names := request:get-parameter-names()
    let $remaining-parameters :=
        indexes:remove-parameter-names(
            for $current-parameter-name in $current-parameter-names
            return
                concat($current-parameter-name, '=', request:get-parameter( $current-parameter-name, () ))
            ,
            $parameter-names-to-remove
            )
    return
        if (exists($remaining-parameters)) then
            concat('?', string-join($remaining-parameters, '&'))
        else
            '?'
};

declare function indexes:remove-parameter-names($current-parameters, $parameter-names-to-remove) {
    for $current-parameter in $current-parameters
    return
        if (substring-before($current-parameter, '=') = $parameter-names-to-remove) then
            ()
        else
            $current-parameter
};

declare function indexes:remove-parameter-names-except($parameter-names-to-keep) {
    let $current-parameter-names := request:get-parameter-names()
    return
        indexes:remove-parameter-names($current-parameter-names[not(. = $parameter-names-to-keep)])
};

declare function indexes:replace-parameters($new-parameters) {
    let $current-parameter-names := request:get-parameter-names()
    let $current-parameters :=
        for $name in $current-parameter-names
        return concat($name, '=', request:get-parameter($name, ()))
    return
        indexes:replace-parameters($current-parameters, $new-parameters)
};

declare function indexes:replace-parameters($current-parameters, $new-parameters) {
    let $new-parameter-names := for $new-parameter in $new-parameters return substring-before($new-parameter, '=')
    let $remaining-parameters := indexes:remove-parameter-names($current-parameters, $new-parameter-names)
    let $result-parameters := for $param in ($remaining-parameters, $new-parameters) order by $param return $param
    return
        concat('?', string-join($result-parameters, '&'))
};

declare function indexes:strip-param-from-param-string($param-string, $param) {
    replace($param-string, concat('&?', $param, '=[^&]*?&?.*$'), '')
};