Module: Dommy::Internal::CSS::Supports
- Defined in:
- lib/dommy/internal/css/supports.rb
Overview
Evaluator for an @supports (prop: value) is
treated optimistically — supported when it is a well-formed declaration
— and selector(s) is supported when s parses as a selector. The
boolean combinators (not / and / or) and grouping compose these.
This matches a browser for the common case (querying a feature that
actually exists); the only divergence is a query about a genuinely
unsupported property, which we lean towards "supported" rather than drop
the block. Anything unparseable evaluates false. Never raises.
Class Method Summary collapse
-
.after_keyword(str, keyword) ⇒ Object
If
strbegins withkeywordfollowed by whitespace, return the remainder; nil otherwise (sonotchisn't read asnot). -
.declaration?(body) ⇒ Boolean
A declaration body is
ident : value(vs a nested condition, which starts with(,not, or a function).. -
.declaration_supported?(body) ⇒ Boolean
Optimistic: a declaration is "supported" when it has a property name and a non-empty value.
-
.evaluate_condition(str) ⇒ Object
= not | [and ]* | [or ]*. -
.evaluate_in_parens(str) ⇒ Object
= ( ) | ( ) | selector( ). -
.function_argument(str, name) ⇒ Object
The argument of
name(...)whenstris exactly that call, else nil. - .match?(condition) ⇒ Boolean
-
.operator_at(str, index) ⇒ Object
"and"/"or" at
indexwhen it stands as a whole word (whitespace before, whitespace or(after).. - .selector_supported?(selector_text) ⇒ Boolean
-
.split_operands(str) ⇒ Object
Split a condition into its top-level operands and the joining operator ("and"/"or"/nil).
-
.supports_declaration?(property, value) ⇒ Boolean
The CSS.supports(property, value) two-argument form: is
property: valuea supported declaration? Optimistic, like a feature query in match? — a non-empty property + value is "supported".
Class Method Details
.after_keyword(str, keyword) ⇒ Object
If str begins with keyword followed by whitespace, return the
remainder; nil otherwise (so notch isn't read as not).
71 72 73 74 75 76 77 |
# File 'lib/dommy/internal/css/supports.rb', line 71 def after_keyword(str, keyword) return nil unless str.length > keyword.length return nil unless str[0, keyword.length].casecmp(keyword).zero? return nil unless str[keyword.length].match?(/\s/) str[keyword.length..].strip end |
.declaration?(body) ⇒ Boolean
A declaration body is ident : value (vs a nested condition, which
starts with (, not, or a function).
138 139 140 |
# File 'lib/dommy/internal/css/supports.rb', line 138 def declaration?(body) body.match?(/\A[-\w]+\s*:/) end |
.declaration_supported?(body) ⇒ Boolean
Optimistic: a declaration is "supported" when it has a property name and a non-empty value. (No feature database to consult.)
144 145 146 147 |
# File 'lib/dommy/internal/css/supports.rb', line 144 def declaration_supported?(body) name, value = body.split(":", 2) !name.to_s.strip.empty? && !value.to_s.strip.empty? end |
.evaluate_condition(str) ⇒ Object
45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/dommy/internal/css/supports.rb', line 45 def evaluate_condition(str) str = str.strip rest = after_keyword(str, "not") return !evaluate_in_parens(rest) if rest operands, operator = split_operands(str) values = operands.map { |operand| evaluate_in_parens(operand) } return values.first if operator.nil? operator == "or" ? values.any? : values.all? end |
.evaluate_in_parens(str) ⇒ Object
58 59 60 61 62 63 64 65 66 67 |
# File 'lib/dommy/internal/css/supports.rb', line 58 def evaluate_in_parens(str) str = str.strip inner = function_argument(str, "selector") return selector_supported?(inner) if inner return false unless str.start_with?("(") && str.end_with?(")") body = str[1...-1].strip declaration?(body) ? declaration_supported?(body) : evaluate_condition(body) end |
.function_argument(str, name) ⇒ Object
The argument of name(...) when str is exactly that call, else nil.
130 131 132 133 134 |
# File 'lib/dommy/internal/css/supports.rb', line 130 def function_argument(str, name) return nil unless str.downcase.start_with?("#{name}(") && str.end_with?(")") str[(name.length + 1)...-1].strip end |
.match?(condition) ⇒ Boolean
20 21 22 23 24 25 26 27 |
# File 'lib/dommy/internal/css/supports.rb', line 20 def match?(condition) condition = condition.to_s.strip return false if condition.empty? evaluate_condition(condition) rescue StandardError false end |
.operator_at(str, index) ⇒ Object
"and"/"or" at index when it stands as a whole word (whitespace
before, whitespace or ( after).
117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/dommy/internal/css/supports.rb', line 117 def operator_at(str, index) return nil unless index.positive? && str[index - 1].match?(/\s/) %w[and or].each do |keyword| next unless str[index, keyword.length]&.casecmp(keyword)&.zero? after = str[index + keyword.length] return keyword if after.nil? || after.match?(/\s/) || after == "(" end nil end |
.selector_supported?(selector_text) ⇒ Boolean
149 150 151 152 153 154 |
# File 'lib/dommy/internal/css/supports.rb', line 149 def selector_supported?(selector_text) SelectorParser.parse!(selector_text) true rescue DOMException::SyntaxError false end |
.split_operands(str) ⇒ Object
Split a condition into its top-level operands and the joining
operator ("and"/"or"/nil). Mixing and and or at one level is
invalid per spec — raise (match? maps it to false).
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/dommy/internal/css/supports.rb', line 82 def split_operands(str) operands = [] operators = [] depth = 0 start = 0 index = 0 length = str.length while index < length char = str[index] if char == "(" depth += 1 index += 1 elsif char == ")" depth -= 1 index += 1 elsif depth.zero? && (keyword = operator_at(str, index)) operands << str[start...index].strip operators << keyword index += keyword.length start = index else index += 1 end end operands << str[start..].strip distinct = operators.uniq raise "mixed @supports combinators" if distinct.length > 1 [operands, distinct.first] end |
.supports_declaration?(property, value) ⇒ Boolean
The CSS.supports(property, value) two-argument form: is
property: value a supported declaration? Optimistic, like a feature
query in match? — a non-empty property + value is "supported".
32 33 34 35 36 37 38 39 40 |
# File 'lib/dommy/internal/css/supports.rb', line 32 def supports_declaration?(property, value) property = property.to_s.strip value = value.to_s.strip return false if property.empty? || value.empty? declaration_supported?("#{property}: #{value}") rescue StandardError false end |