transition to htmx, made truly infinite scroll

This commit is contained in:
Oleg Belyaev 2025-03-29 23:57:33 +03:00
parent a65037f8ea
commit cd513bd796
26 changed files with 849 additions and 1253 deletions

View file

@ -1,96 +0,0 @@
module namespace dict = 'http://ossetic-studies.org/ns/abaevdict-site/dictionary';
import module namespace shared = 'http://ossetic-studies.org/ns/abaevdict-site/shared' at '../shared/shared.xq';
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../../abv-mod.xqm';
(: Sidebar with entry list. Requires abaev.js to be loaded. :)
declare function dict:sidebar($lang as xs:string) {
(
<button id="open-leftbar">
{html:doc('../../../static/chevron-right.svg')}
</button>,
<button id="close-leftbar">
{html:doc('../../../static/chevron-left.svg')}
</button>,
<aside id="leftbar">
<fieldset>
<input type="text" id="filter-entries"
placeholder="{if ($lang = 'ru') then 'Быстрый фильтр' else 'Quick filter…'}"/>
<label>
<input id="show-re" type="checkbox" role="switch"></input>
{if ($lang = 'ru') then 'Производные' else 'Show subentries'}
</label>
</fieldset>
<nav id="entrylist"></nav> <!-- To be filled dynamically in js -->
</aside>
)
};
declare function dict:main-view($lang as xs:string,
$p as xs:integer,
$xpath as xs:string,
$entry as xs:string) {
(dict:sidebar($lang),
<main>
{for $doc at $i in $shared:sorted
where $i > ($p - 1) * $shared:items-per-page and $i <= $p * $shared:items-per-page
let $html := if ($xpath != '' and $entry = $doc/@xml:id)
then abv-m:mark-element(
doc(`abaevdict_{$lang}/xml/{$doc/@xml:id}.xml`),
$xpath) => abv-m:make-html($lang)
else
let $sd := session:get('searchData')
let $sn := session:get('searchN')
return if (exists($sd) and $sd($sn)('entry') = $doc/@xml:id)
then abv-m:make-html($sd($sn)('tei'), $lang)
else doc(`abaevdict_{$lang}/html/{$doc/@xml:id}.html`)
return <div class="abv-lex">{(
(: Block with icons to the left of entry (floating) :)
<div class="icons">
{
<a href="dict/{$doc/@xml:id}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-link-45deg" viewBox="0 0 16 16">
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/>
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
</svg>
</a>,
if (doc('abaevdict_index/mentioned_en.xml')/lang-index
/lang[@id != 'os' and
not(starts-with(@id,'os-'))]/word/entry[@id=string($doc/@xml:id)])
then <a class="abv-map" data-abv-entry="{$doc/@xml:id}">
<img src="/static/map.png"></img>
</a>}</div>,
$html)}</div>
}
</main>,
<footer>
<nav class="pagination">
<ul>
<li>
{if ($p > 1) then <a href="?page={$p - 1}">Previous page</a> else ()}
</li>
</ul>
<!-- <ul><li>Page {$p} of {$page:total}</li></ul> -->
<ul class="abv-nextpage">
<li>
{if ($p < $shared:total) then
<a class="pagination__next" href="?page={$p + 1}">Next page</a> else ()}
</li>
</ul>
</nav>
</footer>,
<dialog id="modal_map">
<article>
<header>
<button class="abv-close-map" aria-label="Close" rel="prev">
</button>
<p>{if ($lang = 'ru') then 'Карта' else 'Map'}</p>
</header>
<p>
<div id="map_display" style="width:100%;height:80%;"></div>
</p>
<footer><button class="abv-close-map">{if ($lang = 'ru') then 'Закрыть' else 'Close'}</button></footer>
</article>
</dialog>)
};

View file

