From 9255fc515b93a005949e80b3f84218551b57aa00 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Mon, 16 Feb 2026 19:55:25 -0500 Subject: [PATCH 1/2] fix: dos2unix line separators --- modules/epub.xql | 1812 +++++++++++----------- modules/frus.xql | 1114 +++++++------- modules/tei-render.xql | 3272 ++++++++++++++++++++-------------------- resources/css/epub.css | 366 ++--- 4 files changed, 3282 insertions(+), 3282 deletions(-) diff --git a/modules/epub.xql b/modules/epub.xql index b1bbe64..0d841f5 100644 --- a/modules/epub.xql +++ b/modules/epub.xql @@ -1,906 +1,906 @@ -xquery version "3.0"; - -(:~ - A module for generating an EPUB file out of a TEI document. - - Assumes FRUS-like TEI file structure. - - Requires eXist 1.5dev rev. 11085 or later. - - @version 0.1 - - @see http://en.wikipedia.org/wiki/EPUB - @see http://www.ibm.com/developerworks/edu/x-dw-x-epubtut.html - @see http://code.google.com/p/epubcheck/ - -:) - -module namespace epub = "http://history.state.gov/ns/xquery/epub"; - -import module namespace compression = "http://exist-db.org/xquery/compression"; -import module namespace render = "http://history.state.gov/ns/xquery/tei-render" at "tei-render.xql"; -import module namespace frus = "http://history.state.gov/ns/xquery/frus" at "frus.xql"; - -import module namespace console="http://exist-db.org/xquery/console"; - -declare namespace tei="http://www.tei-c.org/ns/1.0"; -declare namespace ncx ="http://www.daisy.org/z3986/2005/ncx/"; -declare namespace xhtml="http://www.w3.org/1999/xhtml"; -declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization"; - -(:~ - Main function of the EPUB module for assembling EPUB files: - Takes the elements required for an EPUB document (wrapped in elements), - and uses the compression:zip() function to returns a complete EPUB document. - - @param $title the dc:title of the EPUB - @param $creator the dc:creator of the EPUB - @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB - @param $urn the urn to use in the NCX file - @param $db-path-to-resources the db path to the required static resources (cover.jpg, stylesheet.css) - @param $filename the name of the EPUB file, sans file extension - @return serialized EPUB file - - @see http://demo.exist-db.org/exist/functions/compression/zip -:) -declare function epub:generate-epub($title, $creator, $text, $urn, $db-path-to-resources, $cover, $filename, $options) { - let $entries := - ( - console:log('starting epub:mimetype-entry (1/12)'), epub:mimetype-entry(), - console:log('starting epub:container-entry (2/12)'), epub:container-entry(), - console:log('starting epub:content-opf-entry (3/12)'), epub:content-opf-entry($title, $creator, $urn, $text), - console:log('starting epub:cover-xhtml-entry (4/12)'), epub:cover-xhtml-entry($title), - console:log('starting epub:title-xhtml-entry (5/12)'), epub:title-xhtml-entry(substring-before(util:document-name($text), '.xml')), - console:log('starting epub:about-xhtml-entry (6/12)'), epub:about-xhtml-entry(), - console:log('starting epub:table-of-contents-xhtml-entry (7/12)'), epub:table-of-contents-xhtml-entry($title, $text, true(), $options), - console:log('starting epub:body-xhtml-entries (8/12) - may take a long time!'), epub:body-xhtml-entries($text, $options), - console:log('starting epub:stylesheet-entry (9/12)'), epub:stylesheet-entry($db-path-to-resources), - console:log('starting epub:toc-ncx-entry (10/12)'), epub:toc-ncx-entry($urn, $title, $text), - console:log('starting epub:cover-entry (11/12)'), epub:cover-entry($cover), - console:log('starting epub:graphic-entries (12/12)'), epub:graphic-entries($text) - ) - return - ( - (: - console:log( - { - $entries ! - ( - if (./@type='uri') then - ( - ./string() || ': ' || - util:binary-doc-available(./string()) - ) - else () - ) - } - ) - , - console:log({$entries ! {./@name, ./@type, if (./@type='uri') then ./string() else ()}}) - , - :) - compression:zip( $entries, true() ) - ) -}; - -declare variable $epub:cache-collection := '/db/apps/release/epub-cache'; - -declare function epub:save-frus-epub-to-disk($path-to-tei-document as xs:string, $option as xs:string*, $file-system-output-dir as xs:string) { - let $vol-id := substring-after(substring-before($path-to-tei-document, '.xml'), 'volumes/') - let $images-collection := concat($epub:cache-collection, '/', $vol-id, '/images') - let $item := doc($path-to-tei-document) - let $titles := $item//tei:titleStmt/tei:title - let $title := normalize-space(concat($titles[@type eq 'volume'], ' (', string-join(($titles[@type eq 'series'], $titles[@type eq 'sub-series'], $titles[@type eq 'volume-number'])[. ne ''], ', '), ')')) - let $creator := 'Office of the Historian, Shared Knowledge Services, Bureau of Administration, United States Department of State' - let $text := $item//tei:text - let $urn := concat($vol-id, '-', current-dateTime()) - let $db-path-to-resources := '/db/apps/release/resources' - let $cover-uri := - if (util:binary-doc-available(concat($images-collection, '/', $vol-id, '.jpg'))) then - concat($images-collection, '/', $vol-id, '.jpg') - else - let $href := concat('https://static.history.state.gov/frus/', $vol-id, '/covers/', $vol-id, '.jpg') - let $request := - let $response := hc:send-request($request) - return - if ($response/@status eq '200') then - let $check-collection := - if (xmldb:collection-available($images-collection)) then - () - else - ( - xmldb:create-collection($epub:cache-collection, $vol-id), - xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), 'images') - ) - let $request := - let $response := hc:send-request($request) - let $response-body := $response[2] - let $store := xmldb:store($images-collection, 'cover.jpg', $response-body, 'image/jpeg') - return - concat($images-collection, '/cover.jpg') - else - ( - concat($db-path-to-resources, '/images/epub-cover.jpg') - , - console:log('cover image not found; using default cover image') - ) - return - let $epub-dir := concat($file-system-output-dir, 'epub') - let $mobi-dir := concat($file-system-output-dir, 'mobi-bound') - let $filename := concat($vol-id, '.epub') - let $epub-zip := epub:generate-epub($title, $creator, $text, $urn, $db-path-to-resources, $cover-uri, $filename, $option) - return - file:serialize-binary( - $epub-zip, - {concat($file-system-output-dir, if ($option = 'mobi') then 'mobi-bound/' else 'epub/', $filename)} - ) -}; - -(:~ - Helper function, returns the mimetype entry. - Note that the EPUB specification requires that the mimetype file be uncompressed. - We can ensure the mimetype file is uncompressed by passing compression:zip() an entry element - with a method attribute of "store". - - @return the mimetype entry -:) -declare function epub:mimetype-entry() { - application/epub+zip -}; - -(:~ - Helper function, returns the META-INF/container.xml entry. - - @return the META-INF/container.xml entry -:) -declare function epub:container-entry() { - let $container := - - - - - - return - {$container} -}; - -declare variable $epub:frus-div-xmlids-to-suppress { ('toc', 'pressrelease', 'summary', 'subseriesvols') }; - -declare function epub:frus-toc-divs($text as element(tei:text)) { - let $front := $text/tei:front/tei:div[not(@xml:id = $epub:frus-div-xmlids-to-suppress)] - let $body := $text/tei:body//tei:div - let $back := $text/tei:back//tei:div - return - ($front, $body, $back) -}; - -declare function epub:frus-divs($text as element(tei:text)) { - $text//tei:div[@xml:id and not(@xml:id = $epub:frus-div-xmlids-to-suppress)] -}; - -(:~ - Helper function, returns the OEBPS/content.opf entry. - - @param $title the dc:title of the EPUB - @param $creator the dc:creator of the EPUB - @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB - @return the OEBPS/content.opf entry -:) -declare function epub:content-opf-entry($title, $creator, $urn, $text) { - let $content-opf := - - - {$title} - {$creator} - {$urn} - en-US - - - - - - - - - { - (: get all divs :) - for $div in epub:frus-divs($text) - return - - } - - - { - for $image in $text//tei:graphic[@url][not(ancestor::tei:titlePage)] - return - - , - if ($text//tei:cell[@role='brace']) then - for $brace-png in ('brace-open.png', 'brace-close.png') - return - - else () - } - - - - - - - { - (: get just divs for TOC :) - for $div in epub:frus-divs($text) - return - - } - - - - - - - - { - (: first text div :) - let $first-text-div := $text/tei:body//tei:div[tei:div/@xml:id and not(tei:div/tei:div/@xml:id)][1] - let $id := $first-text-div/@xml:id - let $title := $first-text-div/tei:head - return - - } - { - (: index div :) - if ($text/id('index')) then - - else - () - } - - - return - {$content-opf} -}; - -(:~ - Helper function, contains the basic XHTML shell used by all XHTML files in the EPUB package. - - @param $title the page's title - @param $body the body content - @return the serialized XHTML element -:) -declare function epub:assemble-xhtml($title, $body) { - let $xhtml := - - - {$title} - - - - - {$body} - - - return - epub:serialize($xhtml) -}; - -declare function epub:serialize($xhtml) { - let $serialization-parameters := - - no - - return - normalize-space(serialize($xhtml, $serialization-parameters)) -}; - -(:~ - Helper function, creates the OEBPS/cover.html file. - - @param $title the page's title - @return the entry for the OEBPS/cover.html file -:) -declare function epub:cover-xhtml-entry($title) { - let $body := -
- {$title} -
- let $cover-xhtml := epub:assemble-xhtml($title, $body) - return - {$cover-xhtml} -}; - -(:~ - Helper function, creates the OEBPS/title.html file. - - @param $volume the volume's ID - @return the entry for the OEBPS/cover.html file -:) -declare function epub:title-xhtml-entry($volume-id) { - let $volume := collection('/db/apps/frus/volumes')/tei:TEI[@xml:id eq $volume-id] - let $editor-roles-to-display := ('primary', 'general') - let $editors := $volume//tei:editor[@role = $editor-roles-to-display and . ne ''] - let $published-year := $volume//tei:date[@type eq "publication-date"]/substring(., 1, 4) - let $body := -
-

{concat(frus:volume-title($volume-id, 'series'), ', ', frus:volume-title($volume-id, 'sub-series'))}

-
-

{frus:volume-title($volume-id, 'volume-number')}

-

{frus:volume-title($volume-id, 'volume')}

- { - if ($editors) then -
- { - for $role in frus:editor-roles()[. = $editors/@role] - let $editors-in-role := $editors[@role = $role] - let $label := - if (count($editors-in-role) gt 1) then - frus:editor-role-to-label($role, 'plural') - else - frus:editor-role-to-label($role, 'singular') - return - ( -
{$label}:
- , - for $ed in $editors-in-role - return -
{$ed/string()}
- ) - } -
- else () - } -
-

- U.S. Department of State
- Washington
- {$published-year} -

-

- Office of the Historian
- Shared Knowledge Services
- Bureau of Administration
- U.S. Department of State -

-
-

- This ebook was generated on {format-date(current-date(), "[MNn] [D1], [Y0001]")}.
- Please visit the Office of the Historian ebooks web page to access updates. -

-
- let $title := 'Title page' - let $title-xhtml := epub:assemble-xhtml($title, $body) - return - {$title-xhtml} -}; - -(:~ - Helper function, creates the OEBPS/about.html file. - - @param $title the page's title - @return the entry for the OEBPS/about.html file -:) -declare function epub:about-xhtml-entry() { - let $body := epub:process-xhtml(doc('/db/apps/release/resources/boilerplate/frus-about.xml')) - let $title := 'About the Ebook Edition' - let $cover-xhtml := epub:assemble-xhtml($title, $body) - return - {$cover-xhtml} -}; - -(: a typeswitch routine to process dynamic content such as dates, e.g. returns "May 2012" :) -declare function epub:process-xhtml($node) { - typeswitch($node) - case text() return $node - case element(xhtml:span) return - if ($node/@class eq 'ho:generate-month-year') then - format-date(current-date(), "[MNn] [Y0001]") - else - element { node-name($node) } { epub:process-xhtml-recurse($node) } - case element() return element { node-name($node) } { $node/@*, epub:process-xhtml-recurse($node) } - default return epub:process-xhtml-recurse($node) -}; - -(: helper for epub:process-xhtml :) -declare function epub:process-xhtml-recurse($node) { - for $child in $node/node() - return - epub:process-xhtml($child) -}; - -(:~ - Helper function, creates the OEBPS/table-of-contents.html file. - - @param $title the page's title - @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB - @return the entry for the OEBPS/table-of-contents.html file -:) -declare function epub:table-of-contents-xhtml-entry($title, $text, $suppress-documents, $options) { - let $body := -
-

Contents

- { - if ($options = 'mobi') then - epub:frus-toc-to-li($text, $suppress-documents, $options) - else -
    { - (: Just get top level divs :) - epub:frus-toc-to-li($text, $suppress-documents, $options) - }
- } -
- let $table-of-contents-xhtml := epub:assemble-xhtml($title, $body) - return - {$table-of-contents-xhtml} -}; - -(:~ - Helper function, creates the XHTML files for the body of the EPUB. - - @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB - @return the serialized XHTML page, wrapped in an entry element -:) -declare function epub:body-xhtml-entries($text, $options) { - let $divs := epub:frus-divs($text) - let $div-count := count($divs) - (: TODO only log at discrete points, like every 5% or 10% :) - for $div at $n in $divs - let $log := - if ($div/@type=('document', 'document-pending')) then () else console:log(concat('generating body-xhtml-entry for ', $div/@xml:id)) - (:console:log(concat('generating body-xhtml-entry for ', $div/@xml:id)):) - let $title := frus:head-sans-note($div) - let $body := epub:process-div($div, $title, $options) - let $body-xhtml:= epub:assemble-xhtml($title, $body) - (: previously we cached/stashed $body-xhtml and then included it in the zip via - - as an attempt to "work around likely zip- and memory-related crashes" - but that no longer works: :) - (: - let $vol-id := substring-before(util:document-name($div), '.xml') - let $xhtml-cache := concat($epub:cache-collection, '/', $vol-id, '/xhtml', if ($options = 'mobi') then '-mobi' else '-epub') - let $check-collection := - if (xmldb:collection-available($xhtml-cache)) then - () - else - ( - xmldb:create-collection($epub:cache-collection, $vol-id), - xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), concat('/xhtml', if ($options = 'mobi') then '-mobi' else '-epub')) - ) - let $store := xmldb:store($xhtml-cache, concat($div/@xml:id, '.txt'), $body-xhtml) - :) - return - {$body-xhtml} -}; - -declare function epub:process-div($div as element(tei:div), $title, $options) { - let $parameters := - - - - { - if ($options = 'mobi') then - else () - } - - return - (: just render documents, sections :) - if ($div/@type = ('document', 'document-pending', 'section', 'section-pending')) then - render:render($div, $parameters) - else - let $child-documents-to-show := $div/tei:div[@type=('document', 'document-pending')] - let $has-inner-sections := $div/tei:div[not(@type = ('document', 'document-pending'))] - let $notes := $div/tei:head/tei:note - return -
-

{render:head($div/tei:head, $parameters)}

- { - (: display any footnotes hung on the chapter heading, e.g., frus1952-54v08/comp3 :) - if ($notes) then - for $note at $incr in $notes - let $incr := if ($note/preceding::tei:note[@n = '0']) then $incr - 1 else $incr - return -

- - - {($note/@n/string(), '*')[1]} - -  {render:recurse($note, $parameters)} -

- else () - , - (: for example of chapter div without no documents but a child paragraph, see frus1952-54v08/comp3 - for an example of a subchapter div with a table, see frus1945Malta/ch8subch44 :) - let $child-nodes := $div/node() - let $first-head := index-of($child-nodes, $div/tei:head[1]) - let $first-div := if ($div/tei:div) then index-of($child-nodes, $div/tei:div[1]) else () - let $nodes-to-render := - if ($first-div) then - subsequence($child-nodes, $first-head + 1, $first-div - $first-head - 1) - else - subsequence($child-nodes, $first-head + 1) - let $footnotes := - for $note at $incr in $nodes-to-render//tei:note - let $incr := if ($div/tei:note[@n = '0']) then (count($notes) + $incr - 1) else (count($notes) + $incr) - return -

- - - {($note/@n/string(), '*')[1]} - -  {render:recurse($note, $parameters)} -

- return - ( - render:main($nodes-to-render, $parameters) - , - $footnotes - ) - , - for $document in $child-documents-to-show - let $docnumber := frus:document-number($document) - let $docid := frus:document-id($document) - let $doctitle := frus:document-head-sans-number($document) - let $docsource := frus:source-note($document)/string() - let $docdateline := string-join(render:main(frus:dateline($document), ), '') - let $docsummary := $document//tei:note[@type='summary']/string() - return - ( -
, -

{ - if ($document/@type eq "document-pending") then - () - else if (not(starts-with($document/tei:head, concat($document/@n, '.')))) then - concat('[', $docnumber, ']') - else - concat($docnumber, '. '), - $doctitle - }

, - , - if (exists($docsummary)) then

{$docsummary}

else (), -

{$docsource}

- ) - , - if ($has-inner-sections) then - ( -
- , -
-

Contents

-
    { - epub:frus-toc-to-li($div/tei:div[not(@type=('document', 'document-pending'))], true(), $options) - }
-
- ) - else - () - } -
-}; - -(:~ - Helper function, creates the CSS entry for the EPUB. - - @param $db-path-to-css the db path to the required static resources (cover.jpg, stylesheet.css) - @return the CSS entry -:) -declare function epub:stylesheet-entry($db-path-to-css) { - {util:binary-doc(concat($db-path-to-css, '/css/epub.css'))} -}; - - -(:~ - Helper function, creates the OEBPS/toc.ncx file. - - @param $urn the EPUB's urn - @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB - @return the NCX element's entry -:) -declare function epub:toc-ncx-entry($urn, $title, $text) { - let $toc-ncx := - - - - - - - - - {$title} - - - - - Cover - - - - - - Title - - - - - - About the Electronic Edition - - - - - - Table of Contents - - - - { - epub:frus-toc-to-ncx($text, 5) - } - - - return - {$toc-ncx} -}; - -declare function epub:frus-toc-to-ncx($node, $navpoint-start as xs:integer) { - typeswitch($node) - case element(tei:div) return epub:frus-div-to-ncx($node, $navpoint-start) - default return epub:recurse-ncx($node, $navpoint-start) -}; - -declare function epub:recurse-ncx($node, $navpoint-start) { - for $child in $node/node() - return epub:frus-toc-to-ncx($child, $navpoint-start) -}; - -declare function epub:frus-div-to-ncx($div as element(tei:div), $navpoint-start as xs:integer) { - let $id := $div/@xml:id - let $index := count($div/preceding::tei:div[@xml:id and not(@xml:id = $epub:frus-div-xmlids-to-suppress)]) + count($div/ancestor::tei:div[@xml:id and not(@xml:id = $epub:frus-div-xmlids-to-suppress)]) - return - (: just pass through non-@xml:id divs :) - if (not($id)) then - epub:recurse-ncx($div, $navpoint-start) - (: supress original print TOC, since we generate an ePub-specific one :) - else if ($id = $epub:frus-div-xmlids-to-suppress) then - () - (: show all divs with @xml:id :) - else - - - { - (: show a bracketed document number for volumes that don't use document numbers :) - if ($div/@type = 'document' and not(starts-with($div/tei:head, concat($div/@n, '.')) or starts-with($div/tei:head, concat('No. ', $div/@n)))) then concat('[', $div/@n, ']') else (), - normalize-space(frus:head-sans-note($div))} - - - { epub:recurse-ncx($div, $navpoint-start) } - -}; - -declare function epub:frus-toc-to-li($node, $suppress-documents as xs:boolean, $options) { - typeswitch($node) - case element(tei:div) return epub:frus-div-to-li($node, $suppress-documents, $options) - default return epub:recurse-li($node, $suppress-documents, $options) -}; - -declare function epub:recurse-li($node, $suppress-documents, $options) { - for $child in $node/node() - return epub:frus-toc-to-li($child, $suppress-documents, $options) -}; - -declare function epub:frus-div-to-li($div as element(tei:div), $suppress-documents as xs:boolean, $options) { - let $id := $div/@xml:id - return - (: just pass through non-@xml:id divs :) - if (not($id)) then - epub:recurse-li($div, $suppress-documents, $options) - (: supress original print TOC, since we generate an ePub-specific one :) - else if ($id = $epub:frus-div-xmlids-to-suppress) then - () - else if ($div/@type = ('document', 'document-pending', 'introduction')) then - if ($suppress-documents) then () - else - if ($options = 'mobi') then -
- { - normalize-space(frus:head-sans-note($div)) - } -
- else -
  • - { - normalize-space(frus:head-sans-note($div)) - } -
  • - (: show all divs with @xml:id :) - else - let $child-docs := $div/tei:div[@type eq ("document", "document-pending")] - let $labels := if ($div/ancestor::tei:back) then ("Appendix", "Appendixes") else ("Document", "Documents") - let $label := if (count($child-docs) eq 1) then $labels[1] else $labels[2] - return - if ($options = 'mobi') then -
    - { - normalize-space(frus:head-sans-note($div)) - } - { - if ($child-docs) then - if ($suppress-documents) then - if (count($child-docs) gt 1) then - concat(' (', $label, ' ', $child-docs[@n][1]/@n, '–', $child-docs[@n][last()]/@n, ')') - else - concat(' (', $label, ' ', $child-docs[@n][1]/@n, ')') - else () - else - () - , - if ($div//tei:div[@xml:id and not(@type = ('document', 'introduction'))] or not($suppress-documents)) then - epub:recurse-li($div, $suppress-documents, $options) - else () - } -
    - else -
  • - { - if (ends-with($div/@type, "-pending")) then - attribute style { "font-style: italic;" } - else - () - , - { - normalize-space(frus:head-sans-note($div)) - } - , - if ($child-docs) then - if ($suppress-documents) then - if (count($child-docs) gt 1) then - concat(' (', $label, ' ', $child-docs[@n][1]/@n, '–', $child-docs[@n][last()]/@n, ')') - else - concat(' (', $label, ' ', $child-docs[@n][1]/@n, ')') - else () - else - () - } - { - if ($div//tei:div[@xml:id and not(@type = ('document', 'document-pending', 'introduction'))] or not($suppress-documents)) then -
      { epub:recurse-li($div, $suppress-documents, $options) }
    - else - () - } -
  • -}; - -(:~ - Helper function, creates the cover image entry for the EPUB. - - @param $cover-uri the db path to the file to be used for the cover - @return the cover entry -:) -declare function epub:cover-entry($cover-uri) { - {util:binary-doc($cover-uri)} -}; - -declare function epub:graphic-entries($text) { - let $vol-id := substring-before(util:document-name($text), '.xml') - let $image-uris := epub:cache-all-images($text) - for $image-uri in $image-uris - let $filename := substring-after($image-uri, 'images/') - return - {util:binary-doc($image-uri)} -}; - -declare function epub:cache-image($href, $target-collection, $filename) { - let $request := - let $response := hc:send-request($request) - return - if ($response/@status eq '200') then - let $request := - let $response := hc:send-request($request) - let $response-body := $response[2] - let $store := xmldb:store($target-collection, xmldb:encode($filename), $response-body, 'image/png') - return - concat($target-collection, '/', $filename) - else - error(xs:QName('epub-error'), concat('Unable to fetch image ', $href, ' for volume ', $filename, ' from S3')) -}; - -declare function epub:cache-all-images($text) { - let $vol-id := substring-before(util:document-name($text), '.xml') - let $graphics := - ( - for $graphic-basename in distinct-values($text/(tei:body | tei:back)//tei:graphic/@url) - return - - , - let $has-braces := $text//tei:cell[@role = 'brace'] - return - if ($has-braces) then - for $brace-image in ('brace-open', 'brace-close') - return - - else - () - ) - return - if (exists($graphics)) then - let $ensure-collections-exist := - ( - xmldb:create-collection($epub:cache-collection, 'images'), - xmldb:create-collection($epub:cache-collection, $vol-id), - xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), 'images') - ) - for $graphic in $graphics - let $graphic-binary-uri := - if (util:binary-doc-available(concat($epub:cache-collection, '/', $graphic/@cache-path, '/', $graphic/@file))) then - concat($graphic/@cache-path, '/', $graphic/@file) - else - let $uri := concat('https://static.history.state.gov/', $graphic/@s3-path, '/', encode-for-uri($graphic/@file)) - return - epub:cache-image($uri, concat($epub:cache-collection, '/', $graphic/@cache-path), $graphic/@file) - let $path-to-cached-image := concat($epub:cache-collection, '/', $graphic-binary-uri) - return - $path-to-cached-image - else () -}; - -declare function epub:clear-cache($vol-id) { - xmldb:remove(concat($epub:cache-collection, '/', $vol-id)) -}; - -declare function epub:clear-image-cache($vol-id) { - xmldb:remove(concat($epub:cache-collection, '/', $vol-id, '/images')) -}; - -declare function epub:clear-ncx-cache($vol-id) { - xmldb:remove(concat($epub:cache-collection, '/', $vol-id, '/ncx')) -}; - -declare function epub:clear-xhtml-cache($vol-id) { - xmldb:remove(concat($epub:cache-collection, '/', $vol-id, '/xhtml')) -}; - -declare function epub:cache-ncx-collection($vol-id) { - concat($epub:cache-collection, '/', $vol-id, '/ncx') -}; - -declare function epub:cache-ncx($vol-id) { - let $ncx := epub:toc-ncx-entry($vol-id, $vol-id, frus:volume($vol-id)) - let $check-collection := - if (xmldb:collection-available(epub:cache-ncx-collection($vol-id))) then - () - else - ( - xmldb:create-collection($epub:cache-collection, $vol-id), - xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), 'ncx') - ) - let $store := xmldb:store(epub:cache-ncx-collection($vol-id), concat($vol-id, '.ncx'), $ncx) - return () -}; - -declare function epub:get-ncx($vol-id) { - let $vol := frus:volume($vol-id) - let $cache-collection := epub:cache-ncx-collection($vol-id) - let $cache-filename := concat($vol-id, '.ncx') - let $cache-doc := concat($cache-collection, '/', $cache-filename) - let $exists-cache := doc-available($cache-doc) - return - if ($exists-cache and - xmldb:last-modified(util:collection-name($vol), util:document-name($vol)) - le - xmldb:last-modified($cache-collection, $cache-filename) - ) then - doc($cache-doc) - else ( - epub:cache-ncx($vol-id), - doc($cache-doc) - ) -}; - -declare function epub:get-current-navPoint-playOrder($ncx, $id) { - xs:integer($ncx//ncx:navPoint[ncx:content/@src eq $id]/@playOrder) -}; - -declare function epub:get-previous-navPoint($ncx, $current-playOrder-value as xs:integer) { - $ncx//ncx:navPoint[xs:integer(@playOrder) eq $current-playOrder-value - 1] -}; - -declare function epub:get-next-navPoint($ncx, $current-playOrder-value as xs:integer) { - $ncx//ncx:navPoint[xs:integer(@playOrder) eq $current-playOrder-value + 1] -}; +xquery version "3.0"; + +(:~ + A module for generating an EPUB file out of a TEI document. + + Assumes FRUS-like TEI file structure. + + Requires eXist 1.5dev rev. 11085 or later. + + @version 0.1 + + @see http://en.wikipedia.org/wiki/EPUB + @see http://www.ibm.com/developerworks/edu/x-dw-x-epubtut.html + @see http://code.google.com/p/epubcheck/ + +:) + +module namespace epub = "http://history.state.gov/ns/xquery/epub"; + +import module namespace compression = "http://exist-db.org/xquery/compression"; +import module namespace render = "http://history.state.gov/ns/xquery/tei-render" at "tei-render.xql"; +import module namespace frus = "http://history.state.gov/ns/xquery/frus" at "frus.xql"; + +import module namespace console="http://exist-db.org/xquery/console"; + +declare namespace tei="http://www.tei-c.org/ns/1.0"; +declare namespace ncx ="http://www.daisy.org/z3986/2005/ncx/"; +declare namespace xhtml="http://www.w3.org/1999/xhtml"; +declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization"; + +(:~ + Main function of the EPUB module for assembling EPUB files: + Takes the elements required for an EPUB document (wrapped in elements), + and uses the compression:zip() function to returns a complete EPUB document. + + @param $title the dc:title of the EPUB + @param $creator the dc:creator of the EPUB + @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB + @param $urn the urn to use in the NCX file + @param $db-path-to-resources the db path to the required static resources (cover.jpg, stylesheet.css) + @param $filename the name of the EPUB file, sans file extension + @return serialized EPUB file + + @see http://demo.exist-db.org/exist/functions/compression/zip +:) +declare function epub:generate-epub($title, $creator, $text, $urn, $db-path-to-resources, $cover, $filename, $options) { + let $entries := + ( + console:log('starting epub:mimetype-entry (1/12)'), epub:mimetype-entry(), + console:log('starting epub:container-entry (2/12)'), epub:container-entry(), + console:log('starting epub:content-opf-entry (3/12)'), epub:content-opf-entry($title, $creator, $urn, $text), + console:log('starting epub:cover-xhtml-entry (4/12)'), epub:cover-xhtml-entry($title), + console:log('starting epub:title-xhtml-entry (5/12)'), epub:title-xhtml-entry(substring-before(util:document-name($text), '.xml')), + console:log('starting epub:about-xhtml-entry (6/12)'), epub:about-xhtml-entry(), + console:log('starting epub:table-of-contents-xhtml-entry (7/12)'), epub:table-of-contents-xhtml-entry($title, $text, true(), $options), + console:log('starting epub:body-xhtml-entries (8/12) - may take a long time!'), epub:body-xhtml-entries($text, $options), + console:log('starting epub:stylesheet-entry (9/12)'), epub:stylesheet-entry($db-path-to-resources), + console:log('starting epub:toc-ncx-entry (10/12)'), epub:toc-ncx-entry($urn, $title, $text), + console:log('starting epub:cover-entry (11/12)'), epub:cover-entry($cover), + console:log('starting epub:graphic-entries (12/12)'), epub:graphic-entries($text) + ) + return + ( + (: + console:log( + { + $entries ! + ( + if (./@type='uri') then + ( + ./string() || ': ' || + util:binary-doc-available(./string()) + ) + else () + ) + } + ) + , + console:log({$entries ! {./@name, ./@type, if (./@type='uri') then ./string() else ()}}) + , + :) + compression:zip( $entries, true() ) + ) +}; + +declare variable $epub:cache-collection := '/db/apps/release/epub-cache'; + +declare function epub:save-frus-epub-to-disk($path-to-tei-document as xs:string, $option as xs:string*, $file-system-output-dir as xs:string) { + let $vol-id := substring-after(substring-before($path-to-tei-document, '.xml'), 'volumes/') + let $images-collection := concat($epub:cache-collection, '/', $vol-id, '/images') + let $item := doc($path-to-tei-document) + let $titles := $item//tei:titleStmt/tei:title + let $title := normalize-space(concat($titles[@type eq 'volume'], ' (', string-join(($titles[@type eq 'series'], $titles[@type eq 'sub-series'], $titles[@type eq 'volume-number'])[. ne ''], ', '), ')')) + let $creator := 'Office of the Historian, Shared Knowledge Services, Bureau of Administration, United States Department of State' + let $text := $item//tei:text + let $urn := concat($vol-id, '-', current-dateTime()) + let $db-path-to-resources := '/db/apps/release/resources' + let $cover-uri := + if (util:binary-doc-available(concat($images-collection, '/', $vol-id, '.jpg'))) then + concat($images-collection, '/', $vol-id, '.jpg') + else + let $href := concat('https://static.history.state.gov/frus/', $vol-id, '/covers/', $vol-id, '.jpg') + let $request := + let $response := hc:send-request($request) + return + if ($response/@status eq '200') then + let $check-collection := + if (xmldb:collection-available($images-collection)) then + () + else + ( + xmldb:create-collection($epub:cache-collection, $vol-id), + xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), 'images') + ) + let $request := + let $response := hc:send-request($request) + let $response-body := $response[2] + let $store := xmldb:store($images-collection, 'cover.jpg', $response-body, 'image/jpeg') + return + concat($images-collection, '/cover.jpg') + else + ( + concat($db-path-to-resources, '/images/epub-cover.jpg') + , + console:log('cover image not found; using default cover image') + ) + return + let $epub-dir := concat($file-system-output-dir, 'epub') + let $mobi-dir := concat($file-system-output-dir, 'mobi-bound') + let $filename := concat($vol-id, '.epub') + let $epub-zip := epub:generate-epub($title, $creator, $text, $urn, $db-path-to-resources, $cover-uri, $filename, $option) + return + file:serialize-binary( + $epub-zip, + {concat($file-system-output-dir, if ($option = 'mobi') then 'mobi-bound/' else 'epub/', $filename)} + ) +}; + +(:~ + Helper function, returns the mimetype entry. + Note that the EPUB specification requires that the mimetype file be uncompressed. + We can ensure the mimetype file is uncompressed by passing compression:zip() an entry element + with a method attribute of "store". + + @return the mimetype entry +:) +declare function epub:mimetype-entry() { + application/epub+zip +}; + +(:~ + Helper function, returns the META-INF/container.xml entry. + + @return the META-INF/container.xml entry +:) +declare function epub:container-entry() { + let $container := + + + + + + return + {$container} +}; + +declare variable $epub:frus-div-xmlids-to-suppress { ('toc', 'pressrelease', 'summary', 'subseriesvols') }; + +declare function epub:frus-toc-divs($text as element(tei:text)) { + let $front := $text/tei:front/tei:div[not(@xml:id = $epub:frus-div-xmlids-to-suppress)] + let $body := $text/tei:body//tei:div + let $back := $text/tei:back//tei:div + return + ($front, $body, $back) +}; + +declare function epub:frus-divs($text as element(tei:text)) { + $text//tei:div[@xml:id and not(@xml:id = $epub:frus-div-xmlids-to-suppress)] +}; + +(:~ + Helper function, returns the OEBPS/content.opf entry. + + @param $title the dc:title of the EPUB + @param $creator the dc:creator of the EPUB + @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB + @return the OEBPS/content.opf entry +:) +declare function epub:content-opf-entry($title, $creator, $urn, $text) { + let $content-opf := + + + {$title} + {$creator} + {$urn} + en-US + + + + + + + + + { + (: get all divs :) + for $div in epub:frus-divs($text) + return + + } + + + { + for $image in $text//tei:graphic[@url][not(ancestor::tei:titlePage)] + return + + , + if ($text//tei:cell[@role='brace']) then + for $brace-png in ('brace-open.png', 'brace-close.png') + return + + else () + } + + + + + + + { + (: get just divs for TOC :) + for $div in epub:frus-divs($text) + return + + } + + + + + + + + { + (: first text div :) + let $first-text-div := $text/tei:body//tei:div[tei:div/@xml:id and not(tei:div/tei:div/@xml:id)][1] + let $id := $first-text-div/@xml:id + let $title := $first-text-div/tei:head + return + + } + { + (: index div :) + if ($text/id('index')) then + + else + () + } + + + return + {$content-opf} +}; + +(:~ + Helper function, contains the basic XHTML shell used by all XHTML files in the EPUB package. + + @param $title the page's title + @param $body the body content + @return the serialized XHTML element +:) +declare function epub:assemble-xhtml($title, $body) { + let $xhtml := + + + {$title} + + + + + {$body} + + + return + epub:serialize($xhtml) +}; + +declare function epub:serialize($xhtml) { + let $serialization-parameters := + + no + + return + normalize-space(serialize($xhtml, $serialization-parameters)) +}; + +(:~ + Helper function, creates the OEBPS/cover.html file. + + @param $title the page's title + @return the entry for the OEBPS/cover.html file +:) +declare function epub:cover-xhtml-entry($title) { + let $body := +
    + {$title} +
    + let $cover-xhtml := epub:assemble-xhtml($title, $body) + return + {$cover-xhtml} +}; + +(:~ + Helper function, creates the OEBPS/title.html file. + + @param $volume the volume's ID + @return the entry for the OEBPS/cover.html file +:) +declare function epub:title-xhtml-entry($volume-id) { + let $volume := collection('/db/apps/frus/volumes')/tei:TEI[@xml:id eq $volume-id] + let $editor-roles-to-display := ('primary', 'general') + let $editors := $volume//tei:editor[@role = $editor-roles-to-display and . ne ''] + let $published-year := $volume//tei:date[@type eq "publication-date"]/substring(., 1, 4) + let $body := +
    +

    {concat(frus:volume-title($volume-id, 'series'), ', ', frus:volume-title($volume-id, 'sub-series'))}

    +
    +

    {frus:volume-title($volume-id, 'volume-number')}

    +

    {frus:volume-title($volume-id, 'volume')}

    + { + if ($editors) then +
    + { + for $role in frus:editor-roles()[. = $editors/@role] + let $editors-in-role := $editors[@role = $role] + let $label := + if (count($editors-in-role) gt 1) then + frus:editor-role-to-label($role, 'plural') + else + frus:editor-role-to-label($role, 'singular') + return + ( +
    {$label}:
    + , + for $ed in $editors-in-role + return +
    {$ed/string()}
    + ) + } +
    + else () + } +
    +

    + U.S. Department of State
    + Washington
    + {$published-year} +

    +

    + Office of the Historian
    + Shared Knowledge Services
    + Bureau of Administration
    + U.S. Department of State +

    +
    +

    + This ebook was generated on {format-date(current-date(), "[MNn] [D1], [Y0001]")}.
    + Please visit the Office of the Historian ebooks web page to access updates. +

    +
    + let $title := 'Title page' + let $title-xhtml := epub:assemble-xhtml($title, $body) + return + {$title-xhtml} +}; + +(:~ + Helper function, creates the OEBPS/about.html file. + + @param $title the page's title + @return the entry for the OEBPS/about.html file +:) +declare function epub:about-xhtml-entry() { + let $body := epub:process-xhtml(doc('/db/apps/release/resources/boilerplate/frus-about.xml')) + let $title := 'About the Ebook Edition' + let $cover-xhtml := epub:assemble-xhtml($title, $body) + return + {$cover-xhtml} +}; + +(: a typeswitch routine to process dynamic content such as dates, e.g. returns "May 2012" :) +declare function epub:process-xhtml($node) { + typeswitch($node) + case text() return $node + case element(xhtml:span) return + if ($node/@class eq 'ho:generate-month-year') then + format-date(current-date(), "[MNn] [Y0001]") + else + element { node-name($node) } { epub:process-xhtml-recurse($node) } + case element() return element { node-name($node) } { $node/@*, epub:process-xhtml-recurse($node) } + default return epub:process-xhtml-recurse($node) +}; + +(: helper for epub:process-xhtml :) +declare function epub:process-xhtml-recurse($node) { + for $child in $node/node() + return + epub:process-xhtml($child) +}; + +(:~ + Helper function, creates the OEBPS/table-of-contents.html file. + + @param $title the page's title + @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB + @return the entry for the OEBPS/table-of-contents.html file +:) +declare function epub:table-of-contents-xhtml-entry($title, $text, $suppress-documents, $options) { + let $body := +
    +

    Contents

    + { + if ($options = 'mobi') then + epub:frus-toc-to-li($text, $suppress-documents, $options) + else +
      { + (: Just get top level divs :) + epub:frus-toc-to-li($text, $suppress-documents, $options) + }
    + } +
    + let $table-of-contents-xhtml := epub:assemble-xhtml($title, $body) + return + {$table-of-contents-xhtml} +}; + +(:~ + Helper function, creates the XHTML files for the body of the EPUB. + + @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB + @return the serialized XHTML page, wrapped in an entry element +:) +declare function epub:body-xhtml-entries($text, $options) { + let $divs := epub:frus-divs($text) + let $div-count := count($divs) + (: TODO only log at discrete points, like every 5% or 10% :) + for $div at $n in $divs + let $log := + if ($div/@type=('document', 'document-pending')) then () else console:log(concat('generating body-xhtml-entry for ', $div/@xml:id)) + (:console:log(concat('generating body-xhtml-entry for ', $div/@xml:id)):) + let $title := frus:head-sans-note($div) + let $body := epub:process-div($div, $title, $options) + let $body-xhtml:= epub:assemble-xhtml($title, $body) + (: previously we cached/stashed $body-xhtml and then included it in the zip via + - as an attempt to "work around likely zip- and memory-related crashes" + but that no longer works: :) + (: + let $vol-id := substring-before(util:document-name($div), '.xml') + let $xhtml-cache := concat($epub:cache-collection, '/', $vol-id, '/xhtml', if ($options = 'mobi') then '-mobi' else '-epub') + let $check-collection := + if (xmldb:collection-available($xhtml-cache)) then + () + else + ( + xmldb:create-collection($epub:cache-collection, $vol-id), + xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), concat('/xhtml', if ($options = 'mobi') then '-mobi' else '-epub')) + ) + let $store := xmldb:store($xhtml-cache, concat($div/@xml:id, '.txt'), $body-xhtml) + :) + return + {$body-xhtml} +}; + +declare function epub:process-div($div as element(tei:div), $title, $options) { + let $parameters := + + + + { + if ($options = 'mobi') then + else () + } + + return + (: just render documents, sections :) + if ($div/@type = ('document', 'document-pending', 'section', 'section-pending')) then + render:render($div, $parameters) + else + let $child-documents-to-show := $div/tei:div[@type=('document', 'document-pending')] + let $has-inner-sections := $div/tei:div[not(@type = ('document', 'document-pending'))] + let $notes := $div/tei:head/tei:note + return +
    +

    {render:head($div/tei:head, $parameters)}

    + { + (: display any footnotes hung on the chapter heading, e.g., frus1952-54v08/comp3 :) + if ($notes) then + for $note at $incr in $notes + let $incr := if ($note/preceding::tei:note[@n = '0']) then $incr - 1 else $incr + return +

    + + + {($note/@n/string(), '*')[1]} + +  {render:recurse($note, $parameters)} +

    + else () + , + (: for example of chapter div without no documents but a child paragraph, see frus1952-54v08/comp3 + for an example of a subchapter div with a table, see frus1945Malta/ch8subch44 :) + let $child-nodes := $div/node() + let $first-head := index-of($child-nodes, $div/tei:head[1]) + let $first-div := if ($div/tei:div) then index-of($child-nodes, $div/tei:div[1]) else () + let $nodes-to-render := + if ($first-div) then + subsequence($child-nodes, $first-head + 1, $first-div - $first-head - 1) + else + subsequence($child-nodes, $first-head + 1) + let $footnotes := + for $note at $incr in $nodes-to-render//tei:note + let $incr := if ($div/tei:note[@n = '0']) then (count($notes) + $incr - 1) else (count($notes) + $incr) + return +

    + + + {($note/@n/string(), '*')[1]} + +  {render:recurse($note, $parameters)} +

    + return + ( + render:main($nodes-to-render, $parameters) + , + $footnotes + ) + , + for $document in $child-documents-to-show + let $docnumber := frus:document-number($document) + let $docid := frus:document-id($document) + let $doctitle := frus:document-head-sans-number($document) + let $docsource := frus:source-note($document)/string() + let $docdateline := string-join(render:main(frus:dateline($document), ), '') + let $docsummary := $document//tei:note[@type='summary']/string() + return + ( +
    , +

    { + if ($document/@type eq "document-pending") then + () + else if (not(starts-with($document/tei:head, concat($document/@n, '.')))) then + concat('[', $docnumber, ']') + else + concat($docnumber, '. '), + $doctitle + }

    , + , + if (exists($docsummary)) then

    {$docsummary}

    else (), +

    {$docsource}

    + ) + , + if ($has-inner-sections) then + ( +
    + , +
    +

    Contents

    +
      { + epub:frus-toc-to-li($div/tei:div[not(@type=('document', 'document-pending'))], true(), $options) + }
    +
    + ) + else + () + } +
    +}; + +(:~ + Helper function, creates the CSS entry for the EPUB. + + @param $db-path-to-css the db path to the required static resources (cover.jpg, stylesheet.css) + @return the CSS entry +:) +declare function epub:stylesheet-entry($db-path-to-css) { + {util:binary-doc(concat($db-path-to-css, '/css/epub.css'))} +}; + + +(:~ + Helper function, creates the OEBPS/toc.ncx file. + + @param $urn the EPUB's urn + @param $text the tei:text element for the file, which contains the divs to be processed into the EPUB + @return the NCX element's entry +:) +declare function epub:toc-ncx-entry($urn, $title, $text) { + let $toc-ncx := + + + + + + + + + {$title} + + + + + Cover + + + + + + Title + + + + + + About the Electronic Edition + + + + + + Table of Contents + + + + { + epub:frus-toc-to-ncx($text, 5) + } + + + return + {$toc-ncx} +}; + +declare function epub:frus-toc-to-ncx($node, $navpoint-start as xs:integer) { + typeswitch($node) + case element(tei:div) return epub:frus-div-to-ncx($node, $navpoint-start) + default return epub:recurse-ncx($node, $navpoint-start) +}; + +declare function epub:recurse-ncx($node, $navpoint-start) { + for $child in $node/node() + return epub:frus-toc-to-ncx($child, $navpoint-start) +}; + +declare function epub:frus-div-to-ncx($div as element(tei:div), $navpoint-start as xs:integer) { + let $id := $div/@xml:id + let $index := count($div/preceding::tei:div[@xml:id and not(@xml:id = $epub:frus-div-xmlids-to-suppress)]) + count($div/ancestor::tei:div[@xml:id and not(@xml:id = $epub:frus-div-xmlids-to-suppress)]) + return + (: just pass through non-@xml:id divs :) + if (not($id)) then + epub:recurse-ncx($div, $navpoint-start) + (: supress original print TOC, since we generate an ePub-specific one :) + else if ($id = $epub:frus-div-xmlids-to-suppress) then + () + (: show all divs with @xml:id :) + else + + + { + (: show a bracketed document number for volumes that don't use document numbers :) + if ($div/@type = 'document' and not(starts-with($div/tei:head, concat($div/@n, '.')) or starts-with($div/tei:head, concat('No. ', $div/@n)))) then concat('[', $div/@n, ']') else (), + normalize-space(frus:head-sans-note($div))} + + + { epub:recurse-ncx($div, $navpoint-start) } + +}; + +declare function epub:frus-toc-to-li($node, $suppress-documents as xs:boolean, $options) { + typeswitch($node) + case element(tei:div) return epub:frus-div-to-li($node, $suppress-documents, $options) + default return epub:recurse-li($node, $suppress-documents, $options) +}; + +declare function epub:recurse-li($node, $suppress-documents, $options) { + for $child in $node/node() + return epub:frus-toc-to-li($child, $suppress-documents, $options) +}; + +declare function epub:frus-div-to-li($div as element(tei:div), $suppress-documents as xs:boolean, $options) { + let $id := $div/@xml:id + return + (: just pass through non-@xml:id divs :) + if (not($id)) then + epub:recurse-li($div, $suppress-documents, $options) + (: supress original print TOC, since we generate an ePub-specific one :) + else if ($id = $epub:frus-div-xmlids-to-suppress) then + () + else if ($div/@type = ('document', 'document-pending', 'introduction')) then + if ($suppress-documents) then () + else + if ($options = 'mobi') then +
    + { + normalize-space(frus:head-sans-note($div)) + } +
    + else +
  • + { + normalize-space(frus:head-sans-note($div)) + } +
  • + (: show all divs with @xml:id :) + else + let $child-docs := $div/tei:div[@type eq ("document", "document-pending")] + let $labels := if ($div/ancestor::tei:back) then ("Appendix", "Appendixes") else ("Document", "Documents") + let $label := if (count($child-docs) eq 1) then $labels[1] else $labels[2] + return + if ($options = 'mobi') then +
    + { + normalize-space(frus:head-sans-note($div)) + } + { + if ($child-docs) then + if ($suppress-documents) then + if (count($child-docs) gt 1) then + concat(' (', $label, ' ', $child-docs[@n][1]/@n, '–', $child-docs[@n][last()]/@n, ')') + else + concat(' (', $label, ' ', $child-docs[@n][1]/@n, ')') + else () + else + () + , + if ($div//tei:div[@xml:id and not(@type = ('document', 'introduction'))] or not($suppress-documents)) then + epub:recurse-li($div, $suppress-documents, $options) + else () + } +
    + else +
  • + { + if (ends-with($div/@type, "-pending")) then + attribute style { "font-style: italic;" } + else + () + , + { + normalize-space(frus:head-sans-note($div)) + } + , + if ($child-docs) then + if ($suppress-documents) then + if (count($child-docs) gt 1) then + concat(' (', $label, ' ', $child-docs[@n][1]/@n, '–', $child-docs[@n][last()]/@n, ')') + else + concat(' (', $label, ' ', $child-docs[@n][1]/@n, ')') + else () + else + () + } + { + if ($div//tei:div[@xml:id and not(@type = ('document', 'document-pending', 'introduction'))] or not($suppress-documents)) then +
      { epub:recurse-li($div, $suppress-documents, $options) }
    + else + () + } +
  • +}; + +(:~ + Helper function, creates the cover image entry for the EPUB. + + @param $cover-uri the db path to the file to be used for the cover + @return the cover entry +:) +declare function epub:cover-entry($cover-uri) { + {util:binary-doc($cover-uri)} +}; + +declare function epub:graphic-entries($text) { + let $vol-id := substring-before(util:document-name($text), '.xml') + let $image-uris := epub:cache-all-images($text) + for $image-uri in $image-uris + let $filename := substring-after($image-uri, 'images/') + return + {util:binary-doc($image-uri)} +}; + +declare function epub:cache-image($href, $target-collection, $filename) { + let $request := + let $response := hc:send-request($request) + return + if ($response/@status eq '200') then + let $request := + let $response := hc:send-request($request) + let $response-body := $response[2] + let $store := xmldb:store($target-collection, xmldb:encode($filename), $response-body, 'image/png') + return + concat($target-collection, '/', $filename) + else + error(xs:QName('epub-error'), concat('Unable to fetch image ', $href, ' for volume ', $filename, ' from S3')) +}; + +declare function epub:cache-all-images($text) { + let $vol-id := substring-before(util:document-name($text), '.xml') + let $graphics := + ( + for $graphic-basename in distinct-values($text/(tei:body | tei:back)//tei:graphic/@url) + return + + , + let $has-braces := $text//tei:cell[@role = 'brace'] + return + if ($has-braces) then + for $brace-image in ('brace-open', 'brace-close') + return + + else + () + ) + return + if (exists($graphics)) then + let $ensure-collections-exist := + ( + xmldb:create-collection($epub:cache-collection, 'images'), + xmldb:create-collection($epub:cache-collection, $vol-id), + xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), 'images') + ) + for $graphic in $graphics + let $graphic-binary-uri := + if (util:binary-doc-available(concat($epub:cache-collection, '/', $graphic/@cache-path, '/', $graphic/@file))) then + concat($graphic/@cache-path, '/', $graphic/@file) + else + let $uri := concat('https://static.history.state.gov/', $graphic/@s3-path, '/', encode-for-uri($graphic/@file)) + return + epub:cache-image($uri, concat($epub:cache-collection, '/', $graphic/@cache-path), $graphic/@file) + let $path-to-cached-image := concat($epub:cache-collection, '/', $graphic-binary-uri) + return + $path-to-cached-image + else () +}; + +declare function epub:clear-cache($vol-id) { + xmldb:remove(concat($epub:cache-collection, '/', $vol-id)) +}; + +declare function epub:clear-image-cache($vol-id) { + xmldb:remove(concat($epub:cache-collection, '/', $vol-id, '/images')) +}; + +declare function epub:clear-ncx-cache($vol-id) { + xmldb:remove(concat($epub:cache-collection, '/', $vol-id, '/ncx')) +}; + +declare function epub:clear-xhtml-cache($vol-id) { + xmldb:remove(concat($epub:cache-collection, '/', $vol-id, '/xhtml')) +}; + +declare function epub:cache-ncx-collection($vol-id) { + concat($epub:cache-collection, '/', $vol-id, '/ncx') +}; + +declare function epub:cache-ncx($vol-id) { + let $ncx := epub:toc-ncx-entry($vol-id, $vol-id, frus:volume($vol-id)) + let $check-collection := + if (xmldb:collection-available(epub:cache-ncx-collection($vol-id))) then + () + else + ( + xmldb:create-collection($epub:cache-collection, $vol-id), + xmldb:create-collection(concat($epub:cache-collection, '/', $vol-id), 'ncx') + ) + let $store := xmldb:store(epub:cache-ncx-collection($vol-id), concat($vol-id, '.ncx'), $ncx) + return () +}; + +declare function epub:get-ncx($vol-id) { + let $vol := frus:volume($vol-id) + let $cache-collection := epub:cache-ncx-collection($vol-id) + let $cache-filename := concat($vol-id, '.ncx') + let $cache-doc := concat($cache-collection, '/', $cache-filename) + let $exists-cache := doc-available($cache-doc) + return + if ($exists-cache and + xmldb:last-modified(util:collection-name($vol), util:document-name($vol)) + le + xmldb:last-modified($cache-collection, $cache-filename) + ) then + doc($cache-doc) + else ( + epub:cache-ncx($vol-id), + doc($cache-doc) + ) +}; + +declare function epub:get-current-navPoint-playOrder($ncx, $id) { + xs:integer($ncx//ncx:navPoint[ncx:content/@src eq $id]/@playOrder) +}; + +declare function epub:get-previous-navPoint($ncx, $current-playOrder-value as xs:integer) { + $ncx//ncx:navPoint[xs:integer(@playOrder) eq $current-playOrder-value - 1] +}; + +declare function epub:get-next-navPoint($ncx, $current-playOrder-value as xs:integer) { + $ncx//ncx:navPoint[xs:integer(@playOrder) eq $current-playOrder-value + 1] +}; diff --git a/modules/frus.xql b/modules/frus.xql index c424830..3e2c900 100644 --- a/modules/frus.xql +++ b/modules/frus.xql @@ -1,557 +1,557 @@ -xquery version "3.1"; - -(: - : frus.xq XQuery Module - to facilitate writing xqueries that deal with FRUS TEI files - : and make it easier to write new queries. - : - : To include in sandbox or stored queries, include this in the query prolog: - : import module namespace frus = "http://history.state.gov/xquery/frus" at "xmldb:exist:///db/history/modules/frus.xq"; - :) - -module namespace frus = "http://history.state.gov/ns/xquery/frus"; - -import module namespace functx = "http://www.functx.com"; -import module namespace hsg-config = "http://history.state.gov/ns/site/hsg/config" at "/db/apps/hsg-shell/modules/config.xqm"; -import module namespace render = "http://history.state.gov/ns/xquery/tei-render" at "tei-render.xql"; -import module namespace util= "http://exist-db.org/xquery/util"; - -declare namespace tei = "http://www.tei-c.org/ns/1.0"; - -declare variable $frus:bucket := $hsg-config:S3_BUCKET; - -(: TODO: - : - continue refactoring historicaldocuments.xq to remove repetitive code that can be centralized here - : - add functions that will facilitate building more sophisticated queries - : - add comments/xqdocs for each function - :) - -(: eXist db path to FRUS XML files:) -declare variable $frus:VOLUMES-PATH := '/db/apps/frus/volumes/'; - (: TODO we should be able to use $paho:MAINCOLLECTION but not sure why this import isn't working :) - -(: absolute path to section of website containing FRUS :) -declare variable $frus:FRUS-URL := '/historicaldocuments/'; - -(: static file location values: local, hsg, or s3 :) -declare variable $frus:STATIC-FILE-LOCATION := 's3'; - -(: local eXist db path to FRUS PDF files :) -declare variable $frus:PDF-DB-PATH := '/db/fruspageimages/'; - -(: local eXist db path to FRUS page images :) -declare variable $frus:PAGEIMAGES-DB-PATH := '/db/fruspageimages/'; - -(: URL path to PDFs :) -declare variable $frus:PDF-URL-PATH := - if ($frus:STATIC-FILE-LOCATION eq 'local') then '/historicaldocuments/' - else if ($frus:STATIC-FILE-LOCATION eq 's3') then '//static.history.state.gov/frus/' - else (: hsg :) '//history.state.gov/historicaldocuments/' -; - -(: URL path to PDFs :) -declare variable $frus:PAGEIMAGES-URL-PATH := - if ($frus:STATIC-FILE-LOCATION eq 'local') then '/historicaldocuments/' - else if ($frus:STATIC-FILE-LOCATION eq 's3') then '//static.history.state.gov/frus/' - else (: hsg :) '//history.state.gov/historicaldocuments/' -; - -(: gets document node of a volume from its unique volume id :) -(: TODO: make changes necessary to be able to remove * :) -declare function frus:volume($volumeids as xs:string+) as document-node()* { - for $volumeid in $volumeids - return doc(concat($frus:VOLUMES-PATH, $volumeid, '.xml')) -}; - -(: TODO: make changes necessary to be able to remove * :) -declare function frus:volumes() as element(tei:TEI)* { - collection($frus:VOLUMES-PATH)/tei:TEI -}; - -declare function frus:volumes($partialvolumeid as xs:string) as element(tei:TEI)* { - for $volume in frus:volumes()//tei:idno[@type='frus'][contains(., $partialvolumeid)] - let $volumeid := frus:volumeid($volume) - order by $volumeid - return $volume -}; - -declare function frus:volumeid($id as node()) { - substring-before(util:document-name($id), '.xml') -}; - -declare function frus:volumeids() { - for $volume in frus:volumes() - let $volumeid := frus:volumeid($volume) - order by $volumeid - return $volumeid -}; - -declare function frus:volumeids($partialvolumeid as xs:string) { - for $volume in frus:volumes()//tei:idno[@type='frus'][contains(., $partialvolumeid)] - let $volumeid := frus:volumeid($volume) - order by $volumeid - return $volumeid -}; - -declare function frus:trace($ids as node()+) { - for $id in $ids - let $volumeid := frus:volumeid($id) - let $xmlid := $id/ancestor::*[@xml:id][1]/@xml:id/string() - return concat($volumeid, '/', $xmlid) -}; - -declare function frus:trace-count($hits as node()+) { - let $trace := frus:trace($hits) - for $hit in distinct-values($trace) - return concat($hit, ': ', count($hit[. = $trace])) -}; - -declare function frus:exists-volume($volumeid as xs:string) as xs:boolean { - exists(collection('/db/apps/frus/volumes')/tei:TEI[@xml:id eq $volumeid]) -}; - -declare function frus:exists-fulltext-volume-in-db($volumeid as xs:string) as xs:boolean { - exists(frus:volume($volumeid)//tei:body/tei:div) -}; - -declare function frus:fulltext-volumes-in-db() as xs:string+ { - for $volume in collection($frus:VOLUMES-PATH)/tei:TEI[.//tei:body/tei:div] - return - frus:volumeid($volume) -}; - -declare function frus:volume-title($volumeids as xs:string+, $type as xs:string) as text()* { - for $volumeid in $volumeids - order by $volumeid - return - collection('/db/apps/frus/volumes')/tei:TEI[@xml:id eq $volumeid]//tei:title[@type eq $type]/text() -}; - -declare function frus:volume-title($volumeids as xs:string+) as text()* { - frus:volume-title($volumeids, 'complete') -}; - -declare function frus:exists-id($volumeid as xs:string, $id as xs:string) as xs:boolean { - exists(frus:volume($volumeid)/id($id)) -}; - -declare function frus:id($volumeid as xs:string, $id as xs:string) as element() { - frus:volume($volumeid)/id($id) -}; - -declare function frus:type-of-id($volumeid as xs:string, $id as xs:string) as xs:string { - string(frus:volume($volumeid)/id($id)/@type) -}; - -declare function frus:pdf-collection($volumeid as xs:string) as xs:string { - concat($frus:PDF-DB-PATH, $volumeid, '/pdf/') -}; - -declare function frus:pdf-filename($volumeid as xs:string) as xs:string { - concat($volumeid, '.pdf') -}; - -declare function frus:exists-pdf($volumeid as xs:string) as xs:boolean { - if ($frus:STATIC-FILE-LOCATION = 'local') then - util:binary-doc-available(frus:pdf-db-path($volumeid)) - else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) - let $collection := '/db/history/data/s3-resources/' || $frus:bucket || '/frus/' - let $pdf-filename := concat($volumeid, '.pdf') - return - exists(collection($collection)//filename[. = $pdf-filename]) -}; - -declare function frus:volumes-with-ebooks() { - for $hit in collection('/db/history/data/s3-resources/' || $frus:bucket || '/frus/')//filename[ends-with(., '.epub')] - return - substring-before($hit, '.epub') -}; - -declare function frus:volumes-with-single-pdfs() { - for $hit in collection('/db/history/data/s3-resources/' || $frus:bucket || '/frus/')//filename[ends-with(.,'.pdf')][starts-with(., 'frus')] - return - substring-before($hit, '.pdf') -}; - -declare function frus:volumes-with-ebooks-or-single-pdfs() { - distinct-values((frus:volumes-with-ebooks(), frus:volumes-with-single-pdfs())) -}; - -declare function frus:exists-ebook($volumeid as xs:string) as xs:boolean { - if ($frus:STATIC-FILE-LOCATION = ('local', 'hsg')) then - exists(doc('/db/history/data/historicaldocuments/ebooks.xml')//ebook[@id eq $volumeid]) - else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) - let $collection := concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook') - let $epub-filename := concat($volumeid, '.epub') - return - exists(collection($collection)//filename[. = $epub-filename]) -}; - -declare function frus:epub-url($volumeid as xs:string) as xs:string { - concat($frus:PDF-URL-PATH, $volumeid, '/ebook/', $volumeid, '.epub') -}; - -declare function frus:mobi-url($volumeid as xs:string) as xs:string { - concat($frus:PDF-URL-PATH, $volumeid, '/ebook/', $volumeid, '.mobi') -}; - -declare function frus:epub-size($volumeid as xs:string) { - let $epub := (doc(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook/resources.xml'))//filename[ends-with(., '.epub')]/parent::resource)[1] - let $size := $epub//size - return - frus:bytes-to-readable($size) -}; - -declare function frus:mobi-size($volumeid as xs:string) { - let $mobi := (doc(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook/resources.xml'))//filename[ends-with(., '.mobi')]/parent::resource)[1] - let $size := $mobi//size - return - frus:bytes-to-readable($size) -}; - -(: returns the size of a file in kb or mb :) -declare function frus:file-size($collection as xs:string, $filename as xs:string) as xs:string { - let $sizeinbytes := xmldb:size($collection, $filename) - return frus:bytes-to-readable($sizeinbytes) -}; - -declare function frus:bytes-to-readable($bytes as xs:integer) { - if ($bytes gt 1000000) then - concat((round($bytes div 10000) div 100), 'mb') - else if ($bytes gt 1000) then - concat(round($bytes div 1000), 'kb') - else () -}; - -declare function frus:ebook-last-updated($volumeid as xs:string) { - let $epub-filename := concat($volumeid, '.epub') - let $epub := (doc(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook/resources.xml'))//filename[ends-with(., '.epub')]/parent::resource)[1] - return - $epub/last-modified/string() -}; - -declare function frus:exists-doc-pdf($volumeid, $document) as xs:boolean { - if ($frus:STATIC-FILE-LOCATION = ('local', 'hsg')) then - util:binary-doc-available(concat(frus:pdf-collection($volumeid), '/', $document, '.pdf')) - else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) - collection(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/pdf/'))//filename = concat($document, '.pdf') -}; - -declare function frus:pdf-db-path($volumeid as xs:string) as xs:string { - concat(frus:pdf-collection($volumeid), frus:pdf-filename($volumeid)) -}; - -declare function frus:pdf-url($volumeid as xs:string) as xs:string { - concat($frus:PDF-URL-PATH, $volumeid, if ($frus:STATIC-FILE-LOCATION eq 's3') then () else '/media', '/pdf/', $volumeid, '.pdf') -}; - -declare function frus:page-image-url($id as element(tei:pb)) as xs:string { - let $volumeid := frus:volumeid($id) - let $facs := $id/@facs/string() - return - concat($frus:PAGEIMAGES-URL-PATH, $volumeid, if ($frus:STATIC-FILE-LOCATION eq 's3') then () else '/media', '/medium/', $facs, '.png') -}; - -declare function frus:pdf-size($volumeid as xs:string) { - if ($frus:STATIC-FILE-LOCATION = 'local') then - frus:file-size(frus:pdf-collection($volumeid), frus:pdf-filename($volumeid)) - else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) - frus:bytes-to-readable(collection('/db/history/data/s3-resources/' || $frus:bucket || '/frus/')//filename[. eq frus:pdf-filename($volumeid)]/following-sibling::size) -}; - -declare function frus:isbn($volumeid as xs:string) as text()* { - frus:volume($volumeid)//tei:idno[@type = ('isbn-10','isbn-13')]/text() -}; - -declare function frus:isbn-url($isbn as xs:string) as xs:string { - concat('http://www.worldcat.org/search?q=isbn%3A', $isbn) -}; - -declare function frus:documents() as element(tei:div)* { - collection($frus:VOLUMES-PATH)//tei:div[@type='document' and @xml:id] -}; - -declare function frus:documents($volumeids as xs:string+) as element(tei:div)+ { - let $volumes := frus:volume($volumeids) - return $volumes//tei:div[@type='document' and @xml:id] -}; - -declare function frus:documents($volumeid as xs:string, $id as xs:string) as element(tei:div)* { - frus:id($volumeid, $id)//tei:div[@type='document' and @xml:id] -}; - -declare function frus:document-number($volumeid as xs:string, $id as xs:string) as xs:string { - frus:id($volumeid, $id)/@n/string() -}; - -declare function frus:document-number($id as element(tei:div)) as xs:string { - $id/@n/string() -}; - -declare function frus:document-id($id as element(tei:div)) as xs:string { - $id/@xml:id/string() -}; - -declare function frus:get-citation($id as element()) as xs:string { - let $volume := frus:volumeid($id) - let $volumetitle := frus:volume-title($volume, 'complete') - let $subvolumeinfo := - if ($id/self::tei:div) then - if ($id/@type eq 'document') then - concat ('Document ', $id/@n) - else if ($id/@type = ('section', 'chapter', 'compilation', 'subchapter')) then - $id/tei:head/text() - else () - else if (name($id) eq 'pb' and $id/@n) then - let $ancestor := ($id/following-sibling::element()[1][self::tei:div], $id/ancestor::tei:div[1])[1] - let $ancestorcitation := - if ($ancestor/@type eq 'document') then - concat('Document ', $ancestor/@n, ', ') - else if ($ancestor/@type = ('section', 'chapter', 'compilation', 'subchapter')) then - concat(frus:head-sans-note($ancestor), ', ') - else () - return - concat($ancestorcitation, 'Page ', $id/@n) - else () - let $citation := string-join(($volumetitle, $subvolumeinfo), ', ') - return $citation -}; - -(: TODO: make changes necessary to be able to remove * :) -declare function frus:source-note($id as element(tei:div)) as element(tei:note)* { - ($id//tei:note[@type='source'])[1] -}; - -declare function frus:lb-to-whitespace($node) { - for $n in $node/node() - return - typeswitch ($n) - case element(tei:lb) return ' ' - case element() return element { name($n) } { frus:lb-to-whitespace($n) } - default return - $n -}; - -declare function frus:head-sans-note($id as element(tei:div)) as xs:string { - let $sans-note := functx:remove-elements-deep($id/tei:head[1], "note") - let $lb-to-whitespace := frus:lb-to-whitespace($sans-note) - return - normalize-space(string-join($lb-to-whitespace)) -}; - -(: TODO: make changes necessary to be able to remove * :) -declare function frus:dateline($id as element(tei:div)) as element(tei:dateline)* { - let $dateline := ($id//tei:dateline)[1] (: frus1952-54/d414 has two datelines - the second belonging to an attachment :) - return - functx:remove-elements-deep($dateline, 'note') -}; - -declare function frus:document-summary($id as element(tei:div)) as element(tei:note)* { - if ($id//tei:note/@type='summary') then - $id//tei:note[@type='summary'] - else () -}; - -declare function frus:document-head-sans-number($id as element(tei:div)) as xs:string { - let $head := frus:head-sans-note($id) - let $newtitle := - if (matches($head, "^\d+a?\.")) then - replace($head, "^\d+a?\.", "") - else if (matches($head, "^No. \d+a?")) then - replace($head, "^No. \d+a?", "") - else - $head - return - $newtitle -}; - -declare function frus:sections($volumeid as xs:string) { - frus:volume($volumeid)//tei:div[@type = ('section', 'compilation', 'chapter')] -}; - -declare function frus:url($volumeid as xs:string) as xs:string { - concat($frus:FRUS-URL, $volumeid) -}; - -declare function frus:url($volumeid as xs:string, $id as xs:string) as xs:string { - concat($frus:FRUS-URL, $volumeid, '/', $id) -}; - -declare function frus:toc($volume as xs:string) { - frus:toc($volume, ()) -}; - -(: whole volume TOC :) -declare function frus:toc($volume as xs:string, $id-to-highlight as xs:string?) { - let $vol := frus:volume($volume) - return -
    -
      {frus:toc-passthru($vol, $id-to-highlight)}
    -
    -}; - -(: volume of just the section id and deeper :) -declare function frus:toc-inner($volume as xs:string, $id as xs:string?) { - let $inner-section := frus:id($volume, $id) - return -
    -
      {frus:toc-passthru($inner-section, $id)}
    -
    -}; - -declare function frus:toc-passthru($node as item()*, $id-to-highlight as xs:string?) { - (: if we're given a div, dig deeper :) - if ($node/self::tei:div) then - $node/tei:div ! frus:toc-div(., $id-to-highlight) - (: if we're not given a div - presumably a tei:TEI or tei:text element - find the topmost divs :) - else - $node//tei:div[not(ancestor::tei:div)] ! frus:toc-div(., $id-to-highlight) -}; - -(: handles divs for TOCs :) -declare function frus:toc-div($node as element(tei:div), $id-to-highlight as xs:string?) { - let $sections-to-suppress := ('toc') - return - (: we only show certain divs :) - if (not($node/@xml:id = $sections-to-suppress) and not($node/@type = 'document')) then -
  • - { - let $href := attribute href { concat('/historicaldocuments/', frus:volumeid($node), '/', $node/@xml:id) } - let $highlight := if ($node/@xml:id = $id-to-highlight) then attribute class {'highlight'} else () - return - - { - $href, - $highlight, - frus:toc-head($node/tei:head[1]) - } - - , - - if ($node/tei:div/@type = 'document') then - concat( - ' (Document', - let $child-docs := $node/tei:div[@type = 'document'] - let $first := $child-docs[1]/@n - let $last := $child-docs[last()]/@n - return - if ($first = $last) then - concat(' ', $first) - else - concat('s ', $first, '-', $last) - , ')' - ) - else - () - , - if ($node/tei:div/@type = 'document' and not($node/tei:div/@type = 'document')) then () - else -
      - { - frus:toc-passthru($node, $id-to-highlight) - } -
    - } -
  • - else - () -}; - -(: handles heads for TOCs :) -declare function frus:toc-head($node as element(tei:head)) { - let $head-sans-note := if ($node//tei:note) then functx:remove-elements-deep($node, 'note') else $node - return - render:recurse($head-sans-note, ()) -}; - -declare function frus:toc-link($node, $id-to-highlight) { - let $id := $node/@xml:id - let $vol-id := frus:volumeid($node) - return - concat($vol-id, '/', $id) -}; - -declare function frus:toc-render($volume as xs:string, $id as xs:string, $highlightcurrent, $view) { - (: TODO: make nested lists valid XHTML :) - for $sect in frus:sections($volume)[not(parent::tei:div[@type='compilation'])] - let $type := $sect/@type - let $sectid := $sect/@xml:id - let $secttitle := $sect/tei:head[1] - let $secttitletext := data(functx:remove-elements-deep($secttitle, "note")) - let $currentsection := - (: is $id a document in a chapter/compilation? :) - if (frus:id($volume, $id)/self::tei:div[@type='document']) then - frus:id($volume, $id)/parent::tei:div/tei:head[1] - (: or is $id a chapter/compilation itself? :) - else - frus:id($volume, $id)/tei:head[1] - let $highlightstatus := - if ($highlightcurrent) then - if ($secttitle eq $currentsection) then true() - else () - else () - let $viewstatus := - if ($view) then - (concat("/", $view)) - else () - return - if ($type = ('section', 'compilation')) then - (: 1st tier: for sections and compilations :) -
  • { frus:toc-link($secttitletext, $volume, $sectid, $viewstatus, $highlightstatus) } - { - if ($sect/tei:div[@type='chapter']) then - (: 2nd tier: for chapters contained inside sections and compilations :) -
      - { - for $chapter in $sect/tei:div[@type='chapter'] - let $chapterid := $chapter/@xml:id/string() - let $chaptertitle := $chapter/tei:head[1] - let $chaptertitletext := data(functx:remove-elements-deep($chaptertitle, "note")) - let $currentchapter := - (: is $id a document in a chapter/compilation? :) - if (frus:id($volume, $id)/self::tei:div[@type='document']) then - frus:id($volume, $id)/parent::tei:div/tei:head[1] - (: or is $id a chapter/compilation itself? :) - else - frus:id($volume, $id)/tei:head[1] - let $highlightstatus := - if ($highlightcurrent) then - if ($chaptertitle eq $currentchapter) then true() - else () - else () - return -
    • { frus:toc-link($chaptertitletext, $volume, $chapterid, $viewstatus, $highlightstatus) }
    • - } -
    - else () - } -
  • - else if ($sect/parent::tei:div[@type='compilation']) then - (: if chapter is part of a compilation, don't show it :) - () - else - (: if chapter isn't encapsulated in a compilation, show it :) -
  • { frus:toc-link($secttitletext, $volume, $sectid, $viewstatus, $highlightstatus) }
  • -}; - -declare function frus:toc-link($secttitletext, $volume, $sectid, $viewstatus, $highlightstatus) { - if ($highlightstatus) then - (: highlight :) - ( attribute class {"sidebarhighlighted"}, $secttitletext ) - else - {$secttitletext} -}; - -declare function frus:editor-role-to-label($role as xs:string, $form as xs:string) as xs:string { - let $item := doc('/db/apps/frus/shared/frus-production.xml')/id("frus-editor-roles")//tei:category[@xml:id = $role] - let $label := - if ($form = 'plural') then - $item/tei:catDesc/tei:term[@type eq "plural"] - else (: if ($form = 'plural') then :) - $item/tei:catDesc/tei:term[@type eq "singular"] - return - $label/string() -}; - -declare function frus:editor-roles() as xs:string+ { - doc('/db/apps/frus/shared/frus-production.xml')/id("frus-editor-roles")//tei:category/@xml:id -}; +xquery version "3.1"; + +(: + : frus.xq XQuery Module - to facilitate writing xqueries that deal with FRUS TEI files + : and make it easier to write new queries. + : + : To include in sandbox or stored queries, include this in the query prolog: + : import module namespace frus = "http://history.state.gov/xquery/frus" at "xmldb:exist:///db/history/modules/frus.xq"; + :) + +module namespace frus = "http://history.state.gov/ns/xquery/frus"; + +import module namespace functx = "http://www.functx.com"; +import module namespace hsg-config = "http://history.state.gov/ns/site/hsg/config" at "/db/apps/hsg-shell/modules/config.xqm"; +import module namespace render = "http://history.state.gov/ns/xquery/tei-render" at "tei-render.xql"; +import module namespace util= "http://exist-db.org/xquery/util"; + +declare namespace tei = "http://www.tei-c.org/ns/1.0"; + +declare variable $frus:bucket := $hsg-config:S3_BUCKET; + +(: TODO: + : - continue refactoring historicaldocuments.xq to remove repetitive code that can be centralized here + : - add functions that will facilitate building more sophisticated queries + : - add comments/xqdocs for each function + :) + +(: eXist db path to FRUS XML files:) +declare variable $frus:VOLUMES-PATH := '/db/apps/frus/volumes/'; + (: TODO we should be able to use $paho:MAINCOLLECTION but not sure why this import isn't working :) + +(: absolute path to section of website containing FRUS :) +declare variable $frus:FRUS-URL := '/historicaldocuments/'; + +(: static file location values: local, hsg, or s3 :) +declare variable $frus:STATIC-FILE-LOCATION := 's3'; + +(: local eXist db path to FRUS PDF files :) +declare variable $frus:PDF-DB-PATH := '/db/fruspageimages/'; + +(: local eXist db path to FRUS page images :) +declare variable $frus:PAGEIMAGES-DB-PATH := '/db/fruspageimages/'; + +(: URL path to PDFs :) +declare variable $frus:PDF-URL-PATH := + if ($frus:STATIC-FILE-LOCATION eq 'local') then '/historicaldocuments/' + else if ($frus:STATIC-FILE-LOCATION eq 's3') then '//static.history.state.gov/frus/' + else (: hsg :) '//history.state.gov/historicaldocuments/' +; + +(: URL path to PDFs :) +declare variable $frus:PAGEIMAGES-URL-PATH := + if ($frus:STATIC-FILE-LOCATION eq 'local') then '/historicaldocuments/' + else if ($frus:STATIC-FILE-LOCATION eq 's3') then '//static.history.state.gov/frus/' + else (: hsg :) '//history.state.gov/historicaldocuments/' +; + +(: gets document node of a volume from its unique volume id :) +(: TODO: make changes necessary to be able to remove * :) +declare function frus:volume($volumeids as xs:string+) as document-node()* { + for $volumeid in $volumeids + return doc(concat($frus:VOLUMES-PATH, $volumeid, '.xml')) +}; + +(: TODO: make changes necessary to be able to remove * :) +declare function frus:volumes() as element(tei:TEI)* { + collection($frus:VOLUMES-PATH)/tei:TEI +}; + +declare function frus:volumes($partialvolumeid as xs:string) as element(tei:TEI)* { + for $volume in frus:volumes()//tei:idno[@type='frus'][contains(., $partialvolumeid)] + let $volumeid := frus:volumeid($volume) + order by $volumeid + return $volume +}; + +declare function frus:volumeid($id as node()) { + substring-before(util:document-name($id), '.xml') +}; + +declare function frus:volumeids() { + for $volume in frus:volumes() + let $volumeid := frus:volumeid($volume) + order by $volumeid + return $volumeid +}; + +declare function frus:volumeids($partialvolumeid as xs:string) { + for $volume in frus:volumes()//tei:idno[@type='frus'][contains(., $partialvolumeid)] + let $volumeid := frus:volumeid($volume) + order by $volumeid + return $volumeid +}; + +declare function frus:trace($ids as node()+) { + for $id in $ids + let $volumeid := frus:volumeid($id) + let $xmlid := $id/ancestor::*[@xml:id][1]/@xml:id/string() + return concat($volumeid, '/', $xmlid) +}; + +declare function frus:trace-count($hits as node()+) { + let $trace := frus:trace($hits) + for $hit in distinct-values($trace) + return concat($hit, ': ', count($hit[. = $trace])) +}; + +declare function frus:exists-volume($volumeid as xs:string) as xs:boolean { + exists(collection('/db/apps/frus/volumes')/tei:TEI[@xml:id eq $volumeid]) +}; + +declare function frus:exists-fulltext-volume-in-db($volumeid as xs:string) as xs:boolean { + exists(frus:volume($volumeid)//tei:body/tei:div) +}; + +declare function frus:fulltext-volumes-in-db() as xs:string+ { + for $volume in collection($frus:VOLUMES-PATH)/tei:TEI[.//tei:body/tei:div] + return + frus:volumeid($volume) +}; + +declare function frus:volume-title($volumeids as xs:string+, $type as xs:string) as text()* { + for $volumeid in $volumeids + order by $volumeid + return + collection('/db/apps/frus/volumes')/tei:TEI[@xml:id eq $volumeid]//tei:title[@type eq $type]/text() +}; + +declare function frus:volume-title($volumeids as xs:string+) as text()* { + frus:volume-title($volumeids, 'complete') +}; + +declare function frus:exists-id($volumeid as xs:string, $id as xs:string) as xs:boolean { + exists(frus:volume($volumeid)/id($id)) +}; + +declare function frus:id($volumeid as xs:string, $id as xs:string) as element() { + frus:volume($volumeid)/id($id) +}; + +declare function frus:type-of-id($volumeid as xs:string, $id as xs:string) as xs:string { + string(frus:volume($volumeid)/id($id)/@type) +}; + +declare function frus:pdf-collection($volumeid as xs:string) as xs:string { + concat($frus:PDF-DB-PATH, $volumeid, '/pdf/') +}; + +declare function frus:pdf-filename($volumeid as xs:string) as xs:string { + concat($volumeid, '.pdf') +}; + +declare function frus:exists-pdf($volumeid as xs:string) as xs:boolean { + if ($frus:STATIC-FILE-LOCATION = 'local') then + util:binary-doc-available(frus:pdf-db-path($volumeid)) + else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) + let $collection := '/db/history/data/s3-resources/' || $frus:bucket || '/frus/' + let $pdf-filename := concat($volumeid, '.pdf') + return + exists(collection($collection)//filename[. = $pdf-filename]) +}; + +declare function frus:volumes-with-ebooks() { + for $hit in collection('/db/history/data/s3-resources/' || $frus:bucket || '/frus/')//filename[ends-with(., '.epub')] + return + substring-before($hit, '.epub') +}; + +declare function frus:volumes-with-single-pdfs() { + for $hit in collection('/db/history/data/s3-resources/' || $frus:bucket || '/frus/')//filename[ends-with(.,'.pdf')][starts-with(., 'frus')] + return + substring-before($hit, '.pdf') +}; + +declare function frus:volumes-with-ebooks-or-single-pdfs() { + distinct-values((frus:volumes-with-ebooks(), frus:volumes-with-single-pdfs())) +}; + +declare function frus:exists-ebook($volumeid as xs:string) as xs:boolean { + if ($frus:STATIC-FILE-LOCATION = ('local', 'hsg')) then + exists(doc('/db/history/data/historicaldocuments/ebooks.xml')//ebook[@id eq $volumeid]) + else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) + let $collection := concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook') + let $epub-filename := concat($volumeid, '.epub') + return + exists(collection($collection)//filename[. = $epub-filename]) +}; + +declare function frus:epub-url($volumeid as xs:string) as xs:string { + concat($frus:PDF-URL-PATH, $volumeid, '/ebook/', $volumeid, '.epub') +}; + +declare function frus:mobi-url($volumeid as xs:string) as xs:string { + concat($frus:PDF-URL-PATH, $volumeid, '/ebook/', $volumeid, '.mobi') +}; + +declare function frus:epub-size($volumeid as xs:string) { + let $epub := (doc(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook/resources.xml'))//filename[ends-with(., '.epub')]/parent::resource)[1] + let $size := $epub//size + return + frus:bytes-to-readable($size) +}; + +declare function frus:mobi-size($volumeid as xs:string) { + let $mobi := (doc(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook/resources.xml'))//filename[ends-with(., '.mobi')]/parent::resource)[1] + let $size := $mobi//size + return + frus:bytes-to-readable($size) +}; + +(: returns the size of a file in kb or mb :) +declare function frus:file-size($collection as xs:string, $filename as xs:string) as xs:string { + let $sizeinbytes := xmldb:size($collection, $filename) + return frus:bytes-to-readable($sizeinbytes) +}; + +declare function frus:bytes-to-readable($bytes as xs:integer) { + if ($bytes gt 1000000) then + concat((round($bytes div 10000) div 100), 'mb') + else if ($bytes gt 1000) then + concat(round($bytes div 1000), 'kb') + else () +}; + +declare function frus:ebook-last-updated($volumeid as xs:string) { + let $epub-filename := concat($volumeid, '.epub') + let $epub := (doc(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/ebook/resources.xml'))//filename[ends-with(., '.epub')]/parent::resource)[1] + return + $epub/last-modified/string() +}; + +declare function frus:exists-doc-pdf($volumeid, $document) as xs:boolean { + if ($frus:STATIC-FILE-LOCATION = ('local', 'hsg')) then + util:binary-doc-available(concat(frus:pdf-collection($volumeid), '/', $document, '.pdf')) + else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) + collection(concat('/db/history/data/s3-resources/' || $frus:bucket || '/frus/', $volumeid, '/pdf/'))//filename = concat($document, '.pdf') +}; + +declare function frus:pdf-db-path($volumeid as xs:string) as xs:string { + concat(frus:pdf-collection($volumeid), frus:pdf-filename($volumeid)) +}; + +declare function frus:pdf-url($volumeid as xs:string) as xs:string { + concat($frus:PDF-URL-PATH, $volumeid, if ($frus:STATIC-FILE-LOCATION eq 's3') then () else '/media', '/pdf/', $volumeid, '.pdf') +}; + +declare function frus:page-image-url($id as element(tei:pb)) as xs:string { + let $volumeid := frus:volumeid($id) + let $facs := $id/@facs/string() + return + concat($frus:PAGEIMAGES-URL-PATH, $volumeid, if ($frus:STATIC-FILE-LOCATION eq 's3') then () else '/media', '/medium/', $facs, '.png') +}; + +declare function frus:pdf-size($volumeid as xs:string) { + if ($frus:STATIC-FILE-LOCATION = 'local') then + frus:file-size(frus:pdf-collection($volumeid), frus:pdf-filename($volumeid)) + else (: if ($frusx:STATIC-FILE-LOCATION = 's3') then :) + frus:bytes-to-readable(collection('/db/history/data/s3-resources/' || $frus:bucket || '/frus/')//filename[. eq frus:pdf-filename($volumeid)]/following-sibling::size) +}; + +declare function frus:isbn($volumeid as xs:string) as text()* { + frus:volume($volumeid)//tei:idno[@type = ('isbn-10','isbn-13')]/text() +}; + +declare function frus:isbn-url($isbn as xs:string) as xs:string { + concat('http://www.worldcat.org/search?q=isbn%3A', $isbn) +}; + +declare function frus:documents() as element(tei:div)* { + collection($frus:VOLUMES-PATH)//tei:div[@type='document' and @xml:id] +}; + +declare function frus:documents($volumeids as xs:string+) as element(tei:div)+ { + let $volumes := frus:volume($volumeids) + return $volumes//tei:div[@type='document' and @xml:id] +}; + +declare function frus:documents($volumeid as xs:string, $id as xs:string) as element(tei:div)* { + frus:id($volumeid, $id)//tei:div[@type='document' and @xml:id] +}; + +declare function frus:document-number($volumeid as xs:string, $id as xs:string) as xs:string { + frus:id($volumeid, $id)/@n/string() +}; + +declare function frus:document-number($id as element(tei:div)) as xs:string { + $id/@n/string() +}; + +declare function frus:document-id($id as element(tei:div)) as xs:string { + $id/@xml:id/string() +}; + +declare function frus:get-citation($id as element()) as xs:string { + let $volume := frus:volumeid($id) + let $volumetitle := frus:volume-title($volume, 'complete') + let $subvolumeinfo := + if ($id/self::tei:div) then + if ($id/@type eq 'document') then + concat ('Document ', $id/@n) + else if ($id/@type = ('section', 'chapter', 'compilation', 'subchapter')) then + $id/tei:head/text() + else () + else if (name($id) eq 'pb' and $id/@n) then + let $ancestor := ($id/following-sibling::element()[1][self::tei:div], $id/ancestor::tei:div[1])[1] + let $ancestorcitation := + if ($ancestor/@type eq 'document') then + concat('Document ', $ancestor/@n, ', ') + else if ($ancestor/@type = ('section', 'chapter', 'compilation', 'subchapter')) then + concat(frus:head-sans-note($ancestor), ', ') + else () + return + concat($ancestorcitation, 'Page ', $id/@n) + else () + let $citation := string-join(($volumetitle, $subvolumeinfo), ', ') + return $citation +}; + +(: TODO: make changes necessary to be able to remove * :) +declare function frus:source-note($id as element(tei:div)) as element(tei:note)* { + ($id//tei:note[@type='source'])[1] +}; + +declare function frus:lb-to-whitespace($node) { + for $n in $node/node() + return + typeswitch ($n) + case element(tei:lb) return ' ' + case element() return element { name($n) } { frus:lb-to-whitespace($n) } + default return + $n +}; + +declare function frus:head-sans-note($id as element(tei:div)) as xs:string { + let $sans-note := functx:remove-elements-deep($id/tei:head[1], "note") + let $lb-to-whitespace := frus:lb-to-whitespace($sans-note) + return + normalize-space(string-join($lb-to-whitespace)) +}; + +(: TODO: make changes necessary to be able to remove * :) +declare function frus:dateline($id as element(tei:div)) as element(tei:dateline)* { + let $dateline := ($id//tei:dateline)[1] (: frus1952-54/d414 has two datelines - the second belonging to an attachment :) + return + functx:remove-elements-deep($dateline, 'note') +}; + +declare function frus:document-summary($id as element(tei:div)) as element(tei:note)* { + if ($id//tei:note/@type='summary') then + $id//tei:note[@type='summary'] + else () +}; + +declare function frus:document-head-sans-number($id as element(tei:div)) as xs:string { + let $head := frus:head-sans-note($id) + let $newtitle := + if (matches($head, "^\d+a?\.")) then + replace($head, "^\d+a?\.", "") + else if (matches($head, "^No. \d+a?")) then + replace($head, "^No. \d+a?", "") + else + $head + return + $newtitle +}; + +declare function frus:sections($volumeid as xs:string) { + frus:volume($volumeid)//tei:div[@type = ('section', 'compilation', 'chapter')] +}; + +declare function frus:url($volumeid as xs:string) as xs:string { + concat($frus:FRUS-URL, $volumeid) +}; + +declare function frus:url($volumeid as xs:string, $id as xs:string) as xs:string { + concat($frus:FRUS-URL, $volumeid, '/', $id) +}; + +declare function frus:toc($volume as xs:string) { + frus:toc($volume, ()) +}; + +(: whole volume TOC :) +declare function frus:toc($volume as xs:string, $id-to-highlight as xs:string?) { + let $vol := frus:volume($volume) + return +
    +
      {frus:toc-passthru($vol, $id-to-highlight)}
    +
    +}; + +(: volume of just the section id and deeper :) +declare function frus:toc-inner($volume as xs:string, $id as xs:string?) { + let $inner-section := frus:id($volume, $id) + return +
    +
      {frus:toc-passthru($inner-section, $id)}
    +
    +}; + +declare function frus:toc-passthru($node as item()*, $id-to-highlight as xs:string?) { + (: if we're given a div, dig deeper :) + if ($node/self::tei:div) then + $node/tei:div ! frus:toc-div(., $id-to-highlight) + (: if we're not given a div - presumably a tei:TEI or tei:text element - find the topmost divs :) + else + $node//tei:div[not(ancestor::tei:div)] ! frus:toc-div(., $id-to-highlight) +}; + +(: handles divs for TOCs :) +declare function frus:toc-div($node as element(tei:div), $id-to-highlight as xs:string?) { + let $sections-to-suppress := ('toc') + return + (: we only show certain divs :) + if (not($node/@xml:id = $sections-to-suppress) and not($node/@type = 'document')) then +
  • + { + let $href := attribute href { concat('/historicaldocuments/', frus:volumeid($node), '/', $node/@xml:id) } + let $highlight := if ($node/@xml:id = $id-to-highlight) then attribute class {'highlight'} else () + return + + { + $href, + $highlight, + frus:toc-head($node/tei:head[1]) + } + + , + + if ($node/tei:div/@type = 'document') then + concat( + ' (Document', + let $child-docs := $node/tei:div[@type = 'document'] + let $first := $child-docs[1]/@n + let $last := $child-docs[last()]/@n + return + if ($first = $last) then + concat(' ', $first) + else + concat('s ', $first, '-', $last) + , ')' + ) + else + () + , + if ($node/tei:div/@type = 'document' and not($node/tei:div/@type = 'document')) then () + else +
      + { + frus:toc-passthru($node, $id-to-highlight) + } +
    + } +
  • + else + () +}; + +(: handles heads for TOCs :) +declare function frus:toc-head($node as element(tei:head)) { + let $head-sans-note := if ($node//tei:note) then functx:remove-elements-deep($node, 'note') else $node + return + render:recurse($head-sans-note, ()) +}; + +declare function frus:toc-link($node, $id-to-highlight) { + let $id := $node/@xml:id + let $vol-id := frus:volumeid($node) + return + concat($vol-id, '/', $id) +}; + +declare function frus:toc-render($volume as xs:string, $id as xs:string, $highlightcurrent, $view) { + (: TODO: make nested lists valid XHTML :) + for $sect in frus:sections($volume)[not(parent::tei:div[@type='compilation'])] + let $type := $sect/@type + let $sectid := $sect/@xml:id + let $secttitle := $sect/tei:head[1] + let $secttitletext := data(functx:remove-elements-deep($secttitle, "note")) + let $currentsection := + (: is $id a document in a chapter/compilation? :) + if (frus:id($volume, $id)/self::tei:div[@type='document']) then + frus:id($volume, $id)/parent::tei:div/tei:head[1] + (: or is $id a chapter/compilation itself? :) + else + frus:id($volume, $id)/tei:head[1] + let $highlightstatus := + if ($highlightcurrent) then + if ($secttitle eq $currentsection) then true() + else () + else () + let $viewstatus := + if ($view) then + (concat("/", $view)) + else () + return + if ($type = ('section', 'compilation')) then + (: 1st tier: for sections and compilations :) +
  • { frus:toc-link($secttitletext, $volume, $sectid, $viewstatus, $highlightstatus) } + { + if ($sect/tei:div[@type='chapter']) then + (: 2nd tier: for chapters contained inside sections and compilations :) +
      + { + for $chapter in $sect/tei:div[@type='chapter'] + let $chapterid := $chapter/@xml:id/string() + let $chaptertitle := $chapter/tei:head[1] + let $chaptertitletext := data(functx:remove-elements-deep($chaptertitle, "note")) + let $currentchapter := + (: is $id a document in a chapter/compilation? :) + if (frus:id($volume, $id)/self::tei:div[@type='document']) then + frus:id($volume, $id)/parent::tei:div/tei:head[1] + (: or is $id a chapter/compilation itself? :) + else + frus:id($volume, $id)/tei:head[1] + let $highlightstatus := + if ($highlightcurrent) then + if ($chaptertitle eq $currentchapter) then true() + else () + else () + return +
    • { frus:toc-link($chaptertitletext, $volume, $chapterid, $viewstatus, $highlightstatus) }
    • + } +
    + else () + } +
  • + else if ($sect/parent::tei:div[@type='compilation']) then + (: if chapter is part of a compilation, don't show it :) + () + else + (: if chapter isn't encapsulated in a compilation, show it :) +
  • { frus:toc-link($secttitletext, $volume, $sectid, $viewstatus, $highlightstatus) }
  • +}; + +declare function frus:toc-link($secttitletext, $volume, $sectid, $viewstatus, $highlightstatus) { + if ($highlightstatus) then + (: highlight :) + ( attribute class {"sidebarhighlighted"}, $secttitletext ) + else + {$secttitletext} +}; + +declare function frus:editor-role-to-label($role as xs:string, $form as xs:string) as xs:string { + let $item := doc('/db/apps/frus/shared/frus-production.xml')/id("frus-editor-roles")//tei:category[@xml:id = $role] + let $label := + if ($form = 'plural') then + $item/tei:catDesc/tei:term[@type eq "plural"] + else (: if ($form = 'plural') then :) + $item/tei:catDesc/tei:term[@type eq "singular"] + return + $label/string() +}; + +declare function frus:editor-roles() as xs:string+ { + doc('/db/apps/frus/shared/frus-production.xml')/id("frus-editor-roles")//tei:category/@xml:id +}; diff --git a/modules/tei-render.xql b/modules/tei-render.xql index fda6736..0722c6c 100644 --- a/modules/tei-render.xql +++ b/modules/tei-render.xql @@ -1,1636 +1,1636 @@ -xquery version "3.0"; - -(:~ This module uses XQuery 'typeswitch' to do all of the FRUS TEI-to-HTML - : conversion that we previously did with an XSLT stylesheet (frusteidoc2html.xsl). - : - and pass TEI fragments to render:render() as - render:render($teiFragment, $options) - : where $options contains parameters and other info in an element like: - - - - - - : - : Author: Joe Wicentowski - : Version: 1.0 (Mar 6, 2009) - :) - -module namespace render="http://history.state.gov/ns/xquery/tei-render"; - -import module namespace frusx = "http://history.state.gov/ns/xquery/frus" at "frus.xql"; -import module namespace console="http://exist-db.org/xquery/console"; -import module namespace functx = "http://www.functx.com"; - -(: default namespaces :) -declare default function namespace "http://www.w3.org/2005/xpath-functions"; -declare default element namespace "http://www.w3.org/1999/xhtml"; - -declare namespace tei="http://www.tei-c.org/ns/1.0"; -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; -declare namespace html="http://www.w3.org/1999/xhtml"; -declare namespace frus="http://history.state.gov/frus/ns/1.0"; - -(: a helper function in case no options are passed to the function :) -declare function render:render($content as node()*) as element() { - render:render($content, ()) -}; - -(: creates a document div for fitting TEI into history.state.gov template :) -declare function render:render($content as node()*, $options as element()*) as element() { - let $body-options := {$options/*} - return -
    - { - render:recurse($content, $body-options), - render:note-end($content, $options) - } -
    -}; - -(: just recurses back to render:main() :) -(: DS: live code actually does various stuff here before it recurses :) -declare function render:recurse($content as node()*, $options) as item()* { - (: use recursion as an opportunity to insert space between adjacent elements that would otherwise be smooshed if indent=no :) - (: TODO add check for indent status :) - let $adjacent-nodes := ('hi', 'ref', 'persName', 'placeName', 'date', 'gloss', 'lb', 'del') - let $nodes := $content/node() - let $node-count := count($nodes) - for $node at $n in $nodes - return - ( - render:main($node, $options) - , - (: it helps if the adjacent nodes have already had trailing spaces before close tag removed, e.g., find \s+ replace with :) - if ($n < $node-count) then - if ($node instance of element() and $node/name() = $adjacent-nodes and $nodes[$n + 1] instance of element() and $nodes[$n + 1]/name() = $adjacent-nodes) then ' ' (:' ':) else () - else () - ) -}; - -(: main routine :) -declare function render:main($nodes as node()*, $options) as item()* { - for $node in $nodes - return - ( - (:console:log(util:node-id($node)),:) - typeswitch($node) - case text() return $node - case element(tei:TEI) return render:TEI($node, $options) - case element(tei:text) return render:text($node, $options) - case element(tei:front) return render:front($node, $options) - case element(tei:body) return render:body($node, $options) - case element(tei:back) return render:back($node, $options) - case element(tei:div) return render:div($node, $options) - case element(frus:attachment) return render:frus-attachment($node, $options) - case element(tei:head) return render:head($node, $options) - case element(tei:p) return render:p($node, $options) - case element(tei:q) return render:q($node, $options) - case element(tei:quote) return render:quote($node, $options) - case element(tei:hi) return render:hi($node, $options) - case element(tei:del) return render:del($node, $options) - case element(tei:list) return render:list($node, $options) - case element(tei:item) return render:item($node, $options) - case element(tei:label) return render:label($node, $options) - case element(tei:postscript) return render:postscript($node, $options) - case element(tei:ref) return render:ref($node, $options) - case element(tei:note) return render:note($node, $options) - case element(tei:dateline) return render:dateline($node, $options) - case element(tei:date) return render:date($node, $options) - case element(tei:time) return render:time($node, $options) - case element(tei:persName) return render:persName($node, $options) - case element(tei:gloss) return render:gloss($node, $options) - case element(tei:placeName) return render:placeName($node, $options) - case element(tei:orgName) return render:orgName($node, $options) - case element(tei:term) return render:term($node, $options) - case element(tei:opener) return render:opener($node, $options) - case element(tei:salute) return render:salute($node, $options) - case element(tei:closer) return render:closer($node, $options) - case element(tei:signed) return render:signed($node, $options) - case element(tei:listBibl) return render:listBibl($node, $options) - case element(tei:bibl) return render:bibl($node, $options) - case element(tei:said) return render:said($node, $options) - case element(tei:listPerson) return render:listPerson($node, $options) - case element(tei:lb) return render:lb($node, $options) - case element(tei:milestone) return render:milestone($node, $options) - case element(tei:anchor) return render:anchor($node, $options) - case element(tei:figure) return render:figure($node, $options) - case element(tei:graphic) return render:graphic($node, $options) - case element(tei:table) return render:table($node, $options) - case element(tei:row) return render:row($node, $options) - case element(tei:cell) return render:cell($node, $options) - case element(tei:geo) return () - case element(tei:pb) return render:pb($node, $options) - case element(tei:title) return render:title($node, $options) - case element(tei:byline) return render:byline($node, $options) - case element(tei:seg) return render:seg($node, $options) - case element(tei:idno) return render:idno($node, $options) - case element(tei:lg) return render:lg($node, $options) - case element(tei:l) return render:l($node, $options) - - case element(html:colgroup) return $node - - default return render:recurse($node, $options) - ) -}; - -declare function render:TEI($node as element(tei:TEI), $options) as element() { -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    -}; - -declare function render:text($node as element(tei:text), $options) as element() { -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    -}; - -declare function render:front($node as element(tei:front), $options) as element() { -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    -}; - -declare function render:body($node as element(tei:body), $options) as element() { -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    -}; - -declare function render:back($node as element(tei:back), $options) as element() { -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    -}; - -declare function render:div($node as element(tei:div), $options) { - if ($node/@type = 'theme-highlight') then -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    - else -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    -}; - -declare function render:frus-attachment($node as element(frus:attachment), $options) { -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    -}; - -declare function render:head($node as element(tei:head), $options) as element()* { - if ($node/@type = ('shortened-for-running-head')) then - () - else if ($node/parent::tei:div or $node/parent::frus:attachment) then - let $rendition := $node/@rendition - let $style := if ($rendition) then attribute style {render:rendition-to-css($rendition)} else () - let $type := $node/parent::tei:div/@type - return - if ($type = ('section', 'section-pending', 'appendix', 'compilation', 'compilation-pending', 'part') ) then - if ($type eq 'section' and $options/*:param[@name='suppress-head-if-first-div']/@value eq 'true') then () - else -

    {$style, render:recurse($node, $options)}

    - else if ($type = ('document', 'document-pending', 'subchapter', 'chapter', 'chapter-pending', 'chapter-introduction', 'part') ) then - ( - (: show a bracketed document number for volumes that don't use document numbers :) - if ($type = 'document' and not(starts-with($node, concat($node/parent::tei:div/@n, '.')) or starts-with($node, concat('No. ', $node/parent::tei:div/@n)))) then -

    { - if (matches($node/parent::tei:div/@n, '^\[.+?\]$')) then - $node/parent::tei:div/@n/string() - else - concat('[Document ', $node/parent::tei:div/@n, ']') - }

    - else (), -

    {$style, render:recurse($node, $options)}

    - ) - else if ($type = 'timeline') then - {render:recurse($node, $options)} - else if ($node/ancestor::tei:div/@xml:id) then - element {concat('h', index-of($node/ancestor::tei:div, $node/ancestor::tei:div[@xml:id][1]) + 2)} {$style, render:recurse($node, $options)} - else -

    {$style, render:recurse($node, $options)}

    - else if ($node/parent::tei:figure) then - if ($node/parent::tei:figure/parent::tei:p) then - {render:recurse($node, $options)} - else (: if ($node/parent::tei:figure/parent::tei:div) then :) -

    {render:recurse($node, $options)}

    - else if ($node/parent::tei:list) then - if ($node/parent::tei:list/@type = ('participants', 'subject', 'from', 'references', 'to') ) then - if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then -

    {render:recurse($node, $options)}

    - else -
  • {render:recurse($node, $options)}
  • - else if ($node/ancestor::tei:list/@type = ('participants', 'subject', 'from', 'references', 'to') ) then - if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then -
    {render:recurse($node, $options)}
    - else -
  • {render:recurse($node, $options)}
  • - else - {render:recurse($node, $options)} - else if ($node/parent::tei:table) then -

    {render:recurse($node, $options)}

    - else - {render:recurse($node, $options)} -}; - -(: TODO: Look for instances in the XML of the weird old 'participantcol129' :) -declare function render:p($node as element(tei:p), $options) as item()* { - let $rend := $node/@rend - let $body-mode-for-footnotes := $options/*:param[@name='body-mode-for-footnotes']/@value - let $result := - if ($rend) then - if ($rend = ( - 'subjectentry', 'sectiontitleital', 'sectiontitlebold', 'subjectallcaps', - 'sourceheadcenterboldbig', 'sourcearchiveboldbig', 'sourceparagraphspaceafter', - 'sourceparagraphtightspacing', 'sourceparagraphfullindent', - 'flushleft', 'right', 'center', 'strong', 'centerstrong') - ) then -

    { attribute class {data($rend)} }{ render:recurse($node, $options) }

    - else if ($rend = 'italic') then -

    {render:recurse($node, $options)}

    - else if ($rend = 'underline') then -

    {render:recurse($node, $options)}

    - else -

    {render:recurse($node, $options)}

    - (: TODO: Try to handle multi-paragraph footnotes for Forrest - else if ($node/parent::tei:note and not($node/preceding-sibling::tei:p)) then - render:recurse($node, $options) - :) - else -

    {render:recurse($node, $options)}

    - return - - (: check if we're inside the body version of a footnote, rather than the render:note-end() version. - if so, collapse the block

    into an inline to prevent artifacts of the note from appearing in - the surrounding paragraph text :) - - if ($body-mode-for-footnotes = 'true' and $node/ancestor::tei:note[1][not(@rend = 'inline')]) then - if ($node/preceding-sibling::element()) then - (
    ,
    , {$result/@*, $result/node()}) - else - $result/node() - - (: if we're just a normal paragraph, then carry on :) - - else - $result - -}; - -declare function render:q($node as element(tei:q), $options) as element()* { - let $rend := $node/@rend - return - if ($rend = 'blockquote') then - if ($node/tei:p) then -

    {render:recurse($node, $options)}
    - else -

    {render:recurse($node, $options)}

    - else if ($node/parent::tei:q) then - ‘{render:recurse($node, $options)}’ - else - “{render:recurse($node, $options)}” -}; - -declare function render:quote($node as element(tei:quote), $options) as element()* { - let $rend := $node/@rend - return - (: if ($rend = 'blockquote') then :) - if ($node/tei:p or $node/tei:lg) then -
    {render:recurse($node, $options)}
    - else -

    {render:recurse($node, $options)}

    - (: else - {render:recurse($node, $options)} :) -}; -(: known types: italic, strong :) -declare function render:hi($node as element(tei:hi), $options) as element() { - let $rend := $node/@rend - return - if ($rend = 'italic') then - if ($node/ancestor::tei:signed) then - {render:recurse($node, $options)} - else - {render:recurse($node, $options)} - else if ($rend = 'strong') then - {render:recurse($node, $options)} - else if ($rend = 'sub') then - {render:recurse($node, $options)} - else if ($rend = 'superscript') then - {render:recurse($node, $options)} - else if ($rend = 'underline') then - {render:recurse($node, $options)} - else if ($rend = 'smallcaps') then - {render:recurse($node, $options)} - else if ($rend = 'roman') then - {render:recurse($node, $options)} - else - {render:recurse($node, $options)} -}; - -declare function render:del($node as element(tei:del), $options) as element() { - let $rend := $node/@rend - return - if ($rend = 'strikethrough') then - {render:recurse($node, $options)} - else - {render:recurse($node, $options)} -}; - -declare function render:list($node as element(tei:list), $options) as item()+ { - if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then - render:recurse($node, $options) - else - let $type := $node/@type - let $rend := $node/@rend - (: when showing inline footnotes, we have to force block elements like item to be inline :) - let $force-block-inline := $options/*:param[@name='force-block-inline']/@value - return - if ($force-block-inline) then - (
    , render:recurse($node, $options),
    ) - else if ($type = ('participants', 'subject', 'from', 'to', 'references', 'simple') ) then -
      {render:recurse($node, $options)}
    - else if ($type = 'index') then -
      {render:recurse($node, $options)}
    - else if ($type = 'indexentry') then -
      {render:recurse($node, $options)}
    - else if ($type = 'ordered') then (: TODO fix list/label and list/item :) -
    {render:recurse($node, $options)}
    - else if ($rend = 'bulleted') then -
      {render:recurse($node, $options)}
    - else if ($node/tei:head) then - ( - if ($node/tei:head) then render:head($node/tei:head, $options) else () - , -
      {for $item in $node/tei:head/following-sibling::* return render:main($item, $options)}
    - ) - else -
      { - if ($node/ancestor::tei:list or $node/ancestor::tei:table/@rend='schedule') then () else attribute class {"hanging-indent"}, - render:recurse($node, $options) - }
    -}; - -declare function render:item($node as element(tei:item), $options) as element()+ { - if ($options/*:param[@name='ebook-format']/@value = 'mobi') then -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - if ($node/preceding-sibling::tei:label) then - ( - render:recurse($node/preceding-sibling::tei:label[1], $options), - ' ', - render:recurse($node, $options) - ) - else - render:recurse($node, $options) - }
    - else - let $force-block-inline := $options/*:param[@name='force-block-inline']/@value - return - if ($force-block-inline) then - ( - { - if ($node/parent::tei:list/@rend='bulleted') then - '• ' - else if ($node/preceding-sibling::*[1]/self::tei:label) then - concat($node/preceding-sibling::*[1]/self::tei:label, ' ') - else - () - , - render:recurse($node, $options) - }, -
    - ) - else if ($node/preceding-sibling::tei:label) then -
  • { - render:recurse($node/preceding-sibling::tei:label[1], $options), ' ', render:recurse($node, $options) - }
  • - else if ($node/parent::tei:list/@type eq 'subject' and $node/parent::tei:list/@rend eq 'flushleft') then - (: handles flush left subject lines in 1952-54 volumes, TODO - test when receive first volume :) -
  • { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - if ($node/preceding-sibling::tei:label) then - ( - render:recurse($node/preceding-sibling::tei:label[1], $options), - ' ', - render:recurse($node, $options) - ) - else - render:recurse($node, $options) - }
  • - else -
  • { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - if ($node/preceding-sibling::element()[1]/self::tei:label) then - ( - render:recurse($node/preceding-sibling::tei:label[1], $options), - ' ', - render:recurse($node, $options) - ) - else - render:recurse($node, $options) - }
  • -}; - -declare function render:label($node as element(tei:label), $options) as item()* { - if ($node/parent::tei:list) then - () - else - {render:recurse($node, $options)} -}; - -declare function render:postscript($node as element(tei:postscript), $options) as element()+ { -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - if ($node/node()[1]/self::tei:label and $node/node()[2]/self::tei:p) then - let $new-node := - ( - element tei:p { $node/tei:label[1], ' ', $node/tei:p[1]/node() } - , - $node/tei:p[1]/following-sibling::node() - ) - return - render:main($new-node, $options) - else - render:recurse($node, $options) - }
    -}; - -declare function render:xmlid($node as element(), $options) as element() { - -}; - -declare function local:index-of($seq as node()*, $n as node()) as xs:integer* { - local:index-of($seq, $n, 1) -}; - -declare function local:index-of($seq as node()*, $n as node(), $i as xs:integer) as xs:integer* { - if ( empty($seq) ) then - () - else if ( $seq[1] is $n ) then - ( $i, local:index-of(remove($seq, 1), $n, $i + 1) ) - else - local:index-of(remove($seq, 1), $n, $i + 1) -}; - -(: TODO Add handling for :) -declare function render:note($node as element(tei:note), $options ) as item()* { - let $suppress-note := $options/*:param[@name='suppress-note']/@value - let $rendition := $node/@rendition - let $css := if ($rendition) then render:rendition-to-css($rendition) else () - return - if ($suppress-note eq 'true') then - () - else - let $div := $node/ancestor::tei:div[1]/@xml:id - return - if (empty($node) or $node = '') then - - {data($node/@n)} - - else if ($node/@rend='inline') then - (: display inline notes inline :) -

    { - if ($css) then attribute style { $css } else (), - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }

    - else if ($node/@type='summary') then - (: suppress ePub summary notes from being displayed :) - () - else if ($node/@n = '0') then -
    - Source - {''(: NOTE removed for EPUB - - {render:recurse($node, element {QName('', 'parameters')} {$options/*, element param {attribute name {'strip-links'}, attribute value {'true'}}})} - - :)} - - else if (not($node/@xml:id) and $node/@target) then - (: handle case of multiple references to the same footnote - TODO generate correct @href :) - - - {data($node/@n)} - - - else - let $incr := - xs:integer(local:index-of($node/ancestor::tei:div[1]//tei:note[@n], $node)) - let $incr := - if ($node/preceding::tei:note[@n = '0']) then $incr - 1 - else $incr - return - {data($node/@n)}{'' - (: NOTE removed for EPUB - - {data($node/@n)}. {data($node) (: TODO find a way to use render:recurse() - that doesn't make the CSS hiccup on span/a/em. Until then we lose styling on - inline footnotes :)} - - :)} - , ' ' (: this trailing space is needed until whitespace issues are fully dealt with :) -}; - -declare function render:note-end($content, $options) as element()* { - if (exists($content//tei:note[@n])) then - ( -
    , -
    - { - for $note at $incr in $content//tei:note[@n] - let $div := $note/ancestor::tei:div[1]/@xml:id - return - if ($note/@type = 'summary' or empty($note) or $note = '') then - (: suppress ePub summary notes from being displayed :) - () - else -
    { - let $return-link := - ( - if ($note/@n = '0') then - - * - - else - let $incr := if ($note/preceding::tei:note[@n = '0']) then $incr - 1 else $incr - return - - - {data($note/@n)} - - - , -   - ) - let $content-nodes := render:recurse($note, $options) - return - (: if the 1st child node of the note is a block-level element (e.g., p), we'll get an - unwanted space between the footnote number and the beginning of the text. so we check - for the first child being an element, and if so, tuck the footnote number inside. - TODO: refine this check to operate only on block-level elements. it's currently operating - even on phrase-level elements (e.g., em) :) - if ($content-nodes[1] instance of element()) then - for $content-node at $count in $content-nodes - return - if ($count = 1 and $content-node instance of element()) then - element {$content-node/name()} {$content-node/@*, $return-link, $content-node/node()} - else if ($count = 1) then - ($return-link, $content-node) - else - $content-node - else - ($return-link, $content-nodes) - }
    - } -
    - ) - else () -}; - -declare function render:ref($node as element(tei:ref), $options) as item()* { - let $target := $node/@target - let $volume := $options/*:param[@name = 'volume']/@value - let $abs-site-uri := $options/*:param[@name = 'abs-site-uri']/@value - let $relativeimagepath := $options/*:param[@name = 'relativeimagepath']/@value - let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' - let $strip-links := $options/*:param[@name = 'strip-links']/@value - let $persistent-view := if ($show-annotations) then '?view=annotations' else () - (:let $log := console:log(serialize($node) || ' from ' || $node/ancestor::tei:div[1]/@xml:id):) - let $type := - (: added to support class='mini-doc' for theme doc links :) - if ($node/@type) then attribute class { $node/@type } else () - return - (: catch refs without text :) - if ($node eq '') then - let $newnode := element tei:ref { attribute target {$node/@target}, data($node/@target) } - return - render:ref($newnode, $options) - (: route external links through disclaimer :) - else if (starts-with($target, 'http')) then - (: is it in state.gov domain? :) - (:if (matches($target, '^https?://[^.]*?.state.gov')) then:) - element a { - attribute href { $target }, - attribute title { $target }, - $type, - render:recurse($node, $options) - } - (: otherwise show disclaimer :) - (:else - element a { - attribute href { concat('/redirect?url=', xmldb:encode($target)) }, - attribute title { $target }, - $type, - render:recurse($node, $options) - }:) - (: ref to a target in the same volume by the object's @xml:id :) - else if (starts-with($target, '#')) then - (: don't let bad links through, but let range links through, which need to be parsed further :) - if (not(root($node)/id(substring-after($target, '#')) or starts-with($target, '#range'))) then - render:recurse($node, $options) - else - (: cross-ref to a target in the index :) - if (starts-with($target, '#in')) then - element a { - attribute href { concat('index.html', $target) }, - $type, - render:recurse($node, $options) - } - (: ref to a document :) - else if (matches($target, '^#d\d+$')) then - element a { - attribute href { concat( substring-after($target, '#'), '.html') }, - $type, - render:recurse($node, $options) - } - (: ref to a portion of a document, e.g., footnote :) - else if (matches($target, '^#d\d+.+')) then - element a { - attribute href { concat( replace($target, '^#(d\d+)(.+)$', '$1'), '.html', $target) }, - $type, - render:recurse($node, $options) - } - (: ref to a portion of a chapter, e.g., footnote :) - else if (matches($target, '^#chapter-\d+.*$')) then - element a { - attribute href { - if (contains($target, 'fn')) then - let $ch := substring-before(substring-after($target, '#'), 'fn') - let $fn := substring-after($target, 'fn') - return - concat($ch, '.html#fn', $fn) - else - replace($target, '^#(chapter-\d+)(.*)$', '$1.html') - }, - $type, - render:recurse($node, $options) - } - (: ref to a portion of a chapter, e.g., footnote :) - else if (matches($target, '^#appendix-[a-z].*$')) then - element a { - attribute href { - if (contains($target, 'fn')) then - let $ch := substring-before(substring-after($target, '#'), 'fn') - let $fn := substring-after($target, 'fn') - return - concat($ch, '.html#', $ch, 'fn', $fn) - else - replace($target, '^#(appendix-[a-z])(.*)$', '$1.html') - }, - $type, - render:recurse($node, $options) - } - (: ref to a portion of a conclusion, e.g., footnote :) - else if (matches($target, '^#conclusion.*$')) then - element a { - attribute href { - if (contains($target, 'fn')) then - let $ch := substring-before(substring-after($target, '#'), 'fn') - let $fn := substring-after($target, 'fn') - return - concat($ch, '.html#fn', $fn) - else - replace($target, '^#(conclusion)(.*)$', '$1.html') - }, - $type, - render:recurse($node, $options) - } - (: ref to a portion of a introduction, e.g., footnote :) - else if (matches($target, '^#introduction[a-z].*$')) then - element a { - attribute href { - if (contains($target, 'fn')) then - let $ch := substring-before(substring-after($target, '#'), 'fn') - let $fn := substring-after($target, 'fn') - return - concat($ch, '.html#fn', $fn) - else - replace($target, '^#(introduction)(.*)$', '$1.html') - }, - $type, - render:recurse($node, $options) - } - (: Turn page-based links into document-based links :) - else if (starts-with($target, '#pg')) then - if ( - let $first-node := $node/preceding-sibling::node()[1] - let $second-node := $node/preceding-sibling::node()[2] - return - $first-node eq '–' and $second-node instance of element(tei:ref) and $second-node[starts-with(@target, '#pg')] - ) - then - ( - (:util:log-system-out(concat('pb rule 1: ', $node/@target)),:) - render:recurse($node, $options), - ' ', - render:pb-range-to-document-links(root($node), substring-after($node/preceding-sibling::tei:ref[1]/@target, '#'), substring-after($target, '#')) - ) - else if (subsequence($node/following-sibling::node(), 1, 1) eq '–' and subsequence($node/following-sibling::node(), 2, 1)/./self::tei:ref[starts-with(@target, '#pg')]) then - ( - (:util:log-system-out(concat('pb rule 2: ', $node/@target)),:) - render:recurse($node, $options) - ) - else - ( - (:util:log-system-out(concat('pb rule 3: ', $node/@target)),:) - render:recurse($node, $options), - ' ', - render:pb-to-document-links(root($node), substring-after($target, '#')) - ) - (: handle xpointer-style range references, as found in the frus-history, e.g., - index entries like: - Washington, George, 9–10 - point to: - - and: - - :) - else if (starts-with($target, '#range')) then - let $range := substring-after($target, '(') - let $range := substring-before($range, ')') - let $range := tokenize($range, ',') - let $range-start := $range[1] - let $range-end := $range[2] - let $target-start-node := root($node)/id($range-start) - let $target-end-node := root($node)/id($range-end) - (: use ancestor notes to ensure linkability :) - let $target-start-node := if ($target-start-node/ancestor::tei:note) then $target-start-node/ancestor::tei:note else $target-start-node - let $target-end-node := if ($target-end-node/ancestor::tei:note) then $target-end-node/ancestor::tei:note else $target-end-node - let $target-start-node-ancestor-div := $target-start-node/ancestor::tei:div[1] - let $target-end-node-ancestor-div := $target-end-node/ancestor::tei:div[1] - let $same-ancestor-divs := $target-start-node-ancestor-div = $target-end-node-ancestor-div - (: use the ancestor chapter div's heading, e.g., "Chapter 9: ...", but chop off at the colon :) - let $target-nodes := ($target-start-node, $target-end-node) - let $target-divs := ($target-start-node-ancestor-div, $target-end-node-ancestor-div) - let $target-node-labels := - let $both-notes := $target-nodes[1]/self::tei:note and $target-nodes[2]/self::tei:note - let $one-note := $target-nodes[1]/self::tei:note or $target-nodes[2]/self::tei:note - for $target-node at $n in $target-nodes - let $ancestor-div-label := - if ($same-ancestor-divs and $n = 2) then - () - else - string-join(functx:remove-elements-deep($target-divs[$n]/tei:head[1], 'note'), '') - let $ancestor-div-label := - if (contains($ancestor-div-label, ':')) then substring-before($ancestor-div-label, ':') else $ancestor-div-label - let $node-label := - if ($target-node/self::tei:note) then - concat(if ($n = 1 and $both-notes) then 'footnotes ' else 'footnote ', $target-node/@n) - else - (: paragraph-like-block-number :) - concat(if ($one-note) then 'para ' else if ($n = 1) then 'paras ' else '', index-of($target-start-node-ancestor-div/*[not(self::tei:head)][not(self::tei:byline)][not(self::tei:p[@rend='sectiontitlebold'])], $target-node/ancestor::element()[parent::tei:div][1])) - return - string-join(($ancestor-div-label, $node-label), ' ') - let $label := - replace(string-join($target-node-labels, '–'), 'Chapter', 'Ch.') - let $target-node-destination-hash := - if ($target-start-node/self::tei:note) then - concat('#fnref', substring-after($target-start-node/@xml:id, 'fn')) - else - concat('#', $range-start) - return - (: check to make sure the targets exist :) - if ($target-start-node and $target-end-node) then - element a { - attribute href { concat($target-start-node-ancestor-div/@xml:id, '.html', $target-node-destination-hash) }, - $label - } - (: display the label in case of malformed links :) - else - $label - (: handle single point references, as found in the frus-history, e.g., - index entries like: - Woodford, Stewart, 98 - point to: - - :) - else if (starts-with($target, '#b')) then - let $url := substring-after($target, '#') - let $target-node := root($node)/id($url) - let $target-node := if ($target-node/ancestor::tei:note) then $target-node/ancestor::tei:note else $target-node - let $destination-div := $target-node/ancestor::tei:div[1] - (: use the ancestor chapter div's heading, e.g., "Chapter 9: ...", but chop off at the colon :) - let $head := string-join(functx:remove-elements-deep($destination-div/tei:head[1], 'note'), '') - let $target-node-label := - if ($target-node/self::tei:note) then - concat('footnote ', $target-node/@n) - else - concat('para ', index-of($destination-div/*[not(self::tei:head)][not(self::tei:byline)][not(self::tei:p[@rend='sectiontitlebold'])], $target-node/ancestor::element()[parent::tei:div][1])) - let $label := replace(concat(if (contains($head, ':')) then substring-before($head, ':') else $head, ' ', $target-node-label), 'Chapter', 'Ch.') - let $target-node-destination-hash := - if ($target-node/self::tei:note) then - concat('#fnref', substring-after($target-node/@xml:id, 'fn')) - else - $target - return - if ($target-node) then - element a { - attribute href { concat($destination-div/@xml:id, '.html', $target-node-destination-hash) }, - $label - } - (: display the label in case of malformed links :) - else - $label - (: ref to an appendix :) - else - element a { - attribute href { concat( substring-after($target, '#'), '.html' ) }, - $type, - render:recurse($node, $options) - } - (: ref to a footnote in another volume :) - else if (contains($target, '#') and contains($target, 'fn')) then - element a { - attribute href { concat('http://history.state.gov/', $abs-site-uri, substring-before($target, '#'), '/', concat(substring-before(substring-after($target, '#'), 'fn'), '#fn', substring-after($target, 'fn')), $persistent-view) }, - $type, - render:recurse($node, $options) - } - (: ref to a subsection of another volume :) - else if (contains($target, '#')) then - element a { - attribute href { concat('http://history.state.gov/', $abs-site-uri, substring-before($target, '#'), '/', substring-after($target, '#')) }, - $type, - render:recurse($node, $options) - } - (: just a ref to another volume :) - else if (starts-with($target, 'frus')) then - element a { - attribute href { concat('http://history.state.gov/', $abs-site-uri, $target) }, - $type, - render:recurse($node, $options) - } - (: most likely a ref to another section of the website :) - else - element a { - attribute href { $target }, - $type, - render:recurse($node, $options) - } -}; - - -declare function render:pb-to-document-links($vol as document-node(), $pb-id as xs:string) { - render:pb-range-to-document-links($vol, $pb-id, $pb-id) -}; - -declare function render:pb-range-to-document-links($vol as document-node(), $pb1-id as xs:string, $pb2-id as xs:string) { - let $pb1 := $vol/id($pb1-id) - let $pb2 := $vol/id($pb2-id) - let $range-start := $pb1 - let $range-end := subsequence($pb2/following::tei:pb, 1, 1) - let $divs := $vol//tei:div[@type=('document', 'section') and @xml:id] - let $divs-within-range := $divs[. >> $range-start and . << $range-end]/@xml:id - let $ancestor-document := subsequence($range-start/ancestor::tei:div[@type=('document', 'section') and @xml:id], 1, 1)/@xml:id - let $doc-ids := distinct-values(($ancestor-document, $divs-within-range)[. ne '']) - (: allow for possibility of links to non-document portions of a volume :) - let $doc-ids := if (empty($doc-ids)) then ($pb1/ancestor::tei:div[@xml:id][1]/@xml:id, $pb1/following::tei:div[@xml:id][1]/@xml:id)[1] else $doc-ids - (: let $log := console:log(concat($pb1-id, '-', $pb2-id, ': ', string-join($doc-ids))) :) - let $docs-in-frag := for $doc-id in $doc-ids return $vol/id($doc-id) - let $link := - [{ - let $docs-count := count($docs-in-frag) - return - if ($docs-count = 1) then - ( - if ($pb1 = $pb2) then - concat('Pg. ', $pb1/@n, ' is part of ') - else - concat('Pgs. ', $pb1/@n, '–', $pb2/@n, ' are part of ') - , - { - for $doc in $docs-in-frag - return - if ($doc/@type='document') then - concat('Doc. ', $doc/@n) - else - frusx:head-sans-note($doc) - } - ) - else - (: may need to account for this :) - ( - if ($pb1-id = $pb2-id) then - concat('Pg. ', $pb1/@n, ' includes portions of ') - else - concat('Pgs. ', $pb1/@n, '–', $pb2/@n, ' include portions of ') - , - for $doc at $count in $docs-in-frag - return - ( - {if ($doc/@n ne '') then concat('Doc. ', $doc/@n) else frusx:head-sans-note($doc)}, - if ($count lt $docs-count - 1 and $docs-count gt 2) then - ', ' - else if ($count lt $docs-count) then - if ($docs-count = 2) then - ' and ' - else - ', and ' - else () - ) - ) - }] - return - $link -}; - -declare function render:dateline($node as element(tei:dateline), $options) as element() { - let $rendition := $node/@rendition - let $css := if ($rendition) then render:rendition-to-css($rendition) else () - return - if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then - - else if ($css) then - {render:recurse($node, $options)} - else - -}; - -declare function render:date($node as element(tei:date), $options) as item()* { - let $strip-links := $options/*:param[@name = 'strip-links']/@value - let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' - return - - if ($show-annotations) then - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #496690"} - , - render:recurse($node, $options) - , - [{if ($node/@*) then string-join(for $att in $node/@* return concat('@', name($att), ': ', $att), ', ') else 'no @!!!'}] - ) - } - - else - render:recurse($node, $options) -}; - -declare function render:time($node as element(tei:time), $options) as item() { - let $strip-links := $options/*:param[@name = 'strip-links']/@value - let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' - return - - if ($show-annotations) then - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #496690"} - , - render:recurse($node, $options) - , - [{if ($node/@*) then string-join(for $att in $node/@* return concat('@', name($att), ': ', $att), ', ') else 'no @!!!'}] - ) - } - - else - render:recurse($node, $options) -}; - -declare function render:persName($node as element(tei:persName), $options) as item()+ { - let $strip-links := $options/*:param[@name = 'strip-links']/@value - let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' - return - - ( - if ($node/@xml:id) then render:xmlid($node, $options) else (), - if ($show-annotations) then - let $person-id := substring-after($node/@corresp, '#') - let $person-entry := root($node)/id($person-id) - let $name := string($person-entry) - let $entry := render:main($person-entry/ancestor::tei:item[1]/tei:hi[1]/following-sibling::node(), ()) - return - if ($person-id) then - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #911625"} - , - render:recurse($node, $options) - , - {$name}{$entry} - ) - } - - else if ($node/@xml:id) then - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #911625"} - , - render:recurse($node, $options) - , - [@xml:id: {$node/@xml:id/string()}] - ) - } - - else - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #911625"} - , - render:recurse($node, $options) - , - [no ID!!!] - ) - } - - else - render:recurse($node, $options) - ) -}; - -declare function render:gloss($node as element(tei:gloss), $options) as item()+ { - let $strip-links := $options/*:param[@name = 'strip-links']/@value - let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' - return - - if ($show-annotations) then - let $term-id := substring-after($node/@target, '#') - let $term-entry := root($node)/id($term-id) - let $term := string($term-entry) - let $entry := string($term-entry/ancestor::tei:item[1]/tei:hi[1]/following-sibling::node()) - return - if ($term-id) then - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #496690"} - , - render:recurse($node, $options) - , - {$term}{$entry} - ) - } - - else - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #496690"} - , - render:recurse($node, $options) - , - [no ID!!!] - ) - } - - else - render:recurse($node, $options) -}; - -declare function render:orgName($node as element(tei:orgName), $options) as item()+ { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) -}; - -declare function render:placeName($node as element(tei:placeName), $options) as item()+ { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) -}; - -declare function render:term($node as element(tei:term), $options) as item()+ { - let $strip-links := $options/*:param[@name = 'strip-links']/@value - let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' - return - - ( - if ($node/@xml:id) then render:xmlid($node, $options) else (), - if ($show-annotations) then - if ($node/@xml:id) then - - { - if ($strip-links) then - render:recurse($node, $options) - else - ( - attribute style {"font-weight: bold; color: #496690"} - , - render:recurse($node, $options) - , - [@xml:id: {$node/@xml:id/string()}] - ) - } - - else () - else - render:recurse($node, $options) - ) -}; - -declare function render:opener($node as element(tei:opener), $options) as element()+ { - (: mobi doesn't use floats and so doesn't need the same extra spacing :) - if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then -
    {render:recurse($node, $options)}
    - else - ( -
    {render:recurse($node, $options)}
    - , -
    - ) -}; - -declare function render:salute($node as element(tei:salute), $options) as element() { -

    {render:recurse($node, $options)}

    -}; - -declare function render:closer($node as element(tei:closer), $options) as element() { -

    {render:recurse($node, $options)}

    -}; - -declare function render:signed($node as element(tei:signed), $options) as element() { - {render:recurse($node, $options)} -}; - -declare function render:listBibl($node as element(tei:listBibl), $options) as item()+ { - if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then - render:recurse($node, $options) - else -
      {render:recurse($node, $options)}
    -}; - -declare function render:bibl($node as element(tei:bibl), $options) as element() { - if ($options/*:param[@name='ebook-format']/@value = 'mobi') then -
    { - if ($node/@xml:id) then render:xmlid($node, $options) else (), - render:recurse($node, $options) - }
    - else -
  • {render:recurse($node, $options)}
  • -}; - -declare function render:said($node as element(tei:said), $options) as element() { -

    {data($node/@who)}: {render:recurse($node, $options)}

    -}; - -declare function render:lb($node as element(tei:lb), $options) as item()* { - let $strip-lbs := $options/*:param[@name = 'strip-line-breaks']/@value - return - if ($strip-lbs = 'true') then ' ' else
    -}; - -declare function render:listPerson($node as element(tei:listPerson), $options) as element() { - let $type := $node/@type - return - if ($node/tei:person) then -
      {render:recurse($node, $options)}
    - else -
    • {data($type)}: {render:recurse($node, $options)}
    -}; - -declare function render:person($node as element(tei:person), $options) as element() { -
  • {render:recurse($node, $options)}
  • -}; - -declare function render:milestone($node, $options) as element() { - let $class := $node/@rend - return - if ($node/@rend eq 'hr') then -
    - else if ($node/@rend eq 'centered-asterisks') then -

    * * *

    - else -
    -}; - -declare function render:anchor($node, $options) as element()* { - if ($node/ancestor::tei:note) then - () - else - render:xmlid($node, $options) -}; - -declare function render:figure($node as element(tei:figure), $options) { - let $class := if ($node/@rend = 'smallfloatinline') then 'image-smallfloatinline' else 'image-wide' - return - if ($node/parent::tei:p) then - {render:recurse($node, $options)} - else - ( - (: insert a 'page-break-before' div immediately before image #2+ in the appendix, - to ensure caption has a chance of staying on the same page image in ebook :) - if ($node/ancestor::tei:div/@xml:id = 'appendix' and $node/preceding-sibling::tei:figure) then -
    - else - () - , -
    {render:recurse($node, $options)}
    - ) -}; - -declare function render:graphic($node as element(tei:graphic), $options) as node()+ { - let $url := $node/@url - let $head := $node/following-sibling::tei:head - let $relativeimagepath := $options/*:param[@name = 'relativeimagepath']/@value - return - ( - {normalize-space($head)}, - render:recurse($node, $options) - ) -}; - -declare function render:table($node as element(tei:table), $options) as element() { - let $rend := $node/@rend - let $rendition := $node/@rendition - let $style := - string-join( - ( - if ($rend="bordered") then - if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then - attribute border {'1'} - else - attribute style {'border: 1px solid #606060; border-collapse: collapse'} - else (), - if (matches($rend, '^width:')) then - $rend - else (), - if ($rendition) then - render:rendition-to-css($rendition) else - () - ), - '; ' - ) - return - { - if ($style) then attribute style {$style} else (), - (: legacy: allow older table rend styles :) - if ($rend and not($style)) then - attribute class {$rend} - else (), - render:recurse($node, $options) - }
    -}; - -declare function render:row($node as element(tei:row), $options) as element() { - let $label := $node/@role[. = 'label'] - return - {if ($label) then attribute class {'label'} else ()}{render:recurse($node, $options)} -}; - -declare function render:cell($node as element(tei:cell), $options) as element() { - let $role := $node/@role - let $rend := $node/@rend - let $rendition := $node/@rendition - let $columns := $node/@cols - let $rows := $node/@rows - let $is-label := $role eq 'label' - let $is-brace := $role eq 'brace' - let $is-num := $role eq 'num' - let $style := - string-join( - ( - if ($node/ancestor::tei:table/@rend eq 'bordered') then 'border: 1px solid #606060' else () - , - if (matches($rend, '^width:')) then $rend else () - , - if (matches($rend, '^padding-')) then $rend else () - , - (: Virginia suggested left-align column-spanning cells by default :) - (: - if ($columns) then 'text-align: center' else () - , - :) - if ($is-num) then 'text-align: right' else () - , - if ($rendition) then render:rendition-to-css($rendition) else () - ) - , - '; ' - ) - return - element { if ($is-label) then 'th' else 'td' } { - if ($style) then attribute style {$style} else () - , - if ($columns) then attribute colspan {$columns} else () - , - if ($rows) then attribute rowspan {$rows} else () - , - if ($is-brace) then - let $orientation := if ($node = '{') then 'open' else 'close' - return - {$node/string()} - else - render:recurse($node, $options) - } -}; - -declare function render:pb($node as element(tei:pb), $options) as item()* { - if ($options/*:param[@name = 'show-annotations']/@value = 'true') then - let $volume := $options/*:param[@name = 'volume']/@value - let $abs-site-uri := $options/*:param[@name = 'abs-site-uri']/@value - let $pagenumber := data($node/@n) - let $facs := data($node/@facs) - let $fruspageimagerelativepath := - if ($frusx:STATIC-FILE-LOCATION eq 'local') then '/historicaldocuments/' - else if ($frusx:STATIC-FILE-LOCATION eq 'hsg') then 'http://history.state.gov/historicaldocuments/' - else (: if ($frus:STATIC-FILE-LOCATION eq 's3') then :) - 'https://static.history.state.gov/frus/' - let $imagepath := - if ($frusx:STATIC-FILE-LOCATION = ('local', 'hsg')) then - concat($fruspageimagerelativepath, $volume, "/media/medium/") - else (: if ($frus:STATIC-FILE-LOCATION eq 's3') then :) - concat($fruspageimagerelativepath, $volume, "/medium/") - return - ( -
    , - render:xmlid($node, $options) - , - - { - if ($facs) then - element a { - attribute href {concat($imagepath, $facs, '.png')}, - attribute title {concat('Page ', $pagenumber)}, - (: attribute class {"thickbox"}, - attribute rel {"inline"}, :) - attribute style {'text-decoration:none'}, - , - concat('Page ', $pagenumber) - } - else concat('Page ', $pagenumber) - } - - , -
    - ) - else if ($node/ancestor::tei:table) then - () (: TODO add non--tag-based approach to giving an anchor - somehow get the @id to hang on the previous/next element in the table - otherwise, if a row's cells are broken up by , it resets column widths in browser :) - else - ( - (: drop the pb info if we're inside a list, since it throws epubcheck validation errors :) - if ($node/ancestor::tei:list) then () - else - {render:xmlid($node, $options)} - (: show page number info for EPUB: :) - (:, -
    - , - - {if ($node/@xml:id) then render:xmlid($node, $options) else ()} - [start of page {$node/@n/string()} in original print volume] - - , -
    :) - ) -}; - -declare function render:title($node as element(tei:title), $options) { - let $level := $node/@level - return - if ($level = ('s', 'm')) then - {render:recurse($node, $options)} - else if ($level eq 'a') then - ( - '“', - render:recurse($node, $options), - '”' - ) - else - {render:recurse($node, $options)} -}; - -declare function render:byline($node as element(tei:byline), $options) { -

    by {render:recurse($node, $options)}

    -}; - -declare function render:rendition-to-css($rendition as attribute()) { - let $rendition-ids := tokenize($rendition/string(), '\s+') ! substring-after(., '#') - let $rendition-definitions := - for $id in $rendition-ids - return - root($rendition)/id($id) - return - string-join($rendition-definitions, ' ') -}; - -declare function render:seg($node as element(tei:seg), $options) { - let $rendition := $node/@rendition - let $rend := $node/@rend - let $css := - string-join( - ( - if ($rendition) then render:rendition-to-css($rendition) else (), - if (contains($rend, ':')) then $rend else () - ), - '; ' - ) - return - (: avoid fancy floats in mobi, but try to keep spacing somewhat under control :) - if ($options/*:param[@name='ebook-format']/@value eq 'mobi' and $node/ancestor::tei:opener) then -

    {render:recurse($node, $options)}

    - else if ($css) then - {render:recurse($node, $options)} - else - {render:recurse($node, $options)} -}; - -declare function render:idno($node as element(tei:idno), $options) { - let $type := $node/@type - return - {render:recurse($node, $options)} -}; - -declare function render:lg($node as element(tei:lg), $options) { -
    {render:recurse($node, $options)}
    -}; - -declare function render:l($node as element(tei:l), $options) { -
    {render:recurse($node, $options)}
    -}; - -(: render:create-toc(): Some additional functions to create the TOC for use in left sidebars :) - -(: create the TOC for use by the left sidebar :) -declare function render:create-toc($tei-text, $web-path-to-page-view, $view, $id) as element() { -
    -
      {render:toc-passthru($tei-text, $web-path-to-page-view, $view, $id)}
    -
    -}; - -declare function render:toc-passthru($node, $web-path-to-page-view, $view, $id) { - for $node in $node/node() - return - render:toc-dispatch($node, $web-path-to-page-view, $view, $id) -}; - -(: the central recursive typeswitch function for handling TOCs :) -declare function render:toc-dispatch($node, $web-path-to-page-view, $view, $id) { - typeswitch($node) - case element(tei:div) return render:toc-div($node, $web-path-to-page-view, $view, $id) - case element(tei:head) return render:toc-head($node, $web-path-to-page-view, $view, $id) - default return render:toc-passthru($node, $web-path-to-page-view, $view, $id) -}; - -(: handles divs for TOCs :) -declare function render:toc-div($node as element(tei:div), $web-path-to-page-view, $view, $id) { - (: we only show divs that have @xml:id attributes :) - if ($node/@xml:id) then - (: check the $id to see if it was passed the 'show!first!div' parameter, - in which case we want to highlight the first div, so - we set $id to the value of the first div's @xml:id attribute :) - let $id := if ($id eq 'show!first!div') then ($node/ancestor::tei:text//tei:div[@xml:id])[1]/@xml:id else $id - (: highlight the div if it matches $id :) - let $highlight := if ($node/@xml:id eq $id or ($node/@xml:id eq 'foreword' and not($id) and $view ne 'about')) then attribute class {'highlight'} else () - return - (: handle funky milestones toc, aka 'accordion' toc :) - if (contains(util:collection-name($node), 'milestones')) then - (: milestones landing page - just show article titles :) - if ($view eq 'about') then - (: the article titles are the div nodes whose xml:id is 'foreword' :) - if ($node/@xml:id eq 'foreword') then -
  • - {$highlight} - {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} - -
  • - else - () - (: interior pages, showing the contents of the period defined in $view :) - else - let $period := replace(util:document-name($node), '.xml$', '') - let $article := - (: suppress 'foreward' from being appended to the URL:) - if ($node/@xml:id/string() eq 'foreword') then () - else $node/@xml:id/string() - return -
  • - {$highlight} - {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} - -
  • - - (: this is unused code for a collection-wide accordion TOC view, - instead of the single-doc accordion TOC view used in the milestones section :) - (: else - if ($node/parent::tei:front) then - (: override the highlight so it's just for the current tei doc, not all :) - let $highlight := if (not($id) and $view eq replace(util:document-name($node), '.xml$', '')) then attribute class {'highlight'} else () - return -
  • - {$highlight} - {render:toc-recurse($node, $web-path-to-page-view, $view, $id)} - - { - if ($view eq replace(util:document-name($node), '.xml$', '')) then -
      - {render:toc-recurse($node/ancestor::tei:text/tei:body, $web-path-to-page-view, $view, $id)} -
    - else () - } -
  • - (: if the div doesn't contain child divs, just show the single list item :) - else -
  • - {$highlight} - {render:toc-recurse($node, $web-path-to-page-view, $view, $id)} - -
  • - :) - - - (: the top level of our TOC should only contain the top level divs :) - else if (local-name($node/..) = ('front', 'body', 'back')) then - (: if the div contains child divs, nest them into a new list :) - if ($node/tei:div[@xml:id]) then -
  • - {$highlight} - {data($node/tei:head)} - - { - (: only show child items if the parent is selected :) - if ($node/@xml:id eq $id or $node/tei:div/@xml:id = $id) then -
      - {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} -
    - else ()} -
  • - (: if the div doesn't contain child divs, just show the single list item :) - else -
  • - {$highlight} - {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} - -
  • - (: show non-top level divs as leaf-level list items :) - else -
  • - {$highlight} - {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} - -
  • - (: don't show divs that don't have @xml:id attributes :) - else () -}; - -(: handles heads for TOCs :) -declare function render:toc-head($node as element(tei:head), $web-path-to-page-view, $view, $id) { - (: only handle heads whose parent is a div :) - if ($node/parent::tei:div) then - (: handle funky milestones toc, aka 'accordion' toc :) - if (contains(util:collection-name($node), 'milestones')) then - (: milestones landing page should only show the date :) - if ($view eq 'about') then - $node/tei:date/text() - (: milestones entries should show the full title :) - else - data($node) - (: don't bother showing the head "again" in the case where its parent div has already shown it :) - else if ($node/parent::tei:div/child::tei:div/@xml:id) then - () - (: special handling for 'buildings' article head :) - else if (contains(util:collection-name($node), 'buildings')) then - for $x in $node/node() - return - typeswitch($x) - case element(tei:lb) return ': ' - default return $x - else - render:recurse($node, ) - (: don't show heads whose parents aren't divs, e.g. graphics and figures :) - else () -}; - -declare function render:exist-match($node, $options) { - {render:recurse($node, $options)} -}; +xquery version "3.0"; + +(:~ This module uses XQuery 'typeswitch' to do all of the FRUS TEI-to-HTML + : conversion that we previously did with an XSLT stylesheet (frusteidoc2html.xsl). + : + and pass TEI fragments to render:render() as + render:render($teiFragment, $options) + : where $options contains parameters and other info in an element like: + + + + + + : + : Author: Joe Wicentowski + : Version: 1.0 (Mar 6, 2009) + :) + +module namespace render="http://history.state.gov/ns/xquery/tei-render"; + +import module namespace frusx = "http://history.state.gov/ns/xquery/frus" at "frus.xql"; +import module namespace console="http://exist-db.org/xquery/console"; +import module namespace functx = "http://www.functx.com"; + +(: default namespaces :) +declare default function namespace "http://www.w3.org/2005/xpath-functions"; +declare default element namespace "http://www.w3.org/1999/xhtml"; + +declare namespace tei="http://www.tei-c.org/ns/1.0"; +declare namespace xmldb="http://exist-db.org/xquery/xmldb"; +declare namespace html="http://www.w3.org/1999/xhtml"; +declare namespace frus="http://history.state.gov/frus/ns/1.0"; + +(: a helper function in case no options are passed to the function :) +declare function render:render($content as node()*) as element() { + render:render($content, ()) +}; + +(: creates a document div for fitting TEI into history.state.gov template :) +declare function render:render($content as node()*, $options as element()*) as element() { + let $body-options := {$options/*} + return +
    + { + render:recurse($content, $body-options), + render:note-end($content, $options) + } +
    +}; + +(: just recurses back to render:main() :) +(: DS: live code actually does various stuff here before it recurses :) +declare function render:recurse($content as node()*, $options) as item()* { + (: use recursion as an opportunity to insert space between adjacent elements that would otherwise be smooshed if indent=no :) + (: TODO add check for indent status :) + let $adjacent-nodes := ('hi', 'ref', 'persName', 'placeName', 'date', 'gloss', 'lb', 'del') + let $nodes := $content/node() + let $node-count := count($nodes) + for $node at $n in $nodes + return + ( + render:main($node, $options) + , + (: it helps if the adjacent nodes have already had trailing spaces before close tag removed, e.g., find \s+ replace with :) + if ($n < $node-count) then + if ($node instance of element() and $node/name() = $adjacent-nodes and $nodes[$n + 1] instance of element() and $nodes[$n + 1]/name() = $adjacent-nodes) then ' ' (:' ':) else () + else () + ) +}; + +(: main routine :) +declare function render:main($nodes as node()*, $options) as item()* { + for $node in $nodes + return + ( + (:console:log(util:node-id($node)),:) + typeswitch($node) + case text() return $node + case element(tei:TEI) return render:TEI($node, $options) + case element(tei:text) return render:text($node, $options) + case element(tei:front) return render:front($node, $options) + case element(tei:body) return render:body($node, $options) + case element(tei:back) return render:back($node, $options) + case element(tei:div) return render:div($node, $options) + case element(frus:attachment) return render:frus-attachment($node, $options) + case element(tei:head) return render:head($node, $options) + case element(tei:p) return render:p($node, $options) + case element(tei:q) return render:q($node, $options) + case element(tei:quote) return render:quote($node, $options) + case element(tei:hi) return render:hi($node, $options) + case element(tei:del) return render:del($node, $options) + case element(tei:list) return render:list($node, $options) + case element(tei:item) return render:item($node, $options) + case element(tei:label) return render:label($node, $options) + case element(tei:postscript) return render:postscript($node, $options) + case element(tei:ref) return render:ref($node, $options) + case element(tei:note) return render:note($node, $options) + case element(tei:dateline) return render:dateline($node, $options) + case element(tei:date) return render:date($node, $options) + case element(tei:time) return render:time($node, $options) + case element(tei:persName) return render:persName($node, $options) + case element(tei:gloss) return render:gloss($node, $options) + case element(tei:placeName) return render:placeName($node, $options) + case element(tei:orgName) return render:orgName($node, $options) + case element(tei:term) return render:term($node, $options) + case element(tei:opener) return render:opener($node, $options) + case element(tei:salute) return render:salute($node, $options) + case element(tei:closer) return render:closer($node, $options) + case element(tei:signed) return render:signed($node, $options) + case element(tei:listBibl) return render:listBibl($node, $options) + case element(tei:bibl) return render:bibl($node, $options) + case element(tei:said) return render:said($node, $options) + case element(tei:listPerson) return render:listPerson($node, $options) + case element(tei:lb) return render:lb($node, $options) + case element(tei:milestone) return render:milestone($node, $options) + case element(tei:anchor) return render:anchor($node, $options) + case element(tei:figure) return render:figure($node, $options) + case element(tei:graphic) return render:graphic($node, $options) + case element(tei:table) return render:table($node, $options) + case element(tei:row) return render:row($node, $options) + case element(tei:cell) return render:cell($node, $options) + case element(tei:geo) return () + case element(tei:pb) return render:pb($node, $options) + case element(tei:title) return render:title($node, $options) + case element(tei:byline) return render:byline($node, $options) + case element(tei:seg) return render:seg($node, $options) + case element(tei:idno) return render:idno($node, $options) + case element(tei:lg) return render:lg($node, $options) + case element(tei:l) return render:l($node, $options) + + case element(html:colgroup) return $node + + default return render:recurse($node, $options) + ) +}; + +declare function render:TEI($node as element(tei:TEI), $options) as element() { +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    +}; + +declare function render:text($node as element(tei:text), $options) as element() { +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    +}; + +declare function render:front($node as element(tei:front), $options) as element() { +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    +}; + +declare function render:body($node as element(tei:body), $options) as element() { +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    +}; + +declare function render:back($node as element(tei:back), $options) as element() { +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    +}; + +declare function render:div($node as element(tei:div), $options) { + if ($node/@type = 'theme-highlight') then +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    + else +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    +}; + +declare function render:frus-attachment($node as element(frus:attachment), $options) { +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    +}; + +declare function render:head($node as element(tei:head), $options) as element()* { + if ($node/@type = ('shortened-for-running-head')) then + () + else if ($node/parent::tei:div or $node/parent::frus:attachment) then + let $rendition := $node/@rendition + let $style := if ($rendition) then attribute style {render:rendition-to-css($rendition)} else () + let $type := $node/parent::tei:div/@type + return + if ($type = ('section', 'section-pending', 'appendix', 'compilation', 'compilation-pending', 'part') ) then + if ($type eq 'section' and $options/*:param[@name='suppress-head-if-first-div']/@value eq 'true') then () + else +

    {$style, render:recurse($node, $options)}

    + else if ($type = ('document', 'document-pending', 'subchapter', 'chapter', 'chapter-pending', 'chapter-introduction', 'part') ) then + ( + (: show a bracketed document number for volumes that don't use document numbers :) + if ($type = 'document' and not(starts-with($node, concat($node/parent::tei:div/@n, '.')) or starts-with($node, concat('No. ', $node/parent::tei:div/@n)))) then +

    { + if (matches($node/parent::tei:div/@n, '^\[.+?\]$')) then + $node/parent::tei:div/@n/string() + else + concat('[Document ', $node/parent::tei:div/@n, ']') + }

    + else (), +

    {$style, render:recurse($node, $options)}

    + ) + else if ($type = 'timeline') then + {render:recurse($node, $options)} + else if ($node/ancestor::tei:div/@xml:id) then + element {concat('h', index-of($node/ancestor::tei:div, $node/ancestor::tei:div[@xml:id][1]) + 2)} {$style, render:recurse($node, $options)} + else +

    {$style, render:recurse($node, $options)}

    + else if ($node/parent::tei:figure) then + if ($node/parent::tei:figure/parent::tei:p) then + {render:recurse($node, $options)} + else (: if ($node/parent::tei:figure/parent::tei:div) then :) +

    {render:recurse($node, $options)}

    + else if ($node/parent::tei:list) then + if ($node/parent::tei:list/@type = ('participants', 'subject', 'from', 'references', 'to') ) then + if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then +

    {render:recurse($node, $options)}

    + else +
  • {render:recurse($node, $options)}
  • + else if ($node/ancestor::tei:list/@type = ('participants', 'subject', 'from', 'references', 'to') ) then + if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then +
    {render:recurse($node, $options)}
    + else +
  • {render:recurse($node, $options)}
  • + else + {render:recurse($node, $options)} + else if ($node/parent::tei:table) then +

    {render:recurse($node, $options)}

    + else + {render:recurse($node, $options)} +}; + +(: TODO: Look for instances in the XML of the weird old 'participantcol129' :) +declare function render:p($node as element(tei:p), $options) as item()* { + let $rend := $node/@rend + let $body-mode-for-footnotes := $options/*:param[@name='body-mode-for-footnotes']/@value + let $result := + if ($rend) then + if ($rend = ( + 'subjectentry', 'sectiontitleital', 'sectiontitlebold', 'subjectallcaps', + 'sourceheadcenterboldbig', 'sourcearchiveboldbig', 'sourceparagraphspaceafter', + 'sourceparagraphtightspacing', 'sourceparagraphfullindent', + 'flushleft', 'right', 'center', 'strong', 'centerstrong') + ) then +

    { attribute class {data($rend)} }{ render:recurse($node, $options) }

    + else if ($rend = 'italic') then +

    {render:recurse($node, $options)}

    + else if ($rend = 'underline') then +

    {render:recurse($node, $options)}

    + else +

    {render:recurse($node, $options)}

    + (: TODO: Try to handle multi-paragraph footnotes for Forrest + else if ($node/parent::tei:note and not($node/preceding-sibling::tei:p)) then + render:recurse($node, $options) + :) + else +

    {render:recurse($node, $options)}

    + return + + (: check if we're inside the body version of a footnote, rather than the render:note-end() version. + if so, collapse the block

    into an inline to prevent artifacts of the note from appearing in + the surrounding paragraph text :) + + if ($body-mode-for-footnotes = 'true' and $node/ancestor::tei:note[1][not(@rend = 'inline')]) then + if ($node/preceding-sibling::element()) then + (
    ,
    , {$result/@*, $result/node()}) + else + $result/node() + + (: if we're just a normal paragraph, then carry on :) + + else + $result + +}; + +declare function render:q($node as element(tei:q), $options) as element()* { + let $rend := $node/@rend + return + if ($rend = 'blockquote') then + if ($node/tei:p) then +

    {render:recurse($node, $options)}
    + else +

    {render:recurse($node, $options)}

    + else if ($node/parent::tei:q) then + ‘{render:recurse($node, $options)}’ + else + “{render:recurse($node, $options)}” +}; + +declare function render:quote($node as element(tei:quote), $options) as element()* { + let $rend := $node/@rend + return + (: if ($rend = 'blockquote') then :) + if ($node/tei:p or $node/tei:lg) then +
    {render:recurse($node, $options)}
    + else +

    {render:recurse($node, $options)}

    + (: else + {render:recurse($node, $options)} :) +}; +(: known types: italic, strong :) +declare function render:hi($node as element(tei:hi), $options) as element() { + let $rend := $node/@rend + return + if ($rend = 'italic') then + if ($node/ancestor::tei:signed) then + {render:recurse($node, $options)} + else + {render:recurse($node, $options)} + else if ($rend = 'strong') then + {render:recurse($node, $options)} + else if ($rend = 'sub') then + {render:recurse($node, $options)} + else if ($rend = 'superscript') then + {render:recurse($node, $options)} + else if ($rend = 'underline') then + {render:recurse($node, $options)} + else if ($rend = 'smallcaps') then + {render:recurse($node, $options)} + else if ($rend = 'roman') then + {render:recurse($node, $options)} + else + {render:recurse($node, $options)} +}; + +declare function render:del($node as element(tei:del), $options) as element() { + let $rend := $node/@rend + return + if ($rend = 'strikethrough') then + {render:recurse($node, $options)} + else + {render:recurse($node, $options)} +}; + +declare function render:list($node as element(tei:list), $options) as item()+ { + if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then + render:recurse($node, $options) + else + let $type := $node/@type + let $rend := $node/@rend + (: when showing inline footnotes, we have to force block elements like item to be inline :) + let $force-block-inline := $options/*:param[@name='force-block-inline']/@value + return + if ($force-block-inline) then + (
    , render:recurse($node, $options),
    ) + else if ($type = ('participants', 'subject', 'from', 'to', 'references', 'simple') ) then +
      {render:recurse($node, $options)}
    + else if ($type = 'index') then +
      {render:recurse($node, $options)}
    + else if ($type = 'indexentry') then +
      {render:recurse($node, $options)}
    + else if ($type = 'ordered') then (: TODO fix list/label and list/item :) +
    {render:recurse($node, $options)}
    + else if ($rend = 'bulleted') then +
      {render:recurse($node, $options)}
    + else if ($node/tei:head) then + ( + if ($node/tei:head) then render:head($node/tei:head, $options) else () + , +
      {for $item in $node/tei:head/following-sibling::* return render:main($item, $options)}
    + ) + else +
      { + if ($node/ancestor::tei:list or $node/ancestor::tei:table/@rend='schedule') then () else attribute class {"hanging-indent"}, + render:recurse($node, $options) + }
    +}; + +declare function render:item($node as element(tei:item), $options) as element()+ { + if ($options/*:param[@name='ebook-format']/@value = 'mobi') then +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + if ($node/preceding-sibling::tei:label) then + ( + render:recurse($node/preceding-sibling::tei:label[1], $options), + ' ', + render:recurse($node, $options) + ) + else + render:recurse($node, $options) + }
    + else + let $force-block-inline := $options/*:param[@name='force-block-inline']/@value + return + if ($force-block-inline) then + ( + { + if ($node/parent::tei:list/@rend='bulleted') then + '• ' + else if ($node/preceding-sibling::*[1]/self::tei:label) then + concat($node/preceding-sibling::*[1]/self::tei:label, ' ') + else + () + , + render:recurse($node, $options) + }, +
    + ) + else if ($node/preceding-sibling::tei:label) then +
  • { + render:recurse($node/preceding-sibling::tei:label[1], $options), ' ', render:recurse($node, $options) + }
  • + else if ($node/parent::tei:list/@type eq 'subject' and $node/parent::tei:list/@rend eq 'flushleft') then + (: handles flush left subject lines in 1952-54 volumes, TODO - test when receive first volume :) +
  • { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + if ($node/preceding-sibling::tei:label) then + ( + render:recurse($node/preceding-sibling::tei:label[1], $options), + ' ', + render:recurse($node, $options) + ) + else + render:recurse($node, $options) + }
  • + else +
  • { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + if ($node/preceding-sibling::element()[1]/self::tei:label) then + ( + render:recurse($node/preceding-sibling::tei:label[1], $options), + ' ', + render:recurse($node, $options) + ) + else + render:recurse($node, $options) + }
  • +}; + +declare function render:label($node as element(tei:label), $options) as item()* { + if ($node/parent::tei:list) then + () + else + {render:recurse($node, $options)} +}; + +declare function render:postscript($node as element(tei:postscript), $options) as element()+ { +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + if ($node/node()[1]/self::tei:label and $node/node()[2]/self::tei:p) then + let $new-node := + ( + element tei:p { $node/tei:label[1], ' ', $node/tei:p[1]/node() } + , + $node/tei:p[1]/following-sibling::node() + ) + return + render:main($new-node, $options) + else + render:recurse($node, $options) + }
    +}; + +declare function render:xmlid($node as element(), $options) as element() { + +}; + +declare function local:index-of($seq as node()*, $n as node()) as xs:integer* { + local:index-of($seq, $n, 1) +}; + +declare function local:index-of($seq as node()*, $n as node(), $i as xs:integer) as xs:integer* { + if ( empty($seq) ) then + () + else if ( $seq[1] is $n ) then + ( $i, local:index-of(remove($seq, 1), $n, $i + 1) ) + else + local:index-of(remove($seq, 1), $n, $i + 1) +}; + +(: TODO Add handling for :) +declare function render:note($node as element(tei:note), $options ) as item()* { + let $suppress-note := $options/*:param[@name='suppress-note']/@value + let $rendition := $node/@rendition + let $css := if ($rendition) then render:rendition-to-css($rendition) else () + return + if ($suppress-note eq 'true') then + () + else + let $div := $node/ancestor::tei:div[1]/@xml:id + return + if (empty($node) or $node = '') then + + {data($node/@n)} + + else if ($node/@rend='inline') then + (: display inline notes inline :) +

    { + if ($css) then attribute style { $css } else (), + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }

    + else if ($node/@type='summary') then + (: suppress ePub summary notes from being displayed :) + () + else if ($node/@n = '0') then +
    + Source + {''(: NOTE removed for EPUB + + {render:recurse($node, element {QName('', 'parameters')} {$options/*, element param {attribute name {'strip-links'}, attribute value {'true'}}})} + + :)} + + else if (not($node/@xml:id) and $node/@target) then + (: handle case of multiple references to the same footnote - TODO generate correct @href :) + + + {data($node/@n)} + + + else + let $incr := + xs:integer(local:index-of($node/ancestor::tei:div[1]//tei:note[@n], $node)) + let $incr := + if ($node/preceding::tei:note[@n = '0']) then $incr - 1 + else $incr + return + {data($node/@n)}{'' + (: NOTE removed for EPUB + + {data($node/@n)}. {data($node) (: TODO find a way to use render:recurse() + that doesn't make the CSS hiccup on span/a/em. Until then we lose styling on + inline footnotes :)} + + :)} + , ' ' (: this trailing space is needed until whitespace issues are fully dealt with :) +}; + +declare function render:note-end($content, $options) as element()* { + if (exists($content//tei:note[@n])) then + ( +
    , +
    + { + for $note at $incr in $content//tei:note[@n] + let $div := $note/ancestor::tei:div[1]/@xml:id + return + if ($note/@type = 'summary' or empty($note) or $note = '') then + (: suppress ePub summary notes from being displayed :) + () + else +
    { + let $return-link := + ( + if ($note/@n = '0') then + + * + + else + let $incr := if ($note/preceding::tei:note[@n = '0']) then $incr - 1 else $incr + return + + + {data($note/@n)} + + + , +   + ) + let $content-nodes := render:recurse($note, $options) + return + (: if the 1st child node of the note is a block-level element (e.g., p), we'll get an + unwanted space between the footnote number and the beginning of the text. so we check + for the first child being an element, and if so, tuck the footnote number inside. + TODO: refine this check to operate only on block-level elements. it's currently operating + even on phrase-level elements (e.g., em) :) + if ($content-nodes[1] instance of element()) then + for $content-node at $count in $content-nodes + return + if ($count = 1 and $content-node instance of element()) then + element {$content-node/name()} {$content-node/@*, $return-link, $content-node/node()} + else if ($count = 1) then + ($return-link, $content-node) + else + $content-node + else + ($return-link, $content-nodes) + }
    + } +
    + ) + else () +}; + +declare function render:ref($node as element(tei:ref), $options) as item()* { + let $target := $node/@target + let $volume := $options/*:param[@name = 'volume']/@value + let $abs-site-uri := $options/*:param[@name = 'abs-site-uri']/@value + let $relativeimagepath := $options/*:param[@name = 'relativeimagepath']/@value + let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' + let $strip-links := $options/*:param[@name = 'strip-links']/@value + let $persistent-view := if ($show-annotations) then '?view=annotations' else () + (:let $log := console:log(serialize($node) || ' from ' || $node/ancestor::tei:div[1]/@xml:id):) + let $type := + (: added to support class='mini-doc' for theme doc links :) + if ($node/@type) then attribute class { $node/@type } else () + return + (: catch refs without text :) + if ($node eq '') then + let $newnode := element tei:ref { attribute target {$node/@target}, data($node/@target) } + return + render:ref($newnode, $options) + (: route external links through disclaimer :) + else if (starts-with($target, 'http')) then + (: is it in state.gov domain? :) + (:if (matches($target, '^https?://[^.]*?.state.gov')) then:) + element a { + attribute href { $target }, + attribute title { $target }, + $type, + render:recurse($node, $options) + } + (: otherwise show disclaimer :) + (:else + element a { + attribute href { concat('/redirect?url=', xmldb:encode($target)) }, + attribute title { $target }, + $type, + render:recurse($node, $options) + }:) + (: ref to a target in the same volume by the object's @xml:id :) + else if (starts-with($target, '#')) then + (: don't let bad links through, but let range links through, which need to be parsed further :) + if (not(root($node)/id(substring-after($target, '#')) or starts-with($target, '#range'))) then + render:recurse($node, $options) + else + (: cross-ref to a target in the index :) + if (starts-with($target, '#in')) then + element a { + attribute href { concat('index.html', $target) }, + $type, + render:recurse($node, $options) + } + (: ref to a document :) + else if (matches($target, '^#d\d+$')) then + element a { + attribute href { concat( substring-after($target, '#'), '.html') }, + $type, + render:recurse($node, $options) + } + (: ref to a portion of a document, e.g., footnote :) + else if (matches($target, '^#d\d+.+')) then + element a { + attribute href { concat( replace($target, '^#(d\d+)(.+)$', '$1'), '.html', $target) }, + $type, + render:recurse($node, $options) + } + (: ref to a portion of a chapter, e.g., footnote :) + else if (matches($target, '^#chapter-\d+.*$')) then + element a { + attribute href { + if (contains($target, 'fn')) then + let $ch := substring-before(substring-after($target, '#'), 'fn') + let $fn := substring-after($target, 'fn') + return + concat($ch, '.html#fn', $fn) + else + replace($target, '^#(chapter-\d+)(.*)$', '$1.html') + }, + $type, + render:recurse($node, $options) + } + (: ref to a portion of a chapter, e.g., footnote :) + else if (matches($target, '^#appendix-[a-z].*$')) then + element a { + attribute href { + if (contains($target, 'fn')) then + let $ch := substring-before(substring-after($target, '#'), 'fn') + let $fn := substring-after($target, 'fn') + return + concat($ch, '.html#', $ch, 'fn', $fn) + else + replace($target, '^#(appendix-[a-z])(.*)$', '$1.html') + }, + $type, + render:recurse($node, $options) + } + (: ref to a portion of a conclusion, e.g., footnote :) + else if (matches($target, '^#conclusion.*$')) then + element a { + attribute href { + if (contains($target, 'fn')) then + let $ch := substring-before(substring-after($target, '#'), 'fn') + let $fn := substring-after($target, 'fn') + return + concat($ch, '.html#fn', $fn) + else + replace($target, '^#(conclusion)(.*)$', '$1.html') + }, + $type, + render:recurse($node, $options) + } + (: ref to a portion of a introduction, e.g., footnote :) + else if (matches($target, '^#introduction[a-z].*$')) then + element a { + attribute href { + if (contains($target, 'fn')) then + let $ch := substring-before(substring-after($target, '#'), 'fn') + let $fn := substring-after($target, 'fn') + return + concat($ch, '.html#fn', $fn) + else + replace($target, '^#(introduction)(.*)$', '$1.html') + }, + $type, + render:recurse($node, $options) + } + (: Turn page-based links into document-based links :) + else if (starts-with($target, '#pg')) then + if ( + let $first-node := $node/preceding-sibling::node()[1] + let $second-node := $node/preceding-sibling::node()[2] + return + $first-node eq '–' and $second-node instance of element(tei:ref) and $second-node[starts-with(@target, '#pg')] + ) + then + ( + (:util:log-system-out(concat('pb rule 1: ', $node/@target)),:) + render:recurse($node, $options), + ' ', + render:pb-range-to-document-links(root($node), substring-after($node/preceding-sibling::tei:ref[1]/@target, '#'), substring-after($target, '#')) + ) + else if (subsequence($node/following-sibling::node(), 1, 1) eq '–' and subsequence($node/following-sibling::node(), 2, 1)/./self::tei:ref[starts-with(@target, '#pg')]) then + ( + (:util:log-system-out(concat('pb rule 2: ', $node/@target)),:) + render:recurse($node, $options) + ) + else + ( + (:util:log-system-out(concat('pb rule 3: ', $node/@target)),:) + render:recurse($node, $options), + ' ', + render:pb-to-document-links(root($node), substring-after($target, '#')) + ) + (: handle xpointer-style range references, as found in the frus-history, e.g., + index entries like: + Washington, George, 9–10 + point to: + + and: + + :) + else if (starts-with($target, '#range')) then + let $range := substring-after($target, '(') + let $range := substring-before($range, ')') + let $range := tokenize($range, ',') + let $range-start := $range[1] + let $range-end := $range[2] + let $target-start-node := root($node)/id($range-start) + let $target-end-node := root($node)/id($range-end) + (: use ancestor notes to ensure linkability :) + let $target-start-node := if ($target-start-node/ancestor::tei:note) then $target-start-node/ancestor::tei:note else $target-start-node + let $target-end-node := if ($target-end-node/ancestor::tei:note) then $target-end-node/ancestor::tei:note else $target-end-node + let $target-start-node-ancestor-div := $target-start-node/ancestor::tei:div[1] + let $target-end-node-ancestor-div := $target-end-node/ancestor::tei:div[1] + let $same-ancestor-divs := $target-start-node-ancestor-div = $target-end-node-ancestor-div + (: use the ancestor chapter div's heading, e.g., "Chapter 9: ...", but chop off at the colon :) + let $target-nodes := ($target-start-node, $target-end-node) + let $target-divs := ($target-start-node-ancestor-div, $target-end-node-ancestor-div) + let $target-node-labels := + let $both-notes := $target-nodes[1]/self::tei:note and $target-nodes[2]/self::tei:note + let $one-note := $target-nodes[1]/self::tei:note or $target-nodes[2]/self::tei:note + for $target-node at $n in $target-nodes + let $ancestor-div-label := + if ($same-ancestor-divs and $n = 2) then + () + else + string-join(functx:remove-elements-deep($target-divs[$n]/tei:head[1], 'note'), '') + let $ancestor-div-label := + if (contains($ancestor-div-label, ':')) then substring-before($ancestor-div-label, ':') else $ancestor-div-label + let $node-label := + if ($target-node/self::tei:note) then + concat(if ($n = 1 and $both-notes) then 'footnotes ' else 'footnote ', $target-node/@n) + else + (: paragraph-like-block-number :) + concat(if ($one-note) then 'para ' else if ($n = 1) then 'paras ' else '', index-of($target-start-node-ancestor-div/*[not(self::tei:head)][not(self::tei:byline)][not(self::tei:p[@rend='sectiontitlebold'])], $target-node/ancestor::element()[parent::tei:div][1])) + return + string-join(($ancestor-div-label, $node-label), ' ') + let $label := + replace(string-join($target-node-labels, '–'), 'Chapter', 'Ch.') + let $target-node-destination-hash := + if ($target-start-node/self::tei:note) then + concat('#fnref', substring-after($target-start-node/@xml:id, 'fn')) + else + concat('#', $range-start) + return + (: check to make sure the targets exist :) + if ($target-start-node and $target-end-node) then + element a { + attribute href { concat($target-start-node-ancestor-div/@xml:id, '.html', $target-node-destination-hash) }, + $label + } + (: display the label in case of malformed links :) + else + $label + (: handle single point references, as found in the frus-history, e.g., + index entries like: + Woodford, Stewart, 98 + point to: + + :) + else if (starts-with($target, '#b')) then + let $url := substring-after($target, '#') + let $target-node := root($node)/id($url) + let $target-node := if ($target-node/ancestor::tei:note) then $target-node/ancestor::tei:note else $target-node + let $destination-div := $target-node/ancestor::tei:div[1] + (: use the ancestor chapter div's heading, e.g., "Chapter 9: ...", but chop off at the colon :) + let $head := string-join(functx:remove-elements-deep($destination-div/tei:head[1], 'note'), '') + let $target-node-label := + if ($target-node/self::tei:note) then + concat('footnote ', $target-node/@n) + else + concat('para ', index-of($destination-div/*[not(self::tei:head)][not(self::tei:byline)][not(self::tei:p[@rend='sectiontitlebold'])], $target-node/ancestor::element()[parent::tei:div][1])) + let $label := replace(concat(if (contains($head, ':')) then substring-before($head, ':') else $head, ' ', $target-node-label), 'Chapter', 'Ch.') + let $target-node-destination-hash := + if ($target-node/self::tei:note) then + concat('#fnref', substring-after($target-node/@xml:id, 'fn')) + else + $target + return + if ($target-node) then + element a { + attribute href { concat($destination-div/@xml:id, '.html', $target-node-destination-hash) }, + $label + } + (: display the label in case of malformed links :) + else + $label + (: ref to an appendix :) + else + element a { + attribute href { concat( substring-after($target, '#'), '.html' ) }, + $type, + render:recurse($node, $options) + } + (: ref to a footnote in another volume :) + else if (contains($target, '#') and contains($target, 'fn')) then + element a { + attribute href { concat('http://history.state.gov/', $abs-site-uri, substring-before($target, '#'), '/', concat(substring-before(substring-after($target, '#'), 'fn'), '#fn', substring-after($target, 'fn')), $persistent-view) }, + $type, + render:recurse($node, $options) + } + (: ref to a subsection of another volume :) + else if (contains($target, '#')) then + element a { + attribute href { concat('http://history.state.gov/', $abs-site-uri, substring-before($target, '#'), '/', substring-after($target, '#')) }, + $type, + render:recurse($node, $options) + } + (: just a ref to another volume :) + else if (starts-with($target, 'frus')) then + element a { + attribute href { concat('http://history.state.gov/', $abs-site-uri, $target) }, + $type, + render:recurse($node, $options) + } + (: most likely a ref to another section of the website :) + else + element a { + attribute href { $target }, + $type, + render:recurse($node, $options) + } +}; + + +declare function render:pb-to-document-links($vol as document-node(), $pb-id as xs:string) { + render:pb-range-to-document-links($vol, $pb-id, $pb-id) +}; + +declare function render:pb-range-to-document-links($vol as document-node(), $pb1-id as xs:string, $pb2-id as xs:string) { + let $pb1 := $vol/id($pb1-id) + let $pb2 := $vol/id($pb2-id) + let $range-start := $pb1 + let $range-end := subsequence($pb2/following::tei:pb, 1, 1) + let $divs := $vol//tei:div[@type=('document', 'section') and @xml:id] + let $divs-within-range := $divs[. >> $range-start and . << $range-end]/@xml:id + let $ancestor-document := subsequence($range-start/ancestor::tei:div[@type=('document', 'section') and @xml:id], 1, 1)/@xml:id + let $doc-ids := distinct-values(($ancestor-document, $divs-within-range)[. ne '']) + (: allow for possibility of links to non-document portions of a volume :) + let $doc-ids := if (empty($doc-ids)) then ($pb1/ancestor::tei:div[@xml:id][1]/@xml:id, $pb1/following::tei:div[@xml:id][1]/@xml:id)[1] else $doc-ids + (: let $log := console:log(concat($pb1-id, '-', $pb2-id, ': ', string-join($doc-ids))) :) + let $docs-in-frag := for $doc-id in $doc-ids return $vol/id($doc-id) + let $link := + [{ + let $docs-count := count($docs-in-frag) + return + if ($docs-count = 1) then + ( + if ($pb1 = $pb2) then + concat('Pg. ', $pb1/@n, ' is part of ') + else + concat('Pgs. ', $pb1/@n, '–', $pb2/@n, ' are part of ') + , + { + for $doc in $docs-in-frag + return + if ($doc/@type='document') then + concat('Doc. ', $doc/@n) + else + frusx:head-sans-note($doc) + } + ) + else + (: may need to account for this :) + ( + if ($pb1-id = $pb2-id) then + concat('Pg. ', $pb1/@n, ' includes portions of ') + else + concat('Pgs. ', $pb1/@n, '–', $pb2/@n, ' include portions of ') + , + for $doc at $count in $docs-in-frag + return + ( + {if ($doc/@n ne '') then concat('Doc. ', $doc/@n) else frusx:head-sans-note($doc)}, + if ($count lt $docs-count - 1 and $docs-count gt 2) then + ', ' + else if ($count lt $docs-count) then + if ($docs-count = 2) then + ' and ' + else + ', and ' + else () + ) + ) + }] + return + $link +}; + +declare function render:dateline($node as element(tei:dateline), $options) as element() { + let $rendition := $node/@rendition + let $css := if ($rendition) then render:rendition-to-css($rendition) else () + return + if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then + + else if ($css) then + {render:recurse($node, $options)} + else + +}; + +declare function render:date($node as element(tei:date), $options) as item()* { + let $strip-links := $options/*:param[@name = 'strip-links']/@value + let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' + return + + if ($show-annotations) then + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #496690"} + , + render:recurse($node, $options) + , + [{if ($node/@*) then string-join(for $att in $node/@* return concat('@', name($att), ': ', $att), ', ') else 'no @!!!'}] + ) + } + + else + render:recurse($node, $options) +}; + +declare function render:time($node as element(tei:time), $options) as item() { + let $strip-links := $options/*:param[@name = 'strip-links']/@value + let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' + return + + if ($show-annotations) then + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #496690"} + , + render:recurse($node, $options) + , + [{if ($node/@*) then string-join(for $att in $node/@* return concat('@', name($att), ': ', $att), ', ') else 'no @!!!'}] + ) + } + + else + render:recurse($node, $options) +}; + +declare function render:persName($node as element(tei:persName), $options) as item()+ { + let $strip-links := $options/*:param[@name = 'strip-links']/@value + let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' + return + + ( + if ($node/@xml:id) then render:xmlid($node, $options) else (), + if ($show-annotations) then + let $person-id := substring-after($node/@corresp, '#') + let $person-entry := root($node)/id($person-id) + let $name := string($person-entry) + let $entry := render:main($person-entry/ancestor::tei:item[1]/tei:hi[1]/following-sibling::node(), ()) + return + if ($person-id) then + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #911625"} + , + render:recurse($node, $options) + , + {$name}{$entry} + ) + } + + else if ($node/@xml:id) then + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #911625"} + , + render:recurse($node, $options) + , + [@xml:id: {$node/@xml:id/string()}] + ) + } + + else + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #911625"} + , + render:recurse($node, $options) + , + [no ID!!!] + ) + } + + else + render:recurse($node, $options) + ) +}; + +declare function render:gloss($node as element(tei:gloss), $options) as item()+ { + let $strip-links := $options/*:param[@name = 'strip-links']/@value + let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' + return + + if ($show-annotations) then + let $term-id := substring-after($node/@target, '#') + let $term-entry := root($node)/id($term-id) + let $term := string($term-entry) + let $entry := string($term-entry/ancestor::tei:item[1]/tei:hi[1]/following-sibling::node()) + return + if ($term-id) then + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #496690"} + , + render:recurse($node, $options) + , + {$term}{$entry} + ) + } + + else + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #496690"} + , + render:recurse($node, $options) + , + [no ID!!!] + ) + } + + else + render:recurse($node, $options) +}; + +declare function render:orgName($node as element(tei:orgName), $options) as item()+ { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) +}; + +declare function render:placeName($node as element(tei:placeName), $options) as item()+ { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) +}; + +declare function render:term($node as element(tei:term), $options) as item()+ { + let $strip-links := $options/*:param[@name = 'strip-links']/@value + let $show-annotations := $options/*:param[@name = 'show-annotations']/@value = 'true' + return + + ( + if ($node/@xml:id) then render:xmlid($node, $options) else (), + if ($show-annotations) then + if ($node/@xml:id) then + + { + if ($strip-links) then + render:recurse($node, $options) + else + ( + attribute style {"font-weight: bold; color: #496690"} + , + render:recurse($node, $options) + , + [@xml:id: {$node/@xml:id/string()}] + ) + } + + else () + else + render:recurse($node, $options) + ) +}; + +declare function render:opener($node as element(tei:opener), $options) as element()+ { + (: mobi doesn't use floats and so doesn't need the same extra spacing :) + if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then +
    {render:recurse($node, $options)}
    + else + ( +
    {render:recurse($node, $options)}
    + , +
    + ) +}; + +declare function render:salute($node as element(tei:salute), $options) as element() { +

    {render:recurse($node, $options)}

    +}; + +declare function render:closer($node as element(tei:closer), $options) as element() { +

    {render:recurse($node, $options)}

    +}; + +declare function render:signed($node as element(tei:signed), $options) as element() { + {render:recurse($node, $options)} +}; + +declare function render:listBibl($node as element(tei:listBibl), $options) as item()+ { + if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then + render:recurse($node, $options) + else +
      {render:recurse($node, $options)}
    +}; + +declare function render:bibl($node as element(tei:bibl), $options) as element() { + if ($options/*:param[@name='ebook-format']/@value = 'mobi') then +
    { + if ($node/@xml:id) then render:xmlid($node, $options) else (), + render:recurse($node, $options) + }
    + else +
  • {render:recurse($node, $options)}
  • +}; + +declare function render:said($node as element(tei:said), $options) as element() { +

    {data($node/@who)}: {render:recurse($node, $options)}

    +}; + +declare function render:lb($node as element(tei:lb), $options) as item()* { + let $strip-lbs := $options/*:param[@name = 'strip-line-breaks']/@value + return + if ($strip-lbs = 'true') then ' ' else
    +}; + +declare function render:listPerson($node as element(tei:listPerson), $options) as element() { + let $type := $node/@type + return + if ($node/tei:person) then +
      {render:recurse($node, $options)}
    + else +
    • {data($type)}: {render:recurse($node, $options)}
    +}; + +declare function render:person($node as element(tei:person), $options) as element() { +
  • {render:recurse($node, $options)}
  • +}; + +declare function render:milestone($node, $options) as element() { + let $class := $node/@rend + return + if ($node/@rend eq 'hr') then +
    + else if ($node/@rend eq 'centered-asterisks') then +

    * * *

    + else +
    +}; + +declare function render:anchor($node, $options) as element()* { + if ($node/ancestor::tei:note) then + () + else + render:xmlid($node, $options) +}; + +declare function render:figure($node as element(tei:figure), $options) { + let $class := if ($node/@rend = 'smallfloatinline') then 'image-smallfloatinline' else 'image-wide' + return + if ($node/parent::tei:p) then + {render:recurse($node, $options)} + else + ( + (: insert a 'page-break-before' div immediately before image #2+ in the appendix, + to ensure caption has a chance of staying on the same page image in ebook :) + if ($node/ancestor::tei:div/@xml:id = 'appendix' and $node/preceding-sibling::tei:figure) then +
    + else + () + , +
    {render:recurse($node, $options)}
    + ) +}; + +declare function render:graphic($node as element(tei:graphic), $options) as node()+ { + let $url := $node/@url + let $head := $node/following-sibling::tei:head + let $relativeimagepath := $options/*:param[@name = 'relativeimagepath']/@value + return + ( + {normalize-space($head)}, + render:recurse($node, $options) + ) +}; + +declare function render:table($node as element(tei:table), $options) as element() { + let $rend := $node/@rend + let $rendition := $node/@rendition + let $style := + string-join( + ( + if ($rend="bordered") then + if ($options/*:param[@name='ebook-format']/@value eq 'mobi') then + attribute border {'1'} + else + attribute style {'border: 1px solid #606060; border-collapse: collapse'} + else (), + if (matches($rend, '^width:')) then + $rend + else (), + if ($rendition) then + render:rendition-to-css($rendition) else + () + ), + '; ' + ) + return + { + if ($style) then attribute style {$style} else (), + (: legacy: allow older table rend styles :) + if ($rend and not($style)) then + attribute class {$rend} + else (), + render:recurse($node, $options) + }
    +}; + +declare function render:row($node as element(tei:row), $options) as element() { + let $label := $node/@role[. = 'label'] + return + {if ($label) then attribute class {'label'} else ()}{render:recurse($node, $options)} +}; + +declare function render:cell($node as element(tei:cell), $options) as element() { + let $role := $node/@role + let $rend := $node/@rend + let $rendition := $node/@rendition + let $columns := $node/@cols + let $rows := $node/@rows + let $is-label := $role eq 'label' + let $is-brace := $role eq 'brace' + let $is-num := $role eq 'num' + let $style := + string-join( + ( + if ($node/ancestor::tei:table/@rend eq 'bordered') then 'border: 1px solid #606060' else () + , + if (matches($rend, '^width:')) then $rend else () + , + if (matches($rend, '^padding-')) then $rend else () + , + (: Virginia suggested left-align column-spanning cells by default :) + (: + if ($columns) then 'text-align: center' else () + , + :) + if ($is-num) then 'text-align: right' else () + , + if ($rendition) then render:rendition-to-css($rendition) else () + ) + , + '; ' + ) + return + element { if ($is-label) then 'th' else 'td' } { + if ($style) then attribute style {$style} else () + , + if ($columns) then attribute colspan {$columns} else () + , + if ($rows) then attribute rowspan {$rows} else () + , + if ($is-brace) then + let $orientation := if ($node = '{') then 'open' else 'close' + return + {$node/string()} + else + render:recurse($node, $options) + } +}; + +declare function render:pb($node as element(tei:pb), $options) as item()* { + if ($options/*:param[@name = 'show-annotations']/@value = 'true') then + let $volume := $options/*:param[@name = 'volume']/@value + let $abs-site-uri := $options/*:param[@name = 'abs-site-uri']/@value + let $pagenumber := data($node/@n) + let $facs := data($node/@facs) + let $fruspageimagerelativepath := + if ($frusx:STATIC-FILE-LOCATION eq 'local') then '/historicaldocuments/' + else if ($frusx:STATIC-FILE-LOCATION eq 'hsg') then 'http://history.state.gov/historicaldocuments/' + else (: if ($frus:STATIC-FILE-LOCATION eq 's3') then :) + 'https://static.history.state.gov/frus/' + let $imagepath := + if ($frusx:STATIC-FILE-LOCATION = ('local', 'hsg')) then + concat($fruspageimagerelativepath, $volume, "/media/medium/") + else (: if ($frus:STATIC-FILE-LOCATION eq 's3') then :) + concat($fruspageimagerelativepath, $volume, "/medium/") + return + ( +
    , + render:xmlid($node, $options) + , + + { + if ($facs) then + element a { + attribute href {concat($imagepath, $facs, '.png')}, + attribute title {concat('Page ', $pagenumber)}, + (: attribute class {"thickbox"}, + attribute rel {"inline"}, :) + attribute style {'text-decoration:none'}, + , + concat('Page ', $pagenumber) + } + else concat('Page ', $pagenumber) + } + + , +
    + ) + else if ($node/ancestor::tei:table) then + () (: TODO add non--tag-based approach to giving an anchor - somehow get the @id to hang on the previous/next element in the table - otherwise, if a row's cells are broken up by , it resets column widths in browser :) + else + ( + (: drop the pb info if we're inside a list, since it throws epubcheck validation errors :) + if ($node/ancestor::tei:list) then () + else + {render:xmlid($node, $options)} + (: show page number info for EPUB: :) + (:, +
    + , + + {if ($node/@xml:id) then render:xmlid($node, $options) else ()} + [start of page {$node/@n/string()} in original print volume] + + , +
    :) + ) +}; + +declare function render:title($node as element(tei:title), $options) { + let $level := $node/@level + return + if ($level = ('s', 'm')) then + {render:recurse($node, $options)} + else if ($level eq 'a') then + ( + '“', + render:recurse($node, $options), + '”' + ) + else + {render:recurse($node, $options)} +}; + +declare function render:byline($node as element(tei:byline), $options) { +

    by {render:recurse($node, $options)}

    +}; + +declare function render:rendition-to-css($rendition as attribute()) { + let $rendition-ids := tokenize($rendition/string(), '\s+') ! substring-after(., '#') + let $rendition-definitions := + for $id in $rendition-ids + return + root($rendition)/id($id) + return + string-join($rendition-definitions, ' ') +}; + +declare function render:seg($node as element(tei:seg), $options) { + let $rendition := $node/@rendition + let $rend := $node/@rend + let $css := + string-join( + ( + if ($rendition) then render:rendition-to-css($rendition) else (), + if (contains($rend, ':')) then $rend else () + ), + '; ' + ) + return + (: avoid fancy floats in mobi, but try to keep spacing somewhat under control :) + if ($options/*:param[@name='ebook-format']/@value eq 'mobi' and $node/ancestor::tei:opener) then +

    {render:recurse($node, $options)}

    + else if ($css) then + {render:recurse($node, $options)} + else + {render:recurse($node, $options)} +}; + +declare function render:idno($node as element(tei:idno), $options) { + let $type := $node/@type + return + {render:recurse($node, $options)} +}; + +declare function render:lg($node as element(tei:lg), $options) { +
    {render:recurse($node, $options)}
    +}; + +declare function render:l($node as element(tei:l), $options) { +
    {render:recurse($node, $options)}
    +}; + +(: render:create-toc(): Some additional functions to create the TOC for use in left sidebars :) + +(: create the TOC for use by the left sidebar :) +declare function render:create-toc($tei-text, $web-path-to-page-view, $view, $id) as element() { +
    +
      {render:toc-passthru($tei-text, $web-path-to-page-view, $view, $id)}
    +
    +}; + +declare function render:toc-passthru($node, $web-path-to-page-view, $view, $id) { + for $node in $node/node() + return + render:toc-dispatch($node, $web-path-to-page-view, $view, $id) +}; + +(: the central recursive typeswitch function for handling TOCs :) +declare function render:toc-dispatch($node, $web-path-to-page-view, $view, $id) { + typeswitch($node) + case element(tei:div) return render:toc-div($node, $web-path-to-page-view, $view, $id) + case element(tei:head) return render:toc-head($node, $web-path-to-page-view, $view, $id) + default return render:toc-passthru($node, $web-path-to-page-view, $view, $id) +}; + +(: handles divs for TOCs :) +declare function render:toc-div($node as element(tei:div), $web-path-to-page-view, $view, $id) { + (: we only show divs that have @xml:id attributes :) + if ($node/@xml:id) then + (: check the $id to see if it was passed the 'show!first!div' parameter, + in which case we want to highlight the first div, so + we set $id to the value of the first div's @xml:id attribute :) + let $id := if ($id eq 'show!first!div') then ($node/ancestor::tei:text//tei:div[@xml:id])[1]/@xml:id else $id + (: highlight the div if it matches $id :) + let $highlight := if ($node/@xml:id eq $id or ($node/@xml:id eq 'foreword' and not($id) and $view ne 'about')) then attribute class {'highlight'} else () + return + (: handle funky milestones toc, aka 'accordion' toc :) + if (contains(util:collection-name($node), 'milestones')) then + (: milestones landing page - just show article titles :) + if ($view eq 'about') then + (: the article titles are the div nodes whose xml:id is 'foreword' :) + if ($node/@xml:id eq 'foreword') then +
  • + {$highlight} + {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} + +
  • + else + () + (: interior pages, showing the contents of the period defined in $view :) + else + let $period := replace(util:document-name($node), '.xml$', '') + let $article := + (: suppress 'foreward' from being appended to the URL:) + if ($node/@xml:id/string() eq 'foreword') then () + else $node/@xml:id/string() + return +
  • + {$highlight} + {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} + +
  • + + (: this is unused code for a collection-wide accordion TOC view, + instead of the single-doc accordion TOC view used in the milestones section :) + (: else + if ($node/parent::tei:front) then + (: override the highlight so it's just for the current tei doc, not all :) + let $highlight := if (not($id) and $view eq replace(util:document-name($node), '.xml$', '')) then attribute class {'highlight'} else () + return +
  • + {$highlight} + {render:toc-recurse($node, $web-path-to-page-view, $view, $id)} + + { + if ($view eq replace(util:document-name($node), '.xml$', '')) then +
      + {render:toc-recurse($node/ancestor::tei:text/tei:body, $web-path-to-page-view, $view, $id)} +
    + else () + } +
  • + (: if the div doesn't contain child divs, just show the single list item :) + else +
  • + {$highlight} + {render:toc-recurse($node, $web-path-to-page-view, $view, $id)} + +
  • + :) + + + (: the top level of our TOC should only contain the top level divs :) + else if (local-name($node/..) = ('front', 'body', 'back')) then + (: if the div contains child divs, nest them into a new list :) + if ($node/tei:div[@xml:id]) then +
  • + {$highlight} + {data($node/tei:head)} + + { + (: only show child items if the parent is selected :) + if ($node/@xml:id eq $id or $node/tei:div/@xml:id = $id) then +
      + {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} +
    + else ()} +
  • + (: if the div doesn't contain child divs, just show the single list item :) + else +
  • + {$highlight} + {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} + +
  • + (: show non-top level divs as leaf-level list items :) + else +
  • + {$highlight} + {render:toc-passthru($node, $web-path-to-page-view, $view, $id)} + +
  • + (: don't show divs that don't have @xml:id attributes :) + else () +}; + +(: handles heads for TOCs :) +declare function render:toc-head($node as element(tei:head), $web-path-to-page-view, $view, $id) { + (: only handle heads whose parent is a div :) + if ($node/parent::tei:div) then + (: handle funky milestones toc, aka 'accordion' toc :) + if (contains(util:collection-name($node), 'milestones')) then + (: milestones landing page should only show the date :) + if ($view eq 'about') then + $node/tei:date/text() + (: milestones entries should show the full title :) + else + data($node) + (: don't bother showing the head "again" in the case where its parent div has already shown it :) + else if ($node/parent::tei:div/child::tei:div/@xml:id) then + () + (: special handling for 'buildings' article head :) + else if (contains(util:collection-name($node), 'buildings')) then + for $x in $node/node() + return + typeswitch($x) + case element(tei:lb) return ': ' + default return $x + else + render:recurse($node, ) + (: don't show heads whose parents aren't divs, e.g. graphics and figures :) + else () +}; + +declare function render:exist-match($node, $options) { + {render:recurse($node, $options)} +}; diff --git a/resources/css/epub.css b/resources/css/epub.css index 0ab45da..10bb2c6 100644 --- a/resources/css/epub.css +++ b/resources/css/epub.css @@ -1,184 +1,184 @@ -body { - font-family: Palatino, Georgia, "Times New Roman", Times, serif; -} - -/* ====Table of Contents==== */ -/* ====1. FRUS Document div Styles==== */ -/* ====2. FRUS Paragraph Styles==== */ -/* ====3. FRUS Footnote Styles ==== */ -/* ====4. FRUS List & Index Styles ==== */ -/* ====5. Bibliography Styles ==== */ -/* ====6. Tables ==== */ -/* ====7. Links ==== */ - -/* ====1. FRUS Document div Styles==== */ -/* For ePubs, we use each device's default for divs */ - -/* ====2. FRUS Paragraph Styles==== */ -/* Approximate print volume styles */ -p.sourceheadcenterboldbig { - text-align: center; - font-weight: bold; - font-size: 1.5em; - padding: 2px; -} -p.sourcearchiveboldbig { - font-weight: bold; - font-size:1.12em; -} -p.sourceparagraphspaceafter { - padding: 4px 0 0 0; -} -p.sourceparagraphfullindent { - margin-left: 20px; -} -p.sourceparagraphtightspacing { - line-height: 1em; -} -p.sourceparagraphbibliography { - padding-left: 4em; -} -ul.sourceparagraphbibliography { - padding-left: 10em; -} -p.dateline { - text-align: right -} -p.center { - text-align:center -} -p.right { - text-align: right -} -p.flushleft { - text-align: left; -} -p.closer { - text-align: right; -} -p.closer span.signed { - font-weight: bold -} -p.closer span.signed em { - font-weight: normal; - font-style: italic; -} -p.sectiontitleital { - font-style: italic; - margin-top: 2em; -} -p.sectiontitlebold { - font-weight: bold; - margin-top: 2em; -} -span.strikethrough { - text-decoration:line-through -} - -/* ====3. FRUS Footnote Styles ==== */ -/* sup is used for footnote reference numbers */ -sup { - vertical-align: super; - font-size: smaller; - line-height: 0em; -} -/* prevents sourcenotes from inheriting bold styling of document titles */ -div.footnotes, div.footnotes p { - font-weight: normal; - font-size: .973em; -} -hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;} -hr.space {background:#fff;color:#fff;} - -/* ====4. FRUS List & Index Styles ==== */ -/* FRUS lists by default have no bullets. - Whereas HTML typically asserts list indentation as a ul property, we do it as a li property. - This allows us to move some lists leftward help with tei:list/tei:head */ -blockquote { - padding-left: 2em; - margin-left: 0 -} -ul { - list-style-type: none; - padding-left: 0; - margin-left: 0; -} -li { - padding-left: 1em; - text-indent: -1em; -} -ul.bulleted { - list-style-type: disc; - margin-left: 2em; -} -ul.bulleted li { - padding-left: 0em; - margin-left: 0em; - text-indent: 0em; - padding-bottom: .75em; -} -/* Persons, Terms, and Index lists are flush left, and hanging lines are indented */ -#sources li, #persons li, #terms li, #index li, #table-of-contents li { - padding-left: 2em; - text-indent: -2em; -} -/* give TOC a little extra vertical space*/ -#table-of-contents li{ - padding-top: .2em; - padding-bottom: .2em; -} -/* "Subject" lists are small caps with space below */ -ul.subject { - font-size: .9em; - margin-left: 0; -} -ul.subject li { - text-indent: -2em; - padding-left: 4em; -} -ul.subject li.subjectallcaps { - text-indent: 0; - padding-left: 0; - font-variant: small-caps; -} -/* for 1952-54 subjects - TODO: test once 1st volume is received */ -li.subjectflushleft { - padding-bottom: 1em; -} -ul.subject li ul { - padding-bottom: 1em; -} -p.subjectallcaps { - font-size: smaller; - font-variant: small-caps; -} -p.subjectentry { - padding: 0 0 0 30px; - font-size: smaller; - margin: 0 -} - -/* ====5. Bibliographies ==== */ -ul.bibl { - list-style-type: none; - padding-left: 0; - margin-left: 0; -} -ul.bibl li { - padding-left: 2em; - text-indent: -2em; -} -/* ====6. Tables ==== */ -td.label { - font-weight: bold; -} -tr.label { - font-weight: bold; -} - -/* ====7. Links ==== */ -/* show icon for external links */ -a[href ^='http://'] { - padding-right: 13px; - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAFZJREFUeF59z4EJADEIQ1F36k7u5E7ZKXeUQPACJ3wK7UNokVxVk9kHnQH7bY9hbDyDhNXgjpRLqFlo4M2GgfyJHhjq8V4agfrgPQX3JtJQGbofmCHgA/nAKks+JAjFAAAAAElFTkSuQmCC") no-repeat scroll right center transparent; +body { + font-family: Palatino, Georgia, "Times New Roman", Times, serif; +} + +/* ====Table of Contents==== */ +/* ====1. FRUS Document div Styles==== */ +/* ====2. FRUS Paragraph Styles==== */ +/* ====3. FRUS Footnote Styles ==== */ +/* ====4. FRUS List & Index Styles ==== */ +/* ====5. Bibliography Styles ==== */ +/* ====6. Tables ==== */ +/* ====7. Links ==== */ + +/* ====1. FRUS Document div Styles==== */ +/* For ePubs, we use each device's default for divs */ + +/* ====2. FRUS Paragraph Styles==== */ +/* Approximate print volume styles */ +p.sourceheadcenterboldbig { + text-align: center; + font-weight: bold; + font-size: 1.5em; + padding: 2px; +} +p.sourcearchiveboldbig { + font-weight: bold; + font-size:1.12em; +} +p.sourceparagraphspaceafter { + padding: 4px 0 0 0; +} +p.sourceparagraphfullindent { + margin-left: 20px; +} +p.sourceparagraphtightspacing { + line-height: 1em; +} +p.sourceparagraphbibliography { + padding-left: 4em; +} +ul.sourceparagraphbibliography { + padding-left: 10em; +} +p.dateline { + text-align: right +} +p.center { + text-align:center +} +p.right { + text-align: right +} +p.flushleft { + text-align: left; +} +p.closer { + text-align: right; +} +p.closer span.signed { + font-weight: bold +} +p.closer span.signed em { + font-weight: normal; + font-style: italic; +} +p.sectiontitleital { + font-style: italic; + margin-top: 2em; +} +p.sectiontitlebold { + font-weight: bold; + margin-top: 2em; +} +span.strikethrough { + text-decoration:line-through +} + +/* ====3. FRUS Footnote Styles ==== */ +/* sup is used for footnote reference numbers */ +sup { + vertical-align: super; + font-size: smaller; + line-height: 0em; +} +/* prevents sourcenotes from inheriting bold styling of document titles */ +div.footnotes, div.footnotes p { + font-weight: normal; + font-size: .973em; +} +hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;} +hr.space {background:#fff;color:#fff;} + +/* ====4. FRUS List & Index Styles ==== */ +/* FRUS lists by default have no bullets. + Whereas HTML typically asserts list indentation as a ul property, we do it as a li property. + This allows us to move some lists leftward help with tei:list/tei:head */ +blockquote { + padding-left: 2em; + margin-left: 0 +} +ul { + list-style-type: none; + padding-left: 0; + margin-left: 0; +} +li { + padding-left: 1em; + text-indent: -1em; +} +ul.bulleted { + list-style-type: disc; + margin-left: 2em; +} +ul.bulleted li { + padding-left: 0em; + margin-left: 0em; + text-indent: 0em; + padding-bottom: .75em; +} +/* Persons, Terms, and Index lists are flush left, and hanging lines are indented */ +#sources li, #persons li, #terms li, #index li, #table-of-contents li { + padding-left: 2em; + text-indent: -2em; +} +/* give TOC a little extra vertical space*/ +#table-of-contents li{ + padding-top: .2em; + padding-bottom: .2em; +} +/* "Subject" lists are small caps with space below */ +ul.subject { + font-size: .9em; + margin-left: 0; +} +ul.subject li { + text-indent: -2em; + padding-left: 4em; +} +ul.subject li.subjectallcaps { + text-indent: 0; + padding-left: 0; + font-variant: small-caps; +} +/* for 1952-54 subjects - TODO: test once 1st volume is received */ +li.subjectflushleft { + padding-bottom: 1em; +} +ul.subject li ul { + padding-bottom: 1em; +} +p.subjectallcaps { + font-size: smaller; + font-variant: small-caps; +} +p.subjectentry { + padding: 0 0 0 30px; + font-size: smaller; + margin: 0 +} + +/* ====5. Bibliographies ==== */ +ul.bibl { + list-style-type: none; + padding-left: 0; + margin-left: 0; +} +ul.bibl li { + padding-left: 2em; + text-indent: -2em; +} +/* ====6. Tables ==== */ +td.label { + font-weight: bold; +} +tr.label { + font-weight: bold; +} + +/* ====7. Links ==== */ +/* show icon for external links */ +a[href ^='http://'] { + padding-right: 13px; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAFZJREFUeF59z4EJADEIQ1F36k7u5E7ZKXeUQPACJ3wK7UNokVxVk9kHnQH7bY9hbDyDhNXgjpRLqFlo4M2GgfyJHhjq8V4agfrgPQX3JtJQGbofmCHgA/nAKks+JAjFAAAAAElFTkSuQmCC") no-repeat scroll right center transparent; } \ No newline at end of file From ec84aed5e1caa6d75b3769a5bf126c314c662836 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Tue, 17 Feb 2026 18:47:38 -0500 Subject: [PATCH 2/2] fix(epub): handle doc ids with chapter prefix --- modules/tei-render.xql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/tei-render.xql b/modules/tei-render.xql index 0722c6c..7ab1241 100644 --- a/modules/tei-render.xql +++ b/modules/tei-render.xql @@ -647,17 +647,17 @@ declare function render:ref($node as element(tei:ref), $options) as item()* { $type, render:recurse($node, $options) } - (: ref to a document :) - else if (matches($target, '^#d\d+$')) then + (: ref to a document, with optional prefix e.g., eta_d2 :) + else if (matches($target, '^#([a-z]{2,3}_)?d\d+$')) then element a { attribute href { concat( substring-after($target, '#'), '.html') }, $type, render:recurse($node, $options) } (: ref to a portion of a document, e.g., footnote :) - else if (matches($target, '^#d\d+.+')) then + else if (matches($target, '^#(\w{1,3}_)?d\d+.+')) then element a { - attribute href { concat( replace($target, '^#(d\d+)(.+)$', '$1'), '.html', $target) }, + attribute href { concat( replace($target, '^#((?:[a-z]{2,3}_)?d\d+)(.+)$', '$1'), '.html', $target) }, $type, render:recurse($node, $options) }