]> git.draconx.ca Git - homepage.git/blob - layouts/default.xsl
a97f784c2d3e2cb63a4446e4b6eaadf0ed2f7f21
[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   Attempt to wrap the first bit of linked email addresses to allow
184   linewrapping to occur after the '@'.
185 -->
186 <xsl:template match='xhtml:a[starts-with(@href,"mailto:")]/text()'>
187   <xsl:variable name='addr' select='substring-after(../@href, "mailto:")' />
188
189   <xsl:variable name='wrap'
190     select='concat(substring-before($addr, "@"), "@")' />
191
192   <xsl:choose>
193     <xsl:when test='contains(., $wrap)'>
194       <xsl:value-of select='substring-before(., $wrap)' />
195       <span class='wbr'>
196         <xsl:value-of select='$wrap' />
197       </span>
198       <xsl:value-of select='substring-after(., $wrap)' />
199     </xsl:when>
200     <xsl:otherwise>
201       <xsl:copy />
202     </xsl:otherwise>
203   </xsl:choose>
204 </xsl:template>
205
206 <!--
207   Add a simple way to reference a document node by ID and include the XHTML
208   code listing directly in the document.
209 -->
210 <xsl:template match='xhtml:generate-xhtml-listing'>
211   <xsl:variable name='target' select='@target' />
212   <pre>&#x2060;<code>
213     <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
214   </code></pre>
215 </xsl:template>
216
217 <!-- For paragraphs containing only kbd elements, wrap in blockquote. -->
218 <xsl:template match='xhtml:p[*[last()=count(../xhtml:kbd)]]'>
219   <blockquote>
220     <xsl:copy>
221       <xsl:apply-templates select='node()|@*' />
222     </xsl:copy>
223   </blockquote>
224 </xsl:template>
225
226 <!--
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.
229 -->
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, " ")' />
233   <xsl:choose>
234     <xsl:when test='$firstword'>
235       <span><xsl:value-of select='$firstword' /></span>
236       <xsl:text> </xsl:text>
237     </xsl:when>
238     <xsl:when test='$text'>
239       <span><xsl:value-of select='$text' /></span>
240     </xsl:when>
241   </xsl:choose>
242   <xsl:if test='$firstword'>
243     <xsl:call-template name='spanify-text'>
244       <xsl:with-param name='text' select='substring-after($text, " ")' />
245     </xsl:call-template>
246   </xsl:if>
247 </xsl:template>
248
249 <xsl:template match='copyright'>
250   <p>
251     <xsl:text>Copyright © </xsl:text>
252     <xsl:value-of select='text()' />
253     <xsl:text>.</xsl:text>
254   </p>
255 </xsl:template>
256
257 <xsl:template match='license'>
258   <p>
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>
262     </xsl:if>
263     <xsl:text> is permitted under the terms of the </xsl:text>
264     <a rel='license'>
265       <xsl:attribute name='href'>
266         <xsl:value-of select='normalize-space(uri)' />
267       </xsl:attribute>
268       <xsl:value-of select='name' />
269     </a>
270     <xsl:text>.</xsl:text>
271   </p>
272 </xsl:template>
273
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)]' />
278
279   <func:result select='$nodeset[*[name()=name($child)]=$child]' />
280 </func:function>
281
282 <func:function name='f:attribution-order'>
283   <xsl:param name='a' />
284   <xsl:param name='b' />
285
286   <xsl:variable name='docmatch'
287     select='number($a/copyright-holder = /document/copyright-holder)
288             - number($b/copyright-holder = /document/copyright-holder)' />
289
290   <xsl:variable name='authmatch'
291     select='count(f:matching-child($a/copyright-holder, $a))
292             - count(f:matching-child($b/copyright-holder, $b))' />
293
294   <xsl:variable name='licmatch'
295     select='count(f:matching-child($a/license, $a))
296             - count(f:matching-child($b/license, $b))' />
297
298   <xsl:choose>
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>
303   </xsl:choose>
304 </func:function>
305
306 <xsl:template match='image/license'>
307   <xsl:text>, </xsl:text>
308   <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
309 </xsl:template>
310
311 <xsl:template match='image'>
312   <xsl:choose>
313     <xsl:when test='position() = 1'>, except </xsl:when>
314     <xsl:when test='position() = last()'> and </xsl:when>
315     <xsl:otherwise>, </xsl:otherwise>
316   </xsl:choose>
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' />
321 </xsl:template>
322
323 <xsl:template name='image-attribution'>
324 <!--
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]' />
327 -->
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' />
338
339       <xsl:call-template name='notransform' />
340     </xsl:for-each>
341   </xsl:variable>
342   <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
343
344   <xsl:variable name='abbrev-split'
345       select='count($images[copyright-holder = $images[1]/copyright-holder
346                  and license/identifier = $images[1]/license/identifier])' />
347
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>
352     </xsl:for-each>
353   </xsl:variable>
354   <xsl:variable name='abbrev-years'
355     select='exslt:node-set($abbrev-years-fragment)/*' />
356
357   <p>
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()])' />
362     </xsl:if>
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>
367   </p>
368 </xsl:template>
369
370 <xsl:template match='source'>
371   <p>
372     <xsl:text>This document was compiled</xsl:text>
373     <xsl:choose>
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' />
378         </a>
379       </xsl:when>
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' />
384         </a>
385       </xsl:when>
386     </xsl:choose>
387     <xsl:text> on </xsl:text>
388     <xsl:value-of select='compiletime' />
389     <xsl:text>.</xsl:text>
390   </p>
391 </xsl:template>
392
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'>
397       <p>
398         <xsl:text>Posted </xsl:text>
399         <xsl:value-of select='/document/article/published' />
400       </p>
401     </div>
402   </xsl:if>
403 </xsl:template>
404
405 <xsl:template match='/'>
406   <html>
407     <head>
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:," />
411       <title>
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, " – ")' />
415         </xsl:if>
416         <xsl:value-of select='$site-title' />
417       </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>
421       </xsl:for-each>
422     </head>
423     <body>
424       <xsl:apply-templates select='/document/xhtml:html/@*' />
425
426       <xsl:if test='/document/hierarchy/parent'>
427         <p id='sitetitle'>
428           <small><xsl:value-of select='$site-title' /></small>
429         </p>
430         <div id='breadcrumbs'>
431           <strong>Return to: </strong>
432           <ul>
433             <xsl:for-each select='/document/hierarchy/parent'>
434               <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
435             </xsl:for-each>
436           </ul>
437         </div>
438         <hr />
439       </xsl:if>
440
441       <xsl:apply-templates select='/document/xhtml:html/node()' />
442
443       <hr />
444       <div id='footer'>
445         <xsl:apply-templates select='/document/copyright' />
446         <xsl:apply-templates select='/document/license' />
447         <xsl:apply-templates select='/document/source' />
448       </div>
449     </body>
450   </html>
451 </xsl:template>
452
453 <xsl:include href='layouts/clickytable.xsl' />
454
455 </xsl:stylesheet>