p2 2.6 → 2.9

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: c67f363b3f2f756ab72d89bc9d54f3e7c717a0134b080d522cd7d87923ee21b2
4
- data.tar.gz: 4d8ed90dae13469d324cebef15735386c12161edba87fac791aa8cbdf60f3ba8
3
+ metadata.gz: bda78e0c4b37c895274b5d357022f3f491bf6b1fdab4d52cdc4e7aa7660f2f6a
4
+ data.tar.gz: f9e1295f3208cb6b734d65d20afa41fcb46803b37b51b7d801ab56715fe24db8
5
5
  SHA512:
6
- metadata.gz: f5af31de4e3673acf332c748dbfec372b998958f3807fa410951ec7bf28e33e0fcda2adb92f9455486bc57b1cf40147d43451d31f7ea452e4ff5036accca607d
7
- data.tar.gz: 164dbf6b2e03df22a85a6eb2267d34cd2e9dd4c51a1761bb469cd57e1c3167916438cffb8d66e6a8e62d225568a57f5d053e3da8cf478f56d3118fa459a4cb49
6
+ metadata.gz: 80891d65d533e7fabac367d342c3884c0da607e5c815821ace691fe62e5ae393983e89c60cc226b69627b0ebb66bf7f1d91ae89f8be6d8baca9620498364c756
7
+ data.tar.gz: 3e969bb24fbc9ec1668757e5ac152baf0052928937bd18d8c708a2031c416d8752cf8b85f3047427de82eafd008abd6126d212e3265fcfbfd9275b6ed4bc0b95
data/CHANGELOG.md CHANGED
@@ -1,12 +1,31 @@
1
+ # 2.9 2025-09-02
2
+
3
+ - Tweak generated code to incorporate @byroot's
4
+ [recommendations](https://www.reddit.com/r/ruby/comments/1mtj7bx/comment/n9ckbvt/):
5
+ - Remove call to to_s coercion before calling html_escape
6
+ - Chain calls to `#<<` with emitted HTML parts
7
+
8
+ # 2.8 2025-08-17
9
+
10
+ - Add `#render_children` builtin
11
+ - Rename `#emit_yield` to `#render_yield`
12
+ - Add `Proc#render_cached` for caching render result
13
+
14
+ # 2.7 2025-08-17
15
+
16
+ - Improve source maps and whitespace in compiled code
17
+ - Minor improvements to emit_yield generated code
18
+ - Add support for extensions
19
+
1
20
  # 2.6 2025-08-16
2
21
 
3
- - Add support for block invocation.
22
+ - Add support for block invocation
4
23
 
5
24
  # 2.5 2025-08-15
6
25
 
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.
26
+ - Translate backtrace for exceptions raised in `#render_to_buffer`
27
+ - Improve display of backtrace when source map is missing entries
28
+ - Improve handling of ArgumentError raised on calling the template
10
29
  - Add `Template#apply`, `Template#compiled_proc` methods
11
30
 
12
31
  # 2.4 2025-08-10
data/README.md CHANGED
@@ -10,8 +10,8 @@
10
10
  <a href="http://rubygems.org/gems/p2">
11
11
  <img src="https://badge.fury.io/rb/p2.svg" alt="Ruby gem">
12
12
  </a>
13
- <a href="https://github.com/digital-fabric/p2/actions?query=workflow%3ATests">
14
- <img src="https://github.com/digital-fabric/p2/workflows/Tests/badge.svg" alt="Tests">
13
+ <a href="https://github.com/digital-fabric/p2/actions/workflows/test.yml">
14
+ <img src="https://github.com/digital-fabric/p2/actions/workflows/test.yml/badge.svg" alt="Tests">
15
15
  </a>
16
16
  <a href="https://github.com/digital-fabric/p2/blob/master/LICENSE">
17
17
  <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
@@ -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 {
@@ -39,14 +39,13 @@ page.render {
39
39
  #=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
40
40
  ```
41
41
 
42
-
43
42
  P2 is a templating engine for dynamically producing HTML in Ruby apps. P2
44
43
  templates are expressed as Ruby procs, leading to easier debugging, better
45
44
  protection against HTML injection attacks, and better code reuse.
46
45
 
47
46
  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.
47
+ layout templates, and enabling a component-oriented approach to building web
48
+ interfaces of arbitrary complexity.
50
49
 
51
50
  In P2, dynamic data is passed explicitly to the template as block/lambda
52
51
  arguments, making the data flow easy to follow and understand. P2 also lets
@@ -77,19 +76,21 @@ hello.render(name: 'world')
77
76
  P2 features:
78
77
 
79
78
  - 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
79
  - Automatic compilation for super-fast execution (about as
84
80
  [fast](https://github.com/digital-fabric/p2/blob/master/examples/perf.rb) as
85
81
  compiled ERB/ERubi).
82
+ - Deferred rendering using `defer`.
83
+ - Simple and easy template composition (for uses such as layouts, or modular
84
+ templates).
85
+ - Markdown rendering using [Kramdown](https://github.com/gettalong/kramdown/).
86
+ - Support for extensions.
87
+ - Simple caching API for caching the rendering result.
86
88
 
87
89
  ## Table of Content
88
90
 
89
- - [Basic Usage](#basic-usage)
90
- - [Adding Tags](#adding-tags)
91
- - [Tag and Attribute Formatting](#tag-and-attribute-formatting)
92
- - [Escaping Content](#escaping-content)
91
+ - [Getting Started](#getting-started)
92
+ - [Basic Markup](#basic-markup)
93
+ - [Builtin Methods](#builtin-methods)
93
94
  - [Template Parameters](#template-parameters)
94
95
  - [Template Logic](#template-logic)
95
96
  - [Template Blocks](#template-blocks)
@@ -97,13 +98,14 @@ P2 features:
97
98
  - [Parameter and Block Application](#parameter-and-block-application)
98
99
  - [Higher-Order Templates](#higher-order-templates)
99
100
  - [Layout Template Composition](#layout-template-composition)
100
- - [Emitting Raw HTML](#emitting-raw-html)
101
- - [Emitting a String with HTML Escaping](#emitting-a-string-with-html-escaping)
102
101
  - [Emitting Markdown](#emitting-markdown)
103
102
  - [Deferred Evaluation](#deferred-evaluation)
104
- - [API Reference](#api-reference)
103
+ - [Cached Rendering](#cached-rendering)
104
+
105
+ A typical example for a dashboard-type app markup can be found here:
106
+ https://github.com/digital-fabric/p2/blob/master/examples/dashboard.rb
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
+ ## Basic 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
+ }
206
280
 
207
- - HTML: `escape_utils.escape_html`
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:
291
+
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') }
306
+
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
208
323
 
209
- In order to emit raw HTML, you can use the `#emit` method as [described
210
- below](#emitting-raw-html).
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
  }
@@ -388,24 +511,6 @@ article_layout.render(
388
511
  )
389
512
  ```
390
513
 
391
- ## Emitting Raw HTML
392
-
393
- Raw HTML can be emitted using `#raw`:
394
-
395
- ```ruby
396
- wrapped = -> { |html| div { raw html } }
397
- wrapped.render("<h1>hi</h1>") #=> "<div><h1>hi</h1></div>"
398
- ```
399
-
400
- ## Emitting a String with HTML Escaping
401
-
402
- To emit a string with proper HTML escaping, without wrapping it in an HTML
403
- element, use `#text`:
404
-
405
- ```ruby
406
- -> { text 'hi&lo' }.render #=> "hi&amp;lo"
407
- ```
408
-
409
514
  ## Emitting Markdown
410
515
 
411
516
  Markdown is rendered using the
@@ -477,7 +582,7 @@ default_layout = -> { |**args|
477
582
  head {
478
583
  defer { render deps.head_markup }
479
584
  }
480
- body { emit_yield **args }
585
+ body { render_yield **args }
481
586
  }
482
587
 
483
588
  button = proc { |text, onclick|
@@ -503,18 +608,17 @@ page = default_layout.apply {
503
608
  }
504
609
  ```
505
610
 
506
- ## HTML Utility methods
611
+ ## Cached Rendering
507
612
 
508
- HTML templates include a few HTML-specific methods to facilitate writing modern
509
- HTML:
613
+ P2 provides a simple API for caching the result of a rendering. The cache stores
614
+ renderings of a template respective to the given arguments. To automatically
615
+ retrieve the cached rendered HTML, or generate it for the first time, use
616
+ `Proc#render_cached`:
510
617
 
511
- - `html5 { ... }` - emits an HTML 5 DOCTYPE (`<!DOCTYPE html>`)
512
- - `import_map(root_path, root_url)` - emits an import map including all files
513
- matching `<root_path>/*.js`, based on the given `root_url`
514
- - `js_module(js)` - emits a `<script type="module">` element
515
- - `link_stylesheet(href, **attributes)` - emits a `<link rel="stylesheet" ...>`
516
- element
517
- - `script(js, **attributes)` - emits an inline `<script>` element
518
- - `style(css, **attributes)` - emits an inline `<style>` element
519
- - `versioned_file_href(href, root_path, root_url)` - calculates a versioned href
520
- for the given file
618
+ ```ruby
619
+ template = ->(title) { div { h1 title } }
620
+ template.render_cached('foo') #=> <div><h1>foo</h1></div>
621
+ template.render_cached('foo') #=> <div><h1>foo</h1></div> (from cache)
622
+ template.render_cached('bar') #=> <div><h1>bar</h1></div>
623
+ template.render_cached('bar') #=> <div><h1>bar</h1></div> (from cache)
624
+ ```
@@ -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)
@@ -149,19 +162,62 @@ module P2
149
162
  visitor.visit_builtin_node(self)
150
163
  end
151
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
152
209
  end
153
210
 
154
- class BlockInvocationNode
155
- attr_reader :call_node, :location, :block
211
+ class RenderChildrenNode
212
+ attr_reader :call_node, :location
156
213
 
157
214
  def initialize(call_node, translator)
158
215
  @call_node = call_node
159
216
  @tag = call_node.name
160
217
  @location = call_node.location
161
- @block = call_node.block && translator.visit(call_node.block)
162
218
  end
163
219
 
164
220
  def accept(visitor)
165
- visitor.visit_block_invocation_node(self)
221
+ visitor.visit_render_children_node(self)
166
222
  end
167
223
  end
@@ -23,7 +23,7 @@ module P2
23
23
  return super(node) if dont_translate
24
24
 
25
25
  match_builtin(node) ||
26
- match_emit_yield(node) ||
26
+ match_extension(node) ||
27
27
  match_const_tag(node) ||
28
28
  match_block_call(node) ||
29
29
  match_tag(node) ||
@@ -34,6 +34,10 @@ module P2
34
34
  return if node.receiver
35
35
 
36
36
  case node.name
37
+ when :render_yield
38
+ RenderYieldNode.new(node, self)
39
+ when :render_children
40
+ RenderChildrenNode.new(node, self)
37
41
  when :raise
38
42
  visit_call_node(node, dont_translate: true)
39
43
  when :render
@@ -51,14 +55,11 @@ module P2
51
55
  end
52
56
  end
53
57
 
54
- def match_emit_yield(node)
58
+ def match_extension(node)
55
59
  return if node.receiver
56
- return if node.name != :emit_yield
57
-
58
- yield_node(
59
- location: node.location,
60
- arguments: node.arguments
61
- )
60
+ return if !P2::Extensions[node.name]
61
+
62
+ ExtensionTagNode.new(node, self)
62
63
  end
63
64
 
64
65
  def match_const_tag(node)
data/lib/p2/compiler.rb CHANGED
@@ -53,6 +53,8 @@ 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
@@ -67,9 +69,6 @@ module P2
67
69
  def initialize(**)
68
70
  super(**)
69
71
  @pending_html_parts = []
70
- @html_loc_start = nil
71
- @html_loc_end = nil
72
- @yield_used = nil
73
72
  end
74
73
 
75
74
  # Initializes a source map.
@@ -79,6 +78,8 @@ module P2
79
78
  # @return [self]
80
79
  def with_source_map(orig_proc, orig_ast)
81
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
82
83
  @source_map = {
83
84
  source_fn: orig_proc.source_location.first,
84
85
  compiled_fn: fn
@@ -87,6 +88,15 @@ module P2
87
88
  self
88
89
  end
89
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
+
90
100
  # Formats the source code for a compiled template proc.
91
101
  #
92
102
  # @param ast [Prism::Node] translated AST
@@ -104,7 +114,7 @@ module P2
104
114
  source_code = @buffer
105
115
  @buffer = +''
106
116
  if wrap
107
- @source_map[2] = loc_start(orig_ast.location).first
117
+ @source_map[2] = "#{@orig_proc_fn}:#{loc_start(orig_ast.location).first}"
108
118
  emit("# frozen_string_literal: true\n->(__buffer__")
109
119
 
110
120
  params = orig_ast.parameters
@@ -114,11 +124,12 @@ module P2
114
124
  emit(format_code(params))
115
125
  end
116
126
 
117
- if @yield_used
127
+ if @render_yield_used || @render_children_used
118
128
  emit(', &__block__')
119
129
  end
120
130
 
121
131
  emit(") {\n")
132
+
122
133
  end
123
134
  @buffer << source_code
124
135
  emit_defer_postlude if @defer_mode
@@ -126,8 +137,6 @@ module P2
126
137
  emit('; __buffer__')
127
138
  adjust_whitespace(orig_ast.closing_loc)
128
139
  emit('}')
129
- # emit(";") if @buffer !~ /\n\s*$/m
130
- # emit("rescue Exception => e; P2.translate_backtrace(e, src_map); raise e; end }")
131
140
  end
132
141
  update_source_map
133
142
  Compiler.store_source_map(@source_map)
@@ -141,7 +150,7 @@ module P2
141
150
  def visit_tag_node(node)
142
151
  tag = node.tag
143
152
 
144
- adjust_whitespace(node.location)
153
+ # adjust_whitespace(node.location)
145
154
  is_void = is_void_element?(tag)
146
155
  emit_html(node.tag_location, format_html_tag_open(tag, node.attributes))
147
156
  return if is_void
@@ -159,9 +168,7 @@ module P2
159
168
  if is_static_node?(node.inner_text)
160
169
  emit_html(node.location, ERB::Escape.html_escape(format_literal(node.inner_text)))
161
170
  else
162
- to_s = is_string_type_node?(node.inner_text) ? '' : '.to_s'
163
-
164
- emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)})#{to_s})"))
171
+ emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)}))"))
165
172
  end
