Work around Nokogiri pretty-print issues.
authorNick Bowler <nbowler@draconx.ca>
Wed, 9 Jan 2019 01:23:36 +0000 (20:23 -0500)
committerNick Bowler <nbowler@draconx.ca>
Wed, 9 Jan 2019 01:28:54 +0000 (20:28 -0500)
In general, pretty-printing XHTML will change its meaning because
whitespace added or removed can affect the results.  Instead of using
the xsl:strip-space functionality which is quite limited, use a normal
template to remove unwanted whitespace-only text nodes which can be
manually tweaked as required.

Then we explicitly add dummy nodes to prevent Nokogiri from reindenting
problematic cases, and finally clean those out just before publishing
(after all XSLT processing is finished).

Rules
layouts/default.xsl
layouts/functions.xsl [new file with mode: 0644]
lib/removewj.rb [new file with mode: 0644]

diff --git a/Rules b/Rules
index 06f3a110b041d108383e37d6c4621b356da69761..e81b5e2dc72afb004fbfe88459feddcaebc4f0a7 100644 (file)
--- a/Rules
+++ b/Rules
@@ -36,6 +36,7 @@ compile '/**/*.md' do
     layout '/default.xml'
     layout '/default.xsl'
     filter :relativize_paths, type: :xml
+    filter :remove_wj
     write to_xhtml
 end
 
@@ -45,6 +46,7 @@ compile '/license/cc*.sgml' do
     layout '/default.xml'
     layout '/default.xsl'
     filter :relativize_paths, type: :xml
+    filter :remove_wj
     write to_xhtml
 end
 
@@ -53,6 +55,7 @@ compile '/license/cc*.xhtml' do
     layout '/default.xml'
     layout '/default.xsl'
     filter :relativize_paths, type: :xml
+    filter :remove_wj
     write to_xhtml
 end
 
index 2af8dc264ef345e2b3c17bac443a8c30c79eec56..d7381d5dc5a3ace18fdaa400b2a45ebae917e844 100644 (file)
@@ -1,8 +1,8 @@
 <?xml version='1.0' encoding='UTF-8' ?>
 <!--
-  Nick's web site: Final XHTML output stage
+  Nick's web site: XHTML output stage
 
-  Copyright © 2018 Nick Bowler
+  Copyright © 2018-2019 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
   extension-element-prefixes='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:strip-space elements='*' />
-<xsl:preserve-space elements='xhtml:p' />
 
 <xsl:param name='source-uri'
   select='"//git.draconx.ca/gitweb/homepage.git/blob/"' />
   <xsl:copy><xsl:apply-templates 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-
+  printed.  This works by adding arbitrary whitespace to that element, and
+  then all of its children are eligible to be pretty-printed.
+
+  If an element has any text nodes at all, then it is not pretty-printed and
+  neither are any of its descendents.
+
+  Adding arbitrary whitespace to <pre> is bad, so we inject zero-width non-
+  breaking spaces to prevent this.  This will render fine but the spaces
+  should be removed before final output to avoid problems with copy+paste.
+-->
+<xsl:template match='xhtml:pre'>
+  <xsl:copy>
+    <xsl:apply-templates select='node()|@*' />
+    <xsl:text>&#x2060;</xsl:text>
+  </xsl:copy>
+</xsl:template>
+
+<!--
+  Likewise, adding spaces between consecutive span-level elements where
+  none existed before won't go over well.
+-->
+<xsl:template name='glue-preceding-span'>
+  <xsl:if test='f:element-is-span(preceding-sibling::node()[1])'>
+    <xsl:text>&#x2060;</xsl:text>
+  </xsl:if>
+</xsl:template>
+
+<xsl:template match='*[f:element-is-span()]'>
+  <xsl:call-template name='glue-preceding-span' />
+  <xsl:copy>
+    <xsl:apply-templates select='node()|@*' />
+    <xsl:text>&#x2060;</xsl:text> <!-- avoid breaking within a span element -->
+  </xsl:copy>
+</xsl:template>
+
+<!--
+  Manually strip whitespace-only text nodes so the pretty printer can do its
+  thing on remaining elements.
+-->
+<xsl:template match='text()[normalize-space(.) = ""]'>
+  <xsl:choose>
+    <!-- preserve anything according to xml:space -->
+    <xsl:when test='ancestor::*[@xml:space][1][@xml:space="preserve"]'>
+      <xsl:copy />
+    </xsl:when>
+    <!-- preserve anything under <pre> -->
+    <xsl:when test='ancestor::xhtml:pre'><xsl:copy /></xsl:when>
+  </xsl:choose>
+</xsl:template>
+
 <!-- Add rel attributes to external links -->
 <xsl:template match='xhtml:a[starts-with(@href,"http://")
                           or starts-with(@href,"https://")
