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