papercraft 1.4 → 2.13

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