166
173
  end
167
174
  emit_html(node.location, format_html_tag_close(tag))
@@ -221,7 +228,7 @@ module P2
221
228
  if is_static_node?(first_arg)
222
229
  emit_html(node.location, ERB::Escape.html_escape(format_literal(first_arg)))
223
230
  else
224
- emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)}.to_s)"))
231
+ emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)})"))
225
232
  end
226
233
  else
227
234
  raise "Don't know how to compile #{node}"
@@ -292,20 +299,81 @@ module P2
292
299
  end
293
300
  end
294
301
 
295
- # Visits a yield node.
302
+ # Visits a extension tag node.
296
303
  #
297
- # @param node [P2::YieldNode] node
304
+ # @param node [P2::ExtensionTagNode] node
298
305
  # @return [void]
299
- def visit_yield_node(node)
306
+ def visit_extension_tag_node(node)
300
307
  flush_html_parts!
301
308
  adjust_whitespace(node.location)
302
- @yield_used = true
303
- emit("; (__block__ ? __block__.compiled_proc.(__buffer__")
304
- if node.arguments
309
+ emit("; P2::Extensions[#{node.tag.inspect}].compiled_proc.(__buffer__")
310
+ if node.call_node.arguments
305
311
  emit(', ')
306
- visit(node.arguments)
312
+ visit(node.call_node.arguments)
307
313
  end
