p2 2.0.1 → 2.2

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