]> git.draconx.ca Git - homepage.git/blob - layouts/default.xsl
Embed SVG icons directly into output.
[homepage.git] / layouts / default.xsl
1 <?xml version='1.0' encoding='UTF-8' ?>
2 <!--
3   Nick's web site: XHTML output stage
4
5   Copyright © 2018-2021 Nick Bowler
6
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.
11
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.
16
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/>
19 -->
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'>
28
29 <xsl:import href='layouts/functions.xsl' />
30 <xsl:output cdata-section-elements='style script' />
31
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' />
36
37 <func:function name='f:ends-with'>
38   <xsl:param name='a' />
39   <xsl:param name='b' />
40   <func:result
41     select='substring($a, string-length($a)-string-length($b)+1)=$b' />
42 </func:function>
43
44 <xsl:template match='node()|@*'>
45   <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
46 </xsl:template>
47
48 <!--
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.
53
54   If an element has any text nodes at all, then it is not pretty-printed and
55   neither are any of its descendents.
56
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.
60 -->
61 <xsl:template match='xhtml:pre'>
62   <xsl:copy>
63     <xsl:apply-templates select='node()|@*' />
64     <xsl:text>&#x2060;</xsl:text>
65   </xsl:copy>
66 </xsl:template>
67
68 <!--
69   Likewise, adding spaces between consecutive span-level elements where
70   none existed before won't go over well.
71 -->
72 <xsl:template name='glue-preceding-span'>
73   <xsl:if test='f:element-is-span(preceding-sibling::node()[1])'>
74     <xsl:text>&#x2060;</xsl:text>
75   </xsl:if>
76 </xsl:template>
77
78 <xsl:template match='*[f:element-is-span()]'>
79   <xsl:call-template name='glue-preceding-span' />
80   <xsl:copy>
81     <xsl:apply-templates select='node()|@*' />
82     <xsl:if test='*'>
83       <!-- avoid breaking within a span element -->
84       <xsl:text>&#x2060;</xsl:text>
85     </xsl:if>
86   </xsl:copy>
87 </xsl:template>
88
89 <!--
90   Manually strip whitespace-only text nodes so the pretty printer can do its
91   thing on remaining elements.
92 -->
93 <xsl:template match='text()[normalize-space(.) = ""]'>
94   <xsl:choose>
95     <!-- preserve anything according to xml:space -->
96     <xsl:when test='ancestor::*[@xml:space][1][@xml:space="preserve"]'>
97       <xsl:copy />
98     </xsl:when>
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(.) != ""]'>
108       <xsl:copy />
109     </xsl:when>
110   </xsl:choose>
111 </xsl:template>
112
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()' />
116 </xsl:template>
117 <xsl:template match='xhtml:p/node()[position()=last()][self::text()]'>
118   <xsl:value-of select='f:strip-trailing()' />
119 </xsl:template>
120
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, "//"), ":", "/"), "/"),
128               "/")' />
129
130   <xsl:copy>
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'>
135         <xsl:if test='@rel'>
136           <xsl:value-of select='@rel' />
137           <xsl:text> </xsl:text>
138         </xsl:if>
139         <xsl:text>external noopener noreferrer</xsl:text>
140       </xsl:attribute>
141     </xsl:if>
142     <xsl:apply-templates select='node()' />
143   </xsl:copy>
144 </xsl:template>
145
146 <xsl:template match='xhtml:h2[@id]'>
147   <xsl:variable name='fragment' select='concat("#", @id)' />
148   <xsl:copy>
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>)
154       </small>
155     </xsl:if>
156   </xsl:copy>
157 </xsl:template>
158
159 <!--
160   Convert caption attribute on tables into proper caption elements, to allow
161   a simple way to add captions to kramdown tables.
162 -->
163 <xsl:template match='@caption[parent::xhtml:table]' />
164 <xsl:template match='xhtml:table[@caption]'>
165   <xsl:copy>
166     <xsl:apply-templates select='@*' />
167     <caption><xsl:value-of select='normalize-space(@caption)' /></caption>
168     <xsl:apply-templates select='node()' />
169   </xsl:copy>
170 </xsl:template>
171
172 <!--
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.
176 -->
177 <xsl:template match='xhtml:style|@generate-listing[parent::xhtml:style]' />
178 <xsl:template match='xhtml:style[@generate-listing]'>
179   <pre>&#x2060;<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
180 </xsl:template>
181
182 <!--
183   Add a simple way to reference a document node by ID and include the XHTML
184   code listing directly in the document.
185 -->
186 <xsl:template match='xhtml:generate-xhtml-listing'>
187   <xsl:variable name='target' select='@target' />
188   <pre>&#x2060;<code>
189     <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
190   </code></pre>
191 </xsl:template>
192
193 <!-- For paragraphs containing only kbd elements, wrap in blockquote. -->
194 <xsl:template match='xhtml:p[*[last()=count(../xhtml:kbd)]]'>
195   <blockquote>
196     <xsl:copy>
197       <xsl:apply-templates select='node()|@*' />
198     </xsl:copy>
199   </blockquote>
200 </xsl:template>
201
202 <!--
203   Wrap each word of text in kbd elements in spans, so they can be styled
204   to avoid linebreaks in the middle of option names and other bad places.
205 -->
206 <xsl:template name='spanify-text' match='xhtml:kbd/text()'>
207   <xsl:param name='text' select='normalize-space(.)' />
208   <xsl:variable name='firstword' select='substring-before($text, " ")' />
209   <xsl:choose>
210     <xsl:when test='$firstword'>
211       <span><xsl:value-of select='$firstword' /></span>
212       <xsl:text> </xsl:text>
213     </xsl:when>
214     <xsl:when test='$text'>
215       <span><xsl:value-of select='$text' /></span>
216     </xsl:when>
217   </xsl:choose>
218   <xsl:if test='$firstword'>
219     <xsl:call-template name='spanify-text'>
220       <xsl:with-param name='text' select='substring-after($text, " ")' />
221     </xsl:call-template>
222   </xsl:if>
223 </xsl:template>
224
225 <xsl:template match='copyright'>
226   <p>
227     <xsl:text>Copyright © </xsl:text>
228     <xsl:value-of select='text()' />
229     <xsl:text>.</xsl:text>
230   </p>
231 </xsl:template>
232
233 <xsl:template match='license'>
234   <p>
235     <xsl:text>Copying and distribution of this material</xsl:text>
236     <xsl:if test='normalize-space(modification-allowed)="yes"'>
237       <xsl:text>, with or without modification,</xsl:text>
238     </xsl:if>
239     <xsl:text> is permitted under the terms of the </xsl:text>
240     <a rel='license'>
241       <xsl:attribute name='href'>
242         <xsl:value-of select='normalize-space(uri)' />
243       </xsl:attribute>
244       <xsl:value-of select='name' />
245     </a>
246     <xsl:text>.</xsl:text>
247   </p>
248 </xsl:template>
249
250 <func:function name='f:matching-child'>
251   <xsl:param name='child' select='./copyright-holder' />
252   <xsl:param name='node' select='.' />
253   <xsl:param name='nodeset' select='$node/../*[name()=name($node)]' />
254
255   <func:result select='$nodeset[*[name()=name($child)]=$child]' />
256 </func:function>
257
258 <func:function name='f:attribution-order'>
259   <xsl:param name='a' />
260   <xsl:param name='b' />
261
262   <xsl:variable name='docmatch'
263     select='number($a/copyright-holder = /document/copyright-holder)
264             - number($b/copyright-holder = /document/copyright-holder)' />
265
266   <xsl:variable name='authmatch'
267     select='count(f:matching-child($a/copyright-holder, $a))
268             - count(f:matching-child($b/copyright-holder, $b))' />
269
270   <xsl:variable name='licmatch'
271     select='count(f:matching-child($a/license, $a))
272             - count(f:matching-child($b/license, $b))' />
273
274   <xsl:choose>
275     <xsl:when test='$docmatch'><func:result select='$docmatch' /></xsl:when>
276     <xsl:when test='$authmatch'><func:result select='$authmatch' /></xsl:when>
277     <xsl:when test='$licmatch'><func:result select='$licmatch' /></xsl:when>
278     <xsl:otherwise><func:result select='"nope"' /></xsl:otherwise>
279   </xsl:choose>
280 </func:function>
281
282 <xsl:template match='image/license'>
283   <xsl:text>, </xsl:text>
284   <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
285 </xsl:template>
286
287 <xsl:template match='image'>
288   <xsl:choose>
289     <xsl:when test='position() = 1'>, except </xsl:when>
290     <xsl:when test='position() = last()'> and </xsl:when>
291     <xsl:otherwise>, </xsl:otherwise>
292   </xsl:choose>
293   <a href='{uri}'><xsl:value-of select='title' /></a>
294   <xsl:text> © </xsl:text>
295   <xsl:value-of select='copyright' />
296   <xsl:apply-templates select='license' />
297 </xsl:template>
298
299 <xsl:template name='image-attribution'>
300 <!--
301   <xsl:variable name='x' select='/document/image[copyright-holder="Nick Bowler"][1]' />
302   <xsl:variable name='y' select='/document/image[copyright-holder="Nick Bowler"][4]' />
303 -->
304   <xsl:variable name='images-fragment'>
305     <xsl:for-each select='/document/image'>
306       <xsl:sort select='number(copyright-holder = /document/copyright-holder)'
307                 data-type='number' order='descending' />
308       <xsl:sort select='count(f:matching-child(copyright-holder))'
309                 data-type='number' order='descending' />
310       <xsl:sort select='copyright-holder' order='descending' />
311       <xsl:sort select='count(f:matching-child(license))'
312                 data-type='number' order='descending' />
313       <xsl:sort select='license/identifier' order='descending' />
314
315       <xsl:call-template name='notransform' />
316     </xsl:for-each>
317   </xsl:variable>
318   <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
319
320   <xsl:variable name='abbrev-split'
321       select='count($images[copyright-holder = $images[1]/copyright-holder
322                  and license/identifier = $images[1]/license/identifier])' />
323
324   <xsl:variable name='abbrev-years-fragment'>
325     <xsl:for-each select='$images[$abbrev-split >= position()]/copyright-year'>
326       <xsl:sort data-type='number' />
327       <copyright-year><xsl:value-of select='.' /></copyright-year>
328     </xsl:for-each>
329   </xsl:variable>
330   <xsl:variable name='abbrev-years'
331     select='exslt:node-set($abbrev-years-fragment)/*' />
332
333   <p>
334     <xsl:text>Images © </xsl:text>
335     <xsl:value-of select='$abbrev-years[1]' />
336     <xsl:if test='$abbrev-years[last()] != $abbrev-years[1]'>
337       <xsl:value-of select='concat("–", $abbrev-years[last()])' />
338     </xsl:if>
339     <xsl:value-of select='concat(" ", $images[1]/copyright-holder)' />
340     <xsl:apply-templates select='$images[1]/license' />
341     <xsl:apply-templates select='$images[position() > $abbrev-split]' />
342     <xsl:text>.</xsl:text>
343   </p>
344 </xsl:template>
345
346 <xsl:template match='source'>
347   <p>
348     <xsl:text>This document was compiled</xsl:text>
349     <xsl:choose>
350       <xsl:when test='file'>
351         <xsl:text> from </xsl:text>
352         <a href='{concat($source-uri, "blob/", revision, ":", file)}'>
353           <xsl:value-of select='file' />
354         </a>
355       </xsl:when>
356       <xsl:when test='dir'>
357         <xsl:text> from </xsl:text>
358         <a href='{concat($source-uri, "tree/", revision, ":", dir)}'>
359           <xsl:value-of select='dir' />
360         </a>
361       </xsl:when>
362     </xsl:choose>
363     <xsl:text> on </xsl:text>
364     <xsl:value-of select='compiletime' />
365     <xsl:text>.</xsl:text>
366   </p>
367 </xsl:template>
368
369 <xsl:template match='xhtml:h1[not(preceding::xhtml:h1)]'>
370   <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
371   <xsl:if test='/document/article/published'>
372     <div id='article-info'>
373       <p>
374         <xsl:text>Posted </xsl:text>
375         <xsl:value-of select='/document/article/published' />
376       </p>
377     </div>
378   </xsl:if>
379 </xsl:template>
380
381 <xsl:template match='/'>
382   <html>
383     <head>
384       <meta name='viewport' content='width=device-width, initial-scale=1' />
385       <link rel='stylesheet' type='text/css' href='/style.css' />
386       <link rel="icon" href="data:," />
387       <title>
388         <xsl:variable name='page-title' select='string(/document/title)' />
389         <xsl:if test='$page-title and $site-title != $page-title'>
390           <xsl:value-of select='concat($page-title, " – ")' />
391         </xsl:if>
392         <xsl:value-of select='$site-title' />
393       </title>
394       <!-- Hoist all style elements to <head> as required by the doctype. -->
395       <xsl:for-each select='//xhtml:style'>
396         <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
397       </xsl:for-each>
398     </head>
399     <body>
400       <xsl:apply-templates select='/document/xhtml:html/@*' />
401
402       <xsl:if test='/document/hierarchy/parent'>
403         <p id='sitetitle'>
404           <small><xsl:value-of select='$site-title' /></small>
405         </p>
406         <div id='breadcrumbs'>
407           <strong>Return to: </strong>
408           <ul>
409             <xsl:for-each select='/document/hierarchy/parent'>
410               <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
411             </xsl:for-each>
412           </ul>
413         </div>
414         <hr />
415       </xsl:if>
416
417       <xsl:apply-templates select='/document/xhtml:html/node()' />
418
419       <hr />
420       <div id='footer'>
421         <xsl:apply-templates select='/document/copyright' />
422         <xsl:apply-templates select='/document/license' />
423         <xsl:apply-templates select='/document/source' />
424       </div>
425     </body>
426   </html>
427 </xsl:template>
428
429 <xsl:include href='layouts/clickytable.xsl' />
430
431 </xsl:stylesheet>