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