Service: generalize Python invokation

Service: generalize Python invokation

Also allow to forward param values.

(: Subtitle Conversion Framework (SCF) - SCF service
 :
 : Copyright 2019 Institut für Rundfunktechnik GmbH, Munich, Germany
 :
 : 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, the subject work
 : 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.
 :)

module namespace scf = 'http://www.irt.de/irt_restxq/scf_service';

declare variable $scf:modules_path as xs:string := "modules/";

declare variable $scf:subtitle_format :=
    map {
        'stl': 'STL',
        'stlxml': 'STLXML',
        'ebu-tt': 'EBU-TT',
        'ebu-tt-d': 'EBU-TT-D',
        'ebu-tt-d-basic-de': 'EBU-TT-D-Basic-DE'
    };

declare variable $scf:params_all := ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'separate_tti', 'clear_uda', 'discard_user_data', 'use_line_height_125', 'ignore_manual_offset_for_tcp', 'indent');

(: returns a simple HTML form for a conversion from one format to another :)
declare function scf:form_conversion($format_source, $format_target, $params) {

{concat($scf:subtitle_format($format_source), ' → ', $scf:subtitle_format($format_target))}
,
    <form action="convert" method="post" enctype="multipart/form-data">
        <input type="hidden" name="format_source" value="{$format_source}"/>
        <input type="hidden" name="format_target" value="{$format_target}"/>
        {scf:form_conversion_fields($params)}
    </form>
};

(: returns the necessary form input fields for a conversion :)
declare function scf:form_conversion_fields($params) {
    Input file: <input type="file" name="input" required="required"/>,
    <button type="submit">Convert</button>,

        {if($params = 'offset_seconds') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> consider <input type="text" size="10" name="offset_seconds" value="36000" disabled="disabled"/> seconds offset
 else ()}
        {if($params = 'offset_frames') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> consider <input type="text" size="10" name="offset_frames" value="10:00:00:00" disabled="disabled"/> frames offset
 else ()}
        {if($params = 'offset_start_of_programme') then
<input type="checkbox" name="offset_start_of_programme" value="1"/> consider Start-of-Programme offset
 else ()}
        {if($params = 'separate_tti') then
<input type="checkbox" name="separate_tti" value="1"/> do not merge multiple text TTI blocks of the same subtitle
 else ()}
        {if($params = 'clear_uda') then
<input type="checkbox" name="clear_uda" value="1"/> clear STL User-Defined Area (UDA)
 else ()}
        {if($params = 'discard_user_data') then
<input type="checkbox" name="discard_user_data" value="1"/> discard STL User Data (EBN 0xFE)
 else ()}
        {if($params = 'use_line_height_125') then
<input type="checkbox" name="use_line_height_125" value="1"/> use line height "125%" in EBU-TT-D
 else ()}
        {if($params = 'ignore_manual_offset_for_tcp') then
<input type="checkbox" name="ignore_manual_offset_for_tcp" value="1"/> ignore manual offset for TCP
 else ()}
        {if($params = 'indent') then
<input type="checkbox" name="indent" value="1" checked="checked"/> indented output
 else () (: enabled by default :)}

,

};

(: web interface homepage :)
declare
  %rest:path("/webif")
  %output:method("xhtml")
  function scf:overview() {

        <title>SCF service</title>

SCF service

Multi step conversions

        {scf:form_conversion('stl', 'ebu-tt', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'discard_user_data', 'ignore_manual_offset_for_tcp', 'indent'))}
        {scf:form_conversion('ebu-tt', 'stl', ())}
        {scf:form_conversion('stl', 'ebu-tt-d-basic-de', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'discard_user_data', 'use_line_height_125', 'ignore_manual_offset_for_tcp', 'indent'))}

Custom multi step conversion

        Note: For a particular conversion, not every shown option may be supported!

        <form action="convert" method="post" enctype="multipart/form-data">

                Source format:
                <select name="format_source" required="required">
                    <option selected="selected"/>
                    {for $format in map:keys($scf:subtitle_format) order by $format return <option value="{$format}">{$scf:subtitle_format($format)}</option>}
                </select>
                Target format:
                <select name="format_target" required="required">
                    <option selected="selected"/>
                    {for $format in map:keys($scf:subtitle_format) order by $format return <option value="{$format}">{$scf:subtitle_format($format)}</option>}
                </select>

            {scf:form_conversion_fields($scf:params_all)}
        </form>

Single step conversions

        {scf:form_conversion('stl', 'stlxml', ('separate_tti', 'clear_uda', 'discard_user_data', 'indent'))}
        {scf:form_conversion('stlxml', 'stl', ())}
        {scf:form_conversion('stlxml', 'ebu-tt', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'ignore_manual_offset_for_tcp', 'indent'))}
        {scf:form_conversion('ebu-tt', 'stlxml', ('indent'))}
        {scf:form_conversion('ebu-tt', 'ebu-tt-d', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'use_line_height_125', 'indent'))}
        {scf:form_conversion('ebu-tt-d', 'ebu-tt-d-basic-de', ('indent'))}

  };

(: param->options map: sets a param (key) to the value of a specific status option (value); afterwards clear the option value to indicate that it has been consumed :)

