papercraft 1.2 → 1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ce5c8e506f96cd5e49625ba875bec30df1fe7849cf726e715383edf1feec4dd
4
- data.tar.gz: fa5a04e6baf5b8e8e36d08a8e9bc962da886c48881489f699b1abab51bc06181
3
+ metadata.gz: 4271050e1f7275fc148ae328a83d5e3357b908d47c0fd9817d3d3560def20a44
4
+ data.tar.gz: 723b2f6cbd79e3f7f979caa7411fc4819f6a4cf62b68089095a0e0c967d26913
5
5
  SHA512:
6
- metadata.gz: 9fd2b6f1ce19479f6a57a5ca75f238b7fb7dee33a3275c2d7f786a1630a5ea5ca5f2d3210946c2ee1dd55676dce65fe0d3503bab830980e5d11fef216a76b5fd
7
- data.tar.gz: 32bd431564ba19783822b5f0596d6f13fc9bd3fee6c498b0d5a9d9722bd536f19ee90fec74a8933460324fbe47d1eab860f3700b6d07bc904c60b131933c5a7b
6
+ metadata.gz: ab8a2a5fb35361deb86d4db69abb295f82a23fe17756e23dbe4f51a56e4af56db64966d391133750f40c279be3153b4005bbb005289a678908b365a2bd5bdf5f
7
+ data.tar.gz: 6bd0ffe0f793c87ad37fab7691e1d915f4b29b049a8fc1a69912550c1777e20ade9bc178b88d5812579a5abe986d46df1e98704692100fdc41e40b59ee5e2c5d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.3 2024-12-16
2
+
3
+ - Update dependencies
4
+
1
5
  ## 1.2 2023-08-21
2
6
 
3
7
  - 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