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,766 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+ require "ripper"
5
+
6
+ require "klenod/runtime/source_map"
7
+ require_relative "../markdown_compiler"
8
+ require_relative "parser"
9
+
10
+ module Klenod
11
+ module Build
12
+ module Plugins
13
+ module HamlPlugin
14
+ class Transformer
15
+ VALID_CONST_PATH = /\A[A-Z]\w*(?:::[A-Z]\w*)*\z/
16
+
17
+ ConstPath = Data.define(:value) do
18
+ def self.parse(value, name:)
19
+ path = value.to_s
20
+ raise ArgumentError, "#{name} must be a Ruby constant path" unless path.match?(VALID_CONST_PATH)
21
+
22
+ new(path)
23
+ end
24
+
25
+ def to_s
26
+ value
27
+ end
28
+ end
29
+
30
+ require_relative "transformer/ruby_builder"
31
+ def call(
32
+ source:,
33
+ module_id:,
34
+ component_class_name:,
35
+ component_base_class:,
36
+ factory:,
37
+ styles_source:,
38
+ translations_source:,
39
+ i18n_source: nil,
40
+ styleable: false,
41
+ profiler: nil,
42
+ import_rewriter: nil,
43
+ markdown_components_source: "{}",
44
+ global_variables: nil,
45
+ haml_helper_source: nil,
46
+ cache_static_subtrees: false
47
+ )
48
+ component_base_class = ConstPath.parse(component_base_class, name: "component_base_class")
49
+ factory = ConstPath.parse(factory, name: "factory")
50
+ builder = RubyBuilder.new(profiler: profiler, global_variables: global_variables)
51
+ haml_helper_source ||= builder.constant_assignment("HamlHelper", "Object") if styleable || cache_static_subtrees
52
+ previous_profiler = @profiler
53
+ previous_module_id = @module_id
54
+ previous_static_constants = @static_constants
55
+ previous_cache_static_subtrees = @cache_static_subtrees
56
+ @profiler = profiler
57
+ @module_id = module_id
58
+ @static_constants = []
59
+ @cache_static_subtrees = cache_static_subtrees
60
+ template =
61
+ if profiler
62
+ profiler.measure(:haml_compile_template, module_id: module_id.to_s) do
63
+ compile_template(
64
+ source,
65
+ module_id: module_id,
66
+ factory: factory,
67
+ styleable: styleable,
68
+ builder: builder,
69
+ import_rewriter: import_rewriter,
70
+ markdown_components_source: markdown_components_source
71
+ )
72
+ end
73
+ else
74
+ compile_template(
75
+ source,
76
+ module_id: module_id,
77
+ factory: factory,
78
+ styleable: styleable,
79
+ builder: builder,
80
+ import_rewriter: import_rewriter,
81
+ markdown_components_source: markdown_components_source
82
+ )
83
+ end
84
+ ast =
85
+ if profiler
86
+ profiler.measure(:haml_component_program, module_id: module_id.to_s) do
87
+ builder.component_program(
88
+ component_class_name: component_class_name,
89
+ component_base_class: component_base_class,
90
+ translations_source: translations_source,
91
+ ruby_source: template.ruby,
92
+ render_source: template.render,
93
+ styles_source: styles_source,
94
+ haml_helper_source: haml_helper_source,
95
+ i18n_source: i18n_source,
96
+ static_constants: template.static_constants
97
+ )
98
+ end
99
+ else
100
+ builder.component_program(
101
+ component_class_name: component_class_name,
102
+ component_base_class: component_base_class,
103
+ translations_source: translations_source,
104
+ ruby_source: template.ruby,
105
+ render_source: template.render,
106
+ styles_source: styles_source,
107
+ haml_helper_source: haml_helper_source,
108
+ i18n_source: i18n_source,
109
+ static_constants: template.static_constants
110
+ )
111
+ end
112
+ if profiler
113
+ code = profiler.measure(:haml_generate_code, module_id: module_id.to_s) { ast.source }
114
+ source_map =
115
+ profiler.measure(:haml_source_map_parse, module_id: module_id.to_s) do
116
+ Runtime::SourceMap::SourceMap.parse(source, code)
117
+ end
118
+
119
+ HamlTransformResult.new(code, source_map, {source: source, module_id: module_id}, ast)
120
+ else
121
+ HamlTransformResult.from_ast(
122
+ ast,
123
+ source: source,
124
+ metadata: {source: source, module_id: module_id}
125
+ )
126
+ end
127
+ rescue RubyParseError => error
128
+ raise ParseError.new(error, source: source, module_id: module_id)
129
+ ensure
130
+ @profiler = previous_profiler
131
+ @module_id = previous_module_id
132
+ @static_constants = previous_static_constants
133
+ @cache_static_subtrees = previous_cache_static_subtrees
134
+ end
135
+
136
+ private
137
+
138
+ Template = Data.define(:ruby, :render, :static_constants)
139
+
140
+ def measure_compile(name)
141
+ return yield unless @profiler
142
+
143
+ @profiler.measure(name, module_id: @module_id.to_s) { yield }
144
+ end
145
+
146
+ def measure_compile_detail(name)
147
+ return yield unless @profiler&.category?(:haml_detail)
148
+
149
+ @profiler.measure(name, module_id: @module_id.to_s) { yield }
150
+ end
151
+
152
+ def compile_template(source, factory:, builder:, module_id: nil, styleable: false, import_rewriter: nil, markdown_components_source: "{}")
153
+ parsed = measure_compile(:haml_parse_haml) { HamlPlugin.parse_haml(source, module_id: module_id) }
154
+ render_nodes, ruby_nodes =
155
+ measure_compile(:haml_partition_top_level_nodes) do
156
+ partition_top_level_nodes(parsed.children)
157
+ end
158
+ ruby =
159
+ if ruby_nodes.empty?
160
+ ""
161
+ else
162
+ measure_compile(:haml_compile_ruby_filters) { compile_ruby_filters(ruby_nodes, source: source, builder: builder, import_rewriter: import_rewriter) }
163
+ end
164
+ markdown_compiler = MarkdownCompiler.new(factory: factory, components_source: markdown_components_source)
165
+ render =
166
+ measure_compile(:haml_compile_render_nodes) do
167
+ compile_nodes(render_nodes, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
168
+ end
169
+
170
+ Template.new(ruby, render, @static_constants)
171
+ end
172
+
173
+ def partition_top_level_nodes(nodes)
174
+ class_ruby_nodes = []
175
+ render_nodes = []
176
+ class_ruby_seen = false
177
+
178
+ nodes.each do |node|
179
+ if css_filter?(node)
180
+ next
181
+ elsif !class_ruby_seen && ruby_filter?(node)
182
+ class_ruby_seen = true
183
+ class_ruby_nodes << node
184
+ else
185
+ render_nodes << node
186
+ end
187
+ end
188
+
189
+ [render_nodes, class_ruby_nodes]
190
+ end
191
+
192
+ def compile_nodes(nodes, factory:, builder:, markdown_compiler:, styleable: false)
193
+ expressions =
194
+ measure_compile(:haml_compile_node_expressions) do
195
+ compile_node_expressions(nodes, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
196
+ end
197
+
198
+ measure_compile(:haml_build_expression_list) { builder.expressions(expressions) }
199
+ end
200
+
201
+ def compile_node_expressions(nodes, factory:, builder:, markdown_compiler:, styleable: false)
202
+ expressions = []
203
+ previous_node = nil
204
+ index = 0
205
+
206
+ while index < nodes.length
207
+ node = nodes[index]
208
+
209
+ if script_node?(node) && !continuation?(node)
210
+ group = [node]
211
+ index += 1
212
+
213
+ while index < nodes.length && continuation?(nodes[index])
214
+ group << nodes[index]
215
+ index += 1
216
+ end
217
+
218
+ expression = compile_script_group(group, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
219
+ else
220
+ expression = compile_node(node, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
221
+ index += 1
222
+ end
223
+
224
+ expressions << builder.literal(" ") if previous_node && whitespace_boundary_node?(node) && whitespace_between?(previous_node, node)
225
+ expressions << expression
226
+ previous_node = node if whitespace_boundary_node?(node)
227
+ end
228
+
229
+ expressions
230
+ end
231
+
232
+ def whitespace_boundary_node?(node)
233
+ !(node.type == :silent_script && node.children.empty?)
234
+ end
235
+
236
+ def whitespace_between?(left, right)
237
+ (right.type == :tag && right.value.fetch(:nuke_inner_whitespace)) ||
238
+ (left.type == :tag && left.value.fetch(:nuke_outer_whitespace))
239
+ end
240
+
241
+ def cache_static_subtree?(node, styleable:)
242
+ @cache_static_subtrees && static_subtree?(node, styleable: styleable)
243
+ end
244
+
245
+ def register_static_subtree(builder, expression)
246
+ name = "STATIC_SUBTREE_#{@static_constants.length}"
247
+ @static_constants << builder.constant_assignment(name, builder.freeze_static(expression))
248
+ builder.expression(name)
249
+ end
250
+
251
+ def static_subtree?(node, styleable:)
252
+ return false unless node.type == :tag
253
+ return false if styleable
254
+ return false if slot_tag?(node)
255
+ return false if constant_tag_name?(node.value.fetch(:name))
256
+ return false unless static_tag_value?(node)
257
+ return false unless static_tag_attributes?(node)
258
+
259
+ node.children.all? { |child| static_child_node?(child, styleable: styleable) }
260
+ end
261
+
262
+ def static_child_node?(node, styleable:)
263
+ case node.type
264
+ when :plain
265
+ true
266
+ when :tag
267
+ static_subtree?(node, styleable: styleable)
268
+ else
269
+ false
270
+ end
271
+ end
272
+
273
+ def static_tag_value?(node)
274
+ value = node.value.fetch(:value)
275
+ !node.value.fetch(:parse) || value.nil? || value.empty?
276
+ end
277
+
278
+ def static_tag_attributes?(node)
279
+ value = node.value
280
+ dynamic = value.fetch(:dynamic_attributes)
281
+ return false if dynamic.old || dynamic.new
282
+ return false if value.fetch(:object_ref).is_a?(String)
283
+ return false if value.fetch(:attributes).key?("class")
284
+
285
+ true
286
+ end
287
+
288
+ def compile_node(node, factory:, builder:, markdown_compiler:, styleable: false)
289
+ measure_compile_detail(:"haml_compile_node_#{node.type}") do
290
+ mark = source_mark(node, builder: builder)
291
+ expression =
292
+ case node.type
293
+ when :tag
294
+ if slot_tag?(node)
295
+ builder.marked_expression(mark, compile_slot(node, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler))
296
+ elsif cache_static_subtree?(node, styleable: styleable)
297
+ tag_expression = builder.marked_expression(mark, compile_tag(node, mark: mark, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler))
298
+ builder.marked_expression(mark, register_static_subtree(builder, tag_expression))
299
+ else
300
+ builder.marked_expression(mark, compile_tag(node, mark: mark, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler))
301
+ end
302
+ when :plain
303
+ builder.literal(node.value.fetch(:text))
304
+ when :script
305
+ compile_script(node, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
306
+ when :silent_script
307
+ compile_silent_script(node, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
308
+ when :filter
309
+ compile_filter_node(node, builder: builder, markdown_compiler: markdown_compiler)
310
+ end
311
+
312
+ (node.type == :tag) ? expression : builder.marked_expression(mark, expression)
313
+ end
314
+ end
315
+
316
+ def compile_script_group(nodes, factory:, builder:, markdown_compiler:, styleable: false)
317
+ return compile_script_branch(nodes[0], factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler) if nodes.length == 1 && branch_start?(nodes[0])
318
+ return compile_node(nodes[0], factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler) if nodes.length == 1
319
+
320
+ compile_branches(
321
+ nodes.map { |node| [script_source(node, builder: builder), node.children] },
322
+ factory: factory,
323
+ styleable: styleable,
324
+ builder: builder,
325
+ markdown_compiler: markdown_compiler
326
+ )
327
+ end
328
+
329
+ def compile_script(node, factory:, builder:, markdown_compiler:, styleable: false)
330
+ source = script_source(node, builder: builder)
331
+ return builder.parenthesized_expression(source) if node.children.empty?
332
+
333
+ builder.script_block(
334
+ source,
335
+ compile_nodes(node.children, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler),
336
+ line_no: node.line
337
+ )
338
+ end
339
+
340
+ def compile_script_branch(node, factory:, builder:, markdown_compiler:, styleable: false)
341
+ compile_branches(
342
+ [[script_source(node, builder: builder), node.children]],
343
+ factory: factory,
344
+ styleable: styleable,
345
+ builder: builder,
346
+ markdown_compiler: markdown_compiler
347
+ )
348
+ end
349
+
350
+ def compile_silent_script(node, factory:, builder:, markdown_compiler:, styleable: false)
351
+ source = script_source(node, builder: builder)
352
+ return builder.silent_script(source) if node.children.empty?
353
+ if builder.block_script?(source)
354
+ return builder.silent_script_block(
355
+ source,
356
+ compile_nodes(node.children, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler),
357
+ line_no: node.line
358
+ )
359
+ end
360
+
361
+ compile_silent_branches(
362
+ split_silent_script_branches(node, builder: builder),
363
+ factory: factory,
364
+ styleable: styleable,
365
+ builder: builder,
366
+ markdown_compiler: markdown_compiler
367
+ )
368
+ end
369
+
370
+ def compile_branches(branches, factory:, builder:, markdown_compiler:, styleable: false)
371
+ builder.branches(
372
+ branches.map do |source, children|
373
+ [source, compile_nodes(children, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)]
374
+ end
375
+ )
376
+ end
377
+
378
+ def compile_silent_branches(branches, factory:, builder:, markdown_compiler:, styleable: false)
379
+ builder.silent_branches(
380
+ branches.map do |source, children|
381
+ [source, compile_nodes(children, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)]
382
+ end
383
+ )
384
+ end
385
+
386
+ def split_silent_script_branches(node, builder:)
387
+ branches = []
388
+ current_source = script_source(node, builder: builder)
389
+ current_children = []
390
+
391
+ node.children.each do |child|
392
+ if continuation?(child)
393
+ branches << [current_source, current_children]
394
+ current_source = script_source(child, builder: builder)
395
+ current_children = child.children.dup
396
+ else
397
+ current_children << child
398
+ end
399
+ end
400
+
401
+ branches << [current_source, current_children]
402
+ end
403
+
404
+ def script_node?(node)
405
+ node.type == :script || node.type == :silent_script
406
+ end
407
+
408
+ def script_source(node, builder:)
409
+ builder.line_rewritten_source(node.value.fetch(:text).strip, node.line)
410
+ end
411
+
412
+ def continuation?(node)
413
+ script_node?(node) && %w[elsif else when in rescue ensure].include?(node.value.fetch(:keyword))
414
+ end
415
+
416
+ def branch_start?(node)
417
+ node.type == :script && %w[if unless case begin].include?(node.value.fetch(:keyword))
418
+ end
419
+
420
+ def compile_tag(node, mark:, factory:, builder:, markdown_compiler:, styleable: false)
421
+ children = []
422
+ measure_compile_detail(:haml_compile_tag_value) do
423
+ value = node.value.fetch(:value)
424
+ if value && !value.empty?
425
+ children << (node.value.fetch(:parse) ? builder.parenthesized_expression(value, line_no: node.line) : builder.literal(value))
426
+ end
427
+ end
428
+ unless node.children.empty?
429
+ children.concat(
430
+ measure_compile_detail(:haml_compile_tag_children) do
431
+ compile_node_expressions(node.children, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
432
+ end
433
+ )
434
+ end
435
+
436
+ measure_compile(:haml_compile_factory_call) { compile_factory_call(node, children, mark: mark, factory: factory, styleable: styleable, builder: builder) }
437
+ end
438
+
439
+ def slot_tag?(node)
440
+ node.value.fetch(:name) == "slot"
441
+ end
442
+
443
+ def compile_slot(node, factory:, builder:, markdown_compiler:, styleable: false)
444
+ props = attributes(node, styleable: false, builder: builder)
445
+ name = props.delete(:name)
446
+ fallback =
447
+ if node.children.empty?
448
+ nil
449
+ else
450
+ compile_nodes(node.children, factory: factory, styleable: styleable, builder: builder, markdown_compiler: markdown_compiler)
451
+ end
452
+ builder.slot_call(name: name, fallback: fallback)
453
+ end
454
+
455
+ def compile_factory_call(node, children, mark:, factory:, builder:, styleable: false)
456
+ tag = measure_compile_detail(:haml_compile_tag_name) { compile_tag_name(node, builder: builder) }
457
+ props = measure_compile_detail(:haml_compile_tag_attributes) { attributes(node, styleable: styleable, builder: builder) }
458
+
459
+ builder.factory_call(
460
+ factory: factory,
461
+ tag: tag,
462
+ children: children,
463
+ props: props,
464
+ mark: mark
465
+ )
466
+ end
467
+
468
+ def compile_tag_name(node, builder:)
469
+ tag_name = node.value.fetch(:name)
470
+ constant_tag_name?(tag_name) ? builder.expression(tag_name) : builder.symbol_fragment(tag_name)
471
+ end
472
+
473
+ def attributes(node, builder:, styleable: false)
474
+ value = node.value
475
+ static = value.fetch(:attributes)
476
+ dynamic = value.fetch(:dynamic_attributes)
477
+ object_ref = value.fetch(:object_ref)
478
+ return {} if !styleable && static.empty? && !dynamic.old && !dynamic.new && !object_ref.is_a?(String)
479
+
480
+ measure_compile(:haml_compile_attributes) do
481
+ props = {}
482
+ static_attributes(node, builder: builder, props: props)
483
+ dynamic = dynamic_attributes(node, builder: builder, props: props)
484
+ object_ref_attributes(node, builder: builder, props: props)
485
+ class_attributes(node, dynamic_attributes: dynamic, builder: builder, props: props, styleable: styleable)
486
+ props
487
+ end
488
+ end
489
+
490
+ def compile_ruby_filters(nodes, source:, builder:, import_rewriter: nil)
491
+ builder.ruby_filters(nodes.map { |node| compile_ruby_filter(node, source: source, builder: builder, import_rewriter: import_rewriter) })
492
+ end
493
+
494
+ def compile_ruby_filter(node, builder:, source: "", import_rewriter: nil)
495
+ text = node.value.fetch(:text)
496
+ if import_rewriter && text.include?("import")
497
+ source_column_offset = source.lines.fetch(node.line, "").match(/\A\s*/).to_s.length
498
+ text = import_rewriter.call(text, source_line_offset: node.line, source_column_offset: source_column_offset)
499
+ end
500
+ heredoc_body_lines = ruby_heredoc_body_lines(text)
501
+ source = +""
502
+ text.each_line.with_index(node.line + 1) do |line, line_no|
503
+ source << "\n" unless source.empty?
504
+ relative_line_no = line_no - node.line
505
+
506
+ if heredoc_body_lines.key?(relative_line_no)
507
+ source << line.chomp
508
+ else
509
+ source << builder.source_mark(line_no, nil)
510
+ source << "\n"
511
+ source << builder.line_rewritten_source(line.chomp, line_no).chomp
512
+ end
513
+ end
514
+
515
+ builder.node_fragment(source, nil)
516
+ end
517
+
518
+ def ruby_heredoc_body_lines(source)
519
+ starts = []
520
+ lines = {}
521
+
522
+ ::Ripper.lex(source).each do |(line_no, _column), token, _text, _state|
523
+ case token
524
+ when :on_heredoc_beg
525
+ starts << line_no
526
+ when :on_heredoc_end
527
+ start_line = starts.shift
528
+ next unless start_line
529
+
530
+ ((start_line + 1)..line_no).each { |body_line| lines[body_line] = true }
531
+ end
532
+ end
533
+
534
+ lines
535
+ end
536
+
537
+ def compile_filter_node(node, builder:, markdown_compiler:)
538
+ return builder.render_ruby_filter(compile_ruby_filter(node, builder: builder)) if ruby_filter?(node)
539
+ return builder.expression(markdown_compiler.compile(node.value.fetch(:text), interpolate: true)) if markdown_filter?(node)
540
+
541
+ raise ArgumentError, "Only :ruby and :markdown Haml filters are supported"
542
+ end
543
+
544
+ def ruby_filter?(node)
545
+ node.type == :filter && node.value.fetch(:name) == "ruby"
546
+ end
547
+
548
+ def markdown_filter?(node)
549
+ node.type == :filter && node.value.fetch(:name) == "markdown"
550
+ end
551
+
552
+ def css_filter?(node)
553
+ node.type == :filter && node.value.fetch(:name) == "css"
554
+ end
555
+
556
+ def static_attributes(node, builder:, props:)
557
+ attributes = node.value.fetch(:attributes)
558
+ return if attributes.empty? || (attributes.length == 1 && attributes.key?("class"))
559
+
560
+ measure_compile(:haml_compile_static_attributes) do
561
+ attributes.each do |key, value|
562
+ next if key == "class"
563
+
564
+ props[key.to_sym] = builder.literal(value)
565
+ end
566
+ end
567
+ end
568
+
569
+ def dynamic_attributes(node, builder:, props:)
570
+ dynamic_attributes = node.value.fetch(:dynamic_attributes)
571
+ source = dynamic_attributes.old || dynamic_attributes.new
572
+ return {} unless source
573
+
574
+ measure_compile(:haml_compile_dynamic_attributes) do
575
+ source = builder.line_rewritten_source(source, node.line)
576
+ simple = simple_dynamic_attributes(source, builder: builder)
577
+ if simple
578
+ simple.each { |key, value| props[key] = value }
579
+ return simple
580
+ end
581
+
582
+ hash = builder.hash_expression(source, line_no: node.line)
583
+ unless hash
584
+ builder.ruby_parse_error(source, line_no: node.line, context: "Could not parse Haml dynamic attributes")
585
+ end
586
+
587
+ dynamic = {}
588
+ hash.node.assocs.each do |assoc|
589
+ key = attribute_key(assoc.key, builder: builder)
590
+ value = attribute_value(assoc, source, builder: builder)
591
+ dynamic[key] = value
592
+ props[key] = value
593
+ end
594
+ dynamic
595
+ end
596
+ end
597
+
598
+ def simple_dynamic_attributes(source, builder:)
599
+ source = source.strip
600
+ return nil unless source.start_with?("{") && source.end_with?("}")
601
+
602
+ pairs = split_simple_attribute_pairs(source[1...-1].strip)
603
+ return nil unless pairs
604
+
605
+ pairs.to_h do |pair|
606
+ match = pair.match(/\A([a-zA-Z_]\w*):\s*(.+)\z/m)
607
+ return nil unless match
608
+
609
+ value = match[2].strip
610
+ return nil if value.empty?
611
+
612
+ fragment = builder.expression(value)
613
+ return nil unless fragment.node
614
+
615
+ [match[1].to_sym, fragment]
616
+ end
617
+ end
618
+
619
+ def split_simple_attribute_pairs(source)
620
+ return [] if source.empty?
621
+
622
+ pairs = []
623
+ start = 0
624
+ quote = nil
625
+ escaped = false
626
+
627
+ source.each_byte.with_index do |byte, index|
628
+ if quote
629
+ if escaped
630
+ escaped = false
631
+ elsif byte == 92 # \
632
+ escaped = true
633
+ elsif byte == quote
634
+ quote = nil
635
+ end
636
+ next
637
+ end
638
+
639
+ case byte
640
+ when 34, 39 # " '
641
+ quote = byte
642
+ when 40, 41, 91, 93, 123, 125 # ( ) [ ] { }
643
+ return nil
644
+ when 44 # ,
645
+ pairs << source[start...index].strip
646
+ start = index + 1
647
+ end
648
+ end
649
+
650
+ return nil if quote
651
+
652
+ pairs << source[start..].strip
653
+ pairs
654
+ end
655
+
656
+ def class_attributes(node, dynamic_attributes:, builder:, props:, styleable: false)
657
+ attributes = node.value.fetch(:attributes)
658
+ static_class_source = attributes.fetch("class", "")
659
+ dynamic_class = dynamic_attributes[:class]
660
+ return unless styleable || !static_class_source.empty? || dynamic_class
661
+
662
+ measure_compile(:haml_compile_class_attributes) do
663
+ class_metadata = node.value.fetch(:klenod_class_metadata, {})
664
+ literal_static_classes = class_metadata.fetch(:literal, [])
665
+ scoped_static_classes = class_metadata.fetch(:shorthand, static_class_source.split)
666
+ class_values = [
667
+ tag_class_symbol(node, builder: builder),
668
+ *scoped_static_classes.map { |class_name| builder.symbol(class_name) },
669
+ *literal_static_classes.map { |class_name| builder.literal(class_name) },
670
+ dynamic_class
671
+ ].compact
672
+ unless class_values.any?
673
+ props[:class] = dynamic_class if dynamic_class
674
+ return
675
+ end
676
+
677
+ props[:class] = builder.scoped_class_name(class_values)
678
+ end
679
+ end
680
+
681
+ def tag_class_symbol(node, builder:)
682
+ tag_name = node.value.fetch(:name)
683
+ return nil if constant_tag_name?(tag_name)
684
+
685
+ builder.symbol("__#{tag_name}")
686
+ end
687
+
688
+ def constant_tag_name?(tag_name)
689
+ first = tag_name.getbyte(0)
690
+ first && first >= 65 && first <= 90
691
+ end
692
+
693
+ def object_ref_attributes(node, builder:, props:)
694
+ source = node.value.fetch(:object_ref)
695
+ return unless source.is_a?(String)
696
+
697
+ measure_compile(:haml_compile_object_ref_attributes) do
698
+ expression = builder.expression(source, line_no: node.line)
699
+ key =
700
+ if expression.node.is_a?(SyntaxTree::ArrayLiteral) && expression.node.contents&.parts&.length == 1
701
+ builder.fragment(expression.node.contents.parts.fetch(0))
702
+ else
703
+ expression
704
+ end
705
+
706
+ props[:key] = key
707
+ end
708
+ end
709
+
710
+ def node_source(source, node)
711
+ location = node.location
712
+ return source unless location
713
+
714
+ source[location.start_char...location.end_char]
715
+ end
716
+
717
+ def attribute_value(assoc, source, builder:)
718
+ value = assoc.value
719
+ return builder.node_fragment(node_source(source, value), value) if value
720
+
721
+ key = omitted_attribute_value_name(assoc.key)
722
+ return builder.expression(key) if key
723
+
724
+ builder.ruby_parse_error(source, line_no: assoc.key.location&.start_line, context: "Could not parse Haml dynamic attributes")
725
+ end
726
+
727
+ def omitted_attribute_value_name(node)
728
+ case node
729
+ when SyntaxTree::Label
730
+ name = node.value.delete_suffix(":")
731
+ name if name.match?(/\A[a-zA-Z_]\w*\z/)
732
+ end
733
+ end
734
+
735
+ def attribute_key(node, builder:)
736
+ case node
737
+ when SyntaxTree::Label
738
+ node.value.delete_suffix(":").to_sym
739
+ when SyntaxTree::DynaSymbol
740
+ static_dyna_symbol_value(node)&.to_sym || builder.fragment(node).source.to_sym
741
+ when SyntaxTree::StringLiteral
742
+ node.parts.map(&:value).join.to_sym
743
+ else
744
+ builder.fragment(node).source.to_sym
745
+ end
746
+ end
747
+
748
+ def static_dyna_symbol_value(node)
749
+ values =
750
+ node.parts.map do |part|
751
+ return nil unless part.is_a?(SyntaxTree::TStringContent)
752
+
753
+ part.value
754
+ end
755
+
756
+ values.join
757
+ end
758
+
759
+ def source_mark(node, builder:)
760
+ builder.source_mark(node.line, nil)
761
+ end
762
+ end
763
+ end
764
+ end
765
+ end
766
+ end