refactor - don't need a variable for function-name

xquery version "1.0-ml";

declare variable $xml external;
let $xml := xdmp:unquote("<wrap>" || $xml || "</wrap>")

let $definition := xdmp:xslt-eval(
  <xsl:stylesheet version="2.0" exclude-result-prefixes="#all" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:apidoc="http://marklogic.com/xdmp/apidoc" xmlns:local="local">

    <xsl:output method="text" indent="yes" omit-xml-declaration="yes"/>

    <xsl:template match="@*|node()" mode="#all">
      <xsl:copy>
        <xsl:apply-templates select="@*|node()" mode="#current"/>
      </xsl:copy>
    </xsl:template>

    <xsl:template match="apidoc:function" mode="#all">
      <xsl:param name="use-function-keyword" select="false()"/>
      <xsl:param name="use-declare-keyword" select="false()"/>

      <xsl:text>    /** </xsl:text>
      <xsl:value-of select="normalize-space(apidoc:summary)"/>
      <xsl:text> **/
</xsl:text>

      <xsl:text>  </xsl:text>
      <xsl:if test="$use-declare-keyword">
        <xsl:text>declare </xsl:text>
      </xsl:if>
      <xsl:if test="$use-function-keyword">
        <xsl:text>function </xsl:text>
      </xsl:if>

      <!--
      The function name can usually be derived by convetion. But in some cases (well, one case - toJSON), an override
      is provided in the documentation. In those cases, the override should be used. This is also how this is handled
      in RunDMC in api:javascript-name
      (https://github.com/marklogic-community/RunDMC/blob/master/src/apidoc/model/data-access.xqy#L407).
      -->
      <xsl:variable name="name-override" select="apidoc:name[@class eq 'javascript']/text()"/>
      <xsl:choose>
        <xsl:when test="exists($name-override)">
          <!-- an override exists, use it -->
          <xsl:value-of select="$name-override"/>
        </xsl:when>
        <xsl:otherwise>
          <!-- no override exists, follow convention in local:fixName -->
          <xsl:value-of select="local:fixName(@name)"/>
        </xsl:otherwise>
      </xsl:choose>

      <xsl:text>(</xsl:text>
      <xsl:for-each select="apidoc:params/apidoc:param">
        <xsl:variable name="is-multi" select="matches(@type, ',\.\.\.$')"/>
        <xsl:if test="$is-multi">...</xsl:if>
        <xsl:value-of select="local:fixName(@name)"/>
        <xsl:if test="preceding-sibling::apidoc:param[@name = current()/@name]">2</xsl:if>
        <xsl:if test="not($is-multi) and string(@optional) = 'true'">?</xsl:if>
        <xsl:text>: </xsl:text>
        <xsl:value-of select="local:fixType(@type, true())"/>
        <xsl:if test="$is-multi">[]</xsl:if>
        <xsl:if test="not($is-multi) and string(@optional) = 'true'">|null|undefined</xsl:if>
        <xsl:if test="not(position() = last())">, </xsl:if>
      </xsl:for-each>
      <xsl:text>): </xsl:text>
      <xsl:value-of select="local:fixType(string(apidoc:return), false())"/>
      <xsl:text>;

</xsl:text>
    </xsl:template>

    <xsl:template match="/wrap">
      <xsl:variable name="root" select="."/>

      <xsl:text>// Type definitions for </xsl:text><xsl:value-of select="apidoc:module[exists(apidoc:function)]/@name"/><xsl:text>
</xsl:text>
      <xsl:text>// Definitions: </xsl:text><xsl:value-of select="base-uri(.)"/><xsl:text>

</xsl:text>

        <xsl:for-each select="distinct-values(((//apidoc:module[exists(apidoc:function)]/@lib, 'xdmp')[1], //apidoc:function/@lib))">
          <xsl:sort/>
          <xsl:variable name="lib" select="."/>
          <xsl:variable name="name" select="string-join(local:fixName($lib), '')"/>
          <xsl:variable name="functions" select="$root//apidoc:function[@lib = $lib]"/>
          <xsl:variable name="module" select="$functions/.."/>

          <xsl:text>
/**
</xsl:text>
          <xsl:value-of select="$module/apidoc:summary"/>
          <xsl:text>
**/

</xsl:text>

          <xsl:choose>
          <xsl:when test="$name = 'GlobalObject'">
            <xsl:apply-templates select="$functions">
              <xsl:sort select="@name" />
              <xsl:with-param name="use-function-keyword" select="true()"/>
              <xsl:with-param name="use-declare-keyword" select="true()"/>
            </xsl:apply-templates>
          </xsl:when>
          <xsl:when test="$name = 'alert'">
            <xsl:text>declare module </xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text> {{

</xsl:text>

            <xsl:apply-templates select="$functions">
              <xsl:sort select="@name" />
              <xsl:with-param name="use-function-keyword" select="true()"/>
            </xsl:apply-templates>

            <xsl:text>}}
</xsl:text>
          </xsl:when>
          <xsl:otherwise>
            <xsl:text>interface </xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text>Functions {{

</xsl:text>

            <xsl:apply-templates select="$functions">
              <xsl:sort select="@name" />
            </xsl:apply-templates>

            <xsl:text>}}
</xsl:text>

            <xsl:text>declare var </xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text>:</xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text>Functions
</xsl:text>
          </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each>
    </xsl:template>

    <xsl:function name="local:fixType">
      <xsl:param name="fullType"/>
      <xsl:param name="isForParameter"/>

      <xsl:variable name="fullType" select="translate($fullType, ' ', '')"/>
      <xsl:variable name="type" select="replace(replace($fullType, ',\.\.\.$', ''), '([?*+]|\[\])$', '')"/>
      <xsl:variable name="cardinality" select="substring($fullType, string-length($type) + 1)"/>
      <xsl:variable name="isArray" select="matches($cardinality, '^([*+]|\[\])')"/>
      <xsl:variable name="type" select="if (substring($type, 1, 1) = '(') then substring($type, 2, string-length($type) - 2) else $type"/>

      <xsl:value-of select="local:fixEachType($type, $isArray, $isForParameter)"/>
    </xsl:function>

    <xsl:function name="local:fixEachType">
      <xsl:param name="typeUnion"/>
      <xsl:param name="isArray"/>
      <xsl:param name="isForParameter"/>

      <xsl:variable name="type" select="substring-before(concat($typeUnion,'|'),'|')"/>
      <xsl:variable name="type" select="replace($type, '([?*+]|\[\])$', '')"/>

      <xsl:choose>
        <xsl:when test="matches($type, '^function\(.*\)as(.+)$')">
          <xsl:value-of select="local:fixFunctionType($type, $isArray)"/>
        </xsl:when>
        <xsl:when test="$isArray">
          <xsl:choose>
            <xsl:when test="$type = ('document-node()', 'binary()', 'element()', 'node()')">
              <xsl:value-of select="local:fixArrayType('any', $isForParameter)"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="local:fixArrayType(string-join(local:fixSingleType($type, $isForParameter), ''), $isForParameter)"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="local:fixSingleType($type, $isForParameter)"/>
        </xsl:otherwise>
      </xsl:choose>

      <xsl:if test="contains($typeUnion, '|')">
        <xsl:value-of select="concat('|', string-join(local:fixEachType(substring-after($typeUnion, '|'), $isArray, $isForParameter), ''))"/>
      </xsl:if>
    </xsl:function>

    <xsl:function name="local:fixArrayType">
      <xsl:param name="type"/>
      <xsl:param name="isForParameter"/>
      <xsl:value-of select="concat(concat(if($isForParameter) then 'MLArray<' else 'Sequence<', $type), '>')"/>
    </xsl:function>

    <xsl:function name="local:fixSingleType">
      <xsl:param name="type"/>
      <xsl:param name="isForParameter"/>
      <xsl:choose>
        <!-- workarounds for docs bugs -->
        <xsl:when test="$type = 'query-results-serialize($results)'">MLNode<any></xsl:when>
        <xsl:when test="$type = 'option'">Sequence<any></xsl:when>
        <xsl:when test="$type = 'genericDateTimeArg'">xs.GenericDateTimeArg</xsl:when>
        <xsl:when test="$type = 'rdf:collatedString'">sql.CollatedString</xsl:when>

        <xsl:when test="$type = 'any[]'">Array<any></xsl:when>
        <xsl:when test="$type = ('ValueIterator', 'Sequence')">Sequence<any></xsl:when>
        <xsl:when test="$type = 'document-node()'">DocumentNode<any></xsl:when>
        <xsl:when test="$type = ('binary()')">MLNode<any></xsl:when>
        <xsl:when test="$type = ('element()', 'node()')">
          <xsl:choose>
            <xsl:when test="$isForParameter">MLNodeOrObject<any></xsl:when>
            <xsl:otherwise>MLNode<any></xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:when test="$type = ('empty-seq', 'empty-sequence', 'empty-sequence()', 'null', 'Null')">null</xsl:when>
        <xsl:when test="$type = ('String', 'xs:anyURI', 'xs:NCName', 'xs:string', 'xs:time', 'xs:duration', 'xs:dayTimeDuration')">string</xsl:when>
        <xsl:when test="$type = ('json:array', 'Array')">Array<any></xsl:when>
        <xsl:when test="$type = ('json:object', 'map:map', 'Options')">{[key:string]:any}</xsl:when>
        <xsl:when test="$type = ('function()', 'xdmp:function', 'function(*', 'function(*)', 'function')">() => any</xsl:when>
        <xsl:when test="$type = 'xs:boolean'">boolean</xsl:when>
        <xsl:when test="$type = ('any', 'xs:anyAtomicType', 'item()')">any</xsl:when>
        <xsl:when test="$type = ('xs:dateTime', 'xs:date')">Date</xsl:when>
        <xsl:when test="$type = ('Int', 'double', 'xs:numeric', 'numeric', 'xs:decimal', 'xs:double', 'xs:unsigned', 'xs:unsingedLong', 'xs:unsignedLong', 'xs:int', 'xs:float', 'xs:integer', 'xs:long', 'xs:nonNegativeInteger', 'xs:positiveInteger', 'xs:unsignedInt')">number</xsl:when>
        <xsl:when test="matches($type, '^(schema-)?[Ee]lement\([^)]+\)')">
          <xsl:choose>
            <!-- TODO: Can we perform generics on this? -->
            <xsl:when test="$isForParameter">MLNodeOrObject<any></xsl:when>
            <xsl:otherwise>MLNode<any></xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:otherwise>
          <xsl:variable name="local-name" select="replace($type, '^([^:.]+[:.])?([^\(\)]+)\(?\)?$', '$2')"/>
          <xsl:variable name="fixed-local-name" select="string-join(local:fixName($local-name), '')"/>
          <xsl:variable name="type-name" select="concat(upper-case(substring($fixed-local-name, 1, 1)), substring($fixed-local-name, 2))"/>
          <xsl:choose>
            <xsl:when test="contains($type, ':') or contains($type, '.')">
              <xsl:variable name="prefix-name" select="replace($type, '^([^:.]+[:.])?(.+)$', '$1')"/>
              <xsl:value-of select="concat(substring($prefix-name, 1, string-length($prefix-name) - 1), concat('.', $type-name))"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$type-name"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:function>

    <xsl:function name="local:fixFunctionType">
      <xsl:param name="type"/>
      <xsl:param name="isArray"/>
      <xsl:text>(</xsl:text>
      <xsl:if test="matches($type, '^function\((.+)\)as(.+)$')">
        <xsl:value-of select="local:fixParameters(replace($type, '^function\((.+)\)as(.+)$', '$1'), 1)"/>
      </xsl:if>
      <xsl:text>)=></xsl:text>
      <xsl:variable name="returnType" select="local:fixType(replace($type, '^function\(.*\)as(.+)$', '$1'), false())"/>
      <xsl:choose>
        <xsl:when test="$isArray">
          <xsl:value-of select="local:fixArrayType($returnType, false())"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$returnType"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:function>

    <xsl:function name="local:fixParameters">
      <xsl:param name="parameters"/>
      <xsl:param name="number"/>
      <xsl:value-of select="concat(concat('p', $number), ':')"/>
      <xsl:variable name="type" select="substring-before(concat($parameters,','),',')"/>
      <xsl:value-of select="local:fixType($type, true())"/>
      <xsl:if test="contains($parameters, ',')">
        <xsl:value-of select="concat(',', string-join(local:fixParameters(substring-after($parameters, ','), $number + 1), ''))"/>
      </xsl:if>
    </xsl:function>

    <xsl:function name="local:fixName">
      <xsl:param name="name"/>

      <xsl:choose>
        <xsl:when test="$name = 'default'">defaultVal</xsl:when>
        <xsl:when test="$name = 'function'">functionArg</xsl:when>
        <xsl:when test="$name = 'in'">input</xsl:when>
        <xsl:when test="$name = 'else'">else_</xsl:when>
        <xsl:when test="$name = 'new'">new_</xsl:when>
        <xsl:when test="$name = 'class'">class_</xsl:when>
        <xsl:when test="$name = 'delete'">delete_</xsl:when>
        <xsl:otherwise>
          <xsl:analyze-string select="replace($name, ' ', '_')" regex="-([a-zA-Z])">

            <xsl:matching-substring>
              <xsl:value-of select="upper-case(regex-group(1))"/>
            </xsl:matching-substring>

            <xsl:non-matching-substring>
              <xsl:value-of select="translate(., '-', '')"/>
            </xsl:non-matching-substring>

          </xsl:analyze-string>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:function>

  </xsl:stylesheet>, $xml)

return $definition

xquery version "1.0-ml";

declare variable $xml external;
let $xml := xdmp:unquote("<wrap>" || $xml || "</wrap>")

let $definition := xdmp:xslt-eval(
  <xsl:stylesheet version="2.0" exclude-result-prefixes="#all" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:apidoc="http://marklogic.com/xdmp/apidoc" xmlns:local="local">

    <xsl:output method="text" indent="yes" omit-xml-declaration="yes"/>

    <xsl:template match="@*|node()" mode="#all">
      <xsl:copy>
        <xsl:apply-templates select="@*|node()" mode="#current"/>
      </xsl:copy>
    </xsl:template>

    <xsl:template match="apidoc:function" mode="#all">
      <xsl:param name="use-function-keyword" select="false()"/>
      <xsl:param name="use-declare-keyword" select="false()"/>

      <xsl:text>    /** </xsl:text>
      <xsl:value-of select="normalize-space(apidoc:summary)"/>
      <xsl:text> **/
</xsl:text>

      <xsl:text>  </xsl:text>
      <xsl:if test="$use-declare-keyword">
        <xsl:text>declare </xsl:text>
      </xsl:if>
      <xsl:if test="$use-function-keyword">
        <xsl:text>function </xsl:text>
      </xsl:if>

      <!--
      The function name can usually be derived by convetion. But in some cases (well, one case - toJSON), an override
      is provided in the documentation. In those cases, the override should be used. This is also how this is handled
      in RunDMC in api:javascript-name
      (https://github.com/marklogic-community/RunDMC/blob/master/src/apidoc/model/data-access.xqy#L407).
      -->
      <xsl:variable name="function-name">
        <xsl:value-of>
          <xsl:variable name="name-override" select="apidoc:name[@class eq 'javascript']/text()"/>
          <xsl:choose>
            <xsl:when test="exists($name-override)">
              <!-- an override exists, use it -->
              <xsl:value-of select="$name-override"/>
            </xsl:when>
            <xsl:otherwise>
              <!-- no override exists, follow convention in local:fixName -->
              <xsl:value-of select="local:fixName(@name)"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:value-of>
      </xsl:variable>

      <xsl:value-of select="$function-name"/>
      <xsl:text>(</xsl:text>
      <xsl:for-each select="apidoc:params/apidoc:param">
        <xsl:variable name="is-multi" select="matches(@type, ',\.\.\.$')"/>
        <xsl:if test="$is-multi">...</xsl:if>
        <xsl:value-of select="local:fixName(@name)"/>
        <xsl:if test="preceding-sibling::apidoc:param[@name = current()/@name]">2</xsl:if>
        <xsl:if test="not($is-multi) and string(@optional) = 'true'">?</xsl:if>
        <xsl:text>: </xsl:text>
        <xsl:value-of select="local:fixType(@type, true())"/>
        <xsl:if test="$is-multi">[]</xsl:if>
        <xsl:if test="not($is-multi) and string(@optional) = 'true'">|null|undefined</xsl:if>
        <xsl:if test="not(position() = last())">, </xsl:if>
      </xsl:for-each>
      <xsl:text>): </xsl:text>
      <xsl:value-of select="local:fixType(string(apidoc:return), false())"/>
      <xsl:text>;

</xsl:text>
    </xsl:template>

    <xsl:template match="/wrap">
      <xsl:variable name="root" select="."/>

      <xsl:text>// Type definitions for </xsl:text><xsl:value-of select="apidoc:module[exists(apidoc:function)]/@name"/><xsl:text>
</xsl:text>
      <xsl:text>// Definitions: </xsl:text><xsl:value-of select="base-uri(.)"/><xsl:text>

</xsl:text>

        <xsl:for-each select="distinct-values(((//apidoc:module[exists(apidoc:function)]/@lib, 'xdmp')[1], //apidoc:function/@lib))">
          <xsl:sort/>
          <xsl:variable name="lib" select="."/>
          <xsl:variable name="name" select="string-join(local:fixName($lib), '')"/>
          <xsl:variable name="functions" select="$root//apidoc:function[@lib = $lib]"/>
          <xsl:variable name="module" select="$functions/.."/>

          <xsl:text>
/**
</xsl:text>
          <xsl:value-of select="$module/apidoc:summary"/>
          <xsl:text>
**/

</xsl:text>

          <xsl:choose>
          <xsl:when test="$name = 'GlobalObject'">
            <xsl:apply-templates select="$functions">
              <xsl:sort select="@name" />
              <xsl:with-param name="use-function-keyword" select="true()"/>
              <xsl:with-param name="use-declare-keyword" select="true()"/>
            </xsl:apply-templates>
          </xsl:when>
          <xsl:when test="$name = 'alert'">
            <xsl:text>declare module </xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text> {{

</xsl:text>

            <xsl:apply-templates select="$functions">
              <xsl:sort select="@name" />
              <xsl:with-param name="use-function-keyword" select="true()"/>
            </xsl:apply-templates>

            <xsl:text>}}
</xsl:text>
          </xsl:when>
          <xsl:otherwise>
            <xsl:text>interface </xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text>Functions {{

</xsl:text>

            <xsl:apply-templates select="$functions">
              <xsl:sort select="@name" />
            </xsl:apply-templates>

            <xsl:text>}}
</xsl:text>

            <xsl:text>declare var </xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text>:</xsl:text>
            <xsl:value-of select="$name"/>
            <xsl:text>Functions
</xsl:text>
          </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each>
    </xsl:template>

    <xsl:function name="local:fixType">
      <xsl:param name="fullType"/>
      <xsl:param name="isForParameter"/>

      <xsl:variable name="fullType" select="translate($fullType, ' ', '')"/>
      <xsl:variable name="type" select="replace(replace($fullType, ',\.\.\.$', ''), '([?*+]|\[\])$', '')"/>
      <xsl:variable name="cardinality" select="substring($fullType, string-length($type) + 1)"/>
      <xsl:variable name="isArray" select="matches($cardinality, '^([*+]|\[\])')"/>
      <xsl:variable name="type" select="if (substring($type, 1, 1) = '(') then substring($type, 2, string-length($type) - 2) else $type"/>

      <xsl:value-of select="local:fixEachType($type, $isArray, $isForParameter)"/>
    </xsl:function>

    <xsl:function name="local:fixEachType">
      <xsl:param name="typeUnion"/>
      <xsl:param name="isArray"/>
      <xsl:param name="isForParameter"/>

      <xsl:variable name="type" select="substring-before(concat($typeUnion,'|'),'|')"/>
      <xsl:variable name="type" select="replace($type, '([?*+]|\[\])$', '')"/>

      <xsl:choose>
        <xsl:when test="matches($type, '^function\(.*\)as(.+)$')">
          <xsl:value-of select="local:fixFunctionType($type, $isArray)"/>
        </xsl:when>
        <xsl:when test="$isArray">
          <xsl:choose>
            <xsl:when test="$type = ('document-node()', 'binary()', 'element()', 'node()')">
              <xsl:value-of select="local:fixArrayType('any', $isForParameter)"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="local:fixArrayType(string-join(local:fixSingleType($type, $isForParameter), ''), $isForParameter)"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="local:fixSingleType($type, $isForParameter)"/>
        </xsl:otherwise>
      </xsl:choose>

      <xsl:if test="contains($typeUnion, '|')">
        <xsl:value-of select="concat('|', string-join(local:fixEachType(substring-after($typeUnion, '|'), $isArray, $isForParameter), ''))"/>
      </xsl:if>
    </xsl:function>

    <xsl:function name="local:fixArrayType">
      <xsl:param name="type"/>
      <xsl:param name="isForParameter"/>
      <xsl:value-of select="concat(concat(if($isForParameter) then 'MLArray<' else 'Sequence<', $type), '>')"/>
    </xsl:function>

    <xsl:function name="local:fixSingleType">
      <xsl:param name="type"/>
      <xsl:param name="isForParameter"/>
      <xsl:choose>
        <!-- workarounds for docs bugs -->
        <xsl:when test="$type = 'query-results-serialize($results)'">MLNode<any></xsl:when>
        <xsl:when test="$type = 'option'">Sequence<any></xsl:when>
        <xsl:when test="$type = 'genericDateTimeArg'">xs.GenericDateTimeArg</xsl:when>
        <xsl:when test="$type = 'rdf:collatedString'">sql.CollatedString</xsl:when>

        <xsl:when test="$type = 'any[]'">Array<any></xsl:when>
        <xsl:when test="$type = ('ValueIterator', 'Sequence')">Sequence<any></xsl:when>
        <xsl:when test="$type = 'document-node()'">DocumentNode<any></xsl:when>
        <xsl:when test="$type = ('binary()')">MLNode<any></xsl:when>
        <xsl:when test="$type = ('element()', 'node()')">
          <xsl:choose>
            <xsl:when test="$isForParameter">MLNodeOrObject<any></xsl:when>
            <xsl:otherwise>MLNode<any></xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:when test="$type = ('empty-seq', 'empty-sequence', 'empty-sequence()', 'null', 'Null')">null</xsl:when>
        <xsl:when test="$type = ('String', 'xs:anyURI', 'xs:NCName', 'xs:string', 'xs:time', 'xs:duration', 'xs:dayTimeDuration')">string</xsl:when>
        <xsl:when test="$type = ('json:array', 'Array')">Array<any></xsl:when>
        <xsl:when test="$type = ('json:object', 'map:map', 'Options')">{[key:string]:any}</xsl:when>
        <xsl:when test="$type = ('function()', 'xdmp:function', 'function(*', 'function(*)', 'function')">() => any</xsl:when>
        <xsl:when test="$type = 'xs:boolean'">boolean</xsl:when>
        <xsl:when test="$type = ('any', 'xs:anyAtomicType', 'item()')">any</xsl:when>
        <xsl:when test="$type = ('xs:dateTime', 'xs:date')">Date</xsl:when>
        <xsl:when test="$type = ('Int', 'double', 'xs:numeric', 'numeric', 'xs:decimal', 'xs:double', 'xs:unsigned', 'xs:unsingedLong', 'xs:unsignedLong', 'xs:int', 'xs:float', 'xs:integer', 'xs:long', 'xs:nonNegativeInteger', 'xs:positiveInteger', 'xs:unsignedInt')">number</xsl:when>
        <xsl:when test="matches($type, '^(schema-)?[Ee]lement\([^)]+\)')">
          <xsl:choose>
            <!-- TODO: Can we perform generics on this? -->
            <xsl:when test="$isForParameter">MLNodeOrObject<any></xsl:when>
            <xsl:otherwise>MLNode<any></xsl:otherwise>
          </xsl:choose>
        </xsl:when>
        <xsl:otherwise>
          <xsl:variable name="local-name" select="replace($type, '^([^:.]+[:.])?([^\(\)]+)\(?\)?$', '$2')"/>
          <xsl:variable name="fixed-local-name" select="string-join(local:fixName($local-name), '')"/>
          <xsl:variable name="type-name" select="concat(upper-case(substring($fixed-local-name, 1, 1)), substring($fixed-local-name, 2))"/>
          <xsl:choose>
            <xsl:when test="contains($type, ':') or contains($type, '.')">
              <xsl:variable name="prefix-name" select="replace($type, '^([^:.]+[:.])?(.+)$', '$1')"/>
              <xsl:value-of select="concat(substring($prefix-name, 1, string-length($prefix-name) - 1), concat('.', $type-name))"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$type-name"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:function>

    <xsl:function name="local:fixFunctionType">
      <xsl:param name="type"/>
      <xsl:param name="isArray"/>
      <xsl:text>(</xsl:text>
      <xsl:if test="matches($type, '^function\((.+)\)as(.+)$')">
        <xsl:value-of select="local:fixParameters(replace($type, '^function\((.+)\)as(.+)$', '$1'), 1)"/>
      </xsl:if>
      <xsl:text>)=></xsl:text>
      <xsl:variable name="returnType" select="local:fixType(replace($type, '^function\(.*\)as(.+)$', '$1'), false())"/>
      <xsl:choose>
        <xsl:when test="$isArray">
          <xsl:value-of select="local:fixArrayType($returnType, false())"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$returnType"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:function>

    <xsl:function name="local:fixParameters">
      <xsl:param name="parameters"/>
      <xsl:param name="number"/>
      <xsl:value-of select="concat(concat('p', $number), ':')"/>
      <xsl:variable name="type" select="substring-before(concat($parameters,','),',')"/>
      <xsl:value-of select="local:fixType($type, true())"/>
      <xsl:if test="contains($parameters, ',')">
        <xsl:value-of select="concat(',', string-join(local:fixParameters(substring-after($parameters, ','), $number + 1), ''))"/>
      </xsl:if>
    </xsl:function>

    <xsl:function name="local:fixName">
      <xsl:param name="name"/>

      <xsl:choose>
        <xsl:when test="$name = 'default'">defaultVal</xsl:when>
        <xsl:when test="$name = 'function'">functionArg</xsl:when>
        <xsl:when test="$name = 'in'">input</xsl:when>
        <xsl:when test="$name = 'else'">else_</xsl:when>
        <xsl:when test="$name = 'new'">new_</xsl:when>
        <xsl:when test="$name = 'class'">class_</xsl:when>
        <xsl:when test="$name = 'delete'">delete_</xsl:when>
        <xsl:otherwise>
          <xsl:analyze-string select="replace($name, ' ', '_')" regex="-([a-zA-Z])">

            <xsl:matching-substring>
              <xsl:value-of select="upper-case(regex-group(1))"/>
            </xsl:matching-substring>

            <xsl:non-matching-substring>
              <xsl:value-of select="translate(., '-', '')"/>
            </xsl:non-matching-substring>

          </xsl:analyze-string>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:function>

  </xsl:stylesheet>, $xml)

return $definition


local services cache update works even in case of duplicate records in existing cache

(:~
: This is the Care Services Discovery stored query registry
: @version 1.0
: @see https://github.com/openhie/openinfoman
:
:)
module namespace csd_lsc = "https://github.com/openhie/openinfoman/csd_lsc";
import module namespace csd_psd = "https://github.com/openhie/openinfoman/csd_psd";
import module namespace csd_dm = "https://github.com/openhie/openinfoman/csd_dm";
declare namespace csd = "urn:ihe:iti:csd:2013";
declare default element  namespace   "urn:ihe:iti:csd:2013";

declare function csd_lsc:get_document_name($name) {
  csd_dm:document_source($name)
(:  concat("local_services_cache/",$name,".xml") :)
};

declare function csd_lsc:directory_exists($db,$name) {
  db:is-xml($db,csd_lsc:get_document_name($name))
};

declare updating function csd_lsc:empty_cache($db,$name)
{
  (
    csd_dm:empty($db,$name)
    ,csd_lsc:set_service_directory_mtime($db,$name,$csd_lsc:beginning_of_time)
   )

};

declare function csd_lsc:get_cache($db,$name)
{
 if (csd_lsc:directory_exists($db,$name)) then
    db:open($db,csd_lsc:get_document_name($name))
  else csd_dm:blank_directory()
};

declare variable $csd_lsc:beginning_of_time := '2013-10-01T00:00:00+00:00';
declare variable $csd_lsc:cache_meta_doc := 'local_cache_meta.xml';

declare function csd_lsc:get_cache_data($db,$name)
{
  if ( not( db:is-xml($db,$csd_lsc:cache_meta_doc)))  then
    if ($name) then
      ()
    else
      <cacheData/>
  else
    let $meta_doc :=  db:open($db,$csd_lsc:cache_meta_doc)
    return if ($name) then
      $meta_doc/cacheData/serviceCache[@name = $name]
    else
      $meta_doc/cacheData
};

declare updating function csd_lsc:drop_cache_data($db,$name)
{
  if ( not(db:is-xml($db,$csd_lsc:cache_meta_doc)))  then
    ()
  else
    let $meta_doc :=  db:open($db,$csd_lsc:cache_meta_doc)
      return if ($name) then
	delete node $meta_doc/cacheData/serviceCache[@name = $name]
      else
	delete node $meta_doc/cacheData/*

};

declare  function csd_lsc:cache_meta_exists($db) {
  db:is-xml($db,$csd_lsc:cache_meta_doc)
};

declare updating function csd_lsc:init_cache_meta($db) {
  if ( not(csd_lsc:cache_meta_exists($db)))  then
    db:add($db, <cacheData/>,$csd_lsc:cache_meta_doc)
  else
      ()
};

declare updating function csd_lsc:set_service_directory_mtime($db,$name,$mtime)
{
  ( csd_lsc:init_cache_meta($db)
  ,
  let $meta :=  db:open($db,$csd_lsc:cache_meta_doc)/cacheData
  return
    if (not(exists($meta/serviceCache[@name = $name])))  then
      insert node <serviceCache name="{$name}" mtime="{$csd_lsc:beginning_of_time}"/> into $meta
    else if (not(exists($meta/serviceCache[@name = $name]/@mtime))) then
      let $attr := attribute {"mtime"}{ $csd_lsc:beginning_of_time}
      return ( insert  node $attr into $meta/serviceCache[@name = $name])
    else
      replace value of node $meta/serviceCache[@name = $name]/@mtime with $mtime
  )

};

declare function csd_lsc:get_service_directory_mtime($db,$name)
{
  if ( db:is-xml($db,$csd_lsc:cache_meta_doc))  then
    let $mtime := text{db:open($db,$csd_lsc:cache_meta_doc)/cacheData/serviceCache[@name = $name]/@mtime}
    return if ($mtime) then
	$mtime
      else
	$csd_lsc:beginning_of_time
    else
      $csd_lsc:beginning_of_time

};

declare  updating  function csd_lsc:update_cache($db,$name)
{

  let $mtime :=  csd_lsc:get_service_directory_mtime($db,$name)
(:  querying against self during an update will cause a deadlock :)
(:  see or :)
(:  http://www.mail-archive.com/basex-talk@mailman.uni-konstanz.de/msg02999.htmlhttp://www.mail-archive.com/basex-talk@mailman.uni-konstanz.de/msg02999.html  :)
(:  possible work-around: http://docs.basex.org/wiki/Server_Protocol ? :)
  let $currtime := current-dateTime()
  let $result := csd_psd:poll_service_directory($db,$name,$mtime)
  let $cache_doc := csd_lsc:get_cache($db,$name)

  return (
    csd_lsc:refresh_doc($cache_doc,$result)
    ,csd_lsc:set_service_directory_mtime($db,$name,$currtime)

  )

};

declare updating function csd_lsc:refresh_doc($cache_doc,$updates)
{
  (
   csd_lsc:update_directory($cache_doc/csd:CSD/csd:organizationDirectory,$updates/(csd:CSD/csd:organizationDirectory|csd:organizationDirectory))
  ,csd_lsc:update_directory($cache_doc/csd:CSD/csd:facilityDirectory,$updates/(csd:CSD/csd:facilityDirectory|csd:facilityDirectory))
  ,csd_lsc:update_directory($cache_doc/csd:CSD/csd:serviceDirectory,$updates/(csd:CSD/csd:serviceDirectory|csd:serviceDirectory))
  ,csd_lsc:update_directory($cache_doc/csd:CSD/csd:providerDirectory,$updates/(csd:CSD/csd:providerDirectory|csd:providerDirectory))
  )
};

declare updating function csd_lsc:update_directory($oldDir,$newDir)
{
  for $new in  $newDir/*
  let $old := $oldDir/*[@entityID = $new/@entityID] (:there may be more than one:)
  let $t:=
    for ($o in $old)
    return delete node $old
  return  insert node $new into $oldDir
};

(:~
: This is the Care Services Discovery stored query registry
: @version 1.0
: @see https://github.com/openhie/openinfoman
:
:)
module namespace csd_lsc = "https://github.com/openhie/openinfoman/csd_lsc";
import module namespace csd_psd = "https://github.com/openhie/openinfoman/csd_psd";
import module namespace csd_dm = "https://github.com/openhie/openinfoman/csd_dm";
declare namespace csd = "urn:ihe:iti:csd:2013";
declare default element  namespace   "urn:ihe:iti:csd:2013";

declare function csd_lsc:get_document_name($name) {
  csd_dm:document_source($name)
(:  concat("local_services_cache/",$name,".xml") :)
};

declare function csd_lsc:directory_exists($db,$name) {
  db:is-xml($db,csd_lsc:get_document_name($name))
};

declare updating function csd_lsc:empty_cache($db,$name)
{
  (
    csd_dm:empty($db,$name)
    ,csd_lsc:set_service_directory_mtime($db,$name,$csd_lsc:beginning_of_time)
   )

};

declare function csd_lsc:get_cache($db,$name)
{
 if (csd_lsc:directory_exists($db,$name)) then
    db:open($db,csd_lsc:get_document_name($name))
  else csd_dm:blank_directory()
};

declare variable $csd_lsc:beginning_of_time := '2013-10-01T00:00:00+00:00';
declare variable $csd_lsc:cache_meta_doc := 'local_cache_meta.xml';

declare function csd_lsc:get_cache_data($db,$name)
{
  if ( not( db:is-xml($db,$csd_lsc:cache_meta_doc)))  then
    if ($name) then
      ()
    else
      <cacheData/>
  else
    let $meta_doc :=  db:open($db,$csd_lsc:cache_meta_doc)
    return if ($name) then
      $meta_doc/cacheData/serviceCache[@name = $name]
    else
      $meta_doc/cacheData
};

declare updating function csd_lsc:drop_cache_data($db,$name)
{
  if ( not(db:is-xml($db,$csd_lsc:cache_meta_doc)))  then
    ()
  else
    let $meta_doc :=  db:open($db,$csd_lsc:cache_meta_doc)
      return if ($name) then
	delete node $meta_doc/cacheData/serviceCache[@name = $name]
      else
	delete node $meta_doc/cacheData/*

};

declare  function csd_lsc:cache_meta_exists($db) {
  db:is-xml($db,$csd_lsc:cache_meta_doc)
};

declare updating function csd_lsc:init_cache_meta($db) {
  if ( not(csd_lsc:cache_meta_exists($db)))  then
    db:add($db, <cacheData/>,$csd_lsc:cache_meta_doc)
  else
      ()
};

declare updating function csd_lsc:set_service_directory_mtime($db,$name,$mtime)
{
  ( csd_lsc:init_cache_meta($db)
  ,
  let $meta :=  db:open($db,$csd_lsc:cache_meta_doc)/cacheData
  return
    if (not(exists($meta/serviceCache[@name = $name])))  then
      insert node <serviceCache name="{$name}" mtime="{$csd_lsc:beginning_of_time}"/> into $meta
    else if (not(exists($meta/serviceCache[@name = $name]/@mtime))) then
      let $attr := attribute {"mtime"}{ $csd_lsc:beginning_of_time}
      return ( insert  node $attr into $meta/serviceCache[@name = $name])
    else
      replace value of node $meta/serviceCache[@name = $name]/@mtime with $mtime
  )

};

declare function csd_lsc:get_service_directory_mtime($db,$name)
{
  if ( db:is-xml($db,$csd_lsc:cache_meta_doc))  then
    let $mtime := text{db:open($db,$csd_lsc:cache_meta_doc)/cacheData/serviceCache[@name = $name]/@mtime}
    return if ($mtime) then
	$mtime
      else
	$csd_lsc:beginning_of_time
    else
      $csd_lsc:beginning_of_time

};

declare  updating  function csd_lsc:update_cache($db,$name)
{

  let $mtime :=  csd_lsc:get_service_directory_mtime($db,$name)
(:  querying against self during an update will cause a deadlock :)
(:  see or :)
(:  http://www.mail-archive.com/basex-talk@mailman.uni-konstanz.de/msg02999.htmlhttp://www.mail-archive.com/basex-talk@mailman.uni-konstanz.de/msg02999.html  :)
(:  possible work-around: http://docs.basex.org/wiki/Server_Protocol ? :)
  let $currtime := current-dateTime()
  let $result := csd_psd:poll_service_directory($db,$name,$mtime)
  let $cache_doc := csd_lsc:get_cache($db,$name)

  return (
    csd_lsc:refresh_doc($cache_doc,$result)
    ,csd_lsc:set_service_directory_mtime($db,$name,$currtime)

  )

};

declare updating function csd_lsc:refresh_doc($cache_doc,$updates)
{
  (
   csd_lsc:update_directory($cache_doc/csd:CSD/csd:organizationDirectory,$updates/(csd:CSD/csd:organizationDirectory|csd:organizationDirectory))
  ,csd_lsc:update_directory($cache_doc/csd:CSD/csd:facilityDirectory,$updates/(csd:CSD/csd:facilityDirectory|csd:facilityDirectory))
  ,csd_lsc:update_directory($cache_doc/csd:CSD/csd:serviceDirectory,$updates/(csd:CSD/csd:serviceDirectory|csd:serviceDirectory))
  ,csd_lsc:update_directory($cache_doc/csd:CSD/csd:providerDirectory,$updates/(csd:CSD/csd:providerDirectory|csd:providerDirectory))
  )
};

declare updating function csd_lsc:update_directory($oldDir,$newDir)
{
  for $new in  $newDir/*
  let $old := $oldDir/*[@entityID = $new/@entityID]
  return if ($old) then
    replace node $old with $new
  else
    insert node $new into $oldDir
};


Added function to set attribute defaults per DITA spec

(: =====================================================

   DITA Utilities

   Provides XQuery functions for working with DITA documents,
   including resolving keys and key references, hrefs,
   getting topic navigation titles, etc.

   Author: W. Eliot Kimber

   Copyright (c) 2014, 2015 DITA For Small Teams
   Licensed under Apache License 2

   ===================================================== :)

module namespace df="http://dita-for-small-teams.org/xquery/modules/dita-utils";

import module namespace relpath="http://dita-for-small-teams.org/xquery/modules/relpath-utils";
import module namespace bxutil="http://dita-for-small-teams.org/xquery/modules/basex-utils";

(: Returns true if the specified element is of the specified DITA class.

   Handles the case of a failure to include the trailing space in the @class
   value (this was a bug in MarkLogic 3 in that it did not preserve trailing
   space in #CDATA attributes, corrected in MarkLogic 4).

   elem      - The element to test the class of.
   classSpec - A module/tagname pair, without spaces, e.g. "topic/p"

:)
declare function df:class($elem as element(), $classSpec as xs:string) as xs:boolean {
  (: Refinement from Bob Thomas :)
  let $result := matches($elem/@class, concat(' ', normalize-space($classSpec), ' |\$'))
  return $result
};

(: Gets the base class name of the context element, e.g. "topic/p" :)
 declare function df:getBaseClass($context as element()) as xs:string {
    (: @class value is always "- foo/bar fred/baz " or "+ foo/bar fred/baz " :)
    let $result as xs:string :=
      normalize-space(tokenize($context/@class, ' ')[2])
    return $result
};

declare function df:getHtmlClass($context as element()) as xs:string {
  let $result := if ($context/@outputclass)
                    then string($context/@outputclass)
                    else name($context)
  return $result
};

(: Gets the navigation title for a topic. If there is a navtitle title alternative,
   returns it, otherwise returns the topic's title.

 :)
declare function df:getNavtitleForTopic($topic as element()) as node()* {
   let $navtitle := if ($topic/*[df:class(., 'topic/titlealts')]/*[df:class(., 'topic/navtitle')])
                       then $topic/*[df:class(., 'topic/titlealts')]/*[df:class(., 'topic/navtitle')]/node()
                       else $topic/*[df:class(., 'topic/title')]/node()
   return $navtitle
};

(: Get all the DITA maps in the specified collection :)
declare function df:getMaps($collectionSpec as xs:string) as document-node()* {
  for $doc in collection($collectionSpec) where $doc[contains(/*/@class, ' map/map ')]
      return $doc
};

