1 # Nick's web site: css_darkmode filter. Cut out dark-mode media queries
2 # from a stylesheet and either re-insert them at the end of the same
3 # stylesheet, or produce a separate stylesheet for use as an alternate.
5 # Copyright © 2022 Nick Bowler
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <https://www.gnu.org/licenses/>.
20 class CssDarkModeFilter < Nanoc::Filter
21 identifier :css_darkmode
25 def filter_whitespace(nodes)
26 return nodes.reject {|x| x[:node] == :whitespace }
29 def is_media_dark_block(x)
30 return false unless x[:node] == :simple_block
32 list = filter_whitespace(x[:value])
34 list[0][:node] == :ident and
35 list[0][:value] == "prefers-color-scheme" and
36 list[1][:node] == :colon and
37 list[2][:node] == :ident and
38 list[2][:value] == "dark"
42 return false unless x[:node] == :at_rule and x[:name] == "media"
44 x[:prelude].index {|y| is_media_dark_block(y)}
47 # Remove (prefers-color-scheme: dark) conditions from a media query.
48 # If the resulting query is empty, returns the query's block alone.
49 # Otherwise, returns the modified query.
50 def prune_media_dark(x)
55 x[:prelude].each do |y|
59 if is_media_dark_block(y)
61 elsif y[:node] == :ident and y[:value] == "and"
64 start_index = end_index
69 if is_media_dark_block(y)
71 elsif y[:node] == :whitespace
74 start_index = end_index
78 if y[:node] == :whitespace
81 elsif y[:node] == :ident and y[:value] == "and"
90 x[:prelude].slice!(start_index..end_index-1)
91 return x[:block] if filter_whitespace(x[:prelude]).empty?
96 return false unless a.class == b.class
100 a = filter_whitespace(a)
101 b = filter_whitespace(b)
103 return false unless a.length == b.length
104 return a.map.with_index{|x,i| equiv_query(x, b[i])}.all?
106 return false unless a[:node] == b[:node]
109 return equiv_query(a[:prelude], b[:prelude])
111 return equiv_query(a[:value], b[:value])
113 return (a[:value] == b[:value] and a[:unit] == b[:unit])
118 def run(content, params = {})
120 preserve_comments: true,
124 tree = Crass.parse(content, params)
127 tree.delete_if { |x| darknodes << x if is_media_dark(x) }
129 # Combine consecutive equivalent media queries into a single query
130 darknodes = darknodes.slice_when{|a,b| !equiv_query(a, b)}.each.map \
132 combined = x[0].merge({block: x.map{|x| x[:block]}.flatten})
135 # In alternate mode, remove prefers-color-scheme queries.
136 if params[:alternate]
137 darknodes.map!{|x| prune_media_dark(x)}
141 darknodes.each do |x|
142 darkcss += "#{Crass::Parser.stringify(x).rstrip}\n"
144 darkcss.sub!(/^\n*/, "")
146 if params[:alternate]
147 prologue = tree.take_while do |x|
148 x[:node] == :comment or x[:node] == :whitespace
151 "#{Crass::Parser.stringify(prologue).rstrip}\n\n#{darkcss}"
153 output = "#{Crass::Parser.stringify(tree).rstrip}\n"
154 output += "\n#{darkcss}" unless darkcss.empty?