Class: CSS::Selectors::Parser

Inherits:
Object
  • Object
show all
Includes:
TokenCursor
Defined in:
lib/css/selectors/parser.rb

Overview

Parser for CSS Selectors Level 4. Covers compound and complex selectors, the four standard combinators (descendant, child, next- sibling, subsequent-sibling), pseudo-classes / pseudo-elements (with recursive parsing of ‘:not/:is/:where/:has` and AnB parsing of `:nth-*`, including `An+B of S`), attribute selectors with case- insensitive `i` / `s` flags, the `&` nesting selector, and namespace prefixes (`*|name`, `|name`; a declared prefix is rejected — there is no `@namespace` support).

Out of scope: the column combinator ‘||`, and forgiving vs strict selector list distinctions.

Constant Summary collapse

SELECTOR_LIST_PSEUDOS =
%w[is where not matches].freeze
ANB_PSEUDOS =
%w[nth-child nth-last-child nth-of-type nth-last-of-type].freeze
KNOWN_PSEUDO_ELEMENTS =

Per the Selectors grammar (and every browser’s querySelector), a known pseudo-element is a valid selector that simply matches no element, while an unknown one (‘::example`) is a syntax error. Vendor-prefixed `::-webkit-…` are accepted leniently since they match nothing.

%w[
  before after first-line first-letter
  marker placeholder selection backdrop file-selector-button
  target-text grammar-error spelling-error highlight
  cue cue-region part slotted details-content
  view-transition view-transition-group view-transition-image-pair
  view-transition-old view-transition-new
].to_set.freeze
ATTR_MATCHERS =
{
  '~' => :includes,
  '|' => :dash,
  '^' => :prefix,
  '$' => :suffix,
  '*' => :substring
}.freeze
BRACKET_TYPE_FOR_OPEN =
BRACKET_OPEN_CHAR.invert.freeze
BRACKET_CLOSE_TYPE_FOR_OPEN =
BRACKET_OPEN_CHAR.to_h {|type, ch| [ch, BRACKET_CLOSE_TYPE.fetch(type)] }.freeze

Constants included from TokenCursor

TokenCursor::EOF_TOKEN

Class Method Summary collapse

Instance Method Summary collapse

Methods included from TokenCursor

#consume, #eof?, #init_cursor, #parse_error!, #peek, #peek_token, #skip_whitespace

Constructor Details

#initialize(tokens) ⇒ Parser

Returns a new instance of Parser.



98
99
100
# File 'lib/css/selectors/parser.rb', line 98

def initialize(tokens)
  init_cursor(tokens)
end

Class Method Details

.parse_nesting_selector_list(input) ⇒ Object

Like ‘parse_selector_list`, but each complex selector may begin with a combinator (`>` / `+` / `~`), synthesising a leading `&`. Used for CSS Nesting nested rules, whose preludes follow the `<relative-selector-list>` grammar.



54
55
56
# File 'lib/css/selectors/parser.rb', line 54

def parse_nesting_selector_list(input)
  new(tokens_from(input)).parse_nesting_selector_list_complete
end

.parse_selector(input) ⇒ Object



46
47
48
# File 'lib/css/selectors/parser.rb', line 46

def parse_selector(input)
  new(tokens_from(input)).parse_selector_complete
end

.parse_selector_list(input) ⇒ Object



42
43
44
# File 'lib/css/selectors/parser.rb', line 42

def parse_selector_list(input)
  new(tokens_from(input)).parse_selector_list_complete
end

Instance Method Details

#parse_complex_selectorObject



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/css/selectors/parser.rb', line 145

def parse_complex_selector
  skip_whitespace

  compounds   = [parse_compound_selector]
  combinators = []

  loop do
    combo = try_consume_combinator
    break if combo.nil?

    compounds   << parse_compound_selector
    combinators << combo
  end

  ComplexSelector.new(compounds:, combinators:)
end

#parse_nesting_complex_selectorObject

A nested-rule complex selector may start with a combinator, which implies a leading ‘&` (`> .c` → `& > .c`), preserving the `compounds == combinators + 1` invariant.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/css/selectors/parser.rb', line 194

def parse_nesting_complex_selector
  skip_whitespace

  t = peek

  if t.type == :delim && (combo = combinator_for_delim(t.value))
    consume
    skip_whitespace

    rest = parse_complex_selector

    return ComplexSelector.new(
      compounds:   [CompoundSelector.new(components: [NestingSelector.new]), *rest.compounds],
      combinators: [combo, *rest.combinators]
    )
  end

  parse_complex_selector
end

#parse_nesting_selector_listObject



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/css/selectors/parser.rb', line 172

def parse_nesting_selector_list
  skip_whitespace

  parse_error!('empty selector list') if list_terminator?(peek)

  selectors = [parse_nesting_complex_selector]

  loop do
    skip_whitespace
    break unless peek.type == :comma

    consume
    skip_whitespace
    selectors << parse_nesting_complex_selector
  end

  SelectorList.new(selectors:)
end

#parse_nesting_selector_list_completeObject



162
163
164
165
166
167
168
169
170
# File 'lib/css/selectors/parser.rb', line 162

def parse_nesting_selector_list_complete
  list = parse_nesting_selector_list

  skip_whitespace

  parse_error!("trailing tokens after selector list: #{peek.type}") unless peek.type == :eof

  list
end

#parse_selector_completeObject



112
113
114
115
116
117
118
119
120
121
122
# File 'lib/css/selectors/parser.rb', line 112

def parse_selector_complete
  skip_whitespace

  cs = parse_complex_selector

  skip_whitespace

  parse_error!("trailing tokens after selector: #{peek.type}") unless peek.type == :eof

  cs
end

#parse_selector_listObject

A comma-separated list of complex selectors, terminated by EOF or ‘)` (for use inside functional pseudos like `:is(…)`).



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/css/selectors/parser.rb', line 126

def parse_selector_list
  skip_whitespace

  parse_error!('empty selector list') if list_terminator?(peek)

  selectors = [parse_complex_selector]

  loop do
    skip_whitespace
    break unless peek.type == :comma

    consume
    skip_whitespace
    selectors << parse_complex_selector
  end

  SelectorList.new(selectors:)
end

#parse_selector_list_completeObject



102
103
104
105
106
107
108
109
110
# File 'lib/css/selectors/parser.rb', line 102

def parse_selector_list_complete
  list = parse_selector_list

  skip_whitespace

  parse_error!("trailing tokens after selector list: #{peek.type}") unless peek.type == :eof

  list
end