filter :css_clean_selectors, \
preserve_comments: true, \
preserve_hacks: true
+ snapshot :before_darkmode
+ filter :css_darkmode
write @item.identifier.without_ext + '.css'
end
+compile '/style.scss', rep: :dark do
+ filter :compiled_content, snapshot: :before_darkmode
+ filter :css_darkmode, alternate: true
+ write "/dark.css"
+end
+
compile '/gpg/*' do
filter :wkd_export_armor
write "/pubring/#{@item.identifier.components.last}.asc"
@include usecolours($border-color: foreground);
@at-root { @supports (outline-style: auto) { & {
@include usecolour(outline, focusring, auto);
- border-color: transparent;
+ border-color: transparent !important;
}}}
}
}
}
// page-specific dark mode styles
-@media (min-width: 35em) {
+@media (prefers-color-scheme: dark) and (min-width: 35em) {
#page_weblog_responsive_tables {
@each $tN in t6 t7 t8 {
##{$tN}>tbody>tr.#{$tN}-split {
- @include usecolour_var_(border-bottom, ruledefault);
+ @include usecolour_dark_(border-bottom, ruledefault);
&:nth-of-type(odd) ~ tr:nth-of-type(odd) {
- @include usecolour_var_(background-color, tableshade);
+ @include usecolour_dark_(background-color, tableshade);
}
}
}
<!--
Nick's web site: XHTML output stage
- Copyright © 2018-2021 Nick Bowler
+ Copyright © 2018-2022 Nick Bowler
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
<head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='stylesheet' type='text/css' href='/style.css' />
+ <link rel='alternate stylesheet' type='text/css' href='/dark.css'
+ title='Dark Style' />
<link rel="icon" href="data:," />
<title>
<xsl:variable name='page-title' select='string(/document/title)' />
//
// @include defcolours($bg: white black, $fg: black white);
@mixin defcolours($args...) {
- :root {
- @each $colour, $list in keywords($args) {
- $colourmap: map-merge($colourmap, ($colour: $list)) !global;
- @if length($list) > 1 {
- #{--colour- + $colour}: nth($list, 1);
- }
- }
- }
- @media (prefers-color-scheme: dark) {
- :root {
- @each $colour, $list in keywords($args) {
- @if length($list) > 1 {
- #{--colour- + $colour}: nth($list, 2);
- }
- }
- }
+ @each $colour, $list in keywords($args) {
+ $colourmap: map-merge($colourmap, ($colour: $list)) !global;
}
}
-// For the given previously-defined colour name, returns its value from
-// primary (light) colour scheme.
+// Obtain the colour value for a previously-defined colour name. By
+// default, its primary (light) colour value is returned; this can be
+// changed with the $num keyword parameter.
//
// The $pre and $post keyword arguments may be used to supplement the
// result with additional tokens either before or after the colour
// value, respectively, as might be used for combined properties
// such as border, outline, etc.
-@function getcolour($colour, $pre: (), $post: ()) {
- @return join(append($pre, nth(map-get($colourmap, $colour), 1)), $post);
+@function getcolour($colour, $pre: (), $post: (), $num: 1) {
+ @return join(append($pre, nth(map-get($colourmap, $colour), $num)), $post);
}
-@mixin usecolour_var_($prop, $colour, $pre: (), $post: ()) {
+@mixin usecolour_dark_($prop, $colour, $pre: (), $post: ()) {
@if (length(map-get($colourmap, $colour)) > 1) {
$transprop: map-get($_colourpropmap, $prop);
@if $transprop {
- #{$transprop}: var(--colour- + $colour)
+ #{$transprop}: getcolour($colour, $num: 2)
} @else {
- #{$prop}: join(append($pre, var(--colour- + $colour)), $post);
+ #{$prop}: getcolour($colour, $pre, $post, 2);
}
}
}
// @include usecolour(border, fg, $pre: solid 1px);
@mixin usecolour($prop, $colour, $pre: (), $post: ()) {
#{$prop}: getcolour($colour, $pre, $post);
- @at-root & { @include usecolour_var_($prop, $colour, $pre, $post); }
+ @at-root {
+ @media (prefers-color-scheme: dark) {
+ & { @include usecolour_dark_($prop, $colour, $pre, $post); }
+ }
+ }
}
// Convenience helper to assign multiple colour properties at once, for
@each $prop, $colour in keywords($args) {
#{$prop}: getcolour($colour);
}
- @at-root & {
- @each $prop, $colour in keywords($args) {
- @include usecolour_var_($prop, $colour);
+ @at-root {
+ @media (prefers-color-scheme: dark) {
+ & {
+ @each $prop, $colour in keywords($args) {
+ @include usecolour_dark_($prop, $colour);
+ }
+ }
}
}
}
--- /dev/null
+# Nick's web site: compiled_content filter. Simply calls the compiled_content
+# method on the current item to retrieve the text from a named rep or snapshot.
+#
+# Copyright © 2022 Nick Bowler
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# 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 CompiledContentFilter < Nanoc::Filter
+ identifier :compiled_content
+
+ def run(content, params = {})
+ return @item.compiled_content(params)
+ end
+end
--- /dev/null
+# Nick's web site: css_darkmode filter. Cut out dark-mode media queries
+# from a stylesheet and either re-insert them at the end of the same
+# stylesheet, or produce a separate stylesheet for use as an alternate.
+#
+# Copyright © 2022 Nick Bowler
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# 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 CssDarkModeFilter < Nanoc::Filter
+ identifier :css_darkmode
+
+ require 'crass'
+
+ def filter_whitespace(nodes)
+ return nodes.reject {|x| x[:node] == :whitespace }
+ end
+
+ def is_media_dark_block(x)
+ return false unless x[:node] == :simple_block
+
+ list = filter_whitespace(x[:value])
+ list.length == 3 and
+ list[0][:node] == :ident and
+ list[0][:value] == "prefers-color-scheme" and
+ list[1][:node] == :colon and
+ list[2][:node] == :ident and
+ list[2][:value] == "dark"
+ end
+
+ def is_media_dark(x)
+ return false unless x[:node] == :at_rule and x[:name] == "media"
+
+ x[:prelude].index {|y| is_media_dark_block(y)}
+ end
+
+ # Remove (prefers-color-scheme: dark) conditions from a media query.
+ # If the resulting query is empty, returns the query's block alone.
+ # Otherwise, returns the modified query.
+ def prune_media_dark(x)
+ start_index = 0
+ end_index = 0
+ state = :init
+
+ x[:prelude].each do |y|
+ case state
+ when :init
+ end_index += 1
+ if is_media_dark_block(y)
+ state = :post
+ elsif y[:node] == :ident and y[:value] == "and"
+ state = :pre
+ else
+ start_index = end_index
+ state = :init
+ end
+ when :pre
+ end_index += 1
+ if is_media_dark_block(y)
+ state = :post
+ elsif y[:node] == :whitespace
+ state = :pre
+ else
+ start_index = end_index
+ state = :init
+ end
+ when :post
+ if y[:node] == :whitespace
+ end_index += 1
+ state = :post
+ elsif y[:node] == :ident and y[:value] == "and"
+ end_index += 1
+ state = :post
+ else
+ break
+ end
+ end
+ end
+
+ x[:prelude].slice!(start_index..end_index-1)
+ return x[:block] if filter_whitespace(x[:prelude]).empty?
+ x
+ end
+
+ def equiv_query(a, b)
+ return false unless a.class == b.class
+
+ case a
+ when Array
+ a = filter_whitespace(a)
+ b = filter_whitespace(b)
+
+ return false unless a.length == b.length
+ return a.map.with_index{|x,i| equiv_query(x, b[i])}.all?
+ when Hash
+ return false unless a[:node] == b[:node]
+ case a[:node]
+ when :at_rule
+ return equiv_query(a[:prelude], b[:prelude])
+ when :simple_block
+ return equiv_query(a[:value], b[:value])
+ else
+ return (a[:value] == b[:value] and a[:unit] == b[:unit])
+ end
+ end
+ end
+
+ def run(content, params = {})
+ params = {
+ preserve_comments: true,
+ preserve_hacks: true
+ }.merge(params)
+
+ tree = Crass.parse(content, params)
+
+ darknodes = []
+ tree.delete_if { |x| darknodes << x if is_media_dark(x) }
+
+ # Combine consecutive equivalent media queries into a single query
+ darknodes = darknodes.slice_when{|a,b| !equiv_query(a, b)}.each.map \
+ do |x|
+ combined = x[0].merge({block: x.map{|x| x[:block]}.flatten})
+ end
+
+ # In alternate mode, remove prefers-color-scheme queries.
+ if params[:alternate]
+ darknodes.map!{|x| prune_media_dark(x)}
+ end
+
+ darkcss = ""
+ darknodes.each do |x|
+ darkcss += "#{Crass::Parser.stringify(x).rstrip}\n"
+ end
+ darkcss.sub!(/^\n*/, "")
+
+ if params[:alternate]
+ prologue = tree.take_while do |x|
+ x[:node] == :comment or x[:node] == :whitespace
+ end
+
+ "#{Crass::Parser.stringify(prologue).rstrip}\n\n#{darkcss}"
+ else
+ output = "#{Crass::Parser.stringify(tree).rstrip}\n"
+ output += "\n#{darkcss}" unless darkcss.empty?
+ output
+ end
+ end
+end