papercraft 1.4 → 2.14

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.
@@ -1,263 +1,737 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi'
4
- require 'escape_utils'
5
3
  require 'sirop'
4
+ require 'erb/escape'
6
5
 
7
- class Papercraft::Compiler < Sirop::Sourcifier
8
- module AuxMethods
9
- def format_html_attr(tag)
10
- tag.to_s.tr('_', '-')
6
+ require_relative './compiler/nodes'
7
+ require_relative './compiler/tag_translator'
8
+
9
+ module Papercraft
10
+ # A Compiler converts a template into an optimized form that generates HTML
11
+ # efficiently.
12
+ class Compiler < Sirop::Sourcifier
13
+ @@html_debug_attribute_injector = nil
14
+
15
+ def self.html_debug_attribute_injector=(proc)
16
+ @@html_debug_attribute_injector = proc
17
+ end
18
+
19
+ # Compiles the given proc, returning the generated source map and the
20
+ # generated optimized source code.
21
+ #
22
+ # @param proc [Proc] template
23
+ # @param mode [Symbol] compilation mode (:html, :xml)
24
+ # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
25
+ # @return [Array] array containing the source map and generated code
26
+ def self.compile_to_code(proc, mode: :html, wrap: true)
27
+ ast = Sirop.to_ast(proc)
28
+
29
+ # adjust ast root if proc is defined with proc {} / lambda {} syntax
30
+ ast = ast.block if ast.is_a?(Prism::CallNode)
31
+
32
+ compiler = new(mode:).with_source_map(proc, ast)
33
+ transformed_ast = TagTranslator.transform(ast.body, ast)
34
+ compiler.format_compiled_template(transformed_ast, ast, wrap:, binding: proc.binding)
35
+ [compiler.source_map, compiler.buffer]
36
+ end
37
+
38
+ # Compiles the given template into an optimized Proc that generates HTML.
39
+ #
40
+ # template = -> {
41
+ # h1 'Hello, world!'
42
+ # }
43
+ # compiled = Papercraft::Compiler.compile(template)
44
+ # compiled.render #=> '<h1>Hello, world!'
45
+ #
46
+ # @param proc [Proc] template
47
+ # @param mode [Symbol] compilation mode (:html, :xml)
48
+ # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
49
+ # @return [Proc] compiled proc
50
+ def self.compile(proc, mode: :html, wrap: true)
51
+ source_map, code = compile_to_code(proc, mode:, wrap:)
52
+ if ENV['DEBUG'] == '1'
53
+ puts '*' * 40
54
+ puts code
55
+ end
56
+ eval(code, proc.binding, source_map[:compiled_fn])
57
+ end
58
+
59
+ def self.source_map_store
60
+ @source__map_store ||= {}
61
+ end
62
+
63
+ def self.store_source_map(source_map)
64
+ return if !source_map
65
+
66
+ fn = source_map[:compiled_fn]
67
+ source_map_store[fn] = source_map
68
+ end
69
+
70
+ def self.source_location_to_fn(source_location)
71
+ "::(#{source_location.join(':')})"
72
+ end
73
+
74
+ attr_reader :source_map
75
+
76
+ # Initializes a compiler.
77
+ def initialize(mode:, **)
78
+ super(**)
79
+ @mode = mode
80
+ @pending_html_parts = []
81
+ @level = 0
82
+ end
83
+
84
+ # Initializes a source map.
85
+ #
86
+ # @param orig_proc [Proc] template proc
87
+ # @param orig_ast [Prism::Node] template AST
88
+ # @return [self]
89
+ def with_source_map(orig_proc, orig_ast)
90
+ @fn = orig_proc.source_location.first
91
+ @orig_proc = orig_proc
92
+ @orig_proc_fn = orig_proc.source_location.first
93
+ @source_map = {
94
+ source_fn: orig_proc.source_location.first,
95
+ compiled_fn: Compiler.source_location_to_fn(orig_proc.source_location)
96
+ }
97
+ @source_map_line_ofs = 2
98
+ self
99
+ end
100
+
101
+ def update_source_map(str = nil)
102
+ return if !@source_map
103
+
104
+ buffer_cur_line = @buffer.count("\n") + 1
105
+ orig_source_cur_line = @last_loc_start ? @last_loc_start.first : '?'
106
+ @source_map[buffer_cur_line + @source_map_line_ofs] ||=
107
+ "#{@orig_proc_fn}:#{orig_source_cur_line}"
108
+ end
109
+
110
+ # Formats the source code for a compiled template proc.
111
+ #
112
+ # @param ast [Prism::Node] translated AST
113
+ # @param orig_ast [Prism::Node] original template AST
114
+ # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
115
+ # @return [String] compiled template source code
116
+ def format_compiled_template(ast, orig_ast, wrap:, binding:)
117
+ # generate source code
118
+ @binding = binding
119
+ update_source_map
120
+ visit(ast)
121
+ flush_html_parts!(semicolon_prefix: true)
122
+ update_source_map
123
+
124
+ source_code = @buffer
125
+ @buffer = +''
126
+ if wrap
127
+ @source_map[2] = "#{@orig_proc_fn}:#{loc_start(orig_ast.location).first}"
128
+ emit("# frozen_string_literal: true\n->(__buffer__")
129
+
130
+ params = orig_ast.parameters
131
+ params = params&.parameters
132
+ if params
133
+ emit(', ')
134
+ emit(format_code(params))
135
+ end
136
+
137
+ if @render_yield_used || @render_children_used
138
+ emit(', &__block__')
139
+ end
140
+
141
+ emit(") {\n")
142
+
143
+ end
144
+ @buffer << source_code
145
+ emit_defer_postlude if @defer_mode
146
+ if wrap
147
+ emit('; __buffer__')
148
+ adjust_whitespace(orig_ast.closing_loc)
149
+ emit('}')
150
+ end
151
+ update_source_map
152
+ Compiler.store_source_map(@source_map)
153
+ @buffer
154
+ end
155
+
156
+ # Visits a tag node.
157
+ #
158
+ # @param node [Papercraft::TagNode] node
159
+ # @return [void]
160
+ def visit_tag_node(node)
161
+ @level += 1
162
+ tag = node.tag
163
+
164
+ # adjust_whitespace(node.location)
165
+ is_void = is_void_element?(tag)
166
+ is_raw_inner_text = is_raw_inner_text_element?(tag)
167
+
168
+ emit_html(node.tag_location, format_html_tag_open(node.tag_location, tag, node.attributes))
169
+ return if is_void
170
+
171
+ case node.block
172
+ when Prism::BlockNode
173
+ visit(node.block.body)
174
+ when Prism::BlockArgumentNode
175
+ flush_html_parts!
176
+ adjust_whitespace(node.block)
177
+ emit("; #{format_code(node.block.expression)}.compiled_proc.(__buffer__)")
178
+ end
179
+
180
+ if node.inner_text
181
+ if is_static_node?(node.inner_text)
182
+ if is_raw_inner_text
183
+ emit_html(node.location, format_literal(node.inner_text))
184
+ else
185
+ emit_html(node.location, ERB::Escape.html_escape(format_literal(node.inner_text)))
186
+ end
187
+ else
188
+ if is_raw_inner_text
189
+ emit_html(node.location, interpolated(format_code(node.inner_text)))
190
+ else
191
+ emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)}))"))
192
+ end
193
+ end
194
+ end
195
+ emit_html(node.location, format_html_tag_close(tag))
196
+ ensure
197
+ @level -= 1
11
198
  end
