p2 2.8 → 2.10

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: a84949b86e99acbe131d37cb624dbf60010fe720ce27081d5e5652b33f2de004
4
- data.tar.gz: c03bef8c358bae1536c04739c2fa67bd3ce6e879fd036f2e7352c6291677b888
3
+ metadata.gz: 14c5c67f786953f8783be2cb53cb1eb4ad54be9f1ce769ce340cdc07d45b7a52
4
+ data.tar.gz: 1788cf26c810fa3f4d47b6db4b0de149b223e47d6a8356af7dea580190664da9
5
5
  SHA512:
6
- metadata.gz: 2993a42368846048e02ca7cb01d2ba6f7be013870a5f39597478442d16659b730356443b1b77ee8bb36c6b9a3942bde7d8ef19e66ad087d2a69d9b686d5cc706
7
- data.tar.gz: 7739be72dd3bdf195188339509a21e24bccbae6ac8efb140a16780445f71b60fea12ab0a87831f117422b620102c277391f33db944b94b7945088f16cf099d73
6
+ metadata.gz: c898b73cbfeffcc1227bc15889d43e78dd03f745a9aee584767f4e8c22ae25d698190454c5cea9600735dddcc2da91a95fb7324b5cf906ef19824ba3dce7020c
7
+ data.tar.gz: 6575f62f6158438bc95bea56d60d0f6fe4e11b1e45269584f6a644ffcd3d647e20bd712a869a556d882c4ff00a74a93f1bdefb5e8fe759787588bae9b3ba4abb
data/CHANGELOG.md CHANGED
@@ -1,7 +1,20 @@
1
+ # 2.10 2025-09-11
2
+
3
+ - Add support for rendering XML, implement `Proc#render_xml`
4
+ - Fix handling of literal strings with double quotes
5
+ - Improve error handling for `P2::Error` exceptions
6
+
7
+ # 2.9 2025-09-02
8
+
9
+ - Tweak generated code to incorporate @byroot's
10
+ [recommendations](https://www.reddit.com/r/ruby/comments/1mtj7bx/comment/n9ckbvt/):
11
+ - Remove call to to_s coercion before calling html_escape
12
+ - Chain calls to `#<<` with emitted HTML parts
13
+
1
14
  # 2.8 2025-08-17
2
15
 
3
- - Add render_children builtin
4
- - Rename emit_yield to render_yield
16
+ - Add `#render_children` builtin
17
+ - Rename `#emit_yield` to `#render_yield`
5
18
  - Add `Proc#render_cached` for caching render result
6
19
 
7
20
  # 2.7 2025-08-17
@@ -12,13 +25,13 @@
12
25
 
13
26
  # 2.6 2025-08-16
14
27
 
15
- - Add support for block invocation.
28
+ - Add support for block invocation
16
29
 
17
30
  # 2.5 2025-08-15
18
31
 
19
- - Translate backtrace for exceptions raised in `#render_to_buffer`.
20
- - Improve display of backtrace when source map is missing entries.
21
- - Improve handling of ArgumentError raised on calling the template.
32
+ - Translate backtrace for exceptions raised in `#render_to_buffer`
33
+ - Improve display of backtrace when source map is missing entries
34
+ - Improve handling of ArgumentError raised on calling the template
22
35
  - Add `Template#apply`, `Template#compiled_proc` methods
23
36
 
24
37
  # 2.4 2025-08-10
data/README.md CHANGED
@@ -10,8 +10,8 @@
10
10
  <a href="http://rubygems.org/gems/p2">
11
11
  <img src="https://badge.fury.io/rb/p2.svg" alt="Ruby gem">
12
12
  </a>
13
- <a href="https://github.com/digital-fabric/p2/actions?query=workflow%3ATests">
14
- <img src="https://github.com/digital-fabric/p2/workflows/Tests/badge.svg" alt="Tests">
13
+ <a href="https://github.com/digital-fabric/p2/actions/workflows/test.yml">
14
+ <img src="https://github.com/digital-fabric/p2/actions/workflows/test.yml/badge.svg" alt="Tests">
15
15
  </a>
16
16
  <a href="https://github.com/digital-fabric/p2/blob/master/LICENSE">
17
17
  <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
@@ -39,7 +39,6 @@ page.render {
39
39
  #=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
40
40
  ```
41
41
 
42
-
43
42
  P2 is a templating engine for dynamically producing HTML in Ruby apps. P2
44
43
  templates are expressed as Ruby procs, leading to easier debugging, better
45
44
  protection against HTML injection attacks, and better code reuse.
@@ -90,7 +89,7 @@ P2 features:
90
89
  ## Table of Content
91
90
 
92
91
  - [Getting Started](#getting-started)
93
- - [Basic Markup](#markup)
92
+ - [Basic Markup](#basic-markup)
94
93
  - [Builtin Methods](#builtin-methods)
95
94
  - [Template Parameters](#template-parameters)
96
95
  - [Template Logic](#template-logic)
@@ -99,11 +98,12 @@ P2 features:
99
98
  - [Parameter and Block Application](#parameter-and-block-application)
100
99
  - [Higher-Order Templates](#higher-order-templates)
101
100
  - [Layout Template Composition](#layout-template-composition)
102
- - [Emitting Raw HTML](#emitting-raw-html)
103
- - [Emitting a String with HTML Escaping](#emitting-a-string-with-html-escaping)
104
101
  - [Emitting Markdown](#emitting-markdown)
105
102
  - [Deferred Evaluation](#deferred-evaluation)
106
- - [API Reference](#api-reference)
103
+ - [Cached Rendering](#cached-rendering)
104
+
105
+ A typical example for a dashboard-type app markup can be found here:
106
+ https://github.com/digital-fabric/p2/blob/master/examples/dashboard.rb
107
107
 
108
108
  ## Getting Started
109
109
 
@@ -123,7 +123,7 @@ require 'p2'
123
123
  html.render #=> "<div id="greeter"><p>Hello!</p></div>"
124
124
  ```
125
125
 
126
- ## Markup
126
+ ## Basic Markup
127
127
 
128
128
  Tags are added using unqualified method calls, and can be nested using blocks:
129
129
 
@@ -511,24 +511,6 @@ article_layout.render(
511
511
  )
512
512
  ```
513
513
 
514
- ## Emitting Raw HTML
515
-
516
- Raw HTML can be emitted using `#raw`:
517
-
518
- ```ruby
519
- wrapped = -> { |html| div { raw html } }
520
- wrapped.render("<h1>hi</h1>") #=> "<div><h1>hi</h1></div>"
521
- ```
522
-
523
- ## Emitting a String with HTML Escaping
524
-
525
- To emit a string with proper HTML escaping, without wrapping it in an HTML
526
- element, use `#text`:
527
-
528
- ```ruby
529
- -> { text 'hi&lo' }.render #=> "hi&amp;lo"
530
- ```
531
-
532
514
  ## Emitting Markdown
533
515
 
534
516
  Markdown is rendered using the
@@ -626,18 +608,17 @@ page = default_layout.apply {
626
608
  }
627
609
  ```
628
610
 
629
- ## HTML Utility methods
611
+ ## Cached Rendering
630
612
 
631
- HTML templates include a few HTML-specific methods to facilitate writing modern
632
- HTML:
613
+ P2 provides a simple API for caching the result of a rendering. The cache stores
614
+ renderings of a template respective to the given arguments. To automatically
615
+ retrieve the cached rendered HTML, or generate it for the first time, use
616
+ `Proc#render_cached`:
633
617
 
634
- - `html5 { ... }` - emits an HTML 5 DOCTYPE (`<!DOCTYPE html>`)
635
- - `import_map(root_path, root_url)` - emits an import map including all files
636
- matching `<root_path>/*.js`, based on the given `root_url`
637
- - `js_module(js)` - emits a `<script type="module">` element
638
- - `link_stylesheet(href, **attributes)` - emits a `<link rel="stylesheet" ...>`
639
- element
640
- - `script(js, **attributes)` - emits an inline `<script>` element
641
- - `style(css, **attributes)` - emits an inline `<style>` element
642
- - `versioned_file_href(href, root_path, root_url)` - calculates a versioned href
643
- for the given file
618
+ ```ruby
619
+ template = ->(title) { div { h1 title } }
620
+ template.render_cached('foo') #=> <div><h1>foo</h1></div>
621
+ template.render_cached('foo') #=> <div><h1>foo</h1></div> (from cache)
622
+ template.render_cached('bar') #=> <div><h1>bar</h1></div>
623
+ template.render_cached('bar') #=> <div><h1>bar</h1></div> (from cache)
624
+ ```
@@ -58,7 +58,7 @@ module P2
58
58
  def match_extension(node)
59
59
  return if node.receiver
60
60
  return if !P2::Extensions[node.name]
61
-
61
+
62
62
  ExtensionTagNode.new(node, self)
63
63
  end
64
64
 
data/lib/p2/compiler.rb CHANGED
@@ -14,15 +14,16 @@ module P2
14
14
  # generated optimized source code.
15
15
  #
16
16
  # @param proc [Proc] template
17
+ # @param mode [Symbol] compilation mode (:html, :xml)
17
18
  # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
18
19
  # @return [Array] array containing the source map and generated code
19
- def self.compile_to_code(proc, wrap: true)
20
+ def self.compile_to_code(proc, mode: :html, wrap: true)
20
21
  ast = Sirop.to_ast(proc)
21
22
 
22
23
  # adjust ast root if proc is defined with proc {} / lambda {} syntax
23
24
  ast = ast.block if ast.is_a?(Prism::CallNode)
24
25
 
25
- compiler = new.with_source_map(proc, ast)
26
+ compiler = new(mode:).with_source_map(proc, ast)
26
27
  transformed_ast = TagTranslator.transform(ast.body, ast)
27
28
  compiler.format_compiled_template(transformed_ast, ast, wrap:, binding: proc.binding)
28
29
  [compiler.source_map, compiler.buffer]
@@ -37,10 +38,11 @@ module P2
37
38
  # compiled.render #=> '<h1>Hello, world!'
38
39
  #
39
40
  # @param proc [Proc] template
41
+ # @param mode [Symbol] compilation mode (:html, :xml)
40
42
  # @param wrap [bool] whether to wrap the generated code with a literal Proc definition
41
43
  # @return [Proc] compiled proc
42
- def self.compile(proc, wrap: true)
43
- source_map, code = compile_to_code(proc, wrap:)
44
+ def self.compile(proc, mode: :html, wrap: true)
45
+ source_map, code = compile_to_code(proc, mode:, wrap:)
44
46
  if ENV['DEBUG'] == '1'
45
47
  puts '*' * 40
46
48
  puts code
@@ -66,8 +68,9 @@ module P2
66
68
  attr_reader :source_map
67
69
 
68
70
  # Initializes a compiler.
69
- def initialize(**)
71
+ def initialize(mode:, **)
70
72
  super(**)
73
+ @mode = mode
71
74
  @pending_html_parts = []
72
75
  end
73
76
 
@@ -168,9 +171,7 @@ module P2
168
171
  if is_static_node?(node.inner_text)
169
172
  emit_html(node.location, ERB::Escape.html_escape(format_literal(node.inner_text)))
170
173
  else
171
- to_s = is_string_type_node?(node.inner_text) ? '' : '.to_s'
172
-
173
- emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)})#{to_s})"))
174
+ emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)}))"))
174
175
  end
