1 <?xml version='1.0' encoding='UTF-8' ?>
3 Nick's web site: XHTML output stage
5 Copyright © 2018-2020 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' />
31 <xsl:output method='xml' encoding='UTF-8' indent='yes'
32 doctype-public='-//W3C//DTD XHTML 1.1//EN'
33 doctype-system='http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
34 cdata-section-elements='style script' />
36 <xsl:param name='source-uri'
37 select='"//git.draconx.ca/gitweb/homepage.git/"' />
38 <xsl:param name='site-title' select='"The Citrine Citadel"' />
39 <xsl:param name='section-links' select='//document/section-links' />
41 <func:function name='f:ends-with'>
42 <xsl:param name='a' />
43 <xsl:param name='b' />
45 select='substring($a, string-length($a)-string-length($b)+1)=$b' />
48 <xsl:template match='node()|@*'>
49 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
53 Nokogiri's pretty-printer is a bit weird. Regardless of the indentation
54 setting, if an element has no child text nodes then it will be pretty-
55 printed. This works by adding arbitrary whitespace to that element, and
56 then all of its children are eligible to be pretty-printed.
58 If an element has any text nodes at all, then it is not pretty-printed and
59 neither are any of its descendents.
61 Adding arbitrary whitespace to <pre> is bad, so we inject zero-width non-
62 breaking spaces to prevent this. This will render fine but the spaces
63 should be removed before final output to avoid problems with copy+paste.
65 <xsl:template match='xhtml:pre'>
67 <xsl:apply-templates select='node()|@*' />
68 <xsl:text>⁠</xsl:text>
73 Likewise, adding spaces between consecutive span-level elements where
74 none existed before won't go over well.
76 <xsl:template name='glue-preceding-span'>
77 <xsl:if test='f:element-is-span(preceding-sibling::node()[1])'>
78 <xsl:text>⁠</xsl:text>
82 <xsl:template match='*[f:element-is-span()]'>
83 <xsl:call-template name='glue-preceding-span' />
85 <xsl:apply-templates select='node()|@*' />
87 <!-- avoid breaking within a span element -->
88 <xsl:text>⁠</xsl:text>
94 Manually strip whitespace-only text nodes so the pretty printer can do its
95 thing on remaining elements.
97 <xsl:template match='text()[normalize-space(.) = ""]'>
99 <!-- preserve anything according to xml:space -->
100 <xsl:when test='ancestor::*[@xml:space][1][@xml:space="preserve"]'>
103 <!-- preserve anything under <pre> -->
104 <xsl:when test='ancestor::xhtml:pre'><xsl:copy /></xsl:when>
105 <!-- preserve whitespace which is the only child node of an element -->
106 <xsl:when test='count(../node()) = 1'><xsl:copy /></xsl:when>
107 <!-- preserve whitespace between consecutive span-level elements
108 which have at least one non-whitespace sibling text element -->
109 <xsl:when test='f:element-is-span(preceding-sibling::node()[1])
110 and f:element-is-span(following-sibling::node()[1])
111 and ../text()[normalize-space(.) != ""]'>
117 <!-- Clean up whitespace where harmless to do so -->
118 <xsl:template match='xhtml:p/node()[1][self::text()]'>
119 <xsl:value-of select='f:strip-leading()' />
121 <xsl:template match='xhtml:p/node()[position()=last()][self::text()]'>
122 <xsl:value-of select='f:strip-trailing()' />
125 <!-- Add rel attributes to external links -->
126 <xsl:template match='xhtml:a[starts-with(@href,"http://")
127 or starts-with(@href,"https://")
128 or starts-with(@href,"//")]'>
129 <xsl:variable name='domain'
130 select='substring-before(
131 concat(translate(substring-after(@href, "//"), ":", "/"), "/"),
135 <xsl:apply-templates select='@*' />
136 <xsl:if test='not($domain="draconx.ca"
137 or f:ends-with($domain, ".draconx.ca"))'>
138 <xsl:attribute name='rel'>
140 <xsl:value-of select='@rel' />
141 <xsl:text> </xsl:text>
143 <xsl:text>external noopener noreferrer</xsl:text>
146 <xsl:apply-templates select='node()' />
150 <xsl:template match='xhtml:h2[@id]'>
151 <xsl:variable name='fragment' select='concat("#", @id)' />
153 <xsl:apply-templates select='node()|@*' />
154 <xsl:if test='$section-links = "yes"'>
155 <xsl:text> </xsl:text>
156 <small class='permalink'>
157 (<a href='{$fragment}'><xsl:value-of select='$fragment' /></a>)
164 Convert caption attribute on tables into proper caption elements, to allow
165 a simple way to add captions to kramdown tables.
167 <xsl:template match='@caption[parent::xhtml:table]' />
168 <xsl:template match='xhtml:table[@caption]'>
170 <xsl:apply-templates select='@*' />
171 <caption><xsl:value-of select='normalize-space(@caption)' /></caption>
172 <xsl:apply-templates select='node()' />
177 Delete style elements, as they will get hoisted occur under <head> below.
178 If the generate-listing attribute was specified, produce a code listing
179 where the style attribute was found.
181 <xsl:template match='xhtml:style|@generate-listing[parent::xhtml:style]' />
182 <xsl:template match='xhtml:style[@generate-listing]'>
183 <pre>⁠<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
187 Add a simple way to reference a document node by ID and include the XHTML
188 code listing directly in the document.
190 <xsl:template match='xhtml:generate-xhtml-listing'>
191 <xsl:variable name='target' select='@target' />
193 <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
197 <xsl:template match='copyright'>
199 <xsl:text>Copyright © </xsl:text>
200 <xsl:value-of select='text()' />
201 <xsl:text>.</xsl:text>
205 <xsl:template match='license'>
207 <xsl:text>Copying and distribution of this material</xsl:text>
208 <xsl:if test='normalize-space(modification-allowed)="yes"'>
209 <xsl:text>, with or without modification,</xsl:text>
211 <xsl:text> is permitted under the terms of the </xsl:text>
213 <xsl:attribute name='href'>
214 <xsl:value-of select='normalize-space(uri)' />
216 <xsl:value-of select='name' />
218 <xsl:text>.</xsl:text>
222 <func:function name='f:matching-child'>
223 <xsl:param name='child' select='./copyright-holder' />
224 <xsl:param name='node' select='.' />
225 <xsl:param name='nodeset' select='$node/../*[name()=name($node)]' />
227 <func:result select='$nodeset[*[name()=name($child)]=$child]' />
230 <func:function name='f:attribution-order'>
231 <xsl:param name='a' />
232 <xsl:param name='b' />
234 <xsl:variable name='docmatch'
235 select='number($a/copyright-holder = /document/copyright-holder)
236 - number($b/copyright-holder = /document/copyright-holder)' />
238 <xsl:variable name='authmatch'
239 select='count(f:matching-child($a/copyright-holder, $a))
240 - count(f:matching-child($b/copyright-holder, $b))' />
242 <xsl:variable name='licmatch'
243 select='count(f:matching-child($a/license, $a))
244 - count(f:matching-child($b/license, $b))' />
247 <xsl:when test='$docmatch'><func:result select='$docmatch' /></xsl:when>
248 <xsl:when test='$authmatch'><func:result select='$authmatch' /></xsl:when>
249 <xsl:when test='$licmatch'><func:result select='$licmatch' /></xsl:when>
250 <xsl:otherwise><func:result select='"nope"' /></xsl:otherwise>
254 <xsl:template match='image/license'>
255 <xsl:text>, </xsl:text>
256 <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
259 <xsl:template match='image'>
261 <xsl:when test='position() = 1'>, except </xsl:when>
262 <xsl:when test='position() = last()'> and </xsl:when>
263 <xsl:otherwise>, </xsl:otherwise>
265 <a href='{uri}'><xsl:value-of select='title' /></a>
266 <xsl:text> © </xsl:text>
267 <xsl:value-of select='copyright' />
268 <xsl:apply-templates select='license' />
271 <xsl:template name='image-attribution'>
273 <xsl:variable name='x' select='/document/image[copyright-holder="Nick Bowler"][1]' />
274 <xsl:variable name='y' select='/document/image[copyright-holder="Nick Bowler"][4]' />
276 <xsl:variable name='images-fragment'>
277 <xsl:for-each select='/document/image'>
278 <xsl:sort select='number(copyright-holder = /document/copyright-holder)'
279 data-type='number' order='descending' />
280 <xsl:sort select='count(f:matching-child(copyright-holder))'
281 data-type='number' order='descending' />
282 <xsl:sort select='copyright-holder' order='descending' />
283 <xsl:sort select='count(f:matching-child(license))'
284 data-type='number' order='descending' />
285 <xsl:sort select='license/identifier' order='descending' />
287 <xsl:call-template name='notransform' />
290 <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
292 <xsl:variable name='abbrev-split'
293 select='count($images[copyright-holder = $images[1]/copyright-holder
294 and license/identifier = $images[1]/license/identifier])' />
296 <xsl:variable name='abbrev-years-fragment'>
297 <xsl:for-each select='$images[$abbrev-split >= position()]/copyright-year'>
298 <xsl:sort data-type='number' />
299 <copyright-year><xsl:value-of select='.' /></copyright-year>
302 <xsl:variable name='abbrev-years'
303 select='exslt:node-set($abbrev-years-fragment)/*' />
306 <xsl:text>Images © </xsl:text>
307 <xsl:value-of select='$abbrev-years[1]' />
308 <xsl:if test='$abbrev-years[last()] != $abbrev-years[1]'>
309 <xsl:value-of select='concat("–", $abbrev-years[last()])' />
311 <xsl:value-of select='concat(" ", $images[1]/copyright-holder)' />
312 <xsl:apply-templates select='$images[1]/license' />
313 <xsl:apply-templates select='$images[position() > $abbrev-split]' />
314 <xsl:text>.</xsl:text>
318 <xsl:template match='source'>
320 <xsl:text>This document was compiled</xsl:text>
322 <xsl:when test='file'>
323 <xsl:text> from </xsl:text>
324 <a href='{concat($source-uri, "blob/", revision, ":", file)}'>
325 <xsl:value-of select='file' />
328 <xsl:when test='dir'>
329 <xsl:text> from </xsl:text>
330 <a href='{concat($source-uri, "tree/", revision, ":", dir)}'>
331 <xsl:value-of select='dir' />
335 <xsl:text> on </xsl:text>
336 <xsl:value-of select='compiletime' />
337 <xsl:text>.</xsl:text>
341 <xsl:template match='xhtml:h1[not(preceding::xhtml:h1)]'>
342 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
343 <xsl:if test='/document/article/published'>
344 <div id='article-info'>
346 <xsl:text>Posted </xsl:text>
347 <xsl:value-of select='/document/article/published' />
353 <xsl:template match='/'>
356 <meta name='viewport' content='width=device-width, initial-scale=1' />
357 <link rel='stylesheet' type='text/css' href='/style.css' />
358 <link rel="icon" href="data:," />
360 <xsl:variable name='page-title' select='string(/document/title)' />
361 <xsl:if test='$page-title and $site-title != $page-title'>
362 <xsl:value-of select='concat($page-title, " – ")' />
364 <xsl:value-of select='$site-title' />
366 <!-- Hoist all style elements to <head> as required by the doctype. -->
367 <xsl:for-each select='//xhtml:style'>
368 <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
372 <xsl:apply-templates select='/document/xhtml:html/@*' />
374 <xsl:if test='/document/hierarchy/parent'>
376 <small><xsl:value-of select='$site-title' /></small>
378 <div id='breadcrumbs'>
379 <strong>Return to: </strong>
381 <xsl:for-each select='/document/hierarchy/parent'>
382 <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
389 <xsl:apply-templates select='/document/xhtml:html/node()' />
393 <xsl:apply-templates select='/document/copyright' />
394 <xsl:apply-templates select='/document/license' />
395 <xsl:apply-templates select='/document/source' />