p2 2.4 → 2.6
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 +11 -0
- data/lib/p2/compiler/nodes.rb +28 -0
- data/lib/p2/compiler/tag_translator.rb +60 -12
- data/lib/p2/compiler.rb +38 -11
- data/lib/p2/proc_ext.rb +13 -2
- data/lib/p2/template.rb +4 -2
- data/lib/p2/version.rb +1 -1
- data/lib/p2.rb +22 -9
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c67f363b3f2f756ab72d89bc9d54f3e7c717a0134b080d522cd7d87923ee21b2
|
4
|
+
data.tar.gz: 4d8ed90dae13469d324cebef15735386c12161edba87fac791aa8cbdf60f3ba8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5af31de4e3673acf332c748dbfec372b998958f3807fa410951ec7bf28e33e0fcda2adb92f9455486bc57b1cf40147d43451d31f7ea452e4ff5036accca607d
|
7
|
+
data.tar.gz: 164dbf6b2e03df22a85a6eb2267d34cd2e9dd4c51a1761bb469cd57e1c3167916438cffb8d66e6a8e62d225568a57f5d053e3da8cf478f56d3118fa459a4cb49
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# 2.6 2025-08-16
|
2
|
+
|
3
|
+
- Add support for block invocation.
|
4
|
+
|
5
|
+
# 2.5 2025-08-15
|
6
|
+
|
7
|
+
- Translate backtrace for exceptions raised in `#render_to_buffer`.
|
8
|
+
- Improve display of backtrace when source map is missing entries.
|
9
|
+
- Improve handling of ArgumentError raised on calling the template.
|
10
|
+
- Add `Template#apply`, `Template#compiled_proc` methods
|
11
|
+
|
1
12
|
# 2.4 2025-08-10
|
2
13
|
|
3
14
|
- Add P2::Template wrapper class
|
data/lib/p2/compiler/nodes.rb
CHANGED
@@ -78,6 +78,19 @@ module P2
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
+
class ConstTagNode
|
82
|
+
attr_reader :call_node, :location
|
83
|
+
|
84
|
+
def initialize(call_node, translator)
|
85
|
+
@call_node = call_node
|
86
|
+
@location = call_node.location
|
87
|
+
end
|
88
|
+
|
89
|
+
def accept(visitor)
|
90
|
+
visitor.visit_const_tag_node(self)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
81
94
|
# Represents a text call
|
82
95
|
class TextNode
|
83
96
|
attr_reader :call_node, :location
|
@@ -137,3 +150,18 @@ module P2
|
|
137
150
|
end
|
138
151
|
end
|
139
152
|
end
|
153
|
+
|
154
|
+
class BlockInvocationNode
|
155
|
+
attr_reader :call_node, :location, :block
|
156
|
+
|
157
|
+
def initialize(call_node, translator)
|
158
|
+
@call_node = call_node
|
159
|
+
@tag = call_node.name
|
160
|
+
@location = call_node.location
|
161
|
+
@block = call_node.block && translator.visit(call_node.block)
|
162
|
+
end
|
163
|
+
|
164
|
+
def accept(visitor)
|
165
|
+
visitor.visit_block_invocation_node(self)
|
166
|
+
end
|
167
|
+
end
|
@@ -10,22 +10,32 @@ module P2
|
|
10
10
|
class TagTranslator < Prism::MutationCompiler
|
11
11
|
include Prism::DSL
|
12
12
|
|
13
|
-
def
|
14
|
-
|
13
|
+
def initialize(root)
|
14
|
+
@root = root
|
15
|
+
super()
|
15
16
|
end
|
16
17
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
18
|
+
def self.transform(ast, root)
|
19
|
+
ast.accept(new(root))
|
20
|
+
end
|
21
|
+
|
22
|
+
def visit_call_node(node, dont_translate: false)
|
23
|
+
return super(node) if dont_translate
|
24
|
+
|
25
|
+
match_builtin(node) ||
|
26
|
+
match_emit_yield(node) ||
|
27
|
+
match_const_tag(node) ||
|
28
|
+
match_block_call(node) ||
|
29
|
+
match_tag(node) ||
|
30
|
+
super(node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def match_builtin(node)
|
34
|
+
return if node.receiver
|
20
35
|
|
21
36
|
case node.name
|
22
|
-
when :emit_yield
|
23
|
-
yield_node(
|
24
|
-
location: node.location,
|
25
|
-
arguments: node.arguments
|
26
|
-
)
|
27
37
|
when :raise
|
28
|
-
|
38
|
+
visit_call_node(node, dont_translate: true)
|
29
39
|
when :render
|
30
40
|
RenderNode.new(node, self)
|
31
41
|
when :raw
|
@@ -37,8 +47,46 @@ module P2
|
|
37
47
|
when :html5, :markdown
|
38
48
|
BuiltinNode.new(node, self)
|
39
49
|
else
|
40
|
-
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def match_emit_yield(node)
|
55
|
+
return if node.receiver
|
56
|
+
return if node.name != :emit_yield
|
57
|
+
|
58
|
+
yield_node(
|
59
|
+
location: node.location,
|
60
|
+
arguments: node.arguments
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def match_const_tag(node)
|
65
|
+
return if node.receiver
|
66
|
+
return if node.name !~ /^[A-Z]/
|
67
|
+
|
68
|
+
ConstTagNode.new(node, self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def match_block_call(node)
|
72
|
+
return if !node.receiver
|
73
|
+
return if node.name != :call
|
74
|
+
|
75
|
+
receiver = node.receiver
|
76
|
+
return if !receiver.is_a?(Prism::LocalVariableReadNode)
|
77
|
+
return if @root.parameters&.parameters.block&.name != receiver.name
|
78
|
+
|
79
|
+
if node.block
|
80
|
+
raise P2::Error, 'No support for proc invocation with block'
|
41
81
|
end
|
82
|
+
|
83
|
+
BlockInvocationNode.new(node, self)
|
84
|
+
end
|
85
|
+
|
86
|
+
def match_tag(node)
|
87
|
+
return if node.receiver
|
88
|
+
|
89
|
+
TagNode.new(node, self)
|
42
90
|
end
|
43
91
|
end
|
44
92
|
end
|
data/lib/p2/compiler.rb
CHANGED
@@ -23,7 +23,7 @@ module P2
|
|
23
23
|
ast = ast.block if ast.is_a?(Prism::CallNode)
|
24
24
|
|
25
25
|
compiler = new.with_source_map(proc, ast)
|
26
|
-
transformed_ast = TagTranslator.transform(ast.body)
|
26
|
+
transformed_ast = TagTranslator.transform(ast.body, ast)
|
27
27
|
compiler.format_compiled_template(transformed_ast, ast, wrap:, binding: proc.binding)
|
28
28
|
[compiler.source_map, compiler.buffer]
|
29
29
|
end
|
@@ -57,6 +57,10 @@ module P2
|
|
57
57
|
source_map_store[fn] = source_map
|
58
58
|
end
|
59
59
|
|
60
|
+
def self.source_location_to_fn(source_location)
|
61
|
+
"::(#{source_location.join(':')})"
|
62
|
+
end
|
63
|
+
|
60
64
|
attr_reader :source_map
|
61
65
|
|
62
66
|
# Initializes a compiler.
|
@@ -74,10 +78,10 @@ module P2
|
|
74
78
|
# @param orig_ast [Prism::Node] template AST
|
75
79
|
# @return [self]
|
76
80
|
def with_source_map(orig_proc, orig_ast)
|
77
|
-
|
81
|
+
fn = Compiler.source_location_to_fn(orig_proc.source_location)
|
78
82
|
@source_map = {
|
79
83
|
source_fn: orig_proc.source_location.first,
|
80
|
-
compiled_fn:
|
84
|
+
compiled_fn: fn
|
81
85
|
}
|
82
86
|
@source_map_line_ofs = 2
|
83
87
|
self
|
@@ -92,6 +96,7 @@ module P2
|
|
92
96
|
def format_compiled_template(ast, orig_ast, wrap:, binding:)
|
93
97
|
# generate source code
|
94
98
|
@binding = binding
|
99
|
+
update_source_map
|
95
100
|
visit(ast)
|
96
101
|
flush_html_parts!(semicolon_prefix: true)
|
97
102
|
update_source_map
|
@@ -99,6 +104,7 @@ module P2
|
|
99
104
|
source_code = @buffer
|
100
105
|
@buffer = +''
|
101
106
|
if wrap
|
107
|
+
@source_map[2] = loc_start(orig_ast.location).first
|
102
108
|
emit("# frozen_string_literal: true\n->(__buffer__")
|
103
109
|
|
104
110
|
params = orig_ast.parameters
|
@@ -134,10 +140,8 @@ module P2
|
|
134
140
|
# @return [void]
|
135
141
|
def visit_tag_node(node)
|
136
142
|
tag = node.tag
|
137
|
-
if tag.is_a?(Symbol) && tag =~ /^[A-Z]/
|
138
|
-
return visit_const_tag_node(node.call_node)
|
139
|
-
end
|
140
143
|
|
144
|
+
adjust_whitespace(node.location)
|
141
145
|
is_void = is_void_element?(tag)
|
142
146
|
emit_html(node.tag_location, format_html_tag_open(tag, node.attributes))
|
143
147
|
return if is_void
|
@@ -170,14 +174,14 @@ module P2
|
|
170
174
|
def visit_const_tag_node(node)
|
171
175
|
flush_html_parts!
|
172
176
|
adjust_whitespace(node.location)
|
173
|
-
if node.receiver
|
174
|
-
emit(node.receiver.location)
|
177
|
+
if node.call_node.receiver
|
178
|
+
emit(node.call_node.receiver.location)
|
175
179
|
emit('::')
|
176
180
|
end
|
177
|
-
emit("; #{node.name}.compiled_proc.(__buffer__")
|
178
|
-
if node.arguments
|
181
|
+
emit("; #{node.call_node.name}.compiled_proc.(__buffer__")
|
182
|
+
if node.call_node.arguments
|
179
183
|
emit(', ')
|
180
|
-
visit(node.arguments)
|
184
|
+
visit(node.call_node.arguments)
|
181
185
|
end
|
182
186
|
emit(');')
|
183
187
|
end
|
@@ -304,9 +308,32 @@ module P2
|
|
304
308
|
emit(") : raise(LocalJumpError, 'no block given (yield/emit_yield)'))")
|
305
309
|
end
|
306
310
|
|
311
|
+
def visit_block_invocation_node(node)
|
312
|
+
flush_html_parts!
|
313
|
+
adjust_whitespace(node.location)
|
314
|
+
|
315
|
+
emit("; #{node.call_node.receiver.name}.compiled_proc.(__buffer__")
|
316
|
+
if node.call_node.arguments
|
317
|
+
emit(', ')
|
318
|
+
visit(node.call_node.arguments)
|
319
|
+
end
|
320
|
+
if node.call_node.block
|
321
|
+
emit(", &(->")
|
322
|
+
visit(node.call_node.block)
|
323
|
+
emit(").compiled_proc")
|
324
|
+
end
|
325
|
+
emit(")")
|
326
|
+
end
|
327
|
+
|
307
328
|
private
|
308
329
|
|
309
330
|
# Overrides the Sourcifier behaviour to flush any buffered HTML parts.
|
331
|
+
#
|
332
|
+
# @param loc [Prism::Location] location
|
333
|
+
# @param semicolon [bool] prefix a semicolon before emitted code
|
334
|
+
# @param chomp [bool] chomp the emitted code
|
335
|
+
# @param flush_html [bool] flush pending HTML parts before emitting the code
|
336
|
+
# @return [void]
|
310
337
|
def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
|
311
338
|
flush_html_parts! if flush_html
|
312
339
|
super(loc, semicolon:, chomp: )
|
data/lib/p2/proc_ext.rb
CHANGED
@@ -11,6 +11,16 @@ class ::Proc
|
|
11
11
|
P2::Compiler.compile_to_code(self).last
|
12
12
|
end
|
13
13
|
|
14
|
+
def source_map
|
15
|
+
loc = source_location
|
16
|
+
fn = compiled? ? loc.first : P2::Compiler.source_location_to_fn(loc)
|
17
|
+
P2::Compiler.source_map_store[fn]
|
18
|
+
end
|
19
|
+
|
20
|
+
def ast
|
21
|
+
Sirop.to_ast(self)
|
22
|
+
end
|
23
|
+
|
14
24
|
# Returns true if proc is marked as compiled
|
15
25
|
#
|
16
26
|
# @return [bool] is the proc marked as compiled
|
@@ -50,8 +60,7 @@ class ::Proc
|
|
50
60
|
def render(*a, **b, &c)
|
51
61
|
compiled_proc.(+'', *a, **b, &c)
|
52
62
|
rescue Exception => e
|
53
|
-
P2.translate_backtrace(e)
|
54
|
-
raise e
|
63
|
+
raise P2.translate_backtrace(e)
|
55
64
|
end
|
56
65
|
|
57
66
|
# Renders the proc into the given buffer
|
@@ -59,6 +68,8 @@ class ::Proc
|
|
59
68
|
# @return [String] HTML string
|
60
69
|
def render_to_buffer(buf, *a, **b, &c)
|
61
70
|
compiled_proc.(buf, *a, **b, &c)
|
71
|
+
rescue Exception => e
|
72
|
+
raise P2.translate_backtrace(e)
|
62
73
|
end
|
63
74
|
|
64
75
|
# Returns a proc that applies the given arguments to the original proc
|
data/lib/p2/template.rb
CHANGED
@@ -5,7 +5,9 @@ module P2
|
|
5
5
|
# templates and other kinds of procs.
|
6
6
|
class Template
|
7
7
|
attr_reader :proc
|
8
|
-
def initialize(proc)
|
9
|
-
def render(*, **, &)
|
8
|
+
def initialize(proc) = @proc = proc
|
9
|
+
def render(*, **, &) = @proc.render(*, **, &)
|
10
|
+
def apply(*, **, &) = Template.new(@proc.apply(*, **, &))
|
11
|
+
def compiled_proc = @proc.compiled_proc
|
10
12
|
end
|
11
13
|
end
|
data/lib/p2/version.rb
CHANGED
data/lib/p2.rb
CHANGED
@@ -39,21 +39,23 @@ module P2
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
# Translates
|
43
|
-
#
|
44
|
-
# @param exception [Exception] raised exception
|
45
|
-
# @param source_map [Hash] source map
|
42
|
+
# Translates entries in exception's backtrace to point to original source code.
|
46
43
|
#
|
44
|
+
# @param err [Exception] raised exception
|
47
45
|
# @return [Exception] raised exception
|
48
|
-
def translate_backtrace(
|
46
|
+
def translate_backtrace(err)
|
49
47
|
cache = {}
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
is_argument_error = err.is_a?(ArgumentError) && err.backtrace[0] =~ /^\:\:/
|
49
|
+
backtrace = err.backtrace.map { |e| compute_backtrace_entry(e, cache) }
|
50
|
+
|
51
|
+
return make_argument_error(err, backtrace) if is_argument_error
|
52
|
+
|
53
|
+
err.set_backtrace(backtrace)
|
54
|
+
err
|
53
55
|
end
|
54
56
|
|
55
57
|
# Computes a backtrace entry with caching.
|
56
|
-
#
|
58
|
+
#
|
57
59
|
# @param entry [String] backtrace entry
|
58
60
|
# @param cache [Hash] cache store mapping compiled filename to source_map
|
59
61
|
def compute_backtrace_entry(entry, cache)
|
@@ -69,6 +71,17 @@ module P2
|
|
69
71
|
entry.sub(m[1], "#{source_map[:source_fn]}:#{source_line}")
|
70
72
|
end
|
71
73
|
|
74
|
+
def make_argument_error(err, backtrace)
|
75
|
+
m = err.message.match(/(given (\d+), expected (\d+))/)
|
76
|
+
if m
|
77
|
+
rectified = format('given %d, expected %d', m[2].to_i - 1, m[3].to_i - 1)
|
78
|
+
message = err.message.gsub(m[1], rectified)
|
79
|
+
else
|
80
|
+
message = err.message
|
81
|
+
end
|
82
|
+
ArgumentError.new(message).tap { it.set_backtrace(backtrace) }
|
83
|
+
end
|
84
|
+
|
72
85
|
# Renders Markdown into HTML. The `opts` argument will be merged with the
|
73
86
|
# default Kramdown options in order to change the rendering behaviour.
|
74
87
|
#
|