p2 2.0.1 → 2.2

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.
data/lib/p2/compiler.rb CHANGED
@@ -1,198 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi'
4
3
  require 'sirop'
5
- require 'digest/md5'
4
+ require 'erb/escape'
6
5
 
7
- module P2
8
- class TagNode
9
- attr_reader :call_node, :location, :tag, :tag_location, :inner_text, :attributes, :block
10
-
11
- def initialize(call_node, transformer)
12
- @call_node = call_node
13
- @location = call_node.location
14
- @tag = call_node.name
15
- prepare_block(transformer)
16
-
17
- args = call_node.arguments&.arguments
18
- return if !args
19
-
20
- if @tag == :tag
21
- @tag = args[0]
22
- args = args[1..]
23
- end
24
-
25
- if args.size == 1 && args.first.is_a?(Prism::KeywordHashNode)
26
- @inner_text = nil
27
- @attributes = args.first
28
- else
29
- @inner_text = args.first
30
- @attributes = args[1].is_a?(Prism::KeywordHashNode) ? args[1] : nil
31
- end
32
- end
33
-
34
- def accept(visitor)
35
- visitor.visit_tag_node(self)
36
- end
37
-
38
- def prepare_block(transformer)
39
- @block = call_node.block
40
- if @block.is_a?(Prism::BlockNode)
41
- @block = transformer.visit(@block)
42
- offset = @location.start_offset
43
- length = @block.opening_loc.start_offset - offset
44
- @tag_location = @location.copy(start_offset: offset, length: length)
45
- else
46
- @tag_location = @location
47
- end
48
- end
49
- end
50
-
51
- class EmitNode
52
- attr_reader :call_node, :location, :block
53
-
54
- include Prism::DSL
55
-
56
- def initialize(call_node, transformer)
57
- @call_node = call_node
58
- @location = call_node.location
59
- @transformer = transformer
60
- @block = call_node.block && transformer.visit(call_node.block)
61
-
62
- lambda = call_node.arguments && call_node.arguments.arguments[0]
63
- return unless lambda.is_a?(Prism::LambdaNode)
64
-
65
- location = lambda.location
66
- parameters = lambda.parameters
67
- parameters_location = parameters&.location || location
68
- params = parameters&.parameters
69
- lambda = lambda_node(
70
- location: location,
71
- parameters: block_parameters_node(
72
- location: parameters_location,
73
- parameters: parameters_node(
74
- location: parameters_location,
75
- requireds: [
76
- required_parameter_node(
77
- location: ad_hoc_string_location('__buffer__'),
78
- name: :__buffer__
79
- ),
80
- *params&.requireds
81
- ],
82
- optionals: transform_array(params&.optionals),
83
- rest: transform(params&.rest),
84
- posts: transform_array(params&.posts),
85
- keywords: transform_array(params&.keywords),
86
- keyword_rest: transform(params&.keyword_rest),
87
- block: transform(params&.block)
88
- )
89
- ),
90
- body: transformer.visit(lambda.body)
91
- )
92
- call_node.arguments.arguments[0] = lambda
93
- # pp lambda_body: call_node.arguments.arguments[0]
94
- end
95
-
96
- def ad_hoc_string_location(str)
97
- src = source(str)
98
- Prism::DSL.location(source: src, start_offset: 0, length: str.bytesize)
99
- end
100
-
101
- def transform(node)
102
- node && @transformer.visit(node)
103
- end
104
-
105
- def transform_array(array)
106
- array ? array.map { @transformer.visit(it) } : []
107
- end
108
-
109
- def accept(visitor)
110
- visitor.visit_emit_node(self)
111
- end
112
- end
6
+ require_relative './compiler/nodes'
7
+ require_relative './compiler/tag_translator'
113
8
 
