]> git.draconx.ca Git - homepage.git/commitdiff
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
 * 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
             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
 end
 
 postprocess do
@@ -165,8 +176,8 @@ compile '/**/*.scss' do
 end
 
 compile '/**/*' 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
 end
 
 layout '/**/*.xsl', :xsl
index bd0fca4c52f8fdc33a30797bf0a866da0a328df0..8020140bd8b5df6b2a410d5552bc1140b56a7d7e 100644 (file)
@@ -1,15 +1,18 @@
 ---
 title: The Citrine Citadel
 ---
 title: The Citrine Citadel
-copyright: 2019 Nick Bowler
+copyright: 2020 Nick Bowler
 license: cc-by-nd-4.0
 ---
 
 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
 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.
 
 
 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); }
 
 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; }
 
 
 p>img { max-width: 40em; width: 100%; height: auto; }
 
@@ -65,7 +66,7 @@ p, table, div, ul, ol, dl, hr {
     margin: 0;
 }
 
     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; }
 
 li { margin: 0 0 0 2em; }
 dd { margin: 0 0 0 1em; }
@@ -156,14 +157,13 @@ table.cc {
 }
 
 // Site footer rules
 }
 
 // Site footer rules
-#footer {
+#footer, #article-info {
     text-align: center;
     max-width: 44em;
     padding: 0 3em;
     margin: 0;
 
     p {
     text-align: center;
     max-width: 44em;
     padding: 0 3em;
     margin: 0;
 
     p {
-        color: $annotationcolour;
         display: inline-block;
         font-size: 0.8em;
         max-width: 100%;
         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; }
 // "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.
 
 <!--
   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
 
   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>
       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><%=
     <parent>
       <name><%=
-        if i == @items["/index.*"] then
+        if item == @items["/index.*"] then
           "Entrance"
         else
           "Entrance"
         else
-          i[:title]
+          item[:title]
         end
       %></name>
         end
       %></name>
-      <uri><%= item_uri(i) %></uri>
+      <uri><%= item_uri(item) %></uri>
     </parent><% end %>
   </hierarchy>
     </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
 <% 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
 
 <!--
   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
 
   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'
 
 <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:param name='source-uri'
   select='"//git.draconx.ca/gitweb/homepage.git/blob/"' />
   </xsl:copy>
 </xsl:template>
 
   </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>
 <xsl:template match='copyright'>
   <p>
     <xsl:text>Copyright © </xsl:text>
   </p>
 </xsl:template>
 
   </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:template match='/'>
   <html>
     <head>
         </xsl:if>
         <xsl:value-of select='$site-title' />
       </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/@*' />
     </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.
 
 <!--
   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
 
   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>
 
   </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>
 </xsl:stylesheet>
index acaa61289dd8352c66d6e1273773ef83c78eb893..df48f4c745d72de52464aea6da333e61e97e6df4 100644 (file)
 require 'nokogiri'
 
 use_helper Nanoc::Helpers::Breadcrumbs
 require 'nokogiri'
 
 use_helper Nanoc::Helpers::Breadcrumbs
+use_helper Nanoc::Helpers::Blogging
 
 Xmlns = {
     'xhtml' => 'http://www.w3.org/1999/xhtml'
 }.freeze
 
 Xmlns = {
     'xhtml' => 'http://www.w3.org/1999/xhtml'
 }.freeze
+$counters = {}
 
 def to_xhtml(subpath = "", item = @item)
     if item.identifier =~ '/index.*'
 
 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
 
     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"]
 
 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)