papercraft 1.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +93 -0
- data/README.md +246 -617
- data/lib/papercraft/compiler/nodes.rb +223 -0
- data/lib/papercraft/compiler/tag_translator.rb +93 -0
- data/lib/papercraft/compiler.rb +657 -201
- data/lib/papercraft/proc_ext.rb +118 -0
- data/lib/papercraft/template.rb +16 -195
- data/lib/papercraft/version.rb +1 -1
- data/lib/papercraft.rb +113 -89
- metadata +11 -64
- 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,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
|
-
|
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]
|
11
36
|
end
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
37
|
-
|
38
|
-
@
|
39
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
@
|
49
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
74
|
-
|
75
|
-
end
|
220
|
+
flush_html_parts!
|
221
|
+
adjust_whitespace(node.location)
|
76
222
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
95
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
115
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
133
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
178
|
-
|
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
|
-
|
181
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
222
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
229
|
-
|
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
|
-
|
232
|
-
|
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
|
-
|
235
|
-
|
608
|
+
loc = loc_start(loc)
|
609
|
+
@@html_debug_attribute_injector&.(@level, @fn, loc[0], loc[1] + 1)
|
610
|
+
end
|
236
611
|
|
237
|
-
|
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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
@
|
244
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
251
|
-
|
252
|
-
|
253
|
-
@
|
254
|
-
|
255
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
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
|