(: generic Python invokation :)
declare function scf:call_python($status as map(*), $script as xs:string, $param_option_mapping as map(*)) as map(*) {
    (: build params sequence with present options - it is sufficient that an option is present; the actual value doesn't matter! :)
    let $params :=
        for $k in map:keys($param_option_mapping)
        let $v := $k => $param_option_mapping() => $status()
        where exists($v)
        return $k

    (: transformation :)
    let $input as xs:base64Binary := $status('result')
    let $input_filename := file:create-temp-file('scf-service-', '')
    let $output_filename := concat($input_filename, '.out')
    let $write_input := file:write-binary($input_filename, $input)
    let $result_call := proc:system('python3', (file:resolve-path(concat($scf:modules_path, $script)), $input_filename, '-x', $output_filename, $params ! concat('-', .)))
    let $result as document-node() := (# basex:non-deterministic #) {doc($output_filename)} (: prevent evaluation after deleted output file :)
    let $delete_input := file:delete($input_filename)
    let $delete_output := file:delete($output_filename)

    (: remove consumed options + replace result :)
    return map:merge((map:entry('result', $result), $params ! map:entry($param_option_mapping(.), ()), $status))
};

declare function scf:call_stl2stlxml($status as map(*)) as map(*) {
    scf:call_python($status, 'STL2STLXML/stl2stlxml.py', map {
        's': 'option_separate_tti',
        'a': 'option_clear_uda',
        'u': 'option_discard_user_data'
    })
};

declare function scf:call_stlxml2stl($status as map(*)) as map(*) {
    let $input as document-node() := $status('result')
    let $result as xs:base64Binary := xs:base64Binary(
        (: also invoke STLXML-SplitBlocks :)
        let $input_splitted := xslt:transform($input, concat('../', $scf:modules_path, 'STLXML-SplitBlocks/STLXML-SplitBlocks.xslt'))
        return xquery:eval(
            concat("import module namespace stlxml2stl = 'stlxml2stl' at '../", $scf:modules_path, "STLXML2STL/stlxml2stl.xqm'; stlxml2stl:encode(.)"),
            map { '': $input_splitted }
        )
    )
    return map:put($status, 'result', $result)
};

(: generic XSLT conversion :)
declare function scf:call_xslt($status as map(*), $xslt as xs:string, $param_option_mapping as map(*)) as map(*) {
    (: build params map with present options :)
    let $params := map:merge(
        for $k in map:keys($param_option_mapping)
        let $v := $k => $param_option_mapping() => $status()
        where exists($v)
        return map:entry($k, $v)
    )

    (: transformation :)
    let $input as document-node() := $status('result')
    let $result as document-node() := xslt:transform($input, concat('../', $scf:modules_path, $xslt), $params)

    (: remove consumed options + replace result :)
    return map:merge((map:entry('result', $result), map:keys($params) ! map:entry($param_option_mapping(.), ()), $status))
};

declare function scf:call_stlxml2ebu-tt($status as map(*)) as map(*) {
    scf:call_xslt($status, 'STLXML2EBU-TT/STLXML2EBU-TT.xslt', map {
        'offsetTCP': 'option_offset_start_of_programme',
        'offsetInSeconds': 'option_offset_seconds',
        'offsetInFrames': 'option_offset_frames',
        'ignoreManualOffsetForTCP': 'option_ignore_manual_offset_for_tcp'
    })
};

declare function scf:call_ebu-tt2stlxml($status as map(*)) as map(*) {
    scf:call_xslt($status, 'EBU-TT2STLXML/EBU-TT2STLXML.xslt', map{})
};

declare function scf:call_ebu-tt2ebu-tt-d($status as map(*)) as map(*) {
    scf:call_xslt($status, 'EBU-TT2EBU-TT-D/EBU-TT2EBU-TT-D.xslt', map {
        'useLineHeight125Percent': 'option_use_line_height_125',
        'offsetStartOfProgramme': 'option_offset_start_of_programme',
        'offsetInSeconds': 'option_offset_seconds',
        'offsetInFrames': 'option_offset_frames'
    })
};

declare function scf:call_ebu-tt-d2ebu-tt-d-basic-de($status as map(*)) as map(*) {
    scf:call_xslt($status, 'EBU-TT-D2EBU-TT-D-Basic-DE/EBU-TT-D2EBU-TT-D-Basic-DE.xslt', map{})
};

(: sets an unsupported option error as status result :)
declare function scf:status_set_error($status as map(*), $description as xs:string+) as map(*) {
    map:put($status, 'result',
        <error>
            <steps>
                {$status('steps') ! <step>{$scf:subtitle_format(.)}</step>}
            </steps>

            <description>This conversion chain does not support the following option(s): {string-join($description, '; ')}.</description>
            <value/>
            <module/>
            <line-number/>
            <column-number/>
        </error>
    )
};

(:
items of the status map:
- steps: sequence of the conversion formats the source file has gone through so far (last item = current format)
- option_offset_seconds: if present, offset in seconds that shall be subtracted from all timecodes
- option_offset_frames: if present, offset in frames that shall be subtracted from all timecodes
- option_offset_start_of_programme: if present, subtract Start-of-Programme offset from all timecodes
- option_use_line_height_125: if present, use the value "125%" (instead of the special value "normal") for the line height in EBU-TT-D
- option_ignore_manual_offset_for_tcp: if present, any manual offset (seconds or frames) will *not* be subtracted from the TCP value
- option_separate_tti: if present, do not merge multiple text TTI blocks of the same subtitle
- option_clear_uda: if present, clear STL User-Defined Area (UDA)
- option_discard_user_data: if present, discard STL User Data
- result: subtitle content in the indicated format
:)

(: execute the actual conversion and return a result (or an error) :)
declare
  %rest:path("/convert")
  %rest:POST
  %rest:form-param("input", "{$input}")
  %rest:form-param("format_source", "{$format_source}")
  %rest:form-param("format_target", "{$format_target}")
  %rest:form-param("offset_seconds", "{$offset_seconds}")
  %rest:form-param("offset_frames", "{$offset_frames}")
  %rest:form-param("offset_start_of_programme", "{$offset_start_of_programme}")
  %rest:form-param("use_line_height_125", "{$use_line_height_125}")
  %rest:form-param("ignore_manual_offset_for_tcp", "{$ignore_manual_offset_for_tcp}")
  %rest:form-param("separate_tti", "{$separate_tti}")
  %rest:form-param("clear_uda", "{$clear_uda}")
  %rest:form-param("discard_user_data", "{$discard_user_data}")
  %rest:form-param("indent", "{$indent}")
  function scf:convert(
    $input as map(*),
    $format_source as xs:string,
    $format_target as xs:string,
    $offset_seconds as xs:string?,
    $offset_frames as xs:string?,
    $offset_start_of_programme as xs:string?,
    $use_line_height_125 as xs:string?,
    $ignore_manual_offset_for_tcp as xs:string?,
    $separate_tti as xs:string?,
    $clear_uda as xs:string?,
    $discard_user_data as xs:string?,
    $indent as xs:string?
  ) {
    let $conversion_option_error_messages := map {
        'option_offset_seconds': 'to consider a timecodes seconds offset',
        'option_offset_frames': 'to consider a timecodes frame offset',
        'option_offset_start_of_programme': 'to consider the Start-of-Programme offset',
        'option_use_line_height_125': 'to use the line height value "125%"',
        'option_ignore_manual_offset_for_tcp': 'to ignore any manual offset for TCP',
        'option_separate_tti': 'to disable text TTI block merging',
        'option_clear_uda': 'to clear STL User-Defined Area (UDA)',
        'option_discard_user_data': 'to remove STL User Data'
    }

    let $conversion_format := concat($format_source, '→', $format_target)
    let $source_stl := $format_source eq 'stl'
    let $target_stl := $format_target eq 'stl'

    let $input_filename := map:keys($input)[1]
    let $input_content_raw := $input($input_filename)
    let $input_content := if ($source_stl) then $input_content_raw else parse-xml(bin:decode-string($input_content_raw, 'UTF-8'))

    (: do conversion :)
    let $conversion_status_init := map {
        'steps': $format_source,
        'option_offset_seconds': $offset_seconds,
        'option_offset_frames': $offset_frames,
        'option_offset_start_of_programme': $offset_start_of_programme,
        'option_use_line_height_125': $use_line_height_125,
        'option_ignore_manual_offset_for_tcp': $ignore_manual_offset_for_tcp,
        'option_separate_tti': $separate_tti,
        'option_clear_uda': $clear_uda,
        'option_discard_user_data': $discard_user_data,
        'result': $input_content
    }
    let $conversion_status := scf:convert_step($conversion_status_init, $format_target)
    let $conversion_option_errors := map:keys($conversion_option_error_messages)[exists($conversion_status(.))] ! $conversion_option_error_messages(.)
    let $conversion := if (empty($conversion_option_errors)) then $conversion_status else scf:status_set_error($conversion_status, $conversion_option_errors)

    let $rest_response_format := if ($conversion('result') instance of xs:base64Binary) then 'application/octet-stream' else 'application/xml'
    let $rest_response_filename_without_ext := replace($input_filename, '[.]([Ss][Tt][Ll]|[Xx][Mm][Ll])$', '')
    let $rest_response_filename_suffix := if ($target_stl) then '.stl' else concat('_', $format_target, '.xml')
    let $rest_response_filename := concat($rest_response_filename_without_ext, $rest_response_filename_suffix)
    let $rest_response_success := not($conversion('result') instance of element())

    return (
        (: MIME type + filename :)
        <rest:response>
            <output:serialization-parameters>
                <output:omit-xml-declaration value='no'/>
                <output:indent value='{if (exists($indent)) then 'yes' else 'no'}'/>
                <output:media-type value='{$rest_response_format}'/>
            </output:serialization-parameters>
            <http:response status="{if ($rest_response_success) then 200 else 400}">
                {if ($rest_response_success) then <http:header name="Content-Disposition" value="attachment; filename=""{$rest_response_filename}"""/> else ()}
                <http:header name="SCF-Service-Debug-Conversion-Steps" value="{string-join($conversion('steps'), ',')}"/>
            </http:response>
        </rest:response>,
        if ($rest_response_success) then () else <?xml-stylesheet href="static/error.xsl" type="text/xsl" ?>,
        $conversion('result')
    )
  };

(: execute a single conversion step :)
declare function scf:convert_step($status as map(*), $format_target as xs:string) as map(*) {
    let $current_format := $status('steps')[last()]
    let $next_format := scf:next_format($current_format, $format_target)
    return
        (: return, if no conversion needed :)
        if (empty($next_format))
        then $status
        else
            let $next_steps := ($status('steps'), $next_format)
            return try {
                (: invoke next conversion step :)
                let $next_conversion := concat($current_format, '→', $next_format)
                let $next_step_status :=
                    switch($next_conversion)
                    case "stl→stlxml" return
                        scf:call_stl2stlxml($status)
                    case "stlxml→stl" return
                        scf:call_stlxml2stl($status)
                    case "stlxml→ebu-tt" return
                        scf:call_stlxml2ebu-tt($status)
                    case "ebu-tt→stlxml" return
                        scf:call_ebu-tt2stlxml($status)
                    case "ebu-tt→ebu-tt-d" return
                        scf:call_ebu-tt2ebu-tt-d($status)
                    case "ebu-tt-d→ebu-tt-d-basic-de" return
                        scf:call_ebu-tt-d2ebu-tt-d-basic-de($status)
                    default return
                        ()
                let $next_status := map:put($next_step_status, 'steps', $next_steps)
                return scf:convert_step($next_status, $format_target)
            } catch * {
                (: abort with error :)
                map { 'steps': $next_steps, 'result':
                    <error>
                        <steps>
                            {$next_steps ! <step>{$scf:subtitle_format(.)}</step>}
                        </steps>
                        {$err:code}
                        <description>{$err:description}</description>
                        <value>{$err:value}</value>
                        <module>{$err:module}</module>
                        <line-number>{$err:line-number}</line-number>
                        <column-number>{$err:column-number}</column-number>
                    </error>
                }
            }
};

(: determine the next format to which the current (intermediate) result has to be converted, towards the target format :)
declare function scf:next_format($format_source as xs:string, $format_target as xs:string) as xs:string? {
    switch($format_source)
    case $format_target return
        () (: no conversion needed :)
    case 'stl' return
        'stlxml'
    case 'stlxml' return
        if($format_target eq 'stl')
        then 'stl'
        else 'ebu-tt'
    default return
        (: TTML source :)
        if($format_target = ('stl', 'stlxml'))
        then 'stlxml'
        else
            (: TTML source/target :)
            switch($format_source)
            case 'ebu-tt' return
                'ebu-tt-d'
            case 'ebu-tt-d' return
                if($format_target eq 'ebu-tt')
                then () (: no conversion needed :)
                else 'ebu-tt-d-basic-de'
            case 'ebu-tt-d-basic-de'
                return () (: no conversion needed :)
            default return
                ''
};

(: Subtitle Conversion Framework (SCF) - SCF service
 :
 : Copyright 2019 Institut für Rundfunktechnik GmbH, Munich, Germany
 :
 : 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, the subject work
 : 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.
 :)

module namespace scf = 'http://www.irt.de/irt_restxq/scf_service';

declare variable $scf:modules_path as xs:string := "modules/";

declare variable $scf:subtitle_format :=
    map {
        'stl': 'STL',
        'stlxml': 'STLXML',
        'ebu-tt': 'EBU-TT',
        'ebu-tt-d': 'EBU-TT-D',
        'ebu-tt-d-basic-de': 'EBU-TT-D-Basic-DE'
    };

declare variable $scf:params_all := ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'separate_tti', 'clear_uda', 'discard_user_data', 'use_line_height_125', 'ignore_manual_offset_for_tcp', 'indent');

(: returns a simple HTML form for a conversion from one format to another :)
declare function scf:form_conversion($format_source, $format_target, $params) {

{concat($scf:subtitle_format($format_source), ' → ', $scf:subtitle_format($format_target))}
,
    <form action="convert" method="post" enctype="multipart/form-data">
        <input type="hidden" name="format_source" value="{$format_source}"/>
        <input type="hidden" name="format_target" value="{$format_target}"/>
        {scf:form_conversion_fields($params)}
    </form>
};

(: returns the necessary form input fields for a conversion :)
declare function scf:form_conversion_fields($params) {
    Input file: <input type="file" name="input" required="required"/>,
    <button type="submit">Convert</button>,

        {if($params = 'offset_seconds') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> consider <input type="text" size="10" name="offset_seconds" value="36000" disabled="disabled"/> seconds offset
 else ()}
        {if($params = 'offset_frames') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> consider <input type="text" size="10" name="offset_frames" value="10:00:00:00" disabled="disabled"/> frames offset
 else ()}
        {if($params = 'offset_start_of_programme') then
<input type="checkbox" name="offset_start_of_programme" value="1"/> consider Start-of-Programme offset
 else ()}
        {if($params = 'separate_tti') then
<input type="checkbox" name="separate_tti" value="1"/> do not merge multiple text TTI blocks of the same subtitle
 else ()}
        {if($params = 'clear_uda') then
<input type="checkbox" name="clear_uda" value="1"/> clear STL User-Defined Area (UDA)
 else ()}
        {if($params = 'discard_user_data') then
<input type="checkbox" name="discard_user_data" value="1"/> discard STL User Data (EBN 0xFE)
 else ()}
        {if($params = 'use_line_height_125') then
<input type="checkbox" name="use_line_height_125" value="1"/> use line height "125%" in EBU-TT-D
 else ()}
        {if($params = 'ignore_manual_offset_for_tcp') then
<input type="checkbox" name="ignore_manual_offset_for_tcp" value="1"/> ignore manual offset for TCP
 else ()}
        {if($params = 'indent') then
<input type="checkbox" name="indent" value="1" checked="checked"/> indented output
 else () (: enabled by default :)}

,

};

(: web interface homepage :)
declare
  %rest:path("/webif")
  %output:method("xhtml")
  function scf:overview() {

        <title>SCF service</title>

SCF service

Multi step conversions

        {scf:form_conversion('stl', 'ebu-tt', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'discard_user_data', 'ignore_manual_offset_for_tcp', 'indent'))}
        {scf:form_conversion('ebu-tt', 'stl', ())}
        {scf:form_conversion('stl', 'ebu-tt-d-basic-de', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'discard_user_data', 'use_line_height_125', 'ignore_manual_offset_for_tcp', 'indent'))}