12
-
13
- def format_html_attrs(attrs)
14
- attrs.reduce(+'') do |html, (k, v)|
15
- html << ' ' if !html.empty?
16
- html << "#{format_html_attr(k)}=\"#{v}\""
199
+
200
+ # Visits a const tag node.
201
+ #
202
+ # @param node [Papercraft::ConstTagNode] node
203
+ # @return [void]
204
+ def visit_const_tag_node(node)
205
+ flush_html_parts!
206
+ adjust_whitespace(node.location)
207
+ if node.call_node.receiver
208
+ emit(node.call_node.receiver.location)
209
+ emit('::')
210
+ end
211
+ emit("; #{node.call_node.name}.compiled_proc.(__buffer__")
212
+ if node.call_node.arguments
213
+ emit(', ')
214
+ visit(node.call_node.arguments)
17
215
  end
216
+ emit(');')
18
217
  end
19
218
 
20
- def render_emit_call(o, *a, **b, &block)
21
- case o
22
- when nil
23
- # do nothing
24
- when Papercraft::Template
25
- o.render(*a, **b, &block)
26
- when ::Proc
27
- Papercraft.html(&o).render(*a, **b, &block)
219
+ # Visits a render node.
220
+ #
221
+ # @param node [Papercraft::RenderNode] node
222
+ # @return [void]
223
+ def visit_render_node(node)
224
+ args = node.call_node.arguments.arguments
225
+ first_arg = args.first
226
+
227
+ block_embed = node.block && "&(->(__buffer__) #{format_code(node.block)}.compiled!)"
228
+ block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments
229
+
230
+ flush_html_parts!
231
+ adjust_whitespace(node.location)
232
+
233
+ if args.length == 1
234
+ emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__#{block_embed})")
28
235
  else
