p2 2.0
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 +7 -0
- data/CHANGELOG.md +7 -0
- data/README.md +523 -0
- data/lib/p2/compiler.rb +547 -0
- data/lib/p2/proc_ext.rb +86 -0
- data/lib/p2/version.rb +5 -0
- data/lib/p2.rb +101 -0
- data/p2.png +0 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 37c651c3157074dde83015d5e4e6db8a44ef90b12a8dd890a585539e3eb80b4d
|
4
|
+
data.tar.gz: 384d6a649bc089c24d578185f75a7e5a09b22825d70f1367e55b0e8a7f0e3cd8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6565857078969d64aced3c864d295ca9d4627b08bff842eda2578d8759022ac78c3553b3feef99550efee9498a5f0a943ffae2ca12f5dd001162165101bad557
|
7
|
+
data.tar.gz: d0b758535f27c4ec60dac2649d79504c01fcc3e021e318259ea475215cd3d62efec1e360a9529c2bdc6bc1068ed31c44d6339fd44e99aae1bcb4bd8eb97983e8
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,523 @@
|
|
1
|
+
<h1 align="center">
|
2
|
+
<img src="p2.png">
|
3
|
+
<br>
|
4
|
+
P2
|
5
|
+
</h1>
|
6
|
+
|
7
|
+
<h4 align="center">Composable templating for Ruby</h4>
|
8
|
+
|
9
|
+
<p align="center">
|
10
|
+
<a href="http://rubygems.org/gems/p2">
|
11
|
+
<img src="https://badge.fury.io/rb/p2.svg" alt="Ruby gem">
|
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">
|
15
|
+
</a>
|
16
|
+
<a href="https://github.com/digital-fabric/p2/blob/master/LICENSE">
|
17
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
|
18
|
+
</a>
|
19
|
+
</p>
|
20
|
+
|
21
|
+
<p align="center">
|
22
|
+
<a href="https://www.rubydoc.info/gems/p2">API reference</a>
|
23
|
+
</p>
|
24
|
+
|
25
|
+
## What is P2?
|
26
|
+
|
27
|
+
P2 is a templating engine for dynamically producing [HTML](#html-templates). P2
|
28
|
+
templates are expressed as Ruby procs, leading to easier debugging, better
|
29
|
+
protection against HTML/XML injection attacks, and better code reuse.
|
30
|
+
|
31
|
+
P2 templates can be composed in a variety of ways, facilitating the usage of
|
32
|
+
layout templates, and enabling a component-oriented approach to building complex
|
33
|
+
web interfaces.
|
34
|
+
|
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/)).
|
41
|
+
|
42
|
+
P2 automatically escapes all text emitted in templates according to the template
|
43
|
+
type. For more information see the section on [escaping
|
44
|
+
content](#escaping-content).
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'p2'
|
48
|
+
|
49
|
+
page = ->(**props) {
|
50
|
+
html {
|
51
|
+
head { title 'My Title' }
|
52
|
+
body { yield **props }
|
53
|
+
}
|
54
|
+
}
|
55
|
+
page.render {
|
56
|
+
p 'foo'
|
57
|
+
}
|
58
|
+
#=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
|
59
|
+
|
60
|
+
hello_page = page.apply ->(name:, **) {
|
61
|
+
h1 "Hello, #{name}!"
|
62
|
+
}
|
63
|
+
hello.render(name: 'world')
|
64
|
+
#=> "<html><head><title>Title</title></head><body><h1>Hello, world!</h1></body></html>"
|
65
|
+
```
|
66
|
+
|
67
|
+
## Table of Content
|
68
|
+
|
69
|
+
- [Installing P2](#installing-p2)
|
70
|
+
- [Basic Usage](#basic-usage)
|
71
|
+
- [Adding Tags](#adding-tags)
|
72
|
+
- [Tag and Attribute Formatting](#tag-and-attribute-formatting)
|
73
|
+
- [Escaping Content](#escaping-content)
|
74
|
+
- [Template Parameters](#template-parameters)
|
75
|
+
- [Template Logic](#template-logic)
|
76
|
+
- [Template Blocks](#template-blocks)
|
77
|
+
- [Template Composition](#template-composition)
|
78
|
+
- [Parameter and Block Application](#parameter-and-block-application)
|
79
|
+
- [Higher-Order Templates](#higher-order-templates)
|
80
|
+
- [Layout Template Composition](#layout-template-composition)
|
81
|
+
- [Emitting Raw HTML](#emitting-raw-html)
|
82
|
+
- [Emitting a String with HTML Encoding](#emitting-a-string-with-html-encoding)
|
83
|
+
- [Emitting Markdown](#emitting-markdown)
|
84
|
+
- [Deferred Evaluation](#deferred-evaluation)
|
85
|
+
- [API Reference](#api-reference)
|
86
|
+
|
87
|
+
## Installing P2
|
88
|
+
|
89
|
+
**Note**: P2 requires Ruby version 3.4 or newer.
|
90
|
+
|
91
|
+
Using bundler:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
gem 'p2'
|
95
|
+
```
|
96
|
+
|
97
|
+
Or manually:
|
98
|
+
|
99
|
+
```bash
|
100
|
+
$ gem install p2
|
101
|
+
```
|
102
|
+
|
103
|
+
## Basic Usage
|
104
|
+
|
105
|
+
In P2, an HTML template is expressed as a proc:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
html = -> {
|
109
|
+
div(id: 'greeter') { p 'Hello!' }
|
110
|
+
}
|
111
|
+
```
|
112
|
+
|
113
|
+
Rendering a template is done using `#render`:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
require 'p2'
|
117
|
+
|
118
|
+
html.render #=> "<div id="greeter"><p>Hello!</p></div>"
|
119
|
+
```
|
120
|
+
|
121
|
+
## Adding Tags
|
122
|
+
|
123
|
+
Tags are added using unqualified method calls, and can be nested using blocks:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
-> {
|
127
|
+
html {
|
128
|
+
head {
|
129
|
+
title 'page title'
|
130
|
+
}
|
131
|
+
body {
|
132
|
+
article {
|
133
|
+
h1 'article title'
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
```
|
139
|
+
|
140
|
+
Tag methods accept a string argument, a block, or no argument at all:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
-> { p 'hello' }.render #=> "<p>hello</p>"
|
144
|
+
|
145
|
+
-> { p { span '1'; span '2' } }.render #=> "<p><span>1</span><span>2</span></p>"
|
146
|
+
|
147
|
+
-> { hr() }.render #=> "<hr/>"
|
148
|
+
```
|
149
|
+
|
150
|
+
Tag methods also accept tag attributes, given as a hash:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
-> { img src: '/my.gif' }.render #=> "<img src=\"/my.gif\"/>"
|
154
|
+
|
155
|
+
-> { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
|
156
|
+
```
|
157
|
+
|
158
|
+
A `true` attribute value will emit a valueless attribute. A `nil` or `false`
|
159
|
+
attribute value will emit nothing:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
-> { button disabled: nil }.render #=> "<button></button>"
|
163
|
+
-> { button disabled: true }.render #=> "<button disabled></button>"
|
164
|
+
```
|
165
|
+
|
166
|
+
An attribute value given as an array will be joined by space characters:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
-> { div class: [:foo, :bar] }.render #=> "<div class=\"foo bar\"></div>"
|
170
|
+
```
|
171
|
+
|
172
|
+
## Tag and Attribute Formatting
|
173
|
+
|
174
|
+
P2 does not make any assumption about what tags and attributes you can use. You
|
175
|
+
can mix upper and lower case letters, and you can include arbitrary characters
|
176
|
+
in tag and attribute names. However, in order to best adhere to the HTML specs
|
177
|
+
and common practices, tag names and attributes will be formatted according to
|
178
|
+
the following rules, depending on the template type:
|
179
|
+
|
180
|
+
- HTML: underscores are converted to dashes:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
-> {
|
184
|
+
foo_bar { p 'Hello', data_name: 'world' }
|
185
|
+
}.render #=> '<foo-bar><p data-name="world">Hello</p></foo-bar>'
|
186
|
+
```
|
187
|
+
|
188
|
+
If you need more precise control over tag names, you can use the `#tag` method,
|
189
|
+
which takes the tag name as its first parameter, then the rest of the parameters
|
190
|
+
normally used for tags:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
-> {
|
194
|
+
tag 'cra_zy__:!tag', 'foo'
|
195
|
+
}.render #=> '<cra_zy__:!tag>foo</cra_zy__:!tag>'
|
196
|
+
```
|
197
|
+
|
198
|
+
## Escaping Content
|
199
|
+
|
200
|
+
P2 automatically escapes all text content emitted in a template. The specific
|
201
|
+
escaping algorithm depends on the template type. For HTML templates, P2 uses
|
202
|
+
[escape_utils](https://github.com/brianmario/escape_utils), specifically:
|
203
|
+
|
204
|
+
- HTML: `escape_utils.escape_html`
|
205
|
+
|
206
|
+
In order to emit raw HTML, you can use the `#emit` method as [described
|
207
|
+
below](#emitting-raw-html).
|
208
|
+
|
209
|
+
## Template Parameters
|
210
|
+
|
211
|
+
In P2, parameters are always passed explicitly. This means that template
|
212
|
+
parameters are specified as block parameters, and are passed to the template on
|
213
|
+
rendering:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
greeting = -> { |name| h1 "Hello, #{name}!" }
|
217
|
+
greeting.render('world') #=> "<h1>Hello, world!</h1>"
|
218
|
+
```
|
219
|
+
|
220
|
+
Templates can also accept named parameters:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
greeting = -> { |name:| h1 "Hello, #{name}!" }
|
224
|
+
greeting.render(name: 'world') #=> "<h1>Hello, world!</h1>"
|
225
|
+
```
|
226
|
+
|
227
|
+
## Template Logic
|
228
|
+
|
229
|
+
Since P2 templates are just a bunch of Ruby, you can easily embed your view
|
230
|
+
logic right in the template:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
-> { |user = nil|
|
234
|
+
if user
|
235
|
+
span "Hello, #{user.name}!"
|
236
|
+
else
|
237
|
+
span "Hello, guest!"
|
238
|
+
end
|
239
|
+
}
|
240
|
+
```
|
241
|
+
|
242
|
+
## Template Blocks
|
243
|
+
|
244
|
+
Templates can also accept and render blocks by using `emit_yield`:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
page = -> {
|
248
|
+
html {
|
249
|
+
body { emit_yield }
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
253
|
+
# we pass the inner HTML
|
254
|
+
page.render { h1 'hi' }
|
255
|
+
```
|
256
|
+
|
257
|
+
## Template Composition
|
258
|
+
|
259
|
+
P2 makes it easy to compose multiple templates into a whole HTML document. A P2
|
260
|
+
template can contain other templates, as the following example shows.
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
Title = ->(title) { h1 title }
|
264
|
+
|
265
|
+
Item = ->(id:, text:, checked:) {
|
266
|
+
li {
|
267
|
+
input name: id, type: 'checkbox', checked: checked
|
268
|
+
label text, for: id
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
ItemList = ->(items) {
|
273
|
+
ul {
|
274
|
+
items.each { |i|
|
275
|
+
Item(**i)
|
276
|
+
}
|
277
|
+
}
|
278
|
+
}
|
279
|
+
|
280
|
+
page = -> { |title, items|
|
281
|
+
html5 {
|
282
|
+
head { Title(title) }
|
283
|
+
body { ItemList(items) }
|
284
|
+
}
|
285
|
+
}
|
286
|
+
|
287
|
+
page.render('Hello from composed templates', [
|
288
|
+
{ id: 1, text: 'foo', checked: false },
|
289
|
+
{ id: 2, text: 'bar', checked: true }
|
290
|
+
])
|
291
|
+
```
|
292
|
+
|
293
|
+
In addition to using templates defined as constants, you can also use
|
294
|
+
non-constant templates by invoking the `#emit` method:
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
greeting = -> { span "Hello, world" }
|
298
|
+
|
299
|
+
-> {
|
300
|
+
div {
|
301
|
+
emit greeting
|
302
|
+
}
|
303
|
+
}
|
304
|
+
```
|
305
|
+
|
306
|
+
## Parameter and Block Application
|
307
|
+
|
308
|
+
Parameters and blocks can be applied to a template without it being rendered, by
|
309
|
+
using `#apply`. This mechanism is what allows template composition and the
|
310
|
+
creation of higher-order templates.
|
311
|
+
|
312
|
+
The `#apply` method returns a new template which applies the given parameters
|
313
|
+
and or block to the original template:
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
# parameter application
|
317
|
+
hello = -> { |name| h1 "Hello, #{name}!" }
|
318
|
+
hello_world = hello.apply('world')
|
319
|
+
hello_world.render #=> "<h1>Hello, world!</h1>"
|
320
|
+
|
321
|
+
# block application
|
322
|
+
div_wrap = -> { div { emit_yield } }
|
323
|
+
wrapped_h1 = div_wrap.apply { h1 'hi' }
|
324
|
+
wrapped_h1.render #=> "<div><h1>hi</h1></div>"
|
325
|
+
|
326
|
+
# wrap a template
|
327
|
+
wrapped_hello_world = div_wrap.apply(&hello_world)
|
328
|
+
wrapped_hello_world.render #=> "<div><h1>Hello, world!</h1></div>"
|
329
|
+
```
|
330
|
+
|
331
|
+
## Higher-Order Templates
|
332
|
+
|
333
|
+
P2 also lets you create higher-order templates, that is, templates that take
|
334
|
+
other templates as parameters, or as blocks. Higher-order templates are handy
|
335
|
+
for creating layouts, wrapping templates in arbitrary markup, enhancing
|
336
|
+
templates or injecting template parameters.
|
337
|
+
|
338
|
+
Here is a higher-order template that takes a template as parameter:
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
div_wrap = -> { |inner| div { emit inner } }
|
342
|
+
greeter = -> { h1 'hi' }
|
343
|
+
wrapped_greeter = div_wrap.apply(greeter)
|
344
|
+
wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
|
345
|
+
```
|
346
|
+
|
347
|
+
The inner template can also be passed as a block, as shown above:
|
348
|
+
|
349
|
+
```ruby
|
350
|
+
div_wrap = -> { div { emit_yield } }
|
351
|
+
wrapped_greeter = div_wrap.apply { h1 'hi' }
|
352
|
+
wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
|
353
|
+
```
|
354
|
+
|
355
|
+
## Layout Template Composition
|
356
|
+
|
357
|
+
One of the principal uses of higher-order templates is the creation of nested
|
358
|
+
layouts. Suppose we have a website with a number of different layouts, and we'd
|
359
|
+
like to avoid having to repeat the same code in the different layouts. We can do
|
360
|
+
this by creating a `default` page template that takes a block, then use `#apply`
|
361
|
+
to create the other templates:
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
default_layout = -> { |**params|
|
365
|
+
html5 {
|
366
|
+
head {
|
367
|
+
title: params[:title]
|
368
|
+
}
|
369
|
+
body {
|
370
|
+
emit_yield(**params)
|
371
|
+
}
|
372
|
+
}
|
373
|
+
}
|
374
|
+
|
375
|
+
article_layout = default_layout.apply { |title:, body:|
|
376
|
+
article {
|
377
|
+
h1 title
|
378
|
+
emit_markdown body
|
379
|
+
}
|
380
|
+
}
|
381
|
+
|
382
|
+
article_layout.render(
|
383
|
+
title: 'This is a title',
|
384
|
+
body: 'Hello from *markdown body*'
|
385
|
+
)
|
386
|
+
```
|
387
|
+
|
388
|
+
## Emitting Raw HTML
|
389
|
+
|
390
|
+
Raw HTML can be emitted using `#emit`:
|
391
|
+
|
392
|
+
```ruby
|
393
|
+
wrapped = -> { |html| div { emit html } }
|
394
|
+
wrapped.render("<h1>hi</h1>") #=> "<div><h1>hi</h1></div>"
|
395
|
+
```
|
396
|
+
|
397
|
+
## Emitting a String with HTML Encoding
|
398
|
+
|
399
|
+
To emit a string with proper HTML encoding, without wrapping it in an HTML
|
400
|
+
element, use `#text`:
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
-> { text 'hi&lo' }.render #=> "hi&lo"
|
404
|
+
```
|
405
|
+
|
406
|
+
## Emitting Markdown
|
407
|
+
|
408
|
+
Markdown is rendered using the
|
409
|
+
[Kramdown](https://kramdown.gettalong.org/index.html) gem. To emit Markdown, use
|
410
|
+
`#emit_markdown`:
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
template = -> { |md| div { emit_markdown md } }
|
414
|
+
template.render("Here's some *Markdown*") #=> "<div><p>Here's some <em>Markdown</em><p>\n</div>"
|
415
|
+
```
|
416
|
+
|
417
|
+
[Kramdown
|
418
|
+
options](https://kramdown.gettalong.org/options.html#available-options) can be
|
419
|
+
specified by adding them to the `#emit_markdown` call:
|
420
|
+
|
421
|
+
```ruby
|
422
|
+
template = -> { |md| div { emit_markdown md, auto_ids: false } }
|
423
|
+
template.render("# title") #=> "<div><h1>title</h1></div>"
|
424
|
+
```
|
425
|
+
|
426
|
+
You can also use `P2.markdown` directly:
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
P2.markdown('# title') #=> "<h1>title</h1>"
|
430
|
+
```
|
431
|
+
|
432
|
+
The default Kramdown options are:
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
{
|
436
|
+
entity_output: :numeric,
|
437
|
+
syntax_highlighter: :rouge,
|
438
|
+
input: 'GFM',
|
439
|
+
hard_wrap: false
|
440
|
+
}
|
441
|
+
```
|
442
|
+
|
443
|
+
The deafult options can be configured by accessing
|
444
|
+
`P2.default_kramdown_options`, e.g.:
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
P2.default_kramdown_options[:auto_ids] = false
|
448
|
+
```
|
449
|
+
|
450
|
+
## Deferred Evaluation
|
451
|
+
|
452
|
+
Deferred evaluation allows deferring the rendering of parts of a template until
|
453
|
+
the last moment, thus allowing an inner template to manipulate the state of the
|
454
|
+
outer template. To in order to defer a part of a template, use `#defer`, and
|
455
|
+
include any markup in the provided block. This technique, in in conjunction with
|
456
|
+
holding state in instance variables, is an alternative to passing parameters,
|
457
|
+
which can be limiting in some situations.
|
458
|
+
|
459
|
+
A few use cases for deferred evaulation come to mind:
|
460
|
+
|
461
|
+
- Setting the page title.
|
462
|
+
- Adding a flash message to a page.
|
463
|
+
- Using templates that dynamically add static dependencies (JS and CSS) to the
|
464
|
+
page.
|
465
|
+
|
466
|
+
The last use case is particularly interesting. Imagine a `DependencyMananger`
|
467
|
+
class that can collect JS and CSS dependencies from the different templates
|
468
|
+
integrated into the page, and adds them to the page's `<head>` element:
|
469
|
+
|
470
|
+
```ruby
|
471
|
+
default_layout = -> { |**args|
|
472
|
+
@dependencies = DependencyMananger.new
|
473
|
+
head {
|
474
|
+
defer { emit @dependencies.head_markup }
|
475
|
+
}
|
476
|
+
body { emit_yield **args }
|
477
|
+
}
|
478
|
+
|
479
|
+
button = proc { |text, onclick|
|
480
|
+
@dependencies.js '/static/js/button.js'
|
481
|
+
@dependencies.css '/static/css/button.css'
|
482
|
+
|
483
|
+
button text, onclick: onclick
|
484
|
+
}
|
485
|
+
|
486
|
+
heading = proc { |text|
|
487
|
+
@dependencies.js '/static/js/heading.js'
|
488
|
+
@dependencies.css '/static/css/heading.css'
|
489
|
+
|
490
|
+
h1 text
|
491
|
+
}
|
492
|
+
|
493
|
+
page = default_layout.apply {
|
494
|
+
emit heading, "What's your favorite cheese?"
|
495
|
+
|
496
|
+
emit button, 'Beaufort', 'eat_beaufort()'
|
497
|
+
emit button, 'Mont d''or', 'eat_montdor()'
|
498
|
+
emit button, 'Époisses', 'eat_epoisses()'
|
499
|
+
}
|
500
|
+
```
|
501
|
+
|
502
|
+
## HTML Utility methods
|
503
|
+
|
504
|
+
HTML templates include a few HTML-specific methods to facilitate writing modern
|
505
|
+
HTML:
|
506
|
+
|
507
|
+
- `html5 { ... }` - emits an HTML 5 DOCTYPE (`<!DOCTYPE html>`)
|
508
|
+
- `import_map(root_path, root_url)` - emits an import map including all files
|
509
|
+
matching `<root_path>/*.js`, based on the given `root_url`
|
510
|
+
- `js_module(js)` - emits a `<script type="module">` element
|
511
|
+
- `link_stylesheet(href, **attributes)` - emits a `<link rel="stylesheet" ...>`
|
512
|
+
element
|
513
|
+
- `script(js, **attributes)` - emits an inline `<script>` element
|
514
|
+
- `style(css, **attributes)` - emits an inline `<style>` element
|
515
|
+
- `versioned_file_href(href, root_path, root_url)` - calculates a versioned href
|
516
|
+
for the given file
|
517
|
+
|
518
|
+
[HTML docs](https://www.rubydoc.info/gems/p2/P2/HTML)
|
519
|
+
|
520
|
+
## API Reference
|
521
|
+
|
522
|
+
The API reference for this library can be found
|
523
|
+
[here](https://www.rubydoc.info/gems/p2).
|
data/lib/p2/compiler.rb
ADDED
@@ -0,0 +1,547 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'sirop'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
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 EmitNode
|
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_emit_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 DeferNode
|
128
|
+
attr_reader :call_node, :location, :block
|
129
|
+
|
130
|
+
def initialize(call_node, compiler)
|
131
|
+
@call_node = call_node
|
132
|
+
@location = call_node.location
|
133
|
+
@block = call_node.block && compiler.visit(call_node.block)
|
134
|
+
end
|
135
|
+
|
136
|
+
def accept(visitor)
|
137
|
+
visitor.visit_defer_node(self)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class CustomTagNode
|
142
|
+
attr_reader :tag, :call_node, :location, :block
|
143
|
+
|
144
|
+
def initialize(call_node, compiler)
|
145
|
+
@call_node = call_node
|
146
|
+
@tag = call_node.name
|
147
|
+
@location = call_node.location
|
148
|
+
@block = call_node.block && compiler.visit(call_node.block)
|
149
|
+
end
|
150
|
+
|
151
|
+
def accept(visitor)
|
152
|
+
visitor.visit_custom_tag_node(self)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class TagTransformer < Prism::MutationCompiler
|
157
|
+
include Prism::DSL
|
158
|
+
|
159
|
+
def self.transform(ast)
|
160
|
+
ast.accept(new)
|
161
|
+
end
|
162
|
+
|
163
|
+
def visit_call_node(node)
|
164
|
+
# We're only interested in compiling method calls without a receiver
|
165
|
+
return super(node) if node.receiver
|
166
|
+
|
167
|
+
case node.name
|
168
|
+
when :emit_yield
|
169
|
+
yield_node(
|
170
|
+
location: node.location,
|
171
|
+
arguments: node.arguments
|
172
|
+
)
|
173
|
+
when :raise
|
174
|
+
super(node)
|
175
|
+
when :emit, :e
|
176
|
+
EmitNode.new(node, self)
|
177
|
+
when :text
|
178
|
+
TextNode.new(node, self)
|
179
|
+
when :defer
|
180
|
+
DeferNode.new(node, self)
|
181
|
+
when :html5, :emit_markdown, :markdown
|
182
|
+
CustomTagNode.new(node, self)
|
183
|
+
else
|
184
|
+
TagNode.new(node, self)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class VerbatimSourcifier < Sirop::Sourcifier
|
190
|
+
def visit_tag_node(node)
|
191
|
+
visit(node.call_node)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
class TemplateCompiler < Sirop::Sourcifier
|
196
|
+
def self.compile_to_code(proc, wrap: true)
|
197
|
+
ast = Sirop.to_ast(proc)
|
198
|
+
|
199
|
+
# adjust ast root if proc is defined with proc {} / lambda {} syntax
|
200
|
+
ast = ast.block if ast.is_a?(Prism::CallNode)
|
201
|
+
|
202
|
+
compiler = new.with_source_map(proc, ast)
|
203
|
+
transformed_ast = TagTransformer.transform(ast.body)
|
204
|
+
compiler.format_compiled_template(transformed_ast, ast, wrap:, binding: proc.binding)
|
205
|
+
[compiler.source_map, compiler.buffer]
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.compile(proc, wrap: true)
|
209
|
+
source_map, code = compile_to_code(proc, wrap:)
|
210
|
+
eval(code, proc.binding, source_map[:compiled_fn])
|
211
|
+
end
|
212
|
+
|
213
|
+
attr_reader :source_map
|
214
|
+
|
215
|
+
def initialize(**)
|
216
|
+
super(**)
|
217
|
+
@pending_html_parts = []
|
218
|
+
@html_loc_start = nil
|
219
|
+
@html_loc_end = nil
|
220
|
+
@yield_used = nil
|
221
|
+
end
|
222
|
+
|
223
|
+
def with_source_map(orig_proc, orig_ast)
|
224
|
+
ast_digest = Digest::MD5.hexdigest(orig_ast.inspect)
|
225
|
+
@source_map = {
|
226
|
+
source_fn: orig_proc.source_location.first,
|
227
|
+
compiled_fn: "::#{ast_digest}"
|
228
|
+
}
|
229
|
+
@source_map_line_ofs = 1
|
230
|
+
self
|
231
|
+
end
|
232
|
+
|
233
|
+
def format_compiled_template(ast, orig_ast, wrap:, binding:)
|
234
|
+
# generate source code
|
235
|
+
@binding = binding
|
236
|
+
visit(ast)
|
237
|
+
flush_html_parts!(semicolon_prefix: true)
|
238
|
+
|
239
|
+
source_code = @buffer
|
240
|
+
@buffer = +''
|
241
|
+
if wrap
|
242
|
+
emit("(#{@source_map.inspect}).then { |src_map| ->(__buffer__")
|
243
|
+
|
244
|
+
params = orig_ast.parameters
|
245
|
+
params = params&.parameters
|
246
|
+
if params
|
247
|
+
emit(', ')
|
248
|
+
emit(format_code(params))
|
249
|
+
end
|
250
|
+
|
251
|
+
if @yield_used
|
252
|
+
emit(', &__block__')
|
253
|
+
end
|
254
|
+
|
255
|
+
emit(") do\n")
|
256
|
+
end
|
257
|
+
@buffer << source_code
|
258
|
+
emit_postlude
|
259
|
+
if wrap
|
260
|
+
emit('; __buffer__')
|
261
|
+
adjust_whitespace(orig_ast.closing_loc)
|
262
|
+
emit(";") if @buffer !~ /\n\s*$/m
|
263
|
+
emit("rescue Exception => e; P2.translate_backtrace(e, src_map); raise e; end }")
|
264
|
+
end
|
265
|
+
@buffer
|
266
|
+
end
|
267
|
+
|
268
|
+
def emit_code(loc, semicolon: false, chomp: false, flush_html: true)
|
269
|
+
flush_html_parts! if flush_html
|
270
|
+
super(loc, semicolon:, chomp: )
|
271
|
+
end
|
272
|
+
|
273
|
+
def visit_tag_node(node)
|
274
|
+
tag = node.tag
|
275
|
+
if tag.is_a?(Symbol) && tag =~ /^[A-Z]/
|
276
|
+
return visit_const_tag_node(node.call_node)
|
277
|
+
end
|
278
|
+
|
279
|
+
is_void = is_void_element?(tag)
|
280
|
+
emit_html(node.tag_location, format_html_tag_open(tag, node.attributes))
|
281
|
+
return if is_void
|
282
|
+
|
283
|
+
case node.block
|
284
|
+
when Prism::BlockNode
|
285
|
+
visit(node.block.body)
|
286
|
+
when Prism::BlockArgumentNode
|
287
|
+
flush_html_parts!
|
288
|
+
adjust_whitespace(node.block)
|
289
|
+
emit("; #{format_code(node.block.expression)}.render_to_buffer(__buffer__)")
|
290
|
+
end
|
291
|
+
|
292
|
+
if node.inner_text
|
293
|
+
if is_static_node?(node.inner_text)
|
294
|
+
emit_html(node.location, CGI.escape_html(format_literal(node.inner_text)))
|
295
|
+
else
|
296
|
+
convert_to_s = !is_string_type_node?(node.inner_text)
|
297
|
+
if convert_to_s
|
298
|
+
emit_html(node.location, "#\{CGI.escape_html((#{format_code(node.inner_text)}).to_s)}")
|
299
|
+
else
|
300
|
+
emit_html(node.location, "#\{CGI.escape_html(#{format_code(node.inner_text)})}")
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
emit_html(node.location, format_html_tag_close(tag))
|
305
|
+
end
|
306
|
+
|
307
|
+
def visit_const_tag_node(node)
|
308
|
+
flush_html_parts!
|
309
|
+
adjust_whitespace(node.location)
|
310
|
+
if node.receiver
|
311
|
+
emit(node.receiver.location)
|
312
|
+
emit('::')
|
313
|
+
end
|
314
|
+
emit("; #{node.name}.render_to_buffer(__buffer__")
|
315
|
+
if node.arguments
|
316
|
+
emit(', ')
|
317
|
+
visit(node.arguments)
|
318
|
+
end
|
319
|
+
emit(');')
|
320
|
+
end
|
321
|
+
|
322
|
+
def visit_emit_node(node)
|
323
|
+
args = node.call_node.arguments.arguments
|
324
|
+
first_arg = args.first
|
325
|
+
if args.length == 1
|
326
|
+
if is_static_node?(first_arg)
|
327
|
+
emit_html(node.location, format_literal(first_arg))
|
328
|
+
elsif first_arg.is_a?(Prism::LambdaNode)
|
329
|
+
visit(first_arg.body)
|
330
|
+
else
|
331
|
+
emit_html(node.location, "#\{P2.render_emit_call(#{format_code(first_arg)})}")
|
332
|
+
end
|
333
|
+
else
|
334
|
+
block_embed = node.block ? "&(->(__buffer__) #{format_code(node.block)}.compiled!)" : nil
|
335
|
+
block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments
|
336
|
+
emit_html(node.location, "#\{P2.render_emit_call(#{format_code(node.call_node.arguments)}#{block_embed})}")
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def visit_text_node(node)
|
341
|
+
return if !node.call_node.arguments
|
342
|
+
|
343
|
+
args = node.call_node.arguments.arguments
|
344
|
+
first_arg = args.first
|
345
|
+
if args.length == 1
|
346
|
+
if is_static_node?(first_arg)
|
347
|
+
emit_html(node.location, CGI.escape_html(format_literal(first_arg)))
|
348
|
+
else
|
349
|
+
emit_html(node.location, "#\{CGI.escape_html(#{format_code(first_arg)}.to_s)}")
|
350
|
+
end
|
351
|
+
else
|
352
|
+
raise "Don't know how to compile #{node}"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def visit_defer_node(node)
|
357
|
+
block = node.block
|
358
|
+
return if !block
|
359
|
+
|
360
|
+
flush_html_parts!
|
361
|
+
|
362
|
+
if !@defer_mode
|
363
|
+
adjust_whitespace(node.call_node.message_loc)
|
364
|
+
emit("__orig_buffer__ = __buffer__; __parts__ = __buffer__ = []; ")
|
365
|
+
@defer_mode = true
|
366
|
+
end
|
367
|
+
|
368
|
+
adjust_whitespace(block.opening_loc)
|
369
|
+
emit("__buffer__ << ->{")
|
370
|
+
visit(block.body)
|
371
|
+
flush_html_parts!
|
372
|
+
adjust_whitespace(block.closing_loc)
|
373
|
+
emit("}")
|
374
|
+
end
|
375
|
+
|
376
|
+
def visit_custom_tag_node(node)
|
377
|
+
case node.tag
|
378
|
+
when :tag
|
379
|
+
args = node.call_node.arguments&.arguments
|
380
|
+
when :html5
|
381
|
+
emit_html(node.location, '<!DOCTYPE html><html>')
|
382
|
+
visit(node.block.body) if node.block
|
383
|
+
emit_html(node.block.closing_loc, '</html>')
|
384
|
+
when :emit_markdown, :markdown
|
385
|
+
args = node.call_node.arguments
|
386
|
+
return if !args
|
387
|
+
|
388
|
+
emit_html(node.location, "#\{P2.markdown(#{format_code(args)})}")
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def visit_yield_node(node)
|
393
|
+
adjust_whitespace(node.location)
|
394
|
+
flush_html_parts!
|
395
|
+
@yield_used = true
|
396
|
+
emit("; (__block__ ? __block__.render_to_buffer(__buffer__")
|
397
|
+
if node.arguments
|
398
|
+
emit(', ')
|
399
|
+
visit(node.arguments)
|
400
|
+
end
|
401
|
+
emit(") : raise(LocalJumpError, 'no block given (yield)'))")
|
402
|
+
end
|
403
|
+
|
404
|
+
private
|
405
|
+
|
406
|
+
def format_code(node, klass = TemplateCompiler)
|
407
|
+
klass.new(minimize_whitespace: true).to_source(node)
|
408
|
+
end
|
409
|
+
|
410
|
+
VOID_TAGS = %w(area base br col embed hr img input link meta param source track wbr)
|
411
|
+
|
412
|
+
def is_void_element?(tag)
|
413
|
+
VOID_TAGS.include?(tag.to_s)
|
414
|
+
end
|
415
|
+
|
416
|
+
def format_html_tag_open(tag, attributes)
|
417
|
+
tag = convert_tag(tag)
|
418
|
+
if attributes && attributes&.elements.size > 0
|
419
|
+
"<#{tag} #{format_html_attributes(attributes)}>"
|
420
|
+
else
|
421
|
+
"<#{tag}>"
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def format_html_tag_close(tag)
|
426
|
+
tag = convert_tag(tag)
|
427
|
+
"</#{tag}>"
|
428
|
+
end
|
429
|
+
|
430
|
+
def convert_tag(tag)
|
431
|
+
case tag
|
432
|
+
when Prism::SymbolNode, Prism::StringNode
|
433
|
+
P2.format_tag(tag.unescaped)
|
434
|
+
when Prism::Node
|
435
|
+
"#\{P2.format_tag(#{format_code(tag)})}"
|
436
|
+
else
|
437
|
+
P2.format_tag(tag)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def format_literal(node)
|
442
|
+
case node
|
443
|
+
when Prism::SymbolNode, Prism::StringNode
|
444
|
+
node.unescaped
|
445
|
+
when Prism::IntegerNode, Prism::FloatNode
|
446
|
+
node.value.to_s
|
447
|
+
when Prism::InterpolatedStringNode
|
448
|
+
format_code(node)[1..-2]
|
449
|
+
when Prism::TrueNode
|
450
|
+
'true'
|
451
|
+
when Prism::FalseNode
|
452
|
+
'false'
|
453
|
+
when Prism::NilNode
|
454
|
+
''
|
455
|
+
else
|
456
|
+
"#\{#{format_code(node)}}"
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
STATIC_NODE_TYPES = [
|
461
|
+
Prism::FalseNode,
|
462
|
+
Prism::FloatNode,
|
463
|
+
Prism::IntegerNode,
|
464
|
+
Prism::NilNode,
|
465
|
+
Prism::StringNode,
|
466
|
+
Prism::SymbolNode,
|
467
|
+
Prism::TrueNode
|
468
|
+
]
|
469
|
+
|
470
|
+
def is_static_node?(node)
|
471
|
+
STATIC_NODE_TYPES.include?(node.class)
|
472
|
+
end
|
473
|
+
|
474
|
+
STRING_TYPE_NODE_TYPES = [
|
475
|
+
Prism::StringNode,
|
476
|
+
Prism::InterpolatedStringNode
|
477
|
+
]
|
478
|
+
|
479
|
+
def is_string_type_node?(node)
|
480
|
+
STRING_TYPE_NODE_TYPES.include?(node.class)
|
481
|
+
end
|
482
|
+
|
483
|
+
def format_html_attributes(node)
|
484
|
+
elements = node.elements
|
485
|
+
dynamic_attributes = elements.any? do
|
486
|
+
it.is_a?(Prism::AssocSplatNode) ||
|
487
|
+
!is_static_node?(it.key) || !is_static_node?(it.value)
|
488
|
+
end
|
489
|
+
|
490
|
+
return "#\{P2.format_html_attrs(#{format_code(node)})}" if dynamic_attributes
|
491
|
+
|
492
|
+
parts = elements.map do
|
493
|
+
key = it.key
|
494
|
+
value = it.value
|
495
|
+
case value
|
496
|
+
when Prism::TrueNode
|
497
|
+
format_literal(key)
|
498
|
+
when Prism::FalseNode, Prism::NilNode
|
499
|
+
nil
|
500
|
+
else
|
501
|
+
k = format_literal(key)
|
502
|
+
if is_static_node?(value)
|
503
|
+
value = format_literal(value)
|
504
|
+
"#{P2.format_html_attr_key(k)}=\\\"#{value}\\\""
|
505
|
+
else
|
506
|
+
"#{P2.format_html_attr_key(k)}=\\\"#\{#{format_code(value)}}\\\""
|
507
|
+
end
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
parts.compact.join(' ')
|
512
|
+
end
|
513
|
+
|
514
|
+
def emit_html(loc, str)
|
515
|
+
@html_loc_start ||= loc
|
516
|
+
@html_loc_end = loc
|
517
|
+
@pending_html_parts << str
|
518
|
+
end
|
519
|
+
|
520
|
+
def flush_html_parts!(semicolon_prefix: true)
|
521
|
+
return if @pending_html_parts.empty?
|
522
|
+
|
523
|
+
adjust_whitespace(@html_loc_start)
|
524
|
+
if semicolon_prefix && @buffer =~ /[^\s]\s*$/m
|
525
|
+
emit '; '
|
526
|
+
end
|
527
|
+
|
528
|
+
str = @pending_html_parts.join
|
529
|
+
@pending_html_parts.clear
|
530
|
+
|
531
|
+
@last_loc = @html_loc_end
|
532
|
+
@last_loc_start = loc_start(@html_loc_end)
|
533
|
+
@last_loc_end = loc_end(@html_loc_end)
|
534
|
+
|
535
|
+
@html_loc_start = nil
|
536
|
+
@html_loc_end = nil
|
537
|
+
|
538
|
+
emit "__buffer__ << \"#{str}\""
|
539
|
+
end
|
540
|
+
|
541
|
+
def emit_postlude
|
542
|
+
return if !@defer_mode
|
543
|
+
|
544
|
+
emit("; __buffer__ = __orig_buffer__; __parts__.each { it.is_a?(Proc) ? it.() : (__buffer__ << it) }")
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
data/lib/p2/proc_ext.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './compiler'
|
4
|
+
|
5
|
+
# Extensions to the Proc class
|
6
|
+
class ::Proc
|
7
|
+
def compiled_code
|
8
|
+
P2::TemplateCompiler.compile_to_code(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def compiled?
|
12
|
+
@is_compiled
|
13
|
+
end
|
14
|
+
|
15
|
+
def compiled!
|
16
|
+
@is_compiled = true
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def compiled_proc
|
21
|
+
@compiled_proc ||= @is_compiled ? self : compile
|
22
|
+
end
|
23
|
+
|
24
|
+
def compile
|
25
|
+
P2.compile(self).compiled!
|
26
|
+
rescue Sirop::Error
|
27
|
+
uncompiled_renderer
|
28
|
+
end
|
29
|
+
|
30
|
+
def render(*a, **b, &c)
|
31
|
+
compiled_proc.(+'', *a, **b, &c)
|
32
|
+
end
|
33
|
+
|
34
|
+
def render_to_buffer(buf, *a, **b, &c)
|
35
|
+
compiled_proc.(buf, *a, **b, &c)
|
36
|
+
end
|
37
|
+
|
38
|
+
def uncompiled_renderer
|
39
|
+
->(__buffer__, *a, **b, &c) {
|
40
|
+
P2::UncompiledProcWrapper.new(self).call(__buffer__, *a, **b, &c)
|
41
|
+
__buffer__
|
42
|
+
}.compiled!
|
43
|
+
end
|
44
|
+
|
45
|
+
def apply(*a, **b, &c)
|
46
|
+
compiled = compiled_proc
|
47
|
+
c_compiled = c&.compiled_proc
|
48
|
+
|
49
|
+
->(__buffer__, *x, **y, &z) {
|
50
|
+
c_proc = c_compiled && ->(__buffer__, *d, **e) {
|
51
|
+
c_compiled.(__buffer__, *a, *d, **b, **e, &z)
|
52
|
+
}.compiled!
|
53
|
+
|
54
|
+
compiled.(__buffer__, *a, *x, **b, **y, &c_proc)
|
55
|
+
}.compiled!
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module P2
|
60
|
+
class UncompiledProcWrapper
|
61
|
+
def initialize(proc)
|
62
|
+
@proc = proc
|
63
|
+
end
|
64
|
+
|
65
|
+
def call(buffer, *a, **b)
|
66
|
+
@buffer = buffer
|
67
|
+
instance_exec(*a, **b, &@proc)
|
68
|
+
end
|
69
|
+
|
70
|
+
def method_missing(sym, *a, **b, &c)
|
71
|
+
tag(sym, *a, **b, &c)
|
72
|
+
end
|
73
|
+
|
74
|
+
def p(*a, **b, &c)
|
75
|
+
tag(:p, *a, **b, &c)
|
76
|
+
end
|
77
|
+
|
78
|
+
def tag(sym, *a, **b, &block)
|
79
|
+
@buffer << "<#{sym}>"
|
80
|
+
if block
|
81
|
+
instance_eval(&block)
|
82
|
+
end
|
83
|
+
@buffer << "</#{sym}>"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/p2/version.rb
ADDED
data/lib/p2.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'p2/compiler'
|
4
|
+
require_relative 'p2/proc_ext'
|
5
|
+
|
6
|
+
# P2 is a composable templating library
|
7
|
+
module P2
|
8
|
+
# Exception class used to signal templating-related errors
|
9
|
+
class Error < RuntimeError; end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def compile(proc)
|
13
|
+
P2::TemplateCompiler.compile(proc)
|
14
|
+
end
|
15
|
+
|
16
|
+
def format_tag(tag)
|
17
|
+
tag.to_s.gsub('_', '-')
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_html_attr_key(tag)
|
21
|
+
tag.to_s.tr('_', '-')
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_html_attrs(attrs)
|
25
|
+
attrs.each_with_object(+'') do |(k, v), html|
|
26
|
+
case v
|
27
|
+
when nil, false
|
28
|
+
when true
|
29
|
+
html << ' ' if !html.empty?
|
30
|
+
html << format_html_attr_key(k)
|
31
|
+
else
|
32
|
+
html << ' ' if !html.empty?
|
33
|
+
v = v.join(' ') if v.is_a?(Array)
|
34
|
+
html << "#{format_html_attr_key(k)}=\"#{v}\""
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def render_emit_call(o, *a, **b, &block)
|
40
|
+
case o
|
41
|
+
when nil
|
42
|
+
# do nothing
|
43
|
+
when ::Proc
|
44
|
+
o.render(*a, **b, &block)
|
45
|
+
else
|
46
|
+
o.to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def translate_backtrace(e, source_map)
|
51
|
+
re = /^(#{source_map[:compiled_fn]}\:(\d+))/
|
52
|
+
source_fn = source_map[:source_fn]
|
53
|
+
backtrace = e.backtrace.map {
|
54
|
+
if (m = it.match(re))
|
55
|
+
line = m[2].to_i
|
56
|
+
source_line = source_map[line] || "?(#{line})"
|
57
|
+
it.sub(m[1], "#{source_fn}:#{source_line}")
|
58
|
+
else
|
59
|
+
it
|
60
|
+
end
|
61
|
+
}
|
62
|
+
e.set_backtrace(backtrace)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Renders Markdown into HTML. The `opts` argument will be merged with the
|
66
|
+
# default Kramdown options in order to change the rendering behaviour.
|
67
|
+
#
|
68
|
+
# @param markdown [String] Markdown
|
69
|
+
# @param opts [Hash] Kramdown option overrides
|
70
|
+
# @return [String] HTML
|
71
|
+
def markdown(markdown, **opts)
|
72
|
+
# require relevant deps on use
|
73
|
+
require 'kramdown'
|
74
|
+
require 'rouge'
|
75
|
+
require 'kramdown-parser-gfm'
|
76
|
+
|
77
|
+
opts = default_kramdown_options.merge(opts)
|
78
|
+
Kramdown::Document.new(markdown, **opts).to_html
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the default Kramdown options used for rendering Markdown.
|
82
|
+
#
|
83
|
+
# @return [Hash] Kramdown options
|
84
|
+
def default_kramdown_options
|
85
|
+
@default_kramdown_options ||= {
|
86
|
+
entity_output: :numeric,
|
87
|
+
syntax_highlighter: :rouge,
|
88
|
+
input: 'GFM',
|
89
|
+
hard_wrap: false
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Sets the default Kramdown options used for rendering Markdown.
|
94
|
+
#
|
95
|
+
# @param opts [Hash] Kramdown options
|
96
|
+
# @return [void]
|
97
|
+
def default_kramdown_options=(opts)
|
98
|
+
@default_kramdown_options = opts
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/p2.png
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: p2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '2.0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sharon Rosner
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: sirop
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 0.8.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 0.8.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: kramdown
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.5.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 2.5.1
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rouge
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 4.5.1
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 4.5.1
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: kramdown-parser-gfm
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.1.0
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 1.1.0
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: minitest
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 5.25.4
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 5.25.4
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: benchmark-ips
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 2.7.2
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 2.7.2
|
96
|
+
email: sharon@noteflakes.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files:
|
100
|
+
- README.md
|
101
|
+
- p2.png
|
102
|
+
files:
|
103
|
+
- CHANGELOG.md
|
104
|
+
- README.md
|
105
|
+
- lib/p2.rb
|
106
|
+
- lib/p2/compiler.rb
|
107
|
+
- lib/p2/proc_ext.rb
|
108
|
+
- lib/p2/version.rb
|
109
|
+
- p2.png
|
110
|
+
homepage: http://github.com/digital-fabric/p2
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata:
|
114
|
+
source_code_uri: https://github.com/digital-fabric/p2
|
115
|
+
documentation_uri: https://www.rubydoc.info/gems/p2
|
116
|
+
homepage_uri: https://github.com/digital-fabric/p2
|
117
|
+
changelog_uri: https://github.com/digital-fabric/p2/blob/master/CHANGELOG.md
|
118
|
+
rdoc_options:
|
119
|
+
- "--title"
|
120
|
+
- P2
|
121
|
+
- "--main"
|
122
|
+
- README.md
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '3.4'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
requirements: []
|
136
|
+
rubygems_version: 3.6.9
|
137
|
+
specification_version: 4
|
138
|
+
summary: 'P2: component-based HTML templating for Ruby'
|
139
|
+
test_files: []
|