Custom multi step conversion

        Note: For a particular conversion, not every shown option may be supported!

        <form action="convert" method="post" enctype="multipart/form-data">

                Source format:
                <select name="format_source" required="required">
                    <option selected="selected"/>
                    {for $format in map:keys($scf:subtitle_format) order by $format return <option value="{$format}">{$scf:subtitle_format($format)}</option>}
                </select>
                Target format:
                <select name="format_target" required="required">
                    <option selected="selected"/>
                    {for $format in map:keys($scf:subtitle_format) order by $format return <option value="{$format}">{$scf:subtitle_format($format)}</option>}
                </select>

            {scf:form_conversion_fields($scf:params_all)}
        </form>

Single step conversions

        {scf:form_conversion('stl', 'stlxml', ('separate_tti', 'clear_uda', 'discard_user_data', 'indent'))}
        {scf:form_conversion('stlxml', 'stl', ())}
        {scf:form_conversion('stlxml', 'ebu-tt', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'ignore_manual_offset_for_tcp', 'indent'))}
        {scf:form_conversion('ebu-tt', 'stlxml', ('indent'))}
        {scf:form_conversion('ebu-tt', 'ebu-tt-d', ('offset_seconds', 'offset_frames', 'offset_start_of_programme', 'use_line_height_125', 'indent'))}
        {scf:form_conversion('ebu-tt-d', 'ebu-tt-d-basic-de', ('indent'))}

  };

(: param->options map: sets a param (key) to the value of a specific status option (value); afterwards clear the option value to indicate that it has been consumed :)