175
176
  end
176
177
  emit_html(node.location, format_html_tag_close(tag))
@@ -230,7 +231,7 @@ module P2
230
231
  if is_static_node?(first_arg)
231
232
  emit_html(node.location, ERB::Escape.html_escape(format_literal(first_arg)))
232
233
  else
233
- emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)}.to_s)"))
234
+ emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)})"))
234
235
  end
235
236
  else
236
237
  raise "Don't know how to compile #{node}"
@@ -338,7 +339,7 @@ module P2
338
339
  block_params << format_code(params.keyword_rest) if params.keyword_rest
339
340
  end
340
341
  block_params = block_params.empty? ? '' : ", #{block_params.join(', ')}"
341
-
342
+
342
343
  emit(", &(proc { |__buffer__#{block_params}| #{block_body} }).compiled!")
343
344
  end
344
345
  emit(")")
@@ -423,11 +424,11 @@ module P2
423
424
  # @param node [Prism::Node] AST
424
425
  # @return [String] generated source code
425
426
  def format_code(node)
426
- Compiler.new(minimize_whitespace: true).to_source(node)
427
+ Compiler.new(mode: @mode, minimize_whitespace: true).to_source(node)
427
428
  end
428
429
 
429
430
  def format_inline_block(node)
430
- Compiler.new(minimize_whitespace: true).format_compiled_template(node, node, wrap: false, binding: @binding)
431
+ Compiler.new(mode: @mode, minimize_whitespace: true).format_compiled_template(node, node, wrap: false, binding: @binding)
431
432
  end