29
- o.to_s
236
+ args_code = format_code_comma_separated_nodes(args[1..])
237
+ emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__, #{args_code}#{block_embed})")
30
238
  end
31
239
  end
32
- end
33
-
34
- Papercraft.extend(AuxMethods)
35
240
 
36
- def initialize
37
- super
38
- @html_buffer = +''
39
- end
241
+ # Visits a text node.
242
+ #
243
+ # @param node [Papercraft::TextNode] node
244
+ # @return [void]
245
+ def visit_text_node(node)
246
+ return if !node.call_node.arguments
247
+
248
+ args = node.call_node.arguments.arguments
249
+ first_arg = args.first
250
+ if args.length == 1
251
+ if is_static_node?(first_arg)
252
+ emit_html(node.location, ERB::Escape.html_escape(format_literal(first_arg)))
253
+ else
254
+ emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)})"))
255
+ end
256
+ else
257
+ raise "Don't know how to compile #{node}"
258
+ end
259
+ end
40
260
 
41
- def compile(node)
42
- @root_node = node
43
- inject_buffer_parameter(node)
261
+ # Visits a raw node.
262
+ #
263
+ # @param node [Papercraft::RawNode] node
264
+ # @return [void]
265
+ def visit_raw_node(node)
266
+ return if !node.call_node.arguments
267
+
268
+ args = node.call_node.arguments.arguments
269
+ first_arg = args.first
270
+ if args.length == 1
271
+ if is_static_node?(first_arg)
272
+ emit_html(node.location, format_literal(first_arg))
273
+ else
274
+ emit_html(node.location, interpolated("(#{format_code(first_arg)}).to_s"))
275
+ end
276
+ else
277
+ raise "Don't know how to compile #{node}"
278
+ end
279
+ end
44
280
 
45
- @buffer.clear
46
- @html_buffer.clear
47
- visit(node)
48
- @buffer
49
- end
281
+ # Visits a defer node.
282
+ #
283
+ # @param node [Papercraft::DeferNode] node
284
+ # @return [void]
285
+ def visit_defer_node(node)
286
+ block = node.block
287
+ return if !block
50
288
 
51
- def inject_buffer_parameter(node)
52
- node.inject_parameters('__buffer__')
53
- end
289
+ flush_html_parts!
54
290
 
55
- def embed_visit(node, pre = '', post = '')
56
- tmp_last_loc_start = @last_loc_start
57
- tmp_last_loc_end = @last_loc_end
58
- @last_loc_start = loc_start(node.location)
59
- @last_loc_end = loc_end(node.location)
60
-
61
- @embed_mode = true
62
- tmp_buffer = @buffer
63
- @buffer = +''
64
- visit(node)
65
- @embed_mode = false
66
- @html_buffer << "#{pre}#{@buffer}#{post}"
67
- @buffer = tmp_buffer
68
-
69
- @last_loc_start = tmp_last_loc_start
70
- @last_loc_end = tmp_last_loc_end
71
- end
291
+ if !@defer_mode
292
+ adjust_whitespace(node.call_node.message_loc)
293
+ emit("__orig_buffer__ = __buffer__; __parts__ = __buffer__ = []; ")
294
+ @defer_mode = true
295
+ end
72
296
 
73
- def html_embed_visit(node)
74
- embed_visit(node, '#{CGI.escapeHTML((', ').to_s)}')
75
- end
297
+ adjust_whitespace(block.opening_loc)
298
+ emit("__buffer__ << ->{")
299
+ visit(block.body)
300
+ flush_html_parts!
301
+ adjust_whitespace(block.closing_loc)
302
+ emit("}")
303
+ end
76
304
 
