klenod-build 0.0.1

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +29 -0
  3. data/lib/klenod/build/asset.rb +235 -0
  4. data/lib/klenod/build/asset_compression.rb +58 -0
  5. data/lib/klenod/build/asset_generation_queue.rb +50 -0
  6. data/lib/klenod/build/cli/application.rb +108 -0
  7. data/lib/klenod/build/cli.rb +3 -0
  8. data/lib/klenod/build/config.rb +142 -0
  9. data/lib/klenod/build/context.rb +370 -0
  10. data/lib/klenod/build/dependency.rb +21 -0
  11. data/lib/klenod/build/errors.rb +175 -0
  12. data/lib/klenod/build/filesystem_resolver.rb +147 -0
  13. data/lib/klenod/build/graph/invalidator.rb +246 -0
  14. data/lib/klenod/build/graph.rb +864 -0
  15. data/lib/klenod/build/graphviz.rb +282 -0
  16. data/lib/klenod/build/hashing.rb +21 -0
  17. data/lib/klenod/build/invalidation_result.rb +55 -0
  18. data/lib/klenod/build/load_result.rb +7 -0
  19. data/lib/klenod/build/loaded_module.rb +38 -0
  20. data/lib/klenod/build/module_id.rb +100 -0
  21. data/lib/klenod/build/module_record.rb +22 -0
  22. data/lib/klenod/build/plugin.rb +35 -0
  23. data/lib/klenod/build/plugins/class_names_runtime.rb +125 -0
  24. data/lib/klenod/build/plugins/component_defaults.rb +12 -0
  25. data/lib/klenod/build/plugins/data_plugin.rb +117 -0
  26. data/lib/klenod/build/plugins/gem_import_plugin.rb +125 -0
  27. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +17687 -0
  28. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.rb +105 -0
  29. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +31 -0
  30. data/lib/klenod/build/plugins/google_fonts_plugin.rb +387 -0
  31. data/lib/klenod/build/plugins/haml_plugin/companions.rb +40 -0
  32. data/lib/klenod/build/plugins/haml_plugin/errors.rb +114 -0
  33. data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +123 -0
  34. data/lib/klenod/build/plugins/haml_plugin/parser.rb +85 -0
  35. data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +1039 -0
  36. data/lib/klenod/build/plugins/haml_plugin/transformer.rb +766 -0
  37. data/lib/klenod/build/plugins/haml_plugin.rb +369 -0
  38. data/lib/klenod/build/plugins/image_plugin.rb +401 -0
  39. data/lib/klenod/build/plugins/intl_plugin.rb +34 -0
  40. data/lib/klenod/build/plugins/markdown_compiler.rb +180 -0
  41. data/lib/klenod/build/plugins/markdown_plugin.rb +161 -0
  42. data/lib/klenod/build/plugins/router_plugin.rb +942 -0
  43. data/lib/klenod/build/plugins/ruby_plugin.rb +34 -0
  44. data/lib/klenod/build/plugins/svg_plugin.rb +197 -0
  45. data/lib/klenod/build/plugins.rb +22 -0
  46. data/lib/klenod/build/profiler.rb +79 -0
  47. data/lib/klenod/build/resolution_error_formatter.rb +42 -0
  48. data/lib/klenod/build/resolver.rb +95 -0
  49. data/lib/klenod/build/ruby_import_rewriter.rb +436 -0
  50. data/lib/klenod/build/source_map/editor.rb +110 -0
  51. data/lib/klenod/build/source_map/map.rb +168 -0
  52. data/lib/klenod/build/source_map/vlq.rb +68 -0
  53. data/lib/klenod/build/source_map.rb +12 -0
  54. data/lib/klenod/build/transform_result.rb +12 -0
  55. data/lib/klenod/build/version.rb +7 -0
  56. data/lib/klenod/build/watched_pattern.rb +11 -0
  57. data/lib/klenod/build/watcher.rb +141 -0
  58. data/lib/klenod/build.rb +15 -0
  59. metadata +310 -0