114
- class TextNode
115
- attr_reader :call_node, :location
116
-
117
- def initialize(call_node, _compiler)
118
- @call_node = call_node
119
- @location = call_node.location
120
- end
121
-
122
- def accept(visitor)
123
- visitor.visit_text_node(self)
124
- end
125
- end
126
-
127
- class DeferNode
128
- attr_reader :call_node, :location, :block
129
-
130
- def initialize(call_node, compiler)
131
- @call_node = call_node
132
- @location = call_node.location
133
- @block = call_node.block && compiler.visit(call_node.block)
134
- end
135
-
136
- def accept(visitor)
137
- visitor.visit_defer_node(self)
138
- end
139
- end
140
-
141
- class CustomTagNode
142
- attr_reader :tag, :call_node, :location, :block
143
-
144
- def initialize(call_node, compiler)
145
- @call_node = call_node
146
- @tag = call_node.name
147
- @location = call_node.location
148
- @block = call_node.block && compiler.visit(call_node.block)
149
- end
150
-
151
- def accept(visitor)
152
- visitor.visit_custom_tag_node(self)
153
- end
154
- end
155
-
156
- class TagTransformer < Prism::MutationCompiler
157
- include Prism::DSL
158
-
159
- def self.transform(ast)
160
- ast.accept(new)
161
- end
162
-
163
- def visit_call_node(node)
164
- # We're only interested in compiling method calls without a receiver
165
- return super(node) if node.receiver
166
-
167
- case node.name
168
- when :emit_yield
169
- yield_node(
170
- location: node.location,
171
- arguments: node.arguments
172
- )
173
- when :raise
174
- super(node)
175
- when :emit, :e
176
- EmitNode.new(node, self)
177
- when :text
178
- TextNode.new(node, self)
179
- when :defer
180
- DeferNode.new(node, self)
181
- when :html5, :emit_markdown, :markdown
182
- CustomTagNode.new(node, self)
183
- else
184
- TagNode.new(node, self)
185
- end
186
- end
187
- end
188
-
189
- class VerbatimSourcifier < Sirop::Sourcifier
190
- def visit_tag_node(node)
191
- visit(node.call_node)
192
- end
193
- end
194
-
195
- class TemplateCompiler < Sirop::Sourcifier
9
+ module P2
10
+ # A Compiler converts a template into an optimized form that generates HTML
11
+ # efficiently.
12
+ class Compiler < Sirop::Sourcifier
13
+ # Compiles the given proc, returning the generated source map and the
14
+ # generated optimized source code.
15
+ #
16
+ # @param proc [Proc] template
17
+ # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
18
+ # @return [Array] array containing the source map and generated code
196
19
  def self.compile_to_code(proc, wrap: true)
197
20
  ast = Sirop.to_ast(proc)
198
21
 
@@ -200,18 +23,34 @@ module P2
200
23
  ast = ast.block if ast.is_a?(Prism::CallNode)
201
24
 
202
25
  compiler = new.with_source_map(proc, ast)
203
- transformed_ast = TagTransformer.transform(ast.body)
26
+ transformed_ast = TagTranslator.transform(ast.body)
204
27
  compiler.format_compiled_template(transformed_ast, ast, wrap:, binding: proc.binding)
205
28
  [compiler.source_map, compiler.buffer]
206
29
  end
207
30
 
31
+ # Compiles the given template into an optimized Proc that generates HTML.
32
+ #
33
+ # template = -> {
34
+ # h1 'Hello, world!'
35
+ # }
36
+ # compiled = P2::Compiler.compile(template)
37
+ # compiled.render #=> '<h1>Hello, world!'
38
+ #
39
+ # @param proc [Proc] template
40
+ # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
41
+ # @return [Proc] compiled proc
208
42
  def self.compile(proc, wrap: true)
209
43
  source_map, code = compile_to_code(proc, wrap:)
44
+ if ENV['DEBUG'] == '1'
45
+ puts '*' * 40
46
+ puts code
47
+ end
210
48
  eval(code, proc.binding, source_map[:compiled_fn])
211
49
  end
212
50
 
213
51
  attr_reader :source_map
214
52
 
53
+ # Initializes a compiler.
215
54
  def initialize(**)
216
55
  super(**)
217
56
  @pending_html_parts = []
@@ -220,16 +59,27 @@ module P2
220
59
  @yield_used = nil
221
60
  end
222
61
 
62
+ # Initializes a source map.
63
+ #
64
+ # @param orig_proc [Proc] template proc
65
+ # @param orig_ast [Prism::Node] template AST
66
+ # @return [self]
223
67
  def with_source_map(orig_proc, orig_ast)
224
- ast_digest = Digest::MD5.hexdigest(orig_ast.inspect)
68
+ compiled_fn = "::(#{orig_proc.source_location.join(':')})"
225
69
  @source_map = {
226
70
  source_fn: orig_proc.source_location.first,
227
- compiled_fn: "::#{ast_digest}"
71
+ compiled_fn: compiled_fn
228
72
  }