77
- def tag_attr_embed_visit(node, key)
78
- if key
79
- embed_visit(node, '#{Papercraft.format_html_attr(', ')}')
80
- else
81
- embed_visit(node, '#{', '}')
305
+ # Visits a builtin node.
306
+ #
307
+ # @param node [Papercraft::BuiltinNode] node
308
+ # @return [void]
309
+ def visit_builtin_node(node)
310
+ case node.tag
311
+ when :tag
312
+ args = node.call_node.arguments&.arguments
313
+ when :html5
314
+ emit_html(node.location, '<!DOCTYPE html><html>')
315
+ visit(node.block.body) if node.block
316
+ emit_html(node.block.closing_loc, '</html>')
317
+ when :markdown
318
+ args = node.call_node.arguments
319
+ return if !args
320
+
321
+ emit_html(node.location, interpolated("Papercraft.markdown(#{format_code(args)})"))
322
+ end
82
323
  end
83
- end
84
324
 
85
- def emit_code(loc, semicolon: false)
86
- flush_html_buffer if !@embed_mode
87
- super
88
- end
325
+ # Visits a extension tag node.
326
+ #
327
+ # @param node [Papercraft::ExtensionTagNode] node
328
+ # @return [void]
329
+ def visit_extension_tag_node(node)
330
+ flush_html_parts!
331
+ adjust_whitespace(node.location)
332
+ emit("; Papercraft::Extensions[#{node.tag.inspect}].compiled_proc.(__buffer__")
333
+ if node.call_node.arguments
334
+ emit(', ')
335
+ visit(node.call_node.arguments)
336
+ end
337
+ if node.block
338
+ block_body = format_inline_block(node.block.body)
339
+ block_params = []
340
+
341
+ if node.block.parameters.is_a?(Prism::ItParametersNode)
342
+ raise Papercraft::Error, "Blocks passed to extensions cannot use it parameter"
343
+ end
344
+
345
+ if (params = node.block.parameters&.parameters)
346
+ params.requireds.each do
347
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
348
+ end
349
+ params.optionals.each do
350
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
351
+ end
352
+ block_params << format_code(params.rest) if params.rest
353
+ params.posts.each do
354
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
355
+ end
356
+ params.keywords.each do
357
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
358
+ end
359
+ block_params << format_code(params.keyword_rest) if params.keyword_rest
360
+ end
361
+ block_params = block_params.empty? ? '' : ", #{block_params.join(', ')}"
362
+
363
+ emit(", &(proc { |__buffer__#{block_params}| #{block_body} }).compiled!")
364
+ end
365
+ emit(")")
366
+ end
89
367
 
90
- def emit_html(str)
91
- @html_buffer << str
92
- end
368
+ # Visits a render_yield node.
369
+ #
370
+ # @param node [Papercraft::RenderYieldNode] node
371
+ # @return [void]
372
+ def visit_render_yield_node(node)
373
+ flush_html_parts!
374
+ adjust_whitespace(node.location)
375
+ guard = @render_yield_used ?
376
+ '' : "; raise(LocalJumpError, 'no block given (render_yield)') if !__block__"
377
+ @render_yield_used = true
378
+ emit("#{guard}; __block__.compiled_proc.(__buffer__")
379
+ if node.call_node.arguments
380
+ emit(', ')
381
+ visit(node.call_node.arguments)
382
+ end
383
+ emit(")")
384
+ end
385
+
386
+ # Visits a render_children node.
387
+ #
388
+ # @param node [Papercraft::RenderChildrenNode] node
389
+ # @return [void]
390
+ def visit_render_children_node(node)
391
+ flush_html_parts!
392
+ adjust_whitespace(node.location)
393
+ @render_children_used = true
394
+ emit("; __block__&.compiled_proc&.(__buffer__")
395
+ if node.call_node.arguments
396
+ emit(', ')
397
+ visit(node.call_node.arguments)
398
+ end
399
+ emit(")")
400
+ end
93
401
 
94
- def flush_html_buffer
95
- return if @html_buffer.empty?
402
+ def visit_block_invocation_node(node)
403
+ flush_html_parts!
404
+ adjust_whitespace(node.location)
96
405
 
97
- if @last_loc_start
98
- adjust_whitespace(@html_location_start) if @html_location_start
406
+ emit("; #{node.call_node.receiver.name}.compiled_proc.(__buffer__")
407
+ if node.call_node.arguments
408
+ emit(', ')
409
+ visit(node.call_node.arguments)
410
+ end
411
+ if node.call_node.block
412
+ emit(", &(->")
413
+ visit(node.call_node.block)
414
+ emit(").compiled_proc")
415
+ end
416
+ emit(")")
99
417
  end
