w_syntax_tree-erb 0.9.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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/formatting-report.md +37 -0
- data/.github/ISSUE_TEMPLATE/general.md +10 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/auto-merge.yml +22 -0
- data/.github/workflows/main.yml +32 -0
- data/.gitignore +10 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/Rakefile +16 -0
- data/check_erb_parse.rb +20 -0
- data/lib/syntax_tree/erb/format.rb +230 -0
- data/lib/syntax_tree/erb/nodes.rb +511 -0
- data/lib/syntax_tree/erb/parser.rb +741 -0
- data/lib/syntax_tree/erb/pretty_print.rb +214 -0
- data/lib/syntax_tree/erb/version.rb +7 -0
- data/lib/syntax_tree/erb/visitor.rb +54 -0
- data/lib/syntax_tree/erb.rb +30 -0
- data/syntax_tree-erb.gemspec +33 -0
- metadata +151 -0
@@ -0,0 +1,511 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxTree
|
4
|
+
module ERB
|
5
|
+
# A Location represents a position for a node in the source file.
|
6
|
+
class Location
|
7
|
+
attr_reader :start_char, :end_char, :start_line, :end_line
|
8
|
+
|
9
|
+
def initialize(start_char:, end_char:, start_line:, end_line:)
|
10
|
+
@start_char = start_char
|
11
|
+
@end_char = end_char
|
12
|
+
@start_line = start_line
|
13
|
+
@end_line = end_line
|
14
|
+
end
|
15
|
+
|
16
|
+
def deconstruct_keys(keys)
|
17
|
+
{
|
18
|
+
start_char: start_char,
|
19
|
+
end_char: end_char,
|
20
|
+
start_line: start_line,
|
21
|
+
end_line: end_line
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def to(other)
|
26
|
+
Location.new(
|
27
|
+
start_char: start_char,
|
28
|
+
start_line: start_line,
|
29
|
+
end_char: other.end_char,
|
30
|
+
end_line: other.end_line
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def <=>(other)
|
35
|
+
start_char <=> other.start_char
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
if start_line == end_line
|
40
|
+
"line #{start_line}, char #{start_char}..#{end_char}"
|
41
|
+
else
|
42
|
+
"line #{start_line},char #{start_char} to line #{end_line}, char #{end_char}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# A parent node that contains a bit of shared functionality.
|
48
|
+
class Node
|
49
|
+
def format(q)
|
50
|
+
Format.new(q).visit(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
def pretty_print(q)
|
54
|
+
PrettyPrint.new(q).visit(self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# A Token is any kind of lexical token from the source. It has a type, a
|
59
|
+
# value which is a subset of the source, and an index where it starts in
|
60
|
+
# the source.
|
61
|
+
class Token < Node
|
62
|
+
attr_reader :type, :value, :location
|
63
|
+
|
64
|
+
def initialize(type:, value:, location:)
|
65
|
+
@type = type
|
66
|
+
@value = value
|
67
|
+
@location = location
|
68
|
+
end
|
69
|
+
|
70
|
+
def accept(visitor)
|
71
|
+
visitor.visit_token(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
def child_nodes
|
75
|
+
[]
|
76
|
+
end
|
77
|
+
|
78
|
+
alias deconstruct child_nodes
|
79
|
+
|
80
|
+
def deconstruct_keys(keys)
|
81
|
+
{ type: type, value: value, location: location }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# The Document node is the top of the syntax tree.
|
86
|
+
# It contains any number of:
|
87
|
+
# - Text
|
88
|
+
# - HtmlNode
|
89
|
+
# - ErbNodes
|
90
|
+
class Document < Node
|
91
|
+
attr_reader :elements, :location
|
92
|
+
|
93
|
+
def initialize(elements:, location:)
|
94
|
+
@elements = elements
|
95
|
+
@location = location
|
96
|
+
end
|
97
|
+
|
98
|
+
def accept(visitor)
|
99
|
+
visitor.visit_document(self)
|
100
|
+
end
|
101
|
+
|
102
|
+
def child_nodes
|
103
|
+
[*elements].compact
|
104
|
+
end
|
105
|
+
|
106
|
+
alias deconstruct child_nodes
|
107
|
+
|
108
|
+
def deconstruct_keys(keys)
|
109
|
+
{ elements: elements, location: location }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# This is a base class for a block that contains:
|
114
|
+
# - an opening
|
115
|
+
# - optional elements
|
116
|
+
# - optional closing
|
117
|
+
class Block < Node
|
118
|
+
attr_reader(:opening, :elements, :closing, :location)
|
119
|
+
def initialize(opening:, location:, elements: nil, closing: nil)
|
120
|
+
@opening = opening
|
121
|
+
@elements = elements || []
|
122
|
+
@closing = closing
|
123
|
+
@location = location
|
124
|
+
end
|
125
|
+
|
126
|
+
def accept(visitor)
|
127
|
+
visitor.visit_block(self)
|
128
|
+
end
|
129
|
+
|
130
|
+
def child_nodes
|
131
|
+
[opening, *elements, closing].compact
|
132
|
+
end
|
133
|
+
|
134
|
+
alias deconstruct child_nodes
|
135
|
+
|
136
|
+
def deconstruct_keys(keys)
|
137
|
+
{
|
138
|
+
opening: opening,
|
139
|
+
content: content,
|
140
|
+
closing: closing,
|
141
|
+
location: location
|
142
|
+
}
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# An element is a child of the document. It contains an opening tag, any
|
147
|
+
# optional content within the tag, and a closing tag. It can also
|
148
|
+
# potentially contain an opening tag that self-closes, in which case the
|
149
|
+
# content and closing tag will be nil.
|
150
|
+
class HtmlNode < Block
|
151
|
+
# The opening tag of an element. It contains the opening character (<),
|
152
|
+
# the name of the element, any optional attributes, and the closing
|
153
|
+
# token (either > or />).
|
154
|
+
class OpeningTag < Node
|
155
|
+
attr_reader :opening, :name, :attributes, :closing, :location
|
156
|
+
|
157
|
+
def initialize(opening:, name:, attributes:, closing:, location:)
|
158
|
+
@opening = opening
|
159
|
+
@name = name
|
160
|
+
@attributes = attributes
|
161
|
+
@closing = closing
|
162
|
+
@location = location
|
163
|
+
end
|
164
|
+
|
165
|
+
def accept(visitor)
|
166
|
+
visitor.visit_opening_tag(self)
|
167
|
+
end
|
168
|
+
|
169
|
+
def child_nodes
|
170
|
+
[opening, name, *attributes, closing]
|
171
|
+
end
|
172
|
+
|
173
|
+
alias deconstruct child_nodes
|
174
|
+
|
175
|
+
def deconstruct_keys(keys)
|
176
|
+
{
|
177
|
+
opening: opening,
|
178
|
+
name: name,
|
179
|
+
attributes: attributes,
|
180
|
+
closing: closing,
|
181
|
+
location: location
|
182
|
+
}
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# The closing tag of an element. It contains the opening character (<),
|
187
|
+
# the name of the element, and the closing character (>).
|
188
|
+
class ClosingTag < Node
|
189
|
+
attr_reader :opening, :name, :closing, :location
|
190
|
+
|
191
|
+
def initialize(opening:, name:, closing:, location:)
|
192
|
+
@opening = opening
|
193
|
+
@name = name
|
194
|
+
@closing = closing
|
195
|
+
@location = location
|
196
|
+
end
|
197
|
+
|
198
|
+
def accept(visitor)
|
199
|
+
visitor.visit_closing_tag(self)
|
200
|
+
end
|
201
|
+
|
202
|
+
def child_nodes
|
203
|
+
[opening, name, closing]
|
204
|
+
end
|
205
|
+
|
206
|
+
alias deconstruct child_nodes
|
207
|
+
|
208
|
+
def deconstruct_keys(keys)
|
209
|
+
{ opening: opening, name: name, closing: closing, location: location }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def accept(visitor)
|
214
|
+
visitor.visit_html(self)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class ErbNode < Node
|
219
|
+
attr_reader :opening_tag, :keyword, :content, :closing_tag, :location
|
220
|
+
|
221
|
+
def initialize(opening_tag:, keyword:, content:, closing_tag:, location:)
|
222
|
+
@opening_tag = opening_tag
|
223
|
+
@keyword = keyword
|
224
|
+
@content = ErbContent.new(value: content.map(&:value).join) if content
|
225
|
+
@closing_tag = closing_tag
|
226
|
+
@location = location
|
227
|
+
end
|
228
|
+
|
229
|
+
def accept(visitor)
|
230
|
+
visitor.visit_erb(self)
|
231
|
+
end
|
232
|
+
|
233
|
+
def child_nodes
|
234
|
+
[opening_tag, keyword, content, closing_tag].compact
|
235
|
+
end
|
236
|
+
|
237
|
+
alias deconstruct child_nodes
|
238
|
+
|
239
|
+
def deconstruct_keys(keys)
|
240
|
+
{
|
241
|
+
opening_tag: opening_tag,
|
242
|
+
keyword: keyword,
|
243
|
+
content: content,
|
244
|
+
closing_tag: closing_tag,
|
245
|
+
location: location
|
246
|
+
}
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
class ErbBlock < Block
|
251
|
+
def accept(visitor)
|
252
|
+
visitor.visit_erb_block(self)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
class ErbClose < Node
|
257
|
+
attr_reader :location, :closing
|
258
|
+
|
259
|
+
def initialize(location:, closing:)
|
260
|
+
@location = location
|
261
|
+
@closing = closing
|
262
|
+
end
|
263
|
+
|
264
|
+
def accept(visitor)
|
265
|
+
visitor.visit_erb_close(self)
|
266
|
+
end
|
267
|
+
|
268
|
+
def child_nodes
|
269
|
+
[]
|
270
|
+
end
|
271
|
+
|
272
|
+
alias deconstruct child_nodes
|
273
|
+
|
274
|
+
def deconstruct_keys(keys)
|
275
|
+
{ location: location, closing: closing }
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class ErbDoClose < ErbClose
|
280
|
+
def accept(visitor)
|
281
|
+
visitor.visit_erb_do_close(self)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
class ErbControl < Block
|
286
|
+
end
|
287
|
+
|
288
|
+
class ErbIf < ErbControl
|
289
|
+
# opening: ErbNode
|
290
|
+
# elements: [[HtmlNode | ErbNode | CharDataNode]]
|
291
|
+
# closing: [nil | ErbElsif | ErbElse]
|
292
|
+
def accept(visitor)
|
293
|
+
visitor.visit_erb_if(self)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
class ErbUnless < ErbIf
|
298
|
+
# opening: ErbNode
|
299
|
+
# elements: [[HtmlNode | ErbNode | CharDataNode]]
|
300
|
+
# closing: [nil | ErbElsif | ErbElse]
|
301
|
+
def accept(visitor)
|
302
|
+
visitor.visit_erb_if(self)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
class ErbElsif < ErbIf
|
307
|
+
def accept(visitor)
|
308
|
+
visitor.visit_erb_if(self)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
class ErbElse < ErbIf
|
313
|
+
def accept(visitor)
|
314
|
+
visitor.visit_erb_if(self)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
class ErbEnd < ErbNode
|
319
|
+
def accept(visitor)
|
320
|
+
visitor.visit_erb_end(self)
|
321
|
+
end
|
322
|
+
|
323
|
+
def child_nodes
|
324
|
+
[]
|
325
|
+
end
|
326
|
+
|
327
|
+
alias deconstruct child_nodes
|
328
|
+
end
|
329
|
+
|
330
|
+
class ErbCase < ErbControl
|
331
|
+
# opening: ErbNode
|
332
|
+
# elements: [[HtmlNode | ErbNode | CharDataNode]]
|
333
|
+
# closing: [nil | ErbCaseWhen | ErbElse | ErbEnd]
|
334
|
+
def accept(visitor)
|
335
|
+
visitor.visit_erb_case(self)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class ErbCaseWhen < ErbControl
|
340
|
+
# opening: ErbNode
|
341
|
+
# elements: [[HtmlNode | ErbNode | CharDataNode]]
|
342
|
+
# closing: [nil | ErbCaseWhen | ErbElse | ErbEnd]
|
343
|
+
def accept(visitor)
|
344
|
+
visitor.visit_erb_case_when(self)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
class ErbContent < Node
|
349
|
+
attr_reader(:value, :unparsed_value)
|
350
|
+
|
351
|
+
def initialize(value:)
|
352
|
+
@unparsed_value = value
|
353
|
+
begin
|
354
|
+
@value = SyntaxTree.parse(value.strip)
|
355
|
+
rescue SyntaxTree::Parser::ParseError
|
356
|
+
# Removes leading and trailing whitespace
|
357
|
+
@value = value&.lstrip&.rstrip
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def accept(visitor)
|
362
|
+
visitor.visit_erb_content(self)
|
363
|
+
end
|
364
|
+
|
365
|
+
def child_nodes
|
366
|
+
[]
|
367
|
+
end
|
368
|
+
|
369
|
+
alias deconstruct child_nodes
|
370
|
+
|
371
|
+
def deconstruct_keys(keys)
|
372
|
+
{ value: value }
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# An HtmlAttribute is a key-value pair within a tag. It contains the key, the
|
377
|
+
# equals sign, and the value.
|
378
|
+
class HtmlAttribute < Node
|
379
|
+
attr_reader :key, :equals, :value, :location
|
380
|
+
|
381
|
+
def initialize(key:, equals:, value:, location:)
|
382
|
+
@key = key
|
383
|
+
@equals = equals
|
384
|
+
@value = value
|
385
|
+
@location = location
|
386
|
+
end
|
387
|
+
|
388
|
+
def accept(visitor)
|
389
|
+
visitor.visit_attribute(self)
|
390
|
+
end
|
391
|
+
|
392
|
+
def child_nodes
|
393
|
+
[key, equals, value]
|
394
|
+
end
|
395
|
+
|
396
|
+
alias deconstruct child_nodes
|
397
|
+
|
398
|
+
def deconstruct_keys(keys)
|
399
|
+
{ key: key, equals: equals, value: value, location: location }
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# A HtmlString can include ERB-tags
|
404
|
+
class HtmlString < Node
|
405
|
+
attr_reader :opening, :contents, :closing, :location
|
406
|
+
|
407
|
+
def initialize(opening:, contents:, closing:, location:)
|
408
|
+
@opening = opening
|
409
|
+
@contents = contents
|
410
|
+
@closing = closing
|
411
|
+
@location = location
|
412
|
+
end
|
413
|
+
|
414
|
+
def accept(visitor)
|
415
|
+
visitor.visit_html_string(self)
|
416
|
+
end
|
417
|
+
|
418
|
+
def child_nodes
|
419
|
+
[*contents]
|
420
|
+
end
|
421
|
+
|
422
|
+
alias deconstruct child_nodes
|
423
|
+
|
424
|
+
def deconstruct_keys(keys)
|
425
|
+
{
|
426
|
+
opening: opening,
|
427
|
+
contents: contents,
|
428
|
+
closing: closing,
|
429
|
+
location: location
|
430
|
+
}
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
class HtmlComment < Node
|
435
|
+
attr_reader :token, :location
|
436
|
+
|
437
|
+
def initialize(token:, location:)
|
438
|
+
@token = token
|
439
|
+
@location = location
|
440
|
+
end
|
441
|
+
|
442
|
+
def accept(visitor)
|
443
|
+
visitor.visit_html_comment(self)
|
444
|
+
end
|
445
|
+
|
446
|
+
def child_nodes
|
447
|
+
[]
|
448
|
+
end
|
449
|
+
|
450
|
+
alias deconstruct child_nodes
|
451
|
+
|
452
|
+
def deconstruct_keys(keys)
|
453
|
+
{ token: token, location: location }
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# A CharData contains either plain text or whitespace within an element.
|
458
|
+
# It wraps a single token value.
|
459
|
+
class CharData < Node
|
460
|
+
attr_reader :value, :location
|
461
|
+
|
462
|
+
def initialize(value:, location:)
|
463
|
+
@value = value
|
464
|
+
@location = location
|
465
|
+
end
|
466
|
+
|
467
|
+
def accept(visitor)
|
468
|
+
visitor.visit_char_data(self)
|
469
|
+
end
|
470
|
+
|
471
|
+
def child_nodes
|
472
|
+
[value]
|
473
|
+
end
|
474
|
+
|
475
|
+
alias deconstruct child_nodes
|
476
|
+
|
477
|
+
def deconstruct_keys(keys)
|
478
|
+
{ value: value, location: location }
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# A document type declaration is a special kind of tag that specifies the
|
483
|
+
# type of the document. It contains an opening declaration, the name of
|
484
|
+
# the document type, an optional external identifier, and a closing of the
|
485
|
+
# tag.
|
486
|
+
class Doctype < Node
|
487
|
+
attr_reader :opening, :name, :closing, :location
|
488
|
+
|
489
|
+
def initialize(opening:, name:, closing:, location:)
|
490
|
+
@opening = opening
|
491
|
+
@name = name
|
492
|
+
@closing = closing
|
493
|
+
@location = location
|
494
|
+
end
|
495
|
+
|
496
|
+
def accept(visitor)
|
497
|
+
visitor.visit_doctype(self)
|
498
|
+
end
|
499
|
+
|
500
|
+
def child_nodes
|
501
|
+
[opening, name, closing].compact
|
502
|
+
end
|
503
|
+
|
504
|
+
alias deconstruct child_nodes
|
505
|
+
|
506
|
+
def deconstruct_keys(keys)
|
507
|
+
{ opening: opening, name: name, closing: closing, location: location }
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|