p2 2.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 +6 -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 +162 -235
- data/lib/p2/proc_ext.rb +30 -42
- data/lib/p2/version.rb +1 -1
- data/lib/p2.rb +30 -14
- 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: 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,7 +1,12 @@
|
|
1
|
+
# 2.2 2025-08-09
|
2
|
+
|
3
|
+
- Update docs
|
4
|
+
- Refactor code
|
5
|
+
|
1
6
|
# 2.1 2025-08-08
|
2
7
|
|
3
8
|
- Optimize output code: directly invoke component templates instead of calling
|
4
|
-
`P2.render_emit_call`. P2 is now
|
9
|
+
`P2.render_emit_call`. P2 is now
|
5
10
|
- Optimize output code: use separate pushes to buffer instead of interpolated
|
6
11
|
strings.
|
7
12
|
- 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
|
6
|
+
require_relative './compiler/nodes'
|
7
|
+
require_relative './compiler/tag_translator'
|
142
8
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
class CustomTagNode
|
155
|
-
attr_reader :tag, :call_node, :location, :block
|
156
|
-
|
157
|
-
def initialize(call_node, compiler)
|
158
|
-
@call_node = call_node
|
159
|
-
@tag = call_node.name
|
160
|
-
@location = call_node.location
|
161
|
-
@block = call_node.block && compiler.visit(call_node.block)
|
162
|
-
end
|
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'
|
@@ -231,6 +50,7 @@ module P2
|
|
231
50
|
|
232
51
|
attr_reader :source_map
|
233
52
|
|
53
|
+
# Initializes a compiler.
|
234
54
|
def initialize(**)
|
235
55
|
super(**)
|
236
56
|
@pending_html_parts = []
|
@@ -239,6 +59,11 @@ module P2
|
|
239
59
|
@yield_used = nil
|
240
60
|
end
|
241
61
|
|
62
|
+
# Initializes a source map.
|
63
|
+
#
|
64
|
+
# @param orig_proc [Proc] template proc
|
65
|
+
# @param orig_ast [Prism::Node] template AST
|
66
|
+
# @return [self]
|
242
67
|
def with_source_map(orig_proc, orig_ast)
|
243
68
|
compiled_fn = "::(#{orig_proc.source_location.join(':')})"
|
244
69
|
@source_map = {
|
@@ -249,6 +74,12 @@ module P2
|
|
249
74
|
self
|
250
75
|
end
|
251
76
|
|
77
|
+
# Formats the source code for a compiled template proc.
|
78
|
+
#
|
79
|
+
# @param ast [Prism::Node] translated AST
|
80
|
+
# @param orig_ast [Prism::Node] original template AST
|
81
|
+
# @param wrap [bool] whether to wrap the generated code with a literal Proc definition
|
82
|
+
# @return [String] compiled template source code
|
252
83
|
def format_compiled_template(ast, orig_ast, wrap:, binding:)
|
253
84
|
# generate source code
|
254
85
|
@binding = binding
|
@@ -271,11 +102,11 @@ module P2
|
|
271
102
|
if @yield_used
|
272
103
|
emit(', &__block__')
|
273
104
|
end
|
274
|
-
|
105
|
+
|
275
106
|
emit(") do\n")
|
276
107
|
end
|
277
108
|
@buffer << source_code
|
278
|
-
|
109
|
+
emit_defer_postlude if @defer_mode
|
279
110
|
if wrap
|
280
111
|
emit('; __buffer__')
|
281
112
|
adjust_whitespace(orig_ast.closing_loc)
|
@@ -285,11 +116,10 @@ module P2
|
|
285
116
|
@buffer
|
286
117
|
end
|
287
118
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
119
|
+
# Visits a tag node.
|
120
|
+
#
|
121
|
+
# @param node [P2::TagNode] node
|
122
|
+
# @return [void]
|
293
123
|
def visit_tag_node(node)
|
294
124
|
tag = node.tag
|
295
125
|
if tag.is_a?(Symbol) && tag =~ /^[A-Z]/
|
@@ -324,6 +154,10 @@ module P2
|
|
324
154
|
emit_html(node.location, format_html_tag_close(tag))
|
325
155
|
end
|
326
156
|
|
157
|
+
# Visits a const tag node.
|
158
|
+
#
|
159
|
+
# @param node [P2::ConstTagNode] node
|
160
|
+
# @return [void]
|
327
161
|
def visit_const_tag_node(node)
|
328
162
|
flush_html_parts!
|
329
163
|
adjust_whitespace(node.location)
|
@@ -339,24 +173,32 @@ module P2
|
|
339
173
|
emit(');')
|
340
174
|
end
|
341
175
|
|
176
|
+
# Visits a render node.
|
177
|
+
#
|
178
|
+
# @param node [P2::RenderNode] node
|
179
|
+
# @return [void]
|
342
180
|
def visit_render_node(node)
|
343
181
|
args = node.call_node.arguments.arguments
|
344
182
|
first_arg = args.first
|
345
|
-
|
183
|
+
|
346
184
|
block_embed = node.block && "&(->(__buffer__) #{format_code(node.block)}.compiled!)"
|
347
185
|
block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments
|
348
186
|
|
349
187
|
flush_html_parts!
|
350
188
|
adjust_whitespace(node.location)
|
351
|
-
|
189
|
+
|
352
190
|
if args.length == 1
|
353
191
|
emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__#{block_embed})")
|
354
192
|
else
|
355
193
|
args_code = format_code_comma_separated_nodes(args[1..])
|
356
194
|
emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__, #{args_code}#{block_embed})")
|
357
|
-
end
|
195
|
+
end
|
358
196
|
end
|
359
197
|
|
198
|
+
# Visits a text node.
|
199
|
+
#
|
200
|
+
# @param node [P2::TextNode] node
|
201
|
+
# @return [void]
|
360
202
|
def visit_text_node(node)
|
361
203
|
return if !node.call_node.arguments
|
362
204
|
|
@@ -373,6 +215,10 @@ module P2
|
|
373
215
|
end
|
374
216
|
end
|
375
217
|
|
218
|
+
# Visits a raw node.
|
219
|
+
#
|
220
|
+
# @param node [P2::RawNode] node
|
221
|
+
# @return [void]
|
376
222
|
def visit_raw_node(node)
|
377
223
|
return if !node.call_node.arguments
|
378
224
|
|
@@ -389,6 +235,10 @@ module P2
|
|
389
235
|
end
|
390
236
|
end
|
391
237
|
|
238
|
+
# Visits a defer node.
|
239
|
+
#
|
240
|
+
# @param node [P2::DeferNode] node
|
241
|
+
# @return [void]
|
392
242
|
def visit_defer_node(node)
|
393
243
|
block = node.block
|
394
244
|
return if !block
|
@@ -409,10 +259,14 @@ module P2
|
|
409
259
|
emit("}")
|
410
260
|
end
|
411
261
|
|
412
|
-
|
262
|
+
# Visits a builtin node.
|
263
|
+
#
|
264
|
+
# @param node [P2::BuiltinNode] node
|
265
|
+
# @return [void]
|
266
|
+
def visit_builtin_node(node)
|
413
267
|
case node.tag
|
414
268
|
when :tag
|
415
|
-
args = node.call_node.arguments&.arguments
|
269
|
+
args = node.call_node.arguments&.arguments
|
416
270
|
when :html5
|
417
271
|
emit_html(node.location, '<!DOCTYPE html><html>')
|
418
272
|
visit(node.block.body) if node.block
|
@@ -425,6 +279,10 @@ module P2
|
|
425
279
|
end
|
426
280
|
end
|
427
281
|
|
282
|
+
# Visits a yield node.
|
283
|
+
#
|
284
|
+
# @param node [P2::YieldNode] node
|
285
|
+
# @return [void]
|
428
286
|
def visit_yield_node(node)
|
429
287
|
adjust_whitespace(node.location)
|
430
288
|
flush_html_parts!
|
@@ -439,14 +297,34 @@ module P2
|
|
439
297
|
|
440
298
|
private
|
441
299
|
|
300
|
+
# Overrides the Sourcifier behaviour to flush any buffered HTML parts.
|
301
|
+
def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
|
302
|
+
flush_html_parts! if flush_html
|
303
|
+
super(loc, semicolon:, chomp: )
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns the given str inside interpolation syntax (#{...}).
|
307
|
+
#
|
308
|
+
# @param str [String] input string
|
309
|
+
# @return [String] output string
|
442
310
|
def interpolated(str)
|
443
311
|
"#\{#{str}}"
|
444
312
|
end
|
445
313
|
|
446
|
-
|
447
|
-
|
314
|
+
# Formats the given AST with minimal whitespace. Used for formatting
|
315
|
+
# arbitrary expressions.
|
316
|
+
#
|
317
|
+
# @param node [Prism::Node] AST
|
318
|
+
# @return [String] generated source code
|
319
|
+
def format_code(node)
|
320
|
+
Compiler.new(minimize_whitespace: true).to_source(node)
|
448
321
|
end
|
449
322
|
|
323
|
+
# Formats a comma separated list of AST nodes. Used for formatting partial
|
324
|
+
# argument lists.
|
325
|
+
#
|
326
|
+
# @param list [Array<Prism::Node>] node list
|
327
|
+
# @return [String] generated source code
|
450
328
|
def format_code_comma_separated_nodes(list)
|
451
329
|
compiler = self.class.new(minimize_whitespace: true)
|
452
330
|
compiler.visit_comma_separated_nodes(list)
|
@@ -455,10 +333,19 @@ module P2
|
|
455
333
|
|
456
334
|
VOID_TAGS = %w(area base br col embed hr img input link meta param source track wbr)
|
457
335
|
|
336
|
+
# Returns true if given HTML element is void (needs no closing tag).
|
337
|
+
#
|
338
|
+
# @param tag [String, Symbol] HTML tag
|
339
|
+
# @return [bool] void or not
|
458
340
|
def is_void_element?(tag)
|
459
341
|
VOID_TAGS.include?(tag.to_s)
|
460
342
|
end
|
461
343
|
|
344
|
+
# Formats an open tag with optional attributes.
|
345
|
+
#
|
346
|
+
# @param tag [String, Symbol] HTML tag
|
347
|
+
# @param attributes [Hash, nil] attributes
|
348
|
+
# @return [String] HTML
|
462
349
|
def format_html_tag_open(tag, attributes)
|
463
350
|
tag = convert_tag(tag)
|
464
351
|
if attributes && attributes&.elements.size > 0
|
@@ -468,29 +355,42 @@ module P2
|
|
468
355
|
end
|
469
356
|
end
|
470
357
|
|
358
|
+
# Formats a close tag.
|
359
|
+
#
|
360
|
+
# @param tag [String, Symbol] HTML tag
|
361
|
+
# @return [String] HTML
|
471
362
|
def format_html_tag_close(tag)
|
472
363
|
tag = convert_tag(tag)
|
473
364
|
"</#{tag}>"
|
474
365
|
end
|
475
366
|
|
367
|
+
# Converts a tag's underscores to dashes. If tag is dynamic, emits code to
|
368
|
+
# convert underscores to dashes at runtime.
|
369
|
+
#
|
370
|
+
# @param tag [any] tag
|
371
|
+
# @return [String] convert tag or code
|
476
372
|
def convert_tag(tag)
|
477
373
|
case tag
|
478
374
|
when Prism::SymbolNode, Prism::StringNode
|
479
|
-
P2.
|
375
|
+
P2.underscores_to_dashes(tag.unescaped)
|
480
376
|
when Prism::Node
|
481
|
-
"
|
377
|
+
interpolated("P2.underscores_to_dashes(#{format_code(tag)})")
|
482
378
|
else
|
483
|
-
P2.
|
379
|
+
P2.underscores_to_dashes(tag)
|
484
380
|
end
|
485
381
|
end
|
486
382
|
|
383
|
+
# Formats a literal value for the given node.
|
384
|
+
#
|
385
|
+
# @param node [Prism::Node] AST node
|
386
|
+
# @return [String] literal representation
|
487
387
|
def format_literal(node)
|
488
388
|
case node
|
489
389
|
when Prism::SymbolNode, Prism::StringNode
|
490
390
|
node.unescaped
|
491
391
|
when Prism::IntegerNode, Prism::FloatNode
|
492
392
|
node.value.to_s
|
493
|
-
when Prism::InterpolatedStringNode
|
393
|
+
when Prism::InterpolatedStringNode
|
494
394
|
format_code(node)[1..-2]
|
495
395
|
when Prism::TrueNode
|
496
396
|
'true'
|
@@ -499,7 +399,7 @@ module P2
|
|
499
399
|
when Prism::NilNode
|
500
400
|
''
|
501
401
|
else
|
502
|
-
|
402
|
+
interpolated(format_code(node))
|
503
403
|
end
|
504
404
|
end
|
505
405
|
|
@@ -513,6 +413,10 @@ module P2
|
|
513
413
|
Prism::TrueNode
|
514
414
|
]
|
515
415
|
|
416
|
+
# Returns true if given node is static, i.e. is a literal value.
|
417
|
+
#
|
418
|
+
# @param node [Prism::Node] AST node
|
419
|
+
# @return [bool] static or not
|
516
420
|
def is_static_node?(node)
|
517
421
|
STATIC_NODE_TYPES.include?(node.class)
|
518
422
|
end
|
@@ -522,10 +426,18 @@ module P2
|
|
522
426
|
Prism::InterpolatedStringNode
|
523
427
|
]
|
524
428
|
|
429
|
+
# Returns true if given node a string or interpolated string.
|
430
|
+
#
|
431
|
+
# @param node [Prism::Node] AST node
|
432
|
+
# @return [bool] string node or not
|
525
433
|
def is_string_type_node?(node)
|
526
434
|
STRING_TYPE_NODE_TYPES.include?(node.class)
|
527
435
|
end
|
528
436
|
|
437
|
+
# Formats HTML attributes from the given node.
|
438
|
+
#
|
439
|
+
# @param node [Prism::Node] AST node
|
440
|
+
# @return [String] HTML
|
529
441
|
def format_html_attributes(node)
|
530
442
|
elements = node.elements
|
531
443
|
dynamic_attributes = elements.any? do
|
@@ -533,7 +445,7 @@ module P2
|
|
533
445
|
!is_static_node?(it.key) || !is_static_node?(it.value)
|
534
446
|
end
|
535
447
|
|
536
|
-
return interpolated("P2.
|
448
|
+
return interpolated("P2.format_tag_attrs(#{format_code(node)})") if dynamic_attributes
|
537
449
|
|
538
450
|
parts = elements.map do
|
539
451
|
key = it.key
|
@@ -544,12 +456,12 @@ module P2
|
|
544
456
|
when Prism::FalseNode, Prism::NilNode
|
545
457
|
nil
|
546
458
|
else
|
547
|
-
k = format_literal(key)
|
459
|
+
k = format_literal(key)
|
548
460
|
if is_static_node?(value)
|
549
461
|
value = format_literal(value)
|
550
|
-
"#{P2.
|
462
|
+
"#{P2.underscores_to_dashes(k)}=\\\"#{value}\\\""
|
551
463
|
else
|
552
|
-
"#{P2.
|
464
|
+
"#{P2.underscores_to_dashes(k)}=\\\"#\{#{format_code(value)}}\\\""
|
553
465
|
end
|
554
466
|
end
|
555
467
|
end
|
@@ -557,12 +469,20 @@ module P2
|
|
557
469
|
parts.compact.join(' ')
|
558
470
|
end
|
559
471
|
|
472
|
+
# Emits HTML into the pending HTML buffer.
|
473
|
+
#
|
474
|
+
# @param loc [Prism::Location] location
|
475
|
+
# @param str [String] HTML
|
476
|
+
# @return [void]
|
560
477
|
def emit_html(loc, str)
|
561
478
|
@html_loc_start ||= loc
|
562
479
|
@html_loc_end = loc
|
563
480
|
@pending_html_parts << str
|
564
481
|
end
|
565
482
|
|
483
|
+
# Flushes pending HTML parts to the source code buffer.
|
484
|
+
#
|
485
|
+
# @return [void]
|
566
486
|
def flush_html_parts!(semicolon_prefix: true)
|
567
487
|
return if @pending_html_parts.empty?
|
568
488
|
|
@@ -573,13 +493,13 @@ module P2
|
|
573
493
|
|
574
494
|
@pending_html_parts.each do
|
575
495
|
if (m = it.match(/^#\{(.+)\}$/m))
|
576
|
-
|
577
|
-
|
496
|
+
emit_html_buffer_push(code, part, quotes: true) if !part.empty?
|
497
|
+
emit_html_buffer_push(code, m[1])
|
578
498
|
else
|
579
499
|
part << it
|
580
500
|
end
|
581
501
|
end
|
582
|
-
|
502
|
+
emit_html_buffer_push(code, part, quotes: true) if !part.empty?
|
583
503
|
|
584
504
|
@pending_html_parts.clear
|
585
505
|
|
@@ -593,7 +513,13 @@ module P2
|
|
593
513
|
emit code
|
594
514
|
end
|
595
515
|
|
596
|
-
|
516
|
+
# Emits HTML buffer push code to the given source code buffer.
|
517
|
+
#
|
518
|
+
# @param buf [String] source code buffer
|
519
|
+
# @param part [String] HTML part
|
520
|
+
# @param quotes [bool] whether to wrap emitted HTML in double quotes
|
521
|
+
# @return [void]
|
522
|
+
def emit_html_buffer_push(buf, part, quotes: false)
|
597
523
|
return if part.empty?
|
598
524
|
|
599
525
|
q = quotes ? '"' : ''
|
@@ -601,9 +527,10 @@ module P2
|
|
601
527
|
part.clear
|
602
528
|
end
|
603
529
|
|
604
|
-
|
605
|
-
|
606
|
-
|
530
|
+
# Emits postlude code for templates with deferred parts.
|
531
|
+
#
|
532
|
+
# @return [void]
|
533
|
+
def emit_defer_postlude
|
607
534
|
emit("; __buffer__ = __orig_buffer__; __parts__.each { it.is_a?(Proc) ? it.() : (__buffer__ << it) }")
|
608
535
|
end
|
609
536
|
end
|
data/lib/p2/proc_ext.rb
CHANGED
@@ -4,85 +4,73 @@ 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)
|
34
52
|
end
|
35
53
|
|
54
|
+
# Renders the proc into the given buffer
|
55
|
+
#
|
56
|
+
# @return [String] HTML string
|
36
57
|
def render_to_buffer(buf, *a, **b, &c)
|
37
58
|
compiled_proc.(buf, *a, **b, &c)
|
38
59
|
end
|
39
60
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
__buffer__
|
44
|
-
}.compiled!
|
45
|
-
end
|
46
|
-
|
61
|
+
# Returns a proc that applies the given arguments to the original proc
|
62
|
+
#
|
63
|
+
# @return [Proc] applied proc
|
47
64
|
def apply(*a, **b, &c)
|
48
65
|
compiled = compiled_proc
|
49
66
|
c_compiled = c&.compiled_proc
|
50
|
-
|
67
|
+
|
51
68
|
->(__buffer__, *x, **y, &z) {
|
52
69
|
c_proc = c_compiled && ->(__buffer__, *d, **e) {
|
53
70
|
c_compiled.(__buffer__, *a, *d, **b, **e, &z)
|
54
71
|
}.compiled!
|
55
|
-
|
72
|
+
|
56
73
|
compiled.(__buffer__, *a, *x, **b, **y, &c_proc)
|
57
74
|
}.compiled!
|
58
75
|
end
|
59
76
|
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,40 +3,51 @@
|
|
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
|
-
|
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, source_map)
|
37
48
|
re = compute_source_map_re(source_map)
|
38
49
|
source_fn = source_map[:source_fn]
|
39
|
-
backtrace =
|
50
|
+
backtrace = exception.backtrace.map {
|
40
51
|
if (m = it.match(re))
|
41
52
|
line = m[2].to_i
|
42
53
|
source_line = source_map[line] || "?(#{line})"
|
@@ -45,9 +56,14 @@ module P2
|
|
45
56
|
it
|
46
57
|
end
|
47
58
|
}
|
48
|
-
|
59
|
+
exception.set_backtrace(backtrace)
|
60
|
+
exception
|
49
61
|
end
|
50
62
|
|
63
|
+
# Computes a Regexp for matching hits in a backtrace.
|
64
|
+
#
|
65
|
+
# @param source_map [Hash] source map
|
66
|
+
# @return [Regexp] computed regexp
|
51
67
|
def compute_source_map_re(source_map)
|
52
68
|
escaped = source_map[:compiled_fn].gsub(/[\(\)]/) { "\\#{it[0]}" }
|
53
69
|
/^(#{escaped}\:(\d+))/
|
@@ -64,7 +80,7 @@ module P2
|
|
64
80
|
require 'kramdown'
|
65
81
|
require 'rouge'
|
66
82
|
require 'kramdown-parser-gfm'
|
67
|
-
|
83
|
+
|
68
84
|
opts = default_kramdown_options.merge(opts)
|
69
85
|
Kramdown::Document.new(markdown, **opts).to_html
|
70
86
|
end
|
@@ -84,7 +100,7 @@ module P2
|
|
84
100
|
# Sets the default Kramdown options used for rendering Markdown.
|
85
101
|
#
|
86
102
|
# @param opts [Hash] Kramdown options
|
87
|
-
# @return [
|
103
|
+
# @return [Hash] Kramdown options
|
88
104
|
def default_kramdown_options=(opts)
|
89
105
|
@default_kramdown_options = opts
|
90
106
|
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.2'
|
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
|