100
- if @defer_proc_mode
101
- @buffer << "__b__ << \"#{@html_buffer}\""
102
- elsif @defer_mode
103
- @buffer << "__parts__ << \"#{@html_buffer}\""
104
- else
105
- @buffer << "__buffer__ << \"#{@html_buffer}\""
418
+
419
+ private
420
+
421
+ # Overrides the Sourcifier behaviour to flush any buffered HTML parts.
422
+ #
423
+ # @param loc [Prism::Location] location
424
+ # @param semicolon [bool] prefix a semicolon before emitted code
425
+ # @param chomp [bool] chomp the emitted code
426
+ # @param flush_html [bool] flush pending HTML parts before emitting the code
427
+ # @return [void]
428
+ def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
429
+ flush_html_parts! if flush_html
430
+ super(loc, semicolon:, chomp: )
106
431
  end
107
- @html_buffer.clear
108
- @last_loc_end = loc_end(@html_location_end) if @html_location_end
109
432
 
110
- @html_location_start = nil
111
- @html_location_end = nil
112
- end
433
+ # Returns the given str inside interpolation syntax (#{...}).
434
+ #
435
+ # @param str [String] input string
436
+ # @return [String] output string
437
+ def interpolated(str)
438
+ "#\{#{str}}"
439
+ end
113
440
 
114
- def visit_call_node(node)
115
- return super if node.receiver || @embed_mode
441
+ # Formats the given AST with minimal whitespace. Used for formatting
442
+ # arbitrary expressions.
443
+ #
444
+ # @param node [Prism::Node] AST
445
+ # @return [String] generated source code
446
+ def format_code(node)
447
+ Compiler.new(mode: @mode, minimize_whitespace: true).to_source(node)
448
+ end
116
449
 
117
- @html_location_start ||= node.location
450
+ def format_inline_block(node)
451
+ Compiler.new(mode: @mode, minimize_whitespace: true).format_compiled_template(node, node, wrap: false, binding: @binding)
452
+ end
118
453
 
119
- case node.name
120
- when :text
121
- emit_html_text(node)
122
- when :emit
123
- emit_html_emit(node)
124
- when :emit_yield
125
- raise NotImplementedError, "emit_yield is not yet supported in compiled templates"
126
- when :defer
127
- emit_html_deferred(node)
128
- else
129
- emit_html_tag(node)
454
+ # Formats a comma separated list of AST nodes. Used for formatting partial
455
+ # argument lists.
456
+ #
457
+ # @param list [Array<Prism::Node>] node list
458
+ # @return [String] generated source code
459
+ def format_code_comma_separated_nodes(list)
460
+ compiler = Compiler.new(mode: @mode, minimize_whitespace: true)
461
+ compiler.visit_comma_separated_nodes(list)
462
+ compiler.buffer
130
463
  end
131
464
 
132
- @html_location_end = node.location
133
- end
465
+ VOID_TAGS = %w(area base br col embed hr img input link meta param source track wbr)
134
466
 
135
- def tag_args(node)
136
- args = node.arguments&.arguments
137
- return nil if !args
467
+ # Returns true if given HTML element is void (needs no closing tag).
468
+ #
469
+ # @param tag [String, Symbol] HTML tag
470
+ # @return [bool] void or not
471
+ def is_void_element?(tag)
472
+ return false if @mode == :xml
138
473
 
139
- if args[0]&.is_a?(Prism::KeywordHashNode)
140
- [nil, args[0]]
141
- elsif args[1]&.is_a?(Prism::KeywordHashNode)
142
- args
143
- else
144
- [args && args[0], nil]
474
+ VOID_TAGS.include?(tag.to_s)
145
475
  end
146
- end
147
476
 
148
- def emit_tag_open(node, attrs)
149
- emit_html("<#{node.name}")
150
- emit_tag_attributes(node, attrs) if attrs
151
- emit_html(">")
152
- end
477
+ RAW_INNER_TEXT_TAGS = %w(style script)
153
478
 
154
- def emit_tag_close(node)
155
- emit_html("</#{node.name}>")
156
- end
479
+ def is_raw_inner_text_element?(tag)
480
+ return false if @mode == :xml
157
481
 
158
- def emit_tag_open_close(node, attrs)
159
- emit_html("<#{node.name}")
160
- emit_tag_attributes(node, attrs) if attrs
161
- emit_html("/>")
162
- end
482
+ RAW_INNER_TEXT_TAGS.include?(tag.to_s)
483
+ end
163
484
 
