p2 2.0.1 → 2.2
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 +19 -0
- data/README.md +56 -66
- data/lib/p2/compiler/nodes.rb +139 -0
- data/lib/p2/compiler/tag_translator.rb +44 -0
- data/lib/p2/compiler.rb +226 -237
- data/lib/p2/proc_ext.rb +32 -42
- data/lib/p2/version.rb +1 -1
- data/lib/p2.rb +85 -79
- metadata +5 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b35bc76b761dd22bacd7e2b8844f3be20cabdb38ee6174d4e4eaeb144f5562d8
|
4
|
+
data.tar.gz: d677436567940ef41f922892b55844685d31fc52a8287c06f2ea0d89d2f1db55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37dd322edd50f7061e4375dbc8cb06a22d137106757250d99bc58b625e2dae8c7ea8fc00990e801caee03bae5fe59ba040b8456ef4e14b632874c30cf0784476
|
7
|
+
data.tar.gz: fe24352b6bbc3843c0213e842f07cd094ae83bbb6475f86174e43dd8a55ba9497b832e9b195ef8364f7c5984dc60c4370fc971a7297a79c8d692e9453456d63a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
# 2.2 2025-08-09
|
2
|
+
|
3
|
+
- Update docs
|
4
|
+
- Refactor code
|
5
|
+
|
6
|
+
# 2.1 2025-08-08
|
7
|
+
|
8
|
+
- Optimize output code: directly invoke component templates instead of calling
|
9
|
+
`P2.render_emit_call`. P2 is now
|
10
|
+
- Optimize output code: use separate pushes to buffer instead of interpolated
|
11
|
+
strings.
|
12
|
+
- Streamline API: `emit proc` => `render`, `emit str` => `raw`, `emit_markdown`
|
13
|
+
=> `markdown`
|
14
|
+
- Optimize output code: add `frozen_string_literal` to top of compiled code
|
15
|
+
- Add more benchmarks (#1)
|
16
|
+
- Optimize output code: use ERB::Escape.html_escape instead of CGI.escape_html
|
17
|
+
(#2)
|
18
|
+
- Fix source map calculation
|
19
|
+
|
1
20
|
## 2.0.1 2025-08-07
|
2
21
|
|
3
22
|
- Fix source map calculation
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
P2
|
5
5
|
</h1>
|
6
6
|
|
7
|
-
<h4 align="center">
|
7
|
+
<h4 align="center">Functional HTML templating for Ruby</h4>
|
8
8
|
|
9
9
|
<p align="center">
|
10
10
|
<a href="http://rubygems.org/gems/p2">
|
@@ -24,21 +24,34 @@
|
|
24
24
|
|
25
25
|
## What is P2?
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
```ruby
|
28
|
+
require 'p2'
|
29
|
+
|
30
|
+
page = ->(**props) {
|
31
|
+
html {
|
32
|
+
head { title 'My Title' }
|
33
|
+
body { emit_yield **props }
|
34
|
+
}
|
35
|
+
}
|
36
|
+
page.render {
|
37
|
+
p 'foo'
|
38
|
+
}
|
39
|
+
#=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
|
40
|
+
```
|
41
|
+
|
42
|
+
|
43
|
+
P2 is a templating engine for dynamically producing HTML in Ruby apps. P2
|
44
|
+
templates are expressed as Ruby procs, leading to easier debugging, better
|
45
|
+
protection against HTML injection attacks, and better code reuse.
|
30
46
|
|
31
47
|
P2 templates can be composed in a variety of ways, facilitating the usage of
|
32
48
|
layout templates, and enabling a component-oriented approach to building complex
|
33
49
|
web interfaces.
|
34
50
|
|
35
|
-
In P2, dynamic data is passed explicitly to the template as block
|
36
|
-
making the data flow easy to follow and understand. P2 also lets
|
37
|
-
create derivative templates using full or partial parameter
|
38
|
-
|
39
|
-
P2 includes built-in support for rendering Markdown (using
|
40
|
-
[Kramdown](https://github.com/gettalong/kramdown/)). P2 also automatically
|
41
|
-
escapes all text emitted in templates.
|
51
|
+
In P2, dynamic data is passed explicitly to the template as block/lambda
|
52
|
+
arguments, making the data flow easy to follow and understand. P2 also lets
|
53
|
+
developers create derivative templates using full or partial parameter
|
54
|
+
application.
|
42
55
|
|
43
56
|
```ruby
|
44
57
|
require 'p2'
|
@@ -50,9 +63,9 @@ page = ->(**props) {
|
|
50
63
|
}
|
51
64
|
}
|
52
65
|
page.render {
|
53
|
-
p 'foo'
|
66
|
+
p(class: 'big') 'foo'
|
54
67
|
}
|
55
|
-
#=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
|
68
|
+
#=> "<html><head><title>Title</title></head><body><p class="big">foo</p></body></html>"
|
56
69
|
|
57
70
|
hello_page = page.apply ->(name:, **) {
|
58
71
|
h1 "Hello, #{name}!"
|
@@ -64,16 +77,15 @@ hello.render(name: 'world')
|
|
64
77
|
P2 features:
|
65
78
|
|
66
79
|
- Express HTML using plain Ruby procs.
|
67
|
-
- Automatic compilation for super-fast execution (up to 2X faster than ERB templates).
|
68
80
|
- Deferred rendering using `defer`.
|
69
81
|
- Template composition (for uses such as layouts).
|
70
|
-
-
|
71
|
-
|
72
|
-
|
82
|
+
- Markdown rendering using [Kramdown](https://github.com/gettalong/kramdown/).
|
83
|
+
- Automatic compilation for super-fast execution (about as
|
84
|
+
[fast](https://github.com/digital-fabric/p2/blob/master/examples/perf.rb) as
|
85
|
+
compiled ERB/ERubi).
|
73
86
|
|
74
87
|
## Table of Content
|
75
88
|
|
76
|
-
- [Installing P2](#installing-p2)
|
77
89
|
- [Basic Usage](#basic-usage)
|
78
90
|
- [Adding Tags](#adding-tags)
|
79
91
|
- [Tag and Attribute Formatting](#tag-and-attribute-formatting)
|
@@ -86,27 +98,11 @@ P2 features:
|
|
86
98
|
- [Higher-Order Templates](#higher-order-templates)
|
87
99
|
- [Layout Template Composition](#layout-template-composition)
|
88
100
|
- [Emitting Raw HTML](#emitting-raw-html)
|
89
|
-
- [Emitting a String with HTML
|
101
|
+
- [Emitting a String with HTML Escaping](#emitting-a-string-with-html-escaping)
|
90
102
|
- [Emitting Markdown](#emitting-markdown)
|
91
103
|
- [Deferred Evaluation](#deferred-evaluation)
|
92
104
|
- [API Reference](#api-reference)
|
93
105
|
|
94
|
-
## Installing P2
|
95
|
-
|
96
|
-
**Note**: P2 requires Ruby version 3.4 or newer.
|
97
|
-
|
98
|
-
Using bundler:
|
99
|
-
|
100
|
-
```ruby
|
101
|
-
gem 'p2'
|
102
|
-
```
|
103
|
-
|
104
|
-
Or manually:
|
105
|
-
|
106
|
-
```bash
|
107
|
-
$ gem install p2
|
108
|
-
```
|
109
|
-
|
110
106
|
## Basic Usage
|
111
107
|
|
112
108
|
In P2, an HTML template is expressed as a proc:
|
@@ -125,7 +121,7 @@ require 'p2'
|
|
125
121
|
html.render #=> "<div id="greeter"><p>Hello!</p></div>"
|
126
122
|
```
|
127
123
|
|
128
|
-
##
|
124
|
+
## Expressing HTML Using Ruby
|
129
125
|
|
130
126
|
Tags are added using unqualified method calls, and can be nested using blocks:
|
131
127
|
|
@@ -298,14 +294,14 @@ page.render('Hello from composed templates', [
|
|
298
294
|
```
|
299
295
|
|
300
296
|
In addition to using templates defined as constants, you can also use
|
301
|
-
non-constant templates by invoking the `#
|
297
|
+
non-constant templates by invoking the `#render` method:
|
302
298
|
|
303
299
|
```ruby
|
304
300
|
greeting = -> { span "Hello, world" }
|
305
301
|
|
306
302
|
-> {
|
307
303
|
div {
|
308
|
-
|
304
|
+
render greeting
|
309
305
|
}
|
310
306
|
}
|
311
307
|
```
|
@@ -345,7 +341,7 @@ templates or injecting template parameters.
|
|
345
341
|
Here is a higher-order template that takes a template as parameter:
|
346
342
|
|
347
343
|
```ruby
|
348
|
-
div_wrap = -> { |inner| div {
|
344
|
+
div_wrap = -> { |inner| div { render inner } }
|
349
345
|
greeter = -> { h1 'hi' }
|
350
346
|
wrapped_greeter = div_wrap.apply(greeter)
|
351
347
|
wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
|
@@ -382,7 +378,7 @@ default_layout = -> { |**params|
|
|
382
378
|
article_layout = default_layout.apply { |title:, body:|
|
383
379
|
article {
|
384
380
|
h1 title
|
385
|
-
|
381
|
+
markdown body
|
386
382
|
}
|
387
383
|
}
|
388
384
|
|
@@ -394,16 +390,16 @@ article_layout.render(
|
|
394
390
|
|
395
391
|
## Emitting Raw HTML
|
396
392
|
|
397
|
-
Raw HTML can be emitted using `#
|
393
|
+
Raw HTML can be emitted using `#raw`:
|
398
394
|
|
399
395
|
```ruby
|
400
|
-
wrapped = -> { |html| div {
|
396
|
+
wrapped = -> { |html| div { raw html } }
|
401
397
|
wrapped.render("<h1>hi</h1>") #=> "<div><h1>hi</h1></div>"
|
402
398
|
```
|
403
399
|
|
404
|
-
## Emitting a String with HTML
|
400
|
+
## Emitting a String with HTML Escaping
|
405
401
|
|
406
|
-
To emit a string with proper HTML
|
402
|
+
To emit a string with proper HTML escaping, without wrapping it in an HTML
|
407
403
|
element, use `#text`:
|
408
404
|
|
409
405
|
```ruby
|
@@ -414,19 +410,19 @@ element, use `#text`:
|
|
414
410
|
|
415
411
|
Markdown is rendered using the
|
416
412
|
[Kramdown](https://kramdown.gettalong.org/index.html) gem. To emit Markdown, use
|
417
|
-
`#
|
413
|
+
`#markdown`:
|
418
414
|
|
419
415
|
```ruby
|
420
|
-
template = -> { |md| div {
|
416
|
+
template = -> { |md| div { markdown md } }
|
421
417
|
template.render("Here's some *Markdown*") #=> "<div><p>Here's some <em>Markdown</em><p>\n</div>"
|
422
418
|
```
|
423
419
|
|
424
420
|
[Kramdown
|
425
421
|
options](https://kramdown.gettalong.org/options.html#available-options) can be
|
426
|
-
specified by adding them to the `#
|
422
|
+
specified by adding them to the `#markdown` call:
|
427
423
|
|
428
424
|
```ruby
|
429
|
-
template = -> { |md| div {
|
425
|
+
template = -> { |md| div { markdown md, auto_ids: false } }
|
430
426
|
template.render("# title") #=> "<div><h1>title</h1></div>"
|
431
427
|
```
|
432
428
|
|
@@ -443,7 +439,7 @@ The default Kramdown options are:
|
|
443
439
|
entity_output: :numeric,
|
444
440
|
syntax_highlighter: :rouge,
|
445
441
|
input: 'GFM',
|
446
|
-
hard_wrap: false
|
442
|
+
hard_wrap: false
|
447
443
|
}
|
448
444
|
```
|
449
445
|
|
@@ -475,34 +471,35 @@ class that can collect JS and CSS dependencies from the different templates
|
|
475
471
|
integrated into the page, and adds them to the page's `<head>` element:
|
476
472
|
|
477
473
|
```ruby
|
474
|
+
deps = DependencyMananger.new
|
475
|
+
|
478
476
|
default_layout = -> { |**args|
|
479
|
-
@dependencies = DependencyMananger.new
|
480
477
|
head {
|
481
|
-
defer {
|
478
|
+
defer { render deps.head_markup }
|
482
479
|
}
|
483
480
|
body { emit_yield **args }
|
484
481
|
}
|
485
482
|
|
486
483
|
button = proc { |text, onclick|
|
487
|
-
|
488
|
-
|
484
|
+
deps.js '/static/js/button.js'
|
485
|
+
deps.css '/static/css/button.css'
|
489
486
|
|
490
487
|
button text, onclick: onclick
|
491
488
|
}
|
492
489
|
|
493
490
|
heading = proc { |text|
|
494
|
-
|
495
|
-
|
491
|
+
deps.js '/static/js/heading.js'
|
492
|
+
deps.css '/static/css/heading.css'
|
496
493
|
|
497
494
|
h1 text
|
498
495
|
}
|
499
496
|
|
500
497
|
page = default_layout.apply {
|
501
|
-
|
498
|
+
render heading, "What's your favorite cheese?"
|
502
499
|
|
503
|
-
|
504
|
-
|
505
|
-
|
500
|
+
render button, 'Beaufort', 'eat_beaufort()'
|
501
|
+
render button, 'Mont d''or', 'eat_montdor()'
|
502
|
+
render button, 'Époisses', 'eat_epoisses()'
|
506
503
|
}
|
507
504
|
```
|
508
505
|
|
@@ -521,10 +518,3 @@ HTML:
|
|
521
518
|
- `style(css, **attributes)` - emits an inline `<style>` element
|
522
519
|
- `versioned_file_href(href, root_path, root_url)` - calculates a versioned href
|
523
520
|
for the given file
|
524
|
-
|
525
|
-
[HTML docs](https://www.rubydoc.info/gems/p2/P2/HTML)
|
526
|
-
|
527
|
-
## API Reference
|
528
|
-
|
529
|
-
The API reference for this library can be found
|
530
|
-
[here](https://www.rubydoc.info/gems/p2).
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module P2
|
4
|
+
# Represents a tag call
|
5
|
+
class TagNode
|
6
|
+
attr_reader :call_node, :location, :tag, :tag_location, :inner_text, :attributes, :block
|
7
|
+
|
8
|
+
def initialize(call_node, translator)
|
9
|
+
@call_node = call_node
|
10
|
+
@location = call_node.location
|
11
|
+
@tag = call_node.name
|
12
|
+
prepare_block(translator)
|
13
|
+
|
14
|
+
args = call_node.arguments&.arguments
|
15
|
+
return if !args
|
16
|
+
|
17
|
+
if @tag == :tag
|
18
|
+
@tag = args[0]
|
19
|
+
args = args[1..]
|
20
|
+
end
|
21
|
+
|
22
|
+
if args.size == 1 && args.first.is_a?(Prism::KeywordHashNode)
|
23
|
+
@inner_text = nil
|
24
|
+
@attributes = args.first
|
25
|
+
else
|
26
|
+
@inner_text = args.first
|
27
|
+
@attributes = args[1].is_a?(Prism::KeywordHashNode) ? args[1] : nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def accept(visitor)
|
32
|
+
visitor.visit_tag_node(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
def prepare_block(translator)
|
36
|
+
@block = call_node.block
|
37
|
+
if @block.is_a?(Prism::BlockNode)
|
38
|
+
@block = translator.visit(@block)
|
39
|
+
offset = @location.start_offset
|
40
|
+
length = @block.opening_loc.start_offset - offset
|
41
|
+
@tag_location = @location.copy(start_offset: offset, length: length)
|
42
|
+
else
|
43
|
+
@tag_location = @location
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Represents a render call
|
49
|
+
class RenderNode
|
50
|
+
attr_reader :call_node, :location, :block
|
51
|
+
|
52
|
+
include Prism::DSL
|
53
|
+
|
54
|
+
def initialize(call_node, translator)
|
55
|
+
@call_node = call_node
|
56
|
+
@location = call_node.location
|
57
|
+
@translator = translator
|
58
|
+
@block = call_node.block && translator.visit(call_node.block)
|
59
|
+
|
60
|
+
lambda = call_node.arguments && call_node.arguments.arguments[0]
|
61
|
+
end
|
62
|
+
|
63
|
+
def ad_hoc_string_location(str)
|
64
|
+
src = source(str)
|
65
|
+
Prism::DSL.location(source: src, start_offset: 0, length: str.bytesize)
|
66
|
+
end
|
67
|
+
|
68
|
+
def transform(node)
|
69
|
+
node && @translator.visit(node)
|
70
|
+
end
|
71
|
+
|
72
|
+
def transform_array(array)
|
73
|
+
array ? array.map { @translator.visit(it) } : []
|
74
|
+
end
|
75
|
+
|
76
|
+
def accept(visitor)
|
77
|
+
visitor.visit_render_node(self)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Represents a text call
|
82
|
+
class TextNode
|
83
|
+
attr_reader :call_node, :location
|
84
|
+
|
85
|
+
def initialize(call_node, _translator)
|
86
|
+
@call_node = call_node
|
87
|
+
@location = call_node.location
|
88
|
+
end
|
89
|
+
|
90
|
+
def accept(visitor)
|
91
|
+
visitor.visit_text_node(self)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Represents a raw call
|
96
|
+
class RawNode
|
97
|
+
attr_reader :call_node, :location
|
98
|
+
|
99
|
+
def initialize(call_node, _translator)
|
100
|
+
@call_node = call_node
|
101
|
+
@location = call_node.location
|
102
|
+
end
|
103
|
+
|
104
|
+
def accept(visitor)
|
105
|
+
visitor.visit_raw_node(self)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Represents a defer call
|
110
|
+
class DeferNode
|
111
|
+
attr_reader :call_node, :location, :block
|
112
|
+
|
113
|
+
def initialize(call_node, translator)
|
114
|
+
@call_node = call_node
|
115
|
+
@location = call_node.location
|
116
|
+
@block = call_node.block && translator.visit(call_node.block)
|
117
|
+
end
|
118
|
+
|
119
|
+
def accept(visitor)
|
120
|
+
visitor.visit_defer_node(self)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Represents a builtin call
|
125
|
+
class BuiltinNode
|
126
|
+
attr_reader :tag, :call_node, :location, :block
|
127
|
+
|
128
|
+
def initialize(call_node, translator)
|
129
|
+
@call_node = call_node
|
130
|
+
@tag = call_node.name
|
131
|
+
@location = call_node.location
|
132
|
+
@block = call_node.block && translator.visit(call_node.block)
|
133
|
+
end
|
134
|
+
|
135
|
+
def accept(visitor)
|
136
|
+
visitor.visit_builtin_node(self)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
require_relative './nodes'
|
5
|
+
|
6
|
+
module P2
|
7
|
+
# Translates a normal proc AST into an AST containing custom nodes used for
|
8
|
+
# generating HTML. This translation is the first step in compiling templates
|
9
|
+
# into procs that generate HTML.
|
10
|
+
class TagTranslator < Prism::MutationCompiler
|
11
|
+
include Prism::DSL
|
12
|
+
|
13
|
+
def self.transform(ast)
|
14
|
+
ast.accept(new)
|
15
|
+
end
|
16
|
+
|
17
|
+
def visit_call_node(node)
|
18
|
+
# We're only interested in compiling method calls without a receiver
|
19
|
+
return super(node) if node.receiver
|
20
|
+
|
21
|
+
case node.name
|
22
|
+
when :emit_yield
|
23
|
+
yield_node(
|
24
|
+
location: node.location,
|
25
|
+
arguments: node.arguments
|
26
|
+
)
|
27
|
+
when :raise
|
28
|
+
super(node)
|
29
|
+
when :render
|
30
|
+
RenderNode.new(node, self)
|
31
|
+
when :raw
|
32
|
+
RawNode.new(node, self)
|
33
|
+
when :text
|
34
|
+
TextNode.new(node, self)
|
35
|
+
when :defer
|
36
|
+
DeferNode.new(node, self)
|
37
|
+
when :html5, :markdown
|
38
|
+
BuiltinNode.new(node, self)
|
39
|
+
else
|
40
|
+
TagNode.new(node, self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|