herbst 0.0.1 → 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.
- checksums.yaml +4 -4
- data/README.md +41 -7
- data/Rakefile +11 -1
- data/exe/herbst +6 -0
- data/herbst.gemspec +5 -3
- data/lib/herbst/builder.rb +358 -0
- data/lib/herbst/cli.rb +276 -0
- data/lib/herbst/converter.rb +182 -0
- data/lib/herbst/haml/attributes.rb +161 -0
- data/lib/herbst/haml/converter.rb +244 -0
- data/lib/herbst/haml/filters.rb +61 -0
- data/lib/herbst/haml.rb +37 -0
- data/lib/herbst/identity_printer.rb +438 -0
- data/lib/herbst/printer.rb +54 -0
- data/lib/herbst/round_trip.rb +67 -0
- data/lib/herbst/slim/converter.rb +452 -0
- data/lib/herbst/slim.rb +30 -0
- data/lib/herbst/version.rb +1 -1
- data/lib/herbst.rb +22 -0
- data/sig/herbst/builder.rbs +110 -0
- data/sig/herbst/cli.rbs +101 -0
- data/sig/herbst/converter.rbs +59 -0
- data/sig/herbst/haml/attributes.rbs +49 -0
- data/sig/herbst/haml/converter.rbs +78 -0
- data/sig/herbst/haml/filters.rbs +25 -0
- data/sig/herbst/haml.rbs +22 -0
- data/sig/herbst/identity_printer.rbs +206 -0
- data/sig/herbst/printer.rbs +25 -0
- data/sig/herbst/round_trip.rbs +26 -0
- data/sig/herbst/slim/converter.rbs +123 -0
- data/sig/herbst/slim.rbs +17 -0
- data/sig/herbst/version.rbs +5 -0
- data/sig/herbst.rbs +11 -1
- metadata +34 -6
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "herb"
|
|
4
|
+
|
|
5
|
+
# This printer is meant to move into Herb. Once the installed herb gem ships it, use Herb's
|
|
6
|
+
# printer and skip the copy below.
|
|
7
|
+
if defined?(Herb::Printer::IdentityPrinter)
|
|
8
|
+
Herbst::IdentityPrinter = Herb::Printer::IdentityPrinter # steep:ignore
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Herbst
|
|
13
|
+
class IdentityPrinter < Herb::Visitor
|
|
14
|
+
# @rbs!
|
|
15
|
+
# interface _ERBTag
|
|
16
|
+
# def tag_opening: () -> Herb::Token?
|
|
17
|
+
# def content: () -> Herb::Token?
|
|
18
|
+
# def tag_closing: () -> Herb::Token?
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# interface _Delimited
|
|
22
|
+
# def tag_opening: () -> Herb::Token?
|
|
23
|
+
# def children: () -> Array[Herb::AST::Node]
|
|
24
|
+
# def tag_closing: () -> Herb::Token?
|
|
25
|
+
# end
|
|
26
|
+
|
|
27
|
+
# @rbs @source: String?
|
|
28
|
+
# @rbs @output: String
|
|
29
|
+
# @rbs @line_offsets: Array[Integer]?
|
|
30
|
+
|
|
31
|
+
#: (Herb::AST::Node, ?source: String?) -> String
|
|
32
|
+
def self.print(node, source: nil)
|
|
33
|
+
new(source: source).print(node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#: (?source: String?) -> void
|
|
37
|
+
def initialize(source: nil)
|
|
38
|
+
super()
|
|
39
|
+
|
|
40
|
+
@source = source
|
|
41
|
+
@output = +""
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#: (Herb::AST::Node) -> String
|
|
45
|
+
def print(node)
|
|
46
|
+
visit(node)
|
|
47
|
+
|
|
48
|
+
@output
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#: (Herb::AST::DocumentNode) -> void
|
|
52
|
+
def visit_document_node(node)
|
|
53
|
+
visit_all(node.children)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: (Herb::AST::LiteralNode) -> void
|
|
57
|
+
def visit_literal_node(node)
|
|
58
|
+
write(node.content)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#: (Herb::AST::HTMLTextNode) -> void
|
|
62
|
+
def visit_html_text_node(node)
|
|
63
|
+
write(node.content)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#: (Herb::AST::WhitespaceNode) -> void
|
|
67
|
+
def visit_whitespace_node(node)
|
|
68
|
+
write(node.value&.value)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: (Herb::AST::HTMLOpenTagNode) -> void
|
|
72
|
+
def visit_html_open_tag_node(node)
|
|
73
|
+
write(node.tag_opening&.value)
|
|
74
|
+
write(node.tag_name&.value)
|
|
75
|
+
|
|
76
|
+
previous_end = (node.tag_name || node.tag_opening)&.location&.end
|
|
77
|
+
|
|
78
|
+
node.children.each do |child|
|
|
79
|
+
if previous_end && !child.is_a?(Herb::AST::WhitespaceNode) && !ends_with_whitespace?
|
|
80
|
+
write(separator_between(previous_end, child.location.start))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
visit(child)
|
|
84
|
+
|
|
85
|
+
previous_end = child.location.end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
write(node.tag_closing&.value)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
#: (Herb::AST::HTMLConditionalOpenTagNode) -> void
|
|
92
|
+
def visit_html_conditional_open_tag_node(node)
|
|
93
|
+
visit(node.conditional)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#: (Herb::AST::HTMLCloseTagNode) -> void
|
|
97
|
+
def visit_html_close_tag_node(node)
|
|
98
|
+
write(node.tag_opening&.value)
|
|
99
|
+
|
|
100
|
+
children = node.children || []
|
|
101
|
+
|
|
102
|
+
if node.tag_name
|
|
103
|
+
start = node.tag_name.location.start
|
|
104
|
+
finish = node.tag_name.location.end
|
|
105
|
+
|
|
106
|
+
visit_all(children.select { |child| child.location.end <= start })
|
|
107
|
+
write(node.tag_name.value)
|
|
108
|
+
visit_all(children.select { |child| child.location.start >= finish })
|
|
109
|
+
else
|
|
110
|
+
visit_all(children)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
write(node.tag_closing&.value)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
#: (Herb::AST::HTMLOmittedCloseTagNode) -> void
|
|
117
|
+
def visit_html_omitted_close_tag_node(_node); end
|
|
118
|
+
|
|
119
|
+
#: (Herb::AST::HTMLVirtualCloseTagNode) -> void
|
|
120
|
+
def visit_html_virtual_close_tag_node(_node); end
|
|
121
|
+
|
|
122
|
+
#: (Herb::AST::HTMLElementNode) -> void
|
|
123
|
+
def visit_html_element_node(node)
|
|
124
|
+
visit(node.open_tag)
|
|
125
|
+
visit_all(node.body)
|
|
126
|
+
visit(node.close_tag)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
#: (Herb::AST::HTMLConditionalElementNode) -> void
|
|
130
|
+
def visit_html_conditional_element_node(node)
|
|
131
|
+
visit(node.open_conditional)
|
|
132
|
+
visit_all(node.body)
|
|
133
|
+
visit(node.close_conditional)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
#: (Herb::AST::HTMLAttributeNode) -> void
|
|
137
|
+
def visit_html_attribute_node(node)
|
|
138
|
+
visit(node.name)
|
|
139
|
+
write(node.equals&.value)
|
|
140
|
+
visit(node.value) if node.equals
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
#: (Herb::AST::HTMLAttributeNameNode) -> void
|
|
144
|
+
def visit_html_attribute_name_node(node)
|
|
145
|
+
visit_all(node.children || [])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#: (Herb::AST::HTMLAttributeValueNode) -> void
|
|
149
|
+
def visit_html_attribute_value_node(node)
|
|
150
|
+
write(node.open_quote&.value || '"') if node.quoted
|
|
151
|
+
visit_all(node.children)
|
|
152
|
+
write(node.close_quote&.value || '"') if node.quoted
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
#: (Herb::AST::RubyLiteralNode) -> void
|
|
156
|
+
def visit_ruby_literal_node(node)
|
|
157
|
+
write(node.content)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#: (Herb::AST::RubyHTMLAttributesSplatNode) -> void
|
|
161
|
+
def visit_ruby_html_attributes_splat_node(node)
|
|
162
|
+
write(node.content)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#: (Herb::AST::HTMLCommentNode) -> void
|
|
166
|
+
def visit_html_comment_node(node)
|
|
167
|
+
write(node.comment_start&.value)
|
|
168
|
+
visit_all(node.children)
|
|
169
|
+
write(node.comment_end&.value)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
#: (Herb::AST::HTMLDoctypeNode) -> void
|
|
173
|
+
def visit_html_doctype_node(node)
|
|
174
|
+
print_delimited(node)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
#: (Herb::AST::XMLDeclarationNode) -> void
|
|
178
|
+
def visit_xml_declaration_node(node)
|
|
179
|
+
print_delimited(node)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#: (Herb::AST::CDATANode) -> void
|
|
183
|
+
def visit_cdata_node(node)
|
|
184
|
+
print_delimited(node)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
#: (Herb::AST::XMLProcessingInstructionNode) -> void
|
|
188
|
+
def visit_xml_processing_instruction_node(node)
|
|
189
|
+
write(node.tag_opening&.value)
|
|
190
|
+
write(node.target&.value)
|
|
191
|
+
visit_all(node.children)
|
|
192
|
+
write(node.tag_closing&.value)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
#: (Herb::AST::ERBOpenTagNode) -> void
|
|
196
|
+
def visit_erb_open_tag_node(node)
|
|
197
|
+
print_erb(node)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
#: (Herb::AST::ERBContentNode) -> void
|
|
201
|
+
def visit_erb_content_node(node)
|
|
202
|
+
print_erb(node)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#: (Herb::AST::ERBCommentNode) -> void
|
|
206
|
+
def visit_erb_comment_node(node)
|
|
207
|
+
print_erb(node)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
#: (Herb::AST::ERBIfNode) -> void
|
|
211
|
+
def visit_erb_if_node(node)
|
|
212
|
+
print_erb(node)
|
|
213
|
+
visit_all(node.statements)
|
|
214
|
+
visit(node.subsequent)
|
|
215
|
+
visit(node.end_node)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
#: (Herb::AST::ERBElseNode) -> void
|
|
219
|
+
def visit_erb_else_node(node)
|
|
220
|
+
print_erb(node)
|
|
221
|
+
visit_all(node.statements)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
#: (Herb::AST::ERBEndNode) -> void
|
|
225
|
+
def visit_erb_end_node(node)
|
|
226
|
+
print_erb(node)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
#: (Herb::AST::ERBBlockNode) -> void
|
|
230
|
+
def visit_erb_block_node(node)
|
|
231
|
+
print_erb(node)
|
|
232
|
+
visit_all(node.body)
|
|
233
|
+
visit(node.rescue_clause)
|
|
234
|
+
visit(node.else_clause)
|
|
235
|
+
visit(node.ensure_clause)
|
|
236
|
+
visit(node.end_node)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
#: (Herb::AST::ERBIterationBlockNode) -> void
|
|
240
|
+
def visit_erb_iteration_block_node(node)
|
|
241
|
+
print_erb(node)
|
|
242
|
+
visit_all(node.body)
|
|
243
|
+
visit(node.rescue_clause)
|
|
244
|
+
visit(node.else_clause)
|
|
245
|
+
visit(node.ensure_clause)
|
|
246
|
+
visit(node.end_node)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
#: (Herb::AST::ERBCaseNode) -> void
|
|
250
|
+
def visit_erb_case_node(node)
|
|
251
|
+
print_erb(node)
|
|
252
|
+
visit_all(node.children)
|
|
253
|
+
visit_all(node.conditions)
|
|
254
|
+
visit(node.else_clause)
|
|
255
|
+
visit(node.end_node)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
#: (Herb::AST::ERBWhenNode) -> void
|
|
259
|
+
def visit_erb_when_node(node)
|
|
260
|
+
print_erb(node)
|
|
261
|
+
visit_all(node.statements)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
#: (Herb::AST::ERBWhileNode) -> void
|
|
265
|
+
def visit_erb_while_node(node)
|
|
266
|
+
print_erb(node)
|
|
267
|
+
visit_all(node.statements)
|
|
268
|
+
visit(node.end_node)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
#: (Herb::AST::ERBUntilNode) -> void
|
|
272
|
+
def visit_erb_until_node(node)
|
|
273
|
+
print_erb(node)
|
|
274
|
+
visit_all(node.statements)
|
|
275
|
+
visit(node.end_node)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
#: (Herb::AST::ERBForNode) -> void
|
|
279
|
+
def visit_erb_for_node(node)
|
|
280
|
+
print_erb(node)
|
|
281
|
+
visit_all(node.statements)
|
|
282
|
+
visit(node.end_node)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
#: (Herb::AST::ERBBeginNode) -> void
|
|
286
|
+
def visit_erb_begin_node(node)
|
|
287
|
+
print_erb(node)
|
|
288
|
+
visit_all(node.statements)
|
|
289
|
+
visit(node.rescue_clause)
|
|
290
|
+
visit(node.else_clause)
|
|
291
|
+
visit(node.ensure_clause)
|
|
292
|
+
visit(node.end_node)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
#: (Herb::AST::ERBRescueNode) -> void
|
|
296
|
+
def visit_erb_rescue_node(node)
|
|
297
|
+
print_erb(node)
|
|
298
|
+
visit_all(node.statements)
|
|
299
|
+
visit(node.subsequent)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
#: (Herb::AST::ERBEnsureNode) -> void
|
|
303
|
+
def visit_erb_ensure_node(node)
|
|
304
|
+
print_erb(node)
|
|
305
|
+
visit_all(node.statements)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
#: (Herb::AST::ERBUnlessNode) -> void
|
|
309
|
+
def visit_erb_unless_node(node)
|
|
310
|
+
print_erb(node)
|
|
311
|
+
visit_all(node.statements)
|
|
312
|
+
visit(node.else_clause)
|
|
313
|
+
visit(node.end_node)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
#: (Herb::AST::ERBRenderNode) -> void
|
|
317
|
+
def visit_erb_render_node(node)
|
|
318
|
+
print_erb(node)
|
|
319
|
+
return unless node.end_node
|
|
320
|
+
|
|
321
|
+
visit_all(node.body)
|
|
322
|
+
visit(node.rescue_clause)
|
|
323
|
+
visit(node.else_clause)
|
|
324
|
+
visit(node.ensure_clause)
|
|
325
|
+
visit(node.end_node)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
#: (Herb::AST::RubyRenderKeywordsNode) -> void
|
|
329
|
+
def visit_ruby_render_keywords_node(_node)
|
|
330
|
+
# extracted metadata, nothing to print
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
#: (Herb::AST::RubyRenderLocalNode) -> void
|
|
334
|
+
def visit_ruby_render_local_node(_node)
|
|
335
|
+
# extracted metadata, nothing to print
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
#: (Herb::AST::ERBStrictLocalsNode) -> void
|
|
339
|
+
def visit_erb_strict_locals_node(node)
|
|
340
|
+
print_erb(node)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
#: (Herb::AST::HerbDirectiveNode) -> void
|
|
344
|
+
def visit_herb_directive_node(node)
|
|
345
|
+
print_erb(node)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
#: (Herb::AST::HerbStateDirectiveNode) -> void
|
|
349
|
+
def visit_herb_state_directive_node(node)
|
|
350
|
+
print_erb(node)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
#: (Herb::AST::HerbStateDeclarationNode) -> void
|
|
354
|
+
def visit_herb_state_declaration_node(_node)
|
|
355
|
+
# extracted metadata, nothing to print
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
#: (Herb::AST::RubyParameterNode) -> void
|
|
359
|
+
def visit_ruby_parameter_node(_node)
|
|
360
|
+
# extracted metadata, nothing to print
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
#: (Herb::AST::ERBYieldNode) -> void
|
|
364
|
+
def visit_erb_yield_node(node)
|
|
365
|
+
print_erb(node)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
#: (Herb::AST::ERBInNode) -> void
|
|
369
|
+
def visit_erb_in_node(node)
|
|
370
|
+
print_erb(node)
|
|
371
|
+
visit_all(node.statements)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
#: (Herb::AST::ERBCaseMatchNode) -> void
|
|
375
|
+
def visit_erb_case_match_node(node)
|
|
376
|
+
print_erb(node)
|
|
377
|
+
visit_all(node.children)
|
|
378
|
+
visit_all(node.conditions)
|
|
379
|
+
visit(node.else_clause)
|
|
380
|
+
visit(node.end_node)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
private
|
|
384
|
+
|
|
385
|
+
#: (String?) -> void
|
|
386
|
+
def write(value)
|
|
387
|
+
@output << value if value
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
#: () -> bool
|
|
391
|
+
def ends_with_whitespace?
|
|
392
|
+
@output.match?(/\s\z/)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
#: (_ERBTag) -> void
|
|
396
|
+
def print_erb(node)
|
|
397
|
+
write(node.tag_opening&.value)
|
|
398
|
+
write(node.content&.value)
|
|
399
|
+
write(node.tag_closing&.value)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
#: (_Delimited) -> void
|
|
403
|
+
def print_delimited(node)
|
|
404
|
+
write(node.tag_opening&.value)
|
|
405
|
+
visit_all(node.children)
|
|
406
|
+
write(node.tag_closing&.value)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
#: (Herb::Position, Herb::Position) -> String
|
|
410
|
+
def separator_between(from, to)
|
|
411
|
+
return "" unless (from <=> to)&.negative?
|
|
412
|
+
|
|
413
|
+
source = @source
|
|
414
|
+
return " " unless source
|
|
415
|
+
|
|
416
|
+
separator = slice_between(source, from, to)
|
|
417
|
+
|
|
418
|
+
separator.nil? || separator.empty? || !separator.strip.empty? ? " " : separator
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
#: (String, Herb::Position, Herb::Position) -> String?
|
|
422
|
+
def slice_between(source, from, to)
|
|
423
|
+
start = offset(source, from)
|
|
424
|
+
finish = offset(source, to)
|
|
425
|
+
return nil if start.nil? || finish.nil? || start > finish
|
|
426
|
+
|
|
427
|
+
source.byteslice(start, finish - start)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
#: (String, Herb::Position) -> Integer?
|
|
431
|
+
def offset(source, position)
|
|
432
|
+
line_offsets = @line_offsets ||= source.each_line.with_object([0]) { |line, offsets| offsets << (offsets.last + line.bytesize) }
|
|
433
|
+
line_start = line_offsets[position.line - 1]
|
|
434
|
+
|
|
435
|
+
line_start && (line_start + position.column)
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Herbst
|
|
7
|
+
module Printer
|
|
8
|
+
FORMATTER = ["npx", "--yes", "@herb-tools/formatter@#{Herb::VERSION}"].freeze #: Array[String]
|
|
9
|
+
|
|
10
|
+
class Error < Herbst::Error; end
|
|
11
|
+
|
|
12
|
+
#: (Herb::AST::DocumentNode, ?format: bool) -> String
|
|
13
|
+
def self.print(document, format: true)
|
|
14
|
+
erb = IdentityPrinter.print(document)
|
|
15
|
+
|
|
16
|
+
format ? format(erb) : erb
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: (Array[Herb::AST::DocumentNode], ?format: bool) -> Array[String]
|
|
20
|
+
def self.print_all(documents, format: true)
|
|
21
|
+
sources = documents.map { |document| IdentityPrinter.print(document) }
|
|
22
|
+
|
|
23
|
+
format ? format_all(sources) : sources
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: (String) -> String
|
|
27
|
+
def self.format(source)
|
|
28
|
+
run("-", stdin_data: source)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: (Array[String]) -> Array[String]
|
|
32
|
+
def self.format_all(sources)
|
|
33
|
+
Dir.mktmpdir("herbst") do |directory|
|
|
34
|
+
paths = sources.each_with_index.map do |source, index|
|
|
35
|
+
File.join(directory, "#{index}.html.erb").tap { |path| File.write(path, source) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
run(directory, chdir: directory) unless paths.empty?
|
|
39
|
+
|
|
40
|
+
paths.map { |path| File.read(path, encoding: Encoding::UTF_8) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#: (*String, **untyped) -> String
|
|
45
|
+
def self.run(*, **)
|
|
46
|
+
stdout, stderr, status = Open3.capture3(*FORMATTER, *, **)
|
|
47
|
+
raise Error, "Herb formatter failed:\n#{stderr}" unless status.success?
|
|
48
|
+
|
|
49
|
+
stdout.force_encoding(Encoding::UTF_8)
|
|
50
|
+
rescue Errno::ENOENT
|
|
51
|
+
raise Error, "Node.js is required to format ERB (could not run `npx`)"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "herb"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Herbst
|
|
7
|
+
# Checks that printed ERB parses back into exactly the tree it was printed from.
|
|
8
|
+
#
|
|
9
|
+
# The ERB is parsed with `track_whitespace: true`, and every node, field, token value and
|
|
10
|
+
# whitespace node is compared. Only source positions and Prism data are ignored, since a
|
|
11
|
+
# built tree doesn't have them.
|
|
12
|
+
#
|
|
13
|
+
# Herbst::RoundTrip.difference(document, erb) # => nil, or where the trees differ
|
|
14
|
+
#
|
|
15
|
+
module RoundTrip
|
|
16
|
+
POSITIONAL_FIELDS = ["location", "range", "then_keyword", "prism_node", "prism_context", "analyzed_ruby"].freeze #: Array[String]
|
|
17
|
+
|
|
18
|
+
#: (Herb::AST::DocumentNode, String) -> String?
|
|
19
|
+
def self.difference(document, erb)
|
|
20
|
+
result = Herb.parse(erb, track_whitespace: true)
|
|
21
|
+
|
|
22
|
+
unless result.errors.empty?
|
|
23
|
+
return "printed ERB has parse errors:\n#{result.errors.map { |error| " #{error.message}" }.join("\n")}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
tree_difference(comparable(document), comparable(result.value), "document")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#: (Herb::AST::Node) -> untyped
|
|
30
|
+
def self.comparable(node)
|
|
31
|
+
strip_positions(JSON.parse(node.to_json))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: (untyped) -> untyped
|
|
35
|
+
def self.strip_positions(value)
|
|
36
|
+
case value
|
|
37
|
+
when Hash
|
|
38
|
+
value.except(*POSITIONAL_FIELDS).transform_values { |child| strip_positions(child) }
|
|
39
|
+
when Array
|
|
40
|
+
value.map { |child| strip_positions(child) }
|
|
41
|
+
else
|
|
42
|
+
value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#: (untyped, untyped, String) -> String?
|
|
47
|
+
def self.tree_difference(built, parsed, path)
|
|
48
|
+
return nil if built == parsed
|
|
49
|
+
|
|
50
|
+
if built.is_a?(Hash) && parsed.is_a?(Hash) && built["type"] == parsed["type"]
|
|
51
|
+
(built.keys | parsed.keys).each do |key|
|
|
52
|
+
difference = tree_difference(built[key], parsed[key], "#{path}.#{key}")
|
|
53
|
+
return difference if difference
|
|
54
|
+
end
|
|
55
|
+
elsif built.is_a?(Array) && parsed.is_a?(Array) && built.size == parsed.size
|
|
56
|
+
built.each_index do |index|
|
|
57
|
+
difference = tree_difference(built[index], parsed[index], "#{path}[#{index}]")
|
|
58
|
+
return difference if difference
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
"printed ERB parses into a different tree at #{path}\n " \
|
|
63
|
+
"built: #{built.inspect[0, 300]}\n " \
|
|
64
|
+
"parsed: #{parsed.inspect[0, 300]}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|