164
- def emit_tag_inner_text(node)
165
- case node
166
- when Prism::StringNode, Prism::SymbolNode
167
- @html_buffer << CGI.escapeHTML(node.unescaped)
168
- else
169
- html_embed_visit(node)
485
+ # Formats an open tag with optional attributes.
486
+ #
487
+ # @param loc [Prism::Location] tag location
488
+ # @param tag [String, Symbol] HTML tag
489
+ # @param attributes [Hash, nil] attributes
490
+ # @return [String] HTML
491
+ def format_html_tag_open(loc, tag, attributes)
492
+ tag = convert_tag(tag)
493
+ if attributes && attributes&.elements.size > 0 || @@html_debug_attribute_injector
494
+ "<#{tag} #{format_html_attributes(loc, attributes)}>"
495
+ else
496
+ "<#{tag}>"
497
+ end
170
498
  end
171
- end
172
499
 
173
- def emit_tag_attributes(node, attrs)
174
- attrs.elements.each do |e|
175
- emit_html(" ")
500
+ # Formats a close tag.
501
+ #
502
+ # @param tag [String, Symbol] HTML tag
503
+ # @return [String] HTML
504
+ def format_html_tag_close(tag)
505
+ tag = convert_tag(tag)
506
+ "</#{tag}>"
507
+ end
176
508
 
177
- if e.is_a?(Prism::AssocSplatNode)
178
- embed_visit(e.value, '#{Papercraft.format_html_attrs(', ')}')
509
+ # Converts a tag's underscores to dashes. If tag is dynamic, emits code to
510
+ # convert underscores to dashes at runtime.
511
+ #
512
+ # @param tag [any] tag
513
+ # @return [String] convert tag or code
514
+ def convert_tag(tag)
515
+ case tag
516
+ when Prism::SymbolNode, Prism::StringNode
517
+ Papercraft.underscores_to_dashes(tag.unescaped)
518
+ when Prism::Node
519
+ interpolated("Papercraft.underscores_to_dashes(#{format_code(tag)})")
179
520
  else
180
- emit_tag_attribute_node(e.key, true)
181
- emit_html('=\"')
182
- emit_tag_attribute_node(e.value)
183
- emit_html('\"')
184
- end
521
+ Papercraft.underscores_to_dashes(tag)
522
+ end
523
+ end
524
+
525
+ # Formats a literal value for the given node.
526
+ #
527
+ # @param node [Prism::Node] AST node
528
+ # @return [String] literal representation
529
+ def format_literal(node)
530
+ case node
531
+ when Prism::SymbolNode, Prism::StringNode
532
+ # since the value is copied verbatim into a quoted string, we need to
533
+ # add a backslash before any double quote.
534
+ node.unescaped.gsub('"', '\"')
535
+ when Prism::IntegerNode, Prism::FloatNode
536
+ node.value.to_s
537
+ when Prism::InterpolatedStringNode
538
+ format_code(node)[1..-2]
539
+ when Prism::TrueNode
540
+ 'true'
541
+ when Prism::FalseNode
542
+ 'false'
543
+ when Prism::NilNode
544
+ ''
545
+ else
546
+ interpolated(format_code(node))
547
+ end
185
548
  end
186
- end
187
549
 
188
- def emit_tag_attribute_node(node, key = false)
189
- case node
190
- when Prism::StringNode, Prism::SymbolNode
191
- value = node.unescaped
192
- value = Papercraft.format_html_attr(value) if key
193
- @html_buffer << value
194
- else
195
- tag_attr_embed_visit(node, key)
550
+ STATIC_NODE_TYPES = [
551
+ Prism::FalseNode,
552
+ Prism::FloatNode,
553
+ Prism::IntegerNode,
554
+ Prism::NilNode,
555
+ Prism::StringNode,
556
+ Prism::SymbolNode,
557
+ Prism::TrueNode
558
+ ]
559
+
560
+ # Returns true if given node is static, i.e. is a literal value.
561
+ #
562
+ # @param node [Prism::Node] AST node
563
+ # @return [bool] static or not
564
+ def is_static_node?(node)
565
+ STATIC_NODE_TYPES.include?(node.class)
196
566
  end
197
- end
198
567
 