(: Get all the DITA topic documents in the specified collection, that is,
 : documents whose root element is of type topic/topic or <dita>
 :)
declare function df:getTopicDocs($collectionSpec as xs:string) as document-node()* {
  for $doc in collection($collectionSpec) where $doc[contains(/*/@class, ' topic/topic ') or /*/self::dita]
      return $doc
};

(: Get the title text for the specified element where the element is expected to have topic/title direct child
   Simply returns the text nodes of the title, filtering out elements that don't normally show up in output
   (<data>, <indexterm>, etc.)
:)
declare function df:getTitleText($elem as element()) as xs:string {
   let $title := df:getTitleElement($elem)
   (: FIXME: Implement the filtering, which needs to be a general function :)
   return string($title)
};

(: Get the title element for the specified element where the element is expected to have topic/title direct child :)
declare function df:getTitleElement($elem as element()) as element()? {
   ($elem/*[df:class(., 'topic/title')])[1]
};

(:~
 : Gets the effective value of the attribute, applying DITA-defined default
 : value rules when the attribute does not have a value.
 :
 :)
declare function df:getEffectiveAttributeValue($elem as element(), $attName as xs:string) as xs:string {
   let $att := $elem/@*[name(.) = $attName]
   return
     if ($att)
        then string($att)
        else (: Determined the implicit default, if any :)
         switch ($attName)
          case 'scope' return 'local'
          case 'format' return 'dita'
          default return ''

};

(:~
 : Resolve a topicref to its target topic or map element.
 :
 : Topicref must be a peer or local-scope topicref to a topic
 : or map.
 :
 : Returns the reference element or a <df:error> element if there
 : was some error.
 :
 : Returns empty sequence if the @scope is not
 : peer or local or @format is not dita or ditamap.
 :)
declare function df:resolveTopicRef($topicref as element()) as element()? {
   let $map as document-node() := root($topicref)
   let $format  as xs:string?  := df:getEffectiveAttributeValue($topicref, 'format')
   let $href  as xs:string?    := string($topicref/@href)
   let $keyref as xs:string?   := string($topicref/@keyref)
   let $scope as xs:string?    := df:getEffectiveAttributeValue($topicref, 'scope')

   return if (not(df:class($topicref, 'map/topicref')))
      then <df:error type="not-topicref" xmlns:df="http://dita-for-small-teams.org/xquery/modules/dita-utils">resolveTopicRef(): Element {name($topicref)} is not of class 'map/topicref', class is "{string($topicref/@class)}"</df:error>
      else (: It's a topicref, check the @format value:)
        if (not($format = ('dita', 'ditamap')) or
            ($scope != '' and (not($scope = ('local', 'peer')))))
           then <df:warn>Not format of 'dita' or 'ditamap' or not local or peer scope </df:warn>
           else
             let $targetUri as xs:string := df:getEffectiveTargetUri($topicref)
             let $targetFragId as xs:string :=
                 if (contains($topicref/@href, '#'))
                    then substring-after($topicref/@href, '#')
                    else ''
             return if ($targetUri = '' and $targetFragId = '')
                then ()
                else df:resolveTopicOrMapUri($topicref, $targetUri)

};

declare function df:resolveTopicOrMapUri($topicref as element(), $targetUri as xs:string)
   as element()? {
   let $targetResourcePart as xs:string :=
     if (contains($targetUri, '#'))
        then substring-before($targetUri, '#')
        else $targetUri
   let $topicFragId as xs:string :=
     if (contains($targetUri, '#'))
        then substring-after($targetUri, '#')
        else ''
   let $targetDoc :=
      if ($targetResourcePart != '')
         then
           let $baseUri := base-uri($topicref)
           let $resolvedUri :=
               if (not(starts-with($baseUri, '/')))
                  then (: Handle BaseX way of managing document URLs :)
                     let $parentUri := relpath:getParent($baseUri)
                     return relpath:newFile($parentUri, $targetResourcePart)
                  else resolve-uri($targetResourcePart, $baseUri)
           return doc($resolvedUri)
         else root($topicref)
   return
     if ($targetDoc/*[df:class(., 'topic/topic') or df:class(., 'topic/topic')] )
        then $targetDoc/*[1]
        else if ($targetDoc/*/*[df:class(., 'topic/topic')])
          then $targetDoc/*/*[df:class(., 'topic/topic')][1]
          else $targetDoc/*
 };

(: Give a topicref, return the effective URI of the ultimate target
 : of the topicref.
 :)
declare function df:getEffectiveTargetUri($refElem as element()) as xs:string {
  df:getEffectiveTargetUri(root($refElem)/*, $refElem)
};

(: Give a topicref, return the effective URI of the ultimate target
 : of the topicref.
 :)
declare function df:getEffectiveTargetUri($rootMap, $refElem as element()) as xs:string {
   let $effectiveUri :=
       if ($refElem/@keyref != '')
          then df:getEffectiveUriForKeyref($rootMap, $refElem)
          else string($refElem/@href)
   let $baseUri := relpath:getResourcePartOfUri($effectiveUri)
   let $fragId := relpath:getFragmentId($effectiveUri)
   let $resultBase :=
      if (string($refElem/@copy-to) != '' and not(df:inChunk($refElem)))
         then (: Copy-to in effect, not in a chunk :)
            let $copyTo as xs:string := $refElem/@copy-to
            let $fullUri as xs:string := string(resolve-uri($copyTo, base-uri($refElem)))
            return relpath:getRelativePath(relpath:getParent(base-uri($refElem)), $fullUri)
         else $baseUri
   return $resultBase

};

(:~
 : Given an element with a @keyref attribute, returns
 : the URI of the ultimate resource bound to the referenced
 : key, as defined in the specified root map (note, the map
 : needs to be a resolved map).
 :)
declare function df:getEffectiveUriForKeyref($rootMap, $refElem) as xs:string? {
   let $keyref as xs:string := string($refElem/@keyref)
   let $keyname as xs:string := if (contains($keyref, '/'))
        then tokenize($keyref, '/')[1]
        else $keyref
   (: At this point, need to look up the key reference in the
      the appropriate key space.

    :)
   return "key resolution not yet implemented"
};

(:
 : Returns true if the topicref element points to something and
 : has a @format value of 'ditamap'
 :
 :)
declare function df:isMapRef($topicref as element()) as xs:boolean {
  df:isTopicRef($topicref) and $topicref/@format = 'ditamap'
};

declare function df:isTopicRef($topicref as element()) as xs:boolean {
  df:class($topicref, 'map/topicref') and
      (($topicref/@href and $topicref/@href != '') or
       ($topicref/@keyref and $topicref/@keyref != ''))
};

(:~
 : Get the tree of maps descending from a root map
 :
 : The result is returned a sequence of treeItem elements.
 :)
declare function df:getMapTreeItems($map as element()) as element(treeItem)* {
   let $maprefs := $map//*[df:isMapRef(.)]
   for $mapref in $maprefs
       let $mapElem := df:resolveTopicRef($mapref)
       let $label := if ($mapElem) then df:getTitleText($mapElem)
                               else "Failed to resolve reference to map "
       return <treeItem>
                <label>{$label}</label>
                <properties>
                  <property name="maptype">{name($mapElem)}</property>
                  <property name="url">{bxutil:getPathForDoc(root($mapElem))}</property>
                </properties>
                <children>
                  {df:getMapTreeItems($mapElem)}
                </children>
              </treeItem>
};

(:~
 : Returns true if the context topicref is within
 : the context of a topicref that generates a new content
 : chunk (e.g., chunk="to-content select-branch").
 :)
 declare function df:inChunk($context) as xs:boolean {
   let $nearestChunkSpecifier as element()? := $context/ancestor::*[@chunk != ''][1]
   let $chunkSpec as xs:string := $nearestChunkSpecifier/@chunk
   let $result :=
       contains($chunkSpec, 'to-content') and
              (contains($chunkSpec, 'select-branch') or
               contains($chunkSpec, 'select-document'))
   return $result
};

(: ============== End of Module =================== :)

(: =====================================================

   DITA Utilities

   Provides XQuery functions for working with DITA documents,
   including resolving keys and key references, hrefs,
   getting topic navigation titles, etc.

   Author: W. Eliot Kimber

   Copyright (c) 2014, 2015 DITA For Small Teams
   Licensed under Apache License 2

   ===================================================== :)

module namespace df="http://dita-for-small-teams.org/xquery/modules/dita-utils";

import module namespace relpath="http://dita-for-small-teams.org/xquery/modules/relpath-utils";
import module namespace bxutil="http://dita-for-small-teams.org/xquery/modules/basex-utils";

(: Returns true if the specified element is of the specified DITA class.

   Handles the case of a failure to include the trailing space in the @class
   value (this was a bug in MarkLogic 3 in that it did not preserve trailing
   space in #CDATA attributes, corrected in MarkLogic 4).

   elem      - The element to test the class of.
   classSpec - A module/tagname pair, without spaces, e.g. "topic/p"

:)
declare function df:class($elem as element(), $classSpec as xs:string) as xs:boolean {
  (: Refinement from Bob Thomas :)
  let $result := matches($elem/@class, concat(' ', normalize-space($classSpec), ' |\$'))
  return $result
};

(: Gets the base class name of the context element, e.g. "topic/p" :)
 declare function df:getBaseClass($context as element()) as xs:string {
    (: @class value is always "- foo/bar fred/baz " or "+ foo/bar fred/baz " :)
    let $result as xs:string :=
      normalize-space(tokenize($context/@class, ' ')[2])
    return $result
};

declare function df:getHtmlClass($context as element()) as xs:string {
  let $result := if ($context/@outputclass)
                    then string($context/@outputclass)
                    else name($context)
  return $result
};

(: Gets the navigation title for a topic. If there is a navtitle title alternative,
   returns it, otherwise returns the topic's title.

 :)
declare function df:getNavtitleForTopic($topic as element()) as node()* {
   let $navtitle := if ($topic/*[df:class(., 'topic/titlealts')]/*[df:class(., 'topic/navtitle')])
                       then $topic/*[df:class(., 'topic/titlealts')]/*[df:class(., 'topic/navtitle')]/node()
                       else $topic/*[df:class(., 'topic/title')]/node()
   return $navtitle
};

(: Get all the DITA maps in the specified collection :)
declare function df:getMaps($collectionSpec as xs:string) as document-node()* {
  for $doc in collection($collectionSpec) where $doc[contains(/*/@class, ' map/map ')]
      return $doc
};

