syntax_tree-css 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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