229
- @source_map_line_ofs = 1
73
+ @source_map_line_ofs = 2
230
74
  self
231
75
  end
232
76
 
77
+ # Formats the source code for a compiled template proc.
78
+ #
79
+ # @param ast [Prism::Node] translated AST
80
+ # @param orig_ast [Prism::Node] original template AST
81
+ # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
82
+ # @return [String] compiled template source code
233
83
  def format_compiled_template(ast, orig_ast, wrap:, binding:)
234
84
  # generate source code
235
85
  @binding = binding
@@ -240,7 +90,7 @@ module P2
240
90
  source_code = @buffer
241
91
  @buffer = +''
242
92
  if wrap
243
- emit("(#{@source_map.inspect}).then { |src_map| ->(__buffer__")
93
+ emit("# frozen_string_literal: true\n(#{@source_map.inspect}).then { |src_map| ->(__buffer__")
244
94
 
245
95
  params = orig_ast.parameters
246
96
  params = params&.parameters
@@ -252,11 +102,11 @@ module P2
252
102
  if @yield_used
253
103
  emit(', &__block__')
254
104
  end
255
-
105
+
256
106
  emit(") do\n")
257
107
  end
258
108
  @buffer << source_code
259
- emit_postlude
109
+ emit_defer_postlude if @defer_mode
260
110
  if wrap
261
111
  emit('; __buffer__')
262
112
  adjust_whitespace(orig_ast.closing_loc)
@@ -266,11 +116,10 @@ module P2
266
116
  @buffer
267
117
  end
268
118
 
269
- def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
270
- flush_html_parts! if flush_html
271
- super(loc, semicolon:, chomp: )
272
- end
273
-
119
+ # Visits a tag node.
120
+ #
121
+ # @param node [P2::TagNode] node
122
+ # @return [void]
274
123
  def visit_tag_node(node)
275
124
  tag = node.tag
276
125
  if tag.is_a?(Symbol) && tag =~ /^[A-Z]/
@@ -292,19 +141,23 @@ module P2
292
141
 
293
142
  if node.inner_text
294
143
  if is_static_node?(node.inner_text)
295
- emit_html(node.location, CGI.escape_html(format_literal(node.inner_text)))
144
+ emit_html(node.location, ERB::Escape.html_escape(format_literal(node.inner_text)))
296
145
  else
297
146
  convert_to_s = !is_string_type_node?(node.inner_text)
298
147
  if convert_to_s
299
- emit_html(node.location, "#\{CGI.escape_html((#{format_code(node.inner_text)}).to_s)}")
148
+ emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)}).to_s)"))
300
149
  else
301
- emit_html(node.location, "#\{CGI.escape_html(#{format_code(node.inner_text)})}")
150
+ emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(node.inner_text)})"))
302
151
  end
303
152
  end
304
153
  end
305
154
  emit_html(node.location, format_html_tag_close(tag))
306
155
  end
307
156
 
157
+ # Visits a const tag node.
158
+ #
159
+ # @param node [P2::ConstTagNode] node
160
+ # @return [void]
308
161
  def visit_const_tag_node(node)
309
162
  flush_html_parts!
310
163
  adjust_whitespace(node.location)
@@ -320,40 +173,72 @@ module P2
320
173
  emit(');')
321
174
  end
322
175
 
323
- def visit_emit_node(node)
176
+ # Visits a render node.
177
+ #
178
+ # @param node [P2::RenderNode] node
179
+ # @return [void]
180
+ def visit_render_node(node)
181
+ args = node.call_node.arguments.arguments
182
+ first_arg = args.first
183
+
184
+ block_embed = node.block && "&(->(__buffer__) #{format_code(node.block)}.compiled!)"
185
+ block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments
186
+
187
+ flush_html_parts!
188
+ adjust_whitespace(node.location)
189
+
190
+ if args.length == 1
191
+ emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__#{block_embed})")
192
+ else
193
+ args_code = format_code_comma_separated_nodes(args[1..])
194
+ emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__, #{args_code}#{block_embed})")
195
+ end
196
+ end
197
+
198
+ # Visits a text node.
199
+ #
200
+ # @param node [P2::TextNode] node
201
+ # @return [void]
202
+ def visit_text_node(node)
203
+ return if !node.call_node.arguments
204
+
324
205
  args = node.call_node.arguments.arguments