@ -0,0 +1,182 @@
module namespace entries = 'http://ossetic-studies.org/ns/abaevdict-site/entries';
declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../../abv-mod.xqm';
(: MAIN DICTIONARY VIEW COMPONENT :)
(: GET INDIVIDUAL ENTRY :)
declare %rest:path("{$lang}/entries/{$id}")
%rest:query-param("query", "{$query}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function entries:entry($lang as xs:string, $id as xs:string, $query as xs:string?) {
if (count($query) = 0) then
doc(`abaevdict_{$lang}/html/{$id}.html`)
else
let $xml := doc(`abaevdict_{$lang}/xml/{$id}.xml`)
let $marked := ft:mark(
xquery:eval(
`declare namespace tei = "http://www.tei-c.org/ns/1.0";
declare namespace abv = "http://ossetic-studies.org/ns/abaevdict";
.[{$query}]`, {'': $xml})
)
return abv-m:make-html($marked,$lang)
};
declare %rest:path("/entries/{$id}")
%rest:query-param("query", "{$query}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function entries:entry($id as xs:string, $query as xs:string?) {
entries:entry('en', $id, $query)
};
(: GET ENTRIES CATALOG :)
declare %rest:path("{$lang}/entries")
%rest:query-param("per-chunk", "{$per-chunk}", 5)
%rest:query-param("query", "{$query}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function entries:entries($lang as xs:string,
$per-chunk as xs:integer,
$query as xs:string?) {
let $docs := abv-m:collection($lang,'xml')/tei:entry
let $chosen := if($query) then
xquery:eval(`declare namespace tei = "http://www.tei-c.org/ns/1.0";
declare namespace abv = "http://ossetic-studies.org/ns/abaevdict";
.[{$query}]`,
{'': $docs})/string(@xml:id)
for tumbling window $w in abv-m:lookups()
start at $s when true()
only end at $e when $e - $s eq ($per-chunk - 1)
return
<div class="abv-chunk">
{
for $entry in $w
let $id := $entry/@xml:id
return
<div class="abv-lex"
hx-get="/{$lang}/entries/{$id}"
hx-target="find article"
hx-trigger="intersect once from:closest .abv-chunk"
hx-swap="outerHTML focus-scroll:true">
{if ($id = $chosen) then
attribute hx-vals {
`{'{'}"query": "{$query}"{'}'}`
}}
{
<article id="{$id}"
class="abv-entry">
</article>
}
</div>
}
</div>
};
declare %rest:path("/entries")
%rest:query-param("per-chunk", "{$per-chunk}", 5)
%rest:query-param("query", "{$query}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function entries:entries($per-chunk as xs:integer,
$query as xs:string?) {
entries:entries('en', $per-chunk, $query)
};
(: LIST OF ENTRIES FOR MENU :)
declare %rest:path("{$lang}/entrylist")
%rest:query-param("filter", "{$filter}")
%rest:query-param("subentries", "{$subentries}", false())
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function entries:entrylist($lang as xs:string,
$filter as xs:string?,
$subentries as xs:boolean?) {
<nav id="entrylist">
<ul>
{for $e in abv-m:lookups($subentries)
let $txt := $e/text()
return
if ($txt contains text {`{$filter}.*`} using wildcards at start) then
<li>
{
if (name($e) = 're') then
attribute class {'abv-menu-re'} else (),
<a href="#{$e/@xml:id}">
{$e/text()}
</a>
}
</li>
}
</ul>
</nav>
};
declare %rest:path("entrylist")
%rest:query-param("filter", "{$filter}")
%rest:query-param("subentries", "{$subentries}", false())
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function entries:entrylist($filter as xs:string?,
$subentries as xs:boolean?) {
entries:entrylist('en',$filter,$subentries)
};
(: QUICK ENTRY LIST SIDEBAR :)
declare function entries:sidebar($lang as xs:string) {
(<button id="close-leftbar"
hx-on-click="document.getElementById('leftbar').style.display = 'none';
document.getElementById('open-leftbar').style.display = 'block';
document.getElementById('close-leftbar').style.display = 'none'">
{html:doc('../../../static/chevron-left.svg')}
</button>,
<button id="open-leftbar"
hx-on-click="document.getElementById('leftbar').style.display = 'block';
document.getElementById('open-leftbar').style.display = 'none';
document.getElementById('close-leftbar').style.display = 'block'">
{html:doc('../../../static/chevron-right.svg')}
</button>,
<aside id="leftbar">
<fieldset>
<input type="text" id="filter-entries" name="filter"
hx-get="./entrylist" hx-target="#entrylist"
hx-swap="outerHTML"
hx-trigger="input changed"
placeholder="{if ($lang = 'ru') then 'Быстрый фильтр' else 'Quick filter…'}"/>
<label>
<input id="show-re"
type="checkbox"
role="switch"
name="subentries" value="1"
hx-get="./entrylist" hx-target="#entrylist"
hx-trigger="change"
hx-swap="outerHTML"/>
{if ($lang = 'ru') then 'Производные' else 'Show subentries'}
</label>
</fieldset>
<nav id="entrylist"
hx-get="./entrylist"
hx-swap="outerHTML"
hx-trigger="load">
</nav>
</aside>)
};

View file

@ -1,49 +0,0 @@
module namespace index = 'http://ossetic-studies.org/ns/abaevdict-site/index';
import module namespace shared = 'http://ossetic-studies.org/ns/abaevdict-site/shared' at '../shared/shared.xq';
declare function index:content($lang as xs:string) {
let $mlangs := doc(`abaevdict_index/langnames.xml`)/csv[1]/record
return
<html>
{shared:head('HEDO Index', <script src="/static/abaev-index.js"></script>)}
<body>
{shared:header($lang, `../{shared:invert-lang($lang)}/index`)}
<main>
<div class="index">
<div class="langs">
<label>{if ($lang = 'ru') then 'Языки' else 'Languages'}</label>
<select id="abv-select-lang" size="99999">
{
for $mlang in $mlangs
let $mlang-id := $mlang/code/text()
let $mlang-name := if ($lang = 'ru') then $mlang/ru/text() else $mlang/en/text()
where $mlang-name != ''
order by $mlang-name
return
<option value="{$mlang-id}">
{
$mlang-name
}
</option>
}
</select>
</div>
<div class="forms">
<Label>{if ($lang = 'ru') then 'Формы' else 'Forms'}</Label>
<select id="abv-select-word" size="99999">
</select>
</div>
<div class="entries">
<Label>{if ($lang = 'ru') then 'Лексемы' else 'Lexemes'}</Label>
<select id="abv-select-entry" size="99999">
</select>
</div>
</div>
<div class="index-submit">
<button id="btn-index-submit" hidden="1">{if ($lang = 'ru') then 'К форме' else 'Go to form'}</button>
</div>
</main>
</body>
</html>
};

View file

