[refactor] Simplify the code, lookup operator is superfulous as a Map is a f(k)->v

(:
 : 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";

module namespace mt="http://exist-db.org/xquery/test/maps";

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

declare %private function mt:getMapFixture () {
    map {
        "Su" : "Sunday",
        "Mo" : "Monday",
        "Tu" : "Tuesday",
        "We" : "Wednesday",
        "Th" : "Thursday",
        "Fr" : "Friday",
        "Sa" : "Saturday"
    }
};

declare variable $mt:daysOfWeek := mt:getMapFixture();

declare variable $mt:integerKeys := map {
    1 : "Sunday",
    2 : "Monday",
    3 : "Tuesday",
    4 : "Wednesday",
    5 : "Thursday",
    6 : "Friday",
    7 : "Saturday"
};

declare variable $mt:mapOfSequences := map {0: (), 1 : ("One", "Two") };

declare
    %test:assertEquals("Wednesday")
function mt:createLiteral1() {
    $mt:daysOfWeek("We")
};

declare
    %test:assertEquals("Wednesday")
function mt:createWithIntKeys() {
    let $map := map {
        1 : "Sunday",
        2 : "Monday",
        3 : "Tuesday",
        4 : "Wednesday",
        5 : "Thursday",
        6 : "Friday",
        7 : "Saturday"
    }
    return
        $map(4)
};

declare
    %test:assertError("err:XPST0003")
function mt:draftSyntaxNotAllowed () {
    (: this needs to be evaled, to catch the static compilation error :)
    util:eval('map { 1 := 1 }')
};

declare
    %test:assertEquals(20)
function mt:createWithFunctionVariable() {
    let $fn := function($p) { $p * 2 }
    let $map := map { "callback" : $fn }
    return
        map:get($map, "callback")(10)
};

declare
    %test:assertEquals(2)
function mt:createWithFunctionValue() {
    let $map := map { "callback" : function($p) { $p * 2 } }
    return
        map:get($map, "callback")(1)
};

declare
    %test:assertEquals("Sunday")
function mt:createWithEntry() {
    let $days := ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
    let $map := map:merge($days ! map:entry(substring(., 1, 2), .))
    return
        $map("Su")
};

declare
    %test:assertXPath("count($result) = 8")
    %test:assertXPath("8 = $result")
function mt:createFromTwoMaps() {
    let $map := map:merge(($mt:integerKeys, map { 8 : "Caturday" }))
    return
        map:keys($map)
};

declare
    %test:assertEquals(0)
function mt:size-empty() {
    let $m := map {}
    return
      map:size($m)
};

declare
    %test:assertEquals("Good")
function mt:put-empty() {
    let $m := map {}
    return
      map:put($m, "Cats", "Good")("Cats")
};

declare
    %test:assertEquals("Bad")
function mt:put-single() {
    let $m := map {
        "Cats" : "Good"
    }
    return
      map:put($m, "Dogs", "Bad")("Dogs")
};

declare
    %test:assertEquals("Duck")
function mt:put() {
    let $m := map {
        "Cats" : "Good",
        "Dogs" : "Bad",
        1 : "Chicken"
    }
    return
      map:put($m, 2, "Duck")(2)
};

declare
    %test:assertEquals(4)
function mt:size() {
    let $m := map {
        "Cats": "Good",
        "Dogs": "Bad",
        1: "Chicken",
        2: "Duck"
    }
    return
        map:size($m)
};

declare
    %test:assertEquals("Sunday", "Tuesday", "Thursday", "Saturday")
function mt:for-each() {
    map:for-each($mt:integerKeys, function($key, $value) {
        if ($key mod 2) then ($value) else ()
    })
};

declare
    %test:assertEquals(3)
function mt:for-each2() {
    let $nm := map:merge(
        map:for-each(
            map { "a": 1, "b": 2 },
            function($k, $v) { map:entry($k, $v + 1) }
        )
    )

    return
        $nm?b
};

declare
    %test:assertEquals("Sunday")
function mt:createWithSingleKey() {
    let $map := map { "Su": "Sunday" }
    return
        $map("Su")
};

declare
    %test:assertEquals("Saturday", "Caturday")
function mt:overwriteKeyInNewMap() {
    let $specialWeek := map:merge(($mt:integerKeys, map { 7 : "Caturday" }))
    return
        ($mt:integerKeys(7), $specialWeek(7))
};

declare
    %test:assertEmpty
function mt:mapEmptyValue() {
    let $map := $mt:mapOfSequences
    return
        $map(0)
};

declare
    %test:assertEquals("One", "Two")
function mt:mapSequenceValue() {
    let $map := $mt:mapOfSequences
    return
        $map(1)
};

declare
    %test:assertEquals("Heinz", "Roland", "Uschi", "Verona")
function mt:orderBzKeyTypeDate () {
    let $map := map {
        xs:date("1975-03-19"): "Uschi",
        xs:date("1980-01-22"): "Verona",
        xs:date("1960-06-14"): "Heinz",
        xs:date("1963-10-21"): "Roland"
    }

    for $key in map:keys($map)
    let $name := $map($key)
    order by $name ascending
    return
        $name
};

declare
    %test:assertEquals(0, "One")
function mt:mixedKeyTypes() {
    let $map := map{ "Zero": 0, 1: "One" }
    return
        ($map("Zero"), $map(1))
};

declare
    %test:assertTrue
function mt:containsOnEmptyValue() {
    let $map := map{ 0: () }
    return
        map:contains($map, 0)
};

declare
    %test:assertEquals("Fr", "Mo", "Sa", "Su", "Th", "Tu", "We")
function mt:keysInFor() {
    for $day in map:keys(mt:getMapFixture())
    order by $day ascending
    return
        $day
};

declare
    %test:assertEquals("Su")
function mt:keysOnMapWithSingleKey() {
    let $map := map:entry("Su", "Sunday")
    return
        map:keys($map)
};

declare
    %test:assertFalse
function mt:remove() {
    let $map := mt:getMapFixture()
    return
        map:contains(map:remove($map, "Tu"), "Tu")
};

declare
    %test:assertEquals("Fr", "Mo", "Sa", "Su", "Th")
function mt:remove2() {
    let $map := mt:getMapFixture()
    for $day in map:keys(map:remove(map:remove($map, "We"), "Tu"))
    order by $day
    return
        $day
};

declare
    %test:assertEmpty
function mt:removeSingleKey() {
    let $empty := map:remove(map { "Su": "Sunday" }, "Su")
    return
        map:keys($empty)
};

declare
    %test:assertEquals("Su")
function mt:removeSingleNonExistentKey() {
    let $map := map:remove(map { "Su": "Sunday" }, "Xx")
    return
        map:keys($map)
};

declare
    %test:assertEquals("Sa")
function mt:removeSequenceOfStringKeys() {
    mt:getMapFixture()
        => map:remove(("Mo", "Tu", "We", "Th", "Fr", "Su"))
        => map:keys()
};

declare
    %test:assertEquals("Sa")
function mt:removeSequenceOfStringKeysWithNonExistent() {
    mt:getMapFixture()
        => map:remove(("Mo", "Tu", "We", "Th", "Fr", "Su", "Xx"))
        => map:keys()
};

declare
    %test:assertEquals(7)
function mt:removeRangeOfIntegerKeys() {
    $mt:integerKeys
        => map:remove(1 to 6)
        => map:keys()
};

declare
    %test:assertEquals(7)
function mt:removeIntegerKeysWithNonExistent() {
    $mt:integerKeys
        => map:remove((1, 2, 3, 4, 5, 6, 10))
        => map:keys()
};

declare
    %test:assertTrue
function mt:contains() {
    map:contains($mt:daysOfWeek, "We")
};

declare
    %test:assertFalse
function mt:containsOnMissingKey() {
    map:contains($mt:daysOfWeek, "Al")
};

declare
    %test:assertTrue
function mt:containsSingleKey() {
    map:contains(map { "Su": "Sunday" }, "Su")
};

declare
    %test:assertEquals("Hello world")
function mt:computedKeyValue() {
    let $map := map { 2 + 1 : concat("Hello ", "world") }
    return
        $map(3)
};

declare
    %test:assertEquals("false", "true")
function mt:immutabilityMerge() {
    let $original := mt:getMapFixture()
    let $derived := map:merge(($original, map:entry("Ca", "Caturday")))
    return (
        map:contains($original, "Ca"),
        map:contains($derived, "Ca")
    )
};

declare
    %test:assertEquals("true", "false")
function mt:immutabilityRemove() {
    let $original := mt:getMapFixture()
    let $derived := map:remove($original, "Fr")
    return (
        map:contains($original, "Fr"),
        map:contains($derived, "Fr")
    )
};

declare
    %test:assertEquals("false", "true")
function mt:immutabilityPut() {
    let $original := mt:getMapFixture()
    let $derived := map:put($original, "Ca", "Caturday")
    return (
        map:contains($original, "Ca"),
        map:contains($derived, "Ca")
    )
};

declare
    %test:assertEquals("Sunday")
function mt:sequenceType1() {
    let $map := map { 1 : "Sunday" }
    return
        map:get(mt:mapTest($map), 1)
};

declare
    %test:assertEquals("Sunday")
function mt:sequenceType2() {
    let $map := map { 1 : "Sunday" }
    return
        map:get(mt:mapTestFail($map), 1)
};

declare
    %private
function mt:mapTest($map as map(*)) as map(xs:integer, xs:string) {
    $map
};

declare
    %private
function mt:mapTestFail($map as map(*)) as map(xs:date, xs:string) {
    $map
};

declare
    %test:assertError("EXMPDY001")
    %test:name("Throw error if key is not a single atomic value")
function mt:wrongCardinality() {
    let $map := map { (1, 2) : "illegal" }
    return
        $map(1)
};

declare
    %test:assertEquals("two")
function mt:nestedMaps() {
    let $map := map { 1: map { 1: "one", 2: "two" } }
    return
        $map(1)(2)
};

declare
    %test:assertEquals("Monday")
function mt:nestedMaps2() {
    let $map := map { "week" : map { "days" : $mt:daysOfWeek } }
    return
        $map("week")("days")("Mo")
};

declare
    %test:assertEquals(2)
function mt:qnameKeys() {
    let $map := map { xs:QName("mt:one") : 1, xs:QName("mt:two") : 2 }
    return
        $map(xs:QName("mt:two"))
};

declare
    %test:assertEquals(2)
function mt:longKeys() {
    let $map := map { xs:long(1) : 1, xs:long(2) : 2 }
    return
        $map(2)
};

declare
    %test:args(2)
    %test:assertEquals(3)
    %test:args("Three")
    %test:assertEmpty
function mt:doubleKeys($key as item()) {
    let $map := map { xs:double(1.1) : 1, xs:double(2) : 2 }
    return
        map:merge(($map, map:entry(xs:double(2), 3)))($key)
};

declare
    %test:assertEquals(2, "value")
function mt:mixedNumeric() {
    let $map := map { 1.1 : 1, 2 : 2, "key" : "value" }
    return
        ($map(2), $map("key"))
};

declare
    %test:assertEquals("<c ns='c'/>", "<c ns='c'/>")
function mt:pathExpr() as element(c)+ {
    let $test :=
        <a>
            <b ns="b">
                <c ns="c"/>
            </b>
        </a>
    let $map :=
        map {
            "a" : $test//c,
            "b" : ($test//c)
        }
    return
        ($map("a"), $map("b"))
};

declare
    %test:assertEquals("What were you thinking?")
function mt:lookupUnaryOperator() {
    let $errors := (
      map { "level" : 1, "text" : "Boys will be boys ..." },
      map { "level" : 2, "text" : "What were you thinking?" },
      map { "level" : 3, "text" : "Call the cops!" }
    )
    return $errors[?level = 2]?text
};

declare
    %test:assertEquals("W0342", "M0535")
function mt:lookupMultipleMaps() {
    let $maps := ( map { "id" : "W0342" }, map { "id" : "M0535" } )
    return $maps?id
};

declare
    %test:assertEquals("W0342", "M0535")
function mt:lookupMultipleMaps2() {
    (
        map { "id" : "W0342" },
        map { "id" : "M0535" }
    )?id
};

declare
    %test:assertEquals("large")
function mt:lookupNestedMap() {
    let $map := map {
        "name": "sofa", "keys": map { "price": 200.0, "size": "large" }
    }
    return
        $map?keys?size
};

declare
    %test:args("One")
    %test:assertEquals(1)
    %test:args("Two")
    %test:assertEquals("2")
function mt:lookupParenthesized($key as xs:string) {
    let $map := map { "one": 1, "two": "2" }
    return
        $map?(lower-case($key))
};

declare
    %test:assertEquals(1)
function mt:lookupParenthesized() {
    let $f := function () { "one" }
    let $map := map { "one": 1 }
    return
        $map($f())
};

declare
    %test:assertError
function mt:lookupWrongType() {
    let $map := map { "one": 1, "two": "2" }
    let $str := "Hello"
    return
        ($map, $str)?one
};

declare
    %test:assertTrue
function mt:lookupWildcard() {
    let $expected := map:keys($mt:daysOfWeek) ! $mt:daysOfWeek(.)
    let $actual := $mt:daysOfWeek?*
    return
        count($actual) eq count($expected)
        and
        (every $day in $actual satisfies $day = $expected)
};

declare
    %test:assertEquals(1)
function mt:compat() {
    let $map := map { "one": 1, "two": "2" }
    return
        $map("one")
};

declare
    %test:assertError
function mt:no-atomization() {
    data(map { "k": "v" })
};

declare
    %test:name("for-each on a map with a single entry caused NPE")
    %test:assertEquals("k")
function mt:single-entry-map() {
    let $map := map:entry("k", ())
    return
        map:for-each($map, function($k, $v) { $k })
};

declare
    %test:assertEquals(5)
function mt:qname() {
    let $a := 1
    let $m := map { $a: fn:string-length("hello") }
    return
        $m?1
};

declare
    %test:assertEmpty
function mt:no-such-entry() {
    map:get(map {"foo": "bar"}, "baz")
};

declare
    %test:assertEqualsPermutation("uk", "nl", "se", "de")
function mt:multi-merge() {
    map:merge((
  	    map { "adam": "uk" },
  	    map { "dannes": "nl" },
  	    map { "leif": "se" },
  	    map { "wolfgang": "de" }
  	))?*
};

(:
  immutability tests for https://github.com/eXist-db/exist/issues/3724
:)
declare variable $mt:test-key-one := 1;
declare variable $mt:test-key-two := 2;
declare function mt:create-test-map() {
    map {
        $mt:test-key-one : true(),
        $mt:test-key-two : true()
    }
};

declare
    %test:assertTrue
function mt:immutable-put-then-put() {
    let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
    let $expected := $extended($mt:test-key-one)
    let $result := map:put($extended, $mt:test-key-one, false())
    return $expected eq $extended($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-put-then-remove() {
    let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
    let $expected := $extended($mt:test-key-one)
    let $result := map:remove($extended, $mt:test-key-one)

    return $expected eq $extended($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-put-then-merge() {
    let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
    let $expected := $extended($mt:test-key-one)
    let $result := map:merge(($extended, map { $mt:test-key-one : false() }))

    return $expected eq $extended($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-remove-then-put() {
    let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
    let $expected := $removed($mt:test-key-one)

    let $result := map:put($removed, $mt:test-key-one, false())

    return $expected eq $removed($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-remove-then-remove() {
    let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
    let $expected := $removed($mt:test-key-one)

    let $result := map:put($removed, $mt:test-key-one, false())

    return $expected eq $removed($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-remove-then-merge() {
    let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
    let $expected := $removed($mt:test-key-one)
    let $result := map:merge(($removed, map { $mt:test-key-one : false() }))

    return $expected eq $removed($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-merge-then-put() {
    let $merged := map:merge(mt:create-test-map())
    let $expected := $merged($mt:test-key-one)

    let $result := map:put($merged, $mt:test-key-one, false())

    return $expected eq $merged($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-merge-then-remove() {
    let $merged := map:merge(mt:create-test-map())
    let $expected := $merged($mt:test-key-one)

    let $result := map:remove($merged, $mt:test-key-one)

    return $expected eq $merged($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-merge-then-merge() {
    let $merged := map:merge(mt:create-test-map())
    let $expected := $merged($mt:test-key-one)

    let $result := map:merge(($merged, map { $mt:test-key-one : false() }))

    return $expected eq $merged($mt:test-key-one)
};

(:
 : 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";

module namespace mt="http://exist-db.org/xquery/test/maps";

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

declare %private function mt:getMapFixture () {
    map {
        "Su" : "Sunday",
        "Mo" : "Monday",
        "Tu" : "Tuesday",
        "We" : "Wednesday",
        "Th" : "Thursday",
        "Fr" : "Friday",
        "Sa" : "Saturday"
    }
};

declare variable $mt:daysOfWeek := mt:getMapFixture();

declare variable $mt:integerKeys := map {
    1 : "Sunday",
    2 : "Monday",
    3 : "Tuesday",
    4 : "Wednesday",
    5 : "Thursday",
    6 : "Friday",
    7 : "Saturday"
};

declare variable $mt:mapOfSequences := map {0: (), 1 : ("One", "Two") };

declare
    %test:assertEquals("Wednesday")
function mt:createLiteral1() {
    $mt:daysOfWeek("We")
};

declare
    %test:assertEquals("Wednesday")
function mt:createWithIntKeys() {
    let $map := map {
        1 : "Sunday",
        2 : "Monday",
        3 : "Tuesday",
        4 : "Wednesday",
        5 : "Thursday",
        6 : "Friday",
        7 : "Saturday"
    }
    return
        $map(4)
};

declare
    %test:assertError("err:XPST0003")
function mt:draftSyntaxNotAllowed () {
    (: this needs to be evaled, to catch the static compilation error :)
    util:eval('map { 1 := 1 }')
};

declare
    %test:assertEquals(20)
function mt:createWithFunctionVariable() {
    let $fn := function($p) { $p * 2 }
    let $map := map { "callback" : $fn }
    return
        map:get($map, "callback")(10)
};

declare
    %test:assertEquals(2)
function mt:createWithFunctionValue() {
    let $map := map { "callback" : function($p) { $p * 2 } }
    return
        map:get($map, "callback")(1)
};

declare
    %test:assertEquals("Sunday")
function mt:createWithEntry() {
    let $days := ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
    let $map := map:merge($days ! map:entry(substring(., 1, 2), .))
    return
        $map("Su")
};

declare
    %test:assertXPath("count($result) = 8")
    %test:assertXPath("8 = $result")
function mt:createFromTwoMaps() {
    let $map := map:merge(($mt:integerKeys, map { 8 : "Caturday" }))
    return
        map:keys($map)
};

declare
    %test:assertEquals(0)
function mt:size-empty() {
    let $m := map {}
    return
      map:size($m)
};

declare
    %test:assertEquals("Good")
function mt:put-empty() {
    let $m := map {}
    return
      map:put($m, "Cats", "Good")("Cats")
};

declare
    %test:assertEquals("Bad")
function mt:put-single() {
    let $m := map {
        "Cats" : "Good"
    }
    return
      map:put($m, "Dogs", "Bad")("Dogs")
};

declare
    %test:assertEquals("Duck")
function mt:put() {
    let $m := map {
        "Cats" : "Good",
        "Dogs" : "Bad",
        1 : "Chicken"
    }
    return
      map:put($m, 2, "Duck")(2)
};

declare
    %test:assertEquals(4)
function mt:size() {
    let $m := map {
        "Cats": "Good",
        "Dogs": "Bad",
        1: "Chicken",
        2: "Duck"
    }
    return
        map:size($m)
};

declare
    %test:assertEquals("Sunday", "Tuesday", "Thursday", "Saturday")
function mt:for-each() {
    map:for-each($mt:integerKeys, function($key, $value) {
        if ($key mod 2) then ($value) else ()
    })
};

declare
    %test:assertEquals(3)
function mt:for-each2() {
    let $nm := map:merge(
        map:for-each(
            map { "a": 1, "b": 2 },
            function($k, $v) { map:entry($k, $v + 1) }
        )
    )

    return
        $nm?b
};

declare
    %test:assertEquals("Sunday")
function mt:createWithSingleKey() {
    let $map := map { "Su": "Sunday" }
    return
        $map("Su")
};

declare
    %test:assertEquals("Saturday", "Caturday")
function mt:overwriteKeyInNewMap() {
    let $specialWeek := map:merge(($mt:integerKeys, map { 7 : "Caturday" }))
    return
        ($mt:integerKeys(7), $specialWeek(7))
};

declare
    %test:assertEmpty
function mt:mapEmptyValue() {
    let $map := $mt:mapOfSequences
    return
        $map(0)
};

declare
    %test:assertEquals("One", "Two")
function mt:mapSequenceValue() {
    let $map := $mt:mapOfSequences
    return
        $map(1)
};

declare
    %test:assertEquals("Heinz", "Roland", "Uschi", "Verona")
function mt:orderBzKeyTypeDate () {
    let $map := map {
        xs:date("1975-03-19"): "Uschi",
        xs:date("1980-01-22"): "Verona",
        xs:date("1960-06-14"): "Heinz",
        xs:date("1963-10-21"): "Roland"
    }

    for $key in map:keys($map)
    let $name := $map($key)
    order by $name ascending
    return
        $name
};

declare
    %test:assertEquals(0, "One")
function mt:mixedKeyTypes() {
    let $map := map{ "Zero": 0, 1: "One" }
    return
        ($map("Zero"), $map(1))
};

declare
    %test:assertTrue
function mt:containsOnEmptyValue() {
    let $map := map{ 0: () }
    return
        map:contains($map, 0)
};

declare
    %test:assertEquals("Fr", "Mo", "Sa", "Su", "Th", "Tu", "We")
function mt:keysInFor() {
    for $day in map:keys(mt:getMapFixture())
    order by $day ascending
    return
        $day
};

declare
    %test:assertEquals("Su")
function mt:keysOnMapWithSingleKey() {
    let $map := map:entry("Su", "Sunday")
    return
        map:keys($map)
};

declare
    %test:assertFalse
function mt:remove() {
    let $map := mt:getMapFixture()
    return
        map:contains(map:remove($map, "Tu"), "Tu")
};

declare
    %test:assertEquals("Fr", "Mo", "Sa", "Su", "Th")
function mt:remove2() {
    let $map := mt:getMapFixture()
    for $day in map:keys(map:remove(map:remove($map, "We"), "Tu"))
    order by $day
    return
        $day
};

declare
    %test:assertEmpty
function mt:removeSingleKey() {
    let $empty := map:remove(map { "Su": "Sunday" }, "Su")
    return
        map:keys($empty)
};

declare
    %test:assertEquals("Su")
function mt:removeSingleNonExistentKey() {
    let $map := map:remove(map { "Su": "Sunday" }, "Xx")
    return
        map:keys($map)
};

declare
    %test:assertEquals("Sa")
function mt:removeSequenceOfStringKeys() {
    mt:getMapFixture()
        => map:remove(("Mo", "Tu", "We", "Th", "Fr", "Su"))
        => map:keys()
};

declare
    %test:assertEquals("Sa")
function mt:removeSequenceOfStringKeysWithNonExistent() {
    mt:getMapFixture()
        => map:remove(("Mo", "Tu", "We", "Th", "Fr", "Su", "Xx"))
        => map:keys()
};

declare
    %test:assertEquals(7)
function mt:removeRangeOfIntegerKeys() {
    $mt:integerKeys
        => map:remove(1 to 6)
        => map:keys()
};

declare
    %test:assertEquals(7)
function mt:removeIntegerKeysWithNonExistent() {
    $mt:integerKeys
        => map:remove((1, 2, 3, 4, 5, 6, 10))
        => map:keys()
};

declare
    %test:assertTrue
function mt:contains() {
    map:contains($mt:daysOfWeek, "We")
};

declare
    %test:assertFalse
function mt:containsOnMissingKey() {
    map:contains($mt:daysOfWeek, "Al")
};

declare
    %test:assertTrue
function mt:containsSingleKey() {
    map:contains(map { "Su": "Sunday" }, "Su")
};

declare
    %test:assertEquals("Hello world")
function mt:computedKeyValue() {
    let $map := map { 2 + 1 : concat("Hello ", "world") }
    return
        $map(3)
};

declare
    %test:assertEquals("false", "true")
function mt:immutabilityMerge() {
    let $original := mt:getMapFixture()
    let $derived := map:merge(($original, map:entry("Ca", "Caturday")))
    return (
        map:contains($original, "Ca"),
        map:contains($derived, "Ca")
    )
};

declare
    %test:assertEquals("true", "false")
function mt:immutabilityRemove() {
    let $original := mt:getMapFixture()
    let $derived := map:remove($original, "Fr")
    return (
        map:contains($original, "Fr"),
        map:contains($derived, "Fr")
    )
};

declare
    %test:assertEquals("false", "true")
function mt:immutabilityPut() {
    let $original := mt:getMapFixture()
    let $derived := map:put($original, "Ca", "Caturday")
    return (
        map:contains($original, "Ca"),
        map:contains($derived, "Ca")
    )
};

declare
    %test:assertEquals("Sunday")
function mt:sequenceType1() {
    let $map := map { 1 : "Sunday" }
    return
        map:get(mt:mapTest($map), 1)
};

declare
    %test:assertEquals("Sunday")
function mt:sequenceType2() {
    let $map := map { 1 : "Sunday" }
    return
        map:get(mt:mapTestFail($map), 1)
};

declare
    %private
function mt:mapTest($map as map(*)) as map(xs:integer, xs:string) {
    $map
};

declare
    %private
function mt:mapTestFail($map as map(*)) as map(xs:date, xs:string) {
    $map
};

declare
    %test:assertError("EXMPDY001")
    %test:name("Throw error if key is not a single atomic value")
function mt:wrongCardinality() {
    let $map := map { (1, 2) : "illegal" }
    return
        $map(1)
};

declare
    %test:assertEquals("two")
function mt:nestedMaps() {
    let $map := map { 1: map { 1: "one", 2: "two" } }
    return
        $map(1)(2)
};

declare
    %test:assertEquals("Monday")
function mt:nestedMaps2() {
    let $map := map { "week" : map { "days" : $mt:daysOfWeek } }
    return
        $map("week")("days")("Mo")
};

declare
    %test:assertEquals(2)
function mt:qnameKeys() {
    let $map := map { xs:QName("mt:one") : 1, xs:QName("mt:two") : 2 }
    return
        $map(xs:QName("mt:two"))
};

declare
    %test:assertEquals(2)
function mt:longKeys() {
    let $map := map { xs:long(1) : 1, xs:long(2) : 2 }
    return
        $map(2)
};

declare
    %test:args(2)
    %test:assertEquals(3)
    %test:args("Three")
    %test:assertEmpty
function mt:doubleKeys($key as item()) {
    let $map := map { xs:double(1.1) : 1, xs:double(2) : 2 }
    return
        map:merge(($map, map:entry(xs:double(2), 3)))($key)
};

declare
    %test:assertEquals(2, "value")
function mt:mixedNumeric() {
    let $map := map { 1.1 : 1, 2 : 2, "key" : "value" }
    return
        ($map(2), $map("key"))
};

declare
    %test:assertEquals("<c ns='c'/>", "<c ns='c'/>")
function mt:pathExpr() as element(c)+ {
    let $test :=
        <a>
            <b ns="b">
                <c ns="c"/>
            </b>
        </a>
    let $map :=
        map {
            "a" : $test//c,
            "b" : ($test//c)
        }
    return
        ($map("a"), $map("b"))
};

declare
    %test:assertEquals("What were you thinking?")
function mt:lookupUnaryOperator() {
    let $errors := (
      map { "level" : 1, "text" : "Boys will be boys ..." },
      map { "level" : 2, "text" : "What were you thinking?" },
      map { "level" : 3, "text" : "Call the cops!" }
    )
    return $errors[?level = 2]?text
};

declare
    %test:assertEquals("W0342", "M0535")
function mt:lookupMultipleMaps() {
    let $maps := ( map { "id" : "W0342" }, map { "id" : "M0535" } )
    return $maps?id
};

declare
    %test:assertEquals("W0342", "M0535")
function mt:lookupMultipleMaps2() {
    (
        map { "id" : "W0342" },
        map { "id" : "M0535" }
    )?id
};

declare
    %test:assertEquals("large")
function mt:lookupNestedMap() {
    let $map := map {
        "name": "sofa", "keys": map { "price": 200.0, "size": "large" }
    }
    return
        $map?keys?size
};

declare
    %test:args("One")
    %test:assertEquals(1)
    %test:args("Two")
    %test:assertEquals("2")
function mt:lookupParenthesized($key as xs:string) {
    let $map := map { "one": 1, "two": "2" }
    return
        $map?(lower-case($key))
};

declare
    %test:assertEquals(1)
function mt:lookupParenthesized() {
    let $f := function () { "one" }
    let $map := map { "one": 1 }
    return
        $map($f())
};

declare
    %test:assertError
function mt:lookupWrongType() {
    let $map := map { "one": 1, "two": "2" }
    let $str := "Hello"
    return
        ($map, $str)?one
};

declare
    %test:assertTrue
function mt:lookupWildcard() {
    let $expected := map:keys($mt:daysOfWeek) ! $mt:daysOfWeek(.)
    let $actual := $mt:daysOfWeek?*
    return
        count($actual) eq count($expected)
        and
        (every $day in $actual satisfies $day = $expected)
};

declare
    %test:assertEquals(1)
function mt:compat() {
    let $map := map { "one": 1, "two": "2" }
    return
        $map("one")
};

declare
    %test:assertError
function mt:no-atomization() {
    data(map { "k": "v" })
};

declare
    %test:name("for-each on a map with a single entry caused NPE")
    %test:assertEquals("k")
function mt:single-entry-map() {
    let $map := map:entry("k", ())
    return
        map:for-each($map, function($k, $v) { $k })
};

declare
    %test:assertEquals(5)
function mt:qname() {
    let $a := 1
    let $m := map { $a: fn:string-length("hello") }
    return
        $m?1
};

declare
    %test:assertEmpty
function mt:no-such-entry() {
    map:get(map {"foo": "bar"}, "baz")
};

declare
    %test:assertEqualsPermutation("uk", "nl", "se", "de")
function mt:multi-merge() {
    map:merge((
  	    map { "adam": "uk" },
  	    map { "dannes": "nl" },
  	    map { "leif": "se" },
  	    map { "wolfgang": "de" }
  	))?*
};

(:
  immutability tests for https://github.com/eXist-db/exist/issues/3724
:)
declare variable $mt:test-key-one := 1;
declare variable $mt:test-key-two := 2;
declare function mt:create-test-map() {
    map {
        $mt:test-key-one : true(),
        $mt:test-key-two : true()
    }
};

declare
    %test:assertTrue
function mt:immutable-put-then-put() {
    let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
    let $expected := $extended?($mt:test-key-one)
    let $result := map:put($extended, $mt:test-key-one, false())

    return $expected eq $extended?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-put-then-remove() {
    let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
    let $expected := $extended?($mt:test-key-one)
    let $result := map:remove($extended, $mt:test-key-one)

    return $expected eq $extended?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-put-then-merge() {
    let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
    let $expected := $extended?($mt:test-key-one)
    let $result := map:merge(($extended, map { $mt:test-key-one : false() }))

    return $expected eq $extended?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-remove-then-put() {
    let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
    let $expected := $removed?($mt:test-key-one)

    let $result := map:put($removed, $mt:test-key-one, false())

    return $expected eq $removed?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-remove-then-remove() {
    let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
    let $expected := $removed?($mt:test-key-one)

    let $result := map:put($removed, $mt:test-key-one, false())

    return $expected eq $removed?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-remove-then-merge() {
    let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
    let $expected := $removed?($mt:test-key-one)
    let $result := map:merge(($removed, map { $mt:test-key-one : false() }))

    return $expected eq $removed?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-merge-then-put() {
    let $merged := map:merge(mt:create-test-map())
    let $expected := $merged?($mt:test-key-one)

    let $result := map:put($merged, $mt:test-key-one, false())

    return $expected eq $merged?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-merge-then-remove() {
    let $merged := map:merge(mt:create-test-map())
    let $expected := $merged?($mt:test-key-one)

    let $result := map:remove($merged, $mt:test-key-one)

    return $expected eq $merged?($mt:test-key-one)
};

declare
    %test:assertTrue
function mt:immutable-merge-then-merge() {
    let $merged := map:merge(mt:create-test-map())
    let $expected := $merged?($mt:test-key-one)

    let $result := map:merge(($merged, map { $mt:test-key-one : false() }))

    return $expected eq $merged?($mt:test-key-one)
};


[ignore] Fixed jquery import in admin web app.

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

xquery version "3.0";
(:
    $Id$

    Main module of the database administration interface.
:)

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

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

import module namespace status = "http://exist-db.org/xquery/admin-interface/status" at "status.xqm";
import module namespace browse = "http://exist-db.org/xquery/admin-interface/browse" at "browse.xqm";
import module namespace users = "http://exist-db.org/xquery/admin-interface/users" at "users.xqm";
import module namespace xqueries = "http://exist-db.org/xquery/admin-interface/xqueries" at "xqueries.xqm";
import module namespace shut = "http://exist-db.org/xquery/admin-interface/shutdown" at "shutdown.xqm";
import module namespace setup = "http://exist-db.org/xquery/admin-interface/setup" at "setup.xqm";
import module namespace rev="http://exist-db.org/xquery/admin-interface/revisions" at "versions.xqm";
import module namespace backup="http://exist-db.org/xquery/admin-interface/backup" at "backup.xqm";
import module namespace prof="http://exist-db.org/xquery/profiling" at "trace.xqm";
import module namespace grammar="http://exist-db.org/xquery/admin-interface/grammar" at "grammar.xqm";
import module namespace install="http://exist-db.org/xquery/install-tools" at "install.xqm";
import module namespace repomanager="http://exist-db.org/xquery/admin-interface/repo" at "repo.xqm";
import module namespace indexes="http://exist-db.org/xquery/admin-interface/indexes" at "indexes.xqm";

declare option exist:serialize "method=xhtml media-type=text/html";

(:
    Display the version, SVN revision and user info in the top right corner
:)
declare function admin:info-header() as element()
{

Version: { util:system-property( "product-version" ) }

SVN Revision: { util:system-property( "svn-revision" ) }

Build: {util:system-property("product-build")}

User: { xdb:get-current-user() }

};

(:
    Display the menu on left side
:)
declare function admin:menu-panel() as element()
{
    let $link := session:encode-url(request:get-uri())
    return

Select a Page

Browse

Collections

Indexes

Install

betterFORM Examples

System

Shutdown

Status

Tooling

Backups

Package Repository

Query Profiling

View

Grammar cache

Running Jobs

Home

Logout

};

(:
    Select the page to show. Every page is defined in its own module
:)
declare function admin:panel() as element()
{
    let $panel := request:get-parameter("panel", "status")[1] return
        switch ($panel)
            case "browse"    return  browse:main()
            (:case "users"     return  users:main():)
            case "xqueries"  return  xqueries:main()
            case "shutdown"  return  shut:main()
            case "setup"     return  setup:main()
    		case "repo"      return  repomanager:main()
    		case "revisions" return  rev:main()
    		case "backup"    return  backup:main()
    	    case "trace"     return  prof:main()
            case "grammar"   return  grammar:main()
            case "install"   return  install:main()
            case "indexes"   return  indexes:main()
            default          return  status:main()
};

declare function admin:panel-header() {
    let $panel := request:get-parameter("panel", "status")[1]
    return
        if ($panel[1] eq "install") then
            install:header()
        else()
};

(:~
    Display the login form.
:)
declare function admin:display-login-form() as element()
{

Login

This is a protected resource. Only registered database users can log
        in. If you have not set up any users, login as "admin" and leave the
        password field empty. Note that the "guest" user is not permitted access.

        <form action="{session:encode-url(request:get-uri())}" method="post">

                    Please Login

                    Username:
                    <input name="user" type="text" size="20"/>

                    Password:
                    <input name="pass" type="password" size="20"/>

                    <input type="submit" value="Login"/>

            {
                for $param in request:get-parameter-names()
                return
                    if ( $param = ("user","pass") ) then
                        ()
                    else
                        <input type="hidden" name="{$param}" value="{request:get-parameter($param, ())}"/>

            }
        </form>

};

(: main entry point :)
let $userParam := request:get-parameter("user", ())
let $passwdParam := request:get-parameter("pass", ())
let $isLoggedIn :=  if(xdb:get-current-user() eq "guest") then
    (
        (: is this a login attempt? :)
        if($userParam and not(empty($passwdParam)))then
        (
            if($userParam = ("", "guest") )then
            (
                (: prevent the guest user from accessing the admin webapp :)
                false()
            )
            else
            (
                (: try and log the user in :)
                xdb:login( "/db", $userParam, $passwdParam, true() )
            )
        )
        else
        (
            (: prevent the guest user from accessing the admin webapp :)
            false()
        )
    )
    else
    (
        (: if we are already logged in, are we logging out - i.e. set permissions back to guest :)
        if(request:get-parameter("logout",()))then
        (
        	let $null  := xdb:login("/db", "guest", "guest")
            let $inval := session:invalidate()

            return false()
        )
        else
        (
             (: we are already logged in and we are not the guest user :)
            true()
        )
    )
return (
    <?css-conversion no?>,

            <title>eXist Database Administration</title>
            <link type="text/css" href="admin.css" rel="stylesheet"/>
			<link type="text/css" href="styles/prettify.css" rel="stylesheet"/>
            <link rel="shortcut icon" href="../resources/exist_icon_16x16.ico"/>
			<link rel="icon" href="../resources/exist_icon_16x16.png" type="image/png"/>

			{ admin:panel-header() }

                { admin:info-header() }

                    {
                        if($isLoggedIn) then
                            admin:menu-panel()
                        else
                        ()
                    }

                        Logged in as: {xdb:get-current-user()}

                {
                    if($isLoggedIn)then
                    (
                        admin:panel()
                    )
                    else
                    (
                        admin:display-login-form()
                    )
                }

)


Service: determine media type by output format

(: 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/";

(: fall back to default config, if needed :)
declare variable $scf:config := if(doc-available("scf_service_config.xml")) then doc("scf_service_config.xml") else doc("scf_service_config_default.xml");

(: fall back to module template path, if needed :)
declare variable $scf:config_templates_path :=
    let $config_path := $scf:config/settings/setting[@name eq 'templates_path']
    return
        if ($config_path != '')
        then $config_path
        else file:resolve-path(concat($scf:modules_path, 'SRTXML2TTML/templates/'));

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',
        'srt': 'SRT',
        'srtxml': 'SRTXML',
        'ttml': 'TTML'
    };

(: conversion between formats of different groups are not available :)
declare variable $scf:subtitle_format_group :=
    map {
        'stl': 1,
        'stlxml': 1,
        'ebu-tt': 1,
        'ebu-tt-d': 1,
        'ebu-tt-d-basic-de': 1,
        'srt': 2,
        'srtxml': 2,
        'ttml': 2
    };

(: parameters to show in custom conversion case - any parameters required for a specific module are optional here! :)
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', 'templateOPT', 'language', '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 = 'templateREQ') then
<input type="checkbox" checked="checked" disabled="disabled"/> use template <select name="template" required="required">{scf:template_files() ! <option>{.}</option>}</select> for conversion
 else () (: required :)}
        {if($params = 'templateOPT') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> use template <select name="template" disabled="disabled">{scf:template_files() ! <option>{.}</option>}</select> for conversion
 else () (: optional :)}
        {if($params = 'language') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> use <input type="text" size="10" name="language" value="en" disabled="disabled"/> as general language (override template)
 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'))}
        {scf:form_conversion('srt', 'ttml', ('templateREQ', 'language', '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'))}
        {scf:form_conversion('srt', 'srtxml', ('indent'))}
        {scf:form_conversion('srtxml', 'ttml', ('templateREQ', 'language', 'indent'))}

  };

(: returns an (ordered) list of the existing template files in the template folder :)
declare function scf:template_files() {
    let $path := if($scf:config_templates_path != '') then $scf:config_templates_path else concat($scf:modules_path, 'SRTXML2TTML/templates')
    return
        if (file:exists($path))
        then
            for $file in file:list($path)[matches(lower-case(.), '\.(xml|ttml)$')]
            order by $file
            return $file
        else ()
};

(: returns the aforementioned list :)
declare
  %rest:path("/templates")
  %output:method("json")
  %output:json("format=basic")
  function scf:templates() {
    <j:array xmlns:j="http://www.w3.org/2005/xpath-functions">
      {scf:template_files() ! <j:string>{.}</j:string>}
    </j:array>
};

(: 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)
};

declare function scf:call_srt2srtxml($status as map(*)) as map(*) {
    scf:call_python($status, 'SRT2SRTXML/srt2srtxml.py', map {})
};

(: 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{})
};

declare function scf:call_srtxml2ttml($status as map(*)) as map(*) {
    let $template := $status('option_template')
    return
        (: abort, if the required template is not specified :)
        if (empty($template))
        then scf:status_set_error_generic($status, 'The template parameter is required, but not set!')
        else
            (: abort, if the specified template does not exist :)
            if (not($template = scf:template_files()))
            then scf:status_set_error_generic($status, 'The template parameter is set, but the specified template is unavailable!')
            else
                (: make template path absolute :)
                let $status_updated := map:put($status, 'option_template', concat($scf:config_templates_path, $template))
                return
                    scf:call_xslt($status_updated, 'SRTXML2TTML/SRTXML2TTML.xslt', map {
                        'template': 'option_template',
                        'language': 'option_language'
                    })
};

(: sets a specified description as status result :)
declare function scf:status_set_error_generic($status as map(*), $description as xs:string) as map(*) {
    map:put($status, 'result',
        <error>
            <steps>
                {$status('steps') ! <step>{$scf:subtitle_format(.)}</step>}
            </steps>

            <description>{$description}</description>
            <value/>
            <module/>
            <line-number/>
            <column-number/>
        </error>
    )
};

(: sets an unsupported option error as status result :)
declare function scf:status_set_error_unsupported_option($status as map(*), $description as xs:string+) as map(*) {
    let $description_merged := concat('This conversion chain does not support the following option(s): ', string-join($description, '; '), '.')
    return scf:status_set_error_generic($status, $description_merged)
};

(: returns whether the status result represents an error :)
declare function scf:is_status_error($status as map(*)) as xs:boolean {
    $status('result') instance of element()
};

(:
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
- option_template: if present, set the used TTML template
- option_language: if present, override the template's general language
- 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("template", "{$template}")
  %rest:form-param("language", "{$language}")
  %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?,
    $template as xs:string?,
    $language 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',
        'option_template': 'to use a TTML template',
        'option_language': 'to override the general language of the template'
    }

    let $input_filename := map:keys($input)[1]
    let $input_content_raw := $input($input_filename)
    let $input_content := if ($format_source = ('stl', 'srt')) 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,
        'option_template': $template,
        'option_language': $language,
        '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 (scf:is_status_error($conversion_status) or empty($conversion_option_errors)) then $conversion_status else scf:status_set_error_unsupported_option($conversion_status, $conversion_option_errors)

    let $rest_response_filename_without_ext := replace($input_filename, '[.]([Ss][Tt][Ll]|[Ss][Rr][Tt]|[Xx][Mm][Ll])$', '')
    let $rest_response_filename := concat($rest_response_filename_without_ext, scf:filename_suffix($format_target))
    let $rest_response_success := not(scf:is_status_error($conversion))

    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='{if ($rest_response_success) then scf:media_type($format_target) else 'application/xml'}'/>
            </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')
    )
  };

declare function scf:media_type($format as xs:string) as xs:string {
    switch($format)
    case ('stl') return 'application/octet-stream'
    case ('srt') return 'text/plain'
    default return 'application/xml'
};

declare function scf:filename_suffix($format as xs:string) as xs:string {
    switch($format)
    case ('stl') return '.stl'
    case ('srt') return '.srt'
    default return concat('_', $format, '.xml')
};

(: 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
        switch ($next_format)
        (: return, if no conversion needed :)
        case () return $status
        (: return error, if no conversion available :)
        case '' return scf:status_set_error_generic($status, 'There is no conversion available for the desired source/target format combination!')
        (: next step :)
        default return
            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)
                    case "srt→srtxml" return
                        scf:call_srt2srtxml($status)
                    case "srtxml→ttml" return
                        scf:call_srtxml2ttml($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? {
    (: no conversion available, if source/target formats part of different format groups! :)
    if($scf:subtitle_format_group($format_source) ne $scf:subtitle_format_group($format_target))
    then ''
    else
        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'
        case 'srt' return
            'srtxml'
        case 'srtxml' return
            if($format_target eq 'ttml')
            then 'ttml'
            else '' (: no conversion available! :)
        case 'ttml' return
            '' (: no conversion available! :)
        default return
            (: EBU-TT based 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/";

(: fall back to default config, if needed :)
declare variable $scf:config := if(doc-available("scf_service_config.xml")) then doc("scf_service_config.xml") else doc("scf_service_config_default.xml");

(: fall back to module template path, if needed :)
declare variable $scf:config_templates_path :=
    let $config_path := $scf:config/settings/setting[@name eq 'templates_path']
    return
        if ($config_path != '')
        then $config_path
        else file:resolve-path(concat($scf:modules_path, 'SRTXML2TTML/templates/'));

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',
        'srt': 'SRT',
        'srtxml': 'SRTXML',
        'ttml': 'TTML'
    };

(: conversion between formats of different groups are not available :)
declare variable $scf:subtitle_format_group :=
    map {
        'stl': 1,
        'stlxml': 1,
        'ebu-tt': 1,
        'ebu-tt-d': 1,
        'ebu-tt-d-basic-de': 1,
        'srt': 2,
        'srtxml': 2,
        'ttml': 2
    };

(: parameters to show in custom conversion case - any parameters required for a specific module are optional here! :)
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', 'templateOPT', 'language', '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 = 'templateREQ') then
<input type="checkbox" checked="checked" disabled="disabled"/> use template <select name="template" required="required">{scf:template_files() ! <option>{.}</option>}</select> for conversion
 else () (: required :)}
        {if($params = 'templateOPT') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> use template <select name="template" disabled="disabled">{scf:template_files() ! <option>{.}</option>}</select> for conversion
 else () (: optional :)}
        {if($params = 'language') then
<input type="checkbox" onclick="this.nextElementSibling.disabled=!this.checked"/> use <input type="text" size="10" name="language" value="en" disabled="disabled"/> as general language (override template)
 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'))}
        {scf:form_conversion('srt', 'ttml', ('templateREQ', 'language', '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'))}
        {scf:form_conversion('srt', 'srtxml', ('indent'))}
        {scf:form_conversion('srtxml', 'ttml', ('templateREQ', 'language', 'indent'))}

  };

(: returns an (ordered) list of the existing template files in the template folder :)
declare function scf:template_files() {
    let $path := if($scf:config_templates_path != '') then $scf:config_templates_path else concat($scf:modules_path, 'SRTXML2TTML/templates')
    return
        if (file:exists($path))
        then
            for $file in file:list($path)[matches(lower-case(.), '\.(xml|ttml)$')]
            order by $file
            return $file
        else ()
};

(: returns the aforementioned list :)
declare
  %rest:path("/templates")
  %output:method("json")
  %output:json("format=basic")
  function scf:templates() {
    <j:array xmlns:j="http://www.w3.org/2005/xpath-functions">
      {scf:template_files() ! <j:string>{.}</j:string>}
    </j:array>
};

(: 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)
};

declare function scf:call_srt2srtxml($status as map(*)) as map(*) {
    scf:call_python($status, 'SRT2SRTXML/srt2srtxml.py', map {})
};

(: 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{})
};

declare function scf:call_srtxml2ttml($status as map(*)) as map(*) {
    let $template := $status('option_template')
    return
        (: abort, if the required template is not specified :)
        if (empty($template))
        then scf:status_set_error_generic($status, 'The template parameter is required, but not set!')
        else
            (: abort, if the specified template does not exist :)
            if (not($template = scf:template_files()))
            then scf:status_set_error_generic($status, 'The template parameter is set, but the specified template is unavailable!')
            else
                (: make template path absolute :)
                let $status_updated := map:put($status, 'option_template', concat($scf:config_templates_path, $template))
                return
                    scf:call_xslt($status_updated, 'SRTXML2TTML/SRTXML2TTML.xslt', map {
                        'template': 'option_template',
                        'language': 'option_language'
                    })
};

(: sets a specified description as status result :)
declare function scf:status_set_error_generic($status as map(*), $description as xs:string) as map(*) {
    map:put($status, 'result',
        <error>
            <steps>
                {$status('steps') ! <step>{$scf:subtitle_format(.)}</step>}
            </steps>

            <description>{$description}</description>
            <value/>
            <module/>
            <line-number/>
            <column-number/>
        </error>
    )
};

(: sets an unsupported option error as status result :)
declare function scf:status_set_error_unsupported_option($status as map(*), $description as xs:string+) as map(*) {
    let $description_merged := concat('This conversion chain does not support the following option(s): ', string-join($description, '; '), '.')
    return scf:status_set_error_generic($status, $description_merged)
};

(: returns whether the status result represents an error :)
declare function scf:is_status_error($status as map(*)) as xs:boolean {
    $status('result') instance of element()
};

(:
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
- option_template: if present, set the used TTML template
- option_language: if present, override the template's general language
- 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("template", "{$template}")
  %rest:form-param("language", "{$language}")
  %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?,
    $template as xs:string?,
    $language 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',
        'option_template': 'to use a TTML template',
        'option_language': 'to override the general language of the template'
    }

    let $input_filename := map:keys($input)[1]
    let $input_content_raw := $input($input_filename)
    let $input_content := if ($format_source = ('stl', 'srt')) 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,
        'option_template': $template,
        'option_language': $language,
        '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 (scf:is_status_error($conversion_status) or empty($conversion_option_errors)) then $conversion_status else scf:status_set_error_unsupported_option($conversion_status, $conversion_option_errors)

    let $rest_response_filename_without_ext := replace($input_filename, '[.]([Ss][Tt][Ll]|[Ss][Rr][Tt]|[Xx][Mm][Ll])$', '')
    let $rest_response_filename := concat($rest_response_filename_without_ext, scf:filename_suffix($format_target))
    let $rest_response_success := not(scf:is_status_error($conversion))

    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='{scf:media_type($conversion('result'))}'/>
            </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')
    )
  };

declare function scf:media_type($data) as xs:string {
    typeswitch($data)
    case xs:base64Binary return 'application/octet-stream'
    default return 'application/xml'
};

declare function scf:filename_suffix($format as xs:string) as xs:string {
    switch($format)
    case ('stl') return '.stl'
    case ('srt') return '.srt'
    default return concat('_', $format, '.xml')
};

(: 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
        switch ($next_format)
        (: return, if no conversion needed :)
        case () return $status
        (: return error, if no conversion available :)
        case '' return scf:status_set_error_generic($status, 'There is no conversion available for the desired source/target format combination!')
        (: next step :)
        default return
            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)
                    case "srt→srtxml" return
                        scf:call_srt2srtxml($status)
                    case "srtxml→ttml" return
                        scf:call_srtxml2ttml($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? {
    (: no conversion available, if source/target formats part of different format groups! :)
    if($scf:subtitle_format_group($format_source) ne $scf:subtitle_format_group($format_target))
    then ''
    else
        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'
        case 'srt' return
            'srtxml'
        case 'srtxml' return
            if($format_target eq 'ttml')
            then 'ttml'
            else '' (: no conversion available! :)
        case 'ttml' return
            '' (: no conversion available! :)
        default return
            (: EBU-TT based 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
                    ''
};