xquery version "3.1";

(:~
 : This module transforms TEI to XHTML according to the needs of the Ahiqar project.
 :
 : There are four types of outcomes of the transformation:
 :  * some elements are ignored ($tei2html:ignored-elements),
 :  * some elements are transformed to xhtml:div ($tei2html:block-elements),
 :  * elements that reference another element (indicated by @target, @next, and
 :    @prev) are transformed to xhtml:a,
 :  * the rest of the elements are transformed to xhtml:span.
 :
 : In any case the resulting XHTML element hold the original TEI element's name
 : in its class attribute as well as all attributes.
 : The id attribute, which has been added automatically by the TextAPI and holds
 : the TEI node number, is preserved in any case.
 :
 : Two of the Neo Aramaic testimonies have audio recordings which we want to offer:
 : * 40xns: Mlahso
 : * 49wr6: Urmia
 : For these testimonies we introduce some special processing.
 :
 : @see https://intranet.sub.uni-goettingen.de/display/prjAhiqar/Text+Styling+Specification
 :)

module namespace tei2html="http://ahiqar.uni-goettingen.de/ns/tei2html";

declare namespace tei="http://www.tei-c.org/ns/1.0";
declare namespace xhtml="http://www.w3.org/1999/xhtml";

import module namespace commons="http://ahiqar.uni-goettingen.de/ns/commons" at "commons.xqm";
import module namespace tei2html-text="http://ahiqar.uni-goettingen.de/ns/tei2html/textprocessing" at "tei2html-textprocessing.xqm";

declare variable $tei2html:block-elements := ("ab", "body", "l", "lg");
declare variable $tei2html:ignored-elements := ("lb", "milestone", "corr", "expan", "bibl");
declare variable $tei2html:audio := ("40xns", "49wr6");


