From ec1703ff7346b6d3edeee73f1c6dfa750b7aeed4 Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Sun, 5 Jul 2020 23:11:40 -0400 Subject: [PATCH] Let's start a blog! 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. --- .gitattributes | 1 + Rules | 15 +- content/index.md | 13 +- content/style.scss | 9 +- content/weblog.md | 18 ++ content/weblog/responsive-tables.md | 442 ++++++++++++++++++++++++++++ layouts/default.xml | 20 +- layouts/default.xsl | 55 +++- layouts/functions.xsl | 41 ++- lib/helpers.rb | 9 + lib/scss-var.rb | 54 ++++ tools/weblog-update.rb | 62 ++++ 12 files changed, 719 insertions(+), 20 deletions(-) create mode 100644 content/weblog.md create mode 100644 content/weblog/responsive-tables.md create mode 100644 lib/scss-var.rb create mode 100755 tools/weblog-update.rb diff --git a/.gitattributes b/.gitattributes index dc3671a..ee2a7d7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * annex.backend=SHA512 +content/weblog/**/*.md autopublish diff --git a/Rules b/Rules index f3f1f0a..a881607 100644 --- 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 diff --git a/content/index.md b/content/index.md index bd0fca4..8020140 100644 --- a/content/index.md +++ b/content/index.md @@ -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. diff --git a/content/style.scss b/content/style.scss index b0c042c..f71a6a3 100644 --- a/content/style.scss +++ b/content/style.scss @@ -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 index 0000000..08bb3c8 --- /dev/null +++ b/content/weblog.md @@ -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] +%> +* <%= if item[:published] + then attribute_to_time(item[:published]).strftime("%Y-%m-%d") + else '\[unpublished\]' + end %>: [<%= 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 index 0000000..2063484 --- /dev/null +++ b/content/weblog/responsive-tables.md @@ -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) + + +#### <%= counter(:listing) %>: Style for [table 3](#t3) + + + + + + + + + + + + + + + +
<%= counter(:table) %>: First layout attempt with grid
Header
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
+ +[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) + + +#### <%= counter(:listing) %>: Style for [table 4](#t4) + + + + + + + + + + + + + + + + +
<%= counter(:table) %>: Duplicated header on grid
Header
Header (duplicated)
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
+ +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) + + +#### <%= counter(:listing) %>: Style for [table 5](#t5) + + + + + + + + + + + + + + + + +
<%= counter(:table) %>: Correct row placement on grid
Header
Header (duplicated)
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
+ +## 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) + + +#### <%= counter(:listing) %>: Style for [table 6](#t6) + + + + + + + + + + + + + + + + +
<%= counter(:table) %>: Correct row styling on grid
Header
Header (duplicated)
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
+ +# 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) + + +#### <%= counter(:listing) %>: Style for [table 7](#t7) + + + + + + + + + + + + + + + + + + + + +
<%= counter(:table) %>: Column misalignment on grid
ID Name Age
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
+ +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) + + +#### <%= counter(:listing) %>: Style for [table 8](#t8) + + + + + + + + + + + + + + + + + + + + +
<%= counter(:table) %>: Fully working responsive table
ID Name Age
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
+ +# 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. diff --git a/layouts/default.xml b/layouts/default.xml index 74c454a..6ae8b30 100644 --- a/layouts/default.xml +++ b/layouts/default.xml @@ -2,7 +2,7 @@ + + + + + + + + + + + + +
+
+ + + + +

+    
+  
+
+

Copyright © @@ -196,6 +231,18 @@

+ + + +
+

+ Posted + +

+
+
+
+ @@ -209,6 +256,10 @@ + + + + diff --git a/layouts/functions.xsl b/layouts/functions.xsl index 96952b2..d9c5748 100644 --- a/layouts/functions.xsl +++ b/layouts/functions.xsl @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + + + / + + + + + + + + + + + + + + + + + diff --git a/lib/helpers.rb b/lib/helpers.rb index acaa612..df48f4c 100644 --- a/lib/helpers.rb +++ b/lib/helpers.rb @@ -18,10 +18,12 @@ 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 index 0000000..c6cf654 --- /dev/null +++ b/lib/scss-var.rb @@ -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 . + +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 index 0000000..f955f24 --- /dev/null +++ b/tools/weblog-update.rb @@ -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 . + +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) -- 2.43.0