diff --git a/layouts/functions.xsl b/layouts/functions.xsl
new file mode 100644 (file)
index 0000000..cdfb482
--- /dev/null
@@ -0,0 +1,70 @@
+<!--
+  Nick's web site: XSLT helper functions.
+
+  Copyright © 2019 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
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <https://www.gnu.org/licenses/>
+-->
+<xsl:stylesheet version='1.0'
+  xmlns:xhtml='http://www.w3.org/1999/xhtml'
+  xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
+  xmlns:func='http://exslt.org/functions'
+  xmlns:f='http://draconx.ca/my-functions'
+  extension-element-prefixes='func f'>
+
+<!-- Returns true iff the given element is an XHTML span-level element -->
+<func:function name='f:element-is-span'>
+  <xsl:param name='node' select='.' />
+  <func:result select='$node/self::xhtml:a
+                    or $node/self::xhtml:abbr
+                    or $node/self::xhtml:acronym
+                    or $node/self::xhtml:b
+                    or $node/self::xhtml:bdo
+                    or $node/self::xhtml:big
+                    or $node/self::xhtml:br
+                    or $node/self::xhtml:button
+                    or $node/self::xhtml:cite
+                    or $node/self::xhtml:code
+                    or $node/self::xhtml:dfn
+                    or $node/self::xhtml:em
+                    or $node/self::xhtml:i
+                    or $node/self::xhtml:img
+                    or $node/self::xhtml:input
+                    or $node/self::xhtml:kbd
+                    or $node/self::xhtml:label
+                    or $node/self::xhtml:map
+                    or $node/self::xhtml:object
+                    or $node/self::xhtml:q
+                    or $node/self::xhtml:samp
+                    or $node/self::xhtml:script
+                    or $node/self::xhtml:select
+                    or $node/self::xhtml:small
+                    or $node/self::xhtml:span
+                    or $node/self::xhtml:strong
+                    or $node/self::xhtml:sub
+                    or $node/self::xhtml:sup
+                    or $node/self::xhtml:textarea
+                    or $node/self::xhtml:time
+                    or $node/self::xhtml:tt
+                    or $node/self::xhtml:var' />
+</func:function>
+
+<!-- Returns true iff the node is a nonempty text node or a span element -->
+<func:function name='f:node-is-span'>
+  <xsl:param name='node' select='.' />
+  <func:result test='( $node/self::text() and normalize-text($node) )
+                     or f:element-is-span($node)' />
+</func:function>
+
+</xsl:stylesheet>
diff --git a/lib/removewj.rb b/lib/removewj.rb
new file mode 100644 (file)
index 0000000..18264dd
--- /dev/null
@@ -0,0 +1,25 @@
+# Nick's web site: remove_wj filter.  Delete all zero-width word joiners
+# which were added during XSLT processing.
+#
+# Copyright © 2019 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+class RemoveWJFilter < Nanoc::Filter
+    identifier :remove_wj
+
+    def run(content, params = {})
+        return content.delete "\u2060"
+    end
+end