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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c34fa05073968d2f8aed0d766ec9b59275fdb061ce2a328ad18038ce0f986068
4
- data.tar.gz: c0bab8233ffc293432b33a531e554ebc379ec7d796c2f4046e105b185e0c5f5e
3
+ metadata.gz: ad3bf1a720998e9b118a15d4a7b57677c5938bf74d6f65f474653ecaf58a9387
4
+ data.tar.gz: 6299a9c113a3bf8cc3c0a2c745ba281f7f42304ae7d127fca907b2cead73788c
5
5
  SHA512:
6
- metadata.gz: bb711aac468494ad590779e45a97fa9593d9a7e0e172389ae14125ba1576a4dda90bf5f796c56592d8a1b939cf00d2a69cd91386f5ef583453b2045bee333f5d
7
- data.tar.gz: 8a1ee9990feb82379a01bd1971b00c844d3b0f3f9f0cc6531596619f47abf0e56c629d04e34a54f12594e7a73718e88854ab6056b30ad8019ab9a63d133e2101
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
- P2 is a templating engine for dynamically producing HTML. P2 templates are
28
- expressed as Ruby procs, leading to easier debugging, better protection against
29
- HTML injection attacks, and better code reuse.
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 arguments,
36
- making the data flow easy to follow and understand. P2 also lets developers
37
- create derivative templates using full or partial parameter application.
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
- - Automatic conversion of backtraces for exceptions occurring while rendering,
71
- in order to point to the correct spot in the original template code.
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
- ## Adding Tags
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 @dependencies.head_markup }
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
- @dependencies.js '/static/js/button.js'
488
- @dependencies.css '/static/css/button.css'
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
- @dependencies.js '/static/js/heading.js'
495
- @dependencies.css '/static/css/heading.css'
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
- module P2
8
- class TagNode
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
- 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 = TagTransformer.transform(ast.body)
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(#{@source_map.inspect}).then { |src_map| ->(__buffer__")
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(") do\n")
114
+
115
+ emit(") {\n")
276
116
  end
277
117
  @buffer << source_code
278
- emit_postlude
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(";") if @buffer !~ /\n\s*$/m
283
- emit("rescue Exception => e; P2.translate_backtrace(e, src_map); raise e; end }")
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
- def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
289
- flush_html_parts! if flush_html
290
- super(loc, semicolon:, chomp: )
291
- end
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)}.render_to_buffer(__buffer__)")
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
- convert_to_s = !is_string_type_node?(node.inner_text)
317
- if convert_to_s
318
- emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)}).to_s)"))
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}.render_to_buffer(__buffer__")
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
- def visit_custom_tag_node(node)
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__.render_to_buffer(__buffer__")
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
- def format_code(node, klass = self.class)
447
- klass.new(minimize_whitespace: true).to_source(node)
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.format_tag(tag.unescaped)
384
+ P2.underscores_to_dashes(tag.unescaped)
480
385
  when Prism::Node
481
- "#\{P2.format_tag(#{format_code(tag)})}"
386
+ interpolated("P2.underscores_to_dashes(#{format_code(tag)})")
482
387
  else
483
- P2.format_tag(tag)
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
- "#\{#{format_code(node)}}"
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.format_html_attrs(#{format_code(node)})") if dynamic_attributes
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.format_html_attr_key(k)}=\\\"#{value}\\\""
471
+ "#{P2.underscores_to_dashes(k)}=\\\"#{value}\\\""
551
472
  else
552
- "#{P2.format_html_attr_key(k)}=\\\"#\{#{format_code(value)}}\\\""
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
- emit_buffer_push(code, part, quotes: true) if !part.empty?
577
- emit_buffer_push(code, m[1])
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
- emit_buffer_push(code, part, quotes: true) if !part.empty?
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
- def emit_buffer_push(buf, part, quotes: false)
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
- def emit_postlude
605
- return if !@defer_mode
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::TemplateCompiler.compile_to_code(self).last
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::TemplateCompiler.compile(self).compiled!
42
+ P2::Compiler.compile(self).compiled!
28
43
  rescue Sirop::Error
29
- uncompiled_renderer
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
- def uncompiled_renderer
41
- ->(__buffer__, *a, **b, &c) {
42
- P2::UncompiledProcWrapper.new(self).call(__buffer__, *a, **b, &c)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module P2
4
- VERSION = '2.1'
4
+ VERSION = '2.3'
5
5
  end
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 composable templating library
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
- def format_tag(tag)
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
- def format_html_attr_key(tag)
18
- tag.to_s.tr('_', '-')
19
- end
20
-
21
- def format_html_attrs(attrs)
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 << format_html_attr_key(k)
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 << "#{format_html_attr_key(k)}=\"#{v}\""
36
+ html << "#{underscores_to_dashes(k)}=\"#{v}\""
32
37
  end
33
38
  end
34
39
  end
35
40
 
36
- def translate_backtrace(e, source_map)
37
- re = compute_source_map_re(source_map)
38
- source_fn = source_map[:source_fn]
39
- backtrace = e.backtrace.map {
40
- if (m = it.match(re))
41
- line = m[2].to_i
42
- source_line = source_map[line] || "?(#{line})"
43
- it.sub(m[1], "#{source_fn}:#{source_line}")
44
- else
45
- it
46
- end
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
- def compute_source_map_re(source_map)
52
- escaped = source_map[:compiled_fn].gsub(/[\(\)]/) { "\\#{it[0]}" }
53
- /^(#{escaped}\:(\d+))/
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 [void]
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.1'
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