@@ -0,0 +1,1039 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+ require "syntax_tree"
5
+ require "syntax_tree/dsl"
6
+ require "syntax_suggest/api"
7
+ require "syntax_suggest/explain_syntax"
8
+
9
+ module Klenod
10
+ module Build
11
+ module Plugins
12
+ module HamlPlugin
13
+ class Transformer
14
+ class RubyBuilder
15
+ include SyntaxTree::DSL
16
+
17
+ Fragment = Data.define(:source, :node) do
18
+ def node?
19
+ !node.nil?
20
+ end
21
+
22
+ def statement_body
23
+ node.is_a?(SyntaxTree::Statements) ? node.body : [node].compact
24
+ end
25
+
26
+ def to_s
27
+ source
28
+ end
29
+ end
30
+
31
+ def initialize(profiler: nil, global_variables: nil)
32
+ @profiler = profiler
33
+ @global_variables = global_variables
34
+ @expression_cache = {}
35
+ @statements_cache = {}
36
+ @program_cache = {}
37
+ @literal_cache = {}
38
+ end
39
+
40
+ def component_source(
41
+ component_class_name:,
42
+ component_base_class:,
43
+ translations_source:,
44
+ ruby_source:,
45
+ render_source:,
46
+ styles_source:,
47
+ haml_helper_source: nil,
48
+ static_constants: [],
49
+ i18n_source: nil
50
+ )
51
+ component_program(
52
+ component_class_name: component_class_name,
53
+ component_base_class: component_base_class,
54
+ translations_source: translations_source,
55
+ i18n_source: i18n_source,
56
+ ruby_source: ruby_source,
57
+ render_source: render_source,
58
+ styles_source: styles_source,
59
+ haml_helper_source: haml_helper_source,
60
+ static_constants: static_constants
61
+ ).source
62
+ end
63
+
64
+ def component_program(
65
+ component_class_name:,
66
+ component_base_class:,
67
+ translations_source:,
68
+ ruby_source:,
69
+ render_source:,
70
+ styles_source:,
71
+ haml_helper_source: nil,
72
+ static_constants: [],
73
+ i18n_source: nil
74
+ )
75
+ component_class_name = expression_fragment(component_class_name)
76
+ component_base_class = expression_fragment(component_base_class)
77
+ translations_source = expression_fragment(translations_source)
78
+ ruby_source = statements_fragment(ruby_source)
79
+ render_source = expression_fragment(render_source)
80
+ styles_source = expression_fragment(styles_source)
81
+ haml_helper_source = statements_fragment(haml_helper_source) if haml_helper_source
82
+
83
+ header = [
84
+ Fragment.new("# frozen_string_literal: true", comment_node("# frozen_string_literal: true")),
85
+ constant_assignment(
86
+ "KlenodImport",
87
+ call(receiver: nil, name: "method", arguments: [symbol("__klenod_import__")])
88
+ ),
89
+ haml_helper_source
90
+ ].compact
91
+ component_class =
92
+ component_class_fragment(
93
+ component_class_name: component_class_name,
94
+ component_base_class: component_base_class,
95
+ translations_source: translations_source,
96
+ styles_source: styles_source,
97
+ i18n_source: i18n_source,
98
+ ruby_source: ruby_source,
99
+ render_source: render_source,
100
+ static_constants: static_constants
101
+ )
102
+ footer = [
103
+ constant_assignment("Default", component_class_name),
104
+ constant_assignment("ClassNames", "Default::ClassNames"),
105
+ constant_assignment("Translations", "Default::Translations")
106
+ ]
107
+
108
+ program_from_fragments(header, component_class, footer)
109
+ end
110
+
111
+ def component_class_fragment(
112
+ component_class_name:,
113
+ component_base_class:,
114
+ translations_source:,
115
+ styles_source:,
116
+ ruby_source:,
117
+ render_source:,
118
+ static_constants: [],
119
+ i18n_source: nil
120
+ )
121
+ skeleton = class_skeleton_fragment(component_class_name, component_base_class)
122
+ body_fragments =
123
+ [
124
+ method_definition("module_path", target: "self", body: file_expression),
125
+ constant_assignment("Self", "self"),
126
+ constant_assignment("Translations", translations_source),
127
+ i18n_source,
128
+ method_definition(
129
+ "__klenod_import__",
130
+ target: "self",
131
+ parameters: ["dependency_id"],
132
+ body: call(receiver: "KlenodImport", name: "call", arguments: ["dependency_id"])
133
+ ),
134
+ method_definition(
135
+ "__klenod_import__",
136
+ parameters: ["dependency_id"],
137
+ body: call(receiver: "self.class", name: "__klenod_import__", arguments: ["dependency_id"])
138
+ ),
139
+ constant_assignment("ClassNames", styles_source),
140
+ ruby_source,
141
+ *static_constants,
142
+ public_method_definition("render", body: render_source)
143
+ ]
144
+ body =
145
+ body_fragments.flat_map { |fragment| statement_body_for(fragment) }
146
+
147
+ Fragment.new(
148
+ [
149
+ "class #{to_source(component_class_name)} < #{to_source(component_base_class)}",
150
+ indent(compact_join(body_fragments), 2),
151
+ "end"
152
+ ].join("\n"),
153
+ skeleton.node.copy(
154
+ bodystmt: BodyStmt(
155
+ Statements(body),
156
+ nil,
157
+ nil,
158
+ nil,
159
+ nil
160
+ )
161
+ )
162
+ )
163
+ end
164
+
165
+ def expressions(expressions)
166
+ source_expressions(expressions)
167
+ end
168
+
169
+ def expression(source, line_no: nil)
170
+ source = rewrite_ruby_source(source, line_no)
171
+ return Fragment.new(source, constant_path(source)) if source.match?(VALID_CONST_PATH)
172
+
173
+ node = parse_expression(source, context: :expression)
174
+
175
+ Fragment.new(source, node)
176
+ end
177
+
178
+ def statements(source, line_no: nil)
179
+ source = rewrite_ruby_source(source, line_no)
180
+ node = parse_statements(source)
181
+
182
+ Fragment.new(source, node)
183
+ end
184
+
185
+ def program(source)
186
+ node = parse_program(source)
187
+
188
+ Fragment.new(node ? format_node(node) : source, node)
189
+ end
190
+
191
+ def program_from_fragments(*fragments)
192
+ fragments = fragments.flatten
193
+ body = fragments.flat_map { |fragment| statement_body_for(fragment) }
194
+
195
+ Fragment.new(compact_join(fragments), Program(Statements(body)))
196
+ end
197
+
198
+ def fragment(node)
199
+ Fragment.new(format_node(node), node)
200
+ end
201
+
202
+ def node_fragment(source, node)
203
+ Fragment.new(source.to_s, node)
204
+ end
205
+
206
+ def expression_fragment(value)
207
+ value.is_a?(Fragment) ? value : expression(value.to_s)
208
+ end
209
+
210
+ def statements_fragment(value)
211
+ value.is_a?(Fragment) ? value : statements(value.to_s)
212
+ end
213
+
214
+ def to_source(value)
215
+ value.is_a?(Fragment) ? value.source : value.to_s
216
+ end
217
+
218
+ def literal(value)
219
+ if value.is_a?(String) && value.length <= 1
220
+ return @literal_cache[value] ||= literal_fragment(value)
221
+ end
222
+
223
+ literal_fragment(value)
224
+ end
225
+
226
+ def literal_fragment(value)
227
+ Fragment.new(literal_source(value), literal_node(value))
228
+ end
229
+
230
+ def frozen_literal(value)
231
+ fragment(frozen_literal_node(value))
232
+ end
233
+
234
+ def import_call(dependency_id)
235
+ Fragment.new(
236
+ "__klenod_import__(#{literal_source(dependency_id)})",
237
+ CallNode(
238
+ nil,
239
+ nil,
240
+ Ident("__klenod_import__"),
241
+ ArgParen(Args([literal_node(dependency_id)]))
242
+ )
243
+ )
244
+ end
245
+
246
+ def constant_assignment(name, value)
247
+ value = expression_fragment(value)
248
+ node = Assign(VarField(Const(name.to_s)), node_for(value))
249
+ Fragment.new("#{name} = #{to_source(value)}", node)
250
+ end
251
+
252
+ def call(receiver:, name:, arguments:)
253
+ receiver = expression_fragment(receiver) unless receiver.nil?
254
+ arguments = arguments.map { |argument| expression_fragment(argument) }
255
+ receiver_node = receiver.nil? ? nil : node_for(receiver)
256
+
257
+ node =
258
+ CallNode(
259
+ receiver_node,
260
+ receiver_node ? Period(".") : nil,
261
+ Ident(name.to_s),
262
+ ArgParen(Args(arguments.map { |argument| node_for(argument) }))
263
+ )
264
+ receiver_prefix = receiver ? "#{to_source(receiver)}." : nil
265
+ source = "#{receiver_prefix}#{name}(#{arguments.map { |argument| to_source(argument) }.join(", ")})"
266
+ Fragment.new(source, node)
267
+ end
268
+
269
+ def method_definition(name, body:, target: nil, parameters: [])
270
+ body = Array(body)
271
+ body_source = compact_join(body)
272
+ node =
273
+ DefNode(
274
+ target && node_for(expression_fragment(target)),
275
+ target ? Period(".") : nil,
276
+ Ident(name.to_s),
277
+ Params(parameters.map { |parameter| Ident(parameter.to_s) }, [], nil, [], [], nil, nil),
278
+ body_statement(body)
279
+ )
280
+ target_source = target ? "#{to_source(expression_fragment(target))}." : ""
281
+ params_source = parameters.empty? ? "" : "(#{parameters.join(", ")})"
282
+ source = ["def #{target_source}#{name}#{params_source}", indent(body_source, 2), "end"].join("\n")
283
+ Fragment.new(source, node)
284
+ end
285
+
286
+ def public_method_definition(name, body:, parameters: [])
287
+ method = method_definition(name, parameters: parameters, body: body)
288
+ node =
289
+ Command(
290
+ Ident("public"),
291
+ Args([method.node]),
292
+ nil
293
+ )
294
+ Fragment.new("public #{method.source}", node)
295
+ end
296
+
297
+ def nil_expression
298
+ Fragment.new("nil", nil_node)
299
+ end
300
+
301
+ def file_expression
302
+ Fragment.new("__FILE__", VarRef(Kw("__FILE__")))
303
+ end
304
+
305
+ def symbol(value)
306
+ value = value.to_s
307
+ Fragment.new(symbol_source(value), symbol_node(value))
308
+ end
309
+
310
+ def symbol_fragment(value)
311
+ Fragment.new(symbol_source(value.to_s), nil)
312
+ end
313
+
314
+ def styles_lookup(name)
315
+ name = name.to_s
316
+
317
+ Fragment.new(
318
+ "ClassNames[#{symbol_source(name)}]",
319
+ ARef(VarRef(Const("ClassNames")), Args([symbol_node(name)]))
320
+ )
321
+ end
322
+
323
+ def class_name_lookup(name)
324
+ name = name.to_s
325
+
326
+ Fragment.new(
327
+ "ClassNames[#{symbol_source(name)}]",
328
+ ARef(VarRef(Const("ClassNames")), Args([symbol_node(name)]))
329
+ )
330
+ end
331
+
332
+ def class_names(values)
333
+ fragments = values.map { |value| expression_fragment(value) }
334
+
335
+ Fragment.new(
336
+ "ClassNames.class_name(#{fragments.map(&:source).join(", ")})",
337
+ CallNode(
338
+ constant_path("ClassNames"),
339
+ Period("."),
340
+ Ident("class_name"),
341
+ ArgParen(Args(fragments.map { |fragment| node_for(fragment) }))
342
+ )
343
+ )
344
+ end
345
+
346
+ def scoped_class_name(values)
347
+ fragments = values.map { |value| expression_fragment(value) }
348
+
349
+ Fragment.new(
350
+ "ClassNames.class_name(#{fragments.map(&:source).join(", ")})",
351
+ nil
352
+ )
353
+ end
354
+
355
+ def parenthesized_expression(source, line_no: nil)
356
+ source = rewrite_ruby_source(source, line_no)
357
+ node = parse_expression(source, context: :parenthesized_expression)
358
+ return expression("(#{source})") unless node
359
+
360
+ fragment(Paren(LParen("("), Statements([node])))
361
+ end
362
+
363
+ def hash_expression(source, line_no: nil)
364
+ source = rewrite_ruby_source(source, line_no)
365
+ node = parse_expression(source, context: :hash_expression)
366
+ return nil unless node.is_a?(SyntaxTree::HashLiteral)
367
+
368
+ Fragment.new(source, node)
369
+ end
370
+
371
+ def class_skeleton_fragment(component_class_name, component_base_class)
372
+ Fragment.new(
373
+ "",
374
+ ClassDeclaration(
375
+ constant_path(component_class_name, declaration: true),
376
+ constant_path(component_base_class),
377
+ BodyStmt(
378
+ Statements([]),
379
+ nil,
380
+ nil,
381
+ nil,
382
+ nil
383
+ )
384
+ )
385
+ )
386
+ end
387
+
388
+ def constant_path(value, declaration: false)
389
+ parts = to_source(value).split("::")
390
+ raise ArgumentError, "Expected constant path: #{to_source(value).inspect}" if parts.empty? || parts.any?(&:empty?)
391
+
392
+ return ConstRef(Const(parts.fetch(0))) if declaration && parts.length == 1
393
+ return VarRef(Const(parts.fetch(0))) if parts.length == 1
394
+
395
+ parts.drop(1).reduce(VarRef(Const(parts.fetch(0)))) do |parent, part|
396
+ ConstPathRef(parent, Const(part))
397
+ end
398
+ end
399
+
400
+ def source_mark(line_no, _source)
401
+ "# #{Runtime::SourceMap::MARK_PREFIX}:#{line_no}"
402
+ end
403
+
404
+ def marked_expression(mark, expression)
405
+ source = to_source(expression)
406
+
407
+ source_marked_fragment(mark, source, node_for(expression))
408
+ end
409
+
410
+ def factory_call(factory:, tag:, children:, props:, mark: nil)
411
+ source_factory_call(factory: factory, tag: tag, children: children, props: props, mark: mark)
412
+ end
413
+
414
+ def slot_call(name:, fallback:)
415
+ arguments = ["self", name ? argument_source(name) : "nil"]
416
+ arguments << argument_source(fallback) if fallback
417
+ expression("HamlHelper.render_slot(#{arguments.join(", ")})")
418
+ end
419
+
420
+ def freeze_static(value)
421
+ expression("HamlHelper.freeze_static(#{argument_source(value)})")
422
+ end
423
+
424
+ def script_block(source, body, line_no: nil)
425
+ source = rewrite_ruby_source(source, nil)
426
+ ast_script_block(source, body) || raise_ruby_parse_error(source, line_no: line_no, context: "Could not build Ruby block from Haml script")
427
+ end
428
+
429
+ def silent_script_block(source, body, line_no: nil)
430
+ source = rewrite_ruby_source(source, nil)
431
+ ast_silent_script_block(source, body) || raise_ruby_parse_error(source, line_no: line_no, context: "Could not build Ruby block from Haml script")
432
+ end
433
+
434
+ def silent_script(source)
435
+ source = rewrite_ruby_source(source, nil)
436
+ ast_silent_script(source) || raise(ArgumentError, "Could not build Ruby begin block from Haml script: #{source.inspect}")
437
+ end
438
+
439
+ def branches(branches)
440
+ ast_branches(branches) || raise(ArgumentError, "Could not build Ruby branch from Haml scripts: #{branches.map(&:first).inspect}")
441
+ end
442
+
443
+ def silent_branches(branches)
444
+ ast_silent_branches(branches) || raise(ArgumentError, "Could not build Ruby branch from Haml scripts: #{branches.map(&:first).inspect}")
445
+ end
446
+
447
+ def ruby_filters(nodes)
448
+ return "" if nodes.empty?
449
+
450
+ ast_ruby_filters(nodes) || statements(nodes.map { |node| "begin\n#{indent(node, 2)}\nend" }.join("\n"))
451
+ end
452
+
453
+ def render_ruby_filter(node)
454
+ source = to_source(node)
455
+ parsed = (node if node.is_a?(Fragment) && node.node?) || statements(source)
456
+ return statements("begin\n#{indent(source, 2)}\n nil\nend") unless parsed
457
+
458
+ fragment(
459
+ ast_begin([
460
+ *statement_body_for(parsed),
461
+ nil_node
462
+ ])
463
+ )
464
+ rescue SyntaxTree::Parser::ParseError
465
+ statements("begin\n#{indent(source, 2)}\n nil\nend")
466
+ end
467
+
468
+ def format_node(node)
469
+ if @profiler
470
+ @profiler.measure(:haml_format_node, node: node.class.name) do
471
+ SyntaxTree::Formatter.format(+"", node, 0)
472
+ end
473
+ else
474
+ SyntaxTree::Formatter.format(+"", node, 0)
475
+ end
476
+ end
477
+
478
+ def indent(value, spaces)
479
+ to_source(value).lines.map { |line| "#{" " * spaces}#{line}" }.join
480
+ end
481
+
482
+ def compact_join(fragments)
483
+ Array(fragments)
484
+ .map { |fragment| to_source(fragment).to_s }
485
+ .reject(&:empty?)
486
+ .join("\n")
487
+ end
488
+
489
+ def line_rewritten_source(source, line_no)
490
+ rewrite_ruby_source(source, line_no)
491
+ end
492
+
493
+ def ruby_parse_error(source, line_no:, context:)
494
+ raise_ruby_parse_error(source, line_no: line_no, context: context)
495
+ end
496
+
497
+ def block_script?(source)
498
+ fixed_source = fix_syntax_by_adding_missing_pairs(source)
499
+ node = parse_expression(fixed_source, context: :block_script_predicate)
500
+
501
+ node.is_a?(SyntaxTree::MethodAddBlock)
502
+ end
503
+
504
+ private
505
+
506
+ def rewrite_ruby_source(source, line_no)
507
+ source = rewrite_line_constant(source, line_no)
508
+ rewrite_global_variables(source)
509
+ end
510
+
511
+ def rewrite_line_constant(source, line_no)
512
+ return source.to_s unless line_no
513
+
514
+ source = source.to_s
515
+ return source unless source.include?("__LINE__")
516
+
517
+ line_offsets = [0]
518
+ source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.length }
519
+
520
+ Ripper
521
+ .lex(source)
522
+ .select { |(_line, _column), type, token, _state| type == :on_kw && token == "__LINE__" }
523
+ .reverse_each
524
+ .each_with_object(source.dup) do |((line, column), _type, token, _state), rewritten|
525
+ offset = line_offsets.fetch(line - 1) + column
526
+ rewritten[offset, token.length] = line_no.to_s
527
+ end
528
+ end
529
+
530
+ def rewrite_global_variables(source)
531
+ return source unless @global_variables
532
+
533
+ line_offsets = [0]
534
+ source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.length }
535
+
536
+ Ripper
537
+ .lex(source)
538
+ .select { |(_line, _column), type, token, _state| type == :on_gvar && prop_global_variable?(token) }
539
+ .reverse_each
540
+ .each_with_object(source.dup) do |((line, column), _type, token, _state), rewritten|
541
+ name = token.delete_prefix("$")
542
+ offset = line_offsets.fetch(line - 1) + column
543
+ rewritten[offset, token.length] = "#{@global_variables}[#{symbol_source(name)}]"
544
+ end
545
+ end
546
+
547
+ def prop_global_variable?(token)
548
+ token.match?(/\A\$[a-z]\w*\z/)
549
+ end
550
+
551
+ def source_expressions(expressions)
552
+ case expressions.length
553
+ when 0
554
+ nil_expression
555
+ when 1
556
+ expression = expressions.fetch(0)
557
+
558
+ expression.is_a?(Fragment) ? expression : self.expression(to_source(expression))
559
+ else
560
+ Fragment.new("[#{expressions.map { |item| argument_source(item) }.join(", ")}]", nil)
561
+ end
562
+ end
563
+
564
+ def source_factory_call(factory:, tag:, children:, props:, mark:)
565
+ factory = expression_fragment(factory)
566
+ tag = expression_fragment(tag)
567
+ children = children.map { |child| expression_fragment(child) }
568
+
569
+ source_parts = [
570
+ to_source(tag),
571
+ *children.map { |child| argument_source(child) },
572
+ *keyword_props_source(props, mark: mark)
573
+ ].compact
574
+ Fragment.new("#{to_source(factory)}[#{source_parts.join(", ")}]", nil)
575
+ end
576
+
577
+ def ast_silent_script(source)
578
+ statements = parse_statements(source)
579
+ return nil unless statements
580
+
581
+ node = ast_begin([*statement_body_for(statements), nil_node])
582
+
583
+ Fragment.new(["begin", indent(source, 2), " nil", "end"].join("\n"), node)
584
+ end
585
+
586
+ def ast_script_block(source, body)
587
+ node = block_script_node(source, body)
588
+ return nil unless node
589
+
590
+ Fragment.new(block_source(source, body), node)
591
+ end
592
+
593
+ def ast_silent_script_block(source, body)
594
+ node = block_script_node(source, body)
595
+ return nil unless node
596
+
597
+ node = ast_begin([node, nil_node])
598
+
599
+ Fragment.new(["begin", indent(block_source(source, body), 2), " nil", "end"].join("\n"), node)
600
+ end
601
+
602
+ def ast_branches(branches)
603
+ node = branch_node(branches)
604
+ return nil unless node
605
+
606
+ Fragment.new(branch_source(branches), node)
607
+ end
608
+
609
+ def ast_silent_branches(branches)
610
+ node = branch_node(branches)
611
+ return nil unless node
612
+
613
+ node = ast_begin([node, nil_node])
614
+
615
+ Fragment.new(["begin", indent(branch_source(branches), 2), " nil", "end"].join("\n"), node)
616
+ end
617
+
618
+ def ast_ruby_filters(nodes)
619
+ begins =
620
+ nodes.map do |node|
621
+ statements =
622
+ if node.is_a?(Fragment)
623
+ node
624
+ else
625
+ parse_statements(node)
626
+ end
627
+ return nil unless statements
628
+
629
+ ast_begin(statement_body_for(statements))
630
+ end
631
+
632
+ source = nodes.map { |node| ["begin", indent(to_source(node), 2), "end"].join("\n") }.join("\n")
633
+
634
+ Fragment.new(source, Statements(begins))
635
+ end
636
+
637
+ def ast_begin(statement_nodes)
638
+ Begin(
639
+ BodyStmt(
640
+ Statements(statement_nodes),
641
+ nil,
642
+ nil,
643
+ nil,
644
+ nil
645
+ )
646
+ )
647
+ end
648
+
649
+ def block_source(source, body)
650
+ body_source = to_source(body)
651
+
652
+ if source.include?("{")
653
+ "#{source} #{body_source} }"
654
+ else
655
+ [source, indent(body_source, 2), "end"].join("\n")
656
+ end
657
+ end
658
+
659
+ def branch_source(branches)
660
+ body =
661
+ branches
662
+ .map do |source, body|
663
+ if source == "else"
664
+ ["else", indent(to_source(body), 2)].join("\n")
665
+ else
666
+ [source, indent(to_source(body), 2)].join("\n")
667
+ end
668
+ end
669
+ .join("\n")
670
+
671
+ "#{body}\nend"
672
+ end
673
+
674
+ def body_statement(body)
675
+ BodyStmt(
676
+ Statements(Array(body).flat_map { |statement| statement_body_for(statement) }),
677
+ nil,
678
+ nil,
679
+ nil,
680
+ nil
681
+ )
682
+ end
683
+
684
+ def branch_node(branches)
685
+ first_source, first_body = branches.fetch(0)
686
+ if first_source.match?(/\Acase\b/) && nil_fragment?(first_body)
687
+ case_node(first_source, branches.drop(1))
688
+ else
689
+ if_node(branches)
690
+ end
691
+ end
692
+
693
+ def if_node(branches)
694
+ source, body = branches.fetch(0)
695
+ return else_node(source, body) if source == "else"
696
+
697
+ predicate_source =
698
+ case source
699
+ when /\Aif\s+(.+)\z/ then $1
700
+ when /\Aelsif\s+(.+)\z/ then $1
701
+ else return nil
702
+ end
703
+ predicate = parse_expression(predicate_source, context: :branch_predicate)
704
+ return nil unless predicate
705
+
706
+ consequent =
707
+ if branches.length > 1
708
+ if_node(branches.drop(1))
709
+ end
710
+
711
+ if source.start_with?("elsif")
712
+ Elsif(predicate, Statements(statement_body_for(body)), consequent)
713
+ else
714
+ IfNode(predicate, Statements(statement_body_for(body)), consequent)
715
+ end
716
+ end
717
+
718
+ def else_node(source, body)
719
+ return nil unless source == "else"
720
+
721
+ Else(Kw("else"), Statements(statement_body_for(body)))
722
+ end
723
+
724
+ def case_node(source, branches)
725
+ value_source = source[/\Acase\s*(.*)\z/, 1]
726
+ return nil unless value_source
727
+
728
+ value = value_source.empty? ? nil : parse_expression(value_source, context: :case_value)
729
+ consequent = when_node(branches)
730
+ return nil unless consequent
731
+
732
+ Case(Kw("case"), value, consequent)
733
+ end
734
+
735
+ def when_node(branches)
736
+ source, body = branches.fetch(0)
737
+ return else_node(source, body) if source == "else"
738
+ return nil unless source.start_with?("when ")
739
+
740
+ arguments =
741
+ source
742
+ .delete_prefix("when ")
743
+ .split(",")
744
+ .map { |argument| parse_expression(argument.strip, context: :when_argument) }
745
+ return nil if arguments.any?(&:nil?)
746
+
747
+ consequent =
748
+ if branches.length > 1
749
+ when_node(branches.drop(1))
750
+ end
751
+
752
+ When(Args(arguments), Statements(statement_body_for(body)), consequent)
753
+ end
754
+
755
+ def block_script_node(source, body)
756
+ node = parse_expression(fix_syntax_by_adding_missing_pairs(source), context: :block_script)
757
+ return nil unless node.is_a?(SyntaxTree::MethodAddBlock)
758
+
759
+ MethodAddBlock(
760
+ node.call,
761
+ SyntaxTree::BlockNode.new(
762
+ opening: node.block.opening,
763
+ block_var: node.block.block_var,
764
+ bodystmt: block_body_for(node.block, body),
765
+ location: node.block.location
766
+ )
767
+ )
768
+ end
769
+
770
+ def block_body_for(block, body)
771
+ statements = Statements(Array(body).flat_map { |statement| statement_body_for(statement) })
772
+
773
+ block.opening.is_a?(SyntaxTree::LBrace) ? statements : body_statement(body)
774
+ end
775
+
776
+ def ast_keyword_props(props, mark:)
777
+ return nil if props.empty?
778
+
779
+ AssocSplat(
780
+ HashLiteral(
781
+ LBrace("{"),
782
+ props.map { |name, value| Assoc(prop_key_node(name), argument_node(value, mark: mark)) }
783
+ )
784
+ )
785
+ end
786
+
787
+ def keyword_props_source(props, mark:)
788
+ return [] if props.empty?
789
+
790
+ props.map do |name, value|
791
+ "#{prop_key_source(name)} #{argument_source(value, mark: mark)}"
792
+ end
793
+ end
794
+
795
+ def prop_key_source(name)
796
+ name = name.to_s
797
+ return "#{name}:" if name.match?(/\A[a-zA-Z_]\w*\z/)
798
+
799
+ "#{symbol_source(name)} =>"
800
+ end
801
+
802
+ def frozen_literal_node(value)
803
+ case value
804
+ when Hash
805
+ freeze_node(
806
+ HashLiteral(
807
+ LBrace("{"),
808
+ value.map { |key, child| Assoc(literal_node(key), frozen_literal_node(child)) }
809
+ )
810
+ )
811
+ when Array
812
+ freeze_node(ArrayLiteral(LBracket("["), Args(value.map { |child| frozen_literal_node(child) })))
813
+ else
814
+ literal_node(value)
815
+ end
816
+ end
817
+
818
+ def literal_node(value)
819
+ case value
820
+ when String
821
+ if value.match?(/\\|#[@${]/)
822
+ expression_node(value.inspect)
823
+ else
824
+ StringLiteral([TStringContent(value)], "\"")
825
+ end
826
+ when Integer
827
+ Int(value.to_s)
828
+ when Float
829
+ FloatLiteral(value.to_s)
830
+ when true
831
+ VarRef(Kw("true"))
832
+ when false
833
+ VarRef(Kw("false"))
834
+ when nil
835
+ nil_node
836
+ else
837
+ expression_node(value.inspect)
838
+ end
839
+ end
840
+
841
+ def literal_source(value)
842
+ case value
843
+ when String
844
+ value.inspect
845
+ when Integer, Float
846
+ value.to_s
847
+ when true
848
+ "true"
849
+ when false
850
+ "false"
851
+ when nil
852
+ "nil"
853
+ else
854
+ value.inspect
855
+ end
856
+ end
857
+
858
+ def freeze_node(node)
859
+ CallNode(node, Period("."), Ident("freeze"), nil)
860
+ end
861
+
862
+ def nil_node
863
+ VarRef(Kw("nil"))
864
+ end
865
+
866
+ def symbol_node(value)
867
+ if value.match?(/\A[a-zA-Z_]\w*[!?=]?\z/)
868
+ SymbolLiteral(Ident(value))
869
+ else
870
+ DynaSymbol([TStringContent(value)], ":\"")
871
+ end
872
+ end
873
+
874
+ def symbol_source(value)
875
+ if value.match?(/\A[a-zA-Z_]\w*[!?=]?\z/)
876
+ ":#{value}"
877
+ else
878
+ ":#{value.inspect}"
879
+ end
880
+ end
881
+
882
+ def prop_key_node(name)
883
+ name = name.to_s
884
+ return Label("#{name}:") if name.match?(/\A[a-zA-Z_]\w*\z/)
885
+
886
+ symbol_node(name)
887
+ end
888
+
889
+ def source_marked_fragment(mark, source, node)
890
+ marked_source = "#{mark}\n#{source}"
891
+ return Fragment.new(marked_source, nil) unless node
892
+
893
+ node = Statements([comment_node(mark), node])
894
+
895
+ Fragment.new(marked_source, node)
896
+ end
897
+
898
+ def comment_node(value)
899
+ Comment(value, false)
900
+ end
901
+
902
+ def nil_fragment?(value)
903
+ value.is_a?(Fragment) && value.node.is_a?(SyntaxTree::VarRef) && to_source(value) == "nil"
904
+ end
905
+
906
+ def node_for(value)
907
+ return value.node if value.is_a?(Fragment)
908
+
909
+ parse_expression(to_source(value), context: :node_for)
910
+ end
911
+
912
+ def argument_node(value, mark: nil)
913
+ fragment = expression_fragment(value)
914
+ statements = []
915
+ statements << comment_node(mark) if mark
916
+
917
+ if fragment.node.is_a?(SyntaxTree::Statements)
918
+ statements.concat(fragment.node.body)
919
+ elsif mark
920
+ node = node_for(fragment)
921
+ return nil unless node
922
+
923
+ statements << node
924
+ else
925
+ return node_for(fragment)
926
+ end
927
+
928
+ ast_begin(statements)
929
+ end
930
+
931
+ def argument_source(value, mark: nil)
932
+ fragment = expression_fragment(value)
933
+ source = to_source(fragment)
934
+
935
+ if mark
936
+ source = "#{mark}\n#{source}"
937
+ end
938
+
939
+ if fragment.node.is_a?(SyntaxTree::Statements) || mark || (source.include?("\n") && !multiline_argument_expression?(source))
940
+ ["begin", indent(source, 2), "end"].join("\n")
941
+ else
942
+ source
943
+ end
944
+ end
945
+
946
+ def multiline_argument_expression?(source)
947
+ source.start_with?("if ", "unless ", "case", "begin")
948
+ end
949
+
950
+ def statement_body_for(value)
951
+ return value.statement_body if value.is_a?(Fragment)
952
+
953
+ value.is_a?(SyntaxTree::Statements) ? value.body : [value].compact
954
+ end
955
+
956
+ def expression_node(source)
957
+ source = source.to_s
958
+ return constant_path(source) if source.match?(VALID_CONST_PATH)
959
+
960
+ parse_expression(source, context: :expression_node) || raise(ArgumentError, "Could not parse Ruby expression: #{source.inspect}")
961
+ end
962
+
963
+ def parse_expression(source, context:)
964
+ cached_parse(@expression_cache, source, :"haml_parse_expression:#{context}") do
965
+ SyntaxTree
966
+ .parse(source)
967
+ &.statements
968
+ &.body
969
+ &.find { |node| !node.instance_of?(SyntaxTree::Comment) }
970
+ end
971
+ rescue SyntaxTree::Parser::ParseError
972
+ nil
973
+ end
974
+
975
+ def parse_statements(source)
976
+ cached_parse(@statements_cache, source, :haml_parse_statements) { SyntaxTree.parse(source)&.statements }
977
+ rescue SyntaxTree::Parser::ParseError
978
+ nil
979
+ end
980
+
981
+ def parse_program(source)
982
+ cached_parse(@program_cache, source, :haml_parse_program) { SyntaxTree.parse(source) }
983
+ rescue SyntaxTree::Parser::ParseError
984
+ nil
985
+ end
986
+
987
+ def cached_parse(cache, source, event_name)
988
+ source = source.to_s
989
+ return cache.fetch(source) if cache.key?(source)
990
+
991
+ cache[source] =
992
+ if @profiler
993
+ @profiler.measure(event_name) { yield }
994
+ else
995
+ yield
996
+ end
997
+ end
998
+
999
+ def fix_syntax_by_adding_missing_pairs(source)
1000
+ left_right = SyntaxSuggest::LeftRightLexCount.new
1001
+ SyntaxSuggest::LexAll.new(source: source).each { |lex| left_right.count_lex(lex) }
1002
+
1003
+ [source, *left_right.missing].join("\n")
1004
+ end
1005
+
1006
+ def raise_ruby_parse_error(source, line_no:, context:)
1007
+ parse_error = syntax_tree_parse_error(source)
1008
+ explain =
1009
+ SyntaxSuggest::ExplainSyntax.new(
1010
+ code_lines: SyntaxSuggest::CodeLine.from_source(source)
1011
+ ).call
1012
+ errors = explain.errors
1013
+ missing = explain.missing.map { |item| explain.why(item) } - errors
1014
+
1015
+ message = [context]
1016
+ message << "Errors:\n #{errors.join("\n ")}" unless errors.empty?
1017
+ message << "Missing:\n #{missing.join("\n ")}" unless missing.empty?
1018
+
1019
+ raise RubyParseError.new(message.join("\n\n"), line: source_line_for_parse_error(line_no, parse_error))
1020
+ end
1021
+
1022
+ def syntax_tree_parse_error(source)
1023
+ SyntaxTree.parse(source)
1024
+ nil
1025
+ rescue SyntaxTree::Parser::ParseError => error
1026
+ error
1027
+ end
1028
+
1029
+ def source_line_for_parse_error(line_no, parse_error)
1030
+ return line_no unless line_no && parse_error&.lineno
1031
+
1032
+ line_no + parse_error.lineno - 1
1033
+ end
1034
+ end
1035
+ end
1036
+ end
1037
+ end
1038
+ end
1039
+ end