Simplify footer text when everything is identical.
[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: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'>
29
30 <xsl:import href='layouts/functions.xsl' />
31 <xsl:output cdata-section-elements='style script' />
32
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' />
37
38 <func:function name='f:ends-with'>
39   <xsl:param name='a' />
40   <xsl:param name='b' />
41   <func:result
42     select='substring($a, string-length($a)-string-length($b)+1)=$b' />
43 </func:function>
44
45 <xsl:template match='node()|@*'>
46   <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
47 </xsl:template>
48
49 <xsl:template name='notransform' mode='notransform' match='node()|@*'>
50   <xsl:copy>
51     <xsl:apply-templates mode='notransform' select='node()|@*' />
52   </xsl:copy>
53 </xsl:template>
54
55 <!--
56   Nokogiri's pretty-printer is a bit weird.  Regardless of the indentation
57   setting, if an element has no child text nodes then it will be pretty-
58   printed.  This works by adding arbitrary whitespace to that element, and
59   then all of its children are eligible to be pretty-printed.
60
61   If an element has any text nodes at all, then it is not pretty-printed and
62   neither are any of its descendents.
63
64   Adding arbitrary whitespace to <pre> is bad, so we inject zero-width non-
65   breaking spaces to prevent this.  This will render fine but the spaces
66   should be removed before final output to avoid problems with copy+paste.
67 -->
68 <xsl:template match='xhtml:pre'>
69   <xsl:copy>
70     <xsl:apply-templates select='node()|@*' />
71     <xsl:text>&#x2060;</xsl:text>
72   </xsl:copy>
73 </xsl:template>
74
75 <!--
76   Likewise, adding spaces between consecutive span-level elements where
77   none existed before won't go over well.
78 -->
79 <xsl:template name='glue-preceding-span'>
80   <xsl:if test='f:element-is-span(preceding-sibling::node()[1])'>
81     <xsl:text>&#x2060;</xsl:text>
82   </xsl:if>
83 </xsl:template>
84
85 <xsl:template match='*[f:element-is-span()]'>
86   <xsl:call-template name='glue-preceding-span' />
87   <xsl:copy>
88     <xsl:apply-templates select='node()|@*' />
89     <xsl:if test='*'>
90       <!-- avoid breaking within a span element -->
91       <xsl:text>&#x2060;</xsl:text>
92     </xsl:if>
93   </xsl:copy>
94 </xsl:template>
95
96 <!--
97   Manually strip whitespace-only text nodes so the pretty printer can do its
98   thing on remaining elements.
99 -->
100 <xsl:template match='text()[normalize-space(.) = ""]'>
101   <xsl:choose>
102     <!-- preserve anything according to xml:space -->
103     <xsl:when test='ancestor::*[@xml:space][1][@xml:space="preserve"]'>
104       <xsl:copy />
105     </xsl:when>
106     <!-- preserve anything under <pre> -->
107     <xsl:when test='ancestor::xhtml:pre'><xsl:copy /></xsl:when>
108     <!-- preserve whitespace which is the only child node of an element -->
109     <xsl:when test='count(../node()) = 1'><xsl:copy /></xsl:when>
110     <!-- preserve whitespace between consecutive span-level elements
111          which have at least one non-whitespace sibling text element -->
112     <xsl:when test='f:element-is-span(preceding-sibling::node()[1])
113                     and f:element-is-span(following-sibling::node()[1])
114                     and ../text()[normalize-space(.) != ""]'>
115       <xsl:copy />
116     </xsl:when>
117   </xsl:choose>
118 </xsl:template>
119
120 <!-- Clean up whitespace where harmless to do so -->
121 <xsl:template match='xhtml:p/node()[1][self::text()]'>
122   <xsl:value-of select='f:strip-leading()' />
123 </xsl:template>
124 <xsl:template match='xhtml:p/node()[position()=last()][self::text()]'>
125   <xsl:value-of select='f:strip-trailing()' />
126 </xsl:template>
127
128 <!-- Add rel attributes to external links -->
129 <xsl:template match='xhtml:a[starts-with(@href,"http://")
130                           or starts-with(@href,"https://")
131                           or starts-with(@href,"//")]'>
132   <xsl:variable name='domain'
133     select='substring-before(
134               concat(translate(substring-after(@href, "//"), ":", "/"), "/"),
135               "/")' />
136
137   <xsl:copy>
138     <xsl:apply-templates select='@*' />
139     <xsl:if test='not($domain="draconx.ca"
140                       or f:ends-with($domain, ".draconx.ca"))'>
141       <xsl:attribute name='rel'>
142         <xsl:if test='@rel'>
143           <xsl:value-of select='@rel' />
144           <xsl:text> </xsl:text>
145         </xsl:if>
146         <xsl:text>external noopener noreferrer</xsl:text>
147       </xsl:attribute>
148     </xsl:if>
149     <xsl:apply-templates select='node()' />
150   </xsl:copy>
151 </xsl:template>
152
153 <xsl:template match='xhtml:h2[@id]'>
154   <xsl:variable name='fragment' select='concat("#", @id)' />
155   <xsl:copy>
156     <xsl:apply-templates select='node()|@*' />
157     <xsl:if test='$section-links = "yes"'>
158       <xsl:text> </xsl:text>
159       <small class='permalink'>
160         (<a href='{$fragment}'><xsl:value-of select='$fragment' /></a>)
161       </small>
162     </xsl:if>
163   </xsl:copy>
164 </xsl:template>
165
166 <!--
167   Convert caption attribute on tables into proper caption elements, to allow
168   a simple way to add captions to kramdown tables.
169 -->
170 <xsl:template match='@caption[parent::xhtml:table]' />
171 <xsl:template match='xhtml:table[@caption]'>
172   <xsl:copy>
173     <xsl:apply-templates select='@*' />
174     <caption><xsl:value-of select='normalize-space(@caption)' /></caption>
175     <xsl:apply-templates select='node()' />
176   </xsl:copy>
177 </xsl:template>
178
179 <!--
180   Delete style elements, as they will get hoisted occur under <head> below.
181   If the generate-listing attribute was specified, produce a code listing
182   where the style attribute was found.
183 -->
184 <xsl:template match='xhtml:style|@generate-listing[parent::xhtml:style]' />
185 <xsl:template match='xhtml:style[@generate-listing]'>
186   <pre>&#x2060;<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
187 </xsl:template>
188
189 <!--
190   Attempt to wrap the first bit of linked email addresses to allow
191   linewrapping to occur after the '@'.
192 -->
193 <xsl:template match='xhtml:a[starts-with(@href,"mailto:")]/text()'>
194   <xsl:variable name='addr' select='substring-after(../@href, "mailto:")' />
195
196   <xsl:variable name='wrap'
197     select='concat(substring-before($addr, "@"), "@")' />
198
199   <xsl:choose>
200     <xsl:when test='contains(., $wrap)'>
201       <xsl:value-of select='substring-before(., $wrap)' />
202       <span class='wbr'>
203         <xsl:value-of select='$wrap' />
204       </span>
205       <xsl:value-of select='substring-after(., $wrap)' />
206     </xsl:when>
207     <xsl:otherwise>
208       <xsl:copy />
209     </xsl:otherwise>
210   </xsl:choose>
211 </xsl:template>
212
213 <!--
214   Add a simple way to reference a document node by ID and include the XHTML
215   code listing directly in the document.
216 -->
217 <xsl:template match='xhtml:generate-xhtml-listing'>
218   <xsl:variable name='target' select='@target' />
219   <pre>&#x2060;<code>
220     <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
221   </code></pre>
222 </xsl:template>
223
224 <!-- For paragraphs containing only kbd elements, wrap in blockquote. -->
225 <xsl:template match='xhtml:p[*[last()=count(../xhtml:kbd)]]'>
226   <blockquote>
227     <xsl:copy>
228       <xsl:apply-templates select='node()|@*' />
229     </xsl:copy>
230   </blockquote>
231 </xsl:template>
232
233 <!--
234   Wrap each word of text in kbd elements in spans, so they can be styled
235   to avoid linebreaks in the middle of option names and other bad places.
236 -->
237 <xsl:template name='spanify-text' match='xhtml:kbd/text()'>
238   <xsl:param name='text' select='normalize-space(.)' />
239   <xsl:variable name='firstword' select='substring-before($text, " ")' />
240   <xsl:choose>
241     <xsl:when test='$firstword'>
242       <span><xsl:value-of select='$firstword' /></span>
243       <xsl:text> </xsl:text>
244     </xsl:when>
245     <xsl:when test='$text'>
246       <span><xsl:value-of select='$text' /></span>
247     </xsl:when>
248   </xsl:choose>
249   <xsl:if test='$firstword'>
250     <xsl:call-template name='spanify-text'>
251       <xsl:with-param name='text' select='substring-after($text, " ")' />
252     </xsl:call-template>
253   </xsl:if>
254 </xsl:template>
255
256 <xsl:template match='copyright'>
257   <p>
258     <xsl:text>Copyright © </xsl:text>
259     <xsl:value-of select='text()' />
260     <xsl:text>.</xsl:text>
261   </p>
262 </xsl:template>
263
264 <xsl:template match='license'>
265   <xsl:variable name='node' select='.' />
266   <p>
267     <xsl:choose>
268       <xsl:when test='/document/image[license/identifier != $node/identifier]'>
269         <xsl:text>Except as otherwise noted, copying</xsl:text>
270       </xsl:when>
271       <xsl:otherwise>Copying</xsl:otherwise>
272     </xsl:choose>
273     <xsl:text> and distribution of this material</xsl:text>
274     <xsl:if test='normalize-space(modification-allowed)="yes"'>
275       <xsl:text>, with or without modification,</xsl:text>
276     </xsl:if>
277     <xsl:text> is permitted under the terms of the </xsl:text>
278     <a rel='license'>
279       <xsl:attribute name='href'>
280         <xsl:value-of select='normalize-space(uri)' />
281       </xsl:attribute>
282       <xsl:value-of select='name' />
283     </a>
284     <xsl:text>.</xsl:text>
285   </p>
286 </xsl:template>
287
288 <func:function name='f:matching-child'>
289   <xsl:param name='child' select='./copyright-holder' />
290   <xsl:param name='node' select='.' />
291   <xsl:param name='nodeset' select='$node/../*[name()=name($node)]' />
292
293   <func:result select='$nodeset[*[name()=name($child)]=$child]' />
294 </func:function>
295
296 <func:function name='f:attribution-order'>
297   <xsl:param name='a' />
298   <xsl:param name='b' />
299
300   <xsl:variable name='docmatch'
301     select='number($a/copyright-holder = /document/copyright-holder)
302             - number($b/copyright-holder = /document/copyright-holder)' />
303
304   <xsl:variable name='authmatch'
305     select='count(f:matching-child($a/copyright-holder, $a))
306             - count(f:matching-child($b/copyright-holder, $b))' />
307
308   <xsl:variable name='licmatch'
309     select='count(f:matching-child($a/license, $a))
310             - count(f:matching-child($b/license, $b))' />
311
312   <xsl:choose>
313     <xsl:when test='$docmatch'><func:result select='$docmatch' /></xsl:when>
314     <xsl:when test='$authmatch'><func:result select='$authmatch' /></xsl:when>
315     <xsl:when test='$licmatch'><func:result select='$licmatch' /></xsl:when>
316     <xsl:otherwise><func:result select='"nope"' /></xsl:otherwise>
317   </xsl:choose>
318 </func:function>
319
320 <xsl:template match='image/license'>
321   <xsl:text>, </xsl:text>
322   <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
323 </xsl:template>
324
325 <xsl:template match='image'>
326   <xsl:choose>
327     <xsl:when test='position() = 1'>, except </xsl:when>
328     <xsl:when test='position() = last()'> and </xsl:when>
329     <xsl:otherwise>, </xsl:otherwise>
330   </xsl:choose>
331   <a href='{uri}'><xsl:value-of select='title' /></a>
332   <xsl:text> © </xsl:text>
333   <xsl:value-of select='copyright' />
334   <xsl:apply-templates select='license' />
335 </xsl:template>
336
337 <xsl:template name='image-attribution'>
338   <xsl:variable name='images-fragment'>
339     <xsl:for-each select='/document/image'>
340       <xsl:sort select='number(copyright-holder = /document/copyright-holder)'
341                 data-type='number' order='descending' />
342       <xsl:sort select='count(f:matching-child(copyright-holder))'
343                 data-type='number' order='descending' />
344       <xsl:sort select='copyright-holder' order='descending' />
345       <xsl:sort select='count(f:matching-child(license))'
346                 data-type='number' order='descending' />
347       <xsl:sort select='license/identifier' order='descending' />
348
349       <xsl:call-template name='notransform' />
350     </xsl:for-each>
351   </xsl:variable>
352   <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
353
354   <xsl:variable name='abbrev-split'
355       select='count($images[copyright-holder = $images[1]/copyright-holder
356                  and license/identifier = $images[1]/license/identifier])' />
357
358   <xsl:variable name='abbrev-years-fragment'>
359     <xsl:for-each select='$images[$abbrev-split >= position()]/copyright-year'>
360       <xsl:sort data-type='number' />
361       <copyright-year><xsl:value-of select='.' /></copyright-year>
362     </xsl:for-each>
363   </xsl:variable>
364   <xsl:variable name='abbrev-years'
365     select='exslt:node-set($abbrev-years-fragment)/*' />
366
367   <p>
368     <xsl:text>Images © </xsl:text>
369     <xsl:value-of select='$abbrev-years[1]' />
370     <xsl:if test='$abbrev-years[last()] != $abbrev-years[1]'>
371       <xsl:value-of select='concat("–", $abbrev-years[last()])' />
372     </xsl:if>
373     <xsl:value-of select='concat(" ", $images[1]/copyright-holder)' />
374     <xsl:apply-templates select='$images[1]/license' />
375     <xsl:apply-templates select='$images[position() > $abbrev-split]' />
376     <xsl:text>.</xsl:text>
377   </p>
378 </xsl:template>
379
380 <xsl:template match='source'>
381   <p>
382     <xsl:text>This document was compiled</xsl:text>
383     <xsl:choose>
384       <xsl:when test='file'>
385         <xsl:text> from </xsl:text>
386         <a href='{concat($source-uri, "blob/", revision, ":", file)}'>
387           <xsl:value-of select='file' />
388         </a>
389       </xsl:when>
390       <xsl:when test='dir'>
391         <xsl:text> from </xsl:text>
392         <a href='{concat($source-uri, "tree/", revision, ":", dir)}'>
393           <xsl:value-of select='dir' />
394         </a>
395       </xsl:when>
396     </xsl:choose>
397     <xsl:text> on </xsl:text>
398     <xsl:value-of select='compiletime' />
399     <xsl:text>.</xsl:text>
400   </p>
401 </xsl:template>
402
403 <xsl:template match='xhtml:h1[not(preceding::xhtml:h1)]'>
404   <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
405   <xsl:if test='/document/article/published'>
406     <div id='article-info'>
407       <p>
408         <xsl:text>Posted </xsl:text>
409         <xsl:value-of select='/document/article/published' />
410       </p>
411     </div>
412   </xsl:if>
413 </xsl:template>
414
415 <xsl:template name='imgpara' match='xhtml:p[count(*)=1]
416                                            [normalize-space(text())=""]
417                                            [descendant::xhtml:img]'>
418   <xsl:copy>
419     <xsl:apply-templates select='@*[local-name() != "class"]' />
420     <xsl:attribute name='class'>
421       <xsl:if test='@class'>
422         <xsl:value-of select='concat(@class, " ")' />
423       </xsl:if>
424       <xsl:text>img</xsl:text>
425     </xsl:attribute>
426     <xsl:apply-templates select='node()' />
427   </xsl:copy>
428 </xsl:template>
429
430 <xsl:key name='gallery'
431   match='xhtml:html/xhtml:p[descendant::*[@generate-gallery]]'
432   use='generate-id(
433         ( ( preceding-sibling::*[not(descendant::*[@generate-gallery])][1]
434           /following-sibling::* ) | self::* ) [1])' />
435
436 <xsl:template match='@generate-gallery' />
437 <xsl:template match='xhtml:html/xhtml:p[descendant::*[@generate-gallery]]' />
438 <xsl:template match='xhtml:html/xhtml:p[key("gallery", generate-id(.))]'>
439   <xsl:variable name='images' select='key("gallery", generate-id(.))' />
440   <xsl:choose>
441     <xsl:when test='count($images) > 1'>
442       <div class='gallery'>
443         <xsl:for-each select='$images'>
444           <xsl:call-template name='imgpara' />
445         </xsl:for-each>
446       </div>
447     </xsl:when>
448     <xsl:otherwise>
449       <xsl:call-template name='imgpara' />
450     </xsl:otherwise>
451   </xsl:choose>
452 </xsl:template>
453
454 <xsl:template match='/'>
455   <html>
456     <head>
457       <meta name='viewport' content='width=device-width, initial-scale=1' />
458       <link rel='stylesheet' type='text/css' href='/style.css' />
459       <link rel="icon" href="data:," />
460       <title>
461         <xsl:variable name='page-title' select='string(/document/title)' />
462         <xsl:if test='$page-title and $site-title != $page-title'>
463           <xsl:value-of select='concat($page-title, " – ")' />
464         </xsl:if>
465         <xsl:value-of select='$site-title' />
466       </title>
467       <!-- Hoist all style elements to <head> as required by the doctype. -->
468       <xsl:for-each select='//xhtml:style'>
469         <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
470       </xsl:for-each>
471     </head>
472     <body>
473       <xsl:apply-templates select='/document/xhtml:html/@*' />
474
475       <xsl:if test='/document/hierarchy/parent'>
476         <p id='sitetitle'>
477           <small><xsl:value-of select='$site-title' /></small>
478         </p>
479         <div id='breadcrumbs'>
480           <strong>Return to: </strong>
481           <ul>
482             <xsl:for-each select='/document/hierarchy/parent'>
483               <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
484             </xsl:for-each>
485           </ul>
486         </div>
487         <hr />
488       </xsl:if>
489
490       <xsl:apply-templates select='/document/xhtml:html/node()' />
491
492       <hr />
493       <div id='footer'>
494         <xsl:apply-templates select='/document/copyright' />
495         <xsl:apply-templates select='/document/license' />
496         <xsl:if test='/document/image[copyright != /document/copyright
497                       or license/identifier != /document/license/identifier]'>
498           <xsl:call-template name='image-attribution' />
499         </xsl:if>
500         <xsl:apply-templates select='/document/source' />
501       </div>
502     </body>
503   </html>
504 </xsl:template>
505
506 <xsl:include href='layouts/clickytable.xsl' />
507
508 </xsl:stylesheet>