papercraft 2.23 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f77a2ff47a98a713d1449c0ae27d4a250e478e5455257955fc5e7af34a8331b
4
- data.tar.gz: 6893f861f98361f48fdd0b0c2648c687d24149df217e0125da0ccc4795e40c69
3
+ metadata.gz: ac74fb8004af29f1b15e8ca36de8039d3e47c6642105032fa1a51ec46bda100a
4
+ data.tar.gz: 2b5fcf6bc6d472c33033381577b62e0875594a56e3a8f9f1d769b64d4a6ec2ce
5
5
  SHA512:
6
- metadata.gz: b76597b671e62bdb67483c0cd30e4c716084655d0a80cad41c4a77405c97e8300712201f3cf2215584f979b7ae2d3c7e29770868bbda753b9faca88f633101a7
7
- data.tar.gz: 5bbb94d2ec5c13ab5da17c4474d0338d195abd5149dbddb75d1e0fbcb371c1e620c9666169233dad04cc4b8b6265bfc13d9d41c1115fe37a61e30549e0f06ea2
6
+ metadata.gz: 390c81c371d17216134e931b26e1d6c9728c54e2c8db220b0d21761f9ed7d21b12f784b5642e7473e55c1f65f2bad4fe07f9afa09d78c04b7d54569ed7cc5ebf
7
+ data.tar.gz: 9801c61a0a9788a563396bed4064b0b0f4aeacb82e43677c52bfc83ed1d25d7b001af4915c3bdb86b6191b1f5681bd1b54460fbaaa626bfde32e471cb12a830f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # 3.0.0 2025-10-19
2
+
3
+ - Improve implementation of `Papercraft.apply`
4
+ - Add support for rendering self-closing XML tags
5
+ - Streamline Papercraft API
6
+ - Add support for `Papercraft.render { ... }`
7
+ - Prefix internal Proc extensions with `__papercraft_`
8
+ - Change API to use `Papercraft.html` instead of `Proc#render`. Same for
9
+ `apple`, `render_xml` etc.
10
+
11
+ # 2.24 2025-10-14
12
+
13
+ - Update gem links
14
+ - Simplify `render_cache`, caller must provide cache key
15
+ - Reduce surface area of Proc extensions
16
+
1
17
  # 2.23 2025-10-12
2
18
 
3
19
  - Update ERB to version 5.1.1
data/README.md CHANGED
@@ -18,603 +18,31 @@
18
18
  </a>
19
19
  </p>
20
20
 
21
- <p align="center">
22
- <a href="https://www.rubydoc.info/gems/papercraft">API reference</a>
23
- </p>
24
-
25
- ## What is Papercraft?
26
-
27
21
  ```ruby
28
22
  require 'papercraft'
29
23
 
30
- page = ->(**props) {
31
- html {
32
- head { title 'My Title' }
33
- body { render_children **props }
24
+ Papercraft.html {
25
+ div {
26
+ h1 "Hello from Papercraft!"
34
27
  }
35
28
  }
36
- page.render {
37
- p 'foo'
38
- }
39
- #=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
29
+ #=> "<div><h1>Hello from Papercraft</h1></div>"
40
30
  ```
41
31
 
42
- Papercraft is a templating engine for dynamically producing HTML in Ruby apps. Papercraft
43
- templates are expressed as Ruby procs, leading to easier debugging, better
44
- protection against HTML injection attacks, and better code reuse.
32
+ Papercraft is a templating engine for dynamically producing HTML in Ruby apps.
33
+ Papercraft templates are expressed as Ruby procs, leading to easier debugging,
34
+ better protection against HTML injection attacks, and better code reuse.
45
35
 
46
- Papercraft templates can be composed in a variety of ways, facilitating the usage of
47
- layout templates, and enabling a component-oriented approach to building web
48
- interfaces of arbitrary complexity.
36
+ Papercraft templates can be composed in a variety of ways, facilitating the
37
+ usage of layout templates, and enabling a component-oriented approach to
38
+ building web interfaces of arbitrary complexity.
49
39
 
50
40
  In Papercraft, dynamic data is passed explicitly to the template as block/lambda
51
- arguments, making the data flow easy to follow and understand. Papercraft also lets
52
- developers create derivative templates using full or partial parameter
41
+ arguments, making the data flow easy to follow and understand. Papercraft also
42
+ lets developers create derivative templates using full or partial parameter
53
43
  application.
54
44
 
