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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c34fa05073968d2f8aed0d766ec9b59275fdb061ce2a328ad18038ce0f986068
4
- data.tar.gz: c0bab8233ffc293432b33a531e554ebc379ec7d796c2f4046e105b185e0c5f5e
3
+ metadata.gz: b35bc76b761dd22bacd7e2b8844f3be20cabdb38ee6174d4e4eaeb144f5562d8
4
+ data.tar.gz: d677436567940ef41f922892b55844685d31fc52a8287c06f2ea0d89d2f1db55
5
5
  SHA512:
6
- metadata.gz: bb711aac468494ad590779e45a97fa9593d9a7e0e172389ae14125ba1576a4dda90bf5f796c56592d8a1b939cf00d2a69cd91386f5ef583453b2045bee333f5d
7
- data.tar.gz: 8a1ee9990feb82379a01bd1971b00c844d3b0f3f9f0cc6531596619f47abf0e56c629d04e34a54f12594e7a73718e88854ab6056b30ad8019ab9a63d133e2101
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
- 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
6
+ require_relative './compiler/nodes'
7
+ require_relative './compiler/tag_translator'
142
8
 
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
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 = 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'
@@ -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
- emit_postlude
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
- 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
-
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
- def visit_custom_tag_node(node)
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
- def format_code(node, klass = self.class)
447
- klass.new(minimize_whitespace: true).to_source(node)
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.format_tag(tag.unescaped)
375
+ P2.underscores_to_dashes(tag.unescaped)
480
376
  when Prism::Node
481
- "#\{P2.format_tag(#{format_code(tag)})}"
377
+ interpolated("P2.underscores_to_dashes(#{format_code(tag)})")
482
378
  else
483
- P2.format_tag(tag)
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
- "#\{#{format_code(node)}}"
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.format_html_attrs(#{format_code(node)})") if dynamic_attributes
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.format_html_attr_key(k)}=\\\"#{value}\\\""
462
+ "#{P2.underscores_to_dashes(k)}=\\\"#{value}\\\""
551
463
  else
552
- "#{P2.format_html_attr_key(k)}=\\\"#\{#{format_code(value)}}\\\""
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
- emit_buffer_push(code, part, quotes: true) if !part.empty?
577
- emit_buffer_push(code, m[1])
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
- emit_buffer_push(code, part, quotes: true) if !part.empty?
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
- def emit_buffer_push(buf, part, quotes: false)
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
- def emit_postlude
605
- return if !@defer_mode
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::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)
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
- 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
-
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module P2
4
- VERSION = '2.1'
4
+ VERSION = '2.2'
5
5
  end
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 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)
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 = e.backtrace.map {
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
- e.set_backtrace(backtrace)
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 [void]
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.1'
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