Assume you have an edition in which all references to places are tagged. The references point to a central authority file containing further information on each place. In the simplest case this might be a separate TEI document with the list of places contained in /TEI/standoff/listPlace
. An example (taken from the Alfred Escher Briefedition) is attached below.
As an alternative entry point into the edition, we may want to provide users a page on which they can browse through all place names and see their location on a map. Since there might a large number of places, we ideally want to group them by first letter, plus provide a search feature for filtering. Fortunately, the tei-publisher-components
library (since version 1.33.0) provides a webcomponent for this purpose: pb-split-list
.
The component will retrieve the information to display from an API endpoint. This endpoint should return a JSON object with
The returned JSON record may look like this:
{
"items": [
"<span class=\"place\"><a href=\"Aix-les-Bains (F)?category=A&search=le\">Aix-les-Bains (F)</a></span>"
],
"categories": [
{
"category": "A",
"count": 1
}
{
"category": "C",
"count": 1
},
{
"category": "D",
"count": 2
},
// ... more ...
{
"category": "All",
"count": 54
}
]
}
This example has only one place to be shown under the currently selected category (letter ‘A’): “Aix-les-Bains”.
Our first task now is to implement an API endpoint which returns a JSON record as shown above. For this we have to
modules/custom-api.json
We won’t dive into the Open API standard here. Important to note is just that TEI Publisher uses JSON instead of YAML for the specification. For a more detailed explanation of Open API 3, refer to the tutorial.
In modules/custom-api.json
, insert the following definition into the paths
object:
"paths": {
"/api/places": {
"get": {
"summary": "List places",
"description": "Retrieve list of places in format required by pb-split-list",
"operationId": "custom:places",
"parameters": [
{
"name": "category",
"in": "query",
"schema": {
"type": "string",
"example": "A"
}
},
{
"name": "limit",
"in": "query",
"schema": {
"type": "integer",
"default": 50
}
},
{
"name": "search",
"in": "query",
"schema":{
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Categories and places to display",
"content": {
"application/json": {
"schema":{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
}
},
"categories": {
"type": "array",
"items": {
"type": "object",
"properties": {
"category": {
"type": "string"
},
"count": {
"type": "integer"
}
}
}
}
}
}
}
}
}
}
}
},
// ... existing definitions
}
TEI Publisher splits the Open API definition into two parts: modules/lib/api.json
and modules/custom-api.json
. The first should never be modified. When you upgrade TEI Publisher, this file will likely be replaced by a newer version. You are allowed to overwrite definitions found in modules/lib/api.json
within modules/custom-api.json
though, see the chapter on Can I use a custom table of contents?.
If you reload the API documentation page, you’ll already see the new endpoint popping up in the list. However, trying to call it will result in an error: “Function custom:places could not be resolved”. We can deduce from this that the underlying library is searching for a function custom:places
but couldn’t find it. The name custom:places
in fact is what we defined in the operationId
property of the specification.
Let’s implement this function then. As you may have guessed already, it should go into modules/custom-api.xql
. We first need to declare the TEI namespace and add an import for the config
module. Paste the following two lines before the first import:
declare namespace tei="http://www.tei-c.org/ns/1.0";
import module namespace config="http://www.tei-c.org/tei-simple/config" at "config.xqm";
Next we add two functions to the file:
declare function api:places($request as map(*)) {
let $search := normalize-space($request?parameters?search)
let $letterParam := $request?parameters?category
let $limit := $request?parameters?limit
let $places :=
if ($search and $search != '') then
doc($config:data-root || "/playground/places.xml")//tei:listPlace/tei:place[matches(@n, "^" || $search, "i")]
else
doc($config:data-root || "/playground/places.xml")//tei:listPlace/tei:place
let $sorted := sort($places, "?lang=de-DE", function($place) { lower-case($place/@n) })
let $letter :=
if (count($places) < $limit) then
"Alle"
else if ($letterParam = '') then
substring($sorted[1], 1, 1) => upper-case()
else
$letterParam
let $byLetter :=
if ($letter = 'Alle') then
$sorted
else
filter($sorted, function($entry) {
starts-with(lower-case($entry/@n), lower-case($letter))
})
return
map {
"items": api:output-place($byLetter, $letter, $search),
"categories":
if (count($places) < $limit) then
[]
else array {
for $index in 1 to string-length('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
let $alpha := substring('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index, 1)
let $hits := count(filter($sorted, function($entry) { starts-with(lower-case($entry/@n), lower-case($alpha))}))
where $hits > 0
return
map {
"category": $alpha,
"count": $hits
},
map {
"category": "Alle",
"count": count($sorted)
}
}
}
};
declare function api:output-place($list, $category as xs:string, $search as xs:string?) {
array {
for $place in $list
let $categoryParam := if ($category = "all") then substring($place/@n, 1, 1) else $category
let $params := "category=" || $categoryParam || "&search=" || $search
let $label := $place/@n/string()
return
<span class="place">
<a href="{$label}?{$params}">{$label}</a>
</span>
}
};
If we now again test the API endpoint via the API documentation page, we should see the expected JSON output. We can also play around with the parameters, e.g. filter by name prefix via parameter search
.