308
- emit(") : raise(LocalJumpError, 'no block given (yield/emit_yield)'))")
314
+ if node.block
315
+ block_body = format_inline_block(node.block.body)
316
+ block_params = []
317
+
318
+ if node.block.parameters.is_a?(Prism::ItParametersNode)
319
+ raise P2::Error, "Blocks passed to extensions cannot use it parameter"
320
+ end
321
+
322
+ if (params = node.block.parameters&.parameters)
323
+ params.requireds.each do
324
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
325
+ end
326
+ params.optionals.each do
327
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
328
+ end
329
+ block_params << format_code(params.rest) if params.rest
330
+ params.posts.each do
331
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
332
+ end
333
+ params.keywords.each do
334
+ block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
335
+ end
336
+ block_params << format_code(params.keyword_rest) if params.keyword_rest
337
+ end
338
+ block_params = block_params.empty? ? '' : ", #{block_params.join(', ')}"
339
+
340
+ emit(", &(proc { |__buffer__#{block_params}| #{block_body} }).compiled!")
341
+ end
342
+ emit(")")
343
+ end
344
+
345
+ # Visits a render_yield node.
346
+ #
347
+ # @param node [P2::RenderYieldNode] node
348
+ # @return [void]
349
+ def visit_render_yield_node(node)
350
+ flush_html_parts!
351
+ adjust_whitespace(node.location)
352
+ guard = @render_yield_used ?
353
+ '' : "; raise(LocalJumpError, 'no block given (render_yield)') if !__block__"
354
+ @render_yield_used = true
355
+ emit("#{guard}; __block__.compiled_proc.(__buffer__")
356
+ if node.call_node.arguments
357
+ emit(', ')
358
+ visit(node.call_node.arguments)
359
+ end
360
+ emit(")")
361
+ end
362
+
363
+ # Visits a render_children node.
364
+ #
365
+ # @param node [P2::RenderChildrenNode] node
366
+ # @return [void]
367
+ def visit_render_children_node(node)
368
+ flush_html_parts!
369
+ adjust_whitespace(node.location)
370
+ @render_children_used = true
371
+ emit("; __block__&.compiled_proc&.(__buffer__")
372
+ if node.call_node.arguments
373
+ emit(', ')
374
+ visit(node.call_node.arguments)
375
+ end
376
+ emit(")")
309
377
  end