432
433
 
433
434
  # Formats a comma separated list of AST nodes. Used for formatting partial
@@ -436,7 +437,7 @@ module P2
436
437
  # @param list [Array<Prism::Node>] node list
437
438
  # @return [String] generated source code
438
439
  def format_code_comma_separated_nodes(list)
439
- compiler = self.class.new(minimize_whitespace: true)
440
+ compiler = Compiler.new(mode: @mode, minimize_whitespace: true)
440
441
  compiler.visit_comma_separated_nodes(list)
441
442
  compiler.buffer
442
443
  end
@@ -448,6 +449,8 @@ module P2
448
449
  # @param tag [String, Symbol] HTML tag
449
450
  # @return [bool] void or not
450
451
  def is_void_element?(tag)
452
+ return false if @mode == :xml
453
+
451
454
  VOID_TAGS.include?(tag.to_s)
452
455
  end
453
456
 
@@ -497,7 +500,9 @@ module P2
497
500
  def format_literal(node)
498
501
  case node
499
502
  when Prism::SymbolNode, Prism::StringNode
500
- node.unescaped
503
+ # since the value is copied verbatim into a quoted string, we need to
504
+ # add a backslash before any double quote.
505
+ node.unescaped.gsub('"', '\"')
501
506
  when Prism::IntegerNode, Prism::FloatNode