declare function tei2html:transform($tei-xml-uri as xs:string, $nodes as node()*)
as node()* {
    for $node in $nodes return
        typeswitch ($node)
        case text() return
            tei2html-text:process-text($node)

        case element(tei:body) return
            if (tei2html:has-page-content($node)) then
                if (local:has-two-column-layout($node)) then
                    tei2html:make-two-column-layout($tei-xml-uri, $node)
                else
                    tei2html:transform-body($tei-xml-uri, $node)
            else
                tei2html:make-vacant-page()

        case element(tei:head) return
            element xhtml:h1 {
                attribute id {$node/@id},
                attribute class
                {
                    tei2html:make-class-attribute-values($node)
                },
                tei2html:transform($tei-xml-uri, $node/node())
            }

        case element(tei:damage) return
            if($node/@atLeast) then
                element xhtml:div {
                    attribute class {"damage atLeast"},
                    text { codepoints-to-string(8206) || "[damaged or lost part of the manuscript: at least " || string($node/@atLeast) || " " || string($node/@unit) || "]" || codepoints-to-string(8207) }
                }
            else if (not($node/text())) then
                element xhtml:span {
                    attribute class {"damage extent"},
                    text { codepoints-to-string(8206) || "[damaged or lost part of the manuscript: " || string($node/@extent) || "]" || codepoints-to-string(8207) }
                }                
            else tei2html:make-default-return($tei-xml-uri, $node)

        case element(tei:catchwords) return
            if(normalize-space($node) = normalize-space($node//tei:supplied)) then
                tei2html:transform($tei-xml-uri, $node/node())
            else tei2html:make-default-return($tei-xml-uri, $node)

        (: tei:unclear is not considered during the tokenization that takes place
        before creating the HTML documents. therefore it needs separate handling
        because otherwise no whitespace is produced. :)
        case element(tei:unclear) return
            local:make-default-span-with-whitespace-after($tei-xml-uri, $node)

        (: tei:add[@place = "margin"] is not considered during the tokenization that takes place
        before creating the HTML documents. therefore it needs separate handling
        because otherwise no whitespace is produced. :)
        case element(tei:add) return
            if ($node/@place = "margin") then
                local:make-default-span-with-whitespace-after($tei-xml-uri, $node)
            else
                tei2html:make-default-return($tei-xml-uri, $node)

        case element(tei:del) return
            local:make-default-span-with-whitespace-after($tei-xml-uri, $node)

        case element(tei:w) return
            (
                element xhtml:span {
                    attribute id {$node/@xml:id},
                    attribute class {"token"},
                    tei2html:transform($tei-xml-uri, $node/node())
                },
                if ($node/parent::tei:del) then
                    ()
                else
                    text{" "}
            )

        (: usually, in tei:choice the tei:sic comes before the tei:corr. however,
        this isn't always the case. since a white space is inserted during the
        processing of tei:corr, it might be missing in the HTML when tei:corr
        appears before tei:sic. the code snippet below ensures the correct
        sequence in the HTML. :)
        case element(tei:choice) return
            if ($node/tei:sic and $node/tei:corr) then
                element xhtml:span {
                    attribute id {$node/@id},
                    attribute class {"choice"},
                    local:make-xhtml-span($tei-xml-uri, $node/tei:sic),
                    local:make-xhtml-span-for-ignored-element($node/tei:corr)
                }
            else if ($node/tei:orig and $node/tei:supplied) then
                element xhtml:span {
                    attribute id {$node/@id},
                    attribute class {"choice"},
                    local:make-xhtml-span($tei-xml-uri, $node/tei:orig),
                    for $suppl in $node/tei:supplied return
                        local:make-xhtml-span-for-ignored-element($suppl)
                }
            (:  in come cases, the tei:orig is missing :)
            else if ($node/tei:supplied) then
                element xhtml:span {
                    attribute id {$node/@id},
                    attribute class {"choice"},
                    for $suppl in $node/tei:supplied return
                        local:make-xhtml-span-for-ignored-element($suppl)
                }
            else
                tei2html:make-default-return($tei-xml-uri, $node)

        (: tei:surplus is not considered during the tokenization that takes place
        before creating the HTML documents. therefore it needs separate handling
        because otherwise no whitespace is produced. :)
        case element(tei:surplus) return
            local:make-default-span-with-whitespace-after($tei-xml-uri, $node)

        case element(tei:gap) return
            element xhtml:span {
                attribute id {if($node/@id) then $node/@id else generate-id($node)},
                attribute class {local-name($node)},
                attribute data-extent {$node/@extent},
                attribute data-unit {$node/@unit},
                attribute data-reason {$node/@reason},
                if ($node/@unit eq "line" or $node/@unit eq "lines") then
                    element xhtml:div {
                        attribute class {"damage atLeast"},
                        text { codepoints-to-string(8206) || "[damaged or lost part of the manuscript: " || string($node/@extent) || " " || string($node/@unit) || "]" || codepoints-to-string(8207) }
                    }
                else if ($node/@unit eq "character" or $node/@unit eq "characters") then
                    if ($node/@extent eq "unknown") then
                        "––– (?)"
                    else if (contains($node/@extent, "or")) then
                        let $no := xs:int(substring-before($node/@extent, " or"))
                        for $i in 1 to $no return
                            "–"
                    else
                        for $i in 1 to xs:int($node/@extent) return "–"
                else
                    "––"
            }


        case element(tei:note) return
            if ($node/@type = "manuscript_page_beginning") then
                (
                    text{" "},
                    local:make-xhtml-span($tei-xml-uri, $node),
                    text{" "}
                )
            else
                tei2html:make-default-return($tei-xml-uri, $node)
        default return
            tei2html:make-default-return($tei-xml-uri, $node)
};

declare function local:make-default-span-with-whitespace-after($tei-xml-uri as xs:string,
$node as element()) {
    element xhtml:span {
        if ($node/@xml:id) then
            attribute id {$node/@xml:id}
        else
            attribute id {$node/@id}
        ,
        attribute class {tei2html:make-class-attribute-values($node)},
        tei2html:transform($tei-xml-uri, $node/node())
    },
    if ($node/following-sibling::*[1][self::tei:w]) then
        text{" "}
    else
        ()
};

declare function local:has-two-column-layout($node as element(tei:body))
as xs:boolean {
    exists($node//tei:cb)
};

declare function tei2html:has-page-content($body as element(tei:body))
as xs:boolean {
    $body/descendant::tei:*
    and matches($body, "[\w]") (: will automagically serialize all text() nodes to a single string :)
};

declare function tei2html:transform-body($tei-xml-uri as xs:string, $node as node()*)
as element()+ {
        element xhtml:span {
    (: required for over-spanning motifs to serve as an start node for the range selector :)
    attribute data-target {"n_start"},
    text { " " }
    },
    element xhtml:div {
        attribute id {$node/@id},
        attribute dir {
            if ($node/ancestor::tei:text[1]/@xml:lang = "eng"
            or contains($node//tei:w[1]/@xml:id, "Mlahso")
            or ($node/ancestor::tei:text[1]/@xml:lang = "sog" and $node/root()/@text-type = "transliteration")) then
                "ltr"
            else
                "rtl"},
        attribute class {
            tei2html:make-class-attribute-values($node),
            if (contains($node//tei:w[1]/@xml:id, "Mlahso")) then
                "mlahso"
            else if (contains($node//tei:w[1]/@xml:id, "Sogdian") and $node/root()/@text-type = "transliteration") then
                "sogdian_transliteration"
            else
                ""
        },
        tei2html:transform($tei-xml-uri, $node/node())
    },
    element xhtml:span {
    (: required for over-spanning motifs to serve as an end node for the range selector :)
    attribute data-target {"n_end"},
    text { " " }
    }
};

declare function tei2html:make-default-return($tei-xml-uri as xs:string,
    $node as node()*)
as node()* {
    if (tei2html:is-block-element($node)) then
        local:make-xhtml-div($tei-xml-uri, $node)

    else if (tei2html:references-another-element($node)) then
        tei2html:make-xhtml-a($tei-xml-uri, $node)

    else if (tei2html:is-ignored-element($node)) then
        if(not($node instance of element())) then () else
            local:make-xhtml-span-for-ignored-element($node)

    else
        local:make-xhtml-span($tei-xml-uri, $node)
};

declare function tei2html:is-block-element($node as node()*)
as xs:boolean {
    local-name($node) = $tei2html:block-elements
};

declare function tei2html:is-ignored-element($node as node()*)
as xs:boolean {
    local-name($node) = $tei2html:ignored-elements
    or $node instance of comment()
    or $node instance of processing-instruction()
};

declare function local:make-xhtml-span($tei-xml-uri as xs:string, $element as element())
as element(xhtml:span) {
    if(namespace-uri($element) => contains("motifs")) then
        element xhtml:span {
            attribute data-target {$element/@md5 || "_" || $element/@type},
            attribute class {"motif"}
        }
    else if($element/@type eq "original-phrase") then
        element xhtml:span {
            attribute class { "original-phrase" },
            text { " " }
        }
    else
        element xhtml:span {
            attribute id {$element/@id},
            attribute class {
                tei2html:make-class-attribute-values($element)
            },
            tei2html:transform($tei-xml-uri, $element/node())
        }
};

declare function local:make-xhtml-span-for-ignored-element($element as element())
as element(xhtml:span) {
    element xhtml:span {
            attribute class {"ignored-element", local-name($element) },
            text { " " }
        }
};

declare function local:make-xhtml-div($tei-xml-uri as xs:string, $element as element())
as element(xhtml:div) {
    element xhtml:div {
        attribute id {$element/@id},
        attribute class {
            tei2html:make-class-attribute-values($element)
        },
        if(local-name($element) eq "ab"
            and $element/tei:milestone/@unit = "minute"
            and $tei-xml-uri = $tei2html:audio) then
            element xhtml:audio {
                attribute controls {},
                (: find out in which line we are. the line number is reflected
                   in the file name of the audio file. :)
                let $element-id := $element/@id
                let $tmp-file := doc(concat($commons:tmp, $tei-xml-uri, ".me.xml"))
                let $tmp-node := $tmp-file//*[@id = $element-id]
                (: we are in a tei:ab and since abs are not nested further, we can
                   safely look at the preceding sibling. :)
                let $line-no := count($tmp-node/preceding-sibling::tei:ab) + 1
                return
                    element xhtml:source {
                        attribute src {"AHIQAR_TEXTAPI_URL/content/" || $tei-xml-uri || "_part" || $line-no || ".mp3"},
                        attribute type{"audio/mp3"},
                        element xhtml:p {
                            "Your browser doesn't support this audio file. Here is a",
                            element xhtml:a {
                                attribute href {"/content/" ||$tei-xml-uri || "_part" || $line-no || ".mp3"},
                                "link to the audio"
                            },
                            "instead."
                        }
                    }
            }
        else
            (),
        tei2html:transform($tei-xml-uri, $element/node())
    }
};

declare function tei2html:make-class-attribute-values($element as element())
as xs:string {
    let $element-name :=
        if ($element/local-name() = "lg") then
            "tei-lg"
        else
            $element/local-name()
    let $attribute-values :=
        for $attr in $element/(@* except @id) return
            if (contains($attr, "red")) then
                "red"
            else
                $attr => replace("#", "")
    let $motifs := if( $element/@md5 ) then "motif" else ()
    return
        string-join(($motifs, $element-name, $attribute-values), " ")
};

declare function tei2html:make-vacant-page()
as element(xhtml:div) {
    element xhtml:div {
        attribute class {"vacant-page"}
    }
};


declare function tei2html:references-another-element($node as node())
as xs:boolean {
    exists($node/@next)
    or exists($node/@prev)
    or exists($node/@target)
};

(: since href can not be resolved by TiDO, we rename the element to span keeping the href as data attribute. :)
declare function tei2html:make-xhtml-a($tei-xml-uri as xs:string, $element as element())
as element(xhtml:span) {
    element xhtml:span {
        attribute id {$element/@id},
        attribute class {tei2html:make-class-attribute-values($element)},
        attribute data-href {tei2html:get-href-value($element)},
        tei2html:transform($tei-xml-uri, $element/node())
    }
};


(: While references work on basis of @xml:id (e.g. 'seg_1') in TEI, we link to
 : the TEI node's ID that has been generated by commons:generate-id() instead in XHTML.
 : The reason for this is that we also use the node ID for the AnnotationAPI so
 : the data we receive at this point looks like this:
 :
 : <seg id="N.1.1.1.2.3" type="colophon">...</seg>
 :
 : Since @id has to preserved for the AnnotationAPI, we cannot simply store
 : @xml:id in @id.
 :)
declare function tei2html:get-href-value($element as element())
as xs:string? {
    let $xmlid := tei2html:get-referenced-xmlid($element)
    let $referenced-id := tei2html:find-referenced-node-id($element, $xmlid)
    return
        "#" || $referenced-id
};

declare function tei2html:find-referenced-node-id($element as element(),
    $xmlid as xs:string)
as xs:string? {
    (:
        todo check if positional predicate is needed when motifs expansion
        works as expected, e.g. no duplicate nodes (that will have same xml:id)
     :)
    ($element/root()//*[@xml:id = $xmlid])[last()]/@id
};

declare function tei2html:get-referenced-xmlid($element as element())
as xs:string? {
    if ($element/@next) then
        $element/@next => substring-after("#")
    else if ($element/@prev) then
        $element/@prev => substring-after("#")
    else if ($element/@target) then
        tokenize($element/@target, " ")[1]
        => substring-after("#")
    else
        ()
};

declare function tei2html:make-two-column-layout($tei-xml-uri as xs:string,
    $node as element(tei:body))
as element(xhtml:div) {
    element xhtml:div {
        attribute id {$node/@id},
        attribute class {
            tei2html:make-class-attribute-values($node)
        },
        let $columns := $node//tei:cb
        for $c in $columns
            let $class := "former-column"
            let $column-end :=
                if ($c/following-sibling::tei:cb) then
                    $c/following-sibling::tei:cb
                else
                    $c/following-sibling::tei:ab[last()]
            let $nodes-inbetween := $c/following-sibling::*[. << $column-end]
            return
                element xhtml:div {
                    attribute class {$class},
                    element xhtml:div {
                        attribute class {"column-marker"},
                        "[column " || $c/@n || "]"
                    },
                    tei2html:transform($tei-xml-uri, $nodes-inbetween)
                }
    }
};
