p2 2.4 → 2.8

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: 43fb47ab7470259fddb850f19724caf5b8fdc07a232df7d893405a1031211313
4
- data.tar.gz: 80999e97751f662940ad37f23f9fe10a4ce7914bb80084dc1ffe120d1cafc872
3
+ metadata.gz: a84949b86e99acbe131d37cb624dbf60010fe720ce27081d5e5652b33f2de004
4
+ data.tar.gz: c03bef8c358bae1536c04739c2fa67bd3ce6e879fd036f2e7352c6291677b888
5
5
  SHA512:
6
- metadata.gz: ac2f9c15a39756f9da5da13d5a5cfcc9e4819574da83851c9e526c0d687b16cd36d6634bcabee3f3e15d59e199a54c6915e9251d87749aa4ff187f1c73c44d8e
7
- data.tar.gz: 537dc36e03464ddf332bd63b73bc8cbdbc2e00063480307df60dabba4afed5829ebfa851c69f28cfbd161d292e5e01af183e28fe114532b6aac997caae651b21
6
+ metadata.gz: 2993a42368846048e02ca7cb01d2ba6f7be013870a5f39597478442d16659b730356443b1b77ee8bb36c6b9a3942bde7d8ef19e66ad087d2a69d9b686d5cc706
7
+ data.tar.gz: 7739be72dd3bdf195188339509a21e24bccbae6ac8efb140a16780445f71b60fea12ab0a87831f117422b620102c277391f33db944b94b7945088f16cf099d73
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # 2.8 2025-08-17
2
+
3
+ - Add render_children builtin
4
+ - Rename emit_yield to render_yield
5
+ - Add `Proc#render_cached` for caching render result
6
+
7
+ # 2.7 2025-08-17
8
+
9
+ - Improve source maps and whitespace in compiled code
10
+ - Minor improvements to emit_yield generated code
11
+ - Add support for extensions
12
+
13
+ # 2.6 2025-08-16
14
+
15
+ - Add support for block invocation.
16
+
17
+ # 2.5 2025-08-15
18
+
19
+ - Translate backtrace for exceptions raised in `#render_to_buffer`.
20
+ - Improve display of backtrace when source map is missing entries.
21
+ - Improve handling of ArgumentError raised on calling the template.
22
+ - Add `Template#apply`, `Template#compiled_proc` methods
23
+
1
24
  # 2.4 2025-08-10
2
25
 
3
26
  - Add P2::Template wrapper class
data/README.md CHANGED
@@ -30,7 +30,7 @@ require 'p2'
30
30
  page = ->(**props) {
31
31
  html {
32
32
  head { title 'My Title' }
33
- body { emit_yield **props }
33
+ body { render_yield **props }
34
34
  }
35
35
  }
