papercraft 1.2 → 1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ce5c8e506f96cd5e49625ba875bec30df1fe7849cf726e715383edf1feec4dd
4
- data.tar.gz: fa5a04e6baf5b8e8e36d08a8e9bc962da886c48881489f699b1abab51bc06181
3
+ metadata.gz: 78c8ccb1c86c1ca23bf5db29174c938015c1137fcd4018f0afd09b298b648e94
4
+ data.tar.gz: 196e06b72fef6f750789b64883110a075aef402e34b8616dcc8114c425d1de17
5
5
  SHA512:
6
- metadata.gz: 9fd2b6f1ce19479f6a57a5ca75f238b7fb7dee33a3275c2d7f786a1630a5ea5ca5f2d3210946c2ee1dd55676dce65fe0d3503bab830980e5d11fef216a76b5fd
7
- data.tar.gz: 32bd431564ba19783822b5f0596d6f13fc9bd3fee6c498b0d5a9d9722bd536f19ee90fec74a8933460324fbe47d1eab860f3700b6d07bc904c60b131933c5a7b
6
+ metadata.gz: '0599e7b5340e1dc5afe72f76c930ed0587f1bafdc9b7bac03cb75597efdd8e450faf81012cf16666fe3588ea39aa53ffec6ae14aa3f0002e79036144d3681131'
7
+ data.tar.gz: e8b88f6cd76c8a5a10df7bb84e174e7e0d7ff06aac6c30e1d763982ac7b63261a9203e887e0df647fc5ef55248b8239fa75851e77eeb9ef74bc7ce4be9e8a1d0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 1.4 2025-01-09
2
+
3
+ - Compiler: add support defer
4
+
5
+ ## 1.3 2024-12-16
6
+
7
+ - Update dependencies
8
+
1
9
  ## 1.2 2023-08-21
2
10
 
3
11
  - Update dependencies
data/README.md CHANGED
@@ -98,6 +98,8 @@ hello.render('world')
98
98
 
99
99
  ## Installing Papercraft
100
100
 
101
+ **Note**: Papercraft requires Ruby version 3.2 or newer.
102
+
101
103
  Using bundler:
102
104
 
