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 +4 -4
- data/CHANGELOG.md +23 -4
- data/README.md +171 -67
- data/lib/p2/compiler/nodes.rb +61 -5
- data/lib/p2/compiler/tag_translator.rb +9 -8
- data/lib/p2/compiler.rb +118 -39
- data/lib/p2/proc_ext.rb +11 -1
- data/lib/p2/version.rb +1 -1
- data/lib/p2.rb +19 -6
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bda78e0c4b37c895274b5d357022f3f491bf6b1fdab4d52cdc4e7aa7660f2f6a
|
4
|
+
data.tar.gz: f9e1295f3208cb6b734d65d20afa41fcb46803b37b51b7d801ab56715fe24db8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
14
|
-
<img src="https://github.com/digital-fabric/p2/workflows/
|
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 {
|
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
|
49
|
-
|
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
|
-
- [
|
90
|
-
- [
|
91
|
-
- [
|
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
|
-
- [
|
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
|
-
##
|
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
|
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
|
-
##
|
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
|
-
|
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
|
-
|
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.
|
205
|
-
[
|
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
|
-
|
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
|
-
|
210
|
-
|
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 = -> {
|
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 = -> {
|
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
|
-
->
|
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 `
|
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 {
|
375
|
+
body { render_yield }
|
253
376
|
}
|
254
377
|
}
|
255
378
|
|
@@ -280,7 +403,7 @@ ItemList = ->(items) {
|
|
280
403
|
}
|
281
404
|
}
|
282
405
|
|
283
|
-
page = ->
|
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 {
|
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 {
|
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
|
-
|
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&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 {
|
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
|
-
##
|
611
|
+
## Cached Rendering
|
507
612
|
|
508
|
-
|
509
|
-
|
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
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
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
|
+
```
|
data/lib/p2/compiler/nodes.rb
CHANGED
@@ -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
|
155
|
-
attr_reader :call_node, :location
|
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.
|
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
|
-
|
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
|
58
|
+
def match_extension(node)
|
55
59
|
return if node.receiver
|
56
|
-
return if node.name
|
57
|
-
|
58
|
-
|
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 @
|
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
|
-
|
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)}
|
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
|
302
|
+
# Visits a extension tag node.
|
296
303
|
#
|
297
|
-
# @param node [P2::
|
304
|
+
# @param node [P2::ExtensionTagNode] node
|
298
305
|
# @return [void]
|
299
|
-
def
|
306
|
+
def visit_extension_tag_node(node)
|
300
307
|
flush_html_parts!
|
301
308
|
adjust_whitespace(node.location)
|
302
|
-
|
303
|
-
|
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
|
-
|
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
|
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
|
-
|
528
|
-
part = +''
|
597
|
+
adjust_whitespace(@html_loc_start, advance_to_end: false)
|
598
|
+
emit('; __buffer__')
|
599
|
+
concatenated = +''
|
529
600
|
|
530
|
-
@
|
531
|
-
|
532
|
-
|
533
|
-
emit_html_buffer_push(
|
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
|
-
|
608
|
+
concatenated << part
|
536
609
|
end
|
610
|
+
last_loc = loc
|
537
611
|
end
|
538
|
-
emit_html_buffer_push(
|
612
|
+
emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
|
539
613
|
|
540
614
|
@pending_html_parts.clear
|
541
615
|
|
542
|
-
@last_loc =
|
543
|
-
@last_loc_start = loc_start(@
|
544
|
-
@last_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(
|
630
|
+
def emit_html_buffer_push(part, quotes: false, loc: nil)
|
559
631
|
return if part.empty?
|
560
632
|
|
561
633
|
q = quotes ? '"' : ''
|
562
|
-
|
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
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
|
-
|
71
|
-
entry.sub(m[1],
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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.
|
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.
|
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.
|
25
|
+
version: '0.9'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: kramdown
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|