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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +93 -0
- data/README.md +243 -619
- data/lib/papercraft/compiler/nodes.rb +223 -0
- data/lib/papercraft/compiler/tag_translator.rb +93 -0
- data/lib/papercraft/compiler.rb +674 -200
- data/lib/papercraft/proc_ext.rb +118 -0
- data/lib/papercraft/template.rb +15 -194
- data/lib/papercraft/version.rb +1 -1
- data/lib/papercraft.rb +111 -87
- metadata +11 -60
- data/lib/papercraft/compiler_old.rb +0 -701
- data/lib/papercraft/extension_proxy.rb +0 -41
- data/lib/papercraft/extensions/soap.rb +0 -42
- data/lib/papercraft/html.rb +0 -173
- data/lib/papercraft/json.rb +0 -128
- data/lib/papercraft/renderer.rb +0 -190
- data/lib/papercraft/tags.rb +0 -408
- data/lib/papercraft/xml.rb +0 -47
- data/lib/tilt/papercraft.rb +0 -25
data/lib/papercraft/compiler.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
38
|
-
@
|
39
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
@
|
49
|
-
|
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
|
-
|
52
|
-
node.inject_parameters('__buffer__')
|
53
|
-
end
|
289
|
+
flush_html_parts!
|
54
290
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
95
|
-
|
402
|
+
def visit_block_invocation_node(node)
|
403
|
+
flush_html_parts!
|
404
|
+
adjust_whitespace(node.location)
|
96
405
|
|
97
|
-
|
98
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
115
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
133
|
-
end
|
465
|
+
VOID_TAGS = %w(area base br col embed hr img input link meta param source track wbr)
|
134
466
|
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
155
|
-
|
156
|
-
end
|
479
|
+
def is_raw_inner_text_element?(tag)
|
480
|
+
return false if @mode == :xml
|
157
481
|
|
158
|
-
|
159
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
178
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
222
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
229
|
-
|
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
|
-
|
232
|
-
|
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
|
-
|
235
|
-
|
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
|
-
|
699
|
+
@pending_html_parts.clear
|
238
700
|
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
705
|
+
@html_loc_start = nil
|
706
|
+
@html_loc_end = nil
|
707
|
+
end
|
249
708
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
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
|