@ -0,0 +1,149 @@
module namespace index = 'http://ossetic-studies.org/ns/abaevdict-site/index';
declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../../abv-mod.xqm';
(: GET LANGUAGE LIST :)
declare %rest:path("{$lang}/index/languages")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function index:languages($lang as xs:string) {
let $mlangs := doc(`abaevdict_index/langnames.xml`)/csv[1]/record
for $mlang in $mlangs
let $mlang-id := $mlang/code/text()
let $mlang-name := if ($lang = 'ru') then $mlang/ru/text() else $mlang/en/text()
where $mlang-name != ''
order by $mlang-name
return
<option value="{$mlang-id}">
{
$mlang-name
}
</option>
};
declare %rest:path("/index/languages")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function index:languages() {
index:languages('en')
};
(: FORM LIST :)
declare %rest:path("{$lang}/index/forms")
%rest:query-param("flang", "{$flang}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function index:forms($lang as xs:string, $flang as xs:string) {
let $ments := doc(`abaevdict_index/mentioned_{$lang}.xml`)
/lang-index[1]/lang[@id=$flang]/word
for $w in $ments
let $txt := string($w/@text)
return <option value="{$txt}">{$txt}</option>
};
declare %rest:path("index/forms")
%rest:query-param("flang", "{$flang}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function index:forms($flang as xs:string) {
index:forms('en',$flang)
};
(: RELATED ENTRY LIST :)
declare %rest:path("{$lang}/index/entries")
%rest:query-param("flang", "{$flang}")
%rest:query-param("word", "{$word}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function index:entries($lang as xs:string,
$flang as xs:string,
$word as xs:string) {
let $entries := doc(`abaevdict_index/mentioned_{$lang}.xml`)
/lang-index[1]/lang[@id=$flang]/word[@text=$word]/entry
for $e in $entries
let $id := $e/@id
let $txt := abv-m:entry-form-by-id($id)
let $query := `./@xml:id = '{$id}' and .//text() contains
text '{$word}' using diacritics sensitive`
return <tr>
<td>{string-join(
distinct-values($e/ref/gloss/string(@text)),', ')}</td>
<td>
<a href="./dictionary?query={$query}#{$id}">{$txt}</a>
</td>
<td>
{
string-join(data(doc(`abaevdict_{$lang}/xml/{$id}.xml`)
/tei:entry[1]/tei:sense/(tei:sense|tei:sense/tei:sense|.)
/tei:cit/tei:quote),', ')
}
</td>
</tr>
};
declare %rest:path("index/entries")
%rest:query-param("flang", "{$flang}")
%rest:query-param("word", "{$word}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function index:entries($flang as xs:string,
$word as xs:string) {
index:entries('en',$flang,$word)
};
(: THE MAIN INDEX VIEW :)
declare function index:content($lang as xs:string) {
<html>
<body>
<main>
<div class="index">
<div class="langs">
<label>{if ($lang = 'ru') then 'Языки' else 'Languages'}</label>
<select id="abv-select-lang" size="99999" name="flang"
hx-get='./index/forms' hx-target="#abv-select-word" hx-trigger="input">
{index:languages($lang)}
</select>
</div>
<div class="forms">
<label>{if ($lang = 'ru') then 'Формы' else 'Forms'}</label>
<select id="abv-select-word" size="99999" name="word"
hx-get="./index/entries" hx-target="#abv-select-entry" hx-trigger="input"
hx-include="#abv-select-lang">
</select>
</div>
<div class="entries">
<label>{if ($lang = 'ru') then 'Лексемы' else 'Lexemes'}</label>
<table>
<thead>
<tr>
<th scope="col">{if ($lang = 'ru') then 'Глоссы' else 'Gloss'}</th>
<th scope="col">{if ($lang = 'ru') then 'Лексема' else 'Lemma'}</th>
<th scope="col">{if ($lang = 'ru') then 'Перевод' else 'Translation'}</th>
</tr>
</thead>
<tbody id="abv-select-entry">
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>
};

View file

