← all demos
xsl:stylesheet(
    default-mode="m:docbook",
    exclude-result-prefixes="array db f fp l m map mp v vp xs",
    version="3.0",
    xmlns="http://www.w3.org/1999/xhtml",
    xmlns:array="http://www.w3.org/2005/xpath-functions/array",
    xmlns:db="http://docbook.org/ns/docbook",
    xmlns:err="http://www.w3.org/2005/xqt-errors",
    xmlns:f="http://docbook.org/ns/docbook/functions",
    xmlns:fp="http://docbook.org/ns/docbook/functions/private",
    xmlns:l="http://docbook.org/ns/docbook/l10n",
    xmlns:m="http://docbook.org/ns/docbook/modes",
    xmlns:map="http://www.w3.org/2005/xpath-functions/map",
    xmlns:mp="http://docbook.org/ns/docbook/modes/private",
    xmlns:v="http://docbook.org/ns/docbook/variables",
    xmlns:vp="http://docbook.org/ns/docbook/variables/private",
    xmlns:xs="http://www.w3.org/2001/XMLSchema",
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform")
  xsl:include(href="../standalone-functions.xsl")
  xsl:key(match="*", name="id", use="@xml:id")
  xsl:key(match="*", name="genid", use="generate-id(.)")
  vp:translate-suppress-elements := tokenize($translate-suppress-elements, '\s+')
  function f:translate-attribute(node as element()) -> boolean?:
    if local-name($node) = $vp:translate-suppress-elements:
      <-- false()
  function f:attributes(node as element(), attributes as attribute()*) -> attribute()*:
    <-- f:attributes($node, $attributes, local-name($node), ())
  function f:attributes -> attribute()*:
    param node as element()
    param attributes as attribute()*
    param extra-classes as string*
    param exclude-classes as string*
    names := distinct-values($attributes/node-name())
    foreach $names:
      namespace := namespace-uri-from-QName(.)
      name := .
      values as string* := $attributes[node-name()=$name]/string()
      if exists($values):
        @{$name}
    if not(QName('', 'class') = $attributes/node-name()):
      roles := (tokenize(normalize-space(string-join($extra-classes, ' '))),
                           tokenize(normalize-space($node/@role)),
                           if ($node/@revisionflag)
                           then 'rev'||$node/@revisionflag
                           else ())
      exclude := tokenize(normalize-space(string-join($exclude-classes, ' ')))
      classes as string* :=
        foreach distinct-values($roles):
          xsl:sort(select=".")
          if not(. = $exclude):
            <-- .
      if exists($classes):
        @class
    translate := f:translate-attribute($node)
    if exists($translate):
      @translate
  function f:is-true(value) -> boolean (visibility="public"):
    choose:
      when empty($value):
        <- false()
      when $value castable as xs:boolean:
        <- xs:boolean($value)
      when $value castable as xs:integer:
        <- xs:integer($value) != 0
      when string($value) = ('true', 'yes'):
        <- true()
      when string($value) = ('false', 'no'):
        <- false()
      else:
        xsl:message(expand-text="yes", terminate="yes") = Warning: interpreting ‘{$value}’ as true.
        <- true()
  function f:orderedlist-startingnumber(list as element(db:orderedlist)) -> integer:
    choose:
      when not($list/@continuation = 'continues'):
        <-- 1
      when empty($list/preceding::db:orderedlist):
        xsl:message
          "Warning: orderedlist continuation=continues,"
          "but no preceding list"
        <-- 1
      else:
        plist := $list/outermost(preceding::db:orderedlist)[last()]
        <-- f:orderedlist-startingnumber($plist)
                            + count($plist/db:listitem)
  function f:l10n-language(target as element()) -> string (cache="yes"):
    nearest-lang := $target/ancestor-or-self::*[@xml:lang][1]/@xml:lang
    mc-language as string := if (exists($gentext-language))
                        then $gentext-language
                        else if (exists($nearest-lang) and $nearest-lang = '')
                             then $default-language
                             else ($nearest-lang, $default-language)[1]
    language := lower-case($mc-language)
    adjusted-language := if (contains($language, '-'))
                        then substring-before($language, '-')
                             || '_' || substring-after($language, '-')
                        else $language
    choose:
      when doc-available(
                      resolve-uri($adjusted-language||'.xml', $v:localization-base-uri)):
        <-- $adjusted-language
      when doc-available(
                      resolve-uri(
                        substring-before($adjusted-language, '_')||'.xml', $v:localization-base-uri)):
        <-- substring-before($adjusted-language,'_')
      else:
        xsl:message
          "No localization exists for ""
          <-- $adjusted-language
          "" or ""
          <-- substring-before($adjusted-language,'_')
          "". Using default ""
          <-- $default-language
          ""."
        <-- $default-language
  function f:gentext-letters(node as element()) -> element(l:letters)?:
    <-- f:gentext-letters-for-language($node)
  function f:gentext-letters-for-language(node as element()) -> element(l:letters)?:
    lang := f:l10n-language($node)
    l10n := fp:localization($lang)
    letters := $l10n/l:letters
    if empty($letters):
      xsl:message(select="'No letters for', $lang")
    if count($letters) gt 1:
      xsl:message(select="'Multiple letters for localization:', $lang")
    <-- $letters[1]
  function fp:properties(context as element(), properties as array(map(*))) -> map(*):
    props as map(*)* :=
      foreach 1 to array:size($properties):
        map := array:get($properties, .)
        nodes as node()* :=
          xsl:evaluate(context-item="$context", xpath="$map?xpath")
        if exists($nodes):
          <-- $map
    choose:
      when empty($props):
        xsl:message(
            select="'No properties for ' || local-name($context)",
            use-when="'properties' = $v:debug")
        <-- map { }
      else:
        <-- $props[1]
  function f:date-format(context as element()) -> string:
    format := f:pi($context, 'date-format')
    choose:
      when $context/*:
        <-- 'apply-templates'
      when string($context) castable as xs:dateTime:
        choose:
          when $format:
            <-- $format
          else:
            <-- $date-dateTime-format
      when string($context) castable as xs:date:
        choose:
          when $format:
            <-- $format
          else:
            <-- $date-date-format
      else:
        <-- 'apply-templates'
  function fp:replace-element -> array(*):
    param lines as array(*)
    param elemno as integer
    param new-elem as item()*
    <-- fp:replace-element($lines, $elemno, 1, $new-elem, [])
  function fp:replace-element -> array(*):
    param array as array(*)
    param elemno as integer
    param count as integer
    param new-elem as item()*
    param newarray as array(*)
    choose:
      when $count gt array:size($array):
        <-- $newarray
      when $count = $elemno:
        <-- fp:replace-element($array, $elemno, $count+1, $new-elem,
                                     array:append($newarray, $new-elem))
      else:
        <-- fp:replace-element($array, $elemno, $count+1, $new-elem,
                                     array:append($newarray, $array($count)))
  function f:target(id as string, context as node()) -> element()* (cache="yes"):
    <-- key('id', $id, root($context))
  function f:href(context as node(), node as element()) -> string (cache="yes"):
    <-- '#' || f:generate-id($node)
  vp:gidmap := map {
  'acknowledgements': 'ack',
  'annotation': 'an',
  'appendix': 'ap',
  'article': 'art',
  'bibliodiv': 'bd',
  'bibliography': 'bi',
  'book': 'bo',
  'chapter': 'ch',
  'colophon': 'co',
  'dedication': 'ded',
  'equation': 'eq',
  'example': 'ex',
  'figure': 'fig',
  'glossary': 'g',
  'glossdiv': 'gd',
  'glossentry': 'ge',
  'glossterm': 'gt',
  'itemizedlist': 'il',
  'listitem': 'li',
  'orderedlist': 'ol',
  'part': 'part',
  'preface': 'p',
  'procedure': 'proc',
  'refentry': 're',
  'reference': 'ref',
  'refsect1': 'rs1_',
  'refsect2': 'rs2_',
  'refsect3': 'rs3_',
  'sect1': 's1_',
  'sect2': 's2_',
  'sect3': 's3_',
  'sect4': 's4_',
  'sect5': 's5_',
  'section': 's',
  'table': 'tab',
  'variablelist': 'vl',
  'varlistentry': 'vle'
  }
  function f:generate-id(node as element()) -> string (cache="yes"):
    <-- f:generate-id($node, true())
  function f:generate-id(node as element(), use-xml-id as boolean) -> string (cache="yes"):
    choose:
      when $use-xml-id and $node/@xml:id:
        <-- $node/@xml:id/string()
      when empty($node/parent::*):
        <-- $generated-id-root
      else:
        aid := f:generate-id($node/parent::*, $use-xml-id)
        type := (map:get($vp:gidmap, local-name($node)),
                                         local-name($node))[1]
        prec := $node/preceding-sibling::*[node-name(.)=node-name($node)]
        <-- $aid || $generated-id-sep || $type || string(count($prec)+1)
  function f:id(node as element()) -> string (cache="yes"):
    <-- if ($node/@xml:id)
                        then $node/@xml:id/string()
                        else f:generate-id($node)
  function f:unique-id(node as element()) -> string (cache="yes"):
    <-- f:generate-id($node, false())
  function fp:css-properties(context as element()?) -> attribute()?:
    generic-attributes as map(*) :=
      xsl:map
        foreach $context/@css:*:
          xsl:map-entry(key="local-name(.)", select="string(.)")
    media-attributes as map(*) :=
      xsl:map
        foreach $context/@*:
          if namespace-uri(.) = 'https://xsltng.docbook.org/ns/css#' || $output-media:
            xsl:map-entry(key="local-name(.)", select="string(.)")
    attributes as map(*) := map:merge(($generic-attributes, $media-attributes),
                                  map { 'duplicates': 'use-last' })
    if map:size($attributes) != 0:
      xsl:iterate(select="map:keys($attributes)")
        param css := ''
        xsl:on-completion
          @style
        name := .
        value := map:get($attributes, .)
        xsl:next-iteration
          css := $css || $name || ':' || $value || ';'
  function f:spaces(length as item()*) -> string?:
    choose:
      when empty($length):
      when count($length) gt 1:
        <-- f:spaces(string-join($length ! string(.), ''))
      when $length castable as xs:integer:
        length := xs:integer($length)
        choose:
          when $length lt 0:
          when $length lt 10:
            <-- substring('          ', 1, $length)
          else:
            <-- '          ' || f:spaces($length - 10)
      else:
        <-- f:spaces(string-length(string($length)))
  function fp:lookup-string -> node()*:
    param context as element()
    param lookup as element()
    param table-name as string
    value := $lookup/*[node-name(.)=node-name($context)]
    if count($value) gt 1:
      xsl:message(expand-text="yes") = Duplicate {$table-name} for {node-name($context)}
    <-- if (empty($value))
                        then $lookup/db:_default/node()
                        else $value[1]/node()
  function fp:separator(node as element(), key as string) -> node()*:
    <-- fp:localization-template($node, 'separator')
  function f:label-separator(node as element()) -> node()*:
    <-- fp:separator($node, 'label-separator')
  function fp:parse-key-value-pairs(strings as string*) -> map(xs:string,xs:string):
    <-- fp:parse-key-value-pairs($strings, map { })
  function fp:parse-key-value-pairs -> map(xs:string,xs:string):
    param strings as string*
    param map as map(xs:string,xs:string)
    car := $strings[1]
    cdr := subsequence($strings, 2)
    key := if (contains($car, ':'))
                                   then substring-before($car, ':')
                                   else '_default'
    value := if (contains($car, ':'))
                                     then substring-after($car, ':')
                                     else $car
    choose:
      when empty($car):
        <-- $map
      when map:contains($map, $key):
        xsl:message(select="'Warning: ignoring duplicate key:', $key")
        <-- fp:parse-key-value-pairs($cdr, $map)
      else:
        <-- fp:parse-key-value-pairs($cdr,
                               map:put($map, $key, $value))
  function f:refsection(node as element()) -> boolean:
    <-- $node/self::db:refsection
                        or $node/self::db:refsect1
                        or $node/self::db:refsect2
                        or $node/self::db:refsect3
  function f:section(node as element()) -> boolean (visibility="public"):
    <-- $node/self::db:section
                        or $node/self::db:sect1
                        or $node/self::db:sect2
                        or $node/self::db:sect3
                        or $node/self::db:sect4
                        or $node/self::db:sect5
                        or f:refsection($node)
  function f:section-depth(node as element()?) -> integer (visibility="public"):
    choose:
      when empty($node):
        <- 0
      when $node/self::db:section:
        <- count($node/ancestor::db:section) + 1
      when $node/self::db:sect1 or $node/self::db:sect2
                    or $node/self::db:sect3 or $node/self::db:sect4
                    or $node/self::db:sect5:
        <- xs:integer(substring(local-name($node), 5))
      when $node/self::db:refsection:
        <- count($node/ancestor::db:refsection)+1
      when $node/self::db:refsect1 or $node/self::db:refsect2
                    or $node/self::db:refsect3:
        <- xs:integer(substring(local-name($node), 8))
      else:
        <-- f:section-depth($node/parent::*)
  function f:step-number(node as element(db:step)) -> integer+:
    xsl:iterate(select="reverse($node/ancestor-or-self::*)")
      param number := ()
      choose:
        when self::db:procedure:
          <-- $number
          xsl:break
        when self::db:step:
          xsl:next-iteration
            number := (count(preceding-sibling::db:step)+1, $number)
        else:
          xsl:next-iteration
            number := $number
  function f:step-numeration(node as element(db:step)) -> string:
    depth := count(f:step-number($node))
    depth := $depth
                        mod string-length($procedure-step-numeration)
    depth := if ($depth eq 0)
                        then string-length($procedure-step-numeration)
                        else $depth
    <-- substring($procedure-step-numeration, $depth, 1)
  function f:orderedlist-item-number(node as element(db:listitem)) -> integer+:
    xsl:iterate(select="reverse($node/ancestor-or-self::*)")
      param number := ()
      xsl:on-completion(select="$number")
      choose:
        when self::db:listitem[parent::db:orderedlist]:
          xsl:next-iteration
            number := (count(preceding-sibling::db:listitem)
                                   + f:orderedlist-startingnumber(parent::db:orderedlist),
                                   $number)
        else:
          xsl:next-iteration
            number := $number
  function f:orderedlist-item-numeration(node as element(db:listitem)) -> string:
    numeration := $node/parent::db:orderedlist/@numeration
    choose:
      when exists($numeration):
        choose:
          when $numeration = 'upperalpha':
            <-- 'A'
          when $numeration = 'loweralpha':
            <-- 'a'
          when $numeration = 'upperroman':
            <-- 'I'
          when $numeration = 'lowerroman':
            <-- 'i'
          else:
            <-- '1'
      else:
        depth := count(f:orderedlist-item-number($node))
        depth := $depth
                            mod string-length($orderedlist-item-numeration)
        depth := if ($depth eq 0)
                            then string-length($orderedlist-item-numeration)
                            else $depth
        <-- substring($orderedlist-item-numeration, $depth, 1)
  function f:tokenize-on-char(string as string, char as string) -> string*:
    ch := substring($char||' ', 1, 1)
    tchar := if ($ch = ('.', '?', '*', '{', '}', '\', '\[', '\]'))
                        then '\' || $ch
                        else $ch
    <-- tokenize($string, $tchar)
  function f:uri-scheme(uri as string) -> string?:
    if matches($uri, '^[-a-zA-Z0-9]+:'):
      <-- replace($uri, '^([-a-zA-Z0-9]+):.*$', '$1')
  function f:relative-path(base as string, path as string) -> string:
    choose:
      when exists(f:uri-scheme($path)) and f:uri-scheme($path) ne 'file':
        <-- $path
      else:
        bpath := replace($base, '^file:/+', '')
        ppath := replace($path, '^file:/+', '')
        base-parts := tokenize($bpath, '/')[position() lt last()]
        path-parts := tokenize($ppath, '/')
        common-prefix as string* :=
          xsl:iterate(select="$base-parts")
            param pos := 1
            param common := ()
            xsl:on-completion(select="$common")
            if $base-parts[$pos] = $path-parts[$pos]:
              xsl:next-iteration
                pos := $pos + 1
                common := ($common, $base-parts[$pos])
        base-tail := $base-parts[position() gt count($common-prefix)]
        path-tail := $path-parts[position() gt count($common-prefix)]
        final-parts as string* :=
          foreach 1 to count($base-tail):
            <-- '..'
          <-- $path-tail
        <-- string-join($final-parts, '/')
  function f:orientation-class(node as element()) -> string?:
    nearest := ($node/ancestor-or-self::*[contains-token(@role,'landscape')
                                                   or contains-token(@role,'portrait')]
                         | $node/ancestor-or-self::db:table[@orient]
                         | $node/ancestor-or-self::db:informaltable[@orient])[last()]
    choose:
      when $output-media != 'print':
      when $nearest/@orient and $nearest/@orient = 'land':
        <-- 'landscape'
      when $nearest/@orient:
        <-- 'portrait'
      when contains-token($nearest/@role, 'landscape'):
        <-- 'landscape'
      else:
        <-- 'portrait'
  function f:conditional-orientation-class(node as element()) -> string?:
    parent := $node/parent::element() ! f:orientation-class(.)
    orient := f:orientation-class($node)
    choose:
      when exists($parent) and $parent eq $orient:
      else:
        <-- $orient
  function f:global-syntax-highlighter(context as node()) -> string (cache="yes"):
    choose:
      when f:pi($context/root()/*, 'syntax-highlighter'):
        <-- f:pi($context/root()/*, 'syntax-highlighter')
      when f:pi($context/root()/*/db:info, 'syntax-highlighter'):
        <-- f:pi($context/root()/*/db:info, 'syntax-highlighter')
      else:
        <-- $verbatim-syntax-highlighter
  function fp:add-resource-base-uri(path as string) -> string:
    choose:
      when starts-with($path, '/') or starts-with($path, 'file:'):
        <-- $path
      when ends-with($resource-base-uri, '/'):
        <-- $resource-base-uri || $path
      else:
        <-- $resource-base-uri || '/' || $path