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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +153 -30
- data/lib/p2/compiler/nodes.rb +85 -1
- data/lib/p2/compiler/tag_translator.rb +61 -12
- data/lib/p2/compiler.rb +144 -44
- data/lib/p2/proc_ext.rb +24 -3
- data/lib/p2/template.rb +4 -2
- data/lib/p2/version.rb +1 -1
- data/lib/p2.rb +36 -11
- 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: a84949b86e99acbe131d37cb624dbf60010fe720ce27081d5e5652b33f2de004
|
4
|
+
data.tar.gz: c03bef8c358bae1536c04739c2fa67bd3ce6e879fd036f2e7352c6291677b888
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 {
|
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
|
49
|
-
|
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
|
-
- [
|
90
|
-
- [
|
91
|
-
- [
|
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
|
-
##
|
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
|
+
## 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
|
+
}
|
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
|
-
|
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
|
-
|
210
|
-
|
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 = -> {
|
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
|
}
|
@@ -477,7 +600,7 @@ default_layout = -> { |**args|
|
|
477
600
|
head {
|
478
601
|
defer { render deps.head_markup }
|
479
602
|
}
|
480
|
-
body {
|
603
|
+
body { render_yield **args }
|
481
604
|
}
|
482
605
|
|
483
606
|
button = proc { |text, onclick|
|
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)
|
@@ -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
|
14
|
-
|
13
|
+
def initialize(root)
|
14
|
+
@root = root
|
15
|
+
super()
|
15
16
|
end
|
16
17
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
18
|
+
def self.transform(ast, root)
|
19
|
+
ast.accept(new(root))
|
20
|
+
end
|
21
|
+
|
22
|
+
def visit_call_node(node, dont_translate: false)
|
23
|
+
return super(node) if dont_translate
|
24
|
+
|
25
|
+
match_builtin(node) ||
|
26
|
+
match_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 :
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
)
|
37
|
+
when :render_yield
|
38
|
+
RenderYieldNode.new(node, self)
|
39
|
+
when :render_children
|
40
|
+
RenderChildrenNode.new(node, self)
|
27
41
|
when :raise
|
28
|
-
|
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
|
-
|
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
|
-
|
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:
|
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 @
|
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
|
304
|
+
# Visits a extension tag node.
|
292
305
|
#
|
293
|
-
# @param node [P2::
|
306
|
+
# @param node [P2::ExtensionTagNode] node
|
294
307
|
# @return [void]
|
295
|
-
def
|
308
|
+
def visit_extension_tag_node(node)
|
296
309
|
flush_html_parts!
|
297
310
|
adjust_whitespace(node.location)
|
298
|
-
|
299
|
-
|
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(")
|
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
|
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
|
-
@
|
504
|
-
|
505
|
-
|
506
|
-
emit_html_buffer_push(
|
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
|
-
|
609
|
+
concatenated << part
|
509
610
|
end
|
611
|
+
last_loc = loc
|
510
612
|
end
|
511
|
-
emit_html_buffer_push(
|
613
|
+
emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
|
512
614
|
|
513
615
|
@pending_html_parts.clear
|
514
616
|
|
515
|
-
@last_loc =
|
516
|
-
@last_loc_start = loc_start(@
|
517
|
-
@last_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(
|
631
|
+
def emit_html_buffer_push(part, quotes: false)
|
532
632
|
return if part.empty?
|
533
633
|
|
534
634
|
q = quotes ? '"' : ''
|
535
|
-
|
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)
|
9
|
-
def render(*, **, &)
|
8
|
+
def initialize(proc) = @proc = proc
|
9
|
+
def render(*, **, &) = @proc.render(*, **, &)
|
10
|
+
def apply(*, **, &) = Template.new(@proc.apply(*, **, &))
|
11
|
+
def compiled_proc = @proc.compiled_proc
|
10
12
|
end
|
11
13
|
end
|
data/lib/p2/version.rb
CHANGED
data/lib/p2.rb
CHANGED
@@ -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
|
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(
|
58
|
+
def translate_backtrace(err)
|
49
59
|
cache = {}
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
69
|
-
entry.sub(m[1],
|
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
|
+
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.
|
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
|