Release slotifier-1.
[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-2020 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
31 <xsl:output method='xml' encoding='UTF-8' indent='yes'
32   doctype-public='-//W3C//DTD XHTML 1.1//EN'
33   doctype-system='http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
34   cdata-section-elements='style' />
35
36 <xsl:param name='source-uri'
37   select='"//git.draconx.ca/gitweb/homepage.git/"' />
38 <xsl:param name='site-title' select='"The Citrine Citadel"' />
39 <xsl:param name='section-links' select='//document/section-links' />
40
41 <func:function name='f:ends-with'>
42   <xsl:param name='a' />
43   <xsl:param name='b' />
44   <func:result
45     select='substring($a, string-length($a)-string-length($b)+1)=$b' />
46 </func:function>
47
48 <xsl:template match='node()|@*'>
49   <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
50 </xsl:template>
51
52 <!--
53   Nokogiri's pretty-printer is a bit weird.  Regardless of the indentation
54   setting, if an element has no child text nodes then it will be pretty-
55   printed.  This works by adding arbitrary whitespace to that element, and
56   then all of its children are eligible to be pretty-printed.
57
58   If an element has any text nodes at all, then it is not pretty-printed and
59   neither are any of its descendents.
60
61   Adding arbitrary whitespace to <pre> is bad, so we inject zero-width non-
62   breaking spaces to prevent this.  This will render fine but the spaces
63   should be removed before final output to avoid problems with copy+paste.
64 -->
65 <xsl:template match='xhtml:pre'>
66   <xsl:copy>
67     <xsl:apply-templates select='node()|@*' />
68     <xsl:text>&#x2060;</xsl:text>
69   </xsl:copy>
70 </xsl:template>
71
72 <!--
73   Likewise, adding spaces between consecutive span-level elements where
74   none existed before won't go over well.
75 -->
76 <xsl:template name='glue-preceding-span'>
77   <xsl:if test='f:element-is-span(preceding-sibling::node()[1])'>
78     <xsl:text>&#x2060;</xsl:text>
79   </xsl:if>
80 </xsl:template>
81
82 <xsl:template match='*[f:element-is-span()]'>
83   <xsl:call-template name='glue-preceding-span' />
84   <xsl:copy>
85     <xsl:apply-templates select='node()|@*' />
86     <xsl:if test='*'>
87       <!-- avoid breaking within a span element -->
88       <xsl:text>&#x2060;</xsl:text>
89     </xsl:if>
90   </xsl:copy>
91 </xsl:template>
92
93 <!--
94   Manually strip whitespace-only text nodes so the pretty printer can do its
95   thing on remaining elements.
96 -->
97 <xsl:template match='text()[normalize-space(.) = ""]'>
98   <xsl:choose>
99     <!-- preserve anything according to xml:space -->
100     <xsl:when test='ancestor::*[@xml:space][1][@xml:space="preserve"]'>
101       <xsl:copy />
102     </xsl:when>
103     <!-- preserve anything under <pre> -->
104     <xsl:when test='ancestor::xhtml:pre'><xsl:copy /></xsl:when>
105     <!-- preserve whitespace which is the only child node of an element -->
106     <xsl:when test='count(../node()) = 1'><xsl:copy /></xsl:when>
107     <!-- preserve whitespace between consecutive span-level elements
108          which have at least one non-whitespace sibling text element -->
109     <xsl:when test='f:element-is-span(preceding-sibling::node()[1])
110                     and f:element-is-span(following-sibling::node()[1])
111                     and ../text()[normalize-space(.) != ""]'>
112       <xsl:copy />
113     </xsl:when>
114   </xsl:choose>
115 </xsl:template>
116
117 <!-- Clean up whitespace where harmless to do so -->
118 <xsl:template match='xhtml:p/node()[1][self::text()]'>
119   <xsl:value-of select='f:strip-leading()' />
120 </xsl:template>
121 <xsl:template match='xhtml:p/node()[position()=last()][self::text()]'>
122   <xsl:value-of select='f:strip-trailing()' />
123 </xsl:template>
124
125 <!-- Add rel attributes to external links -->
126 <xsl:template match='xhtml:a[starts-with(@href,"http://")
127                           or starts-with(@href,"https://")
128                           or starts-with(@href,"//")]'>
129   <xsl:variable name='domain'
130     select='substring-before(
131               concat(translate(substring-after(@href, "//"), ":", "/"), "/"),
132               "/")' />
133
134   <xsl:copy>
135     <xsl:apply-templates select='@*' />
136     <xsl:if test='not($domain="draconx.ca"
137                       or f:ends-with($domain, ".draconx.ca"))'>
138       <xsl:attribute name='rel'>
139         <xsl:if test='@rel'>
140           <xsl:value-of select='@rel' />
141           <xsl:text> </xsl:text>
142         </xsl:if>
143         <xsl:text>external noopener noreferrer</xsl:text>
144       </xsl:attribute>
145     </xsl:if>
146     <xsl:apply-templates select='node()' />
147   </xsl:copy>
148 </xsl:template>
149
150 <xsl:template match='xhtml:h2[@id]'>
151   <xsl:variable name='fragment' select='concat("#", @id)' />
152   <xsl:copy>
153     <xsl:apply-templates select='node()|@*' />
154     <xsl:if test='$section-links = "yes"'>
155       <xsl:text> </xsl:text>
156       <small class='permalink'>
157         (<a href='{$fragment}'><xsl:value-of select='$fragment' /></a>)
158       </small>
159     </xsl:if>
160   </xsl:copy>
161 </xsl:template>
162
163 <!--
164   Convert caption attribute on tables into proper caption elements, to allow
165   a simple way to add captions to kramdown tables.
166 -->
167 <xsl:template match='@caption[parent::xhtml:table]' />
168 <xsl:template match='xhtml:table[@caption]'>
169   <xsl:copy>
170     <xsl:apply-templates select='@*' />
171     <caption><xsl:value-of select='normalize-space(@caption)' /></caption>
172     <xsl:apply-templates select='node()' />
173   </xsl:copy>
174 </xsl:template>
175
176 <!--
177   Delete style elements, as they will get hoisted occur under <head> below.
178   If the generate-listing attribute was specified, produce a code listing
179   where the style attribute was found.
180 -->
181 <xsl:template match='xhtml:style|@generate-listing[parent::xhtml:style]' />
182 <xsl:template match='xhtml:style[@generate-listing]'>
183   <pre>&#x2060;<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
184 </xsl:template>
185
186 <!--
187   Add a simple way to reference a document node by ID and include the XHTML
188   code listing directly in the document.
189 -->
190 <xsl:template match='xhtml:generate-xhtml-listing'>
191   <xsl:variable name='target' select='@target' />
192   <pre>&#x2060;<code>
193     <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
194   </code></pre>
195 </xsl:template>
196
197 <xsl:template match='copyright'>
198   <p>
199     <xsl:text>Copyright © </xsl:text>
200     <xsl:value-of select='text()' />
201     <xsl:text>.</xsl:text>
202   </p>
203 </xsl:template>
204
205 <xsl:template match='license'>
206   <p>
207     <xsl:text>Copying and distribution of this material</xsl:text>
208     <xsl:if test='normalize-space(modification-allowed)="yes"'>
209       <xsl:text>, with or without modification,</xsl:text>
210     </xsl:if>
211     <xsl:text> is permitted under the terms of the </xsl:text>
212     <a rel='license'>
213       <xsl:attribute name='href'>
214         <xsl:value-of select='normalize-space(uri)' />
215       </xsl:attribute>
216       <xsl:value-of select='name' />
217     </a>
218     <xsl:text>.</xsl:text>
219   </p>
220 </xsl:template>
221
222 <func:function name='f:matching-child'>
223   <xsl:param name='child' select='./copyright-holder' />
224   <xsl:param name='node' select='.' />
225   <xsl:param name='nodeset' select='$node/../*[name()=name($node)]' />
226
227   <func:result select='$nodeset[*[name()=name($child)]=$child]' />
228 </func:function>
229
230 <func:function name='f:attribution-order'>
231   <xsl:param name='a' />
232   <xsl:param name='b' />
233
234   <xsl:variable name='docmatch'
235     select='number($a/copyright-holder = /document/copyright-holder)
236             - number($b/copyright-holder = /document/copyright-holder)' />
237
238   <xsl:variable name='authmatch'
239     select='count(f:matching-child($a/copyright-holder, $a))
240             - count(f:matching-child($b/copyright-holder, $b))' />
241
242   <xsl:variable name='licmatch'
243     select='count(f:matching-child($a/license, $a))
244             - count(f:matching-child($b/license, $b))' />
245
246   <xsl:choose>
247     <xsl:when test='$docmatch'><func:result select='$docmatch' /></xsl:when>
248     <xsl:when test='$authmatch'><func:result select='$authmatch' /></xsl:when>
249     <xsl:when test='$licmatch'><func:result select='$licmatch' /></xsl:when>
250     <xsl:otherwise><func:result select='"nope"' /></xsl:otherwise>
251   </xsl:choose>
252 </func:function>
253
254 <xsl:template match='image/license'>
255   <xsl:text>, </xsl:text>
256   <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
257 </xsl:template>
258
259 <xsl:template match='image'>
260   <xsl:choose>
261     <xsl:when test='position() = 1'>, except </xsl:when>
262     <xsl:when test='position() = last()'> and </xsl:when>
263     <xsl:otherwise>, </xsl:otherwise>
264   </xsl:choose>
265   <a href='{uri}'><xsl:value-of select='title' /></a>
266   <xsl:text> © </xsl:text>
267   <xsl:value-of select='copyright' />
268   <xsl:apply-templates select='license' />
269 </xsl:template>
270
271 <xsl:template name='image-attribution'>
272 <!--
273   <xsl:variable name='x' select='/document/image[copyright-holder="Nick Bowler"][1]' />
274   <xsl:variable name='y' select='/document/image[copyright-holder="Nick Bowler"][4]' />
275 -->
276   <xsl:variable name='images-fragment'>
277     <xsl:for-each select='/document/image'>
278       <xsl:sort select='number(copyright-holder = /document/copyright-holder)'
279                 data-type='number' order='descending' />
280       <xsl:sort select='count(f:matching-child(copyright-holder))'
281                 data-type='number' order='descending' />
282       <xsl:sort select='copyright-holder' order='descending' />
283       <xsl:sort select='count(f:matching-child(license))'
284                 data-type='number' order='descending' />
285       <xsl:sort select='license/identifier' order='descending' />
286
287       <xsl:call-template name='notransform' />
288     </xsl:for-each>
289   </xsl:variable>
290   <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
291
292   <xsl:variable name='abbrev-split'
293       select='count($images[copyright-holder = $images[1]/copyright-holder
294                  and license/identifier = $images[1]/license/identifier])' />
295
296   <xsl:variable name='abbrev-years-fragment'>
297     <xsl:for-each select='$images[$abbrev-split >= position()]/copyright-year'>
298       <xsl:sort data-type='number' />
299       <copyright-year><xsl:value-of select='.' /></copyright-year>
300     </xsl:for-each>
301   </xsl:variable>
302   <xsl:variable name='abbrev-years'
303     select='exslt:node-set($abbrev-years-fragment)/*' />
304
305   <p>
306     <xsl:text>Images © </xsl:text>
307     <xsl:value-of select='$abbrev-years[1]' />
308     <xsl:if test='$abbrev-years[last()] != $abbrev-years[1]'>
309       <xsl:value-of select='concat("–", $abbrev-years[last()])' />
310     </xsl:if>
311     <xsl:value-of select='concat(" ", $images[1]/copyright-holder)' />
312     <xsl:apply-templates select='$images[1]/license' />
313     <xsl:apply-templates select='$images[position() > $abbrev-split]' />
314     <xsl:text>.</xsl:text>
315   </p>
316 </xsl:template>
317
318 <xsl:template match='source'>
319   <p>
320     <xsl:text>This document was compiled</xsl:text>
321     <xsl:choose>
322       <xsl:when test='file'>
323         <xsl:text> from </xsl:text>
324         <a href='{concat($source-uri, "blob/", revision, ":", file)}'>
325           <xsl:value-of select='file' />
326         </a>
327       </xsl:when>
328       <xsl:when test='dir'>
329         <xsl:text> from </xsl:text>
330         <a href='{concat($source-uri, "tree/", revision, ":", dir)}'>
331           <xsl:value-of select='dir' />
332         </a>
333       </xsl:when>
334     </xsl:choose>
335     <xsl:text> on </xsl:text>
336     <xsl:value-of select='compiletime' />
337     <xsl:text>.</xsl:text>
338   </p>
339 </xsl:template>
340
341 <xsl:template match='xhtml:h1[not(preceding::xhtml:h1)]'>
342   <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
343   <xsl:if test='/document/article/published'>
344     <div id='article-info'>
345       <p>
346         <xsl:text>Posted </xsl:text>
347         <xsl:value-of select='/document/article/published' />
348       </p>
349     </div>
350   </xsl:if>
351 </xsl:template>
352
353 <xsl:template match='/'>
354   <html>
355     <head>
356       <meta name='viewport' content='width=device-width, initial-scale=1' />
357       <link rel='stylesheet' type='text/css' href='/style.css' />
358       <link rel="icon" href="data:," />
359       <title>
360         <xsl:variable name='page-title' select='string(/document/title)' />
361         <xsl:if test='$page-title and $site-title != $page-title'>
362           <xsl:value-of select='concat($page-title, " – ")' />
363         </xsl:if>
364         <xsl:value-of select='$site-title' />
365       </title>
366       <!-- Hoist all style elements to <head> as required by the doctype. -->
367       <xsl:for-each select='//xhtml:style'>
368         <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
369       </xsl:for-each>
370     </head>
371     <body>
372       <xsl:apply-templates select='/document/xhtml:html/@*' />
373
374       <xsl:if test='/document/hierarchy/parent'>
375         <p id='sitetitle'>
376           <small><xsl:value-of select='$site-title' /></small>
377         </p>
378         <div id='breadcrumbs'>
379           <strong>Return to: </strong>
380           <ul>
381             <xsl:for-each select='/document/hierarchy/parent'>
382               <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
383             </xsl:for-each>
384           </ul>
385         </div>
386         <hr />
387       </xsl:if>
388
389       <xsl:apply-templates select='/document/xhtml:html/node()' />
390
391       <hr />
392       <div id='footer'>
393         <xsl:apply-templates select='/document/copyright' />
394         <xsl:apply-templates select='/document/license' />
395         <xsl:apply-templates select='/document/source' />
396       </div>
397     </body>
398   </html>
399 </xsl:template>
400
401 </xsl:stylesheet>