103
105
  ```ruby
@@ -1,428 +1,263 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Papercraft
4
- # The Compiler class compiles Papercraft templates
5
- class Compiler
6
- DEFAULT_CODE_BUFFER_CAPACITY = 8192
7
- DEFAULT_EMIT_BUFFER_CAPACITY = 4096
8
-
9
- def initialize
10
- @level = 1
11
- @code_buffer = String.new(capacity: DEFAULT_CODE_BUFFER_CAPACITY)
12
- end
13
-
14
- def emit_output
15
- @output_mode = true
16
- yield
17
- @output_mode = false
18
- end
19
-
20
- def emit_code_line_break
21
- return if @code_buffer.empty?
22
-
23
- @code_buffer << "\n" if @code_buffer[-1] != "\n"
24
- @line_break = nil
25
- end
26
-
27
- def emit_literal(lit)
28
- if @output_mode
29
- emit_code_line_break if @line_break
30
- @emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
31
- @emit_buffer << lit
32
- else
33
- emit_code(lit)
3
+ require 'cgi'
4
+ require 'escape_utils'
5
+ require 'sirop'
6
+
7
+ class Papercraft::Compiler < Sirop::Sourcifier
8
+ module AuxMethods
9
+ def format_html_attr(tag)
10
+ tag.to_s.tr('_', '-')
11
+ end
12
+
13
+ def format_html_attrs(attrs)
14
+ attrs.reduce(+'') do |html, (k, v)|
15
+ html << ' ' if !html.empty?
16
+ html << "#{format_html_attr(k)}=\"#{v}\""
34
17
  end
35
18
  end
36
19
 
37
- def emit_text(str, encoding: :html)
38
- emit_code_line_break if @line_break
39
- @emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
40
- @emit_buffer << encode(str, encoding).inspect[1..-2]
41
- end
42
-
43
- def encode(str, encoding)
44
- case encoding
45
- when :html
46
- __html_encode__(str)
47
- when :uri
48
- __uri_encode__(str)
20
+ def render_emit_call(o, *a, **b, &block)
21
+ case o
22
+ when nil
23
+ # do nothing
24
+ when Papercraft::Template
25
+ o.render(*a, **b, &block)
26
+ when ::Proc
27
+ Papercraft.html(&o).render(*a, **b, &block)
49
28
  else
50
- raise "Invalid encoding #{encoding.inspect}"
29
+ o.to_s
51
30
  end
52
31
  end
32
+ end
33
+
34
+ Papercraft.extend(AuxMethods)
53
35
 
54
- def emit_expression
55
- if @output_mode
56
- emit_literal('#{__html_encode__(')
57
- yield
58
- emit_literal(')}')
59
- else
60
- yield
61
- end
62
- end
36
+ def initialize
37
+ super
38
+ @html_buffer = +''
39
+ end
63
40
 
64
- def flush_emit_buffer
65
- return if !@emit_buffer
41
+ def compile(node)
42
+ @root_node = node
43
+ inject_buffer_parameter(node)
66
44
 
67
- @code_buffer << "#{' ' * @level}__buffer__ << \"#{@emit_buffer}\"\n"
68
- @emit_buffer = nil
69
- true
70
- end
45
+ @buffer.clear
46
+ @html_buffer.clear
47
+ visit(node)
48
+ @buffer
49
+ end
71
50
 
72
- def emit_code(code)
73
- if flush_emit_buffer || @line_break
74
- emit_code_line_break if @line_break
75
- @code_buffer << "#{' ' * @level}#{code}"
76
- else
77
- if @code_buffer.empty? || (@code_buffer[-1] == "\n")
78
- @code_buffer << "#{' ' * @level}#{code}"
79
- else
80
- @code_buffer << "#{code}"
81
- end
82
- end
83
- end
51
+ def inject_buffer_parameter(node)
52
+ node.inject_parameters('__buffer__')
53
+ end
84
54
 
85
- def compile(template)
86
- @block = template.to_proc
87
- ast = RubyVM::AbstractSyntaxTree.of(@block)
88
- # Compiler.pp_ast(ast)
89
- parse(ast)
90
- flush_emit_buffer
91
- self
92
- end
55
+ def embed_visit(node, pre = '', post = '')
56
+ tmp_last_loc_start = @last_loc_start
57
+ tmp_last_loc_end = @last_loc_end
58
+ @last_loc_start = loc_start(node.location)
59
+ @last_loc_end = loc_end(node.location)
60
+
61
+ @embed_mode = true
62
+ tmp_buffer = @buffer
63
+ @buffer = +''
64
+ visit(node)
65
+ @embed_mode = false
66
+ @html_buffer << "#{pre}#{@buffer}#{post}"
67
+ @buffer = tmp_buffer
68
+
69
+ @last_loc_start = tmp_last_loc_start
70
+ @last_loc_end = tmp_last_loc_end
71
+ end
93
72
 
94
- attr_reader :code_buffer
73
+ def html_embed_visit(node)
74
+ embed_visit(node, '#{CGI.escapeHTML((', ').to_s)}')
75
+ end
95
76
 
96
- def to_code
97
- "->(__buffer__, __context__) do\n#{@code_buffer}end"
77
+ def tag_attr_embed_visit(node, key)
78
+ if key
79
+ embed_visit(node, '#{Papercraft.format_html_attr(', ')}')
80
+ else
81
+ embed_visit(node, '#{', '}')
98
82
  end
83
+ end
99
84
 
100
- def to_proc
101
- @block.binding.eval(to_code)
102
- end
85
+ def emit_code(loc, semicolon: false)
86
+ flush_html_buffer if !@embed_mode
87
+ super
88
+ end
103
89
 
104
- def parse(node)
105
- @line_break = @last_node && node.first_lineno != @last_node.first_lineno
106
- @last_node = node
107
- # puts "- parse(#{node.type}) (break: #{@line_break.inspect})"
108
- send(:"parse_#{node.type.downcase}", node)
109
- end
90
+ def emit_html(str)
91
+ @html_buffer << str
92
+ end
110
93
 
111
- def parse_scope(node)
112
- parse(node.children[2])
113
- end
94
+ def flush_html_buffer
95
+ return if @html_buffer.empty?
114
96
 
115
- def parse_iter(node)
116
- call, scope = node.children
117
- if call.type == :FCALL
118
- parse_fcall(call, scope)
119
- else
120
- parse(call)
121
- emit_code(" do")
122
- args = scope.children[0]
123
- emit_code(" |#{args.join(', ')}|") if args
124
- emit_code("\n")
125
- @level += 1
126
- parse(scope)
127
- flush_emit_buffer
128
- @level -= 1
129
- emit_code("end\n")
130
- end
97
+ if @last_loc_start
98
+ adjust_whitespace(@html_location_start) if @html_location_start
131
99
  end
132
-
133
- def parse_ivar(node)
134
- ivar = node.children.first.match(/^@(.+)*/)[1]
135
- emit_literal("__context__[:#{ivar}]")
100
+ if @defer_proc_mode
101
+ @buffer << "__b__ << \"#{@html_buffer}\""
102
+ elsif @defer_mode
103
+ @buffer << "__parts__ << \"#{@html_buffer}\""
104
+ else
105
+ @buffer << "__buffer__ << \"#{@html_buffer}\""
136
106
  end
107
+ @html_buffer.clear
108
+ @last_loc_end = loc_end(@html_location_end) if @html_location_end
137
109
 
138
- def parse_fcall(node, block = nil)
139
- tag, args = node.children
140
- args = args.children.compact if args
141
- text = fcall_inner_text_from_args(args)
142
- atts = fcall_attributes_from_args(args)
143
- if block
144
- emit_tag(tag, atts) { parse(block) }
145
- elsif text
146
- emit_tag(tag, atts) do
147
- emit_output do
148
- if text.is_a?(String)
149
- emit_text(text)
150
- else
151
- emit_expression { parse(text) }
152
- end
153
- end
154
- end
155
- else
156
- emit_tag(tag, atts)
157
- end
158
- end
110
+ @html_location_start = nil
111
+ @html_location_end = nil
112
+ end
159
113
 
160
- def fcall_inner_text_from_args(args)
161
- return nil if !args
162
-
163
- first = args.first
164
- case first.type
165
- when :STR
166
- first.children.first
167
- when :LIT
168
- first.children.first.to_s
169
- when :HASH
170
- nil
171
- else
172
- first
173
- end
174
- end
114
+ def visit_call_node(node)
115
+ return super if node.receiver || @embed_mode
175
116
 
176
- def fcall_attributes_from_args(args)
177
- return nil if !args
117
+ @html_location_start ||= node.location
178
118
 
179
- last = args.last
180
- (last.type == :HASH) ? last : nil
119
+ case node.name
120
+ when :text
121
+ emit_html_text(node)
122
+ when :emit
123
+ emit_html_emit(node)
124
+ when :emit_yield
125
+ raise NotImplementedError, "emit_yield is not yet supported in compiled templates"
126
+ when :defer
127
+ emit_html_deferred(node)
128
+ else
129
+ emit_html_tag(node)
181
130
  end
182
131
 
183
- def emit_tag(tag, atts, &block)
184
- emit_output do
185
- if atts
186
- emit_literal("<#{tag}")
187
- emit_tag_attributes(atts)
188
- emit_literal(block ? '>' : '/>')
189
- else
190
- emit_literal(block ? "<#{tag}>" : "<#{tag}/>")
191
- end
192
- end
193
- if block
194
- block.call
195
- emit_output { emit_literal("</#{tag}>") }
196
- end
197
- end
132
+ @html_location_end = node.location
133
+ end
198
134
 
199
- def emit_tag_attributes(atts)
200
- list = atts.children.first.children
201
- while true
202
- key = list.shift
203
- break unless key
204
-
205
- value = list.shift
206
- value_type = value.type
207
- case value_type
208
- when :FALSE, :NIL
209
- next
210
- end
211
-
212
- emit_literal(' ')
213
- emit_tag_attribute_key(key)
214
- next if value_type == :TRUE
215
-
216
- emit_literal('=\"')
217
- emit_tag_attribute_value(value, key)
218
- emit_literal('\"')
219
- end
220
- end
135
+ def tag_args(node)
136
+ args = node.arguments&.arguments
137
+ return nil if !args
221
138
 
222
- def emit_tag_attribute_key(key)
223
- case key.type
224
- when :STR
225
- emit_literal(key.children.first)
226
- when :LIT
227
- emit_literal(key.children.first.to_s)
228
- when :NIL
229
- emit_literal('nil')
230
- else
231
- emit_expression { parse(key) }
232
- end
139
+ if args[0]&.is_a?(Prism::KeywordHashNode)
140
+ [nil, args[0]]
141
+ elsif args[1]&.is_a?(Prism::KeywordHashNode)
142
+ args
143
+ else
144
+ [args && args[0], nil]
233
145
  end
146
+ end
234
147
 
235
- def emit_tag_attribute_value(value, key)
236
- case value.type
237
- when :STR
238
- encoding = (key.type == :LIT) && (key.children.first == :href) ? :uri : :html
239
- emit_text(value.children.first, encoding: encoding)
240
- when :LIT
241
- emit_text(value.children.first.to_s)
242
- else
243
- parse(value)
244
- end
245
- end
148
+ def emit_tag_open(node, attrs)
149
+ emit_html("<#{node.name}")
150
+ emit_tag_attributes(node, attrs) if attrs
151
+ emit_html(">")
152
+ end
246
153
 
247
- def parse_call(node)
248
- receiver, method, args = node.children
249
- if receiver.type == :VCALL && receiver.children == [:context]
250
- emit_literal('__context__')
251
- else
252
- parse(receiver)
253
- end
254
- if method == :[]
255
- emit_literal('[')
256
- args = args.children.compact
257
- while true
258
- arg = args.shift
259
- break unless arg
260
-
261
- parse(arg)
262
- emit_literal(', ') if !args.empty?
263
- end
264
- emit_literal(']')
265
- else
266
- emit_literal('.')
267
- emit_literal(method.to_s)
268
- if args
269
- emit_literal('(')
270
- args = args.children.compact
271
- while true
272
- arg = args.shift
273
- break unless arg
274
-
275
- parse(arg)
276
- emit_literal(', ') if !args.empty?
277
- end
278
- emit_literal(')')
279
- end
280
- end
281
- end
154
+ def emit_tag_close(node)
155
+ emit_html("</#{node.name}>")
156
+ end
282
157
 
283
- def parse_str(node)
284
- str = node.children.first
285
- emit_literal(str.inspect)
286
- end
158
+ def emit_tag_open_close(node, attrs)
159
+ emit_html("<#{node.name}")
160
+ emit_tag_attributes(node, attrs) if attrs
161
+ emit_html("/>")
162
+ end
287
163
 
288
- def parse_lit(node)
289
- value = node.children.first
290
- emit_literal(value.inspect)
164
+ def emit_tag_inner_text(node)
165
+ case node
166
+ when Prism::StringNode, Prism::SymbolNode
167
+ @html_buffer << CGI.escapeHTML(node.unescaped)
168
+ else
169
+ html_embed_visit(node)
291
170
  end
171
+ end
292
172
 
293
- def parse_true(node)
294
- emit_expression { emit_literal('true') }
295
- end
173
+ def emit_tag_attributes(node, attrs)
174
+ attrs.elements.each do |e|
175
+ emit_html(" ")
296
176
 
297
- def parse_false(node)
298
- emit_expression { emit_literal('true') }
177
+ if e.is_a?(Prism::AssocSplatNode)
178
+ embed_visit(e.value, '#{Papercraft.format_html_attrs(', ')}')
179
+ else
180
+ emit_tag_attribute_node(e.key, true)
181
+ emit_html('=\"')
182
+ emit_tag_attribute_node(e.value)
183
+ emit_html('\"')
184
+ end
299
185
  end
186
+ end
300
187
 
301
- def parse_list(node)
302
- emit_literal('[')
303
- items = node.children.compact
304
- while true
305
- item = items.shift
306
- break unless item
307
-
308
- parse(item)
309
- emit_literal(', ') if !items.empty?
310
- end
311
- emit_literal(']')
188
+ def emit_tag_attribute_node(node, key = false)
189
+ case node
190
+ when Prism::StringNode, Prism::SymbolNode
191
+ value = node.unescaped
192
+ value = Papercraft.format_html_attr(value) if key
193
+ @html_buffer << value
194
+ else
195
+ tag_attr_embed_visit(node, key)
312
196
  end
197
+ end
313
198
 
314
- def parse_vcall(node)
315
- tag = node.children.first
316
- emit_tag(tag, nil)
199
+ def emit_html_tag(node)
200
+ inner_text, attrs = tag_args(node)
201
+ block = node.block
202
+
203
+ if inner_text
204
+ emit_tag_open(node, attrs)
205
+ emit_tag_inner_text(inner_text)
206
+ emit_tag_close(node)
207
+ elsif block
208
+ emit_tag_open(node, attrs)
209
+ visit(block.body)
210
+ @html_location_start ||= node.block.closing_loc
211
+ emit_tag_close(node)
212
+ else
213
+ emit_tag_open_close(node, attrs)
317
214
  end
215
+ end
318
216
 
319
- def parse_opcall(node)
320
- left, op, right = node.children
321
- parse(left)
322
- emit_literal(" #{op} ")
323
- right.children.compact.each { |c| parse(c) }
324
- end
217
+ def emit_html_text(node)
218
+ args = node.arguments&.arguments
219
+ return nil if !args
325
220
 
326
- def parse_block(node)
327
- node.children.each { |c| parse(c) }
328
- end
221
+ emit_tag_inner_text(args[0])
222
+ end
329
223
 
330
- def parse_if(node)
331
- cond, then_branch, else_branch = node.children
332
- if @output_mode
333
- emit_if_output(cond, then_branch, else_branch)
334
- else
335
- emit_if_code(cond, then_branch, else_branch)
336
- end
337
- end
224
+ def emit_html_emit(node)
225
+ args = node.arguments&.arguments
226
+ return nil if !args
338
227
 
339
- def parse_unless(node)
340
- cond, then_branch, else_branch = node.children
341
- if @output_mode
342
- emit_unless_output(cond, then_branch, else_branch)
343
- else
344
- emit_unless_code(cond, then_branch, else_branch)
345
- end
346
- end
228
+ embed_visit(node.arguments, '#{Papercraft.render_emit_call(', ')}')
229
+ end
347
230
 
348
- def emit_if_output(cond, then_branch, else_branch)
349
- parse(cond)
350
- emit_literal(" ? ")
351
- parse(then_branch)
352
- emit_literal(" : ")
353
- if else_branch
354
- parse(else_branch)
355
- else
356
- emit_literal(nil)
357
- end
358
- end
231
+ def emit_html_deferred(node)
232
+ raise NotImplementedError, "#defer in embed mode is not supported in compiled templates" if @embed_mode
359
233
 
360
- def emit_unless_output(cond, then_branch, else_branch)
361
- parse(cond)
362
- emit_literal(" ? ")
363
- if else_branch
364
- parse(else_branch)
365
- else
366
- emit_literal(nil)
367
- end
368
- emit_literal(" : ")
369
- parse(then_branch)
370
- end
234
+ block = node.block
235
+ return if not block
371
236
 
372
- def emit_if_code(cond, then_branch, else_branch)
373
- emit_code('if ')
374
- parse(cond)
375
- emit_code("\n")
376
- @level += 1
377
- parse(then_branch)
378
- flush_emit_buffer
379
- @level -= 1
380
- if else_branch
381
- emit_code("else\n")
382
- @level += 1
383
- parse(else_branch)
384
- flush_emit_buffer
385
- @level -= 1
386
- end
387
- emit_code("end\n")
388
- end
237
+ setup_defer_mode if !@defer_mode
389
238
 
390
- def emit_unless_code(cond, then_branch, else_branch)
391
- emit_code('unless ')
392
- parse(cond)
393
- emit_code("\n")
394
- @level += 1
395
- parse(then_branch)
396
- flush_emit_buffer
397
- @level -= 1
398
- if else_branch
399
- emit_code("else\n")
400
- @level += 1
401
- parse(else_branch)
402
- flush_emit_buffer
403
- @level -= 1
404
- end
405
- emit_code("end\n")
406
- end
239
+ flush_html_buffer
240
+ @buffer << ';__parts__ << ->(__b__) '
241
+ @defer_proc_mode = true
242
+ visit(node.block)
243
+ @defer_proc_mode = nil
244
+ end
407
245
 
408
- def parse_dvar(node)
246
+ DEFER_PREFIX_EMPTY = "; __parts__ = []"
247
+ DEFER_PREFIX_NOT_EMPTY = "; __parts__ = [__buffer__.dup]; __buffer__.clear"
248
+ DEFER_POSTFIX = ";__parts__.each { |p| p.is_a?(Proc) ? p.(__buffer__) : (__buffer__ << p) }"
409
249
 
410
- emit_literal(node.children.first.to_s)
250
+ def setup_defer_mode
251
+ @defer_mode = true
252
+ if @html_buffer && !@html_buffer.empty?
253
+ @buffer << DEFER_PREFIX_NOT_EMPTY
254
+ else
255
+ @buffer << DEFER_PREFIX_EMPTY
411
256
  end
412
257
 
413
- def self.pp_ast(node, level = 0)
414
- case node
415
- when RubyVM::AbstractSyntaxTree::Node
416
- puts "#{' ' * level}#{node.type.inspect}"
417
- node.children.each { |c| pp_ast(c, level + 1) }
418
- when Array
419
- puts "#{' ' * level}["
420
- node.each { |c| pp_ast(c, level + 1) }
421
- puts "#{' ' * level}]"
422
- else
423
- puts "#{' ' * level}#{node.inspect}"
424
- return
425
- end
258
+ @root_node.after_body do
259
+ flush_html_buffer
260
+ @buffer << DEFER_POSTFIX
426
261
  end
427
262
  end
428
263
  end