@ -0,0 +1,61 @@
module namespace map = 'http://ossetic-studies.org/ns/abaevdict-site/map';
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../../abv-mod.xqm';
declare %rest:path("{$lang}/map")
%rest:query-param("entry", "{$entry}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function map:map($lang as xs:string, $entry as xs:string) {
let $mapdata := abv-m:make-geomap(
doc(`abaevdict_{$lang}/xml/{$entry}.xml`),
$lang)
return
(<article>
<header>
<button class="abv-close-map" aria-label="Close" rel="prev">
</button>
<p>{if ($lang = 'ru') then 'Карта' else 'Map'}</p>
</header>
<p>
<div id="map-display" style="width:100%;height:80%;"></div>
</p>
<footer>
<button class="abv-close-map"
hx-on-click="document.getElementById('modal_map').close()">
{if ($lang = 'ru') then 'Закрыть' else 'Close'}
</button>
</footer>
</article>,
<script defer="1">
{concat(`var mapdata = {json:serialize($mapdata)}`,
"
map = document.getElementById('map-display');
var layout = {
// title: {
// text: 'Canadian cities',
// font: {
// family: 'Droid Serif, serif',
// size: 16
// }
// },
geo: {
scope: 'world',
resolution: 50,
fitbounds: 'locations',
showrivers: true,
rivercolor: '#fff',
showlakes: true,
lakecolor: '#fff',
showland: true,
landcolor: '#EAEAAE',
showcountries: false,
subunitcolor: '#d3d3d3'
}
};
Plotly.newPlot(map, [mapdata], layout);
")}
</script>)
};

View file

@ -1,31 +0,0 @@
module namespace search = 'http://ossetic-studies.org/ns/abaevdict-site/search-modal';
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../../abv-mod.xqm';
(: Modal that appears on all pages, with search results :)
declare function search:content($lang as xs:string) {
if (exists(session:get('searchData'))) then
<dialog id="modal_searchResults">
<article>
<header>
<button class="abv-close-search" aria-label="Close" rel="prev"/>
<p>{if ($lang = 'ru') then 'Результаты поиска'
else 'Search results'}</p>
</header>
<table>
<thead>
<tr>
<td>{if ($lang = 'ru') then '№' else 'No.'}</td>
<td>{if ($lang = 'ru') then 'Лемма' else 'Lemma'}</td>
<td>{if ($lang = 'ru') then 'Фрагмент' else 'Fragment'}</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
<footer>
<button class="abv-close-search">{if ($lang = 'ru') then 'Закрыть' else 'Close'}</button>
</footer>
</article>
</dialog>
};

View file

@ -0,0 +1,133 @@
module namespace search = 'http://ossetic-studies.org/ns/abaevdict-site/search';
declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../../abv-mod.xqm';
declare variable $search:types :=
(
{
'query': ".//text() contains text &#123;'$'&#125;",
'ru': 'Везде',
'en': 'All'
},
{
'query': ".//tei:form/tei:orth//text() contains text &#123;'$'&#125;",
'ru': 'Формы',
'en': 'Forms'
},
{
'query': ".//tei:cit[@type='translationEquivalent']//text() contains text &#123;'$'&#125;",
'ru': 'Значения',
'en': 'Meanings'
},
{
'query': ".//tei:cit[@type='example']/tei:quote//text() contains text &#123;'$'&#125;",
'ru': 'Примеры',
'en': 'Examples'
},
{
'query': ".//tei:cit[@type='translation']//text() contains text &#123;'$'&#125;",
'ru': 'Переводы',
'en': 'Translations'
},
{
'query': ".//tei:mentioned/(tei:m|tei:w|tei:phr|tei:s)/text() contains text &#123;'$'&#125;",
'ru': 'Цит. формы',
'en': 'Mentioned'
},
{
'query': ".//tei:gloss//text() contains text &#123;'$'&#125;",
'ru': 'Глоссы',
'en': 'Glosses'
},
{
'query': "./tei:etym[1]//text() contains text &#123;'$'&#125;",
'ru': 'Этимологии',
'en': 'Etymology'
},
{
'query': "$",
'ru': 'XQuery',
'en': 'XQuery'
}
);
(: GET SEARCH RESULTS :)
declare function search:content($lang as xs:string,
$pattern as xs:string? := (),
$text as xs:string? := ()) {
<form role="search" id="abv-search"
hx-post=""
hx-select="main"
hx-target="main"
hx-swap="outerHTML"
hx-push-url="true">
<select name="pattern" required="1">
{
for $opt in $search:types
return
<option value="{$opt('query')}">
{
if ($pattern = $opt('query')) then
attribute selected {'1'},
if ($lang = 'ru') then $opt('ru')
else $opt('en')
}
</option>
}
</select>
<input name="text" required="1"
placeholder="{if ($lang = 'ru') then 'Искать' else 'Search'}"
value="{$text}"
aria-label="Search">
{if (exists(session:get('searchQuery'))) then
attribute disabled {"1"} }
</input>
<input type="submit" value="Submit"/>
</form>,
<table hx-boost="true" id="abv-search-results">
<thead>
<tr>
<th scope="col">{if ($lang = 'ru') then 'Лексема' else 'Lemma'}</th>
<th scope="col">{if ($lang = 'ru') then 'Перевод' else 'Translation'}</th>
<th scope="col">{if ($lang = 'ru') then 'Контекст' else 'Context'}</th>
</tr>
</thead>
<tbody>
{if (exists($pattern) and exists($text)) then
let $entries := abv-m:collection($lang,'xml')/tei:entry
let $query := replace($pattern, "\$", $text)
let $hits :=
ft:mark(xquery:eval(`declare namespace tei = "http://www.tei-c.org/ns/1.0";
declare namespace abv = "http://ossetic-studies.org/ns/abaevdict";
.[{$query}]`, {'': $entries}))
for $hit in $hits
return
<tr>
<td>
<a href="/{$lang}/dictionary?query={$query}#{$hit/@xml:id}">
{$hit/tei:form[1]/tei:orth[1]}
</a>
</td>
<td>
{
string-join(
data($hit/tei:sense
/(tei:sense|tei:sense/tei:sense|.)/tei:cit/tei:quote),
', '
)
}
</td>
<td>
{
if (count($hit//*[tei:mark]) > 0) then
util:strip-namespaces(($hit//*[tei:mark])[1])/child::node()
}
</td>
</tr>
}
</tbody>
</table>
};

View file

@ -1,210 +0,0 @@
declare namespace api = 'http://ossetic-studies.org/ns/abaevdict-api';
declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../abv-mod.xqm';
(: ======================================================= :)
(: ==================== API STUFF ======================== :)
(: ======================================================= :)
declare option output:method 'json';
declare option output:json "format=xquery";
declare variable $langs-index := doc('abaevdict_index/langnames.xml');
declare variable $lookup := doc('abaevdict_index/lookup.xml');
(: Get the mentioned index for the right metalanguage :)
declare function api:ment-index($db-lang as xs:string) {
doc(`abaevdict_index/mentioned_{$db-lang}.xml`)
};
(: Short entry info (only forms for now). Hash is used because URLs don't handle
some Unicode characters very well in this system :)
declare function api:entry-info-short($n as node())
as map(xs:string, item()) {
let $id := if (name($n) = 're') then string($n/@corresp) else string($n/@xml:id)
return map{'id': hash($id),
'xmlid': $id,
'form': $n/text(),
'subentry': if (name($n) = 're') then true() else false()
}
};
declare function api:entry-info-long($n as node(), $db-lang as xs:string)
as map(xs:string, item()) {
map:merge(
(
api:entry-info-short($n),
map{'glosses': array:build(data(doc(`abaevdict_{$db-lang}/xml/{$n/@xml:id}.xml`)
/tei:entry[1]/tei:sense/(tei:sense|tei:sense/tei:sense|.)/tei:cit/tei:quote))}
)
)
};
(: Short language info -- for the language list :)
declare function api:lang-info-short($n as node(), $db-lang as xs:string) {
map{
'id': $n/code/text(),
'glottolog': $n/glottolog/text(),
'name': map{'full': if ($db-lang = 'ru') then $n/ru/text() else $n/en/text(),
'abbr': if ($db-lang = 'ru') then $n/ru/text() else $n/en_abbr/text()}
}
};
(: Longer info -- for individual languages :)
declare function api:lang-info-full($n as node(), $db-lang as xs:string) {
map:merge(
(api:lang-info-short($n, $db-lang),
map{'words':
array:build(api:ment-index($db-lang)/lang-index[1]/lang[@id=$n/code]/word,
api:word-info-short#1)
}
)
)
};
declare function api:word-info-short($n as node()) {
let $text := xs:string($n/@text)
return map{
(: I am using a hash function as ID, because the words are uniquely
identified within a language by their text. And the mentioned index is not
persistent enough (at least yet) to use persistent ids :)
'id': string(hash($text)),
'text': $text
}
};
declare function api:word-info-long($n as node()) {
map:merge(
(
api:word-info-short($n),
map{
'entries': array:build($n/entry,
fn {
map{'id': string(hash(./@id)),
'xmlid': string(./@id),
'refs': array:build(./ref,
fn {
map:merge((
map:entry('path', string(./@path)),
if(./gloss) then
{'glosses': array:build(distinct-values(./gloss/@text), string#1)}
else ()
)
)
})
}
}
)
(: 'nodes': array:build(string($n/ref/@node-id)),
'glosses': array:build(string($n/gloss/@text)) :)
}
)
)
};
(: RETURN JSON FOR PLOTLY MAP :)
(: English as default language :)
declare %rest:path("dict/map-info/{$entry}")
%rest:GET
function api:map-info($entry) {
(: In this case we do not redirect, but simply give the JSON via
another function :)
api:map-info('en', $entry)
};
(: This has to be remade to use the /api path. But keep it like this for now :)
declare %rest:path("{$lang}/dict/map-info/{$entry}")
%rest:GET
function api:map-info($lang, $entry) {
let $ments := abv-m:make-geomap(doc(`abaevdict_{$lang}/xml/{$entry}.xml`),
$lang)
return $ments
};
(: API FOR THE MENTIONED INDEX :)
(: Default to English :)
declare %rest:path("/api/{$path=.+}")
%rest:GET
function api:default($path as xs:string) {
web:forward(`/en/api/{$path}`)
};
(: Get languages :)
declare %rest:path("{$db-lang}/api/languages")
%rest:GET
function api:langs($db-lang as xs:string := 'en') {
array:build(
$langs-index/csv[1]/record,
api:lang-info-short(?,$db-lang)
)
};
(: Get one language :)
declare %rest:path("{$db-lang}/api/languages/{$lang}")
%rest:GET
function api:langs-lang($db-lang as xs:string, $lang as xs:string) {
api:lang-info-full(
$langs-index/csv[1]/record[code=$lang], $db-lang
)
};
(: Get words for a language :)
declare %rest:path("{$db-lang}/api/languages/{$lang}/words")
%rest:GET
function api:langs-lang-words($db-lang as xs:string, $lang as xs:string) {
array:build(api:ment-index($db-lang)/lang-index[1]/lang[@id=$lang]/word,
api:word-info-short#1)
};
(: Get info on a particular word.
Db-lang is a placeholder, it does nothing here, used only for consistency :)
declare %rest:path("{$db-lang}/api/languages/{$lang}/words/{$word-id}")
%rest:GET
function api:langs-lang-words-word($db-lang as xs:string,
$lang as xs:string,
$word-id as xs:string) {
api:word-info-long(
api:ment-index($db-lang)/lang-index[1]/lang[@id=$lang]/word[string(hash(@text))=$word-id]
)
};
(: Entry info :)
declare %rest:path("{$db-lang}/api/entries")
%rest:GET
function api:entries($db-lang as xs:string) {
array:build(
$lookup/tei:table[1]/*,
api:entry-info-short#1
)
};
declare %rest:path("{$db-lang}/api/entries/{$entry-id}")
%rest:GET
function api:entries($db-lang as xs:string,
$entry-id as xs:string) {
api:entry-info-long(
$lookup/tei:table[1]/*[string(hash(@xml:id))=$entry-id],
$db-lang)
};
(: Search API :)
declare %rest:path("{$db-lang}/api/search")
%rest:query-param("type","{$type}",'full')
%rest:query-param("query","{$query}")
%rest:GET
function api:search($db-lang as xs:string,
$type as xs:string,
$query as xs:string) {
abv-m:search($db-lang,$type,$query)
};
(: Get current search results :)
declare %rest:path("{$db-lang}/api/search/current")
%rest:GET
function api:search-current($db-lang as xs:string) {
session:get('searchData')
};
0

View file

@ -1,123 +0,0 @@
module namespace page = 'http://ossetic-studies.org/ns/abaevdict-site';
declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../abv-mod.xqm';
import module namespace shared = 'http://ossetic-studies.org/ns/abaevdict-site/shared' at './shared/shared.xq';
(: COMPONENTS :)
(: To make the codebase more manageable, components are split into separate
files included as modules. :)
import module namespace home = 'http://ossetic-studies.org/ns/abaevdict-site/home' at './components/home.xq';
import module namespace dict = 'http://ossetic-studies.org/ns/abaevdict-site/dictionary' at './components/dictionary.xq';
import module namespace index = 'http://ossetic-studies.org/ns/abaevdict-site/index' at './components/index.xq';
(: =============================================================== :)
(: ============================ FAVICON ========================== :)
(: =============================================================== :)
declare %rest:path("/favicon.ico")
%rest:GET
function page:favicon() {
fetch:binary('../../static/favicon.ico')
};
(: =============================================================== :)
(: ======================= HOMEPAGE ============================== :)
(: =============================================================== :)
declare %rest:path("{$lang}")
%output:method("html")
%output:html-version('5')
function page:zero($lang) {
page:home($lang)
};
declare %rest:path("/")
%output:method("html")
%output:html-version('5')
function page:zero() {
page:zero('en')
};
declare %rest:path("{$lang}/home")
%output:method("html")
%output:html-version('5')
function page:home($lang) {
<html>
{shared:head(if ($lang='ru') then 'ИЭСОЯ — Главная' else 'Abaevdict — Home',())}
<body>
{shared:header($lang,`../{shared:invert-lang($lang)}/home`)}
<main>
{
home:content($lang)
}
</main>
</body>
</html>
};
declare %rest:path("home")
%output:method("html")
%output:html-version('5')
function page:home() {
page:home('en')
};
(: =============================================================== :)
(: ======================= THE DICTIONARY ======================== :)
(: =============================================================== :)
(: If no language defined, default to English :)
declare %rest:path("dict")
%output:method("html")
%output:html-version('5')
function page:dict() {
web:forward('/en/dict')
};
(: The main dictionary view :)
declare %rest:path("{$lang}/dict")
%rest:query-param("page","{$p}", 1)
%rest:query-param("xpath","{$xpath}", '')
%rest:query-param("entry","{$entry}", '')
%output:method("html")
%output:html-version('5')
function page:dict($lang, $p, $xpath, $entry) {
<html>
{shared:head('HEDO Dictionary',
(<script src="/static/infinite-scroll.pkgd.min.js">
</script>,
<script src="/static/plotly-3.0.1.min.js" charset="utf-8"></script>,
<script src="/static/abaev.js"></script>))}
<body>
{(
shared:header($lang,`../{shared:invert-lang($lang)}/dict`),
dict:main-view($lang, $p, $xpath, $entry)
)}
</body>
</html>
};
(: =============================================================== :)
(: ======================= THE INDEX ============================= :)
(: =============================================================== :)
(: Mentioned index :)
declare %rest:path("{$lang}/index")
%output:method("html")
%output:html-version('5')
function page:index($lang) {
index:content($lang)
};
(: Default to English :)
declare %rest:path("index")
%output:method("html")
%output:html-version('5')
function page:index() {
page:index('en')
};

View file

@ -1,31 +0,0 @@
module namespace entry = 'http://ossetic-studies.org/ns/abaevdict-site/entrypoints';
import module namespace shared = 'http://ossetic-studies.org/ns/abaevdict-site/shared' at './shared/shared.xq';
(: This module includes various ways of accessing dictionary resources by
their identifiers. Right now, this only applies to entries, but may apply to
other things as well :)
(: == ENTRY BY ID == :)
(: English as default language :)
declare %rest:path("dict/{$id}")
%rest:query-param("xpath", "{$xpath}", '')
%output:method("html")
%output:html-version('5')
function entry:by-id($id, $xpath) {
entry:by-id('en', $id, $xpath)
};
declare %rest:path("{$lang}/dict/{$id}")
%rest:query-param("xpath", "{$xpath}", '')
%output:method("html")
%output:html-version('5')
function entry:by-id($lang, $id, $xpath) {
let $doc-index := index-where($shared:sorted, fn { ./@xml:id=$id })
let $pagenum := ceiling($doc-index div $shared:items-per-page)
return web:redirect('../dict',
map:merge(({'page': $pagenum,
'entry': web:decode-url($id)},
if ($xpath != '') then {'xpath': $xpath})),
web:decode-url($id))
};

250
xq/site/restx_main.xq Normal file
View file

@ -0,0 +1,250 @@
declare namespace site = 'http://ossetic-studies.org/ns/abaevdict-site';
declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../abv-mod.xqm';
import module namespace home = 'http://ossetic-studies.org/ns/abaevdict-site/home' at './components/home.xqm';
import module namespace entries = 'http://ossetic-studies.org/ns/abaevdict-site/entries' at './components/entries.xqm';
import module namespace index = 'http://ossetic-studies.org/ns/abaevdict-site/index' at './components/index.xqm';
import module namespace search = 'http://ossetic-studies.org/ns/abaevdict-site/search' at './components/search.xqm';
(: =============================================================== :)
(: ============================ FAVICON ========================== :)
(: =============================================================== :)
declare %rest:path("/favicon.ico")
%rest:GET
function site:favicon() {
fetch:binary('../../static/favicon.ico')
};
(: HTML HEAD :)
declare function site:head($title as xs:string) {
<head>
<title>{$title}</title>
<script src="/static/htmx.min.js"></script>
<script src="/static/plotly-3.0.1.min.js" charset="utf-8"></script>
<link rel="stylesheet" href="/static/pico.classless.min.css"/>
<link rel="stylesheet" href="/static/abaev-html.css"/>
</head>
};
(: HEADER :)
declare function site:header($lang as xs:string, $href-other as xs:string) {
<header id="abv-header">
<nav>
<ul class="abv-brand">
<li><img class="abv-logo" src="/static/Abaev_logo.png" alt="Abaevdict logo"></img>
<strong>{
switch($lang)
case 'ru' return 'ИЭСОЯ'
default return `Abaevdict`
}</strong><i> β</i></li>
<li>{
(element {if ($lang = 'ru') then 'mark' else 'a'}{
if ($lang != 'ru') then
(attribute href {`{$href-other}`}) else (),
'ru'
}, ' / ',
element {if ($lang = 'ru') then 'a' else 'mark'}{
if ($lang = 'ru') then
(attribute href {`{$href-other}`}) else (),
'en'
})
}
</li>
</ul>
<ul>
<li><a href="./home">{
switch($lang)
case 'ru' return 'Главная'
default return 'Home'
}
</a></li>
<li><a href="./dictionary">{switch($lang)
case 'ru' return 'Просмотр'
default return 'Browse'
}
</a></li>
<li><a href="./search">{switch($lang)
case 'ru' return 'Поиск'
default return 'Search'
}
</a></li>
<li><a href="./index">{switch($lang)
case 'ru' return 'Указатель'
default return 'Index'
}</a></li>
<li><a href="#">{switch($lang)
case 'ru' return 'Литература'
default return 'References'
}</a></li>
</ul>
</nav>
</header>
};
(: PATHS :)
(: ZERO PATH :)
declare %rest:path("{$lang}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:null($lang as xs:string) {
site:home($lang)
};
declare %rest:path("/")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:null() {
site:null('en')
};
(: HOME PAGE :)
declare %rest:path("{$lang}/home")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:home($lang as xs:string) {
<html>
{
site:head(if ($lang = 'ru') then 'ИЭСОЯ В.И. Абаева — Главная'
else 'Abaevdict — Main'),
<body>
{site:header($lang,`/{abv-m:invert-lang($lang)}/home`)}
<main>
{home:content($lang)}
</main>
</body>
}
</html>
};
declare %rest:path("/home")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:home() {
site:home('en')
};
(: DICTIONARY INTERFACE :)
declare %rest:path("{$lang}/dictionary")
%rest:query-param("query", "{$query}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:dictionary($lang as xs:string, $query as xs:string?) {
<html>
{
site:head(if($lang = 'ru') then 'ИЭСОЯ В.И. Абаева — Просмотр'
else 'Abaevdict — Browse'),
<body>
{site:header($lang,`/{abv-m:invert-lang($lang)}/dictionary`)}
{entries:sidebar($lang)}
<dialog id="modal_map"/>
<main id="abv-dict">
{entries:entries($lang,20,$query)}
</main>
</body>
}
</html>
};
declare %rest:path("dictionary")
%rest:query-param("query", "{$query}")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:dictionary($query as xs:string?) {
site:dictionary('en', $query)
};
(: INDEX INTERFACE :)
declare %rest:path("{$lang}/index")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:index($lang as xs:string) {
<html>
{
site:head(if ($lang = 'ru') then 'ИЭСОЯ В.И. Абаева — Указатель'
else 'Abaevdict — Index'),
<body>
{site:header($lang,`/{abv-m:invert-lang($lang)}/index`)}
<main>
{index:content($lang)}
</main>
</body>
}
</html>
};
declare %rest:path("index")
%rest:GET
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:index() {
site:index('en')
};
(: SEARCH INTERFACE :)
declare %rest:path("{$lang}/search")
%rest:form-param("pattern", "{$pattern}")
%rest:form-param("text", "{$text}")
%rest:GET
%rest:POST
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:search($lang as xs:string,
$pattern as xs:string?,
$text as xs:string?) {
<html>
{
site:head(if ($lang = 'ru') then 'ИЭСОЯ В.И. Абаева — Поиск'
else 'Abaevdict — Search'),
<body>
{site:header($lang,`/{abv-m:invert-lang($lang)}/search`)}
<main>
{search:content($lang,$pattern,$text)}
</main>
</body>
}
</html>
};
declare %rest:path("search")
%rest:form-param("pattern", "{$pattern}")
%rest:form-param("text", "{$text}")
%rest:GET
%rest:POST
%rest:produces("text/html")
%output:method("html")
%output:html-version('5')
function site:search($pattern as xs:string?,
$text as xs:string?) {
site:index('en')
};
0

View file

@ -1,75 +0,0 @@
module namespace search = 'http://ossetic-studies.org/ns/abaevdict-site/search';
import module namespace abv-m = 'http://ossetic-studies.org/ns/abaevdict-mod' at '../abv-mod.xqm';
import module namespace entry = 'http://ossetic-studies.org/ns/abaevdict-site/entrypoints' at './restx_entrypoints.xq';
(: =============================================================== :)
(: ======================= SEARCH ENTRYPOINT ===================== :)
(: =============================================================== :)
declare %rest:path("/search/{$path=.+}")
%rest:GET
function search:search-default($path as xs:string) {
web:forward(`/en/search/{$path}`)
};
declare %rest:path("{$lang}/search/new")
%rest:query-param("searchType", "{$searchType}", 'full')
%rest:query-param("searchQuery", "{$searchQuery}")
%output:method("html")
%output:html-version('5')
function search:search($lang, $searchType, $searchQuery) {
let $sd := abv-m:search($lang,$searchType,$searchQuery)
let $r1 := session:set('searchType', $searchType)
let $r2 := session:set('searchQuery', $searchQuery)
let $r3 := session:set('searchN', 1)
let $r4 := session:set('searchData', $sd)
return
if(array:size($sd) > 0) then
entry:by-id($lang,
$sd(1)('entry'),
'')
else
web:redirect('../search/clear')
};
declare %rest:path("{$lang}/search/next")
%output:method("html")
%output:html-version('5')
function search:search-next($lang) {
let $n as xs:integer := session:get('searchN')+1
let $r1 := session:set('searchN', $n)
let $sd := session:get('searchData')
return entry:by-id($lang, $sd($n)('entry'), ())
};
declare %rest:path("{$lang}/search/prev")
%output:method("html")
%output:html-version('5')
function search:search-prev($lang) {
let $n as xs:integer := session:get('searchN')-1
let $r1 := session:set('searchN', $n)
let $sd := session:get('searchData')
return entry:by-id($lang, $sd($n)('entry'), ())
};
declare %rest:path("{$lang}/search/position")
%rest:query-param("p", "{$p}")
%output:method("html")
%output:html-version('5')
function search:search-position($lang, $p as xs:integer) {
let $r1 := session:set('searchN', $p)
let $sd := session:get('searchData')
return entry:by-id($lang, $sd($p)('entry'), ())
};
declare %rest:path("{$lang}/search/clear")
%output:method("html")
%output:html-version('5')
function search:clearSearch($lang) {
let $r1 := session:delete('searchType')
let $r2 := session:delete('searchQuery')
let $r3 := session:delete('searchN')
let $r4 := session:delete('searchData')
return web:redirect('../dict')
};

View file

@ -1,165 +0,0 @@
module namespace shared = 'http://ossetic-studies.org/ns/abaevdict-site/shared';
declare namespace tei = "http://www.tei-c.org/ns/1.0";
import module namespace search = 'http://ossetic-studies.org/ns/abaevdict-site/search-modal' at '../components/search.xq';
(: =========================================================== :)
(: ================== GLOBAL VARIABLES ======================= :)
(: =========================================================== :)
(: Number of items per page :)
declare variable $shared:items-per-page as xs:integer := 20;
(: These variables are declared in module scope because they are needed in
different places, but they are still dynamically evaluated :)
declare variable $shared:lookup := doc('abaevdict_index/lookup.xml')/tei:table/tei:entry;
declare variable $shared:lookup-all := doc('abaevdict_index/lookup.xml')/tei:table/*;
declare variable $shared:total := ceiling(count($shared:lookup) div $shared:items-per-page);
(: This is still called sorted, because I haven't changed the code.
But it's now equal to lookup, because the lookup itself is now sorted. :)
declare variable $shared:sorted := $shared:lookup;
(: =============================================================== :)
(: ======================= COMMON STUFF ========================== :)
(: =============================================================== :)
declare function shared:head($title as xs:string, $script as node()* := ()) {
<head>
<script src="/static/jquery-3.7.1.min.js">
</script>
<script src="/static/abaev-global.js">
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css"/>
<link rel="stylesheet" href="{web:create-url('/static/abaev-html.css')}"/>
{$script}
<title>{$title}</title>
</head>
};
(: Navigation header and search dialog :)
declare function shared:header($lang as xs:string, $href-other as xs:string) {
(
<header>
<nav>
<ul class="abv-brand">
<li><img class="abv-logo" src="/static/Abaev_logo.png" alt="Abaevdict logo"></img>
<strong>{
switch($lang)
case 'ru' return 'ИЭСОЯ'
default return `Abaevdict`
}</strong><i> β</i></li>
<li>{
(element {if ($lang = 'ru') then 'mark' else 'a'}{
if ($lang != 'ru') then
(attribute class {'link'},
attribute data-href {`{$href-other}`}) else (),
'ru'
}, ' / ',
element {if ($lang = 'ru') then 'a' else 'mark'}{
if ($lang = 'ru') then
(attribute class {'link'},
attribute data-href {`{$href-other}`}) else (),
'en'
})
}
</li>
</ul>
<ul>
<li>
<form role="search" action="./search/new" id="abv-search">
{if (session:get('searchQuery')) then
<button class="link" data-href="./search/clear" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/>
</svg>
</button>
else
<select name="searchType" required="1">
<option selected="1" value="full">{if ($lang = 'ru')
then 'Везде' else 'All'}</option>
<option value="form">{if ($lang = 'ru')
then 'Формы' else 'Forms'}</option>
<option value="sense">{if ($lang = 'ru')
then 'Значения' else 'Senses'}</option>
<option value="example">{if ($lang = 'ru')
then 'Примеры' else 'Examples'}</option>
<option value="translation">{if ($lang = 'ru')
then 'Переводы' else 'Translations'}</option>
<option value="mentioned">{if ($lang = 'ru')
then 'Цит. формы' else 'Mentioned'}</option>
<option value="gloss">{if ($lang = 'ru')
then 'Глоссы' else 'Glosses'}</option>
<option value="etym">{if ($lang = 'ru')
then 'Этимологии' else 'Etymology'}</option>
</select>
}
<input name="searchQuery"
placeholder="{if ($lang = 'ru') then 'Искать' else 'Search'}"
value="{session:get('searchQuery')}"
aria-label="Search">
{if (exists(session:get('searchQuery'))) then
attribute disabled {"1"} }
</input>
{if (not(session:get('searchQuery'))) then
<button type="submit">
<svg id="abv-search-icon" xmlns="http://www.w3.org/2000/svg"
width="16" height="16"
viewBox="0 0 16 16"
fill="currentColor" class="bi bi-search">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
</svg>
</button>
else
(
if (session:get('searchN') > 1) then
<button type="button" class="link" data-href="./search/prev">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
</svg>
</button>,
<button type="button" id="abv-btn-searchResults">{session:get('searchN')}/{array:size(session:get('searchData'))}</button>
,
if (session:get('searchN') and session:get('searchN') < array:size(session:get('searchData'))) then
<button type="button" class="link" data-href="./search/next">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8"/>
</svg>
</button>
)
}
</form>
</li>
<li><a href="./home">{
switch($lang)
case 'ru' return 'Главная'
default return 'Home'
}
</a></li>
<li><a href="./dict">{switch($lang)
case 'ru' return 'Словарь'
default return 'Dictionary'
}
</a></li>
<li><a href="./index">{switch($lang)
case 'ru' return 'Указатель'
default return 'Index'
}</a></li>
<li><a href="#">{switch($lang)
case 'ru' return 'Литература'
default return 'References'
}</a></li>
</ul>
</nav>
</header>,
search:content($lang)
)
};
declare function shared:invert-lang($lang as xs:string) as xs:string {
if($lang = 'en') then 'ru' else 'en'
};