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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +56 -66
- data/lib/p2/compiler/nodes.rb +139 -0
- data/lib/p2/compiler/tag_translator.rb +44 -0
- data/lib/p2/compiler.rb +226 -237
- data/lib/p2/proc_ext.rb +32 -42
- data/lib/p2/version.rb +1 -1
- data/lib/p2.rb +85 -79
- metadata +5 -17
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 '
|
4
|
+
require 'erb/escape'
|
6
5
|
|
7
|
-
|
8
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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 =
|
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
|
-
|
68
|
+
compiled_fn = "::(#{orig_proc.source_location.join(':')})"
|
225
69
|
@source_map = {
|
226
70
|
source_fn: orig_proc.source_location.first,
|
227
|
-
compiled_fn:
|
71
|
+
compiled_fn: compiled_fn
|
228
72
|
}
|
229
|
-
@source_map_line_ofs =
|
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
|
-
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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,
|
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, "
|
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, "
|
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
|
-
|
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, "
|
211
|
+
emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)}.to_s)"))
|
333
212
|
end
|
334
213
|
else
|
335
|
-
|
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
|
-
|
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,
|
229
|
+
emit_html(node.location, format_literal(first_arg))
|
349
230
|
else
|
350
|
-
emit_html(node.location, "
|
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
|
-
|
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 :
|
274
|
+
when :markdown
|
386
275
|
args = node.call_node.arguments
|
387
276
|
return if !args
|
388
277
|
|
389
|
-
emit_html(node.location, "
|
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
|
-
|
408
|
-
|
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.
|
375
|
+
P2.underscores_to_dashes(tag.unescaped)
|
435
376
|
when Prism::Node
|
436
|
-
"
|
377
|
+
interpolated("P2.underscores_to_dashes(#{format_code(tag)})")
|
437
378
|
else
|
438
|
-
P2.
|
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
|
-
|
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 "
|
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.
|
462
|
+
"#{P2.underscores_to_dashes(k)}=\\\"#{value}\\\""
|
506
463
|
else
|
507
|
-
"#{P2.
|
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
|
-
|
526
|
-
|
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
|
513
|
+
emit code
|
540
514
|
end
|
541
515
|
|
542
|
-
|
543
|
-
|
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
|