p2 2.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad3bf1a720998e9b118a15d4a7b57677c5938bf74d6f65f474653ecaf58a9387
4
- data.tar.gz: 6299a9c113a3bf8cc3c0a2c745ba281f7f42304ae7d127fca907b2cead73788c
3
+ metadata.gz: c67f363b3f2f756ab72d89bc9d54f3e7c717a0134b080d522cd7d87923ee21b2
4
+ data.tar.gz: 4d8ed90dae13469d324cebef15735386c12161edba87fac791aa8cbdf60f3ba8
5
5
  SHA512:
6
- metadata.gz: 8fffd7f51d9efd4a537f5e870987bf7cc849aaf3a22a912cb36f6f2ab046259a1191201966e9497941b863a7e0ac35a2dc49d4e6ecf93445d6d1ed0d4725fc09
7
- data.tar.gz: 6b7165c8d382c91d97d7a4799e2da53e80cacb2100ce7703017af7adee1ec53c0a474a2ff6798231ec848e5463ffce26231026f0f135057803b4d9cf53ac9090
6
+ metadata.gz: f5af31de4e3673acf332c748dbfec372b998958f3807fa410951ec7bf28e33e0fcda2adb92f9455486bc57b1cf40147d43451d31f7ea452e4ff5036accca607d
7
+ data.tar.gz: 164dbf6b2e03df22a85a6eb2267d34cd2e9dd4c51a1761bb469cd57e1c3167916438cffb8d66e6a8e62d225568a57f5d053e3da8cf478f56d3118fa459a4cb49
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
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
+
12
+ # 2.4 2025-08-10
13
+
14
+ - Add P2::Template wrapper class
15
+
1
16
  # 2.3 2025-08-10
2
17
 
3
18
  - Fix whitespace issue in visit_yield_node
@@ -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 self.transform(ast)
14
- ast.accept(new)
13
+ def initialize(root)
14
+ @root = root
15
+ super()
15
16
  end
16
17
 
17
- def visit_call_node(node)
18
- # We're only interested in compiling method calls without a receiver
19
- return super(node) if node.receiver
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
- super(node)
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
- TagNode.new(node, self)
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
- compiled_fn = "::(#{orig_proc.source_location.join(':')})"
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: 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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module P2
4
+ # Template wrapper class. This class can be used to distinguish between P2
5
+ # templates and other kinds of procs.
6
+ class Template
7
+ attr_reader :proc
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
12
+ end
13
+ end
data/lib/p2/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module P2
4
- VERSION = '2.3'
4
+ VERSION = '2.6'
5
5
  end
data/lib/p2.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'p2/template'
3
4
  require_relative 'p2/compiler'
4
5
  require_relative 'p2/proc_ext'
5
6
 
@@ -38,21 +39,23 @@ module P2
38
39
  end
39
40
  end
40
41
 
41
- # Translates an exceptions backtrace using a source map.
42
- #
43
- # @param exception [Exception] raised exception
44
- # @param source_map [Hash] source map
42
+ # Translates entries in exception's backtrace to point to original source code.
45
43
  #
44
+ # @param err [Exception] raised exception
46
45
  # @return [Exception] raised exception
47
- def translate_backtrace(exception)
46
+ def translate_backtrace(err)
48
47
  cache = {}
49
- backtrace = exception.backtrace.map { |e| compute_backtrace_entry(e, cache) }
50
- exception.set_backtrace(backtrace)
51
- exception
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
52
55
  end
53
56
 
54
57
  # Computes a backtrace entry with caching.
55
- #
58
+ #
56
59
  # @param entry [String] backtrace entry
57
60
  # @param cache [Hash] cache store mapping compiled filename to source_map
58
61
  def compute_backtrace_entry(entry, cache)
@@ -68,6 +71,17 @@ module P2
68
71
  entry.sub(m[1], "#{source_map[:source_fn]}:#{source_line}")
69
72
  end
70
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
+
71
85
  # Renders Markdown into HTML. The `opts` argument will be merged with the
72
86
  # default Kramdown options in order to change the rendering behaviour.
73
87
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: p2
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.3'
4
+ version: '2.6'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -107,6 +107,7 @@ files:
107
107
  - lib/p2/compiler/nodes.rb
108
108
  - lib/p2/compiler/tag_translator.rb
109
109
  - lib/p2/proc_ext.rb
110
+ - lib/p2/template.rb
110
111
  - lib/p2/version.rb
111
112
  - p2.png
112
113
  homepage: http://github.com/digital-fabric/p2