1 <?xml version='1.0' encoding='UTF-8' ?>
3 Nick's web site: XHTML output stage
5 Copyright © 2018-2021 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:f='http://draconx.ca/my-functions'
26 extension-element-prefixes='func f'
27 exclude-result-prefixes='xhtml'>
29 <xsl:import href='layouts/functions.xsl' />
30 <xsl:output cdata-section-elements='style script' />
32 <xsl:param name='source-uri'
33 select='"//git.draconx.ca/gitweb/homepage.git/"' />
34 <xsl:param name='site-title' select='"The Citrine Citadel"' />
35 <xsl:param name='section-links' select='//document/section-links' />
37 <func:function name='f:ends-with'>
38 <xsl:param name='a' />
39 <xsl:param name='b' />
41 select='substring($a, string-length($a)-string-length($b)+1)=$b' />
44 <xsl:template match='node()|@*'>
45 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
49 Nokogiri's pretty-printer is a bit weird. Regardless of the indentation
50 setting, if an element has no child text nodes then it will be pretty-
51 printed. This works by adding arbitrary whitespace to that element, and
52 then all of its children are eligible to be pretty-printed.
54 If an element has any text nodes at all, then it is not pretty-printed and
55 neither are any of its descendents.
57 Adding arbitrary whitespace to <pre> is bad, so we inject zero-width non-
58 breaking spaces to prevent this. This will render fine but the spaces
59 should be removed before final output to avoid problems with copy+paste.
61 <xsl:template match='xhtml:pre'>
63 <xsl:apply-templates select='node()|@*' />
64 <xsl:text>⁠</xsl:text>
69 Likewise, adding spaces between consecutive span-level elements where
70 none existed before won't go over well.
72 <xsl:template name='glue-preceding-span'>
73 <xsl:if test='f:element-is-span(preceding-sibling::node()[1])'>
74 <xsl:text>⁠</xsl:text>
78 <xsl:template match='*[f:element-is-span()]'>
79 <xsl:call-template name='glue-preceding-span' />
81 <xsl:apply-templates select='node()|@*' />
83 <!-- avoid breaking within a span element -->
84 <xsl:text>⁠</xsl:text>
90 Manually strip whitespace-only text nodes so the pretty printer can do its
91 thing on remaining elements.
93 <xsl:template match='text()[normalize-space(.) = ""]'>
95 <!-- preserve anything according to xml:space -->
96 <xsl:when test='ancestor::*[@xml:space][1][@xml:space="preserve"]'>
99 <!-- preserve anything under <pre> -->
100 <xsl:when test='ancestor::xhtml:pre'><xsl:copy /></xsl:when>
101 <!-- preserve whitespace which is the only child node of an element -->
102 <xsl:when test='count(../node()) = 1'><xsl:copy /></xsl:when>
103 <!-- preserve whitespace between consecutive span-level elements
104 which have at least one non-whitespace sibling text element -->
105 <xsl:when test='f:element-is-span(preceding-sibling::node()[1])
106 and f:element-is-span(following-sibling::node()[1])
107 and ../text()[normalize-space(.) != ""]'>
113 <!-- Clean up whitespace where harmless to do so -->
114 <xsl:template match='xhtml:p/node()[1][self::text()]'>
115 <xsl:value-of select='f:strip-leading()' />
117 <xsl:template match='xhtml:p/node()[position()=last()][self::text()]'>
118 <xsl:value-of select='f:strip-trailing()' />
121 <!-- Add rel attributes to external links -->
122 <xsl:template match='xhtml:a[starts-with(@href,"http://")
123 or starts-with(@href,"https://")
124 or starts-with(@href,"//")]'>
125 <xsl:variable name='domain'
126 select='substring-before(
127 concat(translate(substring-after(@href, "//"), ":", "/"), "/"),
131 <xsl:apply-templates select='@*' />
132 <xsl:if test='not($domain="draconx.ca"
133 or f:ends-with($domain, ".draconx.ca"))'>
134 <xsl:attribute name='rel'>
136 <xsl:value-of select='@rel' />
137 <xsl:text> </xsl:text>
139 <xsl:text>external noopener noreferrer</xsl:text>
142 <xsl:apply-templates select='node()' />
146 <xsl:template match='xhtml:h2[@id]'>
147 <xsl:variable name='fragment' select='concat("#", @id)' />
149 <xsl:apply-templates select='node()|@*' />
150 <xsl:if test='$section-links = "yes"'>
151 <xsl:text> </xsl:text>
152 <small class='permalink'>
153 (<a href='{$fragment}'><xsl:value-of select='$fragment' /></a>)
160 Convert caption attribute on tables into proper caption elements, to allow
161 a simple way to add captions to kramdown tables.
163 <xsl:template match='@caption[parent::xhtml:table]' />
164 <xsl:template match='xhtml:table[@caption]'>
166 <xsl:apply-templates select='@*' />
167 <caption><xsl:value-of select='normalize-space(@caption)' /></caption>
168 <xsl:apply-templates select='node()' />
173 Delete style elements, as they will get hoisted occur under <head> below.
174 If the generate-listing attribute was specified, produce a code listing
175 where the style attribute was found.
177 <xsl:template match='xhtml:style|@generate-listing[parent::xhtml:style]' />
178 <xsl:template match='xhtml:style[@generate-listing]'>
179 <pre>⁠<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
183 Attempt to wrap the first bit of linked email addresses to allow
184 linewrapping to occur after the '@'.
186 <xsl:template match='xhtml:a[starts-with(@href,"mailto:")]/text()'>
187 <xsl:variable name='addr' select='substring-after(../@href, "mailto:")' />
189 <xsl:variable name='wrap'
190 select='concat(substring-before($addr, "@"), "@")' />
193 <xsl:when test='contains(., $wrap)'>
194 <xsl:value-of select='substring-before(., $wrap)' />
196 <xsl:value-of select='$wrap' />
198 <xsl:value-of select='substring-after(., $wrap)' />
207 Add a simple way to reference a document node by ID and include the XHTML
208 code listing directly in the document.
210 <xsl:template match='xhtml:generate-xhtml-listing'>
211 <xsl:variable name='target' select='@target' />
213 <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
217 <!-- For paragraphs containing only kbd elements, wrap in blockquote. -->
218 <xsl:template match='xhtml:p[*[last()=count(../xhtml:kbd)]]'>
221 <xsl:apply-templates select='node()|@*' />
227 Wrap each word of text in kbd elements in spans, so they can be styled
228 to avoid linebreaks in the middle of option names and other bad places.
230 <xsl:template name='spanify-text' match='xhtml:kbd/text()'>
231 <xsl:param name='text' select='normalize-space(.)' />
232 <xsl:variable name='firstword' select='substring-before($text, " ")' />
234 <xsl:when test='$firstword'>
235 <span><xsl:value-of select='$firstword' /></span>
236 <xsl:text> </xsl:text>
238 <xsl:when test='$text'>
239 <span><xsl:value-of select='$text' /></span>
242 <xsl:if test='$firstword'>
243 <xsl:call-template name='spanify-text'>
244 <xsl:with-param name='text' select='substring-after($text, " ")' />
249 <xsl:template match='copyright'>
251 <xsl:text>Copyright © </xsl:text>
252 <xsl:value-of select='text()' />
253 <xsl:text>.</xsl:text>
257 <xsl:template match='license'>
259 <xsl:text>Copying and distribution of this material</xsl:text>
260 <xsl:if test='normalize-space(modification-allowed)="yes"'>
261 <xsl:text>, with or without modification,</xsl:text>
263 <xsl:text> is permitted under the terms of the </xsl:text>
265 <xsl:attribute name='href'>
266 <xsl:value-of select='normalize-space(uri)' />
268 <xsl:value-of select='name' />
270 <xsl:text>.</xsl:text>
274 <func:function name='f:matching-child'>
275 <xsl:param name='child' select='./copyright-holder' />
276 <xsl:param name='node' select='.' />
277 <xsl:param name='nodeset' select='$node/../*[name()=name($node)]' />
279 <func:result select='$nodeset[*[name()=name($child)]=$child]' />
282 <func:function name='f:attribution-order'>
283 <xsl:param name='a' />
284 <xsl:param name='b' />
286 <xsl:variable name='docmatch'
287 select='number($a/copyright-holder = /document/copyright-holder)
288 - number($b/copyright-holder = /document/copyright-holder)' />
290 <xsl:variable name='authmatch'
291 select='count(f:matching-child($a/copyright-holder, $a))
292 - count(f:matching-child($b/copyright-holder, $b))' />
294 <xsl:variable name='licmatch'
295 select='count(f:matching-child($a/license, $a))
296 - count(f:matching-child($b/license, $b))' />
299 <xsl:when test='$docmatch'><func:result select='$docmatch' /></xsl:when>
300 <xsl:when test='$authmatch'><func:result select='$authmatch' /></xsl:when>
301 <xsl:when test='$licmatch'><func:result select='$licmatch' /></xsl:when>
302 <xsl:otherwise><func:result select='"nope"' /></xsl:otherwise>
306 <xsl:template match='image/license'>
307 <xsl:text>, </xsl:text>
308 <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
311 <xsl:template match='image'>
313 <xsl:when test='position() = 1'>, except </xsl:when>
314 <xsl:when test='position() = last()'> and </xsl:when>
315 <xsl:otherwise>, </xsl:otherwise>
317 <a href='{uri}'><xsl:value-of select='title' /></a>
318 <xsl:text> © </xsl:text>
319 <xsl:value-of select='copyright' />
320 <xsl:apply-templates select='license' />
323 <xsl:template name='image-attribution'>
325 <xsl:variable name='x' select='/document/image[copyright-holder="Nick Bowler"][1]' />
326 <xsl:variable name='y' select='/document/image[copyright-holder="Nick Bowler"][4]' />
328 <xsl:variable name='images-fragment'>
329 <xsl:for-each select='/document/image'>
330 <xsl:sort select='number(copyright-holder = /document/copyright-holder)'
331 data-type='number' order='descending' />
332 <xsl:sort select='count(f:matching-child(copyright-holder))'
333 data-type='number' order='descending' />
334 <xsl:sort select='copyright-holder' order='descending' />
335 <xsl:sort select='count(f:matching-child(license))'
336 data-type='number' order='descending' />
337 <xsl:sort select='license/identifier' order='descending' />
339 <xsl:call-template name='notransform' />
342 <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
344 <xsl:variable name='abbrev-split'
345 select='count($images[copyright-holder = $images[1]/copyright-holder
346 and license/identifier = $images[1]/license/identifier])' />
348 <xsl:variable name='abbrev-years-fragment'>
349 <xsl:for-each select='$images[$abbrev-split >= position()]/copyright-year'>
350 <xsl:sort data-type='number' />
351 <copyright-year><xsl:value-of select='.' /></copyright-year>
354 <xsl:variable name='abbrev-years'
355 select='exslt:node-set($abbrev-years-fragment)/*' />
358 <xsl:text>Images © </xsl:text>
359 <xsl:value-of select='$abbrev-years[1]' />
360 <xsl:if test='$abbrev-years[last()] != $abbrev-years[1]'>
361 <xsl:value-of select='concat("–", $abbrev-years[last()])' />
363 <xsl:value-of select='concat(" ", $images[1]/copyright-holder)' />
364 <xsl:apply-templates select='$images[1]/license' />
365 <xsl:apply-templates select='$images[position() > $abbrev-split]' />
366 <xsl:text>.</xsl:text>
370 <xsl:template match='source'>
372 <xsl:text>This document was compiled</xsl:text>
374 <xsl:when test='file'>
375 <xsl:text> from </xsl:text>
376 <a href='{concat($source-uri, "blob/", revision, ":", file)}'>
377 <xsl:value-of select='file' />
380 <xsl:when test='dir'>
381 <xsl:text> from </xsl:text>
382 <a href='{concat($source-uri, "tree/", revision, ":", dir)}'>
383 <xsl:value-of select='dir' />
387 <xsl:text> on </xsl:text>
388 <xsl:value-of select='compiletime' />
389 <xsl:text>.</xsl:text>
393 <xsl:template match='xhtml:h1[not(preceding::xhtml:h1)]'>
394 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
395 <xsl:if test='/document/article/published'>
396 <div id='article-info'>
398 <xsl:text>Posted </xsl:text>
399 <xsl:value-of select='/document/article/published' />
405 <xsl:template match='/'>
408 <meta name='viewport' content='width=device-width, initial-scale=1' />
409 <link rel='stylesheet' type='text/css' href='/style.css' />
410 <link rel="icon" href="data:," />
412 <xsl:variable name='page-title' select='string(/document/title)' />
413 <xsl:if test='$page-title and $site-title != $page-title'>
414 <xsl:value-of select='concat($page-title, " – ")' />
416 <xsl:value-of select='$site-title' />
418 <!-- Hoist all style elements to <head> as required by the doctype. -->
419 <xsl:for-each select='//xhtml:style'>
420 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
424 <xsl:apply-templates select='/document/xhtml:html/@*' />
426 <xsl:if test='/document/hierarchy/parent'>
428 <small><xsl:value-of select='$site-title' /></small>
430 <div id='breadcrumbs'>
431 <strong>Return to: </strong>
433 <xsl:for-each select='/document/hierarchy/parent'>
434 <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
441 <xsl:apply-templates select='/document/xhtml:html/node()' />
445 <xsl:apply-templates select='/document/copyright' />
446 <xsl:apply-templates select='/document/license' />
447 <xsl:apply-templates select='/document/source' />
453 <xsl:include href='layouts/clickytable.xsl' />