declare function scf:call_stl2stlxml($status as map(*)) as map(*) {
    let $param_option_mapping := map {
        's': 'option_separate_tti',
        'a': 'option_clear_uda',
        'u': 'option_discard_user_data'
    }

    (: build params sequence with present options - for STL2STLXML it is sufficient that an option is present; the actual value doesn't matter! :)
    let $params :=
        for $k in map:keys($param_option_mapping)
        let $v := $k => $param_option_mapping() => $status()
        where exists($v)
        return $k

    (: transformation :)
    let $input as xs:base64Binary := $status('result')
    let $input_filename := file:create-temp-file('scf-service-', '')
    let $output_filename := concat($input_filename, '.out')
    let $write_input := file:write-binary($input_filename, $input)
    let $result_call := proc:system('python3', (file:resolve-path(concat($scf:modules_path, 'STL2STLXML/stl2stlxml.py')), $input_filename, '-x', $output_filename, $params ! concat('-', .)))
    let $result as document-node() := (# basex:non-deterministic #) {doc($output_filename)} (: prevent evaluation after deleted output file :)
    let $delete_input := file:delete($input_filename)
    let $delete_output := file:delete($output_filename)

    (: remove consumed options + replace result :)
    return map:merge((map:entry('result', $result), $params ! map:entry($param_option_mapping(.), ()), $status))
};

declare function scf:call_stlxml2stl($status as map(*)) as map(*) {
    let $input as document-node() := $status('result')
    let $result as xs:base64Binary := xs:base64Binary(
        (: also invoke STLXML-SplitBlocks :)
        let $input_splitted := xslt:transform($input, concat('../', $scf:modules_path, 'STLXML-SplitBlocks/STLXML-SplitBlocks.xslt'))
        return xquery:eval(
            concat("import module namespace stlxml2stl = 'stlxml2stl' at '../", $scf:modules_path, "STLXML2STL/stlxml2stl.xqm'; stlxml2stl:encode(.)"),
            map { '': $input_splitted }
        )
    )
    return map:put($status, 'result', $result)
};

(: generic XSLT conversion :)
declare function scf:call_xslt($status as map(*), $xslt as xs:string, $param_option_mapping as map(*)) as map(*) {
    (: build params map with present options :)
    let $params := map:merge(
        for $k in map:keys($param_option_mapping)
        let $v := $k => $param_option_mapping() => $status()
        where exists($v)
        return map:entry($k, $v)
    )

    (: transformation :)
    let $input as document-node() := $status('result')
    let $result as document-node() := xslt:transform($input, concat('../', $scf:modules_path, $xslt), $params)

    (: remove consumed options + replace result :)
    return map:merge((map:entry('result', $result), map:keys($params) ! map:entry($param_option_mapping(.), ()), $status))
};

declare function scf:call_stlxml2ebu-tt($status as map(*)) as map(*) {
    scf:call_xslt($status, 'STLXML2EBU-TT/STLXML2EBU-TT.xslt', map {
        'offsetTCP': 'option_offset_start_of_programme',
        'offsetInSeconds': 'option_offset_seconds',
        'offsetInFrames': 'option_offset_frames',
        'ignoreManualOffsetForTCP': 'option_ignore_manual_offset_for_tcp'
    })
};

declare function scf:call_ebu-tt2stlxml($status as map(*)) as map(*) {
    scf:call_xslt($status, 'EBU-TT2STLXML/EBU-TT2STLXML.xslt', map{})
};

declare function scf:call_ebu-tt2ebu-tt-d($status as map(*)) as map(*) {
    scf:call_xslt($status, 'EBU-TT2EBU-TT-D/EBU-TT2EBU-TT-D.xslt', map {
        'useLineHeight125Percent': 'option_use_line_height_125',
        'offsetStartOfProgramme': 'option_offset_start_of_programme',
        'offsetInSeconds': 'option_offset_seconds',
        'offsetInFrames': 'option_offset_frames'
    })
};

declare function scf:call_ebu-tt-d2ebu-tt-d-basic-de($status as map(*)) as map(*) {
    scf:call_xslt($status, 'EBU-TT-D2EBU-TT-D-Basic-DE/EBU-TT-D2EBU-TT-D-Basic-DE.xslt', map{})
};

(: sets an unsupported option error as status result :)
declare function scf:status_set_error($status as map(*), $description as xs:string+) as map(*) {
    map:put($status, 'result',
        <error>
            <steps>
                {$status('steps') ! <step>{$scf:subtitle_format(.)}</step>}
            </steps>

            <description>This conversion chain does not support the following option(s): {string-join($description, '; ')}.</description>
            <value/>
            <module/>
            <line-number/>
            <column-number/>
        </error>
    )
};

(:
items of the status map:
- steps: sequence of the conversion formats the source file has gone through so far (last item = current format)
- option_offset_seconds: if present, offset in seconds that shall be subtracted from all timecodes
- option_offset_frames: if present, offset in frames that shall be subtracted from all timecodes
- option_offset_start_of_programme: if present, subtract Start-of-Programme offset from all timecodes
- option_use_line_height_125: if present, use the value "125%" (instead of the special value "normal") for the line height in EBU-TT-D
- option_ignore_manual_offset_for_tcp: if present, any manual offset (seconds or frames) will *not* be subtracted from the TCP value
- option_separate_tti: if present, do not merge multiple text TTI blocks of the same subtitle
- option_clear_uda: if present, clear STL User-Defined Area (UDA)
- option_discard_user_data: if present, discard STL User Data
- result: subtitle content in the indicated format
:)

(: execute the actual conversion and return a result (or an error) :)
declare
  %rest:path("/convert")
  %rest:POST
  %rest:form-param("input", "{$input}")
  %rest:form-param("format_source", "{$format_source}")
  %rest:form-param("format_target", "{$format_target}")
  %rest:form-param("offset_seconds", "{$offset_seconds}")
  %rest:form-param("offset_frames", "{$offset_frames}")
  %rest:form-param("offset_start_of_programme", "{$offset_start_of_programme}")
  %rest:form-param("use_line_height_125", "{$use_line_height_125}")
  %rest:form-param("ignore_manual_offset_for_tcp", "{$ignore_manual_offset_for_tcp}")
  %rest:form-param("separate_tti", "{$separate_tti}")
  %rest:form-param("clear_uda", "{$clear_uda}")
  %rest:form-param("discard_user_data", "{$discard_user_data}")
  %rest:form-param("indent", "{$indent}")
  function scf:convert(
    $input as map(*),
    $format_source as xs:string,
    $format_target as xs:string,
    $offset_seconds as xs:string?,
    $offset_frames as xs:string?,
    $offset_start_of_programme as xs:string?,
    $use_line_height_125 as xs:string?,
    $ignore_manual_offset_for_tcp as xs:string?,
    $separate_tti as xs:string?,
    $clear_uda as xs:string?,
    $discard_user_data as xs:string?,
    $indent as xs:string?
  ) {
    let $conversion_option_error_messages := map {
        'option_offset_seconds': 'to consider a timecodes seconds offset',
        'option_offset_frames': 'to consider a timecodes frame offset',
        'option_offset_start_of_programme': 'to consider the Start-of-Programme offset',
        'option_use_line_height_125': 'to use the line height value "125%"',
        'option_ignore_manual_offset_for_tcp': 'to ignore any manual offset for TCP',
        'option_separate_tti': 'to disable text TTI block merging',
        'option_clear_uda': 'to clear STL User-Defined Area (UDA)',
        'option_discard_user_data': 'to remove STL User Data'
    }

    let $conversion_format := concat($format_source, '→', $format_target)
    let $source_stl := $format_source eq 'stl'
    let $target_stl := $format_target eq 'stl'

    let $input_filename := map:keys($input)[1]
    let $input_content_raw := $input($input_filename)
    let $input_content := if ($source_stl) then $input_content_raw else parse-xml(bin:decode-string($input_content_raw, 'UTF-8'))

    (: do conversion :)
    let $conversion_status_init := map {
        'steps': $format_source,
        'option_offset_seconds': $offset_seconds,
        'option_offset_frames': $offset_frames,
        'option_offset_start_of_programme': $offset_start_of_programme,
        'option_use_line_height_125': $use_line_height_125,
        'option_ignore_manual_offset_for_tcp': $ignore_manual_offset_for_tcp,
        'option_separate_tti': $separate_tti,
        'option_clear_uda': $clear_uda,
        'option_discard_user_data': $discard_user_data,
        'result': $input_content
    }
    let $conversion_status := scf:convert_step($conversion_status_init, $format_target)
    let $conversion_option_errors := map:keys($conversion_option_error_messages)[exists($conversion_status(.))] ! $conversion_option_error_messages(.)
    let $conversion := if (empty($conversion_option_errors)) then $conversion_status else scf:status_set_error($conversion_status, $conversion_option_errors)

    let $rest_response_format := if ($conversion('result') instance of xs:base64Binary) then 'application/octet-stream' else 'application/xml'
    let $rest_response_filename_without_ext := replace($input_filename, '[.]([Ss][Tt][Ll]|[Xx][Mm][Ll])$', '')
    let $rest_response_filename_suffix := if ($target_stl) then '.stl' else concat('_', $format_target, '.xml')
    let $rest_response_filename := concat($rest_response_filename_without_ext, $rest_response_filename_suffix)
    let $rest_response_success := not($conversion('result') instance of element())

    return (
        (: MIME type + filename :)
        <rest:response>
            <output:serialization-parameters>
                <output:omit-xml-declaration value='no'/>
                <output:indent value='{if (exists($indent)) then 'yes' else 'no'}'/>
                <output:media-type value='{$rest_response_format}'/>
            </output:serialization-parameters>
            <http:response status="{if ($rest_response_success) then 200 else 400}">
                {if ($rest_response_success) then <http:header name="Content-Disposition" value="attachment; filename=""{$rest_response_filename}"""/> else ()}
                <http:header name="SCF-Service-Debug-Conversion-Steps" value="{string-join($conversion('steps'), ',')}"/>
            </http:response>
        </rest:response>,
        if ($rest_response_success) then () else <?xml-stylesheet href="static/error.xsl" type="text/xsl" ?>,
        $conversion('result')
    )
  };

(: execute a single conversion step :)
declare function scf:convert_step($status as map(*), $format_target as xs:string) as map(*) {
    let $current_format := $status('steps')[last()]
    let $next_format := scf:next_format($current_format, $format_target)
    return
        (: return, if no conversion needed :)
        if (empty($next_format))
        then $status
        else
            let $next_steps := ($status('steps'), $next_format)
            return try {
                (: invoke next conversion step :)
                let $next_conversion := concat($current_format, '→', $next_format)
                let $next_step_status :=
                    switch($next_conversion)
                    case "stl→stlxml" return
                        scf:call_stl2stlxml($status)
                    case "stlxml→stl" return
                        scf:call_stlxml2stl($status)
                    case "stlxml→ebu-tt" return
                        scf:call_stlxml2ebu-tt($status)
                    case "ebu-tt→stlxml" return
                        scf:call_ebu-tt2stlxml($status)
                    case "ebu-tt→ebu-tt-d" return
                        scf:call_ebu-tt2ebu-tt-d($status)
                    case "ebu-tt-d→ebu-tt-d-basic-de" return
                        scf:call_ebu-tt-d2ebu-tt-d-basic-de($status)
                    default return
                        ()
                let $next_status := map:put($next_step_status, 'steps', $next_steps)
                return scf:convert_step($next_status, $format_target)
            } catch * {
                (: abort with error :)
                map { 'steps': $next_steps, 'result':
                    <error>
                        <steps>
                            {$next_steps ! <step>{$scf:subtitle_format(.)}</step>}
                        </steps>
                        {$err:code}
                        <description>{$err:description}</description>
                        <value>{$err:value}</value>
                        <module>{$err:module}</module>
                        <line-number>{$err:line-number}</line-number>
                        <column-number>{$err:column-number}</column-number>
                    </error>
                }
            }
};

(: determine the next format to which the current (intermediate) result has to be converted, towards the target format :)
declare function scf:next_format($format_source as xs:string, $format_target as xs:string) as xs:string? {
    switch($format_source)
    case $format_target return
        () (: no conversion needed :)
    case 'stl' return
        'stlxml'
    case 'stlxml' return
        if($format_target eq 'stl')
        then 'stl'
        else 'ebu-tt'
    default return
        (: TTML source :)
        if($format_target = ('stl', 'stlxml'))
        then 'stlxml'
        else
            (: TTML source/target :)
            switch($format_source)
            case 'ebu-tt' return
                'ebu-tt-d'
            case 'ebu-tt-d' return
                if($format_target eq 'ebu-tt')
                then () (: no conversion needed :)
                else 'ebu-tt-d-basic-de'
            case 'ebu-tt-d-basic-de'
                return () (: no conversion needed :)
            default return
                ''
};


Add test

Add test

(:
 : eXist-db Open Source Native XML Database
 : Copyright (C) 2001 The eXist-db Authors
 :
 : info@exist-db.org
 : http://www.exist-db.org
 :
 : This library is free software; you can redistribute it and/or
 : modify it under the terms of the GNU Lesser General Public
 : License as published by the Free Software Foundation; either
 : version 2.1 of the License, or (at your option) any later version.
 :
 : This library is distributed in the hope that it will be useful,
 : but WITHOUT ANY WARRANTY; without even the implied warranty of
 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 : Lesser General Public License for more details.
 :
 : You should have received a copy of the GNU Lesser General Public
 : License along with this library; if not, write to the Free Software
 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 :)
xquery version "3.1";

(:~
 : Tests to check if the reported line numbers for errors are correct:
 :
 : * if a function is called dynamically. In previous eXist versions, the
 : line number always pointed to the place where the function item was created,
 : not the actual function body
 : * if newlines occur in string literals, constructors or comments
 :)
module namespace et="http://exist-db.org/xquery/test/error-test";

declare namespace test="http://exist-db.org/xquery/xqsuite";

(: Causes an error when called :)
declare function et:test($a) {
    $a + "bla"
};

(: May cause an error when called :)
declare function et:test2($a) {
    $a/foo
};

declare
    %test:assertEquals(38)
function et:dynamically-called-function() {
    let $fn := function-lookup(xs:QName("et:test"), 1)
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(43)
function et:dynamically-called-function-path-expr() {
    let $fn := function-lookup(xs:QName("et:test2"), 1)
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(74)
function et:inline-function-call() {
    let $fn := function($a) {
        $a + "bla"
    }
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(38)
function et:function-reference-call() {
    let $fn := et:test#1
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(108)
function et:nl-in-string-literal() {
    try {
        let $foo := "bar

        "
        return
            element x {
                attribute y { "1" },
                (: next line will generate dynamic error :)
                element z { sum($foo) }
            }
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(127)
function et:nl-in-string-constructor() {
    try {
        let $foo := ``[bar

        ]``
        return
            element x {
                attribute y { "1" },
                (: next line will generate dynamic error :)
                element z { sum($foo) }
            }
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(145)
function et:nl-in-element-constructor() {
    try {
        let $foo :=
            <test>

foo

            </test>
        return
            "abc"/node()
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(158)
function et:nl-in-comment() {
    try {
        (: This is a

        multiline comment :)
        "abc"/node()
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertXPath("/error[@line='2'][contains(., 'Invalid qname console:log')]")
function et:compile-query-unknown-func() {
    let $query := ``[
        console:log('foo')
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

declare
    %test:assertXPath("/self::info[@result='pass']")
function et:compile-query-variable-decl-pass() {
    let $query := ``[
        declare variable $v := map {
            "a": "b",
            "f": function() {
                ()
            }
        };
        $v?f()
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

(: Should not result in an NPE, see https://github.com/eXist-db/exist/pull/1520#issuecomment-604514099 :)
declare
    %test:assertXPath("/error[@line='5'][contains(., 'Invalid qname console:log')]")
function et:compile-query-variable-decl() {
    let $query := ``[
        declare variable $v := map {
            "a": "b",
            "f": function() {
                console:log('foo')
            }
        };
        $v?f()
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

declare
    %test:assertXPath("/error[@line='6'][contains(., 'local:test')]")
function et:compile-query-func-decl() {
    let $query := ``[
        declare function local:test($x) {
            $x
        };

        local:test(123, 345)
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

(: https://github.com/eXist-db/exist/issues/3450 :)
declare
    %test:assertEquals(229)
function et:issue3450a() {
    try {
    (:
    empty
    :)
        if (map { "foo": "bar" } eq 1) then "yay" else "boo"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3450 :)
declare
    %test:assertEquals(241)
function et:issue3450b() {
    try {
        if ("a" eq 1) then "yay" else "boo"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3462 :)
declare
    %test:assertEquals(254)
function et:issue3462a() {
    try {
        map {
            "foo": ["bar", "baz"]
        }[?foo eq "bar"]
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(269)
function et:issue3462b() {
    try {
            map {
                "foo":
                    map {
                        "bar": "baz"
                    }
            }[?foo eq "bar"]
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3473 :)
declare
    %test:assertEquals(280)
function et:issue3473a() {
    try {
        1 eq "2"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3473 :)
declare
    %test:assertEquals(291)
function et:issue3473b() {
    try {
        1 = "2"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3473 :)
declare
    %test:assertEquals(322)
function et:issue3473c() {
    try {
        let $colors :=
                 map {
                     "blue": "#2d7ff9",
                     "cyan": "#18bfff",
                     "gray": "#666",
                     "grayDark": "666",
                     "green": "#20c933",
                     "greenDark": "20c933",
                     "orange": "#ff6f2c",
                     "pink": "#ff08c2",
                     "pinkDark": "ff08c2",
                     "purple": "#8b46ff",
                     "red": "#f82b60",
                     "redDarker": "f82b60",
                     "teal": "#20d9d2",
                     "yellow": "#fcb400",
                     "yellowDark": "fcb400"
                 }

             return
                error()
    } catch * {
        $err:line-number
    }
};

(:~
 : function type item returned in element content in sequence at runtime
 : related to https://github.com/eXist-db/exist/issues/3474
 :)
declare
    %test:assertTrue
    %test:pending("location info still missing")
function et:subexpression-in-enclosed-expression-evaluates-to-map() {
    try {
        element test { 1, map {} }
    }
    catch err:XQTY0105 {
        exists($err:line-number) and
        $err:line-number > 0 and
        exists($err:column-number) and
        $err:column-number > 0
    }
};

declare
    %test:assertTrue
    %test:pending("location info still missing")
function et:enclosed-expression-evaluates-to-map() {
    try {
        element test { ( "a", map {} )[2] }
    }
    catch err:XQTY0105 {
        exists($err:line-number) and
        $err:line-number > 0 and
        exists($err:column-number) and
        $err:column-number > 0
    }
};

declare
    %test:assertTrue
function et:array-in-enclosed-expression-evaluates-to-map() {
    try {
        element test { [map {}] }
    }
    catch err:XQTY0105 {
        exists($err:line-number) and
        $err:line-number > 0 and
        exists($err:column-number) and
        $err:column-number > 0
    }
};

declare
    %test:assertTrue
function et:element-type-error-reporting() {

    try {
        let $test as element(b)? := <d/> return $test
    } catch * {
            fn:ends-with($err:description, "Invalid type for variable $test. Expected element(b)?, got element(a)")
    }

};

(:
 : eXist-db Open Source Native XML Database
 : Copyright (C) 2001 The eXist-db Authors
 :
 : info@exist-db.org
 : http://www.exist-db.org
 :
 : This library is free software; you can redistribute it and/or
 : modify it under the terms of the GNU Lesser General Public
 : License as published by the Free Software Foundation; either
 : version 2.1 of the License, or (at your option) any later version.
 :
 : This library is distributed in the hope that it will be useful,
 : but WITHOUT ANY WARRANTY; without even the implied warranty of
 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 : Lesser General Public License for more details.
 :
 : You should have received a copy of the GNU Lesser General Public
 : License along with this library; if not, write to the Free Software
 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 :)
xquery version "3.1";

(:~
 : Tests to check if the reported line numbers for errors are correct:
 :
 : * if a function is called dynamically. In previous eXist versions, the
 : line number always pointed to the place where the function item was created,
 : not the actual function body
 : * if newlines occur in string literals, constructors or comments
 :)
module namespace et="http://exist-db.org/xquery/test/error-test";

declare namespace test="http://exist-db.org/xquery/xqsuite";

(: Causes an error when called :)
declare function et:test($a) {
    $a + "bla"
};

(: May cause an error when called :)
declare function et:test2($a) {
    $a/foo
};

declare
    %test:assertEquals(38)
function et:dynamically-called-function() {
    let $fn := function-lookup(xs:QName("et:test"), 1)
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(43)
function et:dynamically-called-function-path-expr() {
    let $fn := function-lookup(xs:QName("et:test2"), 1)
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(74)
function et:inline-function-call() {
    let $fn := function($a) {
        $a + "bla"
    }
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(38)
function et:function-reference-call() {
    let $fn := et:test#1
    return
        try {
            $fn(1)
        } catch * {
            $err:line-number
        }
};

declare
    %test:assertEquals(108)
function et:nl-in-string-literal() {
    try {
        let $foo := "bar

        "
        return
            element x {
                attribute y { "1" },
                (: next line will generate dynamic error :)
                element z { sum($foo) }
            }
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(127)
function et:nl-in-string-constructor() {
    try {
        let $foo := ``[bar

        ]``
        return
            element x {
                attribute y { "1" },
                (: next line will generate dynamic error :)
                element z { sum($foo) }
            }
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(145)
function et:nl-in-element-constructor() {
    try {
        let $foo :=
            <test>
                <p>foo

                </p>
            </test>
        return
            "abc"/node()
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(158)
function et:nl-in-comment() {
    try {
        (: This is a

        multiline comment :)
        "abc"/node()
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertXPath("/error[@line='2'][contains(., 'Invalid qname console:log')]")
function et:compile-query-unknown-func() {
    let $query := ``[
        console:log('foo')
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

declare
    %test:assertXPath("/self::info[@result='pass']")
function et:compile-query-variable-decl-pass() {
    let $query := ``[
        declare variable $v := map {
            "a": "b",
            "f": function() {
                ()
            }
        };
        $v?f()
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

(: Should not result in an NPE, see https://github.com/eXist-db/exist/pull/1520#issuecomment-604514099 :)
declare
    %test:assertXPath("/error[@line='5'][contains(., 'Invalid qname console:log')]")
function et:compile-query-variable-decl() {
    let $query := ``[
        declare variable $v := map {
            "a": "b",
            "f": function() {
                console:log('foo')
            }
        };
        $v?f()
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

declare
    %test:assertXPath("/error[@line='6'][contains(., 'local:test')]")
function et:compile-query-func-decl() {
    let $query := ``[
        declare function local:test($x) {
            $x
        };

        local:test(123, 345)
    ]``
    return
        util:compile-query($query, "xmldb:exist://")
};

(: https://github.com/eXist-db/exist/issues/3450 :)
declare
    %test:assertEquals(229)
function et:issue3450a() {
    try {
    (:
    empty
    :)
        if (map { "foo": "bar" } eq 1) then "yay" else "boo"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3450 :)
declare
    %test:assertEquals(241)
function et:issue3450b() {
    try {
        if ("a" eq 1) then "yay" else "boo"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3462 :)
declare
    %test:assertEquals(254)
function et:issue3462a() {
    try {
        map {
            "foo": ["bar", "baz"]
        }[?foo eq "bar"]
    } catch * {
        $err:line-number
    }
};

declare
    %test:assertEquals(269)
function et:issue3462b() {
    try {
            map {
                "foo":
                    map {
                        "bar": "baz"
                    }
            }[?foo eq "bar"]
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3473 :)
declare
    %test:assertEquals(280)
function et:issue3473a() {
    try {
        1 eq "2"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3473 :)
declare
    %test:assertEquals(291)
function et:issue3473b() {
    try {
        1 = "2"
    } catch * {
        $err:line-number
    }
};

(: https://github.com/eXist-db/exist/issues/3473 :)
declare
    %test:assertEquals(322)
function et:issue3473c() {
    try {
        let $colors :=
                 map {
                     "blue": "#2d7ff9",
                     "cyan": "#18bfff",
                     "gray": "#666",
                     "grayDark": "666",
                     "green": "#20c933",
                     "greenDark": "20c933",
                     "orange": "#ff6f2c",
                     "pink": "#ff08c2",
                     "pinkDark": "ff08c2",
                     "purple": "#8b46ff",
                     "red": "#f82b60",
                     "redDarker": "f82b60",
                     "teal": "#20d9d2",
                     "yellow": "#fcb400",
                     "yellowDark": "fcb400"
                 }

             return
                error()
    } catch * {
        $err:line-number
    }
};

(:~
 : function type item returned in element content in sequence at runtime
 : related to https://github.com/eXist-db/exist/issues/3474
 :)
declare
    %test:assertTrue
    %test:pending("location info still missing")
function et:subexpression-in-enclosed-expression-evaluates-to-map() {
    try {
        element test { 1, map {} }
    }
    catch err:XQTY0105 {
        exists($err:line-number) and
        $err:line-number > 0 and
        exists($err:column-number) and
        $err:column-number > 0
    }
};

declare
    %test:assertTrue
    %test:pending("location info still missing")
function et:enclosed-expression-evaluates-to-map() {
    try {
        element test { ( "a", map {} )[2] }
    }
    catch err:XQTY0105 {
        exists($err:line-number) and
        $err:line-number > 0 and
        exists($err:column-number) and
        $err:column-number > 0
    }
};

declare
    %test:assertTrue
function et:array-in-enclosed-expression-evaluates-to-map() {
    try {
        element test { [map {}] }
    }
    catch err:XQTY0105 {
        exists($err:line-number) and
        $err:line-number > 0 and
        exists($err:column-number) and
        $err:column-number > 0
    }
};


mappings-handling reworked (employ cache-fns, also get ay-data via cache)

xquery version "1.0";
module namespace smc = "http://clarin.eu/smc";

import module namespace repo-utils = "http://aac.ac.at/content_repository/utils" at  "xmldb:exist:///db/cr/repo-utils.xqm";
import module namespace crday  = "http://aac.ac.at/content_repository/data-ay" at "/db/cr/crday.xqm";
import module namespace fcs = "http://clarin.eu/fcs/1.0" at "/db/cr/fcs.xqm";

declare namespace sru = "http://www.loc.gov/zing/srw/";

declare variable $smc:termsets := doc("data/termsets.xml");
declare variable $smc:cmd-terms := doc("data/cmd-terms.xml");

(:~ create and store mapping for every profile in given nodeset
expects the CMD-structure
:)
declare function smc:get-mappings($x-context as xs:string, $config) as item()* {

let $scan-profiles :=  fcs:scan('cmd.profile', $x-context, 1, 50, 1, 1, "text", $config),
    $target_path := repo-utils:config-value($config, 'cache.path')

(: for every profile in the data-set :)
for $profile in $scan-profiles//sru:term
    let $profile-name := xs:string($profile/sru:displayTerm)
    let $map-doc-name := repo-utils:gen-cache-id("map", ($x-context, $profile-name), '')

    let $result :=
        if (repo-utils:is-in-cache($map-doc-name, $config)) then  (:and not($run-flag):)
            repo-utils:get-from-cache($map-doc-name, $config)
        else
            let $ay-data := crday:get-ay-xml($config, $x-context, $profile-name, $crday:defaultMaxDepth, '')
            let $mappings := smc:match-paths($ay-data)
            return repo-utils:store-in-cache($map-doc-name, $mappings, $config)
   return $result
(:  TODO?: add html-serialization
    return repo-utils:serialise-as($result, 'htmlpage', 'terms', $config, ()):)

};

(:~ expects a summary of data, matches the resulting paths with paths in cmd-terms
and returns dcr-indexes, that have a path in the input-data

:)
declare function smc:match-paths($ay-data as item()) as item()* {

let $data-paths := $ay-data//Term/replace(replace(xs:string(@path),'//',''),'/','.')
let $match := $smc:cmd-terms//Term[xs:string(@path) = $data-paths ]

let $mapping := for $datcat in distinct-values($match//xs:string(@datcat))
                    let $key := smc:shorten-uri($datcat)
                    return <index key="{$key}" >
                                { for $path in $match[xs:string(@datcat) = $datcat]/xs:string(@path)
                                    return <path>{$path}</path>
                                }
                            </index>
return <map>{$mapping}</map>
};

(:~ replace url_prefix in the url by the short key
based on definitions in $smc:termsets
:)
declare function smc:shorten-uri($url as xs:string) as xs:string {
    let $termset := $smc:termsets//Termset[url_prefix][starts-with($url, url_prefix)][$url ne '']
    let $url-suffix := substring-after ($url, $termset/url_prefix)
    return if (exists($termset)) then concat($termset/key, ':', $url-suffix) else $url
};

xquery version "1.0";
module namespace smc = "http://clarin.eu/smc";

import module namespace repo-utils = "http://aac.ac.at/content_repository/utils" at  "xmldb:exist:///db/cr/repo-utils.xqm";
import module namespace crday  = "http://aac.ac.at/content_repository/data-ay" at "/db/cr/crday.xqm";
import module namespace cmdcheck  = "http://clarin.eu/cmd/check" at "/db/cr/modules/cmd/cmd-check.xqm";

declare namespace sru = "http://www.loc.gov/zing/srw/";

declare variable $smc:termsets := doc("data/termsets.xml");
declare variable $smc:cmd-terms := doc("data/cmd-terms.xml");
declare variable $smc:ay-data-depth := 8;

(:~ create and store mapping for every profile in given nodeset
expects the CMD-structure
:)
declare function smc:create-mappings($x-context as xs:string, $config) as item()* {

let $data-collection := repo-utils:context-to-collection($x-context, $config),
    $ay-profiles := cmdcheck:stat-profiles($data-collection),
    $target_path := repo-utils:config-value($config, 'cache.path')

(: for every profile in the data-set :)
for $profile in $ay-profiles//sru:term
    let $profile-name := xs:string($profile/sru:displayTerm)
    let $ay-data := crday:ay-xml($data-collection, $profile-name, $smc:ay-data-depth )
    let $mappings := smc:match-paths($ay-data)
    let $ay-doc-name := concat('ay-', repo-utils:sanitize-name($x-context), '-', $profile-name, '.xml')
    let $map-doc-name := concat('map-', repo-utils:sanitize-name($x-context), '-', $profile-name, '.xml')
    return (xmldb:store($target_path, $ay-doc-name, $ay-data),
            xmldb:store($target_path, $map-doc-name, $mappings))

};

(:~ expects a summary of data, matches the resulting paths with paths in cmd-terms
and returns dcr-indexes, that have a path in the input-data

:)
declare function smc:match-paths($ay-data as item()) as item()* {

let $data-paths := $ay-data//Term/replace(replace(xs:string(@path),'//',''),'/','.')
let $match := $smc:cmd-terms//Term[xs:string(@path) = $data-paths ]

let $mapping := for $datcat in distinct-values($match//xs:string(@datcat))
                    let $key := smc:shorten-uri($datcat)
                    return <index key="{$key}" >
                                { for $path in $match[xs:string(@datcat) = $datcat]/xs:string(@path)
                                    return <path>{$path}</path>
                                }
                            </index>
return <map>{$mapping}</map>
};

(:~ replace url_prefix in the url by the short key
based on definitions in $smc:termsets
:)
declare function smc:shorten-uri($url as xs:string) as xs:string {
    let $termset := $smc:termsets//Termset[url_prefix][starts-with($url, url_prefix)][$url ne '']
    let $url-suffix := substring-after ($url, $termset/url_prefix)
    return if (exists($termset)) then concat($termset/key, ':', $url-suffix) else $url
};


lowercase globalid for json

lowercase globalid for json

import module namespace csr_proc = "https://github.com/openhie/openinfoman/csr_proc";
import module namespace csd_webconf =  "https://github.com/openhie/openinfoman/csd_webconf";

declare namespace csd = "urn:ihe:iti:csd:2013";
declare variable $careServicesRequest as item() external;

let $careServicesSubRequest :=
  <csd:careServicesRequest>
    <csd:function  urn="urn:ihe:iti:csd:2014:stored-function:provider-search" >
      <csd:requestParams/>
    </csd:function>
  </csd:careServicesRequest>

let $providers := csr_proc:process_CSR_stored_results($csd_webconf:db, /. , $careServicesSubRequest)

let $contacts :=
  <json type='object'>
    <contacts type="array">
    {
      for $provider in  $providers/csd:providerDirectory/csd:provider
      let $uuid := lower-case(string($provider/@entityID))
      let $name := ($provider/csd:demographic/csd:name/csd:commonName)[1]/text()
      let $tels := $provider/csd:demographic/csd:contactPoint/csd:codedType[@code="BP" and  @codingScheme="urn:ihe:iti:csd:2013:contactPoint"]
      let $tel_1 := $tels[1]/text()
      let $tel_2 := $tels[2]/text()
      let $tel_3 := $tels[3]/text()
      return
	if (true()) (:  ($uuid and $name)  :)
       then
         <_  type="object">
	   <name>{$name}</name>
	   <urns type="array">
             { if ($tel_1) then   <_ type='string'>tel:{$tel_1}</_> else ()}
             { if ($tel_2) then   <_ type='string'>tel:{$tel_2}</_> else ()}
             { if ($tel_3) then   <_ type='string'>tel:{$tel_3}</_> else ()}
	   </urns>
	   <fields type="object">
             <globalid>{$uuid}</globalid>
	   </fields>
         </_>
       else ()
    }
    </contacts>
  </json>

return json:serialize($contacts,map{"format":"direct"})

import module namespace csr_proc = "https://github.com/openhie/openinfoman/csr_proc";
import module namespace csd_webconf =  "https://github.com/openhie/openinfoman/csd_webconf";

declare namespace csd = "urn:ihe:iti:csd:2013";
declare variable $careServicesRequest as item() external;

let $careServicesSubRequest :=
  <csd:careServicesRequest>
    <csd:function  urn="urn:ihe:iti:csd:2014:stored-function:provider-search" >
      <csd:requestParams/>
    </csd:function>
  </csd:careServicesRequest>

let $providers := csr_proc:process_CSR_stored_results($csd_webconf:db, /. , $careServicesSubRequest)

let $contacts :=
  <json type='object'>
    <contacts type="array">
    {
      for $provider in  $providers/csd:providerDirectory/csd:provider
      let $uuid := lower-case(string($provider/@entityID))
      let $name := ($provider/csd:demographic/csd:name/csd:commonName)[1]/text()
      let $tels := $provider/csd:demographic/csd:contactPoint/csd:codedType[@code="BP" and  @codingScheme="urn:ihe:iti:csd:2013:contactPoint"]
      let $tel_1 := $tels[1]/text()
      let $tel_2 := $tels[2]/text()
      let $tel_3 := $tels[3]/text()
      return
	if (true()) (:  ($uuid and $name)  :)
       then
         <_  type="object">
	   <name>{$name}</name>
	   <urns type="array">
             { if ($tel_1) then   <_ type='string'>tel:{$tel_1}</_> else ()}
             { if ($tel_2) then   <_ type='string'>tel:{$tel_2}</_> else ()}
             { if ($tel_3) then   <_ type='string'>tel:{$tel_3}</_> else ()}
	   </urns>
	   <fields type="object">
             <Globalid>{$uuid}</Globalid>
	   </fields>
         </_>
       else ()
    }
    </contacts>
  </json>

return json:serialize($contacts,map{"format":"direct"})


Add two tests as indicated by original issue submitter

xquery version "3.0";

module namespace nt="http://exist-db.org/xquery/test/namespaces";

declare namespace z="http://www.zorba-xquery.com/";
declare namespace ok="http://place-on-interwebz.com/a-ok";

import module namespace test="http://exist-db.org/xquery/xqsuite" at "resource:org/exist/xquery/lib/xqsuite/xqsuite.xql";

declare
    %test:assertEquals("<test xmlns:foo='http://foo.com'/>")
function nt:dynamicNSConstr1() {
    <test>
    { namespace foo { "http://foo.com" } }
    </test>
};

declare
    %test:assertEquals("<test xmlns:foo='http://foo.com'>bla</test>")
function nt:dynamicNSConstr2() {
    element { "test" } {
        namespace foo { "http://foo.com" },
        "bla"
    }
};

declare
    %test:assertEquals("<test xmlns:foo1='http://foo.com'/>")
function nt:dynamicNSConstr3() {
    <test>
    { namespace { "foo" || 1 } { "http://foo.com" } }
    </test>
};

declare
    %test:assertEquals("")
function nt:dynamicNSConstr4() {
    let $xml :=

    return
        element { node-name($xml) } {
            nt:copy-ns($xml)
        }
};

declare %private function nt:copy-ns($node) {
    for $prefix in in-scope-prefixes($node)
    return
        namespace { $prefix } { namespace-uri-for-prefix($prefix, $node) }
};

declare
    %test:assertEquals('<h:html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://www.w3.org/1999/xhtml"/>')
function nt:dynamicNSConstrEmptyNS() {
    <h:html xmlns:h="http://www.w3.org/1999/xhtml">
        { namespace { "" } { "http://www.w3.org/1999/xhtml" } }
    </h:html>
};

declare
    %test:assertEquals("<age xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:type='xs:integer'>23</age>")
function nt:dynamicNSConstrAttrib1() {
    <age xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> {
      namespace xs {"http://www.w3.org/2001/XMLSchema"},
      attribute xsi:type {"xs:integer"},
      23
    }</age>
};

declare
    %test:assertEquals("<age xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:type='xs:integer'>23</age>")
function nt:dynamicNSConstrAttrib2() {
    element age {
        namespace xsi { "http://www.w3.org/2001/XMLSchema-instance" },
        namespace xs {"http://www.w3.org/2001/XMLSchema"},
        attribute xsi:type {"xs:integer"},
        23
    }
};

declare
    %test:assertEquals('<e xmlns:saxon="http://saxon.sf.net/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" a="23"/>')
function nt:nscons-001() {
    let $s := "http://saxon.sf.net/"
    let $xsl := "http://www.w3.org/1999/XSL/Transform"
    return
        <e>{ namespace saxon {$s}, attribute a {23}, namespace xsl {$xsl} }</e>
};

declare
    %test:assertEquals('<out><t:e xmlns:t="http://www.example.com/" xmlns="http://saxon.sf.net/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" a="23"><f xmlns=""/></t:e></out>')
function nt:nscons-002() {
    let $s := "saxon"
    let $xsl := "xsl"
    return
        <out>
            <t:e xmlns:t="http://www.example.com/">{
            namespace {""} {"http://saxon.sf.net/"},
            attribute a {23},
            namespace {$xsl} {"http://www.w3.org/1999/XSL/Transform"}, <f/>
            }</t:e>
        </out>
};

declare
    %test:assertEquals('<out><t:e xmlns:t="http://www.example.com/" xmlns="http://saxon.sf.net/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" a="23"><f xmlns=""/></t:e></out>')
function nt:nscons-003() {
    let $s := "saxon"
    let $xsl := "xsl"
    return
        <out>
            <t:e xmlns:t="http://www.example.com/">{
            namespace {""} {"http://saxon.sf.net/"},
            attribute a {23},
            namespace {$xsl} {"http://www.w3.org/1999/XSL/Transform"}, <f/> }</t:e>
        </out>
};

declare
    %test:assertEquals('<out><t:e xmlns:t="http://www.example.com/" xml:space="preserve" a="23"><f/></t:e></out>')
function nt:nscons-004() {
    let $s := "saxon"
    let $xml := "http://www.w3.org/XML/1998/namespace"
    return
        <out> <t:e xmlns:t="http://www.example.com/" xml:space="preserve">{
            namespace xml {"http://www.w3.org/XML/1998/namespace"},
            attribute a {23}, <f/> }</t:e> </out>
};

declare
    %test:assertEquals('<saxon:extension xmlns:saxon="http://saxon.sf.net/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" a="23"><f>42</f></saxon:extension>')
function nt:nscons-005() {
    let $s := "http://saxon.sf.net/"
    let $xsl := "http://www.w3.org/1999/XSL/Transform"
    return
        element {QName("http://saxon.sf.net/", "saxon:extension")} { namespace saxon {$s}, attribute a {23}, namespace xsl {$xsl}, element f {42} }
};

declare
    %test:assertEquals('<saxon:extension xmlns:saxon="http://saxon.sf.net/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" a="23"><f>42</f></saxon:extension>')
function nt:nscons-006() {
    let $s := "http://saxon.sf.net/"
    let $xsl := "http://www.w3.org/1999/XSL/Transform"
    return
        element {QName("http://saxon.sf.net/", "saxon:extension")}
        { namespace saxon {$s}, attribute a {23}, namespace xsl {$xsl}, namespace saxon {$s}, element f {42} }
};

declare
    %test:assertError("XQDY0101")
function nt:nscons-007() {
    let $s := "http://saxon.sf.net/"
    let $xsl := "http://www.w3.org/1999/XSL/Transform"
    let $xmlns := "xmlns"
    return
        <e> { namespace saxon {$s}, attribute a {23}, namespace xsl {$xsl}, namespace xmlns {$s}, element f {42} }</e>
};

declare
    %test:assertError("XQDY0101")
function nt:nscons-008() {
    let $s := "http://saxon.sf.net/"
    let $xsl := "http://www.w3.org/1999/XSL/Transform"
    let $xmlns := "xml"
    return
        <e> { namespace saxon {$s}, attribute a {23}, namespace xsl {$xsl}, namespace {$xmlns} {$s}, element f {42} }</e>
};

declare function nt:nscons-015() {
    <z:e>{ namespace { /* } { "http://www.w3.org/" } }</z:e>
};

declare
    %test:assertError
function nt:dynamicNSConstrError() {
    <test>
    { namespace { (1, 2) } { "http://foo.com" } }
    </test>
};

declare
    %test:assertEquals('<e xmlns:z="http://www.zorba-xquery.com/"/>')
function nt:ns-cons013() {
    let $pre := <prefix>z</prefix>,
        $uri := "http://www.zorba-xquery.com/"
    return
      <e>{ namespace { $pre } { $uri } }</e>
};

declare
    %test:assertEquals('<e xmlns:z="http://www.zorba-xquery.com/"/>')
function nt:ns-cons014() {
    let $pre := "z",
        $uri := "http://www.zorba-xquery.com/"
    return
      <e>{ namespace { $pre } { $uri } }</e>
};

declare
    %test:assertError("XQDY0074")
function nt:ns-cons016() {
    let $pre := <prefix>z:z</prefix>,
        $uri := "http://www.zorba-xquery.com/"
    return
      <e>{ namespace { $pre } { $uri } }</e>
};

declare
    %test:assertError("XQDY0074")
function nt:ns-cons17() {
    let $pre := "z z",
        $uri := "http://www.zorba-xquery.com/"
    return
      <e>{ namespace { $pre } { $uri } }</e>
};

declare
    %test:assertError("XPTY0004")
function nt:ns-cons18() {
    let $pre := 1,
        $uri := "http://www.zorba-xquery.com/"
    return
      <e>{ namespace { $pre } { $uri } }</e>
};

declare
    %test:assertEquals('<test xmlns:foo="http://foo.com"></test>')
function nt:dynamicNSNodeFromFunc() {
    <test>
    { nt:getNSNode() }
    </test>
};

declare function nt:getNSNode() {
    namespace { "foo" } { "http://foo.com" }
};

declare
    %test:assertError("XQDY0102")
function nt:cannot-override-no-ns() {
    element root {
        namespace {""} {"http://also-on-interwebz.com/problem"},
        namespace ok {"http://place-on-interwebz.com/a-ok"},
        for $n in 1 to 3
        return
            element stuff {$n}
    }
};

declare
    %test:assertEquals(3)
function nt:ns-default-constructor() {
    count(
        element ok:root {
            namespace {""} {"http://also-on-interwebz.com/problem"},
            namespace ok {"http://place-on-interwebz.com/a-ok"},
            for $n in 1 to 3
            return
                element stuff {$n}
        }/stuff
    )
};

declare
    %test:assertError("XPTY0004")
function nt:qname-rhs-compare() {
    'test' eq xs:QName('test')
};

declare
    %test:assertError("XPTY0004")
function nt:qname-lhs-compare() {
    xs:QName('test') eq 'test'
};

declare
    %test:assertError("XPTY0004")
function nt:int-string-compare() {
    5 eq 'five'
};

declare
    %test:assertError("XPTY0004")
function nt:string-int-compare() {
    'five' eq 5
};

declare
    %test:assertError("XPTY0004")
function nt:uri-int-compare() {
    xs:anyURI('5') eq 5
};

declare
    %test:assertError("XPTY0004")
function nt:int-uri-compare() {
    5 eq xs:anyURI('5')
};

declare
    %test:assertEquals("false")
function nt:right_empty_sequence-date() {
    current-date() = ()
};

declare
    %test:assertEquals("false")
function nt:right_empty_sequence-boolean() {
    false() = ()
};


[test] Enable now passing pending tests

[test] Enable now passing pending tests

xquery version "3.0";

module namespace fd="http://exist-db.org/xquery/test/format-dates";

declare namespace test="http://exist-db.org/xquery/xqsuite";

declare
    %test:args("2012-06-26")
    %test:assertEquals("26.6.2012")
    %test:args("2012-06-01")
    %test:assertEquals("1.6.2012")
function fd:simple-date($date as xs:date) {
    format-date($date, "[D].[M].[Y]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("26.06.2012")
    %test:args("2012-06-01")
    %test:assertEquals("01.06.2012")
function fd:simple-format($date as xs:date) {
    format-date($date, "[D00].[M01].[Y0]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("26.6.2012")
    %test:args("2012-06-01")
    %test:assertEquals("1.6.2012")
function fd:format-month-numeric-width($date as xs:date) {
    format-date($date, "[D,1-2].[M,*-*].[Y,*]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("26 JUN 2012")
    %test:args("1970-10-07")
    %test:assertEquals("07 OCT 1970")
function fd:format-month-string-width-caps($date as xs:date) {
    format-date($date, "[D01] [MN,*-3] [Y0001]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("26 Jun 2012")
    %test:args("1970-10-07")
    %test:assertEquals("07 Oct 1970")
function fd:format-month-string-width-title($date as xs:date) {
    format-date($date, "[D01] [MNn,*-3] [Y0001]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("Tuesday, June 26, 2012")
    %test:args("1970-10-07")
    %test:assertEquals("Wednesday, October 7, 1970")
function fd:format-names($date as xs:date) {
    format-date($date, "[FNn], [MNn] [D], [Y]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("Tue Jun 26 2012")
    %test:args("1970-10-07")
    %test:assertEquals("Wed Oct 7 1970")
function fd:format-names-width($date as xs:date) {
    format-date($date, "[FNn,*-3] [MNn,*-3] [D] [Y]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("Tuesday, June 26th, 2012")
    %test:args("2012-06-01")
    %test:assertEquals("Friday, June 1st, 2012")
    %test:args("2012-06-02")
    %test:assertEquals("Saturday, June 2nd, 2012")
    %test:args("2012-06-03")
    %test:assertEquals("Sunday, June 3rd, 2012")
function fd:format-ordinal($date as xs:date) {
    format-date($date, "[FNn], [MNn] [D0o], [Y]")
};

declare
    %test:args("2002-12-31")
    %test:assertEquals("[2002-12-31]")
function fd:format-square-brackets($date as xs:date) {
    format-date($date, "[[[Y0001]-[M01]-[D01]]]")
};

declare
    %test:args("2012-06-26T23:14:22.566+02:00")
    %test:assertEquals("11:14 pm on Tuesday, June 26th, 2012")
function fd:format-dateTime($date as xs:dateTime) {
    format-dateTime($date, "[h00]:[m00] [P] on [FNn], [MNn] [D1o], [Y]")
};

declare
    %test:args("2012-06-26+02:00")
    %test:assertEquals("20120626GMT+02:00")
function fd:format-date-z($date as xs:date) {
    format-date($date, "[Y0001][M01][D01][z]")
};

declare
    %test:args("2012-06-26+02:00")
    %test:assertEquals("20120626+02:00")
function fd:format-date-Z($date as xs:date) {
    format-date($date, "[Y0001][M01][D01][Z]")
};

declare
    %test:args("2012-06-26T23:14:22.566+02:00")
    %test:assertEquals("20120626231422GMT+02:00")
function fd:format-dateTime-z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][z]")
};

declare
    %test:args("2012-06-26T23:14:22.566+02:00")
    %test:assertEquals("20120626231422+02:00")
function fd:format-dateTime-Z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][Z]")
};

declare
    %test:args("2012-06-26T23:14:22.566")
    %test:assertEquals("20120626231422")
function fd:format-dateTime-no-tz-z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][z]")
};

declare
    %test:args("2012-06-26T23:14:22.566")
    %test:assertEquals("20120626231422")
function fd:format-dateTime-no-tz-Z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][Z]")
};

declare
    %test:args("17:45:50")
    %test:assertEquals("17:45:50")
    %test:args("09:45:50")
    %test:assertEquals("09:45:50")
function fd:simple-time($time as xs:time) {
    format-time($time, "[H00]:[m00]:[s]")
};

declare
    %test:args("17:45:50")
    %test:assertEquals("05:45 pm")
    %test:args("09:45:50")
    %test:assertEquals("09:45 am")
function fd:time-am-pm($time as xs:time) {
    format-time($time, "[h00]:[m00] [P]")
};

declare
    %test:assertError
function fd:date-format-in-format-time() {
    format-time(current-time(), "[Y]")
};

declare
    %test:assertError
function fd:time-format-in-format-date() {
    format-date(current-dateTime(), "[H]")
};

declare
    %test:args("2012-06-27T10:21:55.082+02:00")
    %test:assertEquals("Mittwoch, 27. Juni 2012")
    %test:args("1970-10-07T10:21:55.082+02:00")
    %test:assertEquals("Mittwoch, 7. Oktober 1970")
function fd:language($date as xs:dateTime) {
    format-date($date, "[FNn], [D1o] [MNn] [Y]", "de", (), ())
};

declare
    %test:args("2012-06-27T10:21:55.082+02:00")
    %test:assertEquals("Среда, 27 Июня 2012")
    %test:args("1970-10-07T10:21:55.082+02:00")
    %test:assertEquals("Среда, 7 Октября 1970")
function fd:language-ru($date as xs:dateTime) {
    format-date($date, "[FNn], [D1o] [MNn] [Y]", "ru", (), ())
};

xquery version "3.0";

module namespace fd="http://exist-db.org/xquery/test/format-dates";

declare namespace test="http://exist-db.org/xquery/xqsuite";

declare
    %test:args("2012-06-26")
    %test:assertEquals("26.6.2012")
    %test:args("2012-06-01")
    %test:assertEquals("1.6.2012")
function fd:simple-date($date as xs:date) {
    format-date($date, "[D].[M].[Y]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("26.06.2012")
    %test:args("2012-06-01")
    %test:assertEquals("01.06.2012")
function fd:simple-format($date as xs:date) {
    format-date($date, "[D00].[M01].[Y0]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("26.6.2012")
    %test:args("2012-06-01")
    %test:assertEquals("1.6.2012")
function fd:format-month-numeric-width($date as xs:date) {
    format-date($date, "[D,1-2].[M,*-*].[Y,*]")
};

declare
    %test:pending
    %test:args("2012-06-26")
    %test:assertEquals("26 JUN 2012")
    %test:args("1970-10-07")
    %test:assertEquals("07 OCT 1970")
function fd:format-month-string-width-caps($date as xs:date) {
    format-date($date, "[D01] [MN,*-3] [Y0001]")
};

declare
    %test:pending
    %test:args("2012-06-26")
    %test:assertEquals("26 Jun 2012")
    %test:args("1970-10-07")
    %test:assertEquals("07 Oct 1970")
function fd:format-month-string-width-title($date as xs:date) {
    format-date($date, "[D01] [MNn,*-3] [Y0001]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("Tuesday, June 26, 2012")
    %test:args("1970-10-07")
    %test:assertEquals("Wednesday, October 7, 1970")
function fd:format-names($date as xs:date) {
    format-date($date, "[FNn], [MNn] [D], [Y]")
};

declare
    %test:pending
    %test:args("2012-06-26")
    %test:assertEquals("Tue Jun 26 2012")
    %test:args("1970-10-07")
    %test:assertEquals("Wed Oct 7 1970")
function fd:format-names-width($date as xs:date) {
    format-date($date, "[FNn,*-3] [MNn,*-3] [D] [Y]")
};

declare
    %test:args("2012-06-26")
    %test:assertEquals("Tuesday, June 26th, 2012")
    %test:args("2012-06-01")
    %test:assertEquals("Friday, June 1st, 2012")
    %test:args("2012-06-02")
    %test:assertEquals("Saturday, June 2nd, 2012")
    %test:args("2012-06-03")
    %test:assertEquals("Sunday, June 3rd, 2012")
function fd:format-ordinal($date as xs:date) {
    format-date($date, "[FNn], [MNn] [D0o], [Y]")
};

declare
    %test:args("2002-12-31")
    %test:assertEquals("[2002-12-31]")
function fd:format-square-brackets($date as xs:date) {
    format-date($date, "[[[Y0001]-[M01]-[D01]]]")
};

declare
    %test:args("2012-06-26T23:14:22.566+02:00")
    %test:assertEquals("11:14 pm on Tuesday, June 26th, 2012")
function fd:format-dateTime($date as xs:dateTime) {
    format-dateTime($date, "[h00]:[m00] [P] on [FNn], [MNn] [D1o], [Y]")
};

declare
    %test:args("2012-06-26+02:00")
    %test:assertEquals("20120626GMT+02:00")
function fd:format-date-z($date as xs:date) {
    format-date($date, "[Y0001][M01][D01][z]")
};

declare
    %test:args("2012-06-26+02:00")
    %test:assertEquals("20120626+02:00")
function fd:format-date-Z($date as xs:date) {
    format-date($date, "[Y0001][M01][D01][Z]")
};

declare
    %test:args("2012-06-26T23:14:22.566+02:00")
    %test:assertEquals("20120626231422GMT+02:00")
function fd:format-dateTime-z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][z]")
};

declare
    %test:args("2012-06-26T23:14:22.566+02:00")
    %test:assertEquals("20120626231422+02:00")
function fd:format-dateTime-Z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][Z]")
};

declare
    %test:args("2012-06-26T23:14:22.566")
    %test:assertEquals("20120626231422")
function fd:format-dateTime-no-tz-z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][z]")
};

declare
    %test:args("2012-06-26T23:14:22.566")
    %test:assertEquals("20120626231422")
function fd:format-dateTime-no-tz-Z($date as xs:dateTime) {
    format-dateTime($date, "[Y0001][M01][D01][H01][m01][s01][Z]")
};

declare
    %test:args("17:45:50")
    %test:assertEquals("17:45:50")
    %test:args("09:45:50")
    %test:assertEquals("09:45:50")
function fd:simple-time($time as xs:time) {
    format-time($time, "[H00]:[m00]:[s]")
};

declare
    %test:args("17:45:50")
    %test:assertEquals("05:45 pm")
    %test:args("09:45:50")
    %test:assertEquals("09:45 am")
function fd:time-am-pm($time as xs:time) {
    format-time($time, "[h00]:[m00] [P]")
};

declare
    %test:assertError
function fd:date-format-in-format-time() {
    format-time(current-time(), "[Y]")
};

declare
    %test:assertError
function fd:time-format-in-format-date() {
    format-date(current-dateTime(), "[H]")
};

declare
    %test:args("2012-06-27T10:21:55.082+02:00")
    %test:assertEquals("Mittwoch, 27. Juni 2012")
    %test:args("1970-10-07T10:21:55.082+02:00")
    %test:assertEquals("Mittwoch, 7. Oktober 1970")
function fd:language($date as xs:dateTime) {
    format-date($date, "[FNn], [D1o] [MNn] [Y]", "de", (), ())
};

declare
    %test:args("2012-06-27T10:21:55.082+02:00")
    %test:assertEquals("Среда, 27 Июня 2012")
    %test:args("1970-10-07T10:21:55.082+02:00")
    %test:assertEquals("Среда, 7 Октября 1970")
function fd:language-ru($date as xs:dateTime) {
    format-date($date, "[FNn], [D1o] [MNn] [Y]", "ru", (), ())
};


updating bindings

updating bindings

module namespace page = 'http://basex.org/modules/web-page';

(:Import other namespaces.  :)
import module namespace csd_webconf =  "https://github.com/openhie/openinfoman/csd_webconf";
import module namespace csd_webui =  "https://github.com/openhie/openinfoman/csd_webui";
import module namespace csr_proc = "https://github.com/openhie/openinfoman/csr_proc";
import module namespace csd_dm = "https://github.com/openhie/openinfoman/csd_dm";

declare namespace csd = "urn:ihe:iti:csd:2013";

declare function page:is_whomds($search_name) {
  let $function := csr_proc:get_function_definition($csd_webconf:db,$search_name)
  let $ext := $function//csd:extension[  @urn='urn:openhie.org:openinfoman:adapter' and @type='whomds']
  return (count($ext) > 0)
};

declare function page:get_actions($search_name) {
  let $function := csr_proc:get_function_definition($csd_webconf:db,$search_name)
  return
    (
    for $act in $function//csd:extension[  @urn='urn:openhie.org:openinfoman:adapter:whomds:action']/@type
    return string($act)
  )
};

declare
  %rest:path("/CSD/csr/{$doc_name}/careServicesRequest/{$search_name}/adapter/whomds")
  %output:method("xhtml")
  function page:show_endpoints($search_name,$doc_name)
{
    if (not(page:is_whomds($search_name)) )
      then ('Not a WHO MDS Compatible stored function'    )
    else
      let $actions := page:get_actions($search_name)
      let $contents :=

WHO MDS Operations on {$doc_name}

        {
          if ($actions = 'completeness')
	  then

Completeness Report

	     {
	       let $url := csd_webui:generateURL( ("CSD/csr/" , $doc_name , "/careServicesRequest/",$search_name, "/adapter/whomds/completeness"))
	       return
Get the  Completness Report

	     }

	  else ()
	}

      return csd_webui:wrapper($contents)
};

declare
  %rest:path("/CSD/csr/{$doc_name}/careServicesRequest/{$search_name}/adapter/whomds/completeness")
  function page:completness($search_name,$doc_name)
{
  if (not(page:is_whomds($search_name)) )
    then ('Not a WHO MDS Compatible stored function'    )
  else
    let $doc :=  csd_dm:open_document($csd_webconf:db,$doc_name)
    let $function := csr_proc:get_function_definition($csd_webconf:db,$search_name)

    let $careServicesRequest :=
      <csd:careServicesRequest>
       <csd:function urn="{$search_name}" resource="{$doc_name}" base_url="{csd_webui:generateURL()}">
         <csd:requestParams/>
       </csd:function>
      </csd:careServicesRequest>
    let $stats := csr_proc:process_CSR_stored_results($csd_webconf:db, $doc,$careServicesRequest)
    let $output := $function/@content-type
    let $mime :=
      if (exists($output))
      then string($output)
      else "text/html"
    let $content :=
      if (not($output = 'text/html'))
      then $stats
      else
        csd_webui:wrapper(

WHO MDS Completeness Report For {$doc_name}

	    {$stats}

	)
    return
    (
      <rest:response>
	<http:response status="200" >
          <http:header name='Content-Type' value="{$mime}"/>
	</http:response>
      </rest:response>
      ,$content
    )

};

module namespace page = 'http://basex.org/modules/web-page';

(:Import other namespaces.  :)
import module namespace csd_webconf =  "https://github.com/openhie/openinfoman/csd_webconf";
import module namespace csd_webui =  "https://github.com/openhie/openinfoman/csd_webui";
import module namespace csr_proc = "https://github.com/openhie/openinfoman/csr_proc";
import module namespace csd_dm = "https://github.com/openhie/openinfoman/csd_dm";

declare namespace csd = "urn:ihe:iti:csd:2013";

declare function page:is_whomds($search_name) {
  let $function := csr_proc:get_function_definition($csd_webconf:db,$search_name)
  let $ext := $function//csd:extension[  @urn='urn:openhie.org:openinfoman:adapter' and @type='whomds']
  return (count($ext) > 0)
};

declare function page:get_actions($search_name) {
  let $function := csr_proc:get_function_definition($csd_webconf:db,$search_name)
  return
    (
    for $act in $function//csd:extension[  @urn='urn:openhie.org:openinfoman:adapter:whomds:action']/@type
    return string($act)
  )
};

declare
  %rest:path("/CSD/csr/{$doc_name}/careServicesRequest/{$search_name}/adapter/whomds")
  %output:method("xhtml")
  function page:show_endpoints($search_name,$doc_name)
{
    if (not(page:is_whomds($search_name)) )
      then ('Not a WHO MDS Compatible stored function'    )
    else
      let $actions := page:get_actions($search_name)
      let $contents :=

WHO MDS Operations on {$doc_name}

        {
          if ($actions = 'completeness')
	  then

Completeness Report

	     {
	       let $url := concat(csd_webui:generateURL( "CSD/csr/" , $doc_name , "/careServicesRequest/",$search_name, "/adapter/whomds/completeness"))
	       return
Get the  Completness Report

	     }

	  else ()
	}

      return csd_webui:wrapper($contents)
};

declare
  %rest:path("/CSD/csr/{$doc_name}/careServicesRequest/{$search_name}/adapter/whomds/completeness")
  function page:completness($search_name,$doc_name)
{
  if (not(page:is_whomds($search_name)) )
    then ('Not a WHO MDS Compatible stored function'    )
  else
    let $doc :=  csd_dm:open_document($csd_webconf:db,$doc_name)
    let $function := csr_proc:get_function_definition($csd_webconf:db,$search_name)

    let $careServicesRequest :=
      <csd:careServicesRequest>
       <csd:function urn="{$search_name}" resource="{$doc_name}" base_url="{csd_webui:generateURL()}">
         <csd:requestParams/>
       </csd:function>
      </csd:careServicesRequest>
    let $stats := csr_proc:process_CSR_stored_results($csd_webconf:db, $doc,$careServicesRequest)
    let $output := $function/@content-type
    let $mime :=
      if (exists($output))
      then string($output)
      else "text/html"
    let $content :=
      if (not($output = 'text/html'))
      then $stats
      else
        csd_webui:wrapper(

WHO MDS Completeness Report For {$doc_name}

	    {$stats}

	)
    return
    (
      <rest:response>
	<http:response status="200" >
          <http:header name='Content-Type' value="{$mime}"/>
	</http:response>
      </rest:response>
      ,$content
    )

};