310
378
 
311
379
  def visit_block_invocation_node(node)
@@ -356,6 +424,10 @@ module P2
356
424
  Compiler.new(minimize_whitespace: true).to_source(node)
357
425
  end
358
426
 
427
+ def format_inline_block(node)
428
+ Compiler.new(minimize_whitespace: true).format_compiled_template(node, node, wrap: false, binding: @binding)
429
+ end
430
+
359
431
  # Formats a comma separated list of AST nodes. Used for formatting partial
360
432
  # argument lists.
361
433
  #
@@ -512,8 +584,8 @@ module P2
512
584
  # @return [void]
513
585
  def emit_html(loc, str)
514
586
  @html_loc_start ||= loc
515
- @html_loc_end = loc
516
- @pending_html_parts << str
587
+ @html_loc_end ||= loc
588
+ @pending_html_parts << [loc, str]
517
589
  end
518
590
 
519
591
  # Flushes pending HTML parts to the source code buffer.
@@ -522,31 +594,31 @@ module P2
522
594
  def flush_html_parts!(semicolon_prefix: true)
523
595
  return if @pending_html_parts.empty?
524
596
 
525
- adjust_whitespace(@html_loc_start)
526
-
527
- code = +''
528
- part = +''
597
+ adjust_whitespace(@html_loc_start, advance_to_end: false)
598
+ emit('; __buffer__')
599
+ concatenated = +''
529
600
 
