p2 2.1 → 2.3
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 +12 -1
- data/README.md +38 -48
- data/lib/p2/compiler/nodes.rb +139 -0
- data/lib/p2/compiler/tag_translator.rb +44 -0
- data/lib/p2/compiler.rb +186 -250
- data/lib/p2/proc_ext.rb +33 -42
- data/lib/p2/version.rb +1 -1
- data/lib/p2.rb +42 -27
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad3bf1a720998e9b118a15d4a7b57677c5938bf74d6f65f474653ecaf58a9387
|
4
|
+
data.tar.gz: 6299a9c113a3bf8cc3c0a2c745ba281f7f42304ae7d127fca907b2cead73788c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fffd7f51d9efd4a537f5e870987bf7cc849aaf3a22a912cb36f6f2ab046259a1191201966e9497941b863a7e0ac35a2dc49d4e6ecf93445d6d1ed0d4725fc09
|
7
|
+
data.tar.gz: 6b7165c8d382c91d97d7a4799e2da53e80cacb2100ce7703017af7adee1ec53c0a474a2ff6798231ec848e5463ffce26231026f0f135057803b4d9cf53ac9090
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,18 @@
|
|
1
|
+
# 2.3 2025-08-10
|
2
|
+
|
3
|
+
- Fix whitespace issue in visit_yield_node
|
4
|
+
- Reimplement and optimize exception backtrace translation
|
5
|
+
- Minor improvement to code generation
|
6
|
+
|
7
|
+
# 2.2 2025-08-09
|
8
|
+
|
9
|
+
- Update docs
|
10
|
+
- Refactor code
|
11
|
+
|
1
12
|
# 2.1 2025-08-08
|
2
13
|
|
3
14
|
- Optimize output code: directly invoke component templates instead of calling
|
4
|
-
`P2.render_emit_call`. P2 is now
|
15
|
+
`P2.render_emit_call`. P2 is now
|
5
16
|
- Optimize output code: use separate pushes to buffer instead of interpolated
|
6
17
|
strings.
|
7
18
|
- Streamline API: `emit proc` => `render`, `emit str` => `raw`, `emit_markdown`
|
data/README.md
CHANGED
@@ -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)
|
@@ -91,22 +103,6 @@ P2 features:
|
|
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
|
|
@@ -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,24 +471,25 @@ 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 { render
|
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
|
}
|
@@ -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
|
data/lib/p2/compiler.rb
CHANGED
@@ -1,213 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'cgi'
|
4
3
|
require 'sirop'
|
5
4
|
require 'erb/escape'
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
attr_reader :call_node, :location, :tag, :tag_location, :inner_text, :attributes, :block
|
10
|
-
|
11
|
-
def initialize(call_node, transformer)
|
12
|
-
@call_node = call_node
|
13
|
-
@location = call_node.location
|
14
|
-
@tag = call_node.name
|
15
|
-
prepare_block(transformer)
|
16
|
-
|
17
|
-
args = call_node.arguments&.arguments
|
18
|
-
return if !args
|
19
|
-
|
20
|
-
if @tag == :tag
|
21
|
-
@tag = args[0]
|
22
|
-
args = args[1..]
|
23
|
-
end
|
24
|
-
|
25
|
-
if args.size == 1 && args.first.is_a?(Prism::KeywordHashNode)
|
26
|
-
@inner_text = nil
|
27
|
-
@attributes = args.first
|
28
|
-
else
|
29
|
-
@inner_text = args.first
|
30
|
-
@attributes = args[1].is_a?(Prism::KeywordHashNode) ? args[1] : nil
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def accept(visitor)
|
35
|
-
visitor.visit_tag_node(self)
|
36
|
-
end
|
37
|
-
|
38
|
-
def prepare_block(transformer)
|
39
|
-
@block = call_node.block
|
40
|
-
if @block.is_a?(Prism::BlockNode)
|
41
|
-
@block = transformer.visit(@block)
|
42
|
-
offset = @location.start_offset
|
43
|
-
length = @block.opening_loc.start_offset - offset
|
44
|
-
@tag_location = @location.copy(start_offset: offset, length: length)
|
45
|
-
else
|
46
|
-
@tag_location = @location
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class RenderNode
|
52
|
-
attr_reader :call_node, :location, :block
|
53
|
-
|
54
|
-
include Prism::DSL
|
55
|
-
|
56
|
-
def initialize(call_node, transformer)
|
57
|
-
@call_node = call_node
|
58
|
-
@location = call_node.location
|
59
|
-
@transformer = transformer
|
60
|
-
@block = call_node.block && transformer.visit(call_node.block)
|
61
|
-
|
62
|
-
lambda = call_node.arguments && call_node.arguments.arguments[0]
|
63
|
-
return unless lambda.is_a?(Prism::LambdaNode)
|
64
|
-
|
65
|
-
location = lambda.location
|
66
|
-
parameters = lambda.parameters
|
67
|
-
parameters_location = parameters&.location || location
|
68
|
-
params = parameters&.parameters
|
69
|
-
lambda = lambda_node(
|
70
|
-
location: location,
|
71
|
-
parameters: block_parameters_node(
|
72
|
-
location: parameters_location,
|
73
|
-
parameters: parameters_node(
|
74
|
-
location: parameters_location,
|
75
|
-
requireds: [
|
76
|
-
required_parameter_node(
|
77
|
-
location: ad_hoc_string_location('__buffer__'),
|
78
|
-
name: :__buffer__
|
79
|
-
),
|
80
|
-
*params&.requireds
|
81
|
-
],
|
82
|
-
optionals: transform_array(params&.optionals),
|
83
|
-
rest: transform(params&.rest),
|
84
|
-
posts: transform_array(params&.posts),
|
85
|
-
keywords: transform_array(params&.keywords),
|
86
|
-
keyword_rest: transform(params&.keyword_rest),
|
87
|
-
block: transform(params&.block)
|
88
|
-
)
|
89
|
-
),
|
90
|
-
body: transformer.visit(lambda.body)
|
91
|
-
)
|
92
|
-
call_node.arguments.arguments[0] = lambda
|
93
|
-
# pp lambda_body: call_node.arguments.arguments[0]
|
94
|
-
end
|
95
|
-
|
96
|
-
def ad_hoc_string_location(str)
|
97
|
-
src = source(str)
|
98
|
-
Prism::DSL.location(source: src, start_offset: 0, length: str.bytesize)
|
99
|
-
end
|
100
|
-
|
101
|
-
def transform(node)
|
102
|
-
node && @transformer.visit(node)
|
103
|
-
end
|
104
|
-
|
105
|
-
def transform_array(array)
|
106
|
-
array ? array.map { @transformer.visit(it) } : []
|
107
|
-
end
|
108
|
-
|
109
|
-
def accept(visitor)
|
110
|
-
visitor.visit_render_node(self)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
class TextNode
|
115
|
-
attr_reader :call_node, :location
|
116
|
-
|
117
|
-
def initialize(call_node, _compiler)
|
118
|
-
@call_node = call_node
|
119
|
-
@location = call_node.location
|
120
|
-
end
|
121
|
-
|
122
|
-
def accept(visitor)
|
123
|
-
visitor.visit_text_node(self)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
class RawNode
|
128
|
-
attr_reader :call_node, :location
|
129
|
-
|
130
|
-
def initialize(call_node, _compiler)
|
131
|
-
@call_node = call_node
|
132
|
-
@location = call_node.location
|
133
|
-
end
|
134
|
-
|
135
|
-
def accept(visitor)
|
136
|
-
visitor.visit_raw_node(self)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
class DeferNode
|
141
|
-
attr_reader :call_node, :location, :block
|
142
|
-
|
143
|
-
def initialize(call_node, compiler)
|
144
|
-
@call_node = call_node
|
145
|
-
@location = call_node.location
|
146
|
-
@block = call_node.block && compiler.visit(call_node.block)
|
147
|
-
end
|
148
|
-
|
149
|
-
def accept(visitor)
|
150
|
-
visitor.visit_defer_node(self)
|
151
|
-
end
|
152
|
-
end
|
6
|
+
require_relative './compiler/nodes'
|
7
|
+
require_relative './compiler/tag_translator'
|
153
8
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
def accept(visitor)
|
165
|
-
visitor.visit_custom_tag_node(self)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
class TagTransformer < Prism::MutationCompiler
|
170
|
-
include Prism::DSL
|
171
|
-
|
172
|
-
def self.transform(ast)
|
173
|
-
ast.accept(new)
|
174
|
-
end
|
175
|
-
|
176
|
-
def visit_call_node(node)
|
177
|
-
# We're only interested in compiling method calls without a receiver
|
178
|
-
return super(node) if node.receiver
|
179
|
-
|
180
|
-
case node.name
|
181
|
-
when :emit_yield
|
182
|
-
yield_node(
|
183
|
-
location: node.location,
|
184
|
-
arguments: node.arguments
|
185
|
-
)
|
186
|
-
when :raise
|
187
|
-
super(node)
|
188
|
-
when :render
|
189
|
-
RenderNode.new(node, self)
|
190
|
-
when :raw
|
191
|
-
RawNode.new(node, self)
|
192
|
-
when :text
|
193
|
-
TextNode.new(node, self)
|
194
|
-
when :defer
|
195
|
-
DeferNode.new(node, self)
|
196
|
-
when :html5, :markdown
|
197
|
-
CustomTagNode.new(node, self)
|
198
|
-
else
|
199
|
-
TagNode.new(node, self)
|
200
|
-
end
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
class VerbatimSourcifier < Sirop::Sourcifier
|
205
|
-
def visit_tag_node(node)
|
206
|
-
visit(node.call_node)
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
class TemplateCompiler < Sirop::Sourcifier
|
9
|
+
module P2
|
10
|
+
# A Compiler converts a template into an optimized form that generates HTML
|
11
|
+
# efficiently.
|
12
|
+
class Compiler < Sirop::Sourcifier
|
13
|
+
# Compiles the given proc, returning the generated source map and the
|
14
|
+
# generated optimized source code.
|
15
|
+
#
|
16
|
+
# @param proc [Proc] template
|
17
|
+
# @param wrap [bool] whether to wrap the generated code with a literal Proc definition
|
18
|
+
# @return [Array] array containing the source map and generated code
|
211
19
|
def self.compile_to_code(proc, wrap: true)
|
212
20
|
ast = Sirop.to_ast(proc)
|
213
21
|
|
@@ -215,11 +23,22 @@ module P2
|
|
215
23
|
ast = ast.block if ast.is_a?(Prism::CallNode)
|
216
24
|
|
217
25
|
compiler = new.with_source_map(proc, ast)
|
218
|
-
transformed_ast =
|
26
|
+
transformed_ast = TagTranslator.transform(ast.body)
|
219
27
|
compiler.format_compiled_template(transformed_ast, ast, wrap:, binding: proc.binding)
|
220
28
|
[compiler.source_map, compiler.buffer]
|
221
29
|
end
|
222
30
|
|
31
|
+
# Compiles the given template into an optimized Proc that generates HTML.
|
32
|
+
#
|
33
|
+
# template = -> {
|
34
|
+
# h1 'Hello, world!'
|
35
|
+
# }
|
36
|
+
# compiled = P2::Compiler.compile(template)
|
37
|
+
# compiled.render #=> '<h1>Hello, world!'
|
38
|
+
#
|
39
|
+
# @param proc [Proc] template
|
40
|
+
# @param wrap [bool] whether to wrap the generated code with a literal Proc definition
|
41
|
+
# @return [Proc] compiled proc
|
223
42
|
def self.compile(proc, wrap: true)
|
224
43
|
source_map, code = compile_to_code(proc, wrap:)
|
225
44
|
if ENV['DEBUG'] == '1'
|
@@ -229,8 +48,18 @@ module P2
|
|
229
48
|
eval(code, proc.binding, source_map[:compiled_fn])
|
230
49
|
end
|
231
50
|
|
51
|
+
def self.source_map_store
|
52
|
+
@source__map_store ||= {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.store_source_map(source_map)
|
56
|
+
fn = source_map[:compiled_fn]
|
57
|
+
source_map_store[fn] = source_map
|
58
|
+
end
|
59
|
+
|
232
60
|
attr_reader :source_map
|
233
61
|
|
62
|
+
# Initializes a compiler.
|
234
63
|
def initialize(**)
|
235
64
|
super(**)
|
236
65
|
@pending_html_parts = []
|
@@ -239,6 +68,11 @@ module P2
|
|
239
68
|
@yield_used = nil
|
240
69
|
end
|
241
70
|
|
71
|
+
# Initializes a source map.
|
72
|
+
#
|
73
|
+
# @param orig_proc [Proc] template proc
|
74
|
+
# @param orig_ast [Prism::Node] template AST
|
75
|
+
# @return [self]
|
242
76
|
def with_source_map(orig_proc, orig_ast)
|
243
77
|
compiled_fn = "::(#{orig_proc.source_location.join(':')})"
|
244
78
|
@source_map = {
|
@@ -249,6 +83,12 @@ module P2
|
|
249
83
|
self
|
250
84
|
end
|
251
85
|
|
86
|
+
# Formats the source code for a compiled template proc.
|
87
|
+
#
|
88
|
+
# @param ast [Prism::Node] translated AST
|
89
|
+
# @param orig_ast [Prism::Node] original template AST
|
90
|
+
# @param wrap [bool] whether to wrap the generated code with a literal Proc definition
|
91
|
+
# @return [String] compiled template source code
|
252
92
|
def format_compiled_template(ast, orig_ast, wrap:, binding:)
|
253
93
|
# generate source code
|
254
94
|
@binding = binding
|
@@ -259,7 +99,7 @@ module P2
|
|
259
99
|
source_code = @buffer
|
260
100
|
@buffer = +''
|
261
101
|
if wrap
|
262
|
-
emit("# frozen_string_literal: true\n
|
102
|
+
emit("# frozen_string_literal: true\n->(__buffer__")
|
263
103
|
|
264
104
|
params = orig_ast.parameters
|
265
105
|
params = params&.parameters
|
@@ -271,25 +111,27 @@ module P2
|
|
271
111
|
if @yield_used
|
272
112
|
emit(', &__block__')
|
273
113
|
end
|
274
|
-
|
275
|
-
emit(")
|
114
|
+
|
115
|
+
emit(") {\n")
|
276
116
|
end
|
277
117
|
@buffer << source_code
|
278
|
-
|
118
|
+
emit_defer_postlude if @defer_mode
|
279
119
|
if wrap
|
280
120
|
emit('; __buffer__')
|
281
121
|
adjust_whitespace(orig_ast.closing_loc)
|
282
|
-
emit(
|
283
|
-
emit("
|
122
|
+
emit('}')
|
123
|
+
# emit(";") if @buffer !~ /\n\s*$/m
|
124
|
+
# emit("rescue Exception => e; P2.translate_backtrace(e, src_map); raise e; end }")
|
284
125
|
end
|
126
|
+
update_source_map
|
127
|
+
Compiler.store_source_map(@source_map)
|
285
128
|
@buffer
|
286
129
|
end
|
287
130
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
131
|
+
# Visits a tag node.
|
132
|
+
#
|
133
|
+
# @param node [P2::TagNode] node
|
134
|
+
# @return [void]
|
293
135
|
def visit_tag_node(node)
|
294
136
|
tag = node.tag
|
295
137
|
if tag.is_a?(Symbol) && tag =~ /^[A-Z]/
|
@@ -306,24 +148,25 @@ module P2
|
|
306
148
|
when Prism::BlockArgumentNode
|
307
149
|
flush_html_parts!
|
308
150
|
adjust_whitespace(node.block)
|
309
|
-
emit("; #{format_code(node.block.expression)}.
|
151
|
+
emit("; #{format_code(node.block.expression)}.compiled_proc.(__buffer__)")
|
310
152
|
end
|
311
153
|
|
312
154
|
if node.inner_text
|
313
155
|
if is_static_node?(node.inner_text)
|
314
156
|
emit_html(node.location, ERB::Escape.html_escape(format_literal(node.inner_text)))
|
315
157
|
else
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
else
|
320
|
-
emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(node.inner_text)})"))
|
321
|
-
end
|
158
|
+
to_s = is_string_type_node?(node.inner_text) ? '' : '.to_s'
|
159
|
+
|
160
|
+
emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)})#{to_s})"))
|
322
161
|
end
|
323
162
|
end
|
324
163
|
emit_html(node.location, format_html_tag_close(tag))
|
325
164
|
end
|
326
165
|
|
166
|
+
# Visits a const tag node.
|
167
|
+
#
|
168
|
+
# @param node [P2::ConstTagNode] node
|
169
|
+
# @return [void]
|
327
170
|
def visit_const_tag_node(node)
|
328
171
|
flush_html_parts!
|
329
172
|
adjust_whitespace(node.location)
|
@@ -331,7 +174,7 @@ module P2
|
|
331
174
|
emit(node.receiver.location)
|
332
175
|
emit('::')
|
333
176
|
end
|
334
|
-
emit("; #{node.name}.
|
177
|
+
emit("; #{node.name}.compiled_proc.(__buffer__")
|
335
178
|
if node.arguments
|
336
179
|
emit(', ')
|
337
180
|
visit(node.arguments)
|
@@ -339,24 +182,32 @@ module P2
|
|
339
182
|
emit(');')
|
340
183
|
end
|
341
184
|
|
185
|
+
# Visits a render node.
|
186
|
+
#
|
187
|
+
# @param node [P2::RenderNode] node
|
188
|
+
# @return [void]
|
342
189
|
def visit_render_node(node)
|
343
190
|
args = node.call_node.arguments.arguments
|
344
191
|
first_arg = args.first
|
345
|
-
|
192
|
+
|
346
193
|
block_embed = node.block && "&(->(__buffer__) #{format_code(node.block)}.compiled!)"
|
347
194
|
block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments
|
348
195
|
|
349
196
|
flush_html_parts!
|
350
197
|
adjust_whitespace(node.location)
|
351
|
-
|
198
|
+
|
352
199
|
if args.length == 1
|
353
200
|
emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__#{block_embed})")
|
354
201
|
else
|
355
202
|
args_code = format_code_comma_separated_nodes(args[1..])
|
356
203
|
emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__, #{args_code}#{block_embed})")
|
357
|
-
end
|
204
|
+
end
|
358
205
|
end
|
359
206
|
|
207
|
+
# Visits a text node.
|
208
|
+
#
|
209
|
+
# @param node [P2::TextNode] node
|
210
|
+
# @return [void]
|
360
211
|
def visit_text_node(node)
|
361
212
|
return if !node.call_node.arguments
|
362
213
|
|
@@ -373,6 +224,10 @@ module P2
|
|
373
224
|
end
|
374
225
|
end
|
375
226
|
|
227
|
+
# Visits a raw node.
|
228
|
+
#
|
229
|
+
# @param node [P2::RawNode] node
|
230
|
+
# @return [void]
|
376
231
|
def visit_raw_node(node)
|
377
232
|
return if !node.call_node.arguments
|
378
233
|
|
@@ -389,6 +244,10 @@ module P2
|
|
389
244
|
end
|
390
245
|
end
|
391
246
|
|
247
|
+
# Visits a defer node.
|
248
|
+
#
|
249
|
+
# @param node [P2::DeferNode] node
|
250
|
+
# @return [void]
|
392
251
|
def visit_defer_node(node)
|
393
252
|
block = node.block
|
394
253
|
return if !block
|
@@ -409,10 +268,14 @@ module P2
|
|
409
268
|
emit("}")
|
410
269
|
end
|
411
270
|
|
412
|
-
|
271
|
+
# Visits a builtin node.
|
272
|
+
#
|
273
|
+
# @param node [P2::BuiltinNode] node
|
274
|
+
# @return [void]
|
275
|
+
def visit_builtin_node(node)
|
413
276
|
case node.tag
|
414
277
|
when :tag
|
415
|
-
args = node.call_node.arguments&.arguments
|
278
|
+
args = node.call_node.arguments&.arguments
|
416
279
|
when :html5
|
417
280
|
emit_html(node.location, '<!DOCTYPE html><html>')
|
418
281
|
visit(node.block.body) if node.block
|
@@ -425,28 +288,52 @@ module P2
|
|
425
288
|
end
|
426
289
|
end
|
427
290
|
|
291
|
+
# Visits a yield node.
|
292
|
+
#
|
293
|
+
# @param node [P2::YieldNode] node
|
294
|
+
# @return [void]
|
428
295
|
def visit_yield_node(node)
|
429
|
-
adjust_whitespace(node.location)
|
430
296
|
flush_html_parts!
|
297
|
+
adjust_whitespace(node.location)
|
431
298
|
@yield_used = true
|
432
|
-
emit("; (__block__ ? __block__.
|
299
|
+
emit("; (__block__ ? __block__.compiled_proc.(__buffer__")
|
433
300
|
if node.arguments
|
434
301
|
emit(', ')
|
435
302
|
visit(node.arguments)
|
436
303
|
end
|
437
|
-
emit(") : raise(LocalJumpError, 'no block given (yield)'))")
|
304
|
+
emit(") : raise(LocalJumpError, 'no block given (yield/emit_yield)'))")
|
438
305
|
end
|
439
306
|
|
440
307
|
private
|
441
308
|
|
309
|
+
# Overrides the Sourcifier behaviour to flush any buffered HTML parts.
|
310
|
+
def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
|
311
|
+
flush_html_parts! if flush_html
|
312
|
+
super(loc, semicolon:, chomp: )
|
313
|
+
end
|
314
|
+
|
315
|
+
# Returns the given str inside interpolation syntax (#{...}).
|
316
|
+
#
|
317
|
+
# @param str [String] input string
|
318
|
+
# @return [String] output string
|
442
319
|
def interpolated(str)
|
443
320
|
"#\{#{str}}"
|
444
321
|
end
|
445
322
|
|
446
|
-
|
447
|
-
|
323
|
+
# Formats the given AST with minimal whitespace. Used for formatting
|
324
|
+
# arbitrary expressions.
|
325
|
+
#
|
326
|
+
# @param node [Prism::Node] AST
|
327
|
+
# @return [String] generated source code
|
328
|
+
def format_code(node)
|
329
|
+
Compiler.new(minimize_whitespace: true).to_source(node)
|
448
330
|
end
|
449
331
|
|
332
|
+
# Formats a comma separated list of AST nodes. Used for formatting partial
|
333
|
+
# argument lists.
|
334
|
+
#
|
335
|
+
# @param list [Array<Prism::Node>] node list
|
336
|
+
# @return [String] generated source code
|
450
337
|
def format_code_comma_separated_nodes(list)
|
451
338
|
compiler = self.class.new(minimize_whitespace: true)
|
452
339
|
compiler.visit_comma_separated_nodes(list)
|
@@ -455,10 +342,19 @@ module P2
|
|
455
342
|
|
456
343
|
VOID_TAGS = %w(area base br col embed hr img input link meta param source track wbr)
|
457
344
|
|
345
|
+
# Returns true if given HTML element is void (needs no closing tag).
|
346
|
+
#
|
347
|
+
# @param tag [String, Symbol] HTML tag
|
348
|
+
# @return [bool] void or not
|
458
349
|
def is_void_element?(tag)
|
459
350
|
VOID_TAGS.include?(tag.to_s)
|
460
351
|
end
|
461
352
|
|
353
|
+
# Formats an open tag with optional attributes.
|
354
|
+
#
|
355
|
+
# @param tag [String, Symbol] HTML tag
|
356
|
+
# @param attributes [Hash, nil] attributes
|
357
|
+
# @return [String] HTML
|
462
358
|
def format_html_tag_open(tag, attributes)
|
463
359
|
tag = convert_tag(tag)
|
464
360
|
if attributes && attributes&.elements.size > 0
|
@@ -468,29 +364,42 @@ module P2
|
|
468
364
|
end
|
469
365
|
end
|
470
366
|
|
367
|
+
# Formats a close tag.
|
368
|
+
#
|
369
|
+
# @param tag [String, Symbol] HTML tag
|
370
|
+
# @return [String] HTML
|
471
371
|
def format_html_tag_close(tag)
|
472
372
|
tag = convert_tag(tag)
|
473
373
|
"</#{tag}>"
|
474
374
|
end
|
475
375
|
|
376
|
+
# Converts a tag's underscores to dashes. If tag is dynamic, emits code to
|
377
|
+
# convert underscores to dashes at runtime.
|
378
|
+
#
|
379
|
+
# @param tag [any] tag
|
380
|
+
# @return [String] convert tag or code
|
476
381
|
def convert_tag(tag)
|
477
382
|
case tag
|
478
383
|
when Prism::SymbolNode, Prism::StringNode
|
479
|
-
P2.
|
384
|
+
P2.underscores_to_dashes(tag.unescaped)
|
480
385
|
when Prism::Node
|
481
|
-
"
|
386
|
+
interpolated("P2.underscores_to_dashes(#{format_code(tag)})")
|
482
387
|
else
|
483
|
-
P2.
|
388
|
+
P2.underscores_to_dashes(tag)
|
484
389
|
end
|
485
390
|
end
|
486
391
|
|
392
|
+
# Formats a literal value for the given node.
|
393
|
+
#
|
394
|
+
# @param node [Prism::Node] AST node
|
395
|
+
# @return [String] literal representation
|
487
396
|
def format_literal(node)
|
488
397
|
case node
|
489
398
|
when Prism::SymbolNode, Prism::StringNode
|
490
399
|
node.unescaped
|
491
400
|
when Prism::IntegerNode, Prism::FloatNode
|
492
401
|
node.value.to_s
|
493
|
-
when Prism::InterpolatedStringNode
|
402
|
+
when Prism::InterpolatedStringNode
|
494
403
|
format_code(node)[1..-2]
|
495
404
|
when Prism::TrueNode
|
496
405
|
'true'
|
@@ -499,7 +408,7 @@ module P2
|
|
499
408
|
when Prism::NilNode
|
500
409
|
''
|
501
410
|
else
|
502
|
-
|
411
|
+
interpolated(format_code(node))
|
503
412
|
end
|
504
413
|
end
|
505
414
|
|
@@ -513,6 +422,10 @@ module P2
|
|
513
422
|
Prism::TrueNode
|
514
423
|
]
|
515
424
|
|
425
|
+
# Returns true if given node is static, i.e. is a literal value.
|
426
|
+
#
|
427
|
+
# @param node [Prism::Node] AST node
|
428
|
+
# @return [bool] static or not
|
516
429
|
def is_static_node?(node)
|
517
430
|
STATIC_NODE_TYPES.include?(node.class)
|
518
431
|
end
|
@@ -522,10 +435,18 @@ module P2
|
|
522
435
|
Prism::InterpolatedStringNode
|
523
436
|
]
|
524
437
|
|
438
|
+
# Returns true if given node a string or interpolated string.
|
439
|
+
#
|
440
|
+
# @param node [Prism::Node] AST node
|
441
|
+
# @return [bool] string node or not
|
525
442
|
def is_string_type_node?(node)
|
526
443
|
STRING_TYPE_NODE_TYPES.include?(node.class)
|
527
444
|
end
|
528
445
|
|
446
|
+
# Formats HTML attributes from the given node.
|
447
|
+
#
|
448
|
+
# @param node [Prism::Node] AST node
|
449
|
+
# @return [String] HTML
|
529
450
|
def format_html_attributes(node)
|
530
451
|
elements = node.elements
|
531
452
|
dynamic_attributes = elements.any? do
|
@@ -533,7 +454,7 @@ module P2
|
|
533
454
|
!is_static_node?(it.key) || !is_static_node?(it.value)
|
534
455
|
end
|
535
456
|
|
536
|
-
return interpolated("P2.
|
457
|
+
return interpolated("P2.format_tag_attrs(#{format_code(node)})") if dynamic_attributes
|
537
458
|
|
538
459
|
parts = elements.map do
|
539
460
|
key = it.key
|
@@ -544,12 +465,12 @@ module P2
|
|
544
465
|
when Prism::FalseNode, Prism::NilNode
|
545
466
|
nil
|
546
467
|
else
|
547
|
-
k = format_literal(key)
|
468
|
+
k = format_literal(key)
|
548
469
|
if is_static_node?(value)
|
549
470
|
value = format_literal(value)
|
550
|
-
"#{P2.
|
471
|
+
"#{P2.underscores_to_dashes(k)}=\\\"#{value}\\\""
|
551
472
|
else
|
552
|
-
"#{P2.
|
473
|
+
"#{P2.underscores_to_dashes(k)}=\\\"#\{#{format_code(value)}}\\\""
|
553
474
|
end
|
554
475
|
end
|
555
476
|
end
|
@@ -557,12 +478,20 @@ module P2
|
|
557
478
|
parts.compact.join(' ')
|
558
479
|
end
|
559
480
|
|
481
|
+
# Emits HTML into the pending HTML buffer.
|
482
|
+
#
|
483
|
+
# @param loc [Prism::Location] location
|
484
|
+
# @param str [String] HTML
|
485
|
+
# @return [void]
|
560
486
|
def emit_html(loc, str)
|
561
487
|
@html_loc_start ||= loc
|
562
488
|
@html_loc_end = loc
|
563
489
|
@pending_html_parts << str
|
564
490
|
end
|
565
491
|
|
492
|
+
# Flushes pending HTML parts to the source code buffer.
|
493
|
+
#
|
494
|
+
# @return [void]
|
566
495
|
def flush_html_parts!(semicolon_prefix: true)
|
567
496
|
return if @pending_html_parts.empty?
|
568
497
|
|
@@ -573,13 +502,13 @@ module P2
|
|
573
502
|
|
574
503
|
@pending_html_parts.each do
|
575
504
|
if (m = it.match(/^#\{(.+)\}$/m))
|
576
|
-
|
577
|
-
|
505
|
+
emit_html_buffer_push(code, part, quotes: true) if !part.empty?
|
506
|
+
emit_html_buffer_push(code, m[1])
|
578
507
|
else
|
579
508
|
part << it
|
580
509
|
end
|
581
510
|
end
|
582
|
-
|
511
|
+
emit_html_buffer_push(code, part, quotes: true) if !part.empty?
|
583
512
|
|
584
513
|
@pending_html_parts.clear
|
585
514
|
|
@@ -593,7 +522,13 @@ module P2
|
|
593
522
|
emit code
|
594
523
|
end
|
595
524
|
|
596
|
-
|
525
|
+
# Emits HTML buffer push code to the given source code buffer.
|
526
|
+
#
|
527
|
+
# @param buf [String] source code buffer
|
528
|
+
# @param part [String] HTML part
|
529
|
+
# @param quotes [bool] whether to wrap emitted HTML in double quotes
|
530
|
+
# @return [void]
|
531
|
+
def emit_html_buffer_push(buf, part, quotes: false)
|
597
532
|
return if part.empty?
|
598
533
|
|
599
534
|
q = quotes ? '"' : ''
|
@@ -601,9 +536,10 @@ module P2
|
|
601
536
|
part.clear
|
602
537
|
end
|
603
538
|
|
604
|
-
|
605
|
-
|
606
|
-
|
539
|
+
# Emits postlude code for templates with deferred parts.
|
540
|
+
#
|
541
|
+
# @return [void]
|
542
|
+
def emit_defer_postlude
|
607
543
|
emit("; __buffer__ = __orig_buffer__; __parts__.each { it.is_a?(Proc) ? it.() : (__buffer__ << it) }")
|
608
544
|
end
|
609
545
|
end
|
data/lib/p2/proc_ext.rb
CHANGED
@@ -4,85 +4,76 @@ require_relative './compiler'
|
|
4
4
|
|
5
5
|
# Extensions to the Proc class
|
6
6
|
class ::Proc
|
7
|
+
# Returns the compiled form code for the proc
|
8
|
+
#
|
9
|
+
# @return [String] compiled proc code
|
7
10
|
def compiled_code
|
8
|
-
P2::
|
11
|
+
P2::Compiler.compile_to_code(self).last
|
9
12
|
end
|
10
13
|
|
14
|
+
# Returns true if proc is marked as compiled
|
15
|
+
#
|
16
|
+
# @return [bool] is the proc marked as compiled
|
11
17
|
def compiled?
|
12
18
|
@is_compiled
|
13
19
|
end
|
14
20
|
|
15
21
|
# marks the proc as compiled, i.e. can render directly and takes a string
|
16
22
|
# buffer as first argument
|
23
|
+
#
|
24
|
+
# @return [self]
|
17
25
|
def compiled!
|
18
26
|
@is_compiled = true
|
19
27
|
self
|
20
28
|
end
|
21
29
|
|
30
|
+
# Returns the compiled proc for the given proc. If marked as compiled, returns
|
31
|
+
# self.
|
32
|
+
#
|
33
|
+
# @return [Proc] compiled proc or self
|
22
34
|
def compiled_proc
|
23
35
|
@compiled_proc ||= @is_compiled ? self : compile
|
24
36
|
end
|
25
|
-
|
37
|
+
|
38
|
+
# Compiles the proc into the compiled form
|
39
|
+
#
|
40
|
+
# @return [Proc] compiled proc
|
26
41
|
def compile
|
27
|
-
P2::
|
42
|
+
P2::Compiler.compile(self).compiled!
|
28
43
|
rescue Sirop::Error
|
29
|
-
|
44
|
+
raise P2::Error, "Dynamically defined procs cannot be compiled"
|
30
45
|
end
|
31
46
|
|
47
|
+
# Renders the proc to HTML with the given arguments
|
48
|
+
#
|
49
|
+
# @return [String] HTML string
|
32
50
|
def render(*a, **b, &c)
|
33
51
|
compiled_proc.(+'', *a, **b, &c)
|
52
|
+
rescue Exception => e
|
53
|
+
P2.translate_backtrace(e)
|
54
|
+
raise e
|
34
55
|
end
|
35
56
|
|
57
|
+
# Renders the proc into the given buffer
|
58
|
+
#
|
59
|
+
# @return [String] HTML string
|
36
60
|
def render_to_buffer(buf, *a, **b, &c)
|
37
61
|
compiled_proc.(buf, *a, **b, &c)
|
38
62
|
end
|
39
63
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
__buffer__
|
44
|
-
}.compiled!
|
45
|
-
end
|
46
|
-
|
64
|
+
# Returns a proc that applies the given arguments to the original proc
|
65
|
+
#
|
66
|
+
# @return [Proc] applied proc
|
47
67
|
def apply(*a, **b, &c)
|
48
68
|
compiled = compiled_proc
|
49
69
|
c_compiled = c&.compiled_proc
|
50
|
-
|
70
|
+
|
51
71
|
->(__buffer__, *x, **y, &z) {
|
52
72
|
c_proc = c_compiled && ->(__buffer__, *d, **e) {
|
53
73
|
c_compiled.(__buffer__, *a, *d, **b, **e, &z)
|
54
74
|
}.compiled!
|
55
|
-
|
75
|
+
|
56
76
|
compiled.(__buffer__, *a, *x, **b, **y, &c_proc)
|
57
77
|
}.compiled!
|
58
78
|
end
|
59
79
|
end
|
60
|
-
|
61
|
-
module P2
|
62
|
-
class UncompiledProcWrapper
|
63
|
-
def initialize(proc)
|
64
|
-
@proc = proc
|
65
|
-
end
|
66
|
-
|
67
|
-
def call(buffer, *a, **b)
|
68
|
-
@buffer = buffer
|
69
|
-
instance_exec(*a, **b, &@proc)
|
70
|
-
end
|
71
|
-
|
72
|
-
def method_missing(sym, *a, **b, &c)
|
73
|
-
tag(sym, *a, **b, &c)
|
74
|
-
end
|
75
|
-
|
76
|
-
def p(*a, **b, &c)
|
77
|
-
tag(:p, *a, **b, &c)
|
78
|
-
end
|
79
|
-
|
80
|
-
def tag(sym, *a, **b, &block)
|
81
|
-
@buffer << "<#{sym}>"
|
82
|
-
if block
|
83
|
-
instance_eval(&block)
|
84
|
-
end
|
85
|
-
@buffer << "</#{sym}>"
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
data/lib/p2/version.rb
CHANGED
data/lib/p2.rb
CHANGED
@@ -3,54 +3,69 @@
|
|
3
3
|
require_relative 'p2/compiler'
|
4
4
|
require_relative 'p2/proc_ext'
|
5
5
|
|
6
|
-
# P2 is a
|
6
|
+
# P2 is a functional templating library. In P2, templates are expressed as plain
|
7
|
+
# Ruby procs.
|
7
8
|
module P2
|
8
9
|
# Exception class used to signal templating-related errors
|
9
10
|
class Error < RuntimeError; end
|
10
11
|
|
11
12
|
extend self
|
12
13
|
|
13
|
-
|
14
|
+
# Formats the given string, converting underscores to dashes.
|
15
|
+
#
|
16
|
+
# @param tag [String, Symbol] input string
|
17
|
+
# @return [String] output string
|
18
|
+
def underscores_to_dashes(tag)
|
14
19
|
tag.to_s.gsub('_', '-')
|
15
20
|
end
|
16
21
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def
|
22
|
+
# Formats the given hash as tag attributes.
|
23
|
+
#
|
24
|
+
# @param attrs [Hash] input hash
|
25
|
+
# @return [String] formatted attributes
|
26
|
+
def format_tag_attrs(attrs)
|
22
27
|
attrs.each_with_object(+'') do |(k, v), html|
|
23
28
|
case v
|
24
29
|
when nil, false
|
25
30
|
when true
|
26
31
|
html << ' ' if !html.empty?
|
27
|
-
html <<
|
32
|
+
html << underscores_to_dashes(k)
|
28
33
|
else
|
29
34
|
html << ' ' if !html.empty?
|
30
35
|
v = v.join(' ') if v.is_a?(Array)
|
31
|
-
html << "#{
|
36
|
+
html << "#{underscores_to_dashes(k)}=\"#{v}\""
|
32
37
|
end
|
33
38
|
end
|
34
39
|
end
|
35
40
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
}
|
48
|
-
e.set_backtrace(backtrace)
|
41
|
+
# Translates an exceptions backtrace using a source map.
|
42
|
+
#
|
43
|
+
# @param exception [Exception] raised exception
|
44
|
+
# @param source_map [Hash] source map
|
45
|
+
#
|
46
|
+
# @return [Exception] raised exception
|
47
|
+
def translate_backtrace(exception)
|
48
|
+
cache = {}
|
49
|
+
backtrace = exception.backtrace.map { |e| compute_backtrace_entry(e, cache) }
|
50
|
+
exception.set_backtrace(backtrace)
|
51
|
+
exception
|
49
52
|
end
|
50
53
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
+
# Computes a backtrace entry with caching.
|
55
|
+
#
|
56
|
+
# @param entry [String] backtrace entry
|
57
|
+
# @param cache [Hash] cache store mapping compiled filename to source_map
|
58
|
+
def compute_backtrace_entry(entry, cache)
|
59
|
+
m = entry.match(/^((\:\:\(.+\:.+\))\:(\d+))/)
|
60
|
+
return entry if !m
|
61
|
+
|
62
|
+
fn = m[2]
|
63
|
+
line = m[3].to_i
|
64
|
+
source_map = cache[fn] ||= Compiler.source_map_store[fn]
|
65
|
+
return entry if !source_map
|
66
|
+
|
67
|
+
source_line = source_map[line] || "?(#{line})"
|
68
|
+
entry.sub(m[1], "#{source_map[:source_fn]}:#{source_line}")
|
54
69
|
end
|
55
70
|
|
56
71
|
# Renders Markdown into HTML. The `opts` argument will be merged with the
|
@@ -64,7 +79,7 @@ module P2
|
|
64
79
|
require 'kramdown'
|
65
80
|
require 'rouge'
|
66
81
|
require 'kramdown-parser-gfm'
|
67
|
-
|
82
|
+
|
68
83
|
opts = default_kramdown_options.merge(opts)
|
69
84
|
Kramdown::Document.new(markdown, **opts).to_html
|
70
85
|
end
|
@@ -84,7 +99,7 @@ module P2
|
|
84
99
|
# Sets the default Kramdown options used for rendering Markdown.
|
85
100
|
#
|
86
101
|
# @param opts [Hash] Kramdown options
|
87
|
-
# @return [
|
102
|
+
# @return [Hash] Kramdown options
|
88
103
|
def default_kramdown_options=(opts)
|
89
104
|
@default_kramdown_options = opts
|
90
105
|
end
|
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.3'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -104,6 +104,8 @@ files:
|
|
104
104
|
- README.md
|
105
105
|
- lib/p2.rb
|
106
106
|
- lib/p2/compiler.rb
|
107
|
+
- lib/p2/compiler/nodes.rb
|
108
|
+
- lib/p2/compiler/tag_translator.rb
|
107
109
|
- lib/p2/proc_ext.rb
|
108
110
|
- lib/p2/version.rb
|
109
111
|
- p2.png
|