Let's start a blog!
authorNick Bowler <nbowler@draconx.ca>
Mon, 6 Jul 2020 03:11:40 +0000 (23:11 -0400)
committerNick Bowler <nbowler@draconx.ca>
Mon, 6 Jul 2020 03:28:32 +0000 (23:28 -0400)
Since I wrote this diatribe on fancy styles for HTML tables maybe it's
about time I actually went ahead and pushed it to the actual website.

12 files changed:
.gitattributes
Rules
content/index.md
content/style.scss
content/weblog.md [new file with mode: 0644]
content/weblog/responsive-tables.md [new file with mode: 0644]
layouts/default.xml
layouts/default.xsl
layouts/functions.xsl
lib/helpers.rb
lib/scss-var.rb [new file with mode: 0644]
tools/weblog-update.rb [new file with mode: 0755]

index dc3671aa42b0b5a5e799f5a72cf542e1bc5f5047..ee2a7d79c9158d1d6183df471fda186610fed75e 100644 (file)
@@ -1 +1,2 @@
 * annex.backend=SHA512
+content/weblog/**/*.md autopublish
diff --git a/Rules b/Rules
index f3f1f0a197c6f67c3dec7d13a593beef126cf1da..a881607f9b3211cd296780c97a8bdf4678883a61 100644 (file)
--- a/Rules
+++ b/Rules
@@ -36,6 +36,17 @@ preprocess do
             item[:gitrev] = commit if item_source(item)
         end
     end
+
+    @items.find_all('/weblog/*.md').each do |item|
+        item[:kind] ||= 'article'
+    end
+
+    @items.each do |item|
+        item[:created_at] ||=
+            item[:published] || File.stat(item.raw_filename).mtime
+        item[:updated_at] ||=
+            item[:updated] || File.stat(item.raw_filename).mtime
+    end
 end
 
 postprocess do
@@ -165,8 +176,8 @@ compile '/**/*.scss' do
 end
 
 compile '/**/*' do
-  filter :copybin if @item.binary?
-  write @item.identifier.to_s
+    filter :copybin if @item.binary?
+    write @item.identifier.to_s
 end
 
 layout '/**/*.xsl', :xsl
index bd0fca4c52f8fdc33a30797bf0a866da0a328df0..8020140bd8b5df6b2a410d5552bc1140b56a7d7e 100644 (file)
@@ -1,15 +1,18 @@
 ---
 title: The Citrine Citadel
-copyright: 2019 Nick Bowler
+copyright: 2020 Nick Bowler
 license: cc-by-nd-4.0
 ---
 
+[weblog]: <%= item_uri(@items["/weblog.md"]) %>
+[projects]: <%= item_uri(@items["/projects.md"]) %>
+[gitweb]: //git.draconx.ca
+
 Hello traveller, and welcome to this remote isle of the World Wide Web!
 We don't receive many visitors to our fort, but why not sit back and enjoy
