syntax_tree-css 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,519 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module CSS
5
+ # Parses selectors according to https://www.w3.org/TR/selectors-4 from the
6
+ # version dated 7 May 2022.
7
+ class Selectors
8
+ class ParseError < StandardError
9
+ end
10
+
11
+ class MissingTokenError < ParseError
12
+ end
13
+
14
+ # A custom enumerator around the list of tokens. This allows us to save a
15
+ # reference to where we are when we're looking at the stream and rollback
16
+ # to that point if we need to.
17
+ class TokenEnumerator
18
+ class Rollback < StandardError
19
+ end
20
+
21
+ attr_reader :tokens, :index
22
+
23
+ def initialize(tokens)
24
+ @tokens = tokens
25
+ @index = 0
26
+ end
27
+
28
+ def next
29
+ @tokens[@index].tap { @index += 1}
30
+ end
31
+
32
+ def peek
33
+ @tokens[@index]
34
+ end
35
+
36
+ def transaction
37
+ saved = @index
38
+ yield
39
+ rescue Rollback
40
+ @index = saved
41
+ nil
42
+ end
43
+ end
44
+
45
+ AttributeSelector = Struct.new(:wq_name, :matcher, keyword_init: true)
46
+ AttributeSelectorMatcher = Struct.new(:attr_matcher, :token, :modifier, keyword_init: true)
47
+ AttrMatcher = Struct.new(:prefix, keyword_init: true)
48
+ AttrModifier = Struct.new(:value, keyword_init: true)
49
+
50
+ # The class of an element, e.g., .foo
51
+ # https://www.w3.org/TR/selectors-4/#typedef-class-selector
52
+ class ClassSelector < Node
53
+ attr_reader :value
54
+
55
+ def initialize(value:)
56
+ @value = value
57
+ end
58
+
59
+ def accept(visitor)
60
+ visitor.visit_class_selector(self)
61
+ end
62
+
63
+ def child_nodes
64
+ [value]
65
+ end
66
+
67
+ alias deconstruct child_nodes
68
+
69
+ def deconstruct_keys(keys)
70
+ { value: value }
71
+ end
72
+ end
73
+
74
+ Combinator = Struct.new(:value, keyword_init: true)
75
+ ComplexSelector = Struct.new(:left, :combinator, :right, keyword_init: true)
76
+ CompoundSelector = Struct.new(:type, :subclasses, :pseudo_elements, keyword_init: true)
77
+
78
+ # The ID of an element, e.g., #foo
79
+ # https://www.w3.org/TR/selectors-4/#typedef-id-selector
80
+ class IdSelector < Node
81
+ attr_reader :value
82
+
83
+ def initialize(value:)
84
+ @value = value
85
+ end
86
+
87
+ def accept(visitor)
88
+ visitor.visit_id_selector(self)
89
+ end
90
+
91
+ def child_nodes
92
+ [value]
93
+ end
94
+
95
+ alias deconstruct child_nodes
96
+
97
+ def deconstruct_keys(keys)
98
+ { value: value }
99
+ end
100
+ end
101
+
102
+ NsPrefix = Struct.new(:value, keyword_init: true)
103
+
104
+ # A pseudo class function call, like :nth-child.
105
+ class PseudoClassFunction < Node
106
+ attr_reader :name, :arguments
107
+
108
+ def initialize(name:, arguments:)
109
+ @name = name
110
+ @arguments = arguments
111
+ end
112
+
113
+ def accept(visitor)
114
+ visitor.visit_pseudo_class_function(self)
115
+ end
116
+
117
+ def child_nodes
118
+ arguments
119
+ end
120
+
121
+ alias deconstruct child_nodes
122
+
123
+ def deconstruct_keys(keys)
124
+ { name: name, arguments: arguments }
125
+ end
126
+ end
127
+
128
+ # A pseudo class selector, like :hover.
129
+ # https://www.w3.org/TR/selectors-4/#typedef-pseudo-class-selector
130
+ class PseudoClassSelector < Node
131
+ attr_reader :value
132
+
133
+ def initialize(value:)
134
+ @value = value
135
+ end
136
+
137
+ def accept(visitor)
138
+ visitor.visit_pseudo_class_selector(self)
139
+ end
140
+
141
+ def child_nodes
142
+ [value]
143
+ end
144
+
145
+ alias deconstruct child_nodes
146
+
147
+ def deconstruct_keys(keys)
148
+ { value: value }
149
+ end
150
+ end
151
+
152
+ # A pseudo element selector, like ::before.
153
+ # https://www.w3.org/TR/selectors-4/#typedef-pseudo-element-selector
154
+ class PseudoElementSelector < Node
155
+ attr_reader :value
156
+
157
+ def initialize(value:)
158
+ @value = value
159
+ end
160
+
161
+ def accept(visitor)
162
+ visitor.visit_pseudo_element_selector(self)
163
+ end
164
+
165
+ def child_nodes
166
+ [value]
167
+ end
168
+
169
+ alias deconstruct child_nodes
170
+
171
+ def deconstruct_keys(keys)
172
+ { value: value }
173
+ end
174
+ end
175
+
176
+ RelativeSelector = Struct.new(:combinator, :complex_selector, keyword_init: true)
177
+
178
+ # A selector for a specific tag name.
179
+ # https://www.w3.org/TR/selectors-4/#typedef-type-selector
180
+ class TypeSelector < Node
181
+ attr_reader :prefix, :value
182
+
183
+ def initialize(prefix:, value:)
184
+ @prefix = prefix
185
+ @value = value
186
+ end
187
+
188
+ def accept(visitor)
189
+ visitor.visit_type_selector(self)
190
+ end
191
+
192
+ def child_nodes
193
+ [prefix, value]
194
+ end
195
+
196
+ alias deconstruct child_nodes
197
+
198
+ def deconstruct_keys(keys)
199
+ { prefix: prefix, value: value }
200
+ end
201
+ end
202
+
203
+ # The name of an element, e.g., foo
204
+ class WqName < Node
205
+ attr_reader :prefix, :name
206
+
207
+ def initialize(prefix:, name:)
208
+ @prefix = prefix
209
+ @name = name
210
+ end
211
+
212
+ def accept(visitor)
213
+ visitor.visit_wqname(self)
214
+ end
215
+
216
+ def child_nodes
217
+ [prefix, name]
218
+ end
219
+
220
+ alias deconstruct child_nodes
221
+
222
+ def deconstruct_keys(keys)
223
+ { prefix: prefix, name: name }
224
+ end
225
+ end
226
+
227
+ attr_reader :tokens
228
+
229
+ def initialize(tokens)
230
+ @tokens = TokenEnumerator.new(tokens)
231
+ end
232
+
233
+ def parse
234
+ selector_list
235
+ end
236
+
237
+ private
238
+
239
+ #-------------------------------------------------------------------------
240
+ # Parsing methods
241
+ #-------------------------------------------------------------------------
242
+
243
+ # <selector-list> = <complex-selector-list>
244
+ def selector_list
245
+ complex_selector_list
246
+ end
247
+
248
+ # <complex-selector-list> = <complex-selector>#
249
+ def complex_selector_list
250
+ one_or_more { complex_selector }
251
+ end
252
+
253
+ # <compound-selector-list> = <compound-selector>#
254
+ def compound_selector_list
255
+ one_or_more { compound_selector }
256
+ end
257
+
258
+ # <simple-selector-list> = <simple-selector>#
259
+ def simple_selector_list
260
+ one_or_more { simple_selector }
261
+ end
262
+
263
+ # <relative-selector-list> = <relative-selector>#
264
+ def relative_selector_list
265
+ one_or_more { relative_selector }
266
+ end
267
+
268
+ # <complex-selector> = <compound-selector> [ <combinator>? <compound-selector> ]*
269
+ def complex_selector
270
+ left = compound_selector
271
+
272
+ loop do
273
+ if (combinator = maybe { combinator })
274
+ ComplexSelector.new(left: left, combinator: combinator, right: compound_selector)
275
+ elsif (right = maybe { compound_selector })
276
+ ComplexSelector.new(left: left, combinator: nil, right: right)
277
+ else
278
+ break
279
+ end
280
+ end
281
+
282
+ left
283
+ end
284
+
285
+ # <relative-selector> = <combinator>? <complex-selector>
286
+ def relative_selector
287
+ combinator = maybe { combinator }
288
+
289
+ if combinator
290
+ RelativeSelector.new(combinator: combinator, complex_selector: complex_selector)
291
+ else
292
+ complex_selector
293
+ end
294
+ end
295
+
296
+ # <compound-selector> = [ <type-selector>? <subclass-selector>*
297
+ # [ <pseudo-element-selector> <pseudo-class-selector>* ]* ]!
298
+ def compound_selector
299
+ type = maybe { type_selector }
300
+ subclasses = []
301
+
302
+ while (subclass = maybe { subclass_selector })
303
+ subclasses << subclass
304
+ end
305
+
306
+ pseudo_elements = []
307
+ while (pseudo_element = maybe { pseudo_element_selector })
308
+ pseudo_classes = []
309
+
310
+ while (pseudo_class = maybe { pseudo_class_selector })
311
+ pseudo_classes << pseudo_class
312
+ end
313
+
314
+ pseudo_elements << [pseudo_element, pseudo_classes]
315
+ end
316
+
317
+ if type.nil? && subclasses.empty? && pseudo_elements.empty?
318
+ raise MissingTokenError, "Expected compound selector to produce something"
319
+ elsif type && subclasses.empty? && pseudo_elements.empty?
320
+ type
321
+ elsif type.nil? && subclasses.one? && pseudo_elements.empty?
322
+ subclasses.first
323
+ else
324
+ CompoundSelector.new(type: type, subclasses: subclasses, pseudo_elements: pseudo_elements)
325
+ end
326
+ end
327
+
328
+ # <simple-selector> = <type-selector> | <subclass-selector>
329
+ def simple_selector
330
+ options { maybe { type_selector } || maybe { subclass_selector } }
331
+ end
332
+
333
+ # <combinator> = '>' | '+' | '~' | [ '|' '|' ]
334
+ def combinator
335
+ value =
336
+ options do
337
+ maybe { consume(">") } ||
338
+ maybe { consume("+") } ||
339
+ maybe { consume("~") } ||
340
+ maybe { consume("|", "|") }
341
+ end
342
+
343
+ Combinator.new(value: value)
344
+ end
345
+
346
+ # <type-selector> = <wq-name> | <ns-prefix>? '*'
347
+ def type_selector
348
+ selector = maybe { wq_name }
349
+ return TypeSelector.new(prefix: nil, value: selector) if selector
350
+
351
+ prefix = maybe { ns_prefix }
352
+ TypeSelector.new(prefix: prefix, value: consume("*"))
353
+ end
354
+
355
+ # <ns-prefix> = [ <ident-token> | '*' ]? '|'
356
+ def ns_prefix
357
+ value = maybe { consume(IdentToken) } || maybe { consume("*") }
358
+ consume("|")
359
+
360
+ NsPrefix.new(value: value)
361
+ end
362
+
363
+ # <wq-name> = <ns-prefix>? <ident-token>
364
+ def wq_name
365
+ prefix = maybe { ns_prefix }
366
+ name = consume(IdentToken)
367
+
368
+ WqName.new(prefix: prefix, name: name)
369
+ end
370
+
371
+ # <subclass-selector> = <id-selector> | <class-selector> |
372
+ # <attribute-selector> | <pseudo-class-selector>
373
+ def subclass_selector
374
+ options do
375
+ maybe { id_selector } ||
376
+ maybe { class_selector } ||
377
+ maybe { attribute_selector } ||
378
+ maybe { pseudo_class_selector }
379
+ end
380
+ end
381
+
382
+ # <id-selector> = <hash-token>
383
+ def id_selector
384
+ IdSelector.new(value: consume(HashToken))
385
+ end
386
+
387
+ # <class-selector> = '.' <ident-token>
388
+ def class_selector
389
+ consume(".")
390
+ ClassSelector.new(value: consume(IdentToken))
391
+ end
392
+
393
+ # <attribute-selector> = '[' <wq-name> ']' |
394
+ # '[' <wq-name> <attr-matcher> [ <string-token> | <ident-token> ] <attr-modifier>? ']'
395
+ def attribute_selector
396
+ consume(OpenSquareToken)
397
+
398
+ name = wq_name
399
+ matcher =
400
+ maybe do
401
+ AttributeSelectorMatcher.new(
402
+ attr_matcher: attr_matcher,
403
+ token: options { maybe { consume(StringToken) } || maybe { consume(IdentToken) } },
404
+ modifier: maybe { attr_modifier }
405
+ )
406
+ end
407
+
408
+ consume(CloseSquareToken)
409
+ AttributeSelector.new(wq_name: name, matcher: matcher)
410
+ end
411
+
412
+ # <attr-matcher> = [ '~' | '|' | '^' | '$' | '*' ]? '='
413
+ def attr_matcher
414
+ prefix =
415
+ maybe { consume("~") } ||
416
+ maybe { consume("|") } ||
417
+ maybe { consume("^") } ||
418
+ maybe { consume("$") } ||
419
+ maybe { consume("*") }
420
+
421
+ consume("=")
422
+ AttrMatcher.new(prefix: prefix)
423
+ end
424
+
425
+ # <attr-modifier> = i | s
426
+ def attr_modifier
427
+ value = options { maybe { consume("i") } || maybe { consume("s") } }
428
+ AttrModifier.new(value: value)
429
+ end
430
+
431
+ # <pseudo-class-selector> = ':' <ident-token> |
432
+ # ':' <function-token> <any-value> ')'
433
+ def pseudo_class_selector
434
+ consume(ColonToken)
435
+
436
+ case tokens.peek
437
+ in IdentToken
438
+ PseudoClassSelector.new(value: consume(IdentToken))
439
+ in Function
440
+ node = consume(Function)
441
+ function = PseudoClassFunction.new(name: node.name, arguments: node.value)
442
+ PseudoClassSelector.new(value: function)
443
+ else
444
+ raise MissingTokenError, "Expected pseudo class selector to produce something"
445
+ end
446
+ end
447
+
448
+ # <pseudo-element-selector> = ':' <pseudo-class-selector>
449
+ def pseudo_element_selector
450
+ consume(ColonToken)
451
+ PseudoElementSelector.new(value: pseudo_class_selector)
452
+ end
453
+
454
+ #-------------------------------------------------------------------------
455
+ # Helper methods
456
+ #-------------------------------------------------------------------------
457
+
458
+ def consume_whitespace
459
+ loop do
460
+ case tokens.peek
461
+ in CommentToken | WhitespaceToken
462
+ tokens.next
463
+ else
464
+ return
465
+ end
466
+ end
467
+ end
468
+
469
+ def one_or_more
470
+ items = []
471
+
472
+ consume_whitespace
473
+ items << yield
474
+
475
+ loop do
476
+ consume_whitespace
477
+ if maybe { consume(CommaToken) }
478
+ consume_whitespace
479
+ items << yield
480
+ else
481
+ return items
482
+ end
483
+ end
484
+ end
485
+
486
+ def consume(*values)
487
+ result =
488
+ values.map do |value|
489
+ case [value, tokens.peek]
490
+ in [String, DelimToken[value: token_value]] if value == token_value
491
+ tokens.next
492
+ in [Class, token] if token.is_a?(value)
493
+ tokens.next
494
+ in [_, token]
495
+ raise MissingTokenError, "Expected #{value} but got #{token.inspect}"
496
+ end
497
+ end
498
+
499
+ result.size == 1 ? result.first : result
500
+ end
501
+
502
+ def maybe
503
+ tokens.transaction do
504
+ begin
505
+ yield
506
+ rescue MissingTokenError
507
+ raise TokenEnumerator::Rollback
508
+ end
509
+ end
510
+ end
511
+
512
+ def options
513
+ value = yield
514
+ raise MissingTokenError, "Expected one of many to match" if value.nil?
515
+ value
516
+ end
517
+ end
518
+ end
519
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module CSS
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module CSS
5
+ # A visitor that walks through the tree.
6
+ class Visitor
7
+ def visit(node)
8
+ node&.accept(self)
9
+ end
10
+
11
+ def visit_all(nodes)
12
+ nodes.map { |node| visit(node) }
13
+ end
14
+
15
+ def visit_child_nodes(node)
16
+ visit_all(node.child_nodes)
17
+ end
18
+
19
+ #-------------------------------------------------------------------------
20
+ # CSS3 nodes
21
+ #-------------------------------------------------------------------------
22
+
23
+ # Visit an AtKeywordToken node.
24
+ alias visit_at_keyword visit_child_nodes
25
+
26
+ # Visit an AtRule node.
27
+ alias visit_at_rule visit_child_nodes
28
+
29
+ # Visit a BadStringToken node.
30
+ alias visit_bad_string_token visit_child_nodes
31
+
32
+ # Visit a BadURLToken node.
33
+ alias visit_bad_url_token visit_child_nodes
34
+
35
+ # Visit a CDCToken node.
36
+ alias visit_cdc_token visit_child_nodes
37
+
38
+ # Visit a CDOToken node.
39
+ alias visit_cdo_token visit_child_nodes
40
+
41
+ # Visit a CloseCurlyToken node.
42
+ alias visit_close_curly_token visit_child_nodes
43
+
44
+ # Visit a CloseParenToken node.
45
+ alias visit_close_paren_token visit_child_nodes
46
+
47
+ # Visit a CloseSquareToken node.
48
+ alias visit_close_square_token visit_child_nodes
49
+
50
+ # Visit a ColonToken node.
51
+ alias visit_colon_token visit_child_nodes
52
+
53
+ # Visit a CommentToken node.
54
+ alias visit_comment_token visit_child_nodes
55
+
56
+ # Visit a CommaToken node.
57
+ alias visit_comma_token visit_child_nodes
58
+
59
+ # Visit a CSSStyleSheet node.
60
+ alias visit_css_stylesheet visit_child_nodes
61
+
62
+ # Visit a Declaration node.
63
+ alias visit_declaration visit_child_nodes
64
+
65
+ # Visit a DelimToken node.
66
+ alias visit_delim_token visit_child_nodes
67
+
68
+ # Visit a DimensionToken node.
69
+ alias visit_dimension_token visit_child_nodes
70
+
71
+ # Visit an EOFToken node.
72
+ alias visit_eof_token visit_child_nodes
73
+
74
+ # Visit a Function node.
75
+ alias visit_function visit_child_nodes
76
+
77
+ # Visit a FunctionToken node.
78
+ alias visit_function_token visit_child_nodes
79
+
80
+ # Visit a HashToken node.
81
+ alias visit_hash_token visit_child_nodes
82
+
83
+ # Visit an IdentToken node.
84
+ alias visit_ident_token visit_child_nodes
85
+
86
+ # Visit a NumberToken node.
87
+ alias visit_number_token visit_child_nodes
88
+
89
+ # Visit an OpenCurlyToken node.
90
+ alias visit_open_curly_token visit_child_nodes
91
+
92
+ # Visit an OpenParenToken node.
93
+ alias visit_open_paren_token visit_child_nodes
94
+
95
+ # Visit an OpenSquareToken node.
96
+ alias visit_open_square_token visit_child_nodes
97
+
98
+ # Visit a PercentageToken node.
99
+ alias visit_percentage_token visit_child_nodes
100
+
101
+ # Visit a QualifiedRule node.
102
+ alias visit_qualified_rule visit_child_nodes
103
+
104
+ # Visit a SemicolonToken node.
105
+ alias visit_semicolon_token visit_child_nodes
106
+
107
+ # Visit a SimpleBlock node.
108
+ alias visit_simple_block visit_child_nodes
109
+
110
+ # Visit a StringToken node.
111
+ alias visit_string_token visit_child_nodes
112
+
113
+ # Visit a StyleRule node.
114
+ alias visit_style_rule visit_child_nodes
115
+
116
+ # Visit a StyleSheet node.
117
+ alias visit_stylesheet visit_child_nodes
118
+
119
+ # Visit a URange node.
120
+ alias visit_urange visit_child_nodes
121
+
122
+ # Visit a URLToken node.
123
+ alias visit_url_token visit_child_nodes
124
+
125
+ # Visit a WhitespaceToken node.
126
+ alias visit_whitespace_token visit_child_nodes
127
+
128
+ #-------------------------------------------------------------------------
129
+ # Selector nodes
130
+ #-------------------------------------------------------------------------
131
+
132
+ # Visit a Selectors::ClassSelector node.
133
+ alias visit_class_selector visit_child_nodes
134
+
135
+ # Visit a Selectors::IdSelector node.
136
+ alias visit_id_selector visit_child_nodes
137
+
138
+ # Visit a Selectors::PseudoClassFunction node.
139
+ alias visit_pseudo_class_function visit_child_nodes
140
+
141
+ # Visit a Selectors::PseudoClassSelector node.
142
+ alias visit_pseudo_class_selector visit_child_nodes
143
+
144
+ # Visit a Selectors::PseudoElementSelector node.
145
+ alias visit_pseudo_element_selector visit_child_nodes
146
+
147
+ # Visit a Selectors::TypeSelector node.
148
+ alias visit_type_selector visit_child_nodes
149
+
150
+ # Visit a Selectors::WqName node.
151
+ alias visit_wqname visit_child_nodes
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prettier_print"
4
+ require "syntax_tree"
5
+
6
+ require_relative "css/nodes"
7
+ require_relative "css/parser"
8
+ require_relative "css/selectors"
9
+
10
+ require_relative "css/basic_visitor"
11
+ require_relative "css/format"
12
+ require_relative "css/visitor"
13
+ require_relative "css/pretty_print"
14
+
15
+ module SyntaxTree
16
+ module CSS
17
+ def self.format(source, maxwidth = 80)
18
+ PrettierPrint.format(+"", maxwidth) { |q| parse(source).format(q) }
19
+ end
20
+
21
+ def self.parse(source)
22
+ Parser.new(source).parse
23
+ end
24
+
25
+ def self.read(filepath)
26
+ File.read(filepath)
27
+ end
28
+ end
29
+
30
+ register_handler(".css", CSS)
31
+ end