199
- def emit_html_tag(node)
200
- inner_text, attrs = tag_args(node)
201
- block = node.block
568
+ STRING_TYPE_NODE_TYPES = [
569
+ Prism::StringNode,
570
+ Prism::InterpolatedStringNode
571
+ ]
572
+
573
+ # Returns true if given node a string or interpolated string.
574
+ #
575
+ # @param node [Prism::Node] AST node
576
+ # @return [bool] string node or not
577
+ def is_string_type_node?(node)
578
+ STRING_TYPE_NODE_TYPES.include?(node.class)
579
+ end
202
580
 
203
- if inner_text
204
- emit_tag_open(node, attrs)
205
- emit_tag_inner_text(inner_text)
206
- emit_tag_close(node)
207
- elsif block
208
- emit_tag_open(node, attrs)
209
- visit(block.body)
210
- @html_location_start ||= node.block.closing_loc
211
- emit_tag_close(node)
212
- else
213
- emit_tag_open_close(node, attrs)
581
+ # Formats HTML attributes from the given node.
582
+ #
583
+ # @param loc [Prism::Location] tag location
584
+ # @param node [Prism::Node] attributes node
585
+ # @return [String] HTML
586
+ def format_html_attributes(loc, node)
587
+ elements = node&.elements || []
588
+ if elements.any? { is_dynamic_attribute?(it) }
589
+ return format_html_dynamic_attributes(loc, node)
590
+ end
591
+
592
+ injected_atts = format_injected_attributes(loc)
593
+ parts = elements.map { format_attribute(it.key, it.value) }
594
+ (injected_atts + parts).compact.join(' ')
214
595
  end
215
- end
216
596
 
217
- def emit_html_text(node)
218
- args = node.arguments&.arguments
219
- return nil if !args
597
+ # Formats dynamic HTML attributes from the given node.
598
+ #
599
+ # @param loc [Prism::Location] tag location
600
+ # @param node [Prism::Node] attributes node
601
+ # @return [String] HTML
602
+ def format_html_dynamic_attributes(loc, node)
603
+ injected_atts = compute_injected_attributes(loc)
604
+ if injected_atts.empty?
605
+ return interpolated("Papercraft.format_tag_attrs(#{format_code(node)})")
606
+ else
607
+ return interpolated("Papercraft.format_tag_attrs(#{injected_atts.inspect}.merge(#{format_code(node)}))")
608
+ end
609
+ end
220
610
 
221
- emit_tag_inner_text(args[0])
222
- end
611
+ # Returns true if the given node is a dynamic node.
612
+ #
613
+ # @param node [Prism::Node] attributes node
614
+ # @return [bool] is node dynamic
615
+ def is_dynamic_attribute?(node)
616
+ node.is_a?(Prism::AssocSplatNode) || !is_static_node?(node.key) || !is_static_node?(node.value)
617
+ end
223
618
 
224
- def emit_html_emit(node)
225
- args = node.arguments&.arguments
226
- return nil if !args
619
+ # Computes injected attributes for the given tag location.
620
+ #
621
+ # @param loc [Prism::Location] tag location
622
+ # @return [Hash] injected attributes hash
623
+ def compute_injected_attributes(loc)
624
+ return {} if (@mode == :xml) || !@@html_debug_attribute_injector
227
625
 
228
- embed_visit(node.arguments, '#{Papercraft.render_emit_call(', ')}')
229
- end
626
+ loc = loc_start(loc)
627
+ @@html_debug_attribute_injector&.(@level, @fn, loc[0], loc[1] + 1)
628
+ end
629
+
630
+ # Computes injected attributes for the given tag location.
631
+ #
632
+ # @param loc [Prism::Location] tag location
633
+ # @return [Array<String>] array of attribute strings
634
+ def format_injected_attributes(loc)
635
+ atts = compute_injected_attributes(loc)
636
+ atts.map { |k, v| format_attribute(k, v) }
637
+ end
638
+
639
+ # Formats a tag attribute with the given key and value. A nil, or false
640
+ # value will return nil.
641
+ #
642
+ # @param key [any] attribute key
643
+ # @param value [any] attribute value
644
+ # @return [String, nil] formatted attribute
645
+ def format_attribute(key, value)
646
+ case value
647
+ when Prism::TrueNode
648
+ format_literal(key)
649
+ when Prism::FalseNode, Prism::NilNode
650
+ nil
651
+ when String, Integer, Float, Symbol
652
+ "#{Papercraft.underscores_to_dashes(key)}=\\\"#{value}\\\""
653
+ else
654
+ key = format_literal(key)
655
+ if is_static_node?(value)
656
+ value = format_literal(value)
657
+ "#{Papercraft.underscores_to_dashes(key)}=\\\"#{value}\\\""
658
+ else
659
+ "#{Papercraft.underscores_to_dashes(key)}=\\\"#\{#{format_code(value)}}\\\""
660
+ end
661
+ end
662
+ end
230
663
 