55
- ```ruby
56
- require 'papercraft'
57
-
58
- page = ->(**props) {
59
- html {
60
- head { title 'My Title' }
61
- body { render_children **props }
62
- }
63
- }
64
- page.render {
65
- p(class: 'big') 'foo'
66
- }
67
- #=> "<html><head><title>Title</title></head><body><p class="big">foo</p></body></html>"
68
-
69
- hello_page = page.apply ->(name:, **) {
70
- h1 "Hello, #{name}!"
71
- }
72
- hello.render(name: 'world')
73
- #=> "<html><head><title>Title</title></head><body><h1>Hello, world!</h1></body></html>"
74
- ```
75
-
76
- Papercraft features:
77
-
78
- - Express HTML using plain Ruby procs.
79
- - Automatic compilation for super-fast execution (about as
80
- [fast](https://github.com/digital-fabric/papercraft/blob/master/examples/perf.rb) as
81
- compiled ERB/ERubi).
82
- - Deferred rendering using `defer`.
83
- - Simple and easy template composition (for uses such as layouts, or modular
84
- templates).
85
- - Markdown rendering using [Kramdown](https://github.com/gettalong/kramdown/).
86
- - Rudimentary support for generating XML.
87
- - Support for extensions.
88
- - Simple caching API for caching the rendering result.
89
-
90
- ## Table of Content
91
-
92
- - [Getting Started](#getting-started)
93
- - [Basic Markup](#basic-markup)
94
- - [Builtin Methods](#builtin-methods)
95
- - [Template Parameters](#template-parameters)
96
- - [Template Logic](#template-logic)
97
- - [Template Blocks](#template-blocks)
98
- - [Template Composition](#template-composition)
99
- - [Parameter and Block Application](#parameter-and-block-application)
100
- - [Higher-Order Templates](#higher-order-templates)
101
- - [Layout Template Composition](#layout-template-composition)
102
- - [Rendering Markdown](#rendering-markdown)
103
- - [Deferred Evaluation](#deferred-evaluation)
104
- - [Cached Rendering](#cached-rendering)
105
-
106
- A typical example for a dashboard-type app markup can be found here:
107
- https://github.com/digital-fabric/papercraft/blob/master/examples/dashboard.rb
108
-
109
- ## Getting Started
110
-
111
- In Papercraft, an HTML template is expressed as a proc:
112
-
113
- ```ruby
114
- html = -> {
115
- div(id: 'greeter') { p 'Hello!' }
116
- }
117
- ```
118
-
119
- Rendering a template is done using `Proc#render`:
120
-
121
- ```ruby
122
- require 'papercraft'
123
-
124
- html.render #=> "<div id="greeter"><p>Hello!</p></div>"
125
- ```
126
-
127
- ## Basic Markup
128
-
129
- Tags are added using unqualified method calls, and can be nested using blocks:
130
-
131
- ```ruby
132
- -> {
133
- html {
134
- head {
135
- title 'page title'
136
- }
137
- body {
138
- article {
139
- h1 'article title'
140
- }
141
- }
142
- }
143
- }
144
- ```
145
-
146
- Tag methods accept a string argument, a block, or no argument at all:
147
-
148
- ```ruby
149
- -> { p 'hello' }.render #=> "<p>hello</p>"
150
-
151
- -> { p { span '1'; span '2' } }.render #=> "<p><span>1</span><span>2</span></p>"
152
-
153
- -> { hr() }.render #=> "<hr/>"
154
- ```
155
-
156
- Tag methods also accept tag attributes, given as a hash:
157
-
158
- ```ruby
159
- -> { img src: '/my.gif' }.render #=> "<img src=\"/my.gif\"/>"
160
-
161
- -> { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
162
- ```
163
-
164
- A `true` attribute value will emit a valueless attribute. A `nil` or `false`
165
- attribute value will emit nothing:
166
-
167
- ```ruby
168
- -> { button disabled: nil }.render #=> "<button></button>"
169
- -> { button disabled: true }.render #=> "<button disabled></button>"
170
- ```
171
-
172
- An attribute value given as an array will be joined by space characters:
173
-
174
- ```ruby
175
- -> { div class: [:foo, :bar] }.render #=> "<div class=\"foo bar\"></div>"
176
- ```
177
-
178
- ### Tag and Attribute Formatting
179
-
180
- Papercraft does not make any assumption about what tags and attributes you can use. You
181
- can mix upper and lower case letters, and you can include arbitrary characters
182
- in tag and attribute names. However, in order to best adhere to the HTML specs
183
- and common practices, tag names and attributes will be formatted according to
184
- the following rules, depending on the template type:
185
-
186
- - HTML: underscores are converted to dashes:
187
-
188
- ```ruby
189
- -> {
190
- foo_bar { p 'Hello', data_name: 'world' }
191
- }.render #=> '<foo-bar><p data-name="world">Hello</p></foo-bar>'
192
- ```
45
+ ## Documentation
193
46
 
194
- If you need more precise control over tag names, you can use the `#tag` method,
195
- which takes the tag name as its first parameter, then the rest of the parameters
196
- normally used for tags:
197
-
198
- ```ruby
199
- -> {
200
- tag 'cra_zy__:!tag', 'foo'
201
- }.render #=> '<cra_zy__:!tag>foo</cra_zy__:!tag>'
202
- ```
203
-
204
- ### Escaping Content
205
-
206
- Papercraft automatically escapes all text content emitted in a template. The specific
207
- escaping algorithm depends on the template type. To emit raw HTML, use the
208
- `#raw` method as [described below](#builtin-methods).
209
-
210
- ## Builtin Methods
211
-
212
- In addition to normal tags, Papercraft provides the following method calls for templates:
213
-
214
- ### `#text` - emit escaped text
215
-
216
- `#text` is used for emitting text that will be escaped. This method can be used
217
- to emit text not directly inside an enclosing tag:
218
-
219
- ```ruby
220
- -> {
221
- p {
222
- text 'The time is: '
223
- span(Time.now, id: 'clock')
224
- }
225
- }.render #=> <p>The time is: <span id="clock">XX:XX:XX</span></p>
226
- ```
227
-
228
- ### `#raw` - emit raw HTML
229
-
230
- `#raw` is used for emitting raw HTML, i.e. without escaping. You can use this to
231
- emit an HTML snippet:
232
-
233
- ```ruby
234
- TITLE_HTML = '<h1>hi</h1>'
235
- -> {
236
- div {
237
- raw TITLE_HTML
238
- }
239
- }.render #=> <div><h1>hi</h1></div>
240
- ```
241
-
242
- ### `#render_children` - render the given block
243
-
244
- `#render_children` is used to emit a given block. If no block is given, a
245
- `LocalJumpError` exception is raised:
246
-
247
- ```ruby
248
- Card = ->(**props) {
249
- card { render_children(**props) }
250
- }
251
-
252
- Card.render(foo: 'bar') { |foo|
253
- h1 foo
254
- } #=> <card><h1>bar</h1></card>
255
- ```
256
-
257
- `render_children` can be called with or without arguments, which are passed to the
258
- given block.
259
-
260
- ### `#defer` - emit deferred HTML
261
-
262
- `#defer` is used to emit HTML in a deferred fashion - the deferred part will be
263
- evaluated only after processing the entire template:
264
-
265
- ```ruby
266
- Layout = -> {
267
- head {
268
- defer {
269
- title @title
270
- }
271
- }
272
- body {
273
- render_children
274
- }
275
- }
276
-
277
- Layout.render {
278
- @title = 'Foobar'
279
- h1 'hi'
280
- } #=> <head><title>Foobar</title></head><body><h1>hi</h1></body>
281
- ```
282
-
283
- ### `#render` - render the given template inline
284
-
285
- `#render` is used to emit the given template. This can be used to compose
286
- templates:
287
-
288
- ```ruby
289
- partial = -> { p 'foo' }
290
- -> {
291
- div {
292
- render partial
293
- }
294
- }.render #=> <div><p>foo</p></div>
295
- ```
296
-
297
- Any argument following the given template is passed to the template for
298
- rendering:
299
-
300
- ```ruby
301
- large_button = ->(title) { button(title, class: 'large') }
302
-
303
- -> {
304
- render large_button, 'foo'
305
- }.render #=> <button class="large">foo</button>
306
- ```
307
-
308
- ### `#html`/`#html5` - emit an HTML5 document type declaration and html tag
309
-
310
- ```ruby
311
- -> {
312
- html5 {
313
- p 'hi'
314
- }
315
- } #=> <!DOCTYPE html><html><p>hi</p></html>
316
- ```
317
-
318
- ### `#markdown` emit markdown content
319
-
320
- `#markdown` is used for rendering markdown content. The call converts the given
321
- markdown to HTML and emits it into the rendered HTML:
322
-
323
- ```ruby
324
- -> {
325
- div {
326
- markdown 'This is *markdown*'
327
- }
328
- }.render #=> <p>This is <em>markdown</em></p>
329
- ```
330
-
331
- ## Template Parameters
332
-
333
- In Papercraft, parameters are always passed explicitly. This means that template
334
- parameters are specified as block parameters, and are passed to the template on
335
- rendering:
336
-
337
- ```ruby
338
- greeting = ->(name) { h1 "Hello, #{name}!" }
339
- greeting.render('world') #=> "<h1>Hello, world!</h1>"
340
- ```
341
-
342
- Templates can also accept named parameters:
343
-
344
- ```ruby
345
- greeting = ->(name:) { h1 "Hello, #{name}!" }
346
- greeting.render(name: 'world') #=> "<h1>Hello, world!</h1>"
347
- ```
348
-
349
- ## Template Logic
350
-
351
- Since Papercraft templates are just a bunch of Ruby, you can easily embed your view
352
- logic right in the template:
353
-
354
- ```ruby
355
- ->(user = nil) {
356
- if user
357
- span "Hello, #{user.name}!"
358
- else
359
- span "Hello, guest!"
360
- end
361
- }
362
- ```
363
-
364
- ## Template Blocks
365
-
366
- Templates can also accept and render blocks by using `render_children`:
367
-
368
- ```ruby
369
- page = -> {
370
- html {
371
- body { render_children }
372
- }
373
- }
374
-
375
- # we pass the inner HTML
376
- page.render { h1 'hi' }
377
- ```
378
-
379
- ## Template Composition
380
-
381
- Papercraft makes it easy to compose multiple templates into a whole HTML document. A Papercraft
382
- template can contain other templates, as the following example shows.
383
-
384
- ```ruby
385
- Title = ->(title) { h1 title }
386
-
387
- Item = ->(id:, text:, checked:) {
388
- li {
389
- input name: id, type: 'checkbox', checked: checked
390
- label text, for: id
391
- }
392
- }
393
-
394
- ItemList = ->(items) {
395
- ul {
396
- items.each { |i|
397
- Item(**i)
398
- }
399
- }
400
- }
401
-
402
- page = ->(title, items) {
403
- html5 {
404
- head { Title(title) }
405
- body { ItemList(items) }
406
- }
407
- }
408
-
409
- page.render('Hello from composed templates', [
410
- { id: 1, text: 'foo', checked: false },
411
- { id: 2, text: 'bar', checked: true }
412
- ])
413
- ```
414
-
415
- In addition to using templates defined as constants, you can also use
416
- non-constant templates by invoking the `#render` method:
417
-
418
- ```ruby
419
- greeting = -> { span "Hello, world" }
420
-
421
- -> {
422
- div {
423
- render greeting
424
- }
425
- }
426
- ```
427
-
428
- ## Parameter and Block Application
429
-
430
- Parameters and blocks can be applied to a template without it being rendered, by
431
- using `#apply`. This mechanism is what allows template composition and the
432
- creation of higher-order templates.
433
-
434
- The `#apply` method returns a new template which applies the given parameters
435
- and or block to the original template:
436
-
437
- ```ruby
438
- # parameter application
439
- hello = -> { |name| h1 "Hello, #{name}!" }
440
- hello_world = hello.apply('world')
441
- hello_world.render #=> "<h1>Hello, world!</h1>"
442
-
443
- # block application
444
- div_wrap = -> { div { render_children } }
445
- wrapped_h1 = div_wrap.apply { h1 'hi' }
446
- wrapped_h1.render #=> "<div><h1>hi</h1></div>"
447
-
448
- # wrap a template
449
- wrapped_hello_world = div_wrap.apply(&hello_world)
450
- wrapped_hello_world.render #=> "<div><h1>Hello, world!</h1></div>"
451
- ```
452
-
453
- ## Higher-Order Templates
454
-
455
- Papercraft also lets you create higher-order templates, that is, templates that take
456
- other templates as parameters, or as blocks. Higher-order templates are handy
457
- for creating layouts, wrapping templates in arbitrary markup, enhancing
458
- templates or injecting template parameters.
459
-
460
- Here is a higher-order template that takes a template as parameter:
461
-
462
- ```ruby
463
- div_wrap = -> { |inner| div { render inner } }
464
- greeter = -> { h1 'hi' }
465
- wrapped_greeter = div_wrap.apply(greeter)
466
- wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
467
- ```
468
-
469
- The inner template can also be passed as a block, as shown above:
470
-
471
- ```ruby
472
- div_wrap = -> { div { render_children } }
473
- wrapped_greeter = div_wrap.apply { h1 'hi' }
474
- wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
475
- ```
476
-
477
- ## Layout Template Composition
478
-
479
- One of the principal uses of higher-order templates is the creation of nested
480
- layouts. Suppose we have a website with a number of different layouts, and we'd
481
- like to avoid having to repeat the same code in the different layouts. We can do
482
- this by creating a `default` page template that takes a block, then use `#apply`
483
- to create the other templates:
484
-
485
- ```ruby
486
- default_layout = -> { |**params|
487
- html5 {
488
- head {
489
- title: params[:title]
490
- }
491
- body {
492
- render_children(**params)
493
- }
494
- }
495
- }
496
-
497
- article_layout = default_layout.apply { |title:, body:|
498
- article {
499
- h1 title
500
- markdown body
501
- }
502
- }
503
-
504
- article_layout.render(
505
- title: 'This is a title',
506
- body: 'Hello from *markdown body*'
507
- )
508
- ```
509
-
510
- ## Rendering Markdown
511
-
512
- Markdown is rendered using the
513
- [Kramdown](https://kramdown.gettalong.org/index.html) gem. To emit Markdown, use
514
- `#markdown`:
515
-
516
- ```ruby
517
- template = -> { |md| div { markdown md } }
518
- template.render("Here's some *Markdown*") #=> "<div><p>Here's some <em>Markdown</em><p>\n</div>"
519
- ```
520
-
521
- [Kramdown
522
- options](https://kramdown.gettalong.org/options.html#available-options) can be
523
- specified by adding them to the `#markdown` call:
524
-
525
- ```ruby
526
- template = -> { |md| div { markdown md, auto_ids: false } }
527
- template.render("# title") #=> "<div><h1>title</h1></div>"
528
- ```
529
-
530
- You can also use `Papercraft.markdown` directly:
531
-
532
- ```ruby
533
- Papercraft.markdown('# title') #=> "<h1>title</h1>"
534
- ```
535
-
536
- The default Kramdown options are:
537
-
538
- ```ruby
539
- {
540
- entity_output: :numeric,
541
- syntax_highlighter: :rouge,
542
- input: 'GFM',
543
- hard_wrap: false
544
- }
545
- ```
546
-
547
- The deafult options can be configured by accessing
548
- `Papercraft.default_kramdown_options`, e.g.:
549
-
550
- ```ruby
551
- Papercraft.default_kramdown_options[:auto_ids] = false
552
- ```
553
-
554
- ## Deferred Evaluation
555
-
556
- Deferred evaluation allows deferring the rendering of parts of a template until
557
- the last moment, thus allowing an inner template to manipulate the state of the
558
- outer template. To in order to defer a part of a template, use `#defer`, and
559
- include any markup in the provided block. This technique, in in conjunction with
560
- holding state in instance variables, is an alternative to passing parameters,
561
- which can be limiting in some situations.
562
-
563
- A few use cases for deferred evaulation come to mind:
564
-
565
- - Setting the page title.
566
- - Adding a flash message to a page.
567
- - Using templates that dynamically add static dependencies (JS and CSS) to the
568
- page.
569
-
570
- The last use case is particularly interesting. Imagine a `DependencyMananger`
571
- class that can collect JS and CSS dependencies from the different templates
572
- integrated into the page, and adds them to the page's `<head>` element:
573
-
574
- ```ruby
575
- deps = DependencyMananger.new
576
-
577
- default_layout = -> { |**args|
578
- head {
579
- defer { render deps.head_markup }
580
- }
581
- body { render_children **args }
582
- }
583
-
584
- button = proc { |text, onclick|
585
- deps.js '/static/js/button.js'
586
- deps.css '/static/css/button.css'
587
-
588
- button text, onclick: onclick
589
- }
590
-
591
- heading = proc { |text|
592
- deps.js '/static/js/heading.js'
593
- deps.css '/static/css/heading.css'
594
-
595
- h1 text
596
- }
597
-
598
- page = default_layout.apply {
599
- render heading, "What's your favorite cheese?"
600
-
601
- render button, 'Beaufort', 'eat_beaufort()'
602
- render button, 'Mont d''or', 'eat_montdor()'
603
- render button, 'Époisses', 'eat_epoisses()'
604
- }
605
- ```
606
-
607
- ## Cached Rendering
608
-
609
- Papercraft provides a simple API for caching the result of a rendering. The cache stores
610
- renderings of a template respective to the given arguments. To automatically
611
- retrieve the cached rendered HTML, or generate it for the first time, use
612
- `Proc#render_cached`:
613
-
614
- ```ruby
615
- template = ->(title) { div { h1 title } }
616
- template.render_cached('foo') #=> <div><h1>foo</h1></div>
617
- template.render_cached('foo') #=> <div><h1>foo</h1></div> (from cache)
618
- template.render_cached('bar') #=> <div><h1>bar</h1></div>
619
- template.render_cached('bar') #=> <div><h1>bar</h1></div> (from cache)
620
- ```
47
+ For more information, please consult the [Papercraft
48
+ website](https://papercraft.noteflakes.com/).
@@ -164,12 +164,23 @@ module Papercraft
164
164
  # adjust_whitespace(node.location)
165
165
  is_void = is_void_element?(tag)
166
166
  is_raw_inner_text = is_raw_inner_text_element?(tag)
167
+ is_empty = !node.block && !node.inner_text
167
168
 
168
- if is_void && (node.block || node.inner_text)
169
+ if is_void && !is_empty
169
170
  raise Papercraft::Error, "Void element #{tag} cannot contain child nodes or inner text"
170
171
  end
171
172
 
172
- emit_html(node.tag_location, format_html_tag_open(node.tag_location, tag, node.attributes))
173
+ if @mode == :xml && is_empty
174
+ emit_html(
175
+ node.tag_location,
176
+ format_xml_tag_self_closing(node.tag_location, tag, node.attributes)
177
+ )
178
+ return
179
+ end
180
+
181
+ emit_html(
182
+ node.tag_location, format_html_tag_open(node.tag_location, tag, node.attributes)
183
+ )
173
184
  return if is_void
174
185
 
175
186
  case node.block
@@ -178,7 +189,7 @@ module Papercraft
178
189
  when Prism::BlockArgumentNode
179
190
  flush_html_parts!
180
191
  adjust_whitespace(node.block)
181
- emit("; #{format_code(node.block.expression)}.compiled_proc.(__buffer__)")
192
+ emit("; #{format_code(node.block.expression)}.__papercraft_compiled_proc.(__buffer__)")
182
193
  end
183
194
 
184
195
  if node.inner_text
@@ -213,7 +224,7 @@ module Papercraft
213
224
  emit(format_code(node.call_node.receiver))
214
225
  emit('::')
215
226
  end
216
- emit("#{node.call_node.name}.compiled_proc.(__buffer__")
227
+ emit("#{node.call_node.name}.__papercraft_compiled_proc.(__buffer__")
217
228
  if node.call_node.arguments
218
229
  emit(', ')
219
230
  visit(node.call_node.arguments)
@@ -229,17 +240,17 @@ module Papercraft
229
240
  args = node.call_node.arguments.arguments
230
241
  first_arg = args.first
231
242
 
232
- block_embed = node.block && "&(->(__buffer__) #{format_code(node.block)}.compiled!)"
243
+ block_embed = node.block && "&(->(__buffer__) #{format_code(node.block)}.__papercraft_compiled!)"
233
244
  block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments
234
245
 
235
246
  flush_html_parts!
236
247
  adjust_whitespace(node.location)
237
248
 
238
249
  if args.length == 1
239
- emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__#{block_embed})")
250
+ emit("; #{format_code(first_arg)}.__papercraft_compiled_proc.(__buffer__#{block_embed})")
240
251
  else
241
252
  args_code = format_code_comma_separated_nodes(args[1..])
242
- emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__, #{args_code}#{block_embed})")
253
+ emit("; #{format_code(first_arg)}.__papercraft_compiled_proc.(__buffer__, #{args_code}#{block_embed})")
243
254
  end
244
255
  end
245
256
 
@@ -336,7 +347,7 @@ module Papercraft
336
347
  def visit_extension_tag_node(node)
337
348
  flush_html_parts!
338
349
  adjust_whitespace(node.location)
339
- emit("; Papercraft::Extensions[#{node.tag.inspect}].compiled_proc.(__buffer__")
350
+ emit("; Papercraft::Extensions[#{node.tag.inspect}].__papercraft_compiled_proc.(__buffer__")
340
351
  if node.call_node.arguments
341
352
  emit(', ')
342
353
  visit(node.call_node.arguments)
@@ -367,7 +378,7 @@ module Papercraft
367
378
  end
368
379
  block_params = block_params.empty? ? '' : ", #{block_params.join(', ')}"
369
380
 
370
- emit(", &(proc { |__buffer__#{block_params}| #{block_body} }).compiled!")
381
+ emit(", &(proc { |__buffer__#{block_params}| #{block_body} }).__papercraft_compiled!")
371
382
  end
372
383
  emit(")")
373
384
  end
@@ -382,7 +393,7 @@ module Papercraft
382
393
  guard = @render_yield_used ?
383
394
  '' : "; raise(LocalJumpError, 'no block given (render_yield)') if !__block__"
384
395
  @render_yield_used = true
385
- emit("#{guard}; __block__.compiled_proc.(__buffer__")
396
+ emit("#{guard}; __block__.__papercraft_compiled_proc.(__buffer__")
386
397
  if node.call_node.arguments
387
398
  emit(', ')
388
399
  visit(node.call_node.arguments)
@@ -398,7 +409,7 @@ module Papercraft
398
409
  flush_html_parts!
399
410
  adjust_whitespace(node.location)
400
411
  @render_children_used = true
401
- emit("; __block__&.compiled_proc&.(__buffer__")
412
+ emit("; __block__&.__papercraft_compiled_proc&.(__buffer__")
402
413
  if node.call_node.arguments
403
414
  emit(', ')
404
415
  visit(node.call_node.arguments)
@@ -410,7 +421,7 @@ module Papercraft
410
421
  flush_html_parts!
411
422
  adjust_whitespace(node.location)
412
423
 
413
- emit("; #{node.call_node.receiver.name}.compiled_proc.(__buffer__")
424
+ emit("; #{node.call_node.receiver.name}.__papercraft_compiled_proc.(__buffer__")
414
425
  if node.call_node.arguments
415
426
  emit(', ')
416
427
  visit(node.call_node.arguments)
@@ -418,7 +429,7 @@ module Papercraft
418
429
  if node.call_node.block
419
430
  emit(", &(->")
420
431
  visit(node.call_node.block)
421
- emit(").compiled_proc")
432
+ emit(").__papercraft_compiled_proc")
422
433
  end
423
434
  emit(")")
424
435
  end
@@ -489,6 +500,15 @@ module Papercraft
489
500
  RAW_INNER_TEXT_TAGS.include?(tag.to_s)
490
501
  end
491
502
 
503
+ def format_xml_tag_self_closing(loc, tag, attributes)
504
+ tag = convert_tag(tag)
505
+ if attributes && attributes&.elements.size > 0 || @@html_debug_attribute_injector
506
+ "<#{tag} #{format_html_attributes(loc, attributes)}/>"
507
+ else
508
+ "<#{tag}/>"
509
+ end
510
+ end
511
+
492
512
  # Formats an open tag with optional attributes.
493
513
  #
494
514
  # @param loc [Prism::Location] tag location
@@ -5,122 +5,37 @@ require_relative './compiler'
5
5
  module Papercraft
6
6
  # Extensions to the Proc class.
7
7
  module ProcExtensions
8
- # Returns the compiled form code for the proc.
9
- #
10
- # @return [String] compiled proc code
11
- def compiled_code
12
- Papercraft::Compiler.compile_to_code(self).last
13
- end
14
-
15
- # Returns the source map for the compiled proc.
16
- #
17
- # @return [Array<String>] source map
18
- def source_map
19
- loc = source_location
20
- fn = compiled? ? loc.first : Papercraft::Compiler.source_location_to_fn(loc)
21
- Papercraft::Compiler.source_map_store[fn]
22
- end
23
-
24
- # Returns the AST for the proc.
25
- #
26
- # @return [Prism::Node] AST root
27
- def ast
28
- Sirop.to_ast(self)
29
- end
30
-
31
8
  # Returns true if proc is marked as compiled.
32
9
  #
33
10
  # @return [bool] is the proc marked as compiled
34
- def compiled?
35
- @is_compiled
11
+ def __papercraft_compiled?
12
+ @__papercraft_compiled
36
13
  end
37
14
 
38
15
  # Marks the proc as compiled, i.e. can render directly and takes a string
39
16
  # buffer as first argument.
40
17
  #
41
18
  # @return [self]
42
- def compiled!
43
- @is_compiled = true
19
+ def __papercraft_compiled!
20
+ @__papercraft_compiled = true
44
21
  self
45
22
  end
46
23
 
47
- # Returns the compiled proc for the given proc. If marked as compiled, returns
24
+ # Returns the compiled proc for the proc. If marked as compiled, returns
48
25
  # self.
49
26
  #
50
27
  # @param mode [Symbol] compilation mode (:html, :xml)
51
28
  # @return [Proc] compiled proc or self
52
- def compiled_proc(mode: :html)
53
- @compiled_proc ||= @is_compiled ? self : compile(mode:)
54
- end
55
-
56
- # Compiles the proc into the compiled form.
57
- #
58
- # @param mode [Symbol] compilation mode (:html, :xml)
59
- # @return [Proc] compiled proc
60
- def compile(mode: :html)
61
- Papercraft::Compiler.compile(self, mode:).compiled!
62
- rescue Sirop::Error
63
- raise Papercraft::Error, "Dynamically defined procs cannot be compiled"
64
- end
65
-
66
- # Renders the proc to HTML with the given arguments.
67
- #
68
- # @return [String] HTML string
69
- def render(*a, **b, &c)
70
- compiled_proc.(+'', *a, **b, &c)
71
- rescue Exception => e
72
- e.is_a?(Papercraft::Error) ? raise : raise(Papercraft.translate_backtrace(e))
73
- end
74
-
75
- # Renders the proc to XML with the given arguments.
76
- #
77
- # @return [String] XML string
78
- def render_xml(*a, **b, &c)
79
- compiled_proc(mode: :xml).(+'', *a, **b, &c)
80
- rescue Exception => e
81
- e.is_a?(Papercraft::Error) ? raise : raise(Papercraft.translate_backtrace(e))
82
- end
83
-
84
- # Renders the proc to HTML with the given arguments into the given buffer.
85
- #
86
- # @param buf [String] buffer
87
- # @return [String] HTML string
88
- def render_to_buffer(buf, *a, **b, &c)
89
- compiled_proc.(buf, *a, **b, &c)
90
- rescue Exception => e
91
- raise Papercraft.translate_backtrace(e)
92
- end
93
-
94
- # Returns a proc that applies the given arguments to the original proc. The
95
- # returned proc calls the *compiled* form of the proc, merging the
96
- # positional and keywords parameters passed to `#apply` with parameters
97
- # passed to the applied proc. If a block is given, it is wrapped in a proc
98
- # that passed merged parameters to the block.
99
- #
100
- # @param *pos1 [Array<any>] applied positional parameters
101
- # @param **kw1 [Hash<any, any] applied keyword parameters
102
- # @return [Proc] applied proc
103
- def apply(*pos1, **kw1, &block)
104
- compiled = compiled_proc
105
- c_compiled = block&.compiled_proc
106
-
107
- ->(__buffer__, *pos2, **kw2, &block2) {
108
- c_proc = c_compiled && ->(__buffer__, *pos3, **kw3) {
109
- c_compiled.(__buffer__, *pos3, **kw3, &block2)
110
- }.compiled!
111
-
112
- compiled.(__buffer__, *pos1, *pos2, **kw1, **kw2, &c_proc)
113
- }.compiled!
29
+ def __papercraft_compiled_proc(mode: :html)
30
+ @__papercraft_compiled_proc ||= @__papercraft_compiled ?
31
+ self : Papercraft.compile(self, mode:)
114
32
  end
115
33
 
116
- # Caches and returns the rendered HTML for the template with the given
117
- # arguments.
34
+ # Returns the render cache for the proc.
118
35
  #
119
- # @return [String] HTML string
120
- def render_cached(*args, **kargs, &block)
121
- @render_cache ||= {}
122
- key = args.empty? && kargs.empty? && !block ? nil : [args, kargs, block&.source_location]
123
- @render_cache[key] ||= render(*args, **kargs, &block)
36
+ # @return [Hash] cache hash
37
+ def __papercraft_render_cache
38
+ @__papercraft_render_cache ||= {}
124
39
  end
125
40
  end
126
41
  end
@@ -8,21 +8,34 @@ module Papercraft
8
8
 
9
9
  # @param proc [Proc] template proc
10
10
  # @param mode [Symbol] mode (:html, :xml)
11
- def initialize(proc, mode: :html)
12
- @proc = proc
11
+ def initialize(proc = nil, mode: :html, &block)
12
+ @proc = proc || block
13
+ raise ArgumentError, "No template proc given" if !@proc
14
+
13
15
  @mode = mode
14
16
  end
15
17
 
18
+ # Renders the template.
19
+ #
20
+ # @return [String] generated HTML
16
21
  def render(*, **, &)
17
- (mode == :xml) ? @proc.render_xml(*, **, &) : @proc.render(*, **, &)
22
+ (mode == :xml) ? Papercraft.xml(@proc, *, **, &) : Papercraft.html(@proc, *, **, &)
18
23
  end
24
+ alias_method :call, :render
19
25
 
26
+ # Applies the given parameters and block to the template, returning an
27
+ # applied template.
28
+ #
29
+ # @return [Papercraft::Template] applied template
20
30
  def apply(*, **, &)
21
- Template.new(@proc.apply(*, **, &), mode: @mode)
31
+ Template.new(Papercraft.apply(@proc, *, **, &), mode: @mode)
22
32
  end
23
33
 
24
- def compiled_proc
25
- @proc.compiled_proc(mode: @mode)
34
+ # Returns the compiled proc for the template.
35
+ #
36
+ # @return [Proc] compiled proc
37
+ def __papercraft_compiled_proc
38
+ @proc.__papercraft_compiled_proc(mode: @mode)
26
39
  end
27
40
  end
28
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Papercraft
4
- VERSION = '2.23'
4
+ VERSION = '3.0'
5
5
  end
data/lib/papercraft.rb CHANGED
@@ -114,10 +114,11 @@ module Papercraft
114
114
  # @param opts [Hash] Kramdown option overrides
115
115
  # @return [Kramdown::Document] Kramdown document
116
116
  def markdown_doc(markdown, **opts)
117
- @markdown_deps_loaded ||= true.tap do
117
+ @markdown_deps_loaded ||= begin
118
118
  require 'kramdown'
119
119
  require 'rouge'
120
120
  require 'kramdown-parser-gfm'
121
+ true
121
122
  end
122
123
 
123
124
  opts = default_kramdown_options.merge(opts)
@@ -153,4 +154,129 @@ module Papercraft
153
154
  def default_kramdown_options=(opts)
154
155
  @default_kramdown_options = opts
155
156
  end
157
+
158
+ # Returns the compiled form code for the given proc.
159
+ #
160
+ # @param proc [Proc] template proc
161
+ # @return [String] compiled proc code
162
+ def compiled_code(proc)
163
+ Papercraft::Compiler.compile_to_code(proc).last
164
+ end
165
+
166
+ # Returns the source map for the given proc.
167
+ #
168
+ # @param proc [Proc] template proc
169
+ # @return [Array<String>] source map
170
+ def source_map(proc)
171
+ loc = proc.source_location
172
+ fn = proc.__papercraft_compiled? ? loc.first : Papercraft::Compiler.source_location_to_fn(loc)
173
+ Papercraft::Compiler.source_map_store[fn]
174
+ end
175
+
176
+ # Returns the AST for the given proc.
177
+ #
178
+ # @param proc [Proc] template proc
179
+ # @return [Prism::Node] AST root
180
+ def ast(proc)
181
+ Sirop.to_ast(proc)
182
+ end
183
+
184
+ # Compiles the given template.
185
+ #
186
+ # @param proc [Proc] template proc
187
+ # @param mode [Symbol] compilation mode (:html, :xml)
188
+ # @return [Proc] compiled proc
189
+ def compile(proc, mode: :html)
190
+ Papercraft::Compiler.compile(proc, mode:).__papercraft_compiled!
191
+ rescue Sirop::Error
192
+ raise Papercraft::Error, "Can't compile eval'd template"
193
+ end
194
+
195
+ # Renders the given template to HTML with the given arguments. The template
196
+ # can be passed either as the first parameter, or as a block, if no parameter
197
+ # is given.
198
+ #
199
+ # @param template [Proc] template proc
200
+ # @return [String] HTML string
201
+ def html(template = nil, *pos, **kw, &block)
202
+ if !template
203
+ template = block
204
+ elsif template.is_a?(Template)
205
+ template = template.proc
206
+ end
207
+ raise ArgumentError, "No template given" if !template
208
+
209
+ template.__papercraft_compiled_proc.(+'', *pos, **kw, &block)
210
+ rescue Exception => e
211
+ e.is_a?(Papercraft::Error) ? raise : raise(Papercraft.translate_backtrace(e))
212
+ end
213
+
214
+ # Renders the given template to XML with the given arguments. The template can
215
+ # be passed either as the first parameter, or as a block, if no parameter is
216
+ # given.
217
+ #
218
+ # @param template [Proc] template proc
219
+ # @return [String] XML string
220
+ def xml(template = nil, *pos, **kw, &block)
221
+ if !template
222
+ template = block
223
+ elsif template.is_a?(Template)
224
+ template = template.proc
225
+ end
226
+ raise ArgumentError, "No template given" if !template
227
+
228
+ template = template.proc if template.is_a?(Template)
229
+ template.__papercraft_compiled_proc(mode: :xml).(+'', *pos, **kw, &block)
230
+ rescue Exception => e
231
+ e.is_a?(Papercraft::Error) ? raise : raise(Papercraft.translate_backtrace(e))
232
+ end
233
+
234
+ # Returns a proc that applies the given arguments to the original proc. The
235
+ # returned proc calls the *compiled* form of the proc, merging the
236
+ # positional and keywords parameters passed to `#apply` with parameters
237
+ # passed to the applied proc. If a block is given, it is wrapped in a proc
238
+ # that passed merged parameters to the block.
239
+ #
240
+ # @param template [Proc] template proc
241
+ # @param *pos1 [Array<any>] applied positional parameters
242
+ # @param **kw1 [Hash<any, any] applied keyword parameters
243
+ # @return [Proc] applied proc
244
+ def apply(template, *pos1, **kw1, &block1)
245
+ template = template.proc if template.is_a?(Template)
246
+ compiled = template.__papercraft_compiled_proc
247
+ block1_compiled = block1&.__papercraft_compiled_proc
248
+
249
+ ->(__buffer__, *pos2, **kw2, &block2) {
250
+ if block2
251
+ block2_compiled = block1_compiled ?
252
+ ->(__buffer__, *pos3, **kw3) {
253
+ block1_compiled.(__buffer__, *pos3, **kw3, &block2)
254
+ }.__papercraft_compiled! :
255
+ block2.__papercraft_compiled_proc
256
+ compiled.(__buffer__, *pos1, *pos2, **kw1, **kw2, &block2_compiled)
257
+ else
258
+ compiled.(__buffer__, *pos1, *pos2, **kw1, **kw2, &block1_compiled)
259
+ end
260
+ }.__papercraft_compiled!
261
+ end
262
+
263
+ # Caches and returns the rendered HTML for the template with the given
264
+ # arguments.
265
+ #
266
+ # @param template [Proc] template proc
267
+ # @param key [any] Cache key
268
+ # @return [String] HTML string
269
+ def cache_html(template, key, *, **, &)
270
+ template.__papercraft_render_cache[key] ||= html(template, *, **, &)
271
+ end
272
+
273
+ # Caches and returns the rendered XML for the template with the given
274
+ # arguments.
275
+ #
276
+ # @param template [Proc] template proc
277
+ # @param key [any] Cache key
278
+ # @return [String] XML string
279
+ def cache_xml(template, key, *, **, &)
280
+ template.__papercraft_render_cache[key] ||= xml(template, *, **, &)
281
+ end
156
282
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: papercraft
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.23'
4
+ version: '3.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -142,8 +142,8 @@ homepage: http://github.com/digital-fabric/papercraft
142
142
  licenses:
143
143
  - MIT
144
144
  metadata:
145
- homepage_uri: https://github.com/digital-fabric/papercraft
146
- documentation_uri: https://www.rubydoc.info/gems/papercraft
145
+ homepage_uri: https://papercraft.noteflakes.com/
146
+ source_code_uri: https://github.com/digital-fabric/papercraft
147
147
  changelog_uri: https://github.com/digital-fabric/papercraft/blob/master/CHANGELOG.md
148
148
  rdoc_options:
149
149
  - "--title"