325
206
  first_arg = args.first
326
207
  if args.length == 1
327
208
  if is_static_node?(first_arg)
328
- emit_html(node.location, format_literal(first_arg))
329
- elsif first_arg.is_a?(Prism::LambdaNode)
330
- visit(first_arg.body)
209
+ emit_html(node.location, ERB::Escape.html_escape(format_literal(first_arg)))
331
210
  else
332
- emit_html(node.location, "#\{P2.render_emit_call(#{format_code(first_arg)})}")
211
+ emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)}.to_s)"))
333
212
  end
334
213
  else
335
- block_embed = node.block ? "&(->(__buffer__) #{format_code(node.block)}.compiled!)" : nil
336
- block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments
337
- emit_html(node.location, "#\{P2.render_emit_call(#{format_code(node.call_node.arguments)}#{block_embed})}")
214
+ raise "Don't know how to compile #{node}"
338
215
  end
339
216
  end
340
217
 
341
- def visit_text_node(node)
218
+ # Visits a raw node.
219
+ #
220
+ # @param node [P2::RawNode] node
221
+ # @return [void]
222
+ def visit_raw_node(node)
342
223
  return if !node.call_node.arguments
343
224
 
344
225
  args = node.call_node.arguments.arguments
345
226
  first_arg = args.first
346
227
  if args.length == 1
347
228
  if is_static_node?(first_arg)
348
- emit_html(node.location, CGI.escape_html(format_literal(first_arg)))
229
+ emit_html(node.location, format_literal(first_arg))
349
230
  else
350
- emit_html(node.location, "#\{CGI.escape_html(#{format_code(first_arg)}.to_s)}")
231
+ emit_html(node.location, interpolated("(#{format_code(first_arg)}).to_s"))
351
232
  end
352
233
  else
353
234
  raise "Don't know how to compile #{node}"
354
235
  end
355
236
  end
356
237
 
238
+ # Visits a defer node.
239
+ #
240
+ # @param node [P2::DeferNode] node
241
+ # @return [void]
357
242
  def visit_defer_node(node)
358
243
  block = node.block
359
244
  return if !block
@@ -374,22 +259,30 @@ module P2
374
259
  emit("}")
375
260
  end
376
261
 
377
- def visit_custom_tag_node(node)
262
+ # Visits a builtin node.
263
+ #
264
+ # @param node [P2::BuiltinNode] node
265
+ # @return [void]
266
+ def visit_builtin_node(node)
378
267
  case node.tag
379
268
  when :tag
380
- args = node.call_node.arguments&.arguments
269
+ args = node.call_node.arguments&.arguments
381
270
  when :html5
382
271
  emit_html(node.location, '<!DOCTYPE html><html>')
383
272
  visit(node.block.body) if node.block
384
273
  emit_html(node.block.closing_loc, '</html>')
385
- when :emit_markdown, :markdown
274
+ when :markdown
386
275
  args = node.call_node.arguments
387
276
  return if !args
388
277
 
389
- emit_html(node.location, "#\{P2.markdown(#{format_code(args)})}")
278
+ emit_html(node.location, interpolated("P2.markdown(#{format_code(args)})"))
390
279
  end
391
280
  end
392
281
 
282
+ # Visits a yield node.
283
+ #
284
+ # @param node [P2::YieldNode] node
285
+ # @return [void]
393
286
  def visit_yield_node(node)
394
287
  adjust_whitespace(node.location)
395
288
  flush_html_parts!
@@ -404,16 +297,55 @@ module P2
404
297
 
405
298
  private
406
299
 
407
- def format_code(node, klass = TemplateCompiler)
408
- klass.new(minimize_whitespace: true).to_source(node)
300
+ # Overrides the Sourcifier behaviour to flush any buffered HTML parts.
301
+ def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
302
+ flush_html_parts! if flush_html
303
+ super(loc, semicolon:, chomp: )
304
+ end
305
+
306
+ # Returns the given str inside interpolation syntax (#{...}).
307
+ #
308
+ # @param str [String] input string
309
+ # @return [String] output string
310
+ def interpolated(str)
311
+ "#\{#{str}}"
312
+ end
313
+
314
+ # Formats the given AST with minimal whitespace. Used for formatting
315
+ # arbitrary expressions.
316
+ #
317
+ # @param node [Prism::Node] AST
318
+ # @return [String] generated source code
319
+ def format_code(node)
320
+ Compiler.new(minimize_whitespace: true).to_source(node)
321
+ end
322
+
323
+ # Formats a comma separated list of AST nodes. Used for formatting partial
324
+ # argument lists.
325
+ #
326
+ # @param list [Array<Prism::Node>] node list
327
+ # @return [String] generated source code
328
+ def format_code_comma_separated_nodes(list)
329
+ compiler = self.class.new(minimize_whitespace: true)
330
+ compiler.visit_comma_separated_nodes(list)
331
+ compiler.buffer
409
332
  end