530
- @pending_html_parts.each do
531
- if (m = it.match(/^#\{(.+)\}$/m))
532
- emit_html_buffer_push(code, part, quotes: true) if !part.empty?
533
- emit_html_buffer_push(code, m[1])
601
+ last_loc = @html_loc_start
602
+ @pending_html_parts.each do |(loc, part)|
603
+ if (m = part.match(/^#\{(.+)\}$/m))
604
+ emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
605
+ # adjust_whitespace(loc, advance_to_end: false)
606
+ emit_html_buffer_push(m[1], loc:)
534
607
  else
535
- part << it
608
+ concatenated << part
536
609
  end
610
+ last_loc = loc
537
611
  end
538
- emit_html_buffer_push(code, part, quotes: true) if !part.empty?
612
+ emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
539
613
 
540
614
  @pending_html_parts.clear
541
615
 
542
- @last_loc = @html_loc_end
543
- @last_loc_start = loc_start(@html_loc_end)
544
- @last_loc_end = loc_end(@html_loc_end)
616
+ @last_loc = last_loc
617
+ @last_loc_start = loc_start(@last_loc)
618
+ @last_loc_end = @last_loc_start
545
619
 
546
620
  @html_loc_start = nil
547
621
  @html_loc_end = nil
548
-
549
- emit code
550
622
  end
551
623
 
552
624
  # Emits HTML buffer push code to the given source code buffer.
@@ -555,11 +627,18 @@ module P2
555
627
  # @param part [String] HTML part
556
628
  # @param quotes [bool] whether to wrap emitted HTML in double quotes
557
629
  # @return [void]
558
- def emit_html_buffer_push(buf, part, quotes: false)
630
+ def emit_html_buffer_push(part, quotes: false, loc: nil)
559
631
  return if part.empty?
560
632
 
561
633
  q = quotes ? '"' : ''
562
- buf << "; __buffer__ << #{q}#{part}#{q}"
634
+ if loc
635
+ emit(".<<(")
636
+ adjust_whitespace(loc, advance_to_end: false)
637
+ emit("#{q}#{part}#{q}")
638
+ emit(")")
639
+ else
640
+ emit(".<<(#{q}#{part}#{q})")
641
+ end
563
642
  part.clear
564
643
  end
565
644
 
data/lib/p2/proc_ext.rb CHANGED
@@ -50,7 +50,10 @@ class ::Proc
50
50
  # @return [Proc] compiled proc
51
51
  def compile
52
52
  P2::Compiler.compile(self).compiled!
53
- rescue Sirop::Error
53
+ rescue Sirop::Error => e
54
+ puts '!' * 40
55
+ p self
56
+ p e
54
57
  raise P2::Error, "Dynamically defined procs cannot be compiled"
55
58
  end
56
59
 
@@ -87,4 +90,11 @@ class ::Proc
87
90
  compiled.(__buffer__, *a, *x, **b, **y, &c_proc)
88
91
  }.compiled!
89
92
  end
93
+
94
+ # Caches and returns
95
+ def render_cached(*args, **kargs, &block)
96
+ @render_cache ||= {}
97
+ key = args.empty? && kargs.empty? && !block ? nil : [args, kargs, block&.source_location]
98
+ @render_cache[key] ||= render(*args, **kargs, &block)
99
+ end
90
100
  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.6'
4
+ VERSION = '2.9'
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
@@ -67,8 +79,8 @@ module P2
67
79
  source_map = cache[fn] ||= Compiler.source_map_store[fn]
68
80
  return entry if !source_map
69
81
 
70
- source_line = source_map[line] || "?(#{line})"
71
- entry.sub(m[1], "#{source_map[:source_fn]}:#{source_line}")
82
+ ref = source_map[line] || "?(#{line})"
83
+ entry.sub(m[1], ref)
72
84
  end
73
85
 
74
86
  def make_argument_error(err, backtrace)
@@ -89,10 +101,11 @@ module P2
89
101
  # @param opts [Hash] Kramdown option overrides
90
102
  # @return [String] HTML
91
103
  def markdown(markdown, **opts)
92
- # require relevant deps on use
93
- require 'kramdown'
94
- require 'rouge'
95
- require 'kramdown-parser-gfm'
104
+ @markdown_deps_loaded ||= true.tap do
105
+ require 'kramdown'
106
+ require 'rouge'
107
+ require 'kramdown-parser-gfm'
108
+ end
96
109
 
97
110
  opts = default_kramdown_options.merge(opts)
98
111
  Kramdown::Document.new(markdown, **opts).to_html
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.6'
4
+ version: '2.9'
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