<!--
Nick's web site: XHTML output stage
- Copyright © 2018-2019 Nick Bowler
+ Copyright © 2018-2022 Nick Bowler
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
xmlns:xhtml='http://www.w3.org/1999/xhtml'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:func='http://exslt.org/functions'
+ xmlns:exslt='http://exslt.org/common'
xmlns:f='http://draconx.ca/my-functions'
- extension-element-prefixes='func f'
+ extension-element-prefixes='exslt func f'
exclude-result-prefixes='xhtml'>
<xsl:import href='layouts/functions.xsl' />
-
-<xsl:output method='xml' encoding='UTF-8' indent='yes'
- doctype-public='-//W3C//DTD XHTML 1.1//EN'
- doctype-system='http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd' />
+<xsl:output cdata-section-elements='style script' />
<xsl:param name='source-uri'
- select='"//git.draconx.ca/gitweb/homepage.git/blob/"' />
+ select='"//git.draconx.ca/gitweb/homepage.git/"' />
<xsl:param name='site-title' select='"The Citrine Citadel"' />
+<xsl:param name='section-links' select='//document/section-links' />
<func:function name='f:ends-with'>
<xsl:param name='a' />
<xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
</xsl:template>
+<xsl:template name='notransform' mode='notransform' match='node()|@*'>
+ <xsl:copy>
+ <xsl:apply-templates mode='notransform' select='node()|@*' />
+ </xsl:copy>
+</xsl:template>
+
<!--
Nokogiri's pretty-printer is a bit weird. Regardless of the indentation
setting, if an element has no child text nodes then it will be pretty-
<xsl:when test='ancestor::xhtml:pre'><xsl:copy /></xsl:when>
<!-- preserve whitespace which is the only child node of an element -->
<xsl:when test='count(../node()) = 1'><xsl:copy /></xsl:when>
+ <!-- preserve whitespace between consecutive span-level elements
+ which have at least one non-whitespace sibling text element -->
+ <xsl:when test='f:element-is-span(preceding-sibling::node()[1])
+ and f:element-is-span(following-sibling::node()[1])
+ and ../text()[normalize-space(.) != ""]'>
+ <xsl:copy />
+ </xsl:when>
</xsl:choose>
</xsl:template>
<xsl:variable name='fragment' select='concat("#", @id)' />
<xsl:copy>
<xsl:apply-templates select='node()|@*' />
- <xsl:text> </xsl:text>
- <small class='permalink'>
- (<a href='{$fragment}'><xsl:value-of select='$fragment' /></a>)
- </small>
+ <xsl:if test='$section-links = "yes"'>
+ <xsl:text> </xsl:text>
+ <small class='permalink'>
+ (<a href='{$fragment}'><xsl:value-of select='$fragment' /></a>)
+ </small>
+ </xsl:if>
+ </xsl:copy>
+</xsl:template>
+
+<!--
+ Convert caption attribute on tables into proper caption elements, to allow
+ a simple way to add captions to kramdown tables.
+-->
+<xsl:template match='@caption[parent::xhtml:table]' />
+<xsl:template match='xhtml:table[@caption]'>
+ <xsl:copy>
+ <xsl:apply-templates select='@*' />
+ <caption><xsl:value-of select='normalize-space(@caption)' /></caption>
+ <xsl:apply-templates select='node()' />
</xsl:copy>
</xsl:template>
+<!--
+ Delete style elements, as they will get hoisted occur under <head> below.
+ If the generate-listing attribute was specified, produce a code listing
+ where the style attribute was found.
+-->
+<xsl:template match='xhtml:style|@generate-listing[parent::xhtml:style]' />
+<xsl:template match='xhtml:style[@generate-listing]'>
+ <pre>⁠<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
+</xsl:template>
+
+<!--
+ Attempt to wrap the first bit of linked email addresses to allow
+ linewrapping to occur after the '@'.
+-->
+<xsl:template match='xhtml:a[starts-with(@href,"mailto:")]/text()'>
+ <xsl:variable name='addr' select='substring-after(../@href, "mailto:")' />
+
+ <xsl:variable name='wrap'
+ select='concat(substring-before($addr, "@"), "@")' />
+
+ <xsl:choose>
+ <xsl:when test='contains(., $wrap)'>
+ <xsl:value-of select='substring-before(., $wrap)' />
+ <span class='wbr'>
+ <xsl:value-of select='$wrap' />
+ </span>
+ <xsl:value-of select='substring-after(., $wrap)' />
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:copy />
+ </xsl:otherwise>
+ </xsl:choose>
+</xsl:template>
+
+<!--
+ Add a simple way to reference a document node by ID and include the XHTML
+ code listing directly in the document.
+-->
+<xsl:template match='xhtml:generate-xhtml-listing'>
+ <xsl:variable name='target' select='@target' />
+ <pre>⁠<code>
+ <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
+ </code></pre>
+</xsl:template>
+
+<!-- For paragraphs containing only kbd elements, wrap in blockquote. -->
+<xsl:template match='xhtml:p[*[last()=count(../xhtml:kbd)]]'>
+ <blockquote>
+ <xsl:copy>
+ <xsl:apply-templates select='node()|@*' />
+ </xsl:copy>
+ </blockquote>
+</xsl:template>
+
+<!--
+ Wrap each word of text in kbd elements in spans, so they can be styled
+ to avoid linebreaks in the middle of option names and other bad places.
+-->
+<xsl:template name='spanify-text' match='xhtml:kbd/text()'>
+ <xsl:param name='text' select='normalize-space(.)' />
+ <xsl:variable name='firstword' select='substring-before($text, " ")' />
+ <xsl:choose>
+ <xsl:when test='$firstword'>
+ <span><xsl:value-of select='$firstword' /></span>
+ <xsl:text> </xsl:text>
+ </xsl:when>
+ <xsl:when test='$text'>
+ <span><xsl:value-of select='$text' /></span>
+ </xsl:when>
+ </xsl:choose>
+ <xsl:if test='$firstword'>
+ <xsl:call-template name='spanify-text'>
+ <xsl:with-param name='text' select='substring-after($text, " ")' />
+ </xsl:call-template>
+ </xsl:if>
+</xsl:template>
+
<xsl:template match='copyright'>
<p>
<xsl:text>Copyright © </xsl:text>
</xsl:template>
<xsl:template match='license'>
+ <xsl:variable name='node' select='.' />
<p>
- <xsl:text>Copying and distribution of this material</xsl:text>
+ <xsl:choose>
+ <xsl:when test='/document/image[license/identifier != $node/identifier]'>
+ <xsl:text>Except as otherwise noted, copying</xsl:text>
+ </xsl:when>
+ <xsl:otherwise>Copying</xsl:otherwise>
+ </xsl:choose>
+ <xsl:text> and distribution of this material</xsl:text>
<xsl:if test='normalize-space(modification-allowed)="yes"'>
<xsl:text>, with or without modification,</xsl:text>
</xsl:if>
</p>
</xsl:template>
+<func:function name='f:matching-child'>
+ <xsl:param name='child' select='./copyright-holder' />
+ <xsl:param name='node' select='.' />
+ <xsl:param name='nodeset' select='$node/../*[name()=name($node)]' />
+
+ <func:result select='$nodeset[*[name()=name($child)]=$child]' />
+</func:function>
+
+<func:function name='f:attribution-order'>
+ <xsl:param name='a' />
+ <xsl:param name='b' />
+
+ <xsl:variable name='docmatch'
+ select='number($a/copyright-holder = /document/copyright-holder)
+ - number($b/copyright-holder = /document/copyright-holder)' />
+
+ <xsl:variable name='authmatch'
+ select='count(f:matching-child($a/copyright-holder, $a))
+ - count(f:matching-child($b/copyright-holder, $b))' />
+
+ <xsl:variable name='licmatch'
+ select='count(f:matching-child($a/license, $a))
+ - count(f:matching-child($b/license, $b))' />
+
+ <xsl:choose>
+ <xsl:when test='$docmatch'><func:result select='$docmatch' /></xsl:when>
+ <xsl:when test='$authmatch'><func:result select='$authmatch' /></xsl:when>
+ <xsl:when test='$licmatch'><func:result select='$licmatch' /></xsl:when>
+ <xsl:otherwise><func:result select='"nope"' /></xsl:otherwise>
+ </xsl:choose>
+</func:function>
+
+<xsl:template match='image/license'>
+ <xsl:text>, </xsl:text>
+ <a href='{uri}' rel='license'><xsl:value-of select='shortname' /></a>
+</xsl:template>
+
+<xsl:template match='image'>
+ <xsl:choose>
+ <xsl:when test='position() = 1'>, except </xsl:when>
+ <xsl:when test='position() = last()'> and </xsl:when>
+ <xsl:otherwise>, </xsl:otherwise>
+ </xsl:choose>
+ <a href='{uri}'><xsl:value-of select='title' /></a>
+ <xsl:text> © </xsl:text>
+ <xsl:value-of select='copyright' />
+ <xsl:apply-templates select='license' />
+</xsl:template>
+
+<xsl:template name='image-attribution'>
+ <xsl:variable name='images-fragment'>
+ <xsl:for-each select='/document/image'>
+ <xsl:sort select='number(copyright-holder = /document/copyright-holder)'
+ data-type='number' order='descending' />
+ <xsl:sort select='count(f:matching-child(copyright-holder))'
+ data-type='number' order='descending' />
+ <xsl:sort select='copyright-holder' order='descending' />
+ <xsl:sort select='count(f:matching-child(license))'
+ data-type='number' order='descending' />
+ <xsl:sort select='license/identifier' order='descending' />
+
+ <xsl:call-template name='notransform' />
+ </xsl:for-each>
+ </xsl:variable>
+ <xsl:variable name='images' select='exslt:node-set($images-fragment)/*' />
+
+ <xsl:variable name='abbrev-split'
+ select='count($images[copyright-holder = $images[1]/copyright-holder
+ and license/identifier = $images[1]/license/identifier])' />
+
+ <xsl:variable name='abbrev-years-fragment'>
+ <xsl:for-each select='$images[$abbrev-split >= position()]/copyright-year'>
+ <xsl:sort data-type='number' />
+ <copyright-year><xsl:value-of select='.' /></copyright-year>
+ </xsl:for-each>
+ </xsl:variable>
+ <xsl:variable name='abbrev-years'
+ select='exslt:node-set($abbrev-years-fragment)/*' />
+
+ <p>
+ <xsl:text>Images © </xsl:text>
+ <xsl:value-of select='$abbrev-years[1]' />
+ <xsl:if test='$abbrev-years[last()] != $abbrev-years[1]'>
+ <xsl:value-of select='concat("–", $abbrev-years[last()])' />
+ </xsl:if>
+ <xsl:value-of select='concat(" ", $images[1]/copyright-holder)' />
+ <xsl:apply-templates select='$images[1]/license' />
+ <xsl:apply-templates select='$images[position() > $abbrev-split]' />
+ <xsl:text>.</xsl:text>
+ </p>
+</xsl:template>
+
<xsl:template match='source'>
<p>
- <xsl:text>This document was compiled from </xsl:text>
- <a href='{concat($source-uri, revision, ":", file)}'>
- <xsl:value-of select='file' />
- </a>
+ <xsl:text>This document was compiled</xsl:text>
+ <xsl:choose>
+ <xsl:when test='file'>
+ <xsl:text> from </xsl:text>
+ <a href='{concat($source-uri, "blob/", revision, ":", file)}'>
+ <xsl:value-of select='file' />
+ </a>
+ </xsl:when>
+ <xsl:when test='dir'>
+ <xsl:text> from </xsl:text>
+ <a href='{concat($source-uri, "tree/", revision, ":", dir)}'>
+ <xsl:value-of select='dir' />
+ </a>
+ </xsl:when>
+ </xsl:choose>
<xsl:text> on </xsl:text>
<xsl:value-of select='compiletime' />
<xsl:text>.</xsl:text>
</p>
</xsl:template>
+<xsl:template match='xhtml:h1[not(preceding::xhtml:h1)]'>
+ <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
+ <xsl:if test='/document/article/published'>
+ <div id='article-info'>
+ <p>
+ <xsl:text>Posted </xsl:text>
+ <xsl:value-of select='/document/article/published' />
+ </p>
+ </div>
+ </xsl:if>
+</xsl:template>
+
+<xsl:template name='imgpara' match='xhtml:p[count(*)=1]
+ [normalize-space(text())=""]
+ [descendant::xhtml:img]'>
+ <xsl:copy>
+ <xsl:apply-templates select='@*[local-name() != "class"]' />
+ <xsl:attribute name='class'>
+ <xsl:if test='@class'>
+ <xsl:value-of select='concat(@class, " ")' />
+ </xsl:if>
+ <xsl:text>img</xsl:text>
+ </xsl:attribute>
+ <xsl:apply-templates select='node()' />
+ </xsl:copy>
+</xsl:template>
+
+<xsl:key name='gallery'
+ match='xhtml:html/xhtml:p[descendant::*[@generate-gallery]]'
+ use='generate-id(
+ ( ( preceding-sibling::*[not(descendant::*[@generate-gallery])][1]
+ /following-sibling::* ) | self::* ) [1])' />
+
+<xsl:template match='@generate-gallery' />
+<xsl:template match='xhtml:html/xhtml:p[descendant::*[@generate-gallery]]' />
+<xsl:template match='xhtml:html/xhtml:p[key("gallery", generate-id(.))]'>
+ <xsl:variable name='images' select='key("gallery", generate-id(.))' />
+ <xsl:choose>
+ <xsl:when test='count($images) > 1'>
+ <div class='gallery'>
+ <xsl:for-each select='$images'>
+ <xsl:call-template name='imgpara' />
+ </xsl:for-each>
+ </div>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:call-template name='imgpara' />
+ </xsl:otherwise>
+ </xsl:choose>
+</xsl:template>
+
<xsl:template match='/'>
<html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='stylesheet' type='text/css' href='/style.css' />
+ <link rel='alternate stylesheet' type='text/css' href='/dark.css'
+ title='Dark Style' />
<link rel="icon" href="data:," />
<title>
<xsl:variable name='page-title' select='string(/document/title)' />
</xsl:if>
<xsl:value-of select='$site-title' />
</title>
+ <!-- Hoist all style elements to <head> as required by the doctype. -->
+ <xsl:for-each select='//xhtml:style'>
+ <xsl:copy><xsl:apply-templates select='node()|@*' /></xsl:copy>
+ </xsl:for-each>
</head>
<body>
<xsl:apply-templates select='/document/xhtml:html/@*' />
+
+ <xsl:if test='/document/hierarchy/parent'>
+ <p id='sitetitle'>
+ <small><xsl:value-of select='$site-title' /></small>
+ </p>
+ <div id='breadcrumbs'>
+ <strong>Return to: </strong>
+ <ul>
+ <xsl:for-each select='/document/hierarchy/parent'>
+ <li><a href='{uri}'><xsl:value-of select='name'/></a></li>
+ </xsl:for-each>
+ </ul>
+ </div>
+ <hr />
+ </xsl:if>
+
<xsl:apply-templates select='/document/xhtml:html/node()' />
<hr />
<div id='footer'>
<xsl:apply-templates select='/document/copyright' />
<xsl:apply-templates select='/document/license' />
+ <xsl:if test='/document/image[copyright != /document/copyright
+ or license/identifier != /document/license/identifier]'>
+ <xsl:call-template name='image-attribution' />
+ </xsl:if>
<xsl:apply-templates select='/document/source' />
</div>
</body>
</html>
</xsl:template>
+<xsl:include href='layouts/clickytable.xsl' />
+
</xsl:stylesheet>