410
333
 
411
334
  VOID_TAGS = %w(area base br col embed hr img input link meta param source track wbr)
412
335
 
336
+ # Returns true if given HTML element is void (needs no closing tag).
337
+ #
338
+ # @param tag [String, Symbol] HTML tag
339
+ # @return [bool] void or not
413
340
  def is_void_element?(tag)
414
341
  VOID_TAGS.include?(tag.to_s)
415
342
  end
416
343
 
344
+ # Formats an open tag with optional attributes.
345
+ #
346
+ # @param tag [String, Symbol] HTML tag
347
+ # @param attributes [Hash, nil] attributes
348
+ # @return [String] HTML
417
349
  def format_html_tag_open(tag, attributes)
418
350
  tag = convert_tag(tag)
419
351
  if attributes && attributes&.elements.size > 0
@@ -423,29 +355,42 @@ module P2
423
355
  end
424
356
  end
425
357
 
358
+ # Formats a close tag.
359
+ #
360
+ # @param tag [String, Symbol] HTML tag
361
+ # @return [String] HTML
426
362
  def format_html_tag_close(tag)
427
363
  tag = convert_tag(tag)
428
364
  "</#{tag}>"
429
365
  end
430
366
 
367
+ # Converts a tag's underscores to dashes. If tag is dynamic, emits code to
368
+ # convert underscores to dashes at runtime.
369
+ #
370
+ # @param tag [any] tag
371
+ # @return [String] convert tag or code
431
372
  def convert_tag(tag)
432
373
  case tag
433
374
  when Prism::SymbolNode, Prism::StringNode
434
- P2.format_tag(tag.unescaped)
375
+ P2.underscores_to_dashes(tag.unescaped)
435
376
  when Prism::Node
436
- "#\{P2.format_tag(#{format_code(tag)})}"
377
+ interpolated("P2.underscores_to_dashes(#{format_code(tag)})")
437
378
  else
438
- P2.format_tag(tag)
379
+ P2.underscores_to_dashes(tag)
439
380
  end
440
381
  end
441
382
 
383
+ # Formats a literal value for the given node.
384
+ #
385
+ # @param node [Prism::Node] AST node
386
+ # @return [String] literal representation
442
387
  def format_literal(node)
443
388
  case node
444
389
  when Prism::SymbolNode, Prism::StringNode
445
390
  node.unescaped
446
391
  when Prism::IntegerNode, Prism::FloatNode
447
392
  node.value.to_s
448
- when Prism::InterpolatedStringNode
393
+ when Prism::InterpolatedStringNode
449
394
  format_code(node)[1..-2]
450
395
  when Prism::TrueNode
451
396
  'true'
@@ -454,7 +399,7 @@ module P2
454
399
  when Prism::NilNode
455
400
  ''
456
401
  else
457
- "#\{#{format_code(node)}}"
402
+ interpolated(format_code(node))
458
403
  end
459
404
  end
460
405
 
@@ -468,6 +413,10 @@ module P2
468
413
  Prism::TrueNode
469
414
  ]
470
415
 
416
+ # Returns true if given node is static, i.e. is a literal value.
417
+ #
418
+ # @param node [Prism::Node] AST node
419
+ # @return [bool] static or not
471
420
  def is_static_node?(node)
472
421
  STATIC_NODE_TYPES.include?(node.class)
473
422
  end
@@ -477,10 +426,18 @@ module P2
477
426
  Prism::InterpolatedStringNode
478
427
  ]
479
428
 
429
+ # Returns true if given node a string or interpolated string.
430
+ #
431
+ # @param node [Prism::Node] AST node
432
+ # @return [bool] string node or not
480
433
  def is_string_type_node?(node)