(: Get all the DITA topic documents in the specified collection, that is,
 : documents whose root element is of type topic/topic or <dita>
 :)
declare function df:getTopicDocs($collectionSpec as xs:string) as document-node()* {
  for $doc in collection($collectionSpec) where $doc[contains(/*/@class, ' topic/topic ') or /*/self::dita]
      return $doc
};

(: Get the title text for the specified element where the element is expected to have topic/title direct child
   Simply returns the text nodes of the title, filtering out elements that don't normally show up in output
   (<data>, <indexterm>, etc.)
:)
declare function df:getTitleText($elem as element()) as xs:string {
   let $title := df:getTitleElement($elem)
   (: FIXME: Implement the filtering, which needs to be a general function :)
   return string($title)
};

(: Get the title element for the specified element where the element is expected to have topic/title direct child :)
declare function df:getTitleElement($elem as element()) as element()? {
   ($elem/*[df:class(., 'topic/title')])[1]
};

(:~
 : Resolve a topicref to its target topic or map element.
 :
 : Topicref must be a peer or local-scope topicref to a topic
 : or map.
 :
 : Returns the reference element or a <df:error> element if there
 : was some error.
 :
 : Returns empty sequence if the @scope is not
 : peer or local or @format is not dita or ditamap.
 :)
declare function df:resolveTopicRef($topicref as element()) as element()? {
   let $map as document-node() := root($topicref)
   let $format  as xs:string?  := if ($topicref/@format) then string($topicref/@format) else 'dita'
   let $href  as xs:string?    := string($topicref/@href)
   let $keyref as xs:string?   := string($topicref/@keyref)
   let $scope as xs:string?    := if ($topicref/@scope) then string($topicref/@scope) else 'local'

   return if (not(df:class($topicref, 'map/topicref')))
      then <df:error type="not-topicref" xmlns:df="http://dita-for-small-teams.org/xquery/modules/dita-utils">resolveTopicRef(): Element {name($topicref)} is not of class 'map/topicref', class is "{string($topicref/@class)}"</df:error>
      else (: It's a topicref, check the @format value:)
        if (not($format = ('dita', 'ditamap')) or
            ($scope != '' and (not($scope = ('local', 'peer')))))
           then <df:warn>Not format of 'dita' or 'ditamap' or not local or peer scope </df:warn>
           else
             let $targetUri as xs:string := df:getEffectiveTargetUri($topicref)
             let $targetFragId as xs:string :=
                 if (contains($topicref/@href, '#'))
                    then substring-after($topicref/@href, '#')
                    else ''
             return if ($targetUri = '' and $targetFragId = '')
                then ()
                else df:resolveTopicOrMapUri($topicref, $targetUri)

};

declare function df:resolveTopicOrMapUri($topicref as element(), $targetUri as xs:string)
   as element()? {
   let $targetResourcePart as xs:string :=
     if (contains($targetUri, '#'))
        then substring-before($targetUri, '#')
        else $targetUri
   let $topicFragId as xs:string :=
     if (contains($targetUri, '#'))
        then substring-after($targetUri, '#')
        else ''
   let $targetDoc :=
      if ($targetResourcePart != '')
         then
           let $baseUri := base-uri($topicref)
           let $resolvedUri :=
               if (not(starts-with($baseUri, '/')))
                  then (: Handle BaseX way of managing document URLs :)
                     let $parentUri := relpath:getParent($baseUri)
                     return relpath:newFile($parentUri, $targetResourcePart)
                  else resolve-uri($targetResourcePart, $baseUri)
           return doc($resolvedUri)
         else root($topicref)
   return
     if ($targetDoc/*[df:class(., 'topic/topic') or df:class(., 'topic/topic')] )
        then $targetDoc/*[1]
        else if ($targetDoc/*/*[df:class(., 'topic/topic')])
          then $targetDoc/*/*[df:class(., 'topic/topic')][1]
          else $targetDoc/*
 };

(: Give a topicref, return the effective URI of the ultimate target
 : of the topicref.
 :)
declare function df:getEffectiveTargetUri($refElem as element()) as xs:string {
  df:getEffectiveTargetUri(root($refElem)/*, $refElem)
};

(: Give a topicref, return the effective URI of the ultimate target
 : of the topicref.
 :)
declare function df:getEffectiveTargetUri($rootMap, $refElem as element()) as xs:string {
   let $effectiveUri :=
       if ($refElem/@keyref != '')
          then df:getEffectiveUriForKeyref($rootMap, $refElem)
          else string($refElem/@href)
   let $baseUri := relpath:getResourcePartOfUri($effectiveUri)
   let $fragId := relpath:getFragmentId($effectiveUri)
   let $resultBase :=
      if (string($refElem/@copy-to) != '' and not(df:inChunk($refElem)))
         then (: Copy-to in effect, not in a chunk :)
            let $copyTo as xs:string := $refElem/@copy-to
            let $fullUri as xs:string := string(resolve-uri($copyTo, base-uri($refElem)))
            return relpath:getRelativePath(relpath:getParent(base-uri($refElem)), $fullUri)
         else $baseUri
   return $resultBase

};

(:~
 : Given an element with a @keyref attribute, returns
 : the URI of the ultimate resource bound to the referenced
 : key, as defined in the specified root map (note, the map
 : needs to be a resolved map).
 :)
declare function df:getEffectiveUriForKeyref($rootMap, $refElem) as xs:string? {
   let $keyref as xs:string := string($refElem/@keyref)
   let $keyname as xs:string := if (contains($keyref, '/'))
        then tokenize($keyref, '/')[1]
        else $keyref
   (: At this point, need to look up the key reference in the
      the appropriate key space.

    :)
   return "key resolution not yet implemented"
};

(:
 : Returns true if the topicref element points to something and
 : has a @format value of 'ditamap'
 :
 :)
declare function df:isMapRef($topicref as element()) as xs:boolean {
  df:isTopicRef($topicref) and $topicref/@format = 'ditamap'
};

declare function df:isTopicRef($topicref as element()) as xs:boolean {
  df:class($topicref, 'map/topicref') and
      (($topicref/@href and $topicref/@href != '') or
       ($topicref/@keyref and $topicref/@keyref != ''))
};

(:~
 : Get the tree of maps descending from a root map
 :
 : The result is returned a sequence of treeItem elements.
 :)
declare function df:getMapTreeItems($map as element()) as element(treeItem)* {
   let $maprefs := $map//*[df:isMapRef(.)]
   for $mapref in $maprefs
       let $mapElem := df:resolveTopicRef($mapref)
       let $label := if ($mapElem) then df:getTitleText($mapElem)
                               else "Failed to resolve reference to map "
       return <treeItem>
                <label>{$label}</label>
                <properties>
                  <property name="maptype">{name($mapElem)}</property>
                  <property name="url">{bxutil:getPathForDoc(root($mapElem))}</property>
                </properties>
                <children>
                  {df:getMapTreeItems($mapElem)}
                </children>
              </treeItem>
};

(:~
 : Returns true if the context topicref is within
 : the context of a topicref that generates a new content
 : chunk (e.g., chunk="to-content select-branch").
 :)
 declare function df:inChunk($context) as xs:boolean {
   let $nearestChunkSpecifier as element()? := $context/ancestor::*[@chunk != ''][1]
   let $chunkSpec as xs:string := $nearestChunkSpecifier/@chunk
   let $result :=
       contains($chunkSpec, 'to-content') and
              (contains($chunkSpec, 'select-branch') or
               contains($chunkSpec, 'select-document'))
   return $result
};

(: ============== End of Module =================== :)


Test fixes.

Test fixes.

jsoniq version "1.0";

import module namespace ft = "http://zorba.io/modules/full-text";

let $x :=
  <quotes>
    <quote xml:lang="en">
      I love you more than yesterday, less than tomorrow.
    </quote>
    <quote xml:lang="fr">
      Je t'aime plus qu'hier, moins que demain.
    </quote>
  </quotes>

let $includes := $x//quote
let $excludes := ()

let $tokens := ft:tokenize-nodes( $includes, $excludes )

let $t1  := $tokens[1]
let $t2  := $tokens[2]
let $t3  := $tokens[3]

let $t10 := $tokens[10]
let $t11 := $tokens[11]

return  $t1.value = "I"       and $t1.lang = "en"
    and $t2.value = "love"    and $t2.lang = "en"
    and $t3.value = "you"     and $t2.lang = "en"

    and $t10.value = "Je"     and $t10.lang = "fr"
    and $t11.value = "t'aime" and $t11.lang = "fr"

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

import module namespace ft = "http://zorba.io/modules/full-text";

let $x :=
  <quotes>
    <quote xml:lang="en">
      I love you more than yesterday, less than tomorrow.
    </quote>
    <quote xml:lang="fr">
      Je t'aime plus qu'hier, moins que demain.
    </quote>
  </quotes>

let $includes := $x//quote
let $excludes := ()

let $tokens := ft:tokenize-nodes( $includes, $excludes )

let $t1  := validate { $tokens[1] }
let $t2  := validate { $tokens[2] }
let $t3  := validate { $tokens[3] }

let $t10 := validate { $tokens[10] }
let $t11 := validate { $tokens[11] }

return  $t1/@value = "I"        and $t1/@lang = "en"
    and $t2/@value = "love"     and $t2/@lang = "en"
    and $t3/@value = "you"      and $t2/@lang = "en"

    and $t10/@value = "Je"      and $t10/@lang = "fr"
    and $t11/@value = "t'aime"  and $t11/@lang = "fr"

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