Module: Dommy::Internal::CSS::Supports

Defined in:
lib/dommy/internal/css/supports.rb

Overview

Evaluator for an @supports . Dommy has no feature-detection database, so a feature query (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

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).

Returns:

  • (Boolean)


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.)

Returns:

  • (Boolean)


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

= not | [and ]* | [or ]*



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

= ( ) | ( ) | selector()



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

Returns:

  • (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

Returns:

  • (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".

Returns:

  • (Boolean)


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