36
36
  page.render {
@@ -45,8 +45,8 @@ templates are expressed as Ruby procs, leading to easier debugging, better
45
45
  protection against HTML injection attacks, and better code reuse.
46
46
 
47
47
  P2 templates can be composed in a variety of ways, facilitating the usage of
48
- layout templates, and enabling a component-oriented approach to building complex
49
- web interfaces.
48
+ layout templates, and enabling a component-oriented approach to building web
49
+ interfaces of arbitrary complexity.
50
50
 
51
51
  In P2, dynamic data is passed explicitly to the template as block/lambda
52
52
  arguments, making the data flow easy to follow and understand. P2 also lets
@@ -77,19 +77,21 @@ hello.render(name: 'world')
77
77
  P2 features:
78
78
 
79
79
  - Express HTML using plain Ruby procs.
80
- - Deferred rendering using `defer`.
81
- - Template composition (for uses such as layouts).
82
- - Markdown rendering using [Kramdown](https://github.com/gettalong/kramdown/).
83
80
  - Automatic compilation for super-fast execution (about as
84
81
  [fast](https://github.com/digital-fabric/p2/blob/master/examples/perf.rb) as
85
82
  compiled ERB/ERubi).
83
+ - Deferred rendering using `defer`.
84
+ - Simple and easy template composition (for uses such as layouts, or modular
85
+ templates).
86
+ - Markdown rendering using [Kramdown](https://github.com/gettalong/kramdown/).
87
+ - Support for extensions.
88
+ - Simple caching API for caching the rendering result.
86
89
 
87
90
  ## Table of Content
88
91
 
89
- - [Basic Usage](#basic-usage)
90
- - [Adding Tags](#adding-tags)
91
- - [Tag and Attribute Formatting](#tag-and-attribute-formatting)
92
- - [Escaping Content](#escaping-content)
92
+ - [Getting Started](#getting-started)
93
+ - [Basic Markup](#markup)
94
+ - [Builtin Methods](#builtin-methods)
93
95
  - [Template Parameters](#template-parameters)
94
96
  - [Template Logic](#template-logic)
95
97
  - [Template Blocks](#template-blocks)
@@ -103,7 +105,7 @@ P2 features:
103
105
  - [Deferred Evaluation](#deferred-evaluation)
104
106
  - [API Reference](#api-reference)
105
107
 
106
- ## Basic Usage
108
+ ## Getting Started
107
109
 
108
110
  In P2, an HTML template is expressed as a proc:
109
111
 
@@ -113,7 +115,7 @@ html = -> {
113
115
  }
114
116
  ```
115
117
 
116
- Rendering a template is done using `#render`:
118
+ Rendering a template is done using `Proc#render`:
117
119
 
118
120
  ```ruby
119
121
  require 'p2'
@@ -121,7 +123,7 @@ require 'p2'
121
123
  html.render #=> "<div id="greeter"><p>Hello!</p></div>"
122
124
  ```
123
125
 
124
- ## Expressing HTML Using Ruby
126
+ ## Markup
125
127
 
126
128
  Tags are added using unqualified method calls, and can be nested using blocks:
127
129
 
@@ -172,7 +174,7 @@ An attribute value given as an array will be joined by space characters:
172
174
  -> { div class: [:foo, :bar] }.render #=> "<div class=\"foo bar\"></div>"
173
175
  ```
174
176
 
175
- ## Tag and Attribute Formatting
177
+ ### Tag and Attribute Formatting
176
178
 
177
179
  P2 does not make any assumption about what tags and attributes you can use. You
178
180
  can mix upper and lower case letters, and you can include arbitrary characters
@@ -198,16 +200,137 @@ normally used for tags:
198
200
  }.render #=> '<cra_zy__:!tag>foo</cra_zy__:!tag>'
199
201
  ```
200
202
 
201
- ## Escaping Content
203
+ ### Escaping Content
202
204
 
203
205
  P2 automatically escapes all text content emitted in a template. The specific
204
- escaping algorithm depends on the template type. For HTML templates, P2 uses
205
- [escape_utils](https://github.com/brianmario/escape_utils), specifically:
206
+ escaping algorithm depends on the template type. To emit raw HTML, use the
207
+ `#raw` method as [described below](#builtin-methods).
208
+
209
+ ## Builtin Methods
210
+
211
+ In addition to normal tags, P2 provides the following method calls for templates:
212
+
213
+ ### `#text` - emit escaped text
214
+
215
+ `#text` is used for emitting text that will be escaped. This method can be used
216
+ to emit text not directly inside an enclosing tag:
217
+
218
+ ```ruby
219
+ -> {
220
+ p {
221
+ text 'The time is: '
222
+ span(Time.now, id: 'clock')
223
+ }
224
+ }.render #=> <p>The time is: <span id="clock">XX:XX:XX</span></p>
225
+ ```
226
+
227
+ ### `#raw` - emit raw HTML
228
+
229
+ `#raw` is used for emitting raw HTML, i.e. without escaping. You can use this to
230
+ emit an HTML snippet:
231
+
232
+ ```ruby
233
+ TITLE_HTML = '<h1>hi</h1>'
234
+ -> {
235
+ div {
236
+ raw TITLE_HTML
237
+ }
238
+ }.render #=> <div><h1>hi</h1></div>
239
+ ```
240
+
241
+ ### `#render_yield` - emit given block
242
+
243
+ `#render_yield` is used to emit a given block. If no block is given, a
244
+ `LocalJumpError` exception is raised:
245
+
246
+ ```ruby
247
+ Card = ->(**props) {
248
+ card { render_yield(**props) }
249
+ }
250
+
251
+ Card.render(foo: 'bar') { |foo|
252
+ h1 foo
253
+ } #=> <card><h1>bar</h1></card>
254
+ ```
255
+
256
+ `render_yield` can be called with or without arguments, which are passed to the
257
+ given block.
258
+
259
+ ### `#render_children` - emit given block
260
+
261
+ `#render_children` is used to emit a given block, but does not raise an
262
+ exception if no block is given.
263
+
264
+ ### `#defer` - emit deferred HTML
265
+
266
+ `#defer` is used to emit HTML in a deferred fashion - the deferred part will be
267
+ evaluated only after processing the entire template:
268
+
269
+ ```ruby
270
+ Layout = -> {
271
+ head {
272
+ defer {
273
+ title @title
274
+ }
275
+ }
276
+ body {
277
+ render_yield
278
+ }
279
+ }
280
+
281
+ Layout.render {
282
+ @title = 'Foobar'
283
+ h1 'hi'
284
+ } #=> <head><title>Foobar</title></head><body><h1>hi</h1></body>
285
+ ```
286
+
287
+ ### `#render` - render the given template inline
288
+
289
+ `#render` is used to emit the given template. This can be used to compose
290
+ templates:
206
291
 
207
- - HTML: `escape_utils.escape_html`
292
+ ```ruby
293
+ partial = -> { p 'foo' }
294
+ -> {
295
+ div {
296
+ render partial
297
+ }
298
+ }.render #=> <div><p>foo</p></div>
299
+ ```
300
+
301
+ Any argument following the given template is passed to the template for
302
+ rendering:
303
+
304
+ ```ruby
305
+ large_button = ->(title) { button(title, class: 'large') }
208
306
 
209
- In order to emit raw HTML, you can use the `#emit` method as [described
210
- below](#emitting-raw-html).
307
+ -> {
308
+ render large_button, 'foo'
309
+ }.render #=> <button class="large">foo</button>
310
+ ```
311
+
312
+ ### `#html5` - emit an HTML5 document type declaration and html tag
313
+
314
+ ```ruby
315
+ -> {
316
+ html5 {
317
+ p 'hi'
318
+ }
319
+ } #=> <!DOCTYPE html><html><p>hi</p></html>
320
+ ```
321
+
322
+ ### `#markdown` emit markdown content
323
+
324
+ `#markdown` is used for rendering markdown content. The call converts the given
325
+ markdown to HTML and emits it into the rendered HTML:
326
+
327
+ ```ruby
328
+ -> {
329
+ div {
330
+ markdown 'This is *markdown*'
331
+ }
332
+ }.render #=> <p>This is <em>markdown</em></p>
333
+ ```
211
334
 
212
335
  ## Template Parameters
213
336
 
@@ -216,14 +339,14 @@ parameters are specified as block parameters, and are passed to the template on
216
339
  rendering:
217
340
 
218
341
  ```ruby
219
- greeting = -> { |name| h1 "Hello, #{name}!" }
342
+ greeting = ->(name) { h1 "Hello, #{name}!" }
220
343
  greeting.render('world') #=> "<h1>Hello, world!</h1>"
221
344
  ```
222
345
 
223
346
  Templates can also accept named parameters:
224
347
 
225
348
  ```ruby
226
- greeting = -> { |name:| h1 "Hello, #{name}!" }
349
+ greeting = ->(name:) { h1 "Hello, #{name}!" }
227
350
  greeting.render(name: 'world') #=> "<h1>Hello, world!</h1>"
228
351
  ```
229
352
 
@@ -233,7 +356,7 @@ Since P2 templates are just a bunch of Ruby, you can easily embed your view
233
356
  logic right in the template:
234
357
 
235
358
  ```ruby
236
- -> { |user = nil|
359
+ ->(user = nil) {
237
360
  if user
238
361
  span "Hello, #{user.name}!"
239
362
  else
@@ -244,12 +367,12 @@ logic right in the template:
244
367
 
245
368
  ## Template Blocks
246
369
 
247
- Templates can also accept and render blocks by using `emit_yield`:
370
+ Templates can also accept and render blocks by using `render_yield`:
248
371
 
249
372
  ```ruby
250
373
  page = -> {
251
374
  html {
252
- body { emit_yield }
375
+ body { render_yield }
253
376
  }
254
377
  }
255
378
 
@@ -280,7 +403,7 @@ ItemList = ->(items) {
280
403
  }
281
404
  }
282
405
 
283
- page = -> { |title, items|
406
+ page = ->(title, items) {
284
407
  html5 {
285
408
  head { Title(title) }
286
409
  body { ItemList(items) }
@@ -322,7 +445,7 @@ hello_world = hello.apply('world')
322
445
  hello_world.render #=> "<h1>Hello, world!</h1>"
323
446
 
324
447
  # block application
325
- div_wrap = -> { div { emit_yield } }
448
+ div_wrap = -> { div { render_yield } }
326
449
  wrapped_h1 = div_wrap.apply { h1 'hi' }
327
450
  wrapped_h1.render #=> "<div><h1>hi</h1></div>"
328
451
 
@@ -350,7 +473,7 @@ wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
350
473
  The inner template can also be passed as a block, as shown above:
351
474
 
352
475
  ```ruby
353
- div_wrap = -> { div { emit_yield } }
476
+ div_wrap = -> { div { render_yield } }
354
477
  wrapped_greeter = div_wrap.apply { h1 'hi' }
355
478
  wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
356
479
  ```
@@ -370,7 +493,7 @@ default_layout = -> { |**params|
370
493
  title: params[:title]
371
494
  }
372
495
  body {
373
- emit_yield(**params)
496
+ render_yield(**params)
374
497
  }
375
498
  }
376
499
  }
@@ -477,7 +600,7 @@ default_layout = -> { |**args|
477
600
  head {
478
601
  defer { render deps.head_markup }
479
602
  }
480
- body { emit_yield **args }
603
+ body { render_yield **args }
481
604
  }
482
605
 
483
606
  button = proc { |text, onclick|
@@ -1,8 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ class Prism::InspectVisitor
4
+ def visit_tag_node(node)
5
+ commands << [inspect_node("TagNode", node), indent]
6
+ # flags = [("newline" if node.newline?), ("static_literal" if node.static_literal?), ].compact
7
+ # commands << ["├── flags: #{flags.empty? ? "∅" : flags.join(", ")}\n", indent]
8
+ # commands << ["├── left:\n", indent]
9
+ # commands << [node.left, "#{indent}│ "]
10
+ # commands << ["├── right:\n", indent]
11
+ # commands << [node.right, "#{indent}│ "]
12
+ # commands << ["└── operator_loc: #{inspect_location(node.operator_loc)}\n", indent]
13
+ end
14
+ end
15
+
3
16
  module P2
4
17
  # Represents a tag call
5
- class TagNode
18
+ class TagNode < Prism::Node
6
19
  attr_reader :call_node, :location, :tag, :tag_location, :inner_text, :attributes, :block
7
20
 
8
21
  def initialize(call_node, translator)
@@ -78,6 +91,19 @@ module P2
78
91
  end
79
92
  end
80
93
 
94
+ class ConstTagNode
95
+ attr_reader :call_node, :location
96
+
97
+ def initialize(call_node, translator)
98
+ @call_node = call_node
99
+ @location = call_node.location
100
+ end
101
+
102
+ def accept(visitor)
103
+ visitor.visit_const_tag_node(self)
104
+ end
105
+ end
106
+
81
107
  # Represents a text call
82
108
  class TextNode
83
109
  attr_reader :call_node, :location
@@ -136,4 +162,62 @@ module P2
136
162
  visitor.visit_builtin_node(self)
137
163
  end
138
164
  end
165
+
166
+ class ExtensionTagNode
167
+ attr_reader :tag, :call_node, :location, :block
168
+
169
+ def initialize(call_node, translator)
170
+ @call_node = call_node
171
+ @tag = call_node.name
172
+ @location = call_node.location
173
+ @block = call_node.block && translator.visit(call_node.block)
174
+ end
175
+
176
+ def accept(visitor)
177
+ visitor.visit_extension_tag_node(self)
178
+ end
179
+ end
180
+
181
+ class BlockInvocationNode
182
+ attr_reader :call_node, :location, :block
183
+
184
+ def initialize(call_node, translator)
185
+ @call_node = call_node
186
+ @tag = call_node.name
187
+ @location = call_node.location
188
+ @block = call_node.block && translator.visit(call_node.block)
189
+ end
190
+
191
+ def accept(visitor)
192
+ visitor.visit_block_invocation_node(self)
193
+ end
194
+ end
195
+ end
196
+
197
+ class RenderYieldNode
198
+ attr_reader :call_node, :location
199
+
200
+ def initialize(call_node, translator)
201
+ @call_node = call_node
202
+ @tag = call_node.name
203
+ @location = call_node.location
204
+ end
205
+
206
+ def accept(visitor)
207
+ visitor.visit_render_yield_node(self)
208
+ end
209
+ end
210
+
211
+ class RenderChildrenNode
212
+ attr_reader :call_node, :location
213
+
214
+ def initialize(call_node, translator)
215
+ @call_node = call_node
216
+ @tag = call_node.name
217
+ @location = call_node.location
218
+ end
219
+
220
+ def accept(visitor)
221
+ visitor.visit_render_children_node(self)
222
+ end
139
223
  end
@@ -10,22 +10,36 @@ 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_extension(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
- )
37
+ when :render_yield
38
+ RenderYieldNode.new(node, self)
39
+ when :render_children
40
+ RenderChildrenNode.new(node, self)
27
41
  when :raise
28
- super(node)
42
+ visit_call_node(node, dont_translate: true)
29
43
  when :render
30
44
  RenderNode.new(node, self)
31
45
  when :raw
@@ -37,8 +51,43 @@ module P2
37
51
  when :html5, :markdown
38
52
  BuiltinNode.new(node, self)
39
53
  else
40
- TagNode.new(node, self)
54
+ nil
41
55
  end
42
56
  end
57
+
58
+ def match_extension(node)
59
+ return if node.receiver
60
+ return if !P2::Extensions[node.name]
61
+
62
+ ExtensionTagNode.new(node, self)
63
+ end
64
+
65
+ def match_const_tag(node)
66
+ return if node.receiver
67
+ return if node.name !~ /^[A-Z]/
68
+
69
+ ConstTagNode.new(node, self)
70
+ end
71
+
72
+ def match_block_call(node)
73
+ return if !node.receiver
74
+ return if node.name != :call
75
+
76
+ receiver = node.receiver
77
+ return if !receiver.is_a?(Prism::LocalVariableReadNode)
78
+ return if @root.parameters&.parameters.block&.name != receiver.name
79
+
80
+ if node.block
81
+ raise P2::Error, 'No support for proc invocation with block'
82
+ end
83
+
84
+ BlockInvocationNode.new(node, self)
85
+ end
86
+
87
+ def match_tag(node)
88
+ return if node.receiver
89
+
90
+ TagNode.new(node, self)
91
+ end
43
92
  end
44
93
  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
@@ -53,19 +53,22 @@ module P2
53
53
  end
54
54
 
55
55
  def self.store_source_map(source_map)
56
+ return if !source_map
57
+
56
58
  fn = source_map[:compiled_fn]
57
59
  source_map_store[fn] = source_map
58
60
  end
59
61
 
62
+ def self.source_location_to_fn(source_location)
63
+ "::(#{source_location.join(':')})"
64
+ end
65
+
60
66
  attr_reader :source_map
61
67
 
62
68
  # Initializes a compiler.
63
69
  def initialize(**)
64
70
  super(**)
65
71
  @pending_html_parts = []
66
- @html_loc_start = nil
67
- @html_loc_end = nil
68
- @yield_used = nil
69
72
  end
70
73
 
71
74
  # Initializes a source map.
@@ -74,15 +77,26 @@ module P2
74
77
  # @param orig_ast [Prism::Node] template AST
75
78
  # @return [self]
76
79
  def with_source_map(orig_proc, orig_ast)
77
- compiled_fn = "::(#{orig_proc.source_location.join(':')})"
80
+ fn = Compiler.source_location_to_fn(orig_proc.source_location)
81
+ @orig_proc = orig_proc
82
+ @orig_proc_fn = orig_proc.source_location.first
78
83
  @source_map = {
79
84
  source_fn: orig_proc.source_location.first,
80
- compiled_fn: compiled_fn
85
+ compiled_fn: fn
81
86
  }
82
87
  @source_map_line_ofs = 2
83
88
  self
84
89
  end
85
90
 
91
+ def update_source_map(str = nil)
92
+ return if !@source_map
93
+
94
+ buffer_cur_line = @buffer.count("\n") + 1
95
+ orig_source_cur_line = @last_loc_start ? @last_loc_start.first : '?'
96
+ @source_map[buffer_cur_line + @source_map_line_ofs] ||=
97
+ "#{@orig_proc_fn}:#{orig_source_cur_line}"
98
+ end
99
+
86
100
  # Formats the source code for a compiled template proc.
87
101
  #
88
102
  # @param ast [Prism::Node] translated AST
@@ -92,6 +106,7 @@ module P2
92
106
  def format_compiled_template(ast, orig_ast, wrap:, binding:)
93
107
  # generate source code
94
108
  @binding = binding
109
+ update_source_map
95
110
  visit(ast)
96
111
  flush_html_parts!(semicolon_prefix: true)
97
112
  update_source_map
@@ -99,6 +114,7 @@ module P2
99
114
  source_code = @buffer
100
115
  @buffer = +''
101
116
  if wrap
117
+ @source_map[2] = "#{@orig_proc_fn}:#{loc_start(orig_ast.location).first}"
102
118
  emit("# frozen_string_literal: true\n->(__buffer__")
103
119
 
104
120
  params = orig_ast.parameters
@@ -108,11 +124,12 @@ module P2
108
124
  emit(format_code(params))
109
125
  end
110
126
 
111
- if @yield_used
127
+ if @render_yield_used || @render_children_used
112
128
  emit(', &__block__')
113
129
  end
114
130
 
115
131
  emit(") {\n")
132
+
116
133
  end
117
134
  @buffer << source_code
118
135
  emit_defer_postlude if @defer_mode
@@ -120,8 +137,6 @@ module P2
120
137
  emit('; __buffer__')
121
138
  adjust_whitespace(orig_ast.closing_loc)
122
139
  emit('}')
123
- # emit(";") if @buffer !~ /\n\s*$/m
124
- # emit("rescue Exception => e; P2.translate_backtrace(e, src_map); raise e; end }")
125
140
  end
126
141
  update_source_map
127
142
  Compiler.store_source_map(@source_map)
@@ -134,10 +149,8 @@ module P2
134
149
  # @return [void]
135
150
  def visit_tag_node(node)
136
151
  tag = node.tag
137
- if tag.is_a?(Symbol) && tag =~ /^[A-Z]/
138
- return visit_const_tag_node(node.call_node)
139
- end
140
152
 
153
+ # adjust_whitespace(node.location)
141
154
  is_void = is_void_element?(tag)
142
155
  emit_html(node.tag_location, format_html_tag_open(tag, node.attributes))
143
156
  return if is_void
@@ -170,14 +183,14 @@ module P2
170
183
  def visit_const_tag_node(node)
171
184
  flush_html_parts!
172
185
  adjust_whitespace(node.location)
173
- if node.receiver
174
- emit(node.receiver.location)
186
+ if node.call_node.receiver
187
+ emit(node.call_node.receiver.location)
175
188
  emit('::')
176
189
  end
177
- emit("; #{node.name}.compiled_proc.(__buffer__")
178
- if node.arguments
190
+ emit("; #{node.call_node.name}.compiled_proc.(__buffer__")
191
+ if node.call_node.arguments
179
192
  emit(', ')
180
- visit(node.arguments)
193
+ visit(node.call_node.arguments)
181
194
  end
182
195
  emit(');')
183
196
  end
@@ -288,25 +301,109 @@ module P2
288
301
  end
289
302
  end
290
303
 
291
- # Visits a yield node.
304
+ # Visits a extension tag node.
292
305
  #
293
- # @param node [P2::YieldNode] node
306
+ # @param node [P2::ExtensionTagNode] node
294
307
  # @return [void]
295
- def visit_yield_node(node)
308
+ def visit_extension_tag_node(node)
296
309
  flush_html_parts!
297
310
  adjust_whitespace(node.location)
298
- @yield_used = true
299
- emit("; (__block__ ? __block__.compiled_proc.(__buffer__")
300
- if node.arguments
311
+ emit("; P2::Extensions[#{node.tag.inspect}].compiled_proc.(__buffer__")
312
+ if node.call_node.arguments
301
313
  emit(', ')
302
- visit(node.arguments)
314
+ visit(node.call_node.arguments)
315
+ end
316
+ if node.block
317
+ block_body = format_inline_block(node.block.body)
318
+ block_params = []
319
+
320
+ if node.block.parameters.is_a?(Prism::ItParametersNode)
321
+ raise P2::Error, "Blocks passed to extensions cannot use it parameter"
322
+ end
323
+
324
+ if (params = node.block.parameters&.parameters)
325
+ params.requireds.each do
326
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
327
+ end
328
+ params.optionals.each do
329
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
330
+ end
331
+ block_params << format_code(params.rest) if params.rest
332
+ params.posts.each do
333
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
334
+ end
335
+ params.keywords.each do
336
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
337
+ end
338
+ block_params << format_code(params.keyword_rest) if params.keyword_rest
339
+ end
340
+ block_params = block_params.empty? ? '' : ", #{block_params.join(', ')}"
341
+
342
+ emit(", &(proc { |__buffer__#{block_params}| #{block_body} }).compiled!")
303
343
  end
304
- emit(") : raise(LocalJumpError, 'no block given (yield/emit_yield)'))")
344
+ emit(")")
345
+ end
346
+
347
+ # Visits a render_yield node.
348
+ #
349
+ # @param node [P2::RenderYieldNode] node
350
+ # @return [void]
351
+ def visit_render_yield_node(node)
352
+ flush_html_parts!
353
+ adjust_whitespace(node.location)
354
+ guard = @render_yield_used ?
355
+ '' : "; raise(LocalJumpError, 'no block given (render_yield)') if !__block__"
356
+ @render_yield_used = true
357
+ emit("#{guard}; __block__.compiled_proc.(__buffer__")
358
+ if node.call_node.arguments
359
+ emit(', ')
360
+ visit(node.call_node.arguments)
361
+ end
362
+ emit(")")
363
+ end
364
+
365
+ # Visits a render_children node.
366
+ #
367
+ # @param node [P2::RenderChildrenNode] node
368
+ # @return [void]
369
+ def visit_render_children_node(node)
370
+ flush_html_parts!
371
+ adjust_whitespace(node.location)
372
+ @render_children_used = true
373
+ emit("; __block__&.compiled_proc&.(__buffer__")
374
+ if node.call_node.arguments
375
+ emit(', ')
376
+ visit(node.call_node.arguments)
377
+ end
378
+ emit(")")
379
+ end
380
+
381
+ def visit_block_invocation_node(node)
382
+ flush_html_parts!
383
+ adjust_whitespace(node.location)
384
+
385
+ emit("; #{node.call_node.receiver.name}.compiled_proc.(__buffer__")
386
+ if node.call_node.arguments
387
+ emit(', ')
388
+ visit(node.call_node.arguments)
389
+ end
390
+ if node.call_node.block
391
+ emit(", &(->")
392
+ visit(node.call_node.block)
393
+ emit(").compiled_proc")
394
+ end
395
+ emit(")")
305
396
  end
306
397
 
307
398
  private
308
399
 
309
400
  # Overrides the Sourcifier behaviour to flush any buffered HTML parts.
401
+ #
402
+ # @param loc [Prism::Location] location
403
+ # @param semicolon [bool] prefix a semicolon before emitted code
404
+ # @param chomp [bool] chomp the emitted code
405
+ # @param flush_html [bool] flush pending HTML parts before emitting the code
406
+ # @return [void]
310
407
  def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
311
408
  flush_html_parts! if flush_html
312
409
  super(loc, semicolon:, chomp: )
@@ -329,6 +426,10 @@ module P2
329
426
  Compiler.new(minimize_whitespace: true).to_source(node)
330
427
  end
331
428
 
429
+ def format_inline_block(node)
430
+ Compiler.new(minimize_whitespace: true).format_compiled_template(node, node, wrap: false, binding: @binding)
431
+ end
432
+
332
433
  # Formats a comma separated list of AST nodes. Used for formatting partial
333
434
  # argument lists.
334
435
  #
@@ -485,8 +586,8 @@ module P2
485
586
  # @return [void]
486
587
  def emit_html(loc, str)
487
588
  @html_loc_start ||= loc
488
- @html_loc_end = loc
489
- @pending_html_parts << str
589
+ @html_loc_end ||= loc
590
+ @pending_html_parts << [loc, str]
490
591
  end
491
592
 
492
593
  # Flushes pending HTML parts to the source code buffer.
@@ -495,31 +596,30 @@ module P2
495
596
  def flush_html_parts!(semicolon_prefix: true)
496
597
  return if @pending_html_parts.empty?
497
598
 
498
- adjust_whitespace(@html_loc_start)
499
-
500
- code = +''
501
- part = +''
599
+ adjust_whitespace(@html_loc_start, advance_to_end: false)
600
+ concatenated = +''
502
601
 
503
- @pending_html_parts.each do
504
- if (m = it.match(/^#\{(.+)\}$/m))
505
- emit_html_buffer_push(code, part, quotes: true) if !part.empty?
506
- emit_html_buffer_push(code, m[1])
602
+ last_loc = @html_loc_start
603
+ @pending_html_parts.each do |(loc, part)|
604
+ if (m = part.match(/^#\{(.+)\}$/m))
605
+ emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
606
+ adjust_whitespace(loc, advance_to_end: false)
607
+ emit_html_buffer_push(m[1])
507
608
  else
508
- part << it
609
+ concatenated << part
509
610
  end
611
+ last_loc = loc
510
612
  end
511
- emit_html_buffer_push(code, part, quotes: true) if !part.empty?
613
+ emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
512
614
 
513
615
  @pending_html_parts.clear
514
616
 
515
- @last_loc = @html_loc_end
516
- @last_loc_start = loc_start(@html_loc_end)
517
- @last_loc_end = loc_end(@html_loc_end)
617
+ @last_loc = last_loc
618
+ @last_loc_start = loc_start(@last_loc)
619
+ @last_loc_end = @last_loc_start
518
620
 
519
621
  @html_loc_start = nil
520
622
  @html_loc_end = nil
521
-
522
- emit code
523
623
  end
524
624
 
525
625
  # Emits HTML buffer push code to the given source code buffer.
@@ -528,11 +628,11 @@ module P2
528
628
  # @param part [String] HTML part
529
629
  # @param quotes [bool] whether to wrap emitted HTML in double quotes
530
630
  # @return [void]
531
- def emit_html_buffer_push(buf, part, quotes: false)
631
+ def emit_html_buffer_push(part, quotes: false)
532
632
  return if part.empty?
533
633
 
534
634
  q = quotes ? '"' : ''
535
- buf << "; __buffer__ << #{q}#{part}#{q}"
635
+ emit("; __buffer__ << #{q}#{part}#{q}")
536
636
  part.clear
537
637
  end
538
638
 
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
@@ -40,7 +50,10 @@ class ::Proc
40
50
  # @return [Proc] compiled proc
41
51
  def compile
42
52
  P2::Compiler.compile(self).compiled!
43
- rescue Sirop::Error
53
+ rescue Sirop::Error => e
54
+ puts '!' * 40
55
+ p self
56
+ p e
44
57
  raise P2::Error, "Dynamically defined procs cannot be compiled"
45
58
  end
46
59
 
@@ -50,8 +63,7 @@ class ::Proc
50
63
  def render(*a, **b, &c)
51
64
  compiled_proc.(+'', *a, **b, &c)
52
65
  rescue Exception => e
53
- P2.translate_backtrace(e)
54
- raise e
66
+ raise P2.translate_backtrace(e)
55
67
  end
56
68
 
57
69
  # Renders the proc into the given buffer
@@ -59,6 +71,8 @@ class ::Proc
59
71
  # @return [String] HTML string
60
72
  def render_to_buffer(buf, *a, **b, &c)
61
73
  compiled_proc.(buf, *a, **b, &c)
74
+ rescue Exception => e
75
+ raise P2.translate_backtrace(e)
62
76
  end
63
77
 
64
78
  # Returns a proc that applies the given arguments to the original proc
@@ -76,4 +90,11 @@ class ::Proc
76
90
  compiled.(__buffer__, *a, *x, **b, **y, &c_proc)
77
91
  }.compiled!
78
92
  end
93
+
94
+ # Caches and returns
95
+ def render_cached(*args, **kargs)
96
+ @render_cache ||= {}
97
+ key = args.empty? && kargs.empty? ? nil : [args, kargs]
98
+ @render_cache[key] ||= render(*args, **kargs)
99
+ end
79
100
  end
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) = @proc = proc
9
- def render(*, **, &) = @proc.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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module P2
4
- VERSION = '2.4'
4
+ VERSION = '2.8'
5
5
  end
data/lib/p2.rb CHANGED
@@ -12,6 +12,18 @@ module P2
12
12
 
13
13
  extend self
14
14
 
15
+ # Registry of P2 exgtensions
16
+ Extensions = {}
17
+
18
+ # Registers extensions to the P2 syntax.
19
+ #
20
+ # @param spec [Hash] hash mapping symbols to procs
21
+ # @return [self]
22
+ def extension(spec)
23
+ Extensions.merge!(spec)
24
+ self
25
+ end
26
+
15
27
  # Formats the given string, converting underscores to dashes.
16
28
  #
17
29
  # @param tag [String, Symbol] input string
@@ -39,21 +51,23 @@ module P2
39
51
  end
40
52
  end
41
53
 
42
- # Translates an exceptions backtrace using a source map.
43
- #
44
- # @param exception [Exception] raised exception
45
- # @param source_map [Hash] source map
54
+ # Translates entries in exception's backtrace to point to original source code.
46
55
  #
56
+ # @param err [Exception] raised exception
47
57
  # @return [Exception] raised exception
48
- def translate_backtrace(exception)
58
+ def translate_backtrace(err)
49
59
  cache = {}
50
- backtrace = exception.backtrace.map { |e| compute_backtrace_entry(e, cache) }
51
- exception.set_backtrace(backtrace)
52
- exception
60
+ is_argument_error = err.is_a?(ArgumentError) && err.backtrace[0] =~ /^\:\:/
61
+ backtrace = err.backtrace.map { |e| compute_backtrace_entry(e, cache) }
62
+
63
+ return make_argument_error(err, backtrace) if is_argument_error
64
+
65
+ err.set_backtrace(backtrace)
66
+ err
53
67
  end
54
68
 
55
69
  # Computes a backtrace entry with caching.
56
- #
70
+ #
57
71
  # @param entry [String] backtrace entry
58
72
  # @param cache [Hash] cache store mapping compiled filename to source_map
59
73
  def compute_backtrace_entry(entry, cache)
@@ -65,8 +79,19 @@ module P2
65
79
  source_map = cache[fn] ||= Compiler.source_map_store[fn]
66
80
  return entry if !source_map
67
81
 
68
- source_line = source_map[line] || "?(#{line})"
69
- entry.sub(m[1], "#{source_map[:source_fn]}:#{source_line}")
82
+ ref = source_map[line] || "?(#{line})"
83
+ entry.sub(m[1], ref)
84
+ end
85
+
86
+ def make_argument_error(err, backtrace)
87
+ m = err.message.match(/(given (\d+), expected (\d+))/)
88
+ if m
89
+ rectified = format('given %d, expected %d', m[2].to_i - 1, m[3].to_i - 1)
90
+ message = err.message.gsub(m[1], rectified)
91
+ else
92
+ message = err.message
93
+ end
94
+ ArgumentError.new(message).tap { it.set_backtrace(backtrace) }
70
95
  end
71
96
 
72
97
  # Renders Markdown into HTML. The `opts` argument will be merged with the
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.4'
4
+ version: '2.8'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.8.3
18
+ version: '0.9'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.8.3
25
+ version: '0.9'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: kramdown
28
28
  requirement: !ruby/object:Gem::Requirement