481
434
  STRING_TYPE_NODE_TYPES.include?(node.class)
482
435
  end
483
436
 
437
+ # Formats HTML attributes from the given node.
438
+ #
439
+ # @param node [Prism::Node] AST node
440
+ # @return [String] HTML
484
441
  def format_html_attributes(node)
485
442
  elements = node.elements
486
443
  dynamic_attributes = elements.any? do
@@ -488,7 +445,7 @@ module P2
488
445
  !is_static_node?(it.key) || !is_static_node?(it.value)
489
446
  end
490
447
 
491
- return "#\{P2.format_html_attrs(#{format_code(node)})}" if dynamic_attributes
448
+ return interpolated("P2.format_tag_attrs(#{format_code(node)})") if dynamic_attributes
492
449
 
493
450
  parts = elements.map do
494
451
  key = it.key
@@ -499,12 +456,12 @@ module P2
499
456
  when Prism::FalseNode, Prism::NilNode
500
457
  nil
501
458
  else
502
- k = format_literal(key)
459
+ k = format_literal(key)
503
460
  if is_static_node?(value)
504
461
  value = format_literal(value)
505
- "#{P2.format_html_attr_key(k)}=\\\"#{value}\\\""
462
+ "#{P2.underscores_to_dashes(k)}=\\\"#{value}\\\""
506
463
  else
507
- "#{P2.format_html_attr_key(k)}=\\\"#\{#{format_code(value)}}\\\""
464
+ "#{P2.underscores_to_dashes(k)}=\\\"#\{#{format_code(value)}}\\\""
508
465
  end
509
466
  end
510
467
  end
@@ -512,21 +469,38 @@ module P2
512
469
  parts.compact.join(' ')
513
470
  end
514
471
 
472
+ # Emits HTML into the pending HTML buffer.
473
+ #
474
+ # @param loc [Prism::Location] location
475
+ # @param str [String] HTML
476
+ # @return [void]
515
477
  def emit_html(loc, str)
516
478
  @html_loc_start ||= loc
517
479
  @html_loc_end = loc
518
480
  @pending_html_parts << str
519
481
  end
520
482
 
483
+ # Flushes pending HTML parts to the source code buffer.
484
+ #
485
+ # @return [void]
521
486
  def flush_html_parts!(semicolon_prefix: true)
522
487
  return if @pending_html_parts.empty?
523
488
 
524
489
  adjust_whitespace(@html_loc_start)
525
- if semicolon_prefix && @buffer =~ /[^\s]\s*$/m
526
- emit '; '
490
+
491
+ code = +''
492
+ part = +''
493
+
494
+ @pending_html_parts.each do
495
+ if (m = it.match(/^#\{(.+)\}$/m))
496
+ emit_html_buffer_push(code, part, quotes: true) if !part.empty?
497
+ emit_html_buffer_push(code, m[1])
498
+ else
499
+ part << it
500
+ end
527
501
  end
502
+ emit_html_buffer_push(code, part, quotes: true) if !part.empty?
528
503
 
529
- str = @pending_html_parts.join
530
504
  @pending_html_parts.clear
531
505
 
532
506
  @last_loc = @html_loc_end
@@ -536,12 +510,27 @@ module P2
536
510
  @html_loc_start = nil
537
511
  @html_loc_end = nil
538
512
 
539
- emit "__buffer__ << \"#{str}\""
513
+ emit code
540
514
  end
541
515
 
542
- def emit_postlude
543
- return if !@defer_mode
516
+ # Emits HTML buffer push code to the given source code buffer.
517
+ #
518
+ # @param buf [String] source code buffer
519
+ # @param part [String] HTML part
520
+ # @param quotes [bool] whether to wrap emitted HTML in double quotes
521
+ # @return [void]
522
+ def emit_html_buffer_push(buf, part, quotes: false)
523
+ return if part.empty?
524
+
525
+ q = quotes ? '"' : ''
526
+ buf << "; __buffer__ << #{q}#{part}#{q}"
527
+ part.clear
528
+ end
544
529
 
530
+ # Emits postlude code for templates with deferred parts.
531
+ #
532
+ # @return [void]
533
+ def emit_defer_postlude
545
534
  emit("; __buffer__ = __orig_buffer__; __parts__.each { it.is_a?(Proc) ? it.() : (__buffer__ << it) }")
546
535
  end
547
536
  end