1 <?xml version='1.0' encoding='UTF-8' ?>
3 Nick's web site: XHTML output stage
5 Copyright © 2018-2022 Nick Bowler
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program. If not, see <https://www.gnu.org/licenses/>
20 <xsl:stylesheet version='1.0'
21 xmlns='http://www.w3.org/1999/xhtml'
22 xmlns:xhtml='http://www.w3.org/1999/xhtml'
23 xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
24 xmlns:func='http://exslt.org/functions'
25 xmlns:exslt='http://exslt.org/common'
26 xmlns:f='http://draconx.ca/my-functions'
27 extension-element-prefixes='exslt func f'
28 exclude-result-prefixes='xhtml'>
30 <xsl:import href='layouts/functions.xsl' />
31 <xsl:output cdata-section-elements='style script' />
33 <xsl:param name='source-uri'
34 select='"//git.draconx.ca/gitweb/homepage.git/"' />
35 <xsl:param name='site-title' select='"The Citrine Citadel"' />
36 <xsl:param name='section-links' select='//document/section-links' />
38 <xsl:template match='node()|@*'>
39 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
42 <xsl:template name='notransform' mode='notransform' match='node()|@*'>
44 <xsl:apply-templates mode='notransform' select='node()|@*' />
48 <!-- Add rel attributes to external links -->
49 <xsl:template match='xhtml:a[starts-with(@href,"http://")
50 or starts-with(@href,"https://")
51 or starts-with(@href,"//")]'>
52 <xsl:variable name='domain'
53 select='substring-before(
54 concat(translate(substring-after(@href, "//"), ":", "/"), "/"),
58 <xsl:apply-templates select='@*' />
59 <xsl:if test='not($domain="draconx.ca"
60 or f:ends-with($domain, ".draconx.ca"))'>
61 <xsl:attribute name='rel'>
63 <xsl:value-of select='@rel' />
64 <xsl:text> </xsl:text>
66 <xsl:text>external noopener noreferrer</xsl:text>
69 <xsl:apply-templates select='node()' />
73 <xsl:template match='xhtml:h2[@id]'>
74 <xsl:variable name='fragment' select='concat("#", @id)' />
76 <xsl:apply-templates select='node()|@*' />
77 <xsl:if test='$section-links = "yes"'>
78 <xsl:text> </xsl:text>
79 <small class='permalink'>
80 (<a href='{$fragment}'><xsl:value-of select='$fragment' /></a>)
87 Allow abbr to apply to document titles too, since these are generated
88 and kramdown's abbr support won't influence them. We do this by just
89 checking each word of the heading to see if is identical to an existing
90 abbr tag, and just substituting that in its place.
92 <xsl:key name='abbr' match='xhtml:abbr' use='string(.)' />
93 <xsl:template name='insert-abbr' match='xhtml:h1/text()'>
94 <xsl:param name='string' select='normalize-space(.)' />
96 <xsl:variable name='head'
97 select='substring-before(concat($string, " "), " ")' />
98 <xsl:variable name='tail' select='substring-after($string, " ")' />
99 <xsl:variable name='match' select='key("abbr", $head)[1]' />
102 <xsl:when test='$match'><xsl:apply-templates select='$match' /></xsl:when>
103 <xsl:otherwise><xsl:value-of select='$head' /></xsl:otherwise>
105 <xsl:if test='$tail'>
106 <xsl:text> </xsl:text>
107 <xsl:call-template name='insert-abbr'>
108 <xsl:with-param name='string' select='$tail' />
114 Convert caption attribute on tables into proper caption elements, to allow
115 a simple way to add captions to kramdown tables.
117 <xsl:template match='@caption[parent::xhtml:table]' />
118 <xsl:template match='xhtml:table[@caption]'>
120 <xsl:apply-templates select='@*' />
121 <caption><xsl:value-of select='normalize-space(@caption)' /></caption>
122 <xsl:apply-templates select='node()' />
127 Delete style elements, as they will get hoisted occur under <head> below.
128 If the generate-listing attribute was specified, produce a code listing
129 where the style attribute was found.
131 <xsl:template match='xhtml:style|@generate-listing[parent::xhtml:style]' />
132 <xsl:template match='xhtml:style[@generate-listing]'>
133 <pre>⁠<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
137 Attempt to wrap the first bit of linked email addresses to allow
138 linewrapping to occur after the '@'.
140 <xsl:template match='xhtml:a[starts-with(@href,"mailto:")]/text()'>
141 <xsl:variable name='addr' select='substring-after(../@href, "mailto:")' />
143 <xsl:variable name='wrap'
144 select='concat(substring-before($addr, "@"), "@")' />
147 <xsl:when test='contains(., $wrap)'>
148 <xsl:value-of select='substring-before(., $wrap)' />
150 <xsl:value-of select='$wrap' />
152 <xsl:value-of select='substring-after(., $wrap)' />
161 Add a simple way to reference a document node by ID and include the XHTML
162 code listing directly in the document.
164 <xsl:template match='xhtml:generate-xhtml-listing'>
165 <xsl:variable name='target' select='@target' />
167 <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
171 <!-- For paragraphs containing only kbd elements, wrap in blockquote. -->
172 <xsl:template match='xhtml:p[*[last()=count(../xhtml:kbd|../xhtml:br)]]'>
175 <xsl:apply-templates select='node()|@*' />
181 Wrap each word of text in kbd elements in spans, so they can be styled
182 to avoid linebreaks in the middle of option names and other bad places.
184 <xsl:template name='spanify-text' match='xhtml:kbd/text()'>
185 <xsl:param name='text' select='normalize-space(.)' />
186 <xsl:variable name='firstword' select='substring-before($text, " ")' />
188 <xsl:when test='$firstword'>
189 <span><xsl:value-of select='$firstword' /></span>
190 <xsl:text> </xsl:text>
192 <xsl:when test='$text'>
193 <span><xsl:value-of select='$text' /></span>
196 <xsl:if test='$firstword'>
197 <xsl:call-template name='spanify-text'>
198 <xsl:with-param name='text' select='substring-after($text, " ")' />
203 <xsl:template match='copyright'>
205 <xsl:text>Copyright © </xsl:text>
206 <xsl:value-of select='text()' />
207 <xsl:text>.</xsl:text>
211 <xsl:template match='license'>
212 <xsl:variable name='node' select='.' />
215 <xsl:when test='/document/image[license/identifier != $node/identifier]'>
216 <xsl:text>Except as otherwise noted, copying</xsl:text>
218 <xsl:otherwise>Copying</xsl:otherwise>
220 <xsl:text> and distribution of this material</xsl:text>
221 <xsl:if test='normalize-space(modification-allowed)="yes"'>
222 <xsl:text>, with or without modification,</xsl:text>
224 <xsl:text> is permitted under the terms of the </xsl:text>
226 <xsl:attribute name='href'>
227 <xsl:value-of select='normalize-space(uri)' />
229 <xsl:value-of select='name' />
231 <xsl:text>.</xsl:text>
235 <func:function name='f:matching-child'>
236 <xsl:param name='child' select='./copyright-holder' />
237 <xsl:param name='node' select='.' />
238 <xsl:param name='nodeset' select='$node/../*[name()=name($node)]' />
240 <func:result select='$nodeset[*[name()=name($child)]=$child]' />
243 <func:function name='f:attribution-order'>
244 <xsl:param name='a' />
245 <xsl:param name='b' />
247 <xsl:variable name='docmatch'
248 select='number($a/copyright-holder = /document/copyright-holder)
249 - number($b/copyright-holder = /document/copyright-holder)' />
251 <xsl:variable name='authmatch'
252 select='count(f:matching-child($a/copyright-holder, $a))
253 - count(f:matching-child($b/copyright-holder, $b))' />
255 <xsl:variable name='licmatch'
256 select='count(f:matching-child($a/license, $a))
257 - count(f:matching-child($b/license, $b))' />
260 <xsl:when test='$docmatch'><func:result select='$docmatch' /></xsl:when>
261 <xsl:when test='$authmatch'><func:result select='$authmatch' /></xsl:when>
262 <xsl:when test='$licmatch'><func:result select='$licmatch' /></xsl:when>
263 <xsl:otherwise><func:result select='"nope"' /></xsl:otherwise>
267 <xsl:template match='image/license'>
268 <xsl:text>, </xsl:text>
269 <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
272 <xsl:template match='image'>
274 <xsl:when test='position() = 1'>, except </xsl:when>
275 <xsl:when test='position() = last()'> and </xsl:when>
276 <xsl:otherwise>, </xsl:otherwise>
278 <a href='{uri}'><xsl:value-of select='title' /></a>
279 <xsl:text> © </xsl:text>
280 <xsl:value-of select='copyright' />
281 <xsl:apply-templates select='license' />
284 <xsl:template name='image-attribution'>
285 <xsl:variable name='images-fragment'>
286 <xsl:for-each select='/document/image'>
287 <xsl:sort select='number(copyright-holder = /document/copyright-holder)'
288 data-type='number' order='descending' />
289 <xsl:sort select='count(f:matching-child(copyright-holder))'
290 data-type='number' order='descending' />
291 <xsl:sort select='copyright-holder' order='descending' />
292 <xsl:sort select='count(f:matching-child(license))'
293 data-type='number' order='descending' />
294 <xsl:sort select='license/identifier' order='descending' />
296 <xsl:call-template name='notransform' />
299 <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
301 <xsl:variable name='abbrev-split'
302 select='count($images[copyright-holder = $images[1]/copyright-holder
303 and license/identifier = $images[1]/license/identifier])' />
305 <xsl:variable name='abbrev-years-fragment'>
306 <xsl:for-each select='$images[$abbrev-split >= position()]/copyright-year'>
307 <xsl:sort data-type='number' />
308 <copyright-year><xsl:value-of select='.' /></copyright-year>
311 <xsl:variable name='abbrev-years'
312 select='exslt:node-set($abbrev-years-fragment)/*' />
315 <xsl:text>Images © </xsl:text>
316 <xsl:value-of select='$abbrev-years[1]' />
317 <xsl:if test='$abbrev-years[last()] != $abbrev-years[1]'>
318 <xsl:value-of select='concat("–", $abbrev-years[last()])' />
320 <xsl:value-of select='concat(" ", $images[1]/copyright-holder)' />
321 <xsl:apply-templates select='$images[1]/license' />
322 <xsl:apply-templates select='$images[position() > $abbrev-split]' />
323 <xsl:text>.</xsl:text>
327 <xsl:template match='source'>
329 <xsl:text>This document was compiled</xsl:text>
331 <xsl:when test='file'>
332 <xsl:text> from </xsl:text>
333 <a href='{concat($source-uri, "blob/", revision, ":", file)}'>
334 <xsl:value-of select='file' />
337 <xsl:when test='dir'>
338 <xsl:text> from </xsl:text>
339 <a href='{concat($source-uri, "tree/", revision, ":", dir)}'>
340 <xsl:value-of select='dir' />
344 <xsl:text> on </xsl:text>
345 <xsl:value-of select='compiletime' />
346 <xsl:text>.</xsl:text>
350 <!-- Article info block inserted between heading and main contents -->
351 <xsl:template match='xhtml:h1[not(preceding::xhtml:h1)]'>
352 <xsl:variable name='nodes'
353 select='/document/article/published|//xhtml:*[@article-info]' />
355 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
357 <xsl:if test='$nodes'>
358 <div id='article-info'>
359 <xsl:apply-templates mode='article-info' select='$nodes'>
360 <xsl:sort data-type='number'
361 select='number(generate-id(..)=generate-id(/document/article))' />
362 </xsl:apply-templates>
367 <xsl:template mode='article-info' match='/document/article/published'>
369 <xsl:text>Posted </xsl:text>
370 <xsl:value-of select='/document/article/published' />
371 <xsl:if test='/document/article/updated'>
372 <xsl:if test='/document/article/updated != /document/article/published'>
373 <xsl:text>, last updated </xsl:text>
374 <xsl:value-of select='/document/article/updated' />
380 <xsl:template match='*[@article-info]' />
381 <xsl:template mode='article-info' match='*[@article-info]'>
383 <xsl:apply-templates select='@*[local-name() != "article-info"]' />
384 <xsl:apply-templates select='node()' />
388 <xsl:template name='imgpara' match='xhtml:p[count(*)=1]
389 [normalize-space(text())=""]
390 [descendant::xhtml:img]'>
392 <xsl:apply-templates select='@*[local-name() != "class"]' />
393 <xsl:attribute name='class'>
394 <xsl:if test='@class'>
395 <xsl:value-of select='concat(@class, " ")' />
397 <xsl:text>img</xsl:text>
399 <xsl:apply-templates select='node()' />
403 <xsl:key name='gallery'
404 match='xhtml:html/xhtml:p[descendant::*[@generate-gallery]]'
406 ( ( preceding-sibling::*[not(descendant::*[@generate-gallery])][1]
407 /following-sibling::* ) | self::* ) [1])' />
409 <xsl:template match='@generate-gallery' />
410 <xsl:template match='xhtml:html/xhtml:p[descendant::*[@generate-gallery]]' />
411 <xsl:template match='xhtml:html/xhtml:p[key("gallery", generate-id(.))]'>
412 <xsl:variable name='images' select='key("gallery", generate-id(.))' />
414 <xsl:when test='count($images) > 1'>
416 <xsl:attribute name='class'>
417 <xsl:text>gallery</xsl:text>
418 <xsl:if test='//xhtml:a[f:contains-token(@class, "left")
419 or f:contains-token(@class, "right")]'>
420 <xsl:text> inline</xsl:text>
423 <xsl:for-each select='$images'>
424 <xsl:call-template name='imgpara' />
429 <xsl:call-template name='imgpara' />
434 <xsl:template match='/'>
437 <meta name='viewport' content='width=device-width, initial-scale=1' />
438 <link rel='stylesheet' type='text/css' href='/style.css' />
439 <link rel='alternate stylesheet' type='text/css' href='/dark.css'
440 title='Dark Style' />
441 <link rel="icon" href="data:," />
443 <xsl:variable name='page-title' select='string(/document/title)' />
444 <xsl:if test='$page-title and $site-title != $page-title'>
445 <xsl:value-of select='concat($page-title, " – ")' />
447 <xsl:value-of select='$site-title' />
449 <!-- Hoist all style elements to <head> as required by the doctype. -->
450 <xsl:for-each select='//xhtml:style'>
451 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
455 <xsl:apply-templates select='/document/xhtml:html/@*' />
457 <xsl:if test='/document/hierarchy/parent'>
459 <small><xsl:value-of select='$site-title' /></small>
461 <div id='breadcrumbs'>
462 <strong>Return to: </strong>
464 <xsl:for-each select='/document/hierarchy/parent'>
465 <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
472 <xsl:apply-templates select='/document/xhtml:html/node()' />
476 <xsl:apply-templates select='/document/copyright' />
477 <xsl:apply-templates select='/document/license' />
478 <xsl:if test='/document/image[copyright != /document/copyright
479 or license/identifier != /document/license/identifier]'>
480 <xsl:call-template name='image-attribution' />
482 <xsl:apply-templates select='/document/source' />
488 <xsl:include href='layouts/whitespace.xsl' />
489 <xsl:include href='layouts/clickytable.xsl' />