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.
@@ -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