add highlighting to default search result format

xquery version "1.0";

declare namespace demo="http://luxdb.net/demo";
declare namespace xsl="http://www.w3.org/1999/XSL/Transform";

import module namespace config="http://luxdb.net/demo/config" at "config.xqy";
import module namespace layout="http://luxdb.net/demo/layout" at "layout.xqy";
import module namespace search="http://luxdb.net/search" at "search-lib.xqy";

declare variable $lux:http as document-node() external;

(: Search Page :)

declare function demo:search-results ($query, $start as xs:integer, $page-size as xs:integer, $sort as xs:string?, $total as xs:integer)
{
  for $doc at $index in subsequence(lux:search ($query, (), $sort), $start, $page-size)
  let $doctype := name($doc/*)
  let $stylesheet-name := concat("file:", $doctype, "-result.xsl")
  let $enquery := search:encode-query(($lux:http/http/params/param,
    <param name="total"><value>{$total}</value></param>,
    <param name="index"><value>{$index}</value></param>
    ))
  let $result-markup :=
    if (doc-available ($stylesheet-name)) then
      lux:transform (doc($stylesheet-name), $doc, ("query", $query, "enquery", $enquery))
    else
      let $uri := base-uri ($doc)
      let $absuri := if (starts-with($uri, "/")) then $uri else concat ("/", $uri)
      return demo:snippet($doc, $absuri, $query, $enquery)
    return
{$result-markup}

};

declare function demo:snippet ($doc as node(), $uri as xs:string, $q as xs:string, $enquery as xs:string)
  as node()
{
  let $title := ($doc/descendant::title/string(),$uri)[1]
  let $hl := lux:highlight ($q, $doc)
  let $match := $hl/descendant::B[1]
  let $length := (80 - string-length($match)) div 2
  let $left-text := demo:snip-left ($match, $length)
  let $right-text := demo:snip-right ($match, $length)
  return

    {$title}

{
      $left-text, $match, $right-text
    }

};

(: get $length text immediately to the left of (preceding) $node TODO:
accept a list of element names that should introduce white space :)

declare function demo:snip-left($node as node(), $length)
  as xs:string
{
  let $prev := $node/preceding::text()[1]
  return if ($prev) then
    let $plen := string-length($prev)
    return
      if ($plen gt $length) then
        concat("...", substring ($prev, string-length($prev) - $length + 1, $length))
      else if ($plen eq $length) then
        if ($prev/preceding::text()) then
          concat("...", $prev)
        else
          $prev/string()
      else
        concat (demo:snip-left($prev, $length - $plen), $prev/string())
  else ""
};

declare function demo:snip-right($node as node(), $length)
  as xs:string
{
  let $next := $node/following::text()[1]
  return if ($next) then
    let $plen := string-length($next)
    return
      if ($plen gt $length) then
        concat(substring ($next, 1, $length), "...")
      else if ($plen eq $length) then
        if ($next/following::text()) then
          concat($next, "...")
        else
          $next/string()
      else
        concat ($next/string(), demo:snip-right($next, $length - $plen))
  else ""
};

declare function demo:search ($params as element(param)*, $start as xs:integer, $page-size as xs:integer)
{
    let $sort := $params[@name='sort']/value/string()
    let $query := search:query ($params)
    let $total := if ($query) then lux:count ($query) else 0
    return
    <search-results total="{$total}">{
        if ($query) then demo:search-results ($query, $start, $page-size, $sort, $total) else ()
    }</search-results>
};

declare function demo:search () {
let $page-size := 20
let $params := $lux:http/http/params/param
let $start as xs:integer := if (number($params[@name='start'])) then xs:integer($params[@name='start']) else 1
let $results := demo:search ($params, $start, $page-size)
let $next := $start + count($results/*)
let $body :=

    <form action="index.xqy" id="search" name="search">
      { search:search-controls ($params) }
      { search:search-description ($start, $results/@total, $next, $params) }
    </form>

{$results/*}

    { search:search-pagination ($start, $results/@total, $next, $page-size) }

      <input type="button" value="erase all" onclick="javascript:if (confirm('Are you sure you want to irretrievably delete the entire contents of your database?')) location.href='delete-all.xqy'" />

    { search:javascript-postamble () }
    {
      let $qname := $params[@name='qname'] where not ($qname) return

    }
  /*

return layout:outer('/index.xqy', $body)
};

demo:search()

xquery version "1.0";

declare namespace demo="http://luxdb.net/demo";

import module namespace config="http://luxdb.net/demo/config" at "config.xqy";
import module namespace layout="http://luxdb.net/demo/layout" at "layout.xqy";
import module namespace search="http://luxdb.net/search" at "search-lib.xqy";

declare variable $lux:http as document-node() external;

(: Search Page :)

declare function demo:search-results ($query, $start as xs:integer, $page-size as xs:integer, $sort as xs:string?, $total as xs:integer)
{
  for $doc at $index in subsequence(lux:search ($query, (), $sort), $start, $page-size)
  let $doctype := name($doc/*)
  let $stylesheet-name := concat("file:", $doctype, "-result.xsl")
  let $enquery := search:encode-query(($lux:http/http/params/param,
    <param name="total"><value>{$total}</value></param>,
    <param name="index"><value>{$index}</value></param>
    ))
  let $result-markup :=
    if (doc-available ($stylesheet-name)) then
      lux:transform (doc($stylesheet-name), $doc, ("query", $query, "enquery", $enquery))
    else
      let $uri := base-uri ($doc)
      return
        if (starts-with($uri, "/")) then
        {$uri}
      else {$uri}
    return
{$result-markup}

};

declare function demo:search ($params as element(param)*, $start as xs:integer, $page-size as xs:integer)
{
    let $sort := $params[@name='sort']/value/string()
    let $query := search:query ($params)
    let $total := if ($query) then lux:count ($query) else 0
    return
    <search-results total="{$total}">{
        if ($query) then demo:search-results ($query, $start, $page-size, $sort, $total) else ()
    }</search-results>
};

declare function demo:search () {
let $page-size := 20
let $params := $lux:http/http/params/param
let $start as xs:integer := if (number($params[@name='start'])) then xs:integer($params[@name='start']) else 1
let $results := demo:search ($params, $start, $page-size)
let $next := $start + count($results/*)
let $body :=

    <form action="index.xqy" id="search" name="search">
      { search:search-controls ($params) }
      { search:search-description ($start, $results/@total, $next, $params) }
    </form>

{$results/*}

    { search:search-pagination ($start, $results/@total, $next, $page-size) }

      <input type="button" value="erase all" onclick="javascript:if (confirm('Are you sure you want to irretrievably delete the entire contents of your database?')) location.href='delete-all.xqy'" />

    { search:javascript-postamble () }
    {
      let $qname := $params[@name='qname'] where not ($qname) return

    }
  /*

return layout:outer('/index.xqy', $body)
};

demo:search()


refactor code within translation comment

(:

 Copyright 2002-2017 MarkLogic Corporation.  All Rights Reserved.

 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 "1.0-ml";

(: This module supports code generation for entity services.
 : This module has no public API.  See entity-services.xqy for
 : the public interface.
 :)
module namespace es-codegen = "http://marklogic.com/entity-services-codegen";

import module namespace esi = "http://marklogic.com/entity-services-impl"
    at "entity-services-impl.xqy";

import module namespace functx   = "http://www.functx.com"
    at "/MarkLogic/functx/functx-1.0-nodoc-2007-01.xqy";

declare namespace es = "http://marklogic.com/entity-services";
declare namespace tde = "http://marklogic.com/xdmp/tde";
declare namespace xq = "http://www.w3.org/2012/xquery";

declare option xdmp:mapping "false";
declare option xq:require-feature "xdmp:three-one";

declare private function es-codegen:casting-function-name(
    $datatype as xs:string
) as xs:string
{
    if ($datatype eq "iri")
    then "sem:iri"
    else "xs:" || $datatype
};

declare private function es-codegen:comment(
    $comment-text as xs:string*
) as xs:string
{
    concat('    (: ', string-join($comment-text, "
       "), '  :)
')
};

declare private function es-codegen:variable-line-for(
    $prefix as xs:string,
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as xs:string
{
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $property-qname :=
        if ($namespace)
        then $namespace-prefix || ":" || $property-name
        else $property-name
    let $properties := map:get($entity-type, "properties")
    let $required-properties := if (empty(map:get($entity-type, "required")))
        then ()
        else json:array-values( map:get($entity-type, "required") )
    let $is-required := $property-name =
            ( map:get($entity-type, "primaryKey"), $required-properties )
    let $is-array :=
            map:get(map:get($properties, $property-name), "datatype")
            eq "array"
    let $property-datatype := esi:resolve-datatype($model, $entity-type-name, $property-name)
    let $casting-function-name := es-codegen:casting-function-name($property-datatype)
    let $wrap-if-array := function($str, $fn, $arrity-ok) {
            if ($is-array and $is-required)
            then concat("json:to-array(", $str, " ! ", $fn, "(.) )")
            else
            if ($is-array)
            then concat("es:extract-array(", $str, ", ", $fn, if ($arrity-ok) then "#1" else (), ")")
            else concat($str, " ! ", $fn, "(.)")
        }
    let $ref :=
        if ($is-array)
        then $properties=>map:get($property-name)=>map:get("items")=>map:get("$ref")
        else $properties=>map:get($property-name)=>map:get("$ref")
    let $path-to-property := concat("$source-node/", $property-qname)
    let $property-comment :=
        if (empty($ref))
        then ""
        else if (contains($ref, "#/definitions"))
        then es-codegen:comment("The following property is a local reference.")
        else
        es-codegen:comment((
            'The following property assigment comes from an external reference.',
            'Its generated value probably requires developer attention.'))
    let $ref-name := functx:substring-after-last($ref, "/")
    let $ref-namespace-prefix :=
        if (contains($ref, "#/definitions"))
        then $model=>map:get("definitions")=>map:get($ref-name)=>map:get("namespacePrefix")
        else ()
    let $ref-namespace :=
        if (contains($ref, "#/definitions"))
        then $model=>map:get("definitions")=>map:get($ref-name)=>map:get("namespace")
        else ()
    let $extract-reference-fn :=
            concat("es:init-instance(?, '",$ref-name,"')",
                if ($ref-namespace-prefix)
                then
                concat("
    =>es:with-namespace('",$ref-namespace,"','",$ref-namespace-prefix,"')")
                else ()
            )
    let $value :=
        if (empty($ref))
        then
            $wrap-if-array($path-to-property, $casting-function-name, true())
        else
            if (contains($ref, "#/definitions"))
            then
                $wrap-if-array($path-to-property, concat($prefix, ":extract-instance-", $ref-name), true())
            else
                $wrap-if-array($path-to-property, $extract-reference-fn, false())

    return
        fn:concat( $property-comment,
                   "    let $",
                   $property-name,
               functx:pad-string-to-length(
                 "  := ",
                 " ",
                 max( ( (string-length($property-name)+4), 16 ) )+1),
               $value)
};

declare private function es-codegen:setter-for(
    $prefix as xs:string,
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
)
{
    (: if a property is required, use map:with to force inclusion :)
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := if (empty(map:get($entity-type, "required")))
        then ()
        else json:array-values( map:get($entity-type, "required") )
    let $is-required := $property-name =
            ( map:get($entity-type, "primaryKey"), $required-properties )
    let $function-call-string :=
        if ($is-required)
        then "        =>   map:with("
        else "        =>es:optional("
    return fn:concat($function-call-string, "'", $property-name, "', $",$property-name,")")
};

declare function es-codegen:instance-converter-generate(
    $model as map:map
) as document-node()
{
    let $info := map:get($model, "info")
    let $title := map:get($info, "title")
    let $prefix := lower-case(substring($title,1,1)) || substring($title,2)
    let $version:= map:get($info, "version")
    let $base-uri := esi:resolve-base-uri($info)
    return
document {
<module>xquery version '1.0-ml';

(:
 This module was generated by MarkLogic Entity Services.
 The source model was {$title}-{$version}

 For usage and extension points, see the Entity Services Developer's Guide

 https://docs.marklogic.com/guide/entity-services

 After modifying this file, put it in your project for deployment to the modules
 database of your application, and check it into your source control system.

 Generated at timestamp: {fn:current-dateTime()}
 :)

module namespace {$prefix}
    = '{$base-uri}{$title}-{$version}';

import module namespace es = 'http://marklogic.com/entity-services'
    at '/MarkLogic/entity-services/entity-services.xqy';

import module namespace json = "http://marklogic.com/xdmp/json"
    at "/MarkLogic/json/json.xqy";

{
  (: namespace declarations :)
    for $entity-type-name in map:keys(map:get($model, "definitions"))
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    return
        if ($namespace)
        then concat("declare namespace ",
             $namespace-prefix,
             " = '",
             $namespace,
             "';
")
        else ()
}

declare option xdmp:mapping 'false';

{
    for $entity-type-name in map:keys(map:get($model, "definitions"))
    return
    <extract-instance>
(:~
 : Extracts instance data, as a map:map, from some source document.
 : @param $source-node  A document or node that contains
 :   data for populating a {$entity-type-name}
 : @return A map:map instance with extracted data and
 :   metadata about the instance.
 :)
declare function {$prefix}:extract-instance-{$entity-type-name}(
    $source as item()?
) as map:map
{{
    let $source-node := es:init-source($source, '{$entity-type-name}')
{
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := $entity-type=>map:get("properties")
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $variable-setters :=
        for $property-name in map:keys($properties)
        return es-codegen:variable-line-for($prefix, $model, $entity-type-name, $property-name)
    return (
        fn:string-join( $variable-setters, "
"),
        "
   ",
        if ($namespace-prefix)
        then
            concat("let $instance := es:init-instance($source-node, '",
                    $entity-type-name,
                    "')",
                    "
    =>es:with-namespace('",$namespace,"','",$namespace-prefix,"')")
        else
            concat("let $instance := es:init-instance($source-node, '",
                    $entity-type-name,
                    "')")
        )
    }
    (: Comment or remove the following line to suppress attachments :)
        =>es:add-attachments($source)

    return
    if (empty($source-node/*))
    then $instance
    else $instance
{
    (: Begin code generation block :)

    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := $entity-type=>map:get("properties")
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $value-lines :=
        (
        for $property-name in map:keys($properties)
        return es-codegen:setter-for($prefix, $model, $entity-type-name, $property-name)
        )
    return fn:string-join($value-lines, "
")
        (: end code generation block :)
    }
}};
</extract-instance>/text()
}

(:~
 : Turns an entity instance into a canonical document structure.
 : Results in either a JSON document, or an XML document that conforms
 : to the entity-services schema.
 : Using this function as-is should be sufficient for most use
 : cases, and will play well with other generated artifacts.
 : @param $entity-instance A map:map instance returned from one of the extract-instance
 :    functions.
 : @param $format Either "json" or "xml". Determines output format of function
 : @return An XML element that encodes the instance.
 :)
declare function {$prefix}:instance-to-canonical(

    $entity-instance as map:map,
    $instance-format as xs:string
) as node()
{{

        if ($instance-format eq "json")
        then xdmp:to-json( {$prefix}:canonicalize($entity-instance) )/node()
        else {$prefix}:instance-to-canonical-xml($entity-instance)
}};

(:~
 : helper function to turn map structure of an instance, which uses specialized
 : keys to encode metadata, into a document tree, which uses the node structure
 : to encode all type and property information.
 :)
declare private function {$prefix}:canonicalize(
    $entity-instance as map:map
) as map:map
{{
    json:object()
    =>map:with( map:get($entity-instance,'$type'),
                if ( map:contains($entity-instance, '$ref') )
                then fn:head( (map:get($entity-instance, '$ref'), json:object()) )
                else
                let $m := json:object()
                let $_ :=
                    for $key in map:keys($entity-instance)
                    let $instance-property := map:get($entity-instance, $key)
                    where ($key castable as xs:NCName)
                    return
                        typeswitch ($instance-property)
                        (: This branch handles embedded objects.  You can choose to prune
                           an entity's representation of extend it with lookups here. :)
                        case json:object
                            return
                                if (empty(map:keys($instance-property)))
                                then map:put($m, $key, json:object())
                                else map:put($m, $key, {$prefix}:canonicalize($instance-property))
                        (: An array can also treated as multiple elements :)
                        case json:array
                            return
                                (
                                for $val at $i in json:array-values($instance-property)
                                return
                                    if ($val instance of json:object)
                                    then json:set-item-at($instance-property, $i, {$prefix}:canonicalize($val))
                                    else (),
                                map:put($m, $key, $instance-property)
                                )

                        (: A sequence of values should be simply treated as multiple elements :)
                        (: TODO is this lossy? :)
                        case item()+
                            return
                                for $val in $instance-property
                                return map:put($m, $key, $val)
                        default return map:put($m, $key, $instance-property)
                return $m)
}};

(:~
 : Turns an entity instance into an XML structure.
 : This out-of-the box implementation traverses a map structure
 : and turns it deterministically into an XML tree.
 : Using this function as-is should be sufficient for most use
 : cases, and will play well with other generated artifacts.
 : @param $entity-instance A map:map instance returned from one of the extract-instance
 :    functions.
 : @return An XML element that encodes the instance.
 :)
declare private function {$prefix}:instance-to-canonical-xml(
    $entity-instance as map:map
) as element()
{{
    (: Construct an element that is named the same as the Entity Type :)
    let $namespace := map:get($entity-instance, "$namespace")
    let $namespace-prefix := map:get($entity-instance, "$namespacePrefix")
    let $nsdecl :=
        if ($namespace) then
        namespace {{ $namespace-prefix }} {{ $namespace }}
        else ()
    let $type-name := map:get($entity-instance, '$type')
    let $type-qname :=
        if ($namespace)
        then fn:QName( $namespace, $namespace-prefix || ":" || $type-name)
        else $type-name
    return
        element {{ $type-qname }}  {{
            $nsdecl,
            if ( map:contains($entity-instance, '$ref') )
            then map:get($entity-instance, '$ref')
            else
                for $key in map:keys($entity-instance)
                let $instance-property := map:get($entity-instance, $key)
                let $ns-key :=
                    if ($namespace and $key castable as xs:NCName)
                    then fn:QName( $namespace, $namespace-prefix || ":" || $key)
                    else $key
                where ($key castable as xs:NCName)
                return
                    typeswitch ($instance-property)
                    (: This branch handles embedded objects.  You can choose to prune
                       an entity's representation of extend it with lookups here. :)
                    case json:object+
                        return
                            for $prop in $instance-property
                            return element {{ $ns-key }} {{ {$prefix}:instance-to-canonical-xml($prop) }}
                    (: An array can also treated as multiple elements :)
                    case json:array
                        return
                            for $val in json:array-values($instance-property)
                            return
                                if ($val instance of json:object)
                                then element {{ $ns-key }} {{
                                    attribute datatype {{ 'array' }},
                                    {$prefix}:instance-to-canonical-xml($val)
                                }}
                                else element {{ $ns-key }} {{
                                    attribute datatype {{ 'array' }},
                                    $val }}
                    (: A sequence of values should be simply treated as multiple elements :)
                    case item()+
                        return
                            for $val in $instance-property
                            return element {{ $ns-key }} {{ $val }}
                    default return element {{ $ns-key }} {{ $instance-property }}
        }}
}};

(:
 : Wraps a canonical instance (returned by instance-to-canonical())
 : within an envelope patterned document, along with the source
 : document, which is stored in an attachments section.
 : @param $entity-instance an instance, as returned by an extract-instance
 : function
 : @param $entity-format Either "json" or "xml", selects the output format
 : for the envelope
 : @return A document which wraps both the canonical instance and source docs.
 :)
declare function {$prefix}:instance-to-envelope(
    $entity-instance as map:map,
    $envelope-format as xs:string
) as document-node()
{{
    let $canonical := {$prefix}:instance-to-canonical($entity-instance, $envelope-format)
    let $attachments := es:serialize-attachments($entity-instance, $envelope-format)
    return
    if ($envelope-format eq "xml")
    then
        document {{
            element es:envelope {{
                element es:instance {{
                    element es:info {{
                        element es:title {{ map:get($entity-instance,'$type') }},
                        element es:version {{ '{$version}' }}
                    }},
                    $canonical
                }},
                $attachments
            }}
        }}
    else
    document {{
        object-node {{ 'envelope' :
            object-node {{ 'instance' :
                object-node {{ 'info' :
                    object-node {{
                        'title' : map:get($entity-instance,'$type'),
                        'version' : '{$version}'
                    }}
                }}
                +
                $canonical
            }}
            +
            $attachments
        }}
    }}
}};

(:
 : @param $entity-instance an instance, as returned by an extract-instance
 : function
 : @return A document which wraps both the canonical instance and source docs.
 :)
declare function {$prefix}:instance-to-envelope(
    $entity-instance as map:map
) as document-node()
{{
    {$prefix}:instance-to-envelope($entity-instance, "xml")
}};

</module>/text()
}

};

declare private function es-codegen:value-for-conversion(
    $source-model as map:map,
    $target-model as map:map,
    $target-entity-type-name as xs:string,
    $target-property-name as xs:string,
    $display-property-name as xs:string,
    $let-expressions as map:map
) as xs:string
{
    let $target-info := map:get($target-model, "info")
    let $target-title := map:get($target-info, "title")
    let $target-prefix := lower-case(substring($target-title,1,1)) || substring($target-title,2)
    let $source-info := map:get($source-model, "info")
    let $source-title := map:get($source-info, "title")
    let $source-prefix := lower-case(substring($source-title,1,1)) || substring($source-title,2)
    let $module-prefix := $target-prefix || "-from-" || $source-prefix

    let $target-entity-type := $target-model
        =>map:get("definitions")
        =>map:get($target-entity-type-name)
    let $target-namespace-prefix := $target-entity-type
        =>map:get("namespacePrefix")
    let $target-property := $target-entity-type
        =>map:get("properties")
        =>map:get($target-property-name)
    let $source-entity-type := $source-model
        =>map:get("definitions")
        =>map:get($target-entity-type-name)    (: this function is only called with matching types/props :)
    let $source-namespace-prefix :=
        if (exists($source-entity-type))
        then $source-entity-type=>map:get("namespacePrefix")
        else ()
    let $source-properties :=
        if (exists($source-entity-type))
        then $source-entity-type=>map:get("properties")
        else ()
    let $is-missing-source :=
        (exists($source-properties) and not($target-property-name = map:keys($source-properties)))
    let $source-correlate :=
        if (exists($source-properties))
        then map:get($source-properties, $target-property-name)
        else ()
    let $target-is-array :=
        if (exists($target-property))
        then $target-property=>map:get("datatype") eq 'array'
        else false()
    let $source-is-array :=
        exists($source-correlate) and $source-correlate=>map:get("datatype") eq 'array'
    let $target-ref :=
        if (exists($target-property))
        then
            if ($target-is-array)
            then $target-property=>map:get("items")=>map:get("$ref")
            else $target-property=>map:get("$ref")
        else ()
    let $source-ref :=
        if ($source-is-array)
        then $source-correlate=>map:get("items")=>map:get("$ref")
        else
        if (exists($source-correlate))
        then $source-correlate=>map:get("$ref")
        else ()
    let $is-scalar-from-ref := empty($target-ref) and exists($source-ref)
    let $target-is-scalar-array := $target-is-array and empty($target-ref)
         and map:get($target-property, "items")=>map:contains("datatype")
    let $source-is-scalar-array := $source-is-array and empty($source-ref)
        and map:get($source-correlate, "items")=>map:contains("datatype")
    let $properties-correlate := not($target-is-scalar-array) and not($source-is-scalar-array)
    let $is-array-from-scalar := $target-is-scalar-array and not($source-is-scalar-array)
    let $is-array-from-array := $target-is-scalar-array and $source-is-scalar-array
    let $truncates-array := not($target-is-array) and $source-is-scalar-array
    let $is-scalar-from-array := empty($target-ref) and $truncates-array

    let $target-datatype := esi:resolve-datatype($target-model, $target-entity-type-name, $target-property-name)
    let $casting-function-name := es-codegen:casting-function-name($target-datatype)
    let $required-properties := if (empty(map:get($target-entity-type, "required")))
        then ()
        else json:array-values( map:get($target-entity-type, "required") )
    let $is-required := $target-property-name =
            ( map:get($target-entity-type, "primaryKey"), $required-properties )
    let $prefixed-source-property-name :=
        if ($source-namespace-prefix)
        then $source-namespace-prefix || ":" || $target-property-name
        else $target-property-name
    let $path-to-property := concat("$source-node/", $prefixed-source-property-name)
    let $target-ref-name := functx:substring-after-last($target-ref, "/")
    let $source-ref-name := functx:substring-after-last($source-ref, "/")
    let $source-ref-namespace-prefix :=
        if (exists($source-model=>map:get("definitions")=>map:get($source-ref-name)))
        then $source-model=>map:get("definitions")=>map:get($source-ref-name)=>map:get("namespacePrefix")
        else ()
    let $prefixed-source-ref-name :=
        if ($source-ref-namespace-prefix)
        then $source-ref-namespace-prefix || ":" || $source-ref-name
        else $source-ref-name
    (: warning: property path override if source is ref and target is scalar :)
    let $path-to-property := if ($is-scalar-from-ref)
                            then $path-to-property || "/" || $prefixed-source-ref-name
                            else $path-to-property
    let $wrap-if-array := function($str, $fn, $arrity-ok) {
            if ($target-is-array and $is-required)
            then concat("json:to-array(", $str, " ! ", $fn, "(.) )")
            else
            if ($target-is-array)
            then concat("es:extract-array(", $str, ", ", $fn, if ($arrity-ok) then "#1" else (), ")")
            else if($source-is-array)
            then concat("fn:head("||$str||")", " ! ", $fn, "(.)")
            else concat($str, " ! ", $fn, "(.)")
        }
    let $is-reference-from-scalar-or-array :=
        exists($target-ref) and empty($source-ref)

    let $comment :=
        if ($is-missing-source)
        then es-codegen:comment((
                "The following property was missing from the source type.",
                "The XPath will not up-convert without intervention."))
        else if ($truncates-array)
        then es-codegen:comment("Warning: potential data loss, truncated array.")
        else ""

    let $extract-fn :=
        if ($is-reference-from-scalar-or-array)
        then
            map:put($let-expressions, $target-ref,
                fn:string-join(
                    (
                    concat('    let $extract-scalar-',$target-ref-name,' := '),
                    if (contains($target-ref, "#/definitions"))
                    then
                        let $ref-namespace-prefix :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespacePrefix")
                        let $ref-namespace :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespace")
                        return
                            if ($ref-namespace)
                            then
                                (
                         "        function($src-node) { ",
                  concat("            es:init-instance($src-node, '", $target-ref-name, "')"),
                  concat("            =>es:with-namespace('",$ref-namespace,"','",$ref-namespace-prefix,"')"),
                         "        }"
                                )
                            else
                  concat( "       es:init-instance(?, '", $target-ref-name, "')")
                    else
                  concat( "       es:init-instance(?, '", $target-ref-name, "')"),
                        ""
                        ),
                    "
"))
        else if ($target-ref)
        then
            map:put($let-expressions, $target-ref,
                fn:string-join(
                    (
                    concat('    let $extract-reference-',$target-ref-name,' := '),
                    if (contains($target-ref, "#/definitions"))
                    then
                        let $ref-namespace-prefix :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespacePrefix")
                        let $ref-namespace :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespace")
                        return
                            if ($ref-namespace)
                            then
                        ("        function($path) { ",
                         "         if ($path/*)",
                         concat("         then ", $module-prefix, ":convert-instance-", $target-ref-name, "($path)"),
                         concat("         else es:init-instance($path, '", $target-ref-name, "')",
                                "
    =>es:with-namespace('",$ref-namespace,"','",$ref-namespace-prefix,"')",
                                "         }"))
                            else
                        ("        function($path) { ",
                         "         if ($path/*)",
                         concat("         then ", $module-prefix, ":convert-instance-", $target-ref-name, "($path)"),
                         concat("         else es:init-instance($path, '", $target-ref-name, "')"),
                        "         }")

                    else
                        concat("        es:init-instance(?, '", $target-ref-name, "')"),
                        ""
                    ),
                "
"))
        else ()

    let $function-call-string :=
        if ($is-required)
        then "    =>   map:with("
        else "    =>es:optional("
    let $property-padding :=
        functx:pad-string-to-length("'" || $display-property-name || "',", " ", max((  (string-length($target-property-name)+5), 10) )+1 )
    let $value :=
        if ($is-scalar-from-array)
        then concat($casting-function-name, "( fn:head(", $path-to-property, ") )")
        else if (empty($target-ref))
        then $wrap-if-array($path-to-property, $casting-function-name, true())
        else if ($is-reference-from-scalar-or-array)
        then $wrap-if-array($path-to-property, "$extract-scalar-" || $target-ref-name, false())
        else $wrap-if-array($path-to-property || "/*", "$extract-reference-" || $target-ref-name, false() )

    let $let-expr := map:put($let-expressions, $target-property-name,
         concat(
            $comment,
            "    let $", $target-property-name, " := ", $value
            ))
    return
        fn:concat($function-call-string,
            $property-padding,
            "$", $target-property-name,
            ")","
")
};

declare function es-codegen:version-translator-generate(
    $source-model as map:map,
    $target-model as map:map
) as document-node()
{
    let $target-info := map:get($target-model, "info")
    let $target-title := map:get($target-info, "title")
    let $target-prefix := lower-case(substring($target-title,1,1)) || substring($target-title,2)
    let $target-version:= map:get($target-info, "version")
    let $target-definitions := map:get($target-model, "definitions")
    let $target-entity-type-names := map:keys($target-definitions)
    let $target-base-uri := esi:resolve-base-uri($target-info)

    let $source-info := map:get($source-model, "info")
    let $source-title := map:get($source-info, "title")
    let $source-prefix := lower-case(substring($source-title,1,1)) || substring($source-title,2)
    let $source-version:= map:get($source-info, "version")
    let $source-definitions := map:get($source-model, "definitions")
    let $source-base-uri := esi:resolve-base-uri($source-info)

    let $module-prefix := $target-prefix || "-from-" || $source-prefix
    let $module-namespace := concat(
        $target-base-uri,
        $target-title,
        "-" ,
        $target-version,
        "-from-",
        $source-title,
        "-",
        $source-version)
    let $target-info := json:object()

(: BEGIN convert instance block :)
    let $convert-instance :=
        for $entity-type-name in $target-entity-type-names
        let $target-namespace := $target-definitions=>map:get($entity-type-name)=>map:get("namespace")
        let $target-namespace-prefix := $target-definitions=>map:get($entity-type-name)=>map:get("namespacePrefix")
        let $info-map := json:object()
        let $_ := map:put($target-info, $entity-type-name, $info-map)
        return
    <convert-instance>
(:~
 : Creates a map:map instance representation of the target
 : entity type {$entity-type-name} from an envelope document
 : containing a source entity instance, that is, instance data
 : of type {$entity-type-name}, version {$source-version}.
 : @param $source  An Entity Services envelope document (<es:envelope>)
 :  or a canonical XML instance of type {$entity-type-name}.
 : @return A map:map instance that holds the data for {$entity-type-name},
 :  version {$target-version}.
 :)
{ if (not($entity-type-name = map:keys($source-definitions)))
    then "
(: Type " || $entity-type-name || " is not in the source model.
 : XPath expressions are created as though there were no change between source and target type.
 :)"
    else () }
declare function {$module-prefix}:convert-instance-{$entity-type-name}(
    $source as node()
) as map:map
{{
    let $source-node := es:init-translation-source($source, '{$entity-type-name}')

{
    (: Begin code generation block :)
    let $let-expressions := json:object()
    let $entity-type := map:get($target-definitions, $entity-type-name)
    let $source-entity-type :=
            if (map:contains($source-definitions, $entity-type-name))
            then map:get($source-definitions, $entity-type-name)
            else map:put($info-map, "missing from source model.", ())
    let $ts-func :=
        function($value) {
            typeswitch($value)
            case empty-sequence() return "None"
            case json:array return fn:string-join(json:array-values($value), ", ")
            default return $value
        }
    let $compare :=
        function($property) {
            (
            $ts-func(map:get($entity-type, $property)),
            if (exists($source-entity-type) and map:contains($source-entity-type, $property))
            then "( in source: " || $ts-func(map:get($source-entity-type, $property)) || " )"
            else "( in source: None )"
            )
        }
    let $_ :=
            (
                map:put($info-map, "primaryKey: ", $compare("primaryKey")),
                map:put($info-map, "required: ", $compare("required")),
                map:put($info-map, "range indexes: ", $compare("rangeIndex")),
                map:put($info-map, "word lexicons: ", $compare("wordLexicon")),
                map:put($info-map, "namespace: ", $compare("namespace")),
                map:put($info-map, "namespace prefix: ", $compare("namespacePrefix"))
            )
    let $properties := map:get($entity-type, "properties")
    let $values :=
        for $property-name in map:keys($properties)
        return
            (: note, this call passes mutable let-expressions for modification :)
            es-codegen:value-for-conversion($source-model,
                $target-model,
                $entity-type-name,
                $property-name,
                $property-name,
                $let-expressions)
    let $missing-properties :=
        if (exists($source-entity-type))
        then
            for $property-name in map:keys(map:get($source-entity-type, "properties"))
            where not($property-name = map:keys(map:get($entity-type, "properties")))
                return
                    es-codegen:value-for-conversion($source-model, $target-model, $entity-type-name, $property-name, "NO TARGET", $let-expressions)
        else ()
    return
        fn:concat(
            fn:string-join(
                for $k in map:keys($let-expressions)
                where $k ne ""
                return map:get($let-expressions, $k), "
"),
'

    return
        es:init-instance($source, "', $entity-type-name, '")',
        if ($target-namespace-prefix)
        then
        concat("
        =>es:with-namespace('",$target-namespace,"','",$target-namespace-prefix,"')")
        else (),
'
       (: Copy attachments from source document to the target :)
        =>es:copy-attachments($source-node)
    (: The following lines are generated from the "',$entity-type-name,'" entity type. :)
',
            fn:string-join($values),
            if (exists($missing-properties))
            then
                es-codegen:comment(fn:string-join(
                    ("The following properties are in the source, but not the target:
",
                    $missing-properties)))
            else ()
            )
    (: end code generation block :)
    }
}};
    </convert-instance>
(: END convert instance block :)

    let $removed-type :=
        (: Make comments for removed ET types :)
        for $removed-entity-type-name in map:keys($source-definitions)
        let $removed-entity-type := map:get($source-definitions, $removed-entity-type-name)
        where not( $removed-entity-type-name = $target-entity-type-names)
        return
<removed-type>
(:
 Entity type {$removed-entity-type-name} is in source document
 but not in target document.
 The following XPath expressions should get values from the source
 instances but there is no specified target.
 This comment can be as a starting point for writing a custom
 version converter.

declare function {$module-prefix}:convert-instance-{$removed-entity-type-name}(
    $source-node as node()
) as map:map
{{
{

map:put($target-info, $removed-entity-type-name, map:entry("", "Removed Type")),
    let $properties := $source-model
        =>map:get("definitions")
        =>map:get($removed-entity-type-name)
        =>map:get("properties")
    let $variable-setters :=
        for $property-name in map:keys($properties)
        return es-codegen:variable-line-for($module-prefix, $source-model, $removed-entity-type-name, $property-name)
    let $values :=
        for $property-name in map:keys($properties)
        return
            (: note, this call passes mutable let-expressions for modification :)
            es-codegen:value-for-conversion($source-model,
                $source-model,
                $removed-entity-type-name,
                $property-name,
                $property-name,
                map:map())
    return

    fn:concat(
        fn:string-join( $variable-setters, "
"),
'

    let $instance := es:init-instance($source-node, "', $removed-entity-type-name, ')")
    return
    $instance
    (: If the source is an envelope or part of an envelope document,
     : copies attachments to the target :)
    =>es:copy-attachments($source-node)

',
    fn:string-join($values)
    )
}
}};
:)
</removed-type>

    return document {
<module>xquery version '1.0-ml';
module namespace {$module-prefix}
    = '{$module-namespace}';

import module namespace es = 'http://marklogic.com/entity-services'
    at '/MarkLogic/entity-services/entity-services.xqy';

{
    (: namespace declarations :)
    let $namespace-accumulator := map:map()
    let $_ :=
        for $entity-type-name in $source-definitions=>map:keys()
        let $entity-type := $source-definitions=>map:get($entity-type-name)
        let $namespace := $entity-type=>map:get("namespace")
        let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
        return
            if ($namespace)
            then map:put($namespace-accumulator, $namespace-prefix, concat("declare namespace ",
                 $namespace-prefix,
                 " = '",
                 $namespace,
                 "';
"))
            else ()
    let $_ :=
        for $entity-type-name in $target-definitions=>map:keys()
        let $entity-type := $target-definitions=>map:get($entity-type-name)
        let $namespace := $entity-type=>map:get("namespace")
        let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
        return
            if ($namespace)
            then map:put($namespace-accumulator, $namespace-prefix, concat("declare namespace ",
                 $namespace-prefix,
                 " = '",
                 $namespace,
                 "';
"))
            else ()
    return
        map:keys($namespace-accumulator) ! map:get($namespace-accumulator, .)
}

declare option xdmp:mapping 'false';

(:
 This module was generated by MarkLogic Entity Services.
 Its purpose is to create instances of entity types
 defined in
 {$target-title || ", version " || $target-version}
 from documents that were persisted according to model
 {$source-title || ", version " || $source-version}

 For usage and extension points, see the Entity Services Developer's Guide

 https://docs.marklogic.com/guide/entity-services

 Generated at timestamp: {fn:current-dateTime()}

 Target Model {$target-title || "-" || $target-version} Info:

 {
map:keys($target-info) !
    function($et) {
        let $et-report := map:get($target-info, $et)
        return
        fn:concat("Type ", $et, ":
",
            fn:string-join(
               map:keys($et-report) !
                    function($report-key) {
                        "    ",
                        $report-key,
                        fn:string-join(map:get($et-report, $report-key), ", "),
                        "
"
                    }(.)
            )
        ),
        "
"
   }(.)
}:)

{
(: this flowr makes order deterministic, which is better for dev UX. :)
for $c in $convert-instance order by $c return $c/text()}

{for $r in $removed-type order by $r return $r/text()}

</module>/text()
    }
};

(:

 Copyright 2002-2017 MarkLogic Corporation.  All Rights Reserved.

 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 "1.0-ml";

(: This module supports code generation for entity services.
 : This module has no public API.  See entity-services.xqy for
 : the public interface.
 :)
module namespace es-codegen = "http://marklogic.com/entity-services-codegen";

import module namespace esi = "http://marklogic.com/entity-services-impl"
    at "entity-services-impl.xqy";

import module namespace functx   = "http://www.functx.com"
    at "/MarkLogic/functx/functx-1.0-nodoc-2007-01.xqy";

declare namespace es = "http://marklogic.com/entity-services";
declare namespace tde = "http://marklogic.com/xdmp/tde";
declare namespace xq = "http://www.w3.org/2012/xquery";

declare option xdmp:mapping "false";
declare option xq:require-feature "xdmp:three-one";

declare private function es-codegen:casting-function-name(
    $datatype as xs:string
) as xs:string
{
    if ($datatype eq "iri")
    then "sem:iri"
    else "xs:" || $datatype
};

declare private function es-codegen:comment(
    $comment-text as xs:string*
) as xs:string
{
    concat('    (: ', string-join($comment-text, "
       "), '  :)
')
};

declare private function es-codegen:variable-line-for(
    $prefix as xs:string,
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as xs:string
{
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $property-qname :=
        if ($namespace)
        then $namespace-prefix || ":" || $property-name
        else $property-name
    let $properties := map:get($entity-type, "properties")
    let $required-properties := if (empty(map:get($entity-type, "required")))
        then ()
        else json:array-values( map:get($entity-type, "required") )
    let $is-required := $property-name =
            ( map:get($entity-type, "primaryKey"), $required-properties )
    let $is-array :=
            map:get(map:get($properties, $property-name), "datatype")
            eq "array"
    let $property-datatype := esi:resolve-datatype($model, $entity-type-name, $property-name)
    let $casting-function-name := es-codegen:casting-function-name($property-datatype)
    let $wrap-if-array := function($str, $fn, $arrity-ok) {
            if ($is-array and $is-required)
            then concat("json:to-array(", $str, " ! ", $fn, "(.) )")
            else
            if ($is-array)
            then concat("es:extract-array(", $str, ", ", $fn, if ($arrity-ok) then "#1" else (), ")")
            else concat($str, " ! ", $fn, "(.)")
        }
    let $ref :=
        if ($is-array)
        then $properties=>map:get($property-name)=>map:get("items")=>map:get("$ref")
        else $properties=>map:get($property-name)=>map:get("$ref")
    let $path-to-property := concat("$source-node/", $property-qname)
    let $property-comment :=
        if (empty($ref))
        then ""
        else if (contains($ref, "#/definitions"))
        then es-codegen:comment("The following property is a local reference.")
        else
        es-codegen:comment((
            'The following property assigment comes from an external reference.',
            'Its generated value probably requires developer attention.'))
    let $ref-name := functx:substring-after-last($ref, "/")
    let $ref-namespace-prefix :=
        if (contains($ref, "#/definitions"))
        then $model=>map:get("definitions")=>map:get($ref-name)=>map:get("namespacePrefix")
        else ()
    let $ref-namespace :=
        if (contains($ref, "#/definitions"))
        then $model=>map:get("definitions")=>map:get($ref-name)=>map:get("namespace")
        else ()
    let $extract-reference-fn :=
            concat("es:init-instance(?, '",$ref-name,"')",
                if ($ref-namespace-prefix)
                then
                concat("
    =>es:with-namespace('",$ref-namespace,"','",$ref-namespace-prefix,"')")
                else ()
            )
    let $value :=
        if (empty($ref))
        then
            $wrap-if-array($path-to-property, $casting-function-name, true())
        else
            if (contains($ref, "#/definitions"))
            then
                $wrap-if-array($path-to-property, concat($prefix, ":extract-instance-", $ref-name), true())
            else
                $wrap-if-array($path-to-property, $extract-reference-fn, false())

    return
        fn:concat( $property-comment,
                   "    let $",
                   $property-name,
               functx:pad-string-to-length(
                 "  := ",
                 " ",
                 max( ( (string-length($property-name)+4), 16 ) )+1),
               $value)
};

declare private function es-codegen:setter-for(
    $prefix as xs:string,
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
)
{
    (: if a property is required, use map:with to force inclusion :)
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := if (empty(map:get($entity-type, "required")))
        then ()
        else json:array-values( map:get($entity-type, "required") )
    let $is-required := $property-name =
            ( map:get($entity-type, "primaryKey"), $required-properties )
    let $function-call-string :=
        if ($is-required)
        then "        =>   map:with("
        else "        =>es:optional("
    return fn:concat($function-call-string, "'", $property-name, "', $",$property-name,")")
};

declare function es-codegen:instance-converter-generate(
    $model as map:map
) as document-node()
{
    let $info := map:get($model, "info")
    let $title := map:get($info, "title")
    let $prefix := lower-case(substring($title,1,1)) || substring($title,2)
    let $version:= map:get($info, "version")
    let $base-uri := esi:resolve-base-uri($info)
    return
document {
<module>xquery version '1.0-ml';

(:
 This module was generated by MarkLogic Entity Services.
 The source model was {$title}-{$version}

 For usage and extension points, see the Entity Services Developer's Guide

 https://docs.marklogic.com/guide/entity-services

 After modifying this file, put it in your project for deployment to the modules
 database of your application, and check it into your source control system.

 Generated at timestamp: {fn:current-dateTime()}
 :)

module namespace {$prefix}
    = '{$base-uri}{$title}-{$version}';

import module namespace es = 'http://marklogic.com/entity-services'
    at '/MarkLogic/entity-services/entity-services.xqy';

import module namespace json = "http://marklogic.com/xdmp/json"
    at "/MarkLogic/json/json.xqy";

{
  (: namespace declarations :)
    for $entity-type-name in map:keys(map:get($model, "definitions"))
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    return
        if ($namespace)
        then concat("declare namespace ",
             $namespace-prefix,
             " = '",
             $namespace,
             "';
")
        else ()
}

declare option xdmp:mapping 'false';

{
    for $entity-type-name in map:keys(map:get($model, "definitions"))
    return
    <extract-instance>
(:~
 : Extracts instance data, as a map:map, from some source document.
 : @param $source-node  A document or node that contains
 :   data for populating a {$entity-type-name}
 : @return A map:map instance with extracted data and
 :   metadata about the instance.
 :)
declare function {$prefix}:extract-instance-{$entity-type-name}(
    $source as item()?
) as map:map
{{
    let $source-node := es:init-source($source, '{$entity-type-name}')
{
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := $entity-type=>map:get("properties")
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $variable-setters :=
        for $property-name in map:keys($properties)
        return es-codegen:variable-line-for($prefix, $model, $entity-type-name, $property-name)
    return (
        fn:string-join( $variable-setters, "
"),
        "
   ",
        if ($namespace-prefix)
        then
            concat("let $instance := es:init-instance($source-node, '",
                    $entity-type-name,
                    "')",
                    "
    =>es:with-namespace('",$namespace,"','",$namespace-prefix,"')")
        else
            concat("let $instance := es:init-instance($source-node, '",
                    $entity-type-name,
                    "')")
        )
    }
    (: Comment or remove the following line to suppress attachments :)
        =>es:add-attachments($source)

    return
    if (empty($source-node/*))
    then $instance
    else $instance
{
    (: Begin code generation block :)

    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := $entity-type=>map:get("properties")
    let $namespace := $entity-type=>map:get("namespace")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $value-lines :=
        (
        for $property-name in map:keys($properties)
        return es-codegen:setter-for($prefix, $model, $entity-type-name, $property-name)
        )
    return fn:string-join($value-lines, "
")
        (: end code generation block :)
    }
}};
</extract-instance>/text()
}

(:~
 : Turns an entity instance into a canonical document structure.
 : Results in either a JSON document, or an XML document that conforms
 : to the entity-services schema.
 : Using this function as-is should be sufficient for most use
 : cases, and will play well with other generated artifacts.
 : @param $entity-instance A map:map instance returned from one of the extract-instance
 :    functions.
 : @param $format Either "json" or "xml". Determines output format of function
 : @return An XML element that encodes the instance.
 :)
declare function {$prefix}:instance-to-canonical(

    $entity-instance as map:map,
    $instance-format as xs:string
) as node()
{{

        if ($instance-format eq "json")
        then xdmp:to-json( {$prefix}:canonicalize($entity-instance) )/node()
        else {$prefix}:instance-to-canonical-xml($entity-instance)
}};

(:~
 : helper function to turn map structure of an instance, which uses specialized
 : keys to encode metadata, into a document tree, which uses the node structure
 : to encode all type and property information.
 :)
declare private function {$prefix}:canonicalize(
    $entity-instance as map:map
) as map:map
{{
    json:object()
    =>map:with( map:get($entity-instance,'$type'),
                if ( map:contains($entity-instance, '$ref') )
                then fn:head( (map:get($entity-instance, '$ref'), json:object()) )
                else
                let $m := json:object()
                let $_ :=
                    for $key in map:keys($entity-instance)
                    let $instance-property := map:get($entity-instance, $key)
                    where ($key castable as xs:NCName)
                    return
                        typeswitch ($instance-property)
                        (: This branch handles embedded objects.  You can choose to prune
                           an entity's representation of extend it with lookups here. :)
                        case json:object
                            return
                                if (empty(map:keys($instance-property)))
                                then map:put($m, $key, json:object())
                                else map:put($m, $key, {$prefix}:canonicalize($instance-property))
                        (: An array can also treated as multiple elements :)
                        case json:array
                            return
                                (
                                for $val at $i in json:array-values($instance-property)
                                return
                                    if ($val instance of json:object)
                                    then json:set-item-at($instance-property, $i, {$prefix}:canonicalize($val))
                                    else (),
                                map:put($m, $key, $instance-property)
                                )

                        (: A sequence of values should be simply treated as multiple elements :)
                        (: TODO is this lossy? :)
                        case item()+
                            return
                                for $val in $instance-property
                                return map:put($m, $key, $val)
                        default return map:put($m, $key, $instance-property)
                return $m)
}};

(:~
 : Turns an entity instance into an XML structure.
 : This out-of-the box implementation traverses a map structure
 : and turns it deterministically into an XML tree.
 : Using this function as-is should be sufficient for most use
 : cases, and will play well with other generated artifacts.
 : @param $entity-instance A map:map instance returned from one of the extract-instance
 :    functions.
 : @return An XML element that encodes the instance.
 :)
declare private function {$prefix}:instance-to-canonical-xml(
    $entity-instance as map:map
) as element()
{{
    (: Construct an element that is named the same as the Entity Type :)
    let $namespace := map:get($entity-instance, "$namespace")
    let $namespace-prefix := map:get($entity-instance, "$namespacePrefix")
    let $nsdecl :=
        if ($namespace) then
        namespace {{ $namespace-prefix }} {{ $namespace }}
        else ()
    let $type-name := map:get($entity-instance, '$type')
    let $type-qname :=
        if ($namespace)
        then fn:QName( $namespace, $namespace-prefix || ":" || $type-name)
        else $type-name
    return
        element {{ $type-qname }}  {{
            $nsdecl,
            if ( map:contains($entity-instance, '$ref') )
            then map:get($entity-instance, '$ref')
            else
                for $key in map:keys($entity-instance)
                let $instance-property := map:get($entity-instance, $key)
                let $ns-key :=
                    if ($namespace and $key castable as xs:NCName)
                    then fn:QName( $namespace, $namespace-prefix || ":" || $key)
                    else $key
                where ($key castable as xs:NCName)
                return
                    typeswitch ($instance-property)
                    (: This branch handles embedded objects.  You can choose to prune
                       an entity's representation of extend it with lookups here. :)
                    case json:object+
                        return
                            for $prop in $instance-property
                            return element {{ $ns-key }} {{ {$prefix}:instance-to-canonical-xml($prop) }}
                    (: An array can also treated as multiple elements :)
                    case json:array
                        return
                            for $val in json:array-values($instance-property)
                            return
                                if ($val instance of json:object)
                                then element {{ $ns-key }} {{
                                    attribute datatype {{ 'array' }},
                                    {$prefix}:instance-to-canonical-xml($val)
                                }}
                                else element {{ $ns-key }} {{
                                    attribute datatype {{ 'array' }},
                                    $val }}
                    (: A sequence of values should be simply treated as multiple elements :)
                    case item()+
                        return
                            for $val in $instance-property
                            return element {{ $ns-key }} {{ $val }}
                    default return element {{ $ns-key }} {{ $instance-property }}
        }}
}};

(:
 : Wraps a canonical instance (returned by instance-to-canonical())
 : within an envelope patterned document, along with the source
 : document, which is stored in an attachments section.
 : @param $entity-instance an instance, as returned by an extract-instance
 : function
 : @param $entity-format Either "json" or "xml", selects the output format
 : for the envelope
 : @return A document which wraps both the canonical instance and source docs.
 :)
declare function {$prefix}:instance-to-envelope(
    $entity-instance as map:map,
    $envelope-format as xs:string
) as document-node()
{{
    let $canonical := {$prefix}:instance-to-canonical($entity-instance, $envelope-format)
    let $attachments := es:serialize-attachments($entity-instance, $envelope-format)
    return
    if ($envelope-format eq "xml")
    then
        document {{
            element es:envelope {{
                element es:instance {{
                    element es:info {{
                        element es:title {{ map:get($entity-instance,'$type') }},
                        element es:version {{ '{$version}' }}
                    }},
                    $canonical
                }},
                $attachments
            }}
        }}
    else
    document {{
        object-node {{ 'envelope' :
            object-node {{ 'instance' :
                object-node {{ 'info' :
                    object-node {{
                        'title' : map:get($entity-instance,'$type'),
                        'version' : '{$version}'
                    }}
                }}
                +
                $canonical
            }}
            +
            $attachments
        }}
    }}
}};

(:
 : @param $entity-instance an instance, as returned by an extract-instance
 : function
 : @return A document which wraps both the canonical instance and source docs.
 :)
declare function {$prefix}:instance-to-envelope(
    $entity-instance as map:map
) as document-node()
{{
    {$prefix}:instance-to-envelope($entity-instance, "xml")
}};

</module>/text()
}

};

declare private function es-codegen:value-for-conversion(
    $source-model as map:map,
    $target-model as map:map,
    $target-entity-type-name as xs:string,
    $target-property-name as xs:string,
    $display-property-name as xs:string,
    $let-expressions as map:map
) as xs:string
{
    let $target-info := map:get($target-model, "info")
    let $target-title := map:get($target-info, "title")
    let $target-prefix := lower-case(substring($target-title,1,1)) || substring($target-title,2)
    let $source-info := map:get($source-model, "info")
    let $source-title := map:get($source-info, "title")
    let $source-prefix := lower-case(substring($source-title,1,1)) || substring($source-title,2)
    let $module-prefix := $target-prefix || "-from-" || $source-prefix

    let $target-entity-type := $target-model
        =>map:get("definitions")
        =>map:get($target-entity-type-name)
    let $target-namespace-prefix := $target-entity-type
        =>map:get("namespacePrefix")
    let $target-property := $target-entity-type
        =>map:get("properties")
        =>map:get($target-property-name)
    let $source-entity-type := $source-model
        =>map:get("definitions")
        =>map:get($target-entity-type-name)    (: this function is only called with matching types/props :)
    let $source-namespace-prefix :=
        if (exists($source-entity-type))
        then $source-entity-type=>map:get("namespacePrefix")
        else ()
    let $source-properties :=
        if (exists($source-entity-type))
        then $source-entity-type=>map:get("properties")
        else ()
    let $is-missing-source :=
        (exists($source-properties) and not($target-property-name = map:keys($source-properties)))
    let $source-correlate :=
        if (exists($source-properties))
        then map:get($source-properties, $target-property-name)
        else ()
    let $target-is-array :=
        if (exists($target-property))
        then $target-property=>map:get("datatype") eq 'array'
        else false()
    let $source-is-array :=
        exists($source-correlate) and $source-correlate=>map:get("datatype") eq 'array'
    let $target-ref :=
        if (exists($target-property))
        then
            if ($target-is-array)
            then $target-property=>map:get("items")=>map:get("$ref")
            else $target-property=>map:get("$ref")
        else ()
    let $source-ref :=
        if ($source-is-array)
        then $source-correlate=>map:get("items")=>map:get("$ref")
        else
        if (exists($source-correlate))
        then $source-correlate=>map:get("$ref")
        else ()
    let $is-scalar-from-ref := empty($target-ref) and exists($source-ref)
    let $target-is-scalar-array := $target-is-array and empty($target-ref)
         and map:get($target-property, "items")=>map:contains("datatype")
    let $source-is-scalar-array := $source-is-array and empty($source-ref)
        and map:get($source-correlate, "items")=>map:contains("datatype")
    let $properties-correlate := not($target-is-scalar-array) and not($source-is-scalar-array)
    let $is-array-from-scalar := $target-is-scalar-array and not($source-is-scalar-array)
    let $is-array-from-array := $target-is-scalar-array and $source-is-scalar-array
    let $truncates-array := not($target-is-array) and $source-is-scalar-array
    let $is-scalar-from-array := empty($target-ref) and $truncates-array

    let $target-datatype := esi:resolve-datatype($target-model, $target-entity-type-name, $target-property-name)
    let $casting-function-name := es-codegen:casting-function-name($target-datatype)
    let $required-properties := if (empty(map:get($target-entity-type, "required")))
        then ()
        else json:array-values( map:get($target-entity-type, "required") )
    let $is-required := $target-property-name =
            ( map:get($target-entity-type, "primaryKey"), $required-properties )
    let $prefixed-source-property-name :=
        if ($source-namespace-prefix)
        then $source-namespace-prefix || ":" || $target-property-name
        else $target-property-name
    let $path-to-property := concat("$source-node/", $prefixed-source-property-name)
    let $target-ref-name := functx:substring-after-last($target-ref, "/")
    let $source-ref-name := functx:substring-after-last($source-ref, "/")
    let $source-ref-namespace-prefix :=
        if (exists($source-model=>map:get("definitions")=>map:get($source-ref-name)))
        then $source-model=>map:get("definitions")=>map:get($source-ref-name)=>map:get("namespacePrefix")
        else ()
    let $prefixed-source-ref-name :=
        if ($source-ref-namespace-prefix)
        then $source-ref-namespace-prefix || ":" || $source-ref-name
        else $source-ref-name
    (: warning: property path override if source is ref and target is scalar :)
    let $path-to-property := if ($is-scalar-from-ref)
                            then $path-to-property || "/" || $prefixed-source-ref-name
                            else $path-to-property
    let $wrap-if-array := function($str, $fn, $arrity-ok) {
            if ($target-is-array and $is-required)
            then concat("json:to-array(", $str, " ! ", $fn, "(.) )")
            else
            if ($target-is-array)
            then concat("es:extract-array(", $str, ", ", $fn, if ($arrity-ok) then "#1" else (), ")")
            else if($source-is-array)
            then concat("fn:head("||$str||")", " ! ", $fn, "(.)")
            else concat($str, " ! ", $fn, "(.)")
        }
    let $is-reference-from-scalar-or-array :=
        exists($target-ref) and empty($source-ref)

    let $comment :=
        if ($is-missing-source)
        then es-codegen:comment((
                "The following property was missing from the source type.",
                "The XPath will not up-convert without intervention."))
        else if ($truncates-array)
        then es-codegen:comment("Warning: potential data loss, truncated array.")
        else ""

    let $extract-fn :=
        if ($is-reference-from-scalar-or-array)
        then
            map:put($let-expressions, $target-ref,
                fn:string-join(
                    (
                    concat('    let $extract-scalar-',$target-ref-name,' := '),
                    if (contains($target-ref, "#/definitions"))
                    then
                        let $ref-namespace-prefix :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespacePrefix")
                        let $ref-namespace :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespace")
                        return
                            if ($ref-namespace)
                            then
                                (
                         "        function($src-node) { ",
                  concat("            es:init-instance($src-node, '", $target-ref-name, "')"),
                  concat("            =>es:with-namespace('",$ref-namespace,"','",$ref-namespace-prefix,"')"),
                         "        }"
                                )
                            else
                  concat( "       es:init-instance(?, '", $target-ref-name, "')")
                    else
                  concat( "       es:init-instance(?, '", $target-ref-name, "')"),
                        ""
                        ),
                    "
"))
        else if ($target-ref)
        then
            map:put($let-expressions, $target-ref,
                fn:string-join(
                    (
                    concat('    let $extract-reference-',$target-ref-name,' := '),
                    if (contains($target-ref, "#/definitions"))
                    then
                        let $ref-namespace-prefix :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespacePrefix")
                        let $ref-namespace :=
                            $target-model=>map:get("definitions")=>map:get($target-ref-name)=>map:get("namespace")
                        return
                            if ($ref-namespace)
                            then
                        ("        function($path) { ",
                         "         if ($path/*)",
                         concat("         then ", $module-prefix, ":convert-instance-", $target-ref-name, "($path)"),
                         concat("         else es:init-instance($path, '", $target-ref-name, "')",
                                "
    =>es:with-namespace('",$ref-namespace,"','",$ref-namespace-prefix,"')",
                                "         }"))
                            else
                        ("        function($path) { ",
                         "         if ($path/*)",
                         concat("         then ", $module-prefix, ":convert-instance-", $target-ref-name, "($path)"),
                         concat("         else es:init-instance($path, '", $target-ref-name, "')"),
                        "         }")

                    else
                        concat("        es:init-instance(?, '", $target-ref-name, "')"),
                        ""
                    ),
                "
"))
        else ()

    let $function-call-string :=
        if ($is-required)
        then "    =>   map:with("
        else "    =>es:optional("
    let $property-padding :=
        functx:pad-string-to-length("'" || $display-property-name || "',", " ", max((  (string-length($target-property-name)+5), 10) )+1 )
    let $value :=
        if ($is-scalar-from-array)
        then concat($casting-function-name, "( fn:head(", $path-to-property, ") )")
        else if (empty($target-ref))
        then $wrap-if-array($path-to-property, $casting-function-name, true())
        else if ($is-reference-from-scalar-or-array)
        then $wrap-if-array($path-to-property, "$extract-scalar-" || $target-ref-name, false())
        else $wrap-if-array($path-to-property || "/*", "$extract-reference-" || $target-ref-name, false() )

    let $let-expr := map:put($let-expressions, $target-property-name,
         concat(
            $comment,
            "    let $", $target-property-name, " := ", $value
            ))
    return
        fn:concat($function-call-string,
            $property-padding,
            "$", $target-property-name,
            ")","
")
};

declare function es-codegen:version-translator-generate(
    $source-model as map:map,
    $target-model as map:map
) as document-node()
{
    let $target-info := map:get($target-model, "info")
    let $target-title := map:get($target-info, "title")
    let $target-prefix := lower-case(substring($target-title,1,1)) || substring($target-title,2)
    let $target-version:= map:get($target-info, "version")
    let $target-definitions := map:get($target-model, "definitions")
    let $target-entity-type-names := map:keys($target-definitions)
    let $target-base-uri := esi:resolve-base-uri($target-info)

    let $source-info := map:get($source-model, "info")
    let $source-title := map:get($source-info, "title")
    let $source-prefix := lower-case(substring($source-title,1,1)) || substring($source-title,2)
    let $source-version:= map:get($source-info, "version")
    let $source-definitions := map:get($source-model, "definitions")
    let $source-base-uri := esi:resolve-base-uri($source-info)

    let $module-prefix := $target-prefix || "-from-" || $source-prefix
    let $module-namespace := concat(
        $target-base-uri,
        $target-title,
        "-" ,
        $target-version,
        "-from-",
        $source-title,
        "-",
        $source-version)
    let $target-info := json:object()

(: BEGIN convert instance block :)
    let $convert-instance :=
        for $entity-type-name in $target-entity-type-names
        let $target-namespace := $target-definitions=>map:get($entity-type-name)=>map:get("namespace")
        let $target-namespace-prefix := $target-definitions=>map:get($entity-type-name)=>map:get("namespacePrefix")
        let $info-map := json:object()
        let $_ := map:put($target-info, $entity-type-name, $info-map)
        return
    <convert-instance>
(:~
 : Creates a map:map instance representation of the target
 : entity type {$entity-type-name} from an envelope document
 : containing a source entity instance, that is, instance data
 : of type {$entity-type-name}, version {$source-version}.
 : @param $source  An Entity Services envelope document (<es:envelope>)
 :  or a canonical XML instance of type {$entity-type-name}.
 : @return A map:map instance that holds the data for {$entity-type-name},
 :  version {$target-version}.
 :)
{ if (not($entity-type-name = map:keys($source-definitions)))
    then "
(: Type " || $entity-type-name || " is not in the source model.
 : XPath expressions are created as though there were no change between source and target type.
 :)"
    else () }
declare function {$module-prefix}:convert-instance-{$entity-type-name}(
    $source as node()
) as map:map
{{
    let $source-node := es:init-translation-source($source, '{$entity-type-name}')

{
    (: Begin code generation block :)
    let $let-expressions := json:object()
    let $entity-type := map:get($target-definitions, $entity-type-name)
    let $source-entity-type :=
            if (map:contains($source-definitions, $entity-type-name))
            then map:get($source-definitions, $entity-type-name)
            else map:put($info-map, "missing from source model.", ())
    let $ts-func :=
        function($value) {
            typeswitch($value)
            case empty-sequence() return "None"
            case json:array return fn:string-join(json:array-values($value), ", ")
            default return $value
        }
    let $compare :=
        function($property) {
            (
            $ts-func(map:get($entity-type, $property)),
            if (exists($source-entity-type) and map:contains($source-entity-type, $property))
            then "( in source: " || $ts-func(map:get($source-entity-type, $property)) || " )"
            else "( in source: None )"
            )
        }
    let $_ :=
            (
                map:put($info-map, "primaryKey: ", $compare("primaryKey")),
                map:put($info-map, "required: ", $compare("required")),
                map:put($info-map, "range indexes: ", $compare("rangeIndex")),
                map:put($info-map, "word lexicons: ", $compare("wordLexicon")),
                map:put($info-map, "namespace: ", $compare("namespace")),
                map:put($info-map, "namespace prefix: ", $compare("namespacePrefix"))
            )
    let $properties := map:get($entity-type, "properties")
    let $values :=
        for $property-name in map:keys($properties)
        return
            (: note, this call passes mutable let-expressions for modification :)
            es-codegen:value-for-conversion($source-model,
                $target-model,
                $entity-type-name,
                $property-name,
                $property-name,
                $let-expressions)
    let $missing-properties :=
        if (exists($source-entity-type))
        then
            for $property-name in map:keys(map:get($source-entity-type, "properties"))
            where not($property-name = map:keys(map:get($entity-type, "properties")))
                return
                    es-codegen:value-for-conversion($source-model, $target-model, $entity-type-name, $property-name, "NO TARGET", $let-expressions)
        else ()
    return
        fn:concat(
            fn:string-join(
                for $k in map:keys($let-expressions)
                where $k ne ""
                return map:get($let-expressions, $k), "
"),
'

    return
        es:init-instance($source, "', $entity-type-name, '")',
        if ($target-namespace-prefix)
        then
        concat("
        =>es:with-namespace('",$target-namespace,"','",$target-namespace-prefix,"')")
        else (),
'
       (: Copy attachments from source document to the target :)
        =>es:copy-attachments($source-node)
    (: The following lines are generated from the "',$entity-type-name,'" entity type. :)
',
            fn:string-join($values),
            if (exists($missing-properties))
            then
                es-codegen:comment(fn:string-join(
                    ("The following properties are in the source, but not the target:
",
                    $missing-properties)))
            else ()
            )
    (: end code generation block :)
    }
}};
    </convert-instance>
(: END convert instance block :)

    let $removed-type :=
        (: Make comments for removed ET types :)
        for $removed-entity-type-name in map:keys($source-definitions)
        let $removed-entity-type := map:get($source-definitions, $removed-entity-type-name)
        where not( $removed-entity-type-name = $target-entity-type-names)
        return
<removed-type>
(:
 Entity type {$removed-entity-type-name} is in source document
 but not in target document.
 The following XPath expressions should get values from the source
 instances but there is no specified target.
 This comment can be as a starting point for writing a custom
 version converter.

declare function {$module-prefix}:convert-instance-{$removed-entity-type-name}(
    $source-node as node()
) as map:map
{{
{

map:put($target-info, $removed-entity-type-name, map:entry("", "Removed Type")),
    let $properties := $source-model
        =>map:get("definitions")
        =>map:get($removed-entity-type-name)
        =>map:get("properties")
    let $variable-setters :=
        for $property-name in map:keys($properties)
        return es-codegen:variable-line-for($module-prefix, $source-model, $removed-entity-type-name, $property-name)
    let $values :=
        for $property-name in map:keys($properties)
        return
            (: note, this call passes mutable let-expressions for modification :)
            es-codegen:value-for-conversion($source-model,
                $source-model,
                $removed-entity-type-name,
                $property-name,
                $property-name,
                map:map())
    return

    fn:concat(
        fn:string-join( $variable-setters, "
"),
'

    return
    json:object()
    (: If the source is an envelope or part of an envelope document,
     : copies attachments to the target :)
    =>es:copy-attachments($source-node)
    =>map:with("$type", "', $removed-entity-type-name, '" )',
    '
',
    fn:string-join($values)
    )
}
}};
:)
</removed-type>

    return document {
<module>xquery version '1.0-ml';
module namespace {$module-prefix}
    = '{$module-namespace}';

import module namespace es = 'http://marklogic.com/entity-services'
    at '/MarkLogic/entity-services/entity-services.xqy';

{
    (: namespace declarations :)
    let $namespace-accumulator := map:map()
    let $_ :=
        for $entity-type-name in $source-definitions=>map:keys()
        let $entity-type := $source-definitions=>map:get($entity-type-name)
        let $namespace := $entity-type=>map:get("namespace")
        let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
        return
            if ($namespace)
            then map:put($namespace-accumulator, $namespace-prefix, concat("declare namespace ",
                 $namespace-prefix,
                 " = '",
                 $namespace,
                 "';
"))
            else ()
    let $_ :=
        for $entity-type-name in $target-definitions=>map:keys()
        let $entity-type := $target-definitions=>map:get($entity-type-name)
        let $namespace := $entity-type=>map:get("namespace")
        let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
        return
            if ($namespace)
            then map:put($namespace-accumulator, $namespace-prefix, concat("declare namespace ",
                 $namespace-prefix,
                 " = '",
                 $namespace,
                 "';
"))
            else ()
    return
        map:keys($namespace-accumulator) ! map:get($namespace-accumulator, .)
}

declare option xdmp:mapping 'false';

(:
 This module was generated by MarkLogic Entity Services.
 Its purpose is to create instances of entity types
 defined in
 {$target-title || ", version " || $target-version}
 from documents that were persisted according to model
 {$source-title || ", version " || $source-version}

 For usage and extension points, see the Entity Services Developer's Guide

 https://docs.marklogic.com/guide/entity-services

 Generated at timestamp: {fn:current-dateTime()}

 Target Model {$target-title || "-" || $target-version} Info:

 {
map:keys($target-info) !
    function($et) {
        let $et-report := map:get($target-info, $et)
        return
        fn:concat("Type ", $et, ":
",
            fn:string-join(
               map:keys($et-report) !
                    function($report-key) {
                        "    ",
                        $report-key,
                        fn:string-join(map:get($et-report, $report-key), ", "),
                        "
"
                    }(.)
            )
        ),
        "
"
   }(.)
}:)

{
(: this flowr makes order deterministic, which is better for dev UX. :)
for $c in $convert-instance order by $c return $c/text()}

{for $r in $removed-type order by $r return $r/text()}

</module>/text()
    }
};


Fixed a typo.

Fixed a typo.

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

(:~
 :

 : This module provides functions to tidy a HTML document.

 : The functions in this module take an HTML document (a string) as parameter,
 : tidy it in order to result in valid XHTML, and return this XHTML document as a document-node.
 :

 :
 :
 : @author Sorin Nasoi
 : @library Tidy C++ Library
 :
 :)
module namespace html = "http://www.zorba-xquery.com/modules/convertors/html";

(:~
 : Import module for checking if html options element is validated.
 :)
import module namespace zorba-schema = "http://www.zorba-xquery.com/modules/schema";

(:~
 : Contains the definitions of the html options element.
 :)
import schema namespace html-options = "http://www.zorba-xquery.com/modules/convertors/html-options";

(:~
 : Errors namespace URI.
:)
declare variable $html:htmlNS as xs:string := "http://www.zorba-xquery.com/modules/convertors/html";

(:~
 : Error code for wrong parameter situations.

 : Possible error messages:

 : * "Options are not set correctly."

:)
declare variable $html:errWrongParam as xs:QName := fn:QName($html:htmlNS, "$html:errWrongParam");

(:~
 :
This function tidies the given HTML string and returns
 : a valid XHTML document node.

 :
 :
This functions automatically sets the following tidying parameters:
 :

output-xml:yes

doctype:omit

quote-nbsp:no

 :
char-encoding:utf8

newline:LF

tidy-mark:no

 :
 : @param $html the HTML string to tidy
 : @return the tidied XHTML document node
 : @example tidy_2.xq
 :)
declare function html:parse (
  $html as xs:string
) as document-node()
{
  let $validated-options :=
          <options xmlns="http://www.zorba-xquery.com/modules/convertors/html-options" >
              <tidyParam name="output-xml" value="yes" />
              <tidyParam name="doctype" value="omit" />
              <tidyParam name="quote-nbsp" value="no" />
              <tidyParam name="char-encoding" value="utf8" />
              <tidyParam name="newline" value="LF" />
              <tidyParam name="tidy-mark" value="no" />
            </options>
  return
    html:parse-internal($html, $validated-options)
};

(:~
 :
This function tidies the given HTML string and returns
 : a valid XHTML document node.

 :
 :
The second parameter allows to specify options that
 : configure the tidy process. This parameter is a sequence
 : of name=value pairs. Allowed parameter names and values
 : are documented at
 : http://tidy.sourceforge.net/docs/quickref.html.

 :
 : @param $html the HTML string to tidy
 : @param $options a set of name and value pairs that provide options
 :        to configure the tidy process that have to be validated against the
 :        "http://www.zorba-xquery.com/modules/convertors/html-options" schema.
 : @return the tidied XHTML document node
 : @error $html:errWrongParam if the otions were not set correctly.
 : @example tidy_1.xq
 :)
declare function html:parse (
  $html as xs:string,
  $options as element(html-options:options)
) as document-node()
{
  try {
    let $validated-options := if(empty($options)) then
                                $options
                              else
                                if(zorba-schema:is-validated($options)) then
                                  $options
                                else
                                  validate{$options}
    return
      html:parse-internal($html, $validated-options)
  } catch *
  {
    fn:error($html:errWrongParam, "The html-options element is not valid.")
  }
};

declare %private function html:parse-internal(
  $html as xs:string,
  $options as element(html-options:options)
  ) as document-node() external;

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

(:~
 :

 : This module provides functions to tidy a HTML document.

 : The functions in this module take an HTML document (a string) as parameter,
 : tidy it in order to result in valid XHTML, and return this XHTML document as a document-node.
 :

 :
 :
 : @author Sorin Nasoi
 : @library Tidy C++ Library
 :
 :)
module namespace html = "http://www.zorba-xquery.com/modules/convertors/html";

(:~
 : Import module for checking if html options element is validated.
 :)
import module namespace zorba-schema = "http://www.zorba-xquery.com/modules/schema";

(:~
 : Contains the definitions of the html options element.
 :)
import schema namespace html-options = "http://www.zorba-xquery.com/modules/convertors/html-options";

(:~
 : Errors namespace URI.
:)
declare variable $html:htmlNS as xs:string := "http://www.zorba-xquery.com/modules/convertors/html";

(:~
 : Error code for wrong parameter situations.

 : Possible error messages:

 : * "Options are not set correctly."

:)
declare variable $html:errWrongParam as xs:QName := fn:QName($html:htmlNS, "zorba-csv:WrongParam");

(:~
 :
This function tidies the given HTML string and returns
 : a valid XHTML document node.

 :
 :
This functions automatically sets the following tidying parameters:
 :

output-xml:yes

doctype:omit

quote-nbsp:no

 :
char-encoding:utf8

newline:LF

tidy-mark:no

 :
 : @param $html the HTML string to tidy
 : @return the tidied XHTML document node
 : @example tidy_2.xq
 :)
declare function html:parse (
  $html as xs:string
) as document-node()
{
  let $validated-options :=
          <options xmlns="http://www.zorba-xquery.com/modules/convertors/html-options" >
              <tidyParam name="output-xml" value="yes" />
              <tidyParam name="doctype" value="omit" />
              <tidyParam name="quote-nbsp" value="no" />
              <tidyParam name="char-encoding" value="utf8" />
              <tidyParam name="newline" value="LF" />
              <tidyParam name="tidy-mark" value="no" />
            </options>
  return
    html:parse-internal($html, $validated-options)
};

(:~
 :
This function tidies the given HTML string and returns
 : a valid XHTML document node.

 :
 :
The second parameter allows to specify options that
 : configure the tidy process. This parameter is a sequence
 : of name=value pairs. Allowed parameter names and values
 : are documented at
 : http://tidy.sourceforge.net/docs/quickref.html.

 :
 : @param $html the HTML string to tidy
 : @param $options a set of name and value pairs that provide options
 :        to configure the tidy process that have to be validated against the
 :        "http://www.zorba-xquery.com/modules/convertors/html-options" schema.
 : @return the tidied XHTML document node
 : @error $html:errWrongParam if the otions were not set correctly.
 : @example tidy_1.xq
 :)
declare function html:parse (
  $html as xs:string,
  $options as element(html-options:options)
) as document-node()
{
  try {
    let $validated-options := if(empty($options)) then
                                $options
                              else
                                if(zorba-schema:is-validated($options)) then
                                  $options
                                else
                                  validate{$options}
    return
      html:parse-internal($html, $validated-options)
  } catch *
  {
    fn:error($html:errWrongParam, "The html-options element is not valid.")
  }
};

declare %private function html:parse-internal(
  $html as xs:string,
  $options as element(html-options:options)
  ) as document-node() external;