231
- def emit_html_deferred(node)
232
- raise NotImplementedError, "#defer in embed mode is not supported in compiled templates" if @embed_mode
664
+ # Emits HTML into the pending HTML buffer.
665
+ #
666
+ # @param loc [Prism::Location] location
667
+ # @param str [String] HTML
668
+ # @return [void]
669
+ def emit_html(loc, str)
670
+ @html_loc_start ||= loc
671
+ @html_loc_end ||= loc
672
+ @pending_html_parts << [loc, str]
673
+ end
233
674
 
234
- block = node.block
235
- return if not block
675
+ # Flushes pending HTML parts to the source code buffer.
676
+ #
677
+ # @return [void]
678
+ def flush_html_parts!(semicolon_prefix: true)
679
+ return if @pending_html_parts.empty?
680
+
681
+ adjust_whitespace(@html_loc_start, advance_to_end: false)
682
+ emit('; __buffer__')
683
+ concatenated = +''
684
+
685
+ last_loc = @html_loc_start
686
+ @pending_html_parts.each do |(loc, part)|
687
+ if (m = part.match(/^#\{(.+)\}$/m))
688
+ # interpolated part
689
+ emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
690
+ # adjust_whitespace(loc, advance_to_end: false)
691
+ emit_html_buffer_push(m[1], loc:)
692
+ else
693
+ concatenated << part
694
+ end
695
+ last_loc = loc
696
+ end
697
+ emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
236
698
 
237
- setup_defer_mode if !@defer_mode
699
+ @pending_html_parts.clear
238
700
 
239
- flush_html_buffer
240
- @buffer << ';__parts__ << ->(__b__) '
241
- @defer_proc_mode = true
242
- visit(node.block)
243
- @defer_proc_mode = nil
244
- end
701
+ @last_loc = last_loc
702
+ @last_loc_start = loc_start(@last_loc)
703
+ @last_loc_end = @last_loc_start
245
704
 
246
- DEFER_PREFIX_EMPTY = "; __parts__ = []"
247
- DEFER_PREFIX_NOT_EMPTY = "; __parts__ = [__buffer__.dup]; __buffer__.clear"
248
- DEFER_POSTFIX = ";__parts__.each { |p| p.is_a?(Proc) ? p.(__buffer__) : (__buffer__ << p) }"
705
+ @html_loc_start = nil
706
+ @html_loc_end = nil
707
+ end
249
708
 
250
- def setup_defer_mode
251
- @defer_mode = true
252
- if @html_buffer && !@html_buffer.empty?
253
- @buffer << DEFER_PREFIX_NOT_EMPTY
254
- else
255
- @buffer << DEFER_PREFIX_EMPTY
709
+ # Emits HTML buffer push code to the given source code buffer.
710
+ #
711
+ # @param buf [String] source code buffer
712
+ # @param part [String] HTML part
713
+ # @param quotes [bool] whether to wrap emitted HTML in double quotes
714
+ # @return [void]
715
+ def emit_html_buffer_push(part, quotes: false, loc: nil)
716
+ return if part.empty?
717
+
718
+ q = quotes ? '"' : ''
719
+ if loc
720
+ emit(".<<(")
721
+ adjust_whitespace(loc, advance_to_end: false)
722
+ emit("#{q}#{part}#{q}")
723
+ emit(")")
724
+ else
725
+ emit(".<<(#{q}#{part}#{q})")
726
+ end
727
+ part.clear
256
728
  end
257
729
 
258
- @root_node.after_body do
259
- flush_html_buffer
260
- @buffer << DEFER_POSTFIX
730
+ # Emits postlude code for templates with deferred parts.
731
+ #
732
+ # @return [void]
733
+ def emit_defer_postlude
734
+ emit("; __buffer__ = __orig_buffer__; __parts__.each { it.is_a?(Proc) ? it.() : (__buffer__ << it) }")
261
735
  end
262
736
  end
263
737
  end