-a pint at the tavern while you are here?  The locals are friendly!  You
-may also visit the [workshop](<%= item_uri(@items["/projects.md"]) %>).
-There you will find the local [git server](//git.draconx.ca) which you
-could find interesting.
+a pint at the [tavern][weblog] while you are here?  The locals are friendly!
+You may also visit the [workshop][projects].  There you will find the local
+[git server][gitweb] which you could find interesting.
 
 In time more facilities may open to visitors.
 
index b0c042cdc6f5f3a44f784de6e016dda0ead212f8..f71a6a37846b1f4bc48a40a1aa25f2b6e6222178 100644 (file)
@@ -50,6 +50,7 @@ a:active { color: $linkactivecolour; }
 
 h1 { @include header_size(60em, 2em); }
 h2 { @include header_size(60em, 1.5em); }
+h5 { @include header_size(60em, 1em); }
 
 p>img { max-width: 40em; width: 100%; height: auto; }
 
@@ -65,7 +66,7 @@ p, table, div, ul, ol, dl, hr {
     margin: 0;
 }
 
-p, table, body>div { margin: 1em 0; }
+p, table, body>div, h5 { margin: 1em 0; }
 
 li { margin: 0 0 0 2em; }
 dd { margin: 0 0 0 1em; }
@@ -156,14 +157,13 @@ table.cc {
 }
 
 // Site footer rules
-#footer {
+#footer, #article-info {
     text-align: center;
     max-width: 44em;
     padding: 0 3em;
     margin: 0;
 
     p {
-        color: $annotationcolour;
         display: inline-block;
         font-size: 0.8em;
         max-width: 100%;
@@ -171,6 +171,9 @@ table.cc {
     }
 }
 
+#footer p { color: $annotationcolour; }
+#article-info p { font-style: italic; }
+
 // "unordered" lists with explicit ordering in content
 ul.ordered > {
     li { list-style: none; }
diff --git a/content/weblog.md b/content/weblog.md
new file mode 100644 (file)
index 0000000..08bb3c8
--- /dev/null
@@ -0,0 +1,18 @@
+---
+title: Tavern
+copyright: 2020 Nick Bowler
+license: cc-by-nd-4.0
+---
+
+The bartender greets you heartily as you enter the bustling tavern.  With a
+delicious pint in hand, you can overhear discussions taking place on a variety
+of topics.
+
+<% sorted_articles.each do |item|
+  next unless item[:title]
+%>
+* <span><%= if item[:published]
+  then attribute_to_time(item[:published]).strftime("%Y-%m-%d")
+  else '<small>\[unpublished\]</small>'
+  end %>:&#xa0;</span>[<%= item[:title] %>](<%= item_uri(item) %>)
+<% end %> {: class="ordered"}
diff --git a/content/weblog/responsive-tables.md b/content/weblog/responsive-tables.md
new file mode 100644 (file)
index 0000000..2063484
--- /dev/null
@@ -0,0 +1,442 @@
+---
+title: Adventure in Responsive WWW Tables
+copyright: 2020 Nick Bowler
+license: cc-by-sa-4.0
+published: 2020-07-05T23:25:02-0400
+---
+
+*[CSS]: Cascading Style Sheets
+*[HTML]: HyperText Markup Language
+
+A quest to improve table display on the World Wide Web, using modern
+technologies that degrade gracefully.
+
+Sometimes, when presenting tables, better use of screen (or paper) space is
+achieved by displaying table rows in two (or more) side-by-side vertical
+columns.  I would generally use this approach whenever rows can be reasonably
+formatted to fit in less than half the page width and the table is sufficiently
+long.
+
+For an example, compare and contrast [table 1](#t1) (a straightforward HTML
+table) with [table 2](#t2) (exactly the same information, split into two
+sets of columns).
+
+   ID | Name      | Age
+   ---|-----------|----
+    1 | Barbara   | 34
+    2 | Charles   | 42
+    3 | David     | 53
+    4 | Elizabeth | 32
+    5 | James     | 33
+    6 | Jennifer  | 38
+    7 | Jessica   | 37
+    8 | John      | 31
+    9 | Joseph    | 38
+   10 | Karen     | 57
+   {:#t1 caption="<%= counter(:table) %>:
+    An inefficient use of available space"}
+
+   ID | Name      | Age | ID | Name     | Age
+   ---|-----------|-----|----|----------|----
+    1 | Barbara   | 34  |  6 | Jennifer | 38
+    2 | Charles   | 42  |  7 | Jessica  | 37
+    3 | David     | 53  |  8 | John     | 31
+    4 | Elizabeth | 32  |  9 | Joseph   | 38
+    5 | James     | 33  | 10 | Karen    | 57
+   {:#t2 caption="<%= counter(:table) %>:
+    Same information, more compact presentation"}
+
+This works, but there is a significant problem: the presentation of [table
+2](#t2) into two sets of columns is hardcoded in the HTML markup.  Aside from a
+general aversion to mixing content with style, there is a practical problem: on
+a narrow display the table could easily be too wide.  If columns end up off the
+screen this would be worse than [table 1](#t1) which probably fits nicely.
+
+What we really want is for the user agent to automatically select the most
+appropriate presentation for display---a so-called responsive layout.
+
+# Enter CSS Grid
+
+[cssgrid]: //www.w3.org/TR/css-grid-1/
+
+The [CSS grid module][cssgrid] enables CSS rules to define a table-like
+presentation, with quite a lot of flexibility.  Unfortunately CSS grids
+have a number of major practical problems and limitations.  We will discuss
+and overcome some of these limitations to achieve a responsive layout for
+our table with a high degree of compatibility.
+
+The first and most obvious problem is that not all browsers support CSS grids.
+Whatever solution we implement must, to the greatest extent possible, work in
+all browsers.  This means graceful degradation: if the grid does not work for
+any reason we want to get a readable table, ideally one like [table 1](#t1).
+
+Even with the latest and greatest browser, there is no guarantee the styles
+actually work because they might not even load.  Firefox still lets users
+disable styles at the touch of a button.  Our table must be readable in
+these scenarios.
+
+With a CSS grid, the _grid items_ are all the child elements of the _grid
+container_, which is an element styled with `display: grid`.  In particular,
+descendent elements that are not direct children of the grid container do not
+participate in the grid layout.  While the CSS `display: contents` style exists
+to help overcome this limitation, it turns out to not be very useful: among
+browsers that support grids, not all support `display: contents`.  Thus we
+cannot expect grid layouts to work if we depend on `display: contents`.
+
+If you search online for laying out tabular data with CSS grid, you will find
+many examples that flatten all the table cells into a linear sequence of
+elements within a big container `div` or similar.  This satisfies the markup
+constraints but is, quite frankly, terrible.  The structure of the data has
+been moved from the markup into the stylesheet.  The table is meaningless if
+the stylesheet is not working for any reason.  Whatever happened to separation
+of content and style?
+
+To achieve graceful degradation, we must start with markup that looks very
+similar to the markup of [table 1](#t1)---a straightforward HTML table, and
+only apply styles on top of that.
+
+## Rows as grid items
+
+The markup constraints introduce an immediate practical problem: the actual
+table cells cannot be the primary grid items for our responsive layout,
+because we need to arrange items row-by-row.  So instead, our table rows will
+be the grid items.  Therefore the grid containers must be `thead` and `tbody`.
+We will not bother with `tfoot` at this time, but it should be pretty similar
+to `thead`.
+
+Using a media query, a simple 2-column grid can be configured which fills the
+available space only if there is sufficient width available.
+
+#### <%= counter(:listing) %>: Markup for [table 3](#t3)
+<generate-xhtml-listing target='t3' />
+
+#### <%= counter(:listing) %>: Style for [table 3](#t3)
+<style type='text/css' generate-listing>
+@supports (display: grid) {
+  #t3>* {
+    grid-column-gap: 0.5ex;
+    grid-template-columns: 1fr 1fr;
+  }
+
+  @media (min-width: 35em) {
+    #t3>thead, #t3>tbody { display: grid; }
+  }
+}
+</style>
+
+<table id='t3'>
+  <caption><%= counter(:table) %>: First layout attempt with grid</caption>
+  <thead>
+    <tr><th>Header</th></tr>
+  </thead>
+  <tbody>
+    <tr><td>Row 1</td></tr>
+    <tr><td>Row 2</td></tr>
+    <tr><td>Row 3</td></tr>
+    <tr><td>Row 4</td></tr>
+    <tr><td>Row 5</td></tr>
+    <tr><td>Row 6</td></tr>
+  </tbody>
+</table>
+
+[Table 3](#t3) shows the basic structure working but there are several
+obvious deficiencies.  The most glaring is the lack of a header on the
+right-hand grid column.  Some of the table styling (odd/even row shading
+in particular) is busted.  And the rows are not in the desired location:
+row 2 should be beneath row 1, not beside it.
+
+## Duplicating the header
+
+There is unfortunately no way to duplicate the header in a stylesheet, so we
+have no choice but to duplicate the header row in the table markup itself.  We
+can hide it with an inline style attribute and reveal it when the grid is
+enabled in the media query.  An inline `display: none` is widely supported to
+hide this row normally.
+
+#### <%= counter(:listing) %>: Markup for [table 4](#t4)
+<generate-xhtml-listing target='t4' />
+
+#### <%= counter(:listing) %>: Style for [table 4](#t4)
+<style type='text/css' generate-listing>
+@supports (display: grid) {
+  #t4>* {
+    grid-column-gap: 0.5ex;
+    grid-template-columns: 1fr 1fr;
+  }
+
+  @media (min-width: 35em) {
+    #t4>*>tr { display: initial !important; }
+    #t4>thead, #t4>tbody { display: grid; }
+  }
+}
+</style>
+
+<table id='t4'>
+  <caption><%= counter(:table) %>: Duplicated header on grid</caption>
+  <thead>
+    <tr><th>Header</th></tr>
+    <tr style='display: none;'><th>Header (duplicated)</th></tr>
+  </thead>
+  <tbody>
+    <tr><td>Row 1</td></tr>
+    <tr><td>Row 2</td></tr>
+    <tr><td>Row 3</td></tr>
+    <tr><td>Row 4</td></tr>
+    <tr><td>Row 5</td></tr>
+    <tr><td>Row 6</td></tr>
+  </tbody>
+</table>
+
+This is not perfect: the duplicate header will appear if styles are disabled
+or if the browser does not support CSS at all, which includes all text-mode
+browsers that I am aware of.  But it is a fairly minor annoyance and the
+situation can be improved somewhat with some hacks that we might explore in
+another adventure.
+
+## Fixing the row placement
+
+Our grid currently consists of two grid columns and a dynamic number of
+grid rows, and the automatic grid layout fills each row before creating
+a new one.  We can place the rows where they need to go by styling the
+first half of the rows differently from the last half.  The first half
+can be forced to the first column, and the remainder will auto-place
+to the correct locations when using `grid-auto-flow: dense`.
+
+Perhaps the easiest way to do this is to markup the halfway point with a
+class, so that CSS selectors can reference this row without hardcoding the
+number of rows in the table.
+
+#### <%= counter(:listing) %>: Markup for [table 5](#t5)
+<generate-xhtml-listing target='t5' />
+
+#### <%= counter(:listing) %>: Style for [table 5](#t5)
+<style type='text/css' generate-listing>
+@supports (display: grid) {
+  #t5>* {
+    grid-column-gap: 0.5ex;
+    grid-template-columns: 1fr 1fr;
+    grid-auto-flow: dense;
+  }
+
+  #t5>tbody>tr { grid-column-start: 1; }
+  #t5>tbody>tr.t5-split ~ tr { grid-column-start: auto; }
+
+  @media (min-width: 35em) {
+    #t5>*>tr { display: initial !important; }
+    #t5>thead, #t5>tbody { display: grid; }
+  }
+}
+</style>
+
+<table id='t5'>
+  <caption><%= counter(:table) %>: Correct row placement on grid</caption>
+  <thead>
+    <tr><th>Header</th></tr>
+    <tr style='display: none;'><th>Header (duplicated)</th></tr>
+  </thead>
+  <tbody>
+    <tr><td>Row 1</td></tr>
+    <tr><td>Row 2</td></tr>
+    <tr class='t5-split'><td>Row 3</td></tr>
+    <tr><td>Row 4</td></tr>
+    <tr><td>Row 5</td></tr>
+    <tr><td>Row 6</td></tr>
+  </tbody>
+</table>
+
+## Fixing the row styling
+
+Depending on the style used, [table 5](#t5) might be good enough.  But here,
+the odd/even row shading is messed up because the split was not performed
+on an even-numbered row, and there is no bottom border on the first grid
+column.  Now that the the row positioning is correct, both of these issues
+are pretty easy to fix in the stylesheet.
+
+#### <%= counter(:listing) %>: Markup for [table 6](#t6)
+<generate-xhtml-listing target='t6' />
+
+#### <%= counter(:listing) %>: Style for [table 6](#t6)
+<style type='text/css' generate-listing>
+@supports (display: grid) {
+  #t6>* {
+    grid-column-gap: 0.5ex;
+    grid-template-columns: 1fr 1fr;
+    grid-auto-flow: dense;
+  }
+
+  #t6>tbody>tr { grid-column-start: 1; }
+  #t6>tbody>tr.t6-split ~ tr { grid-column-start: auto; }
+
+  @media (min-width: 35em) {
+    #t6>*>tr { display: initial !important; }
+    #t6>thead, #t6>tbody { display: grid; }
+
+    #t6>tbody>tr.t6-split { border-bottom: 1px solid <%=
+      scss_get_var(:ruledefaultcolour) %>; }
+    #t6>tbody>tr.t6-split:nth-of-type(odd) ~ tr:nth-of-type(even) {
+      background-color: initial;
+    }
+    #t6>tbody>tr.t6-split:nth-of-type(odd) ~ tr:nth-of-type(odd) {
+      background-color: <%= scss_get_var(:tableshadecolour) %>;
+    }
+  }
+}
+</style>
+
+<table id='t6'>
+  <caption><%= counter(:table) %>: Correct row styling on grid</caption>
+  <thead>
+    <tr><th>Header</th></tr>
+    <tr style='display: none;'><th>Header (duplicated)</th></tr>
+  </thead>
+  <tbody>
+    <tr><td>Row 1</td></tr>
+    <tr><td>Row 2</td></tr>
+    <tr class='t6-split'><td>Row 3</td></tr>
+    <tr><td>Row 4</td></tr>
+    <tr><td>Row 5</td></tr>
+    <tr><td>Row 6</td></tr>
+  </tbody>
+</table>
+
+# Realistic Tables
+
+[Table 6](#t6) is pretty close to what we want, except for the minor detail
+that real tables typically have more than one column.  So let's try applying
+these techniques to [table 1](#t1).
+
+#### <%= counter(:listing) %>: Markup for [table 7](#t7)
+<generate-xhtml-listing target='t7' />
+
+#### <%= counter(:listing) %>: Style for [table 7](#t7)
+<style type='text/css' generate-listing>
+@supports (display: grid) {
+  #t7>* {
+    grid-column-gap: 0.5ex;
+    grid-template-columns: 1fr 1fr;
+    grid-auto-flow: dense;
+  }
+
+  #t7>tbody>tr { grid-column-start: 1; }
+  #t7>tbody>tr.t7-split ~ tr { grid-column-start: auto; }
+
+  @media (min-width: 35em) {
+    #t7>*>tr { display: initial !important; }
+    #t7>thead, #t7>tbody { display: grid; }
+
+    #t7>tbody>tr.t7-split { border-bottom: 1px solid <%=
+      scss_get_var(:ruledefaultcolour) %>; }
+    #t7>tbody>tr.t7-split:nth-of-type(odd) ~ tr:nth-of-type(even) {
+      background-color: initial;
+    }
+    #t7>tbody>tr.t7-split:nth-of-type(odd) ~ tr:nth-of-type(odd) {
+      background-color: <%= scss_get_var(:tableshadecolour) %>;
+    }
+  }
+}
+</style>
+
+<table id="t7">
+  <caption><%= counter(:table) %>: Column misalignment on grid</caption>
+  <thead>
+    <tr> <th>ID</th> <th>Name</th> <th>Age</th> </tr>
+    <tr style='display: none;'> <th>ID</th> <th>Name</th> <th>Age</th> </tr>
+  </thead>
+  <tbody>
+    <tr> <td>1</td> <td>Barbara</td> <td>34</td> </tr>
+    <tr> <td>2</td> <td>Charles</td> <td>42</td> </tr>
+    <tr> <td>3</td> <td>David</td> <td>53</td> </tr>
+    <tr> <td>4</td> <td>Elizabeth</td> <td>32</td> </tr>
+    <tr class='t7-split'> <td>5</td> <td>James</td> <td>33</td> </tr>
+    <tr> <td>6</td> <td>Jennifer</td> <td>38</td> </tr>
+    <tr> <td>7</td> <td>Jessica</td> <td>37</td> </tr>
+    <tr> <td>8</td> <td>John</td> <td>31</td> </tr>
+    <tr> <td>9</td> <td>Joseph</td> <td>38</td> </tr>
+    <tr> <td>10</td> <td>Karen</td> <td>57</td> </tr>
+  </tbody>
+</table>
+
+The fallback for [table 7](#t7) works properly but when the grid columns are
+activated, the alignment of table cells into their respective columns is not
+correct.  This is something that we could potentially solve with CSS subgrids,
+but as with `display: contents` lack of browser support makes their use
+untenable.
+
+Nevertheless, provided that the width of each cell is independent of its
+contents, they will all line up correctly.  There are many ways to do this
+in a stylesheet.  We will do it with more grids.
+
+#### <%= counter(:listing) %>: Markup for [table 8](#t8)
+<generate-xhtml-listing target='t8' />
+
+#### <%= counter(:listing) %>: Style for [table 8](#t8)
+<style type='text/css' generate-listing>
+@supports (display: grid) {
+  #t8>* {
+    grid-column-gap: 0.5ex;
+    grid-template-columns: 1fr 1fr;
+    grid-auto-flow: dense;
+  }
+
+  #t8>tbody>tr { grid-column-start: 1; }
+  #t8>tbody>tr.t8-split ~ tr { grid-column-start: auto; }
+
+  @media (min-width: 35em) {
+    #t8>*>tr { display: grid !important; }
+    #t8>thead, #t8>tbody { display: grid; }
+
+    #t8>tbody>tr.t8-split { border-bottom: 1px solid <%=
+      scss_get_var(:ruledefaultcolour) %>; }
+    #t8>tbody>tr.t8-split:nth-of-type(odd) ~ tr:nth-of-type(even) {
+      background-color: initial;
+    }
+    #t8>tbody>tr.t8-split:nth-of-type(odd) ~ tr:nth-of-type(odd) {
+      background-color: <%= scss_get_var(:tableshadecolour) %>;
+    }
+  }
+
+  #t8>*>tr {
+    grid-template-columns: minmax(3em, 1fr) 5fr minmax(3em, 1fr);
+  }
+}
+</style>
+
+<table id="t8">
+  <caption><%= counter(:table) %>: Fully working responsive table</caption>
+  <thead>
+    <tr> <th>ID</th> <th>Name</th> <th>Age</th> </tr>
+    <tr style='display: none;'> <th>ID</th> <th>Name</th> <th>Age</th> </tr>
+  </thead>
+  <tbody>
+    <tr> <td>1</td> <td>Barbara</td> <td>34</td> </tr>
+    <tr> <td>2</td> <td>Charles</td> <td>42</td> </tr>
+    <tr> <td>3</td> <td>David</td> <td>53</td> </tr>
+    <tr> <td>4</td> <td>Elizabeth</td> <td>32</td> </tr>
+    <tr class='t8-split'> <td>5</td> <td>James</td> <td>33</td> </tr>
+    <tr> <td>6</td> <td>Jennifer</td> <td>38</td> </tr>
+    <tr> <td>7</td> <td>Jessica</td> <td>37</td> </tr>
+    <tr> <td>8</td> <td>John</td> <td>31</td> </tr>
+    <tr> <td>9</td> <td>Joseph</td> <td>38</td> </tr>
+    <tr> <td>10</td> <td>Karen</td> <td>57</td> </tr>
+  </tbody>
+</table>
+
+# Epilogue
+
+Some issues remain, but I think the results in [table 8](#t8) are pretty good.
+
+I don't consider the loss of automatic cell sizing to be a big deal, sizes
+usually need to be tweaked per-table anyway and the use of `fr` units can
+give pretty nice results that scale with available width.
+
+More than two grid columns should be possible but I have not attempted to do
+so.  There might be a lot more fighting with the automatic grid placement.
+
+Depending on the table it may be useful to tweak various alignment properties
+of the grid items.
+
+It seems that Firefox really sucks at printing these grids if they happen to
+cross a page boundary (I did not try printing in other browsers).  Firefox is
+pretty bad at printing regular tables when they do this too, but it is way
+worse with grids.  Using e.g. `@media screen` in the stylesheet can help by
+falling back to an ordinary table for printouts.
index 74c454a42d679b59dd8ea7da5909114204bdc91c..6ae8b307a20599f98db9b3b2491c345d7cc17f36 100644 (file)
@@ -2,7 +2,7 @@
 <!--
   Nick's web site: Intermediate document structure.
 
-  Copyright © 2016-2018 Nick Bowler
+  Copyright © 2016-2020 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
       Time.now.gmtime.strftime "%Y-%m-%d %H:%M UTC"
     %></compiletime>
   </source>
-  <hierarchy><% breadcrumbs_trail().compact.each do |i|
-    if i == @item then next end %>
+  <hierarchy><% breadcrumbs_trail().compact.each do |item|
+    next if item == @item || item[:"breadcrumb-ignore"] %>
     <parent>
       <name><%=
-        if i == @items["/index.*"] then
+        if item == @items["/index.*"] then
           "Entrance"
         else
-          i[:title]
+          item[:title]
         end
       %></name>
-      <uri><%= item_uri(i) %></uri>
+      <uri><%= item_uri(item) %></uri>
     </parent><% end %>
   </hierarchy>
-  <html xmlns="<%= Xmlns['xhtml'] %>">
+<% if @item[:kind] == "article" then
+%>  <article>
+<%= attribute_to_time(@item[:published]).strftime \
+"    <published>%Y-%m-%d</published>\n" if @item[:published]
+%>  </article>
+<% end
+%>  <html xmlns="<%= Xmlns['xhtml'] %>">
 <% if !doc_header then
 %>    <h1><%= @item.fetch(:header, @item[:title]) %></h1>
 <% end %><%= doc_str
index eaeef724f3411cb14ea63a5b6e7593d8fefc0423..f41467bd87e01d6de9a7aa0d594ab074f16ac494 100644 (file)
@@ -2,7 +2,7 @@
 <!--
   Nick's web site: XHTML output stage
 
-  Copyright © 2018-2019 Nick Bowler
+  Copyright © 2018-2020 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
@@ -30,7 +30,8 @@
 
 <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' />
+  doctype-system='http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
+  cdata-section-elements='style' />
 
 <xsl:param name='source-uri'
   select='"//git.draconx.ca/gitweb/homepage.git/blob/"' />
   </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>&#x2060;<code><xsl:value-of select='f:strip-leading(.)' /></code></pre>
+</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>&#x2060;<code>
+    <xsl:value-of select='f:xhtml-listing(//xhtml:*[@id=$target])' />
+  </code></pre>
+</xsl:template>
+
 <xsl:template match='copyright'>
   <p>
     <xsl:text>Copyright © </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 match='/'>
   <html>
     <head>
         </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/@*' />
index 96952b20d1c49c4e912686d63094e85ba616dad9..d9c5748053bbf4a3759b4a572d05f5d90061c12f 100644 (file)
@@ -1,7 +1,7 @@
 <!--
   Nick's web site: XSLT helper functions.
 
-  Copyright © 2019 Nick Bowler
+  Copyright © 2019-2020 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
   </xsl:choose>
 </func:function>
 
+<!--
+  Convert the given node to a string containing an XHTML listing for that node.
+-->
+<func:function name='f:xhtml-listing'>
+  <xsl:param name='nodeset' select='.' />
+  <xsl:variable name='node' select='$nodeset[1]' />
+
+  <func:result>
+    <xsl:choose>
+      <xsl:when test='$node/self::text()'>
+        <!-- text node -->
+        <xsl:value-of select='$node' />
+      </xsl:when>
+      <xsl:when test='$node/self::comment()'>
+        <!-- comment node -->
+        <xsl:value-of select='concat("&lt;!--", $node, "--&gt;")' />
+      </xsl:when>
+      <xsl:when test='$node/self::*'>
+        <!-- element node -->
+        <xsl:value-of select='concat("&lt;", local-name($node))' />
+        <xsl:value-of select='f:xhtml-listing($node/@*)' />
+        <xsl:if test='not($node/node())'>/</xsl:if>
+        <xsl:value-of select='concat("&gt;", f:xhtml-listing($node/node()))' />
+        <xsl:if test='$node/node()'>
+          <xsl:value-of select='concat("&lt;/", local-name($node), "&gt;")' />
+        </xsl:if>
+      </xsl:when>
+      <xsl:when test='$node'>
+        <!-- attribute node -->
+        <xsl:value-of select='concat(" ", local-name($node), "=")' />
+        <xsl:value-of select='concat("&apos;", $node, "&apos;")' />
+      </xsl:when>
+    </xsl:choose>
+    <xsl:for-each select='$nodeset[position()>1]'>
+      <xsl:value-of select='f:xhtml-listing()' />
+    </xsl:for-each>
+  </func:result>
+</func:function>
+
 </xsl:stylesheet>
index acaa61289dd8352c66d6e1273773ef83c78eb893..df48f4c745d72de52464aea6da333e61e97e6df4 100644 (file)
 require 'nokogiri'
 
 use_helper Nanoc::Helpers::Breadcrumbs
+use_helper Nanoc::Helpers::Blogging
 
 Xmlns = {
     'xhtml' => 'http://www.w3.org/1999/xhtml'
 }.freeze
+$counters = {}
 
 def to_xhtml(subpath = "", item = @item)
     if item.identifier =~ '/index.*'
@@ -62,6 +64,13 @@ def item_longdesc(item)
     if p.empty? then nil else p[0].xpath('string(.)') end
 end
 
+def counter(name = :default, item = @item)
+    $counters[item] ||= {}
+    $counters[item][name] ||= 0
+
+    name.to_s.capitalize + " " + ($counters[item][name] += 1).to_s
+end
+
 def human_filesize(size)
     units = ["B", "KiB", "MiB", "GiB"]
 
diff --git a/lib/scss-var.rb b/lib/scss-var.rb
new file mode 100644 (file)
index 0000000..c6cf654
--- /dev/null
@@ -0,0 +1,54 @@
+# Nick's web site: Helper to retrieve global style variables in ruby.
+#
+# Copyright © 2020 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/>.
+
+require 'sass'
+
+class GetSCSSGlobals < Sass::Tree::Visitors::Perform
+    def self.visit(root, name = nil)
+        x = new(nil)
+        x.send(:visit, root)
+        result = x.instance_variable_get(:@globals)
+        if name.nil?
+            return result.freeze
+        else
+            return result[name]
+        end
+    end
+
+    protected
+
+    def initialize(environment)
+        @globals = {}
+        @globals.default_proc = proc { |h, k|
+            if h.key? k.to_s then h[k.to_s] end }
+        super
+    end
+
+    def visit_variable(node)
+        super
+        
+        x = @environment.global_env.var(node.name)
+        if !x.nil?
+            @globals[node.name] = x
+        end
+    end
+end
+
+def scss_get_var(variable, item = @items["/style.scss"])
+    engine = Sass::Engine.for_file(item.raw_filename, { :syntax => :scss })
+    return GetSCSSGlobals.visit(engine.to_tree, variable)
+end
diff --git a/tools/weblog-update.rb b/tools/weblog-update.rb
new file mode 100755 (executable)
index 0000000..f955f24
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/env ruby
+#
+# Nick's web site: Autogenerate timestamps for nanoc items.
+#
+# Copyright © 2020 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/>.
+
+require 'yaml'
+
+updatetime = Time.now
+
+content = STDIN.read
+if content =~ /\A---(--)?\s*$/
+    parts = content.split(/^---(--)?[ \t]*\r?\n?/, 3)
+    metadata = parts[1]
+    content = parts[2]
+end
+
+if metadata
+    meta = YAML.load(metadata)
+    timefmt = "%FT%T%z"
+    updatestr = updatetime.round.strftime(timefmt)
+    autoset = nil
+
+    if meta["published"] and meta["updated"].is_a?(Time)
+        s = meta["updated"].strftime(timefmt)
+        if metadata.sub!(/^updated:\s*#{s}\s*$/, "updated: " + updatestr)
+            autoset = "updated"
+        end
+    elsif meta["published"] and !meta["updated"]
+        metadata += "updated: " + updatestr
+        autoset = "updated"
+    elsif !meta["published"]
+        metadata += "published: " + updatestr
+        autoset = "published"
+    end
+
+    if autoset
+        # Revalidate YAML
+        meta = YAML.load(metadata)
+        unless meta[autoset] == updatetime.round
+            raise "failed to auto-insert " + autoset
+        end
+    end
+
+    puts("---")
+    puts(metadata)
+    puts("---")
+end
+puts(content)