502
507
  node.value.to_s
503
508
  when Prism::InterpolatedStringNode
@@ -597,14 +602,16 @@ module P2
597
602
  return if @pending_html_parts.empty?
598
603
 
599
604
  adjust_whitespace(@html_loc_start, advance_to_end: false)
605
+ emit('; __buffer__')
600
606
  concatenated = +''
601
607
 
602
608
  last_loc = @html_loc_start
603
609
  @pending_html_parts.each do |(loc, part)|
604
610
  if (m = part.match(/^#\{(.+)\}$/m))
611
+ # interpolated part
605
612
  emit_html_buffer_push(concatenated, quotes: true) if !concatenated.empty?
606
- adjust_whitespace(loc, advance_to_end: false)
607
- emit_html_buffer_push(m[1])
613
+ # adjust_whitespace(loc, advance_to_end: false)
614
+ emit_html_buffer_push(m[1], loc:)
608
615
  else
609
616
  concatenated << part
610
617
  end
@@ -628,11 +635,18 @@ module P2
628
635
  # @param part [String] HTML part
629
636
  # @param quotes [bool] whether to wrap emitted HTML in double quotes
630
637
  # @return [void]
631
- def emit_html_buffer_push(part, quotes: false)
638
+ def emit_html_buffer_push(part, quotes: false, loc: nil)
632
639
  return if part.empty?
633
640
 
634
641
  q = quotes ? '"' : ''
635
- emit("; __buffer__ << #{q}#{part}#{q}")
642
+ if loc
643
+ emit(".<<(")
644
+ adjust_whitespace(loc, advance_to_end: false)
645
+ emit("#{q}#{part}#{q}")
646
+ emit(")")
647
+ else
648
+ emit(".<<(#{q}#{part}#{q})")
649
+ end
636
650
  part.clear
637
651
  end
638
652
 
data/lib/p2/proc_ext.rb CHANGED
@@ -2,34 +2,40 @@
2
2
 
3
3
  require_relative './compiler'
4
4
 
5
- # Extensions to the Proc class
5
+ # Extensions to the Proc class.
6
6
  class ::Proc
7
- # Returns the compiled form code for the proc
7
+ # Returns the compiled form code for the proc.
8
8
  #
9
9
  # @return [String] compiled proc code
10
10
  def compiled_code
11
11
  P2::Compiler.compile_to_code(self).last
12
12
  end
13
13
 
14
+ # Returns the source map for the compiled proc.
15
+ #
16
+ # @return [Array<String>] source map
14
17
  def source_map
15
18
  loc = source_location
16
19
  fn = compiled? ? loc.first : P2::Compiler.source_location_to_fn(loc)
17
20
  P2::Compiler.source_map_store[fn]
18
21
  end
19
22
 
23
+ # Returns the AST for the proc.
24
+ #
25
+ # @return [Prism::Node] AST root
20
26
  def ast
21
27
  Sirop.to_ast(self)
22
28
  end
23
29
 
24
- # Returns true if proc is marked as compiled
30
+ # Returns true if proc is marked as compiled.
25
31
  #
26
32
  # @return [bool] is the proc marked as compiled
27
33
  def compiled?
28
34
  @is_compiled
29
35
  end
30
36
 
31
- # marks the proc as compiled, i.e. can render directly and takes a string
32
- # buffer as first argument
37
+ # Marks the proc as compiled, i.e. can render directly and takes a string
38
+ # buffer as first argument.
33
39
  #
34
40
  # @return [self]
35
41
  def compiled!
@@ -40,34 +46,43 @@ class ::Proc
40
46
  # Returns the compiled proc for the given proc. If marked as compiled, returns
41
47
  # self.
42
48
  #
49
+ # @param mode [Symbol] compilation mode (:html, :xml)
43
50
  # @return [Proc] compiled proc or self
44
- def compiled_proc
45
- @compiled_proc ||= @is_compiled ? self : compile
51
+ def compiled_proc(mode: :html)
52
+ @compiled_proc ||= @is_compiled ? self : compile(mode:)
46
53
  end
47
54
 
48
- # Compiles the proc into the compiled form
55
+ # Compiles the proc into the compiled form.
49
56
  #
57
+ # @param mode [Symbol] compilation mode (:html, :xml)
50
58
  # @return [Proc] compiled proc
51
- def compile
52
- P2::Compiler.compile(self).compiled!
53
- rescue Sirop::Error => e
54
- puts '!' * 40
55
- p self
56
- p e
59
+ def compile(mode: :html)
60
+ P2::Compiler.compile(self, mode:).compiled!
61
+ rescue Sirop::Error
57
62
  raise P2::Error, "Dynamically defined procs cannot be compiled"
58
63
  end
59
64
 
60
- # Renders the proc to HTML with the given arguments
65
+ # Renders the proc to HTML with the given arguments.
61
66
  #
62
67
  # @return [String] HTML string
63
68
  def render(*a, **b, &c)
64
69
  compiled_proc.(+'', *a, **b, &c)
65
70
  rescue Exception => e
66
- raise P2.translate_backtrace(e)
71
+ e.is_a?(P2::Error) ? raise : raise(P2.translate_backtrace(e))
67
72
  end
68
73
 
69
- # Renders the proc into the given buffer
74
+ # Renders the proc to XML with the given arguments.
70
75
  #
76
+ # @return [String] XML string
77
+ def render_xml(*a, **b, &c)
78
+ compiled_proc(mode: :xml).(+'', *a, **b, &c)
79
+ rescue Exception => e
80
+ e.is_a?(P2::Error) ? raise : raise(P2.translate_backtrace(e))
81
+ end
82
+
83
+ # Renders the proc to HTML with the given arguments into the given buffer.
84
+ #
85
+ # @param buf [String] buffer
71
86
  # @return [String] HTML string
72
87
  def render_to_buffer(buf, *a, **b, &c)
73
88
  compiled_proc.(buf, *a, **b, &c)
@@ -75,7 +90,7 @@ class ::Proc
75
90
  raise P2.translate_backtrace(e)
76
91
  end
77
92
 
78
- # Returns a proc that applies the given arguments to the original proc
93
+ # Returns a proc that applies the given arguments to the original proc.
79
94
  #
80
95
  # @return [Proc] applied proc
81
96
  def apply(*a, **b, &c)
@@ -91,10 +106,13 @@ class ::Proc
91
106
  }.compiled!
92
107
  end
93
108
 
94
- # Caches and returns
95
- def render_cached(*args, **kargs)
109
+ # Caches and returns the rendered HTML for the template with the given
110
+ # arguments.
111
+ #
112
+ # @return [String] HTML string
113
+ def render_cached(*args, **kargs, &block)
96
114
  @render_cache ||= {}
97
- key = args.empty? && kargs.empty? ? nil : [args, kargs]
98
- @render_cache[key] ||= render(*args, **kargs)
115
+ key = args.empty? && kargs.empty? && !block ? nil : [args, kargs, block&.source_location]
116
+ @render_cache[key] ||= render(*args, **kargs, &block)
99
117
  end
100
118
  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.8'
4
+ VERSION = '2.10'
5
5
  end
data/lib/p2.rb CHANGED
@@ -16,7 +16,7 @@ module P2
16
16
  Extensions = {}
17
17
 
18
18
  # Registers extensions to the P2 syntax.
19
- #
19
+ #
20
20
  # @param spec [Hash] hash mapping symbols to procs
21
21
  # @return [self]
22
22
  def extension(spec)
@@ -101,10 +101,11 @@ module P2
101
101
  # @param opts [Hash] Kramdown option overrides
102
102
  # @return [String] HTML
103
103
  def markdown(markdown, **opts)
104
- # require relevant deps on use
105
- require 'kramdown'
106
- require 'rouge'
107
- require 'kramdown-parser-gfm'
104
+ @markdown_deps_loaded ||= true.tap do
105
+ require 'kramdown'
106
+ require 'rouge'
107
+ require 'kramdown-parser-gfm'
108
+ end
108
109
 
109
110
  opts = default_kramdown_options.merge(opts)
110
111
  Kramdown::Document.new(markdown, **opts).to_html
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.8'
4
+ version: '2.10'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -135,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  - !ruby/object:Gem::Version
136
136
  version: '0'
137
137
  requirements: []
138
- rubygems_version: 3.6.9
138
+ rubygems_version: 3.7.0.dev
139
139
  specification_version: 4
140
140
  summary: 'P2: component-based HTML templating for Ruby'
141
141
  test_files: []