X-Git-Url: https://git.draconx.ca/gitweb/homepage.git/blobdiff_plain/062c731462e3ec513b470308cc2dd475098ce231..19f5d7df33b2deaf3bd2efc68bd3d163c93b194f:/lib/css-darkmode.rb diff --git a/lib/css-darkmode.rb b/lib/css-darkmode.rb new file mode 100644 index 0000000..50662e3 --- /dev/null +++ b/lib/css-darkmode.rb @@ -0,0 +1,158 @@ +# 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 . + +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