papercraft 0.8.2 → 0.10.1
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +132 -123
- data/lib/papercraft/component.rb +18 -11
- data/lib/papercraft/extension_proxy.rb +30 -0
- data/lib/papercraft/html.rb +36 -4
- data/lib/papercraft/renderer.rb +107 -41
- data/lib/papercraft/version.rb +1 -1
- data/lib/papercraft.rb +31 -11
- metadata +50 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 757137ea8d3a2b5d5d329b7e841bad13cc26ea3486ebcb216f1ef016d7d3ff4a
|
4
|
+
data.tar.gz: 281ee3b2911b1f4e1ff3337dbefef407ce07b17e21acb949dce158a93d8758a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22225498fce014a17962541bac3c17f20243239b709e191e75fa2f1abbc0f7f7fdef3b0c4d110753f0de1c2ca4efc19f516cb797a02cbafa70af95085449096e
|
7
|
+
data.tar.gz: 57bb7aae102900de14ba9d8f4a0e2be83978e5106ff098ffda5acf39431273fd76be646d012da72094715ad8a3da7fd81a485e51151c74df94091a91e72866f4
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## 0.10.1 2021-12-25
|
2
|
+
|
3
|
+
- Fix tag rendering with empty text in Ruby 3.0
|
4
|
+
|
5
|
+
## 0.10 2021-12-25
|
6
|
+
|
7
|
+
- Add support for extensions
|
8
|
+
|
9
|
+
## 0.9 2021-12-23
|
10
|
+
|
11
|
+
- Add support for emitting Markdown
|
12
|
+
- Add support for passing proc as argument to `#H` and `#X`
|
13
|
+
- Deprecate `Encoding` module
|
14
|
+
|
1
15
|
## 0.8.1 2021-12-22
|
2
16
|
|
3
17
|
- Fix gemspec
|
data/README.md
CHANGED
@@ -1,30 +1,56 @@
|
|
1
|
-
|
1
|
+
<h1 align="center">
|
2
|
+
Papercraft
|
3
|
+
</h1>
|
4
|
+
|
5
|
+
<h4 align="center">Composable HTML templating for Ruby</h4>
|
6
|
+
|
7
|
+
<p align="center">
|
8
|
+
<a href="http://rubygems.org/gems/papercraft">
|
9
|
+
<img src="https://badge.fury.io/rb/papercraft.svg" alt="Ruby gem">
|
10
|
+
</a>
|
11
|
+
<a href="https://github.com/digital-fabric/papercraft/actions?query=workflow%3ATests">
|
12
|
+
<img src="https://github.com/digital-fabric/papercraft/workflows/Tests/badge.svg" alt="Tests">
|
13
|
+
</a>
|
14
|
+
<a href="https://github.com/digital-fabric/papercraft/blob/master/LICENSE">
|
15
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
|
16
|
+
</a>
|
17
|
+
</p>
|
18
|
+
|
19
|
+
<p align="center">
|
20
|
+
<a href="https://www.rubydoc.info/gems/papercraft">API reference</a>
|
21
|
+
</p>
|
2
22
|
|
3
|
-
|
4
|
-
[TUTORIAL](#getting-started) |
|
5
|
-
[EXAMPLES](examples) |
|
6
|
-
[REFERENCE](#api-reference)
|
23
|
+
# Papercraft - Composable HTML templating for Ruby
|
7
24
|
|
8
25
|
## What is Papercraft?
|
9
26
|
|
27
|
+
```ruby
|
28
|
+
require 'papercraft'
|
29
|
+
|
30
|
+
page = H { |*args|
|
31
|
+
html {
|
32
|
+
head { }
|
33
|
+
body { emit_yield *args }
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
hello = H.apply { |name| h1 "Hello, #{name}!" }
|
38
|
+
hello.render('world')
|
39
|
+
#=> "<html><head/><body><h1>Hello, world!</h1></body></html>"
|
40
|
+
```
|
41
|
+
|
10
42
|
Papercraft is an HTML templating engine for Ruby that offers the following
|
11
43
|
features:
|
12
44
|
|
13
|
-
- HTML templating using plain Ruby syntax
|
45
|
+
- HTML and XML templating using plain Ruby syntax
|
14
46
|
- Minimal boilerplate
|
15
47
|
- Mix logic and tags freely
|
16
|
-
-
|
17
|
-
- Automatic HTML escaping
|
48
|
+
- Automatic HTML and XML escaping
|
18
49
|
- Composable components
|
50
|
+
- Explicit parameter passing to nested components
|
19
51
|
- Higher order components
|
20
|
-
- Built-in support for rendering Markdown
|
21
|
-
|
22
|
-
> **Note** Papercraft is a new library and as such may be missing features and
|
23
|
-
> contain bugs. Also, its API may change unexpectedly. Your issue reports and
|
24
|
-
> code contributions are most welcome!
|
25
|
-
|
26
|
-
With Papercraft you can structure your templates as nested HTML components, in a
|
27
|
-
somewhat similar fashion to React.
|
52
|
+
- Built-in support for rendering [Markdown](#emitting-markdown)
|
53
|
+
- Support for namespaced extensions
|
28
54
|
|
29
55
|
## Installing Papercraft
|
30
56
|
|
@@ -42,42 +68,25 @@ $ gem install papercraft
|
|
42
68
|
|
43
69
|
## Getting started
|
44
70
|
|
45
|
-
To
|
71
|
+
To create a template use the global method `Kernel#H`:
|
46
72
|
|
47
73
|
```ruby
|
48
74
|
require 'papercraft'
|
49
|
-
```
|
50
|
-
|
51
|
-
To create a template use `Papercraft.new` or the global method `Kernel#H`:
|
52
75
|
|
53
|
-
```ruby
|
54
|
-
# can also use Papercraft.new
|
55
76
|
html = H {
|
56
|
-
div { p '
|
77
|
+
div(id: 'greeter') { p 'Hello!' }
|
57
78
|
}
|
58
79
|
```
|
59
80
|
|
60
|
-
|
61
|
-
|
62
|
-
To render a Papercraft template use the `#render` method:
|
81
|
+
Rendering a template is done using `#render`:
|
63
82
|
|
64
83
|
```ruby
|
65
|
-
|
66
|
-
```
|
67
|
-
|
68
|
-
The render method accepts an arbitrary context variable:
|
69
|
-
|
70
|
-
```ruby
|
71
|
-
html = H {
|
72
|
-
h1 context[:title]
|
73
|
-
}
|
74
|
-
|
75
|
-
html.render(title: 'My title') #=> "<h1>My title</h1>"
|
84
|
+
html.render #=> "<div id="greeter"><p>Hello!</p></div>"
|
76
85
|
```
|
77
86
|
|
78
87
|
## All about tags
|
79
88
|
|
80
|
-
Tags are added using unqualified method calls, and
|
89
|
+
Tags are added using unqualified method calls, and can be nested using blocks:
|
81
90
|
|
82
91
|
```ruby
|
83
92
|
H {
|
@@ -114,8 +123,9 @@ H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</
|
|
114
123
|
|
115
124
|
## Template parameters
|
116
125
|
|
117
|
-
|
118
|
-
template on
|
126
|
+
In Papercraft, parameters are always passed explicitly. This means that template
|
127
|
+
parameters are specified as block parameters, and are passed to the template on
|
128
|
+
rendering:
|
119
129
|
|
120
130
|
```ruby
|
121
131
|
greeting = H { |name| h1 "Hello, #{name}!" }
|
@@ -328,110 +338,109 @@ H { str 'hi&lo' }.render #=> "hi&lo"
|
|
328
338
|
|
329
339
|
## Emitting Markdown
|
330
340
|
|
331
|
-
|
341
|
+
Markdown is rendered using the
|
342
|
+
[Kramdown](https://kramdown.gettalong.org/index.html) gem. To emit Markdown, use
|
343
|
+
`#emit_markdown`:
|
332
344
|
|
333
345
|
```ruby
|
334
346
|
template = H { |md| div { emit_markdown md } }
|
335
|
-
template.render("Here's some *Markdown*") #=> "<div>Here's some <em>Markdown</em
|
347
|
+
template.render("Here's some *Markdown*") #=> "<div><p>Here's some <em>Markdown</em><p>\n</div>"
|
336
348
|
```
|
337
349
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
together pieces of HTML. Feel free to explore the API!
|
350
|
+
[Kramdown
|
351
|
+
options](https://kramdown.gettalong.org/options.html#available-options) can be
|
352
|
+
specified by adding them to the `#emit_markdown` call:
|
342
353
|
|
343
|
-
|
354
|
+
```ruby
|
355
|
+
template = H { |md| div { emit_markdown md, auto_ids: false } }
|
356
|
+
template.render("# title") #=> "<div><h1>title</h1></div>"
|
357
|
+
```
|
344
358
|
|
345
|
-
|
346
|
-
takes an item component as an argument. The `List` component can be reused for
|
347
|
-
rendering any kind of unordered list, and with any kind of item component:
|
359
|
+
The default Kramdown options are:
|
348
360
|
|
349
361
|
```ruby
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
li { emit item_component }
|
356
|
-
}
|
357
|
-
}
|
358
|
-
}
|
359
|
-
}
|
360
|
-
}
|
361
|
-
|
362
|
-
TodoItem = H {
|
363
|
-
span item.text, class: item.completed ? 'completed' : 'pending'
|
362
|
+
{
|
363
|
+
entity_output: :numeric,
|
364
|
+
syntax_highlighter: :rouge,
|
365
|
+
input: 'GFM',
|
366
|
+
hard_wrap: false
|
364
367
|
}
|
365
|
-
|
366
|
-
def todo_list(items)
|
367
|
-
H {
|
368
|
-
div { List(items, TodoItem) }
|
369
|
-
}
|
370
|
-
end
|
371
368
|
```
|
372
369
|
|
373
|
-
|
374
|
-
|
375
|
-
#### `Papercraft#initialize(**context, &block)` a.k.a. `Kernel#H`
|
376
|
-
|
377
|
-
- `context`: local context hash
|
378
|
-
- `block`: template block
|
379
|
-
|
380
|
-
Initializes a new Papercraft instance. This method takes a block of template
|
381
|
-
code, and an optional [local context](#local-context) in the form of a hash.
|
382
|
-
The `Kernel#H` method serves as a shortcut for creating Papercraft instances.
|
383
|
-
|
384
|
-
#### `Papercraft#render(**context)`
|
385
|
-
|
386
|
-
- `context`: global context hash
|
387
|
-
|
388
|
-
Renders the template with an optional [global context](#global-context)
|
389
|
-
hash.
|
390
|
-
|
391
|
-
#### Methods accessible inside template blocks
|
392
|
-
|
393
|
-
#### `#<tag/component>(*args, **props, &block)`
|
394
|
-
|
395
|
-
- `args`: tag arguments. For an HTML tag Papercraft expects a single `String`
|
396
|
-
argument containing the inner text of the tag.
|
397
|
-
- `props`: hash of tag attributes
|
398
|
-
- `block`: inner HTML block
|
399
|
-
|
400
|
-
Adds a tag or component to the current template. If the method name starts with
|
401
|
-
an upper-case letter, it is considered a [component](#templates-as-components).
|
402
|
-
|
403
|
-
If a text argument is given for a tag, it will be escaped.
|
404
|
-
|
405
|
-
#### `#cache(*vary, &block)`
|
406
|
-
|
407
|
-
- `vary`: variables used in cached block. The given values will be used to
|
408
|
-
create a separate cache entry.
|
409
|
-
- `block`: inner HTML block
|
370
|
+
The deafult options can be configured by accessing
|
371
|
+
`Papercraft::HTML.kramdown_options`:
|
410
372
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
a separate cache entry.
|
373
|
+
```ruby
|
374
|
+
Papercraft::HTML.kramdown_options[:auto_ids] = false
|
375
|
+
```
|
415
376
|
|
416
|
-
|
377
|
+
## Papercraft extensions
|
378
|
+
|
379
|
+
Papercraft extensions are modules that contain one or more methods that can be
|
380
|
+
used to render complex HTML components. Extension modules can be used by
|
381
|
+
installing them as a namespaced extension using `Papercraft.extension`.
|
382
|
+
Extensions are particularly useful when you work with CSS frameworks such as
|
383
|
+
[Bootstrap](https://getbootstrap.com/), [Tailwind](https://tailwindui.com/) or
|
384
|
+
[Primer](https://primer.style/).
|
385
|
+
|
386
|
+
For example, to create a Bootstrap card component, the following HTML markup is
|
387
|
+
needed (example taken from the [Bootstrap
|
388
|
+
docs](https://getbootstrap.com/docs/5.1/components/card/#titles-text-and-links)):
|
389
|
+
|
390
|
+
```html
|
391
|
+
<div class="card" style="width: 18rem;">
|
392
|
+
<div class="card-body">
|
393
|
+
<h5 class="card-title">Card title</h5>
|
394
|
+
<h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
|
395
|
+
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
|
396
|
+
<a href="#" class="card-link">Card link</a>
|
397
|
+
<a href="#" class="card-link">Another link</a>
|
398
|
+
</div>
|
399
|
+
</div>
|
400
|
+
```
|
417
401
|
|
418
|
-
|
402
|
+
With Papercraft, we could create a `Bootstrap` extension with a `#card` method
|
403
|
+
and other associated methods:
|
419
404
|
|
420
|
-
|
405
|
+
```ruby
|
406
|
+
module BootstrapComponents
|
407
|
+
...
|
421
408
|
|
422
|
-
|
409
|
+
def card(**props)
|
410
|
+
div(class: 'card', **props) {
|
411
|
+
div(class: 'card-body') {
|
412
|
+
emit_yield
|
413
|
+
}
|
414
|
+
}
|
415
|
+
end
|
423
416
|
|
424
|
-
|
425
|
-
|
417
|
+
def card_title(title)
|
418
|
+
h5 title, class: 'card-title'
|
419
|
+
end
|
426
420
|
|
427
|
-
|
421
|
+
...
|
422
|
+
end
|
428
423
|
|
429
|
-
|
424
|
+
Papercraft.extension(bootstrap: BootstrapComponents)
|
425
|
+
```
|
430
426
|
|
431
|
-
|
427
|
+
The call to `Papercraft.extension` lets us access the different methods of
|
428
|
+
`BootstrapComponents` by calling `#bootstrap` inside a template. With this,
|
429
|
+
we'll be able to express the above markup as follows:
|
432
430
|
|
433
|
-
|
431
|
+
```ruby
|
432
|
+
H {
|
433
|
+
bootstrap.card(style: 'width: 18rem') {
|
434
|
+
bootstrap.card_title 'Card title'
|
435
|
+
bootstrap.card_subtitle 'Card subtitle'
|
436
|
+
bootstrap.card_text 'Some quick example text to build on the card title and make up the bulk of the card''s content.'
|
437
|
+
bootstrap.card_link '#', 'Card link'
|
438
|
+
bootstrap.card_link '#', 'Another link'
|
439
|
+
}
|
440
|
+
}
|
441
|
+
```
|
434
442
|
|
435
|
-
|
443
|
+
## API Reference
|
436
444
|
|
437
|
-
|
445
|
+
The API reference for this library can be found
|
446
|
+
[here](https://www.rubydoc.info/gems/papercraft).
|
data/lib/papercraft/component.rb
CHANGED
@@ -14,8 +14,8 @@ module Papercraft
|
|
14
14
|
# Components are usually created using the global methods `H` or `X`, for HTML
|
15
15
|
# or XML templates, respectively:
|
16
16
|
#
|
17
|
-
# greeter = H { |name| h1 "Hello, #{name}!" }
|
18
|
-
# "<h1>Hello, world!</h1>"
|
17
|
+
# greeter = H { |name| h1 "Hello, #{name}!" }
|
18
|
+
# greeter.render('world') #=> "<h1>Hello, world!</h1>"
|
19
19
|
#
|
20
20
|
# Components can also be created using the normal constructor:
|
21
21
|
#
|
@@ -25,7 +25,10 @@ module Papercraft
|
|
25
25
|
# In the component block, HTML elements are created by simply calling
|
26
26
|
# unqualified methods:
|
27
27
|
#
|
28
|
-
# page_layout = H {
|
28
|
+
# page_layout = H {
|
29
|
+
# html5 {
|
30
|
+
# head {
|
31
|
+
# title 'foo'
|
29
32
|
# }
|
30
33
|
# body {
|
31
34
|
# h1 "Hello, world!"
|
@@ -75,7 +78,11 @@ module Papercraft
|
|
75
78
|
# anchor.apply(uri: '/about', text: 'About')
|
76
79
|
# ])
|
77
80
|
# links_with_anchors.render
|
81
|
+
#
|
78
82
|
class Component < Proc
|
83
|
+
|
84
|
+
# Determines the rendering mode: `:html` or `:xml`.
|
85
|
+
attr_accessor :mode
|
79
86
|
|
80
87
|
# Initializes a component with the given block. The rendering mode (HTML or
|
81
88
|
# XML) can be passed in the `mode:` parameter. If `mode:` is not specified,
|
@@ -113,15 +120,15 @@ module Papercraft
|
|
113
120
|
# current one. Application is one of the principal methods of composing
|
114
121
|
# components, particularly when passing inner components as blocks:
|
115
122
|
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
123
|
+
# article_wrapper = H {
|
124
|
+
# article {
|
125
|
+
# emit_yield
|
126
|
+
# }
|
127
|
+
# }
|
128
|
+
# wrapped_article = article_wrapper.apply {
|
129
|
+
# h1 'Article title'
|
119
130
|
# }
|
120
|
-
#
|
121
|
-
# wrapped_article = article_wrapper.apply {
|
122
|
-
# h1 'Article title'
|
123
|
-
# }
|
124
|
-
# wrapped_article.render #=> "<article><h1>Article title</h1></article>"
|
131
|
+
# wrapped_article.render #=> "<article><h1>Article title</h1></article>"
|
125
132
|
#
|
126
133
|
# @param *a [<any>] normal parameters
|
127
134
|
# @param **b [Hash] named parameters
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Papercraft
|
4
|
+
|
5
|
+
# An ExtensionProxy proxies method calls to a renderer. Extension proxies are
|
6
|
+
# used to provide an namespaced interface to Papercraft extensions. When an
|
7
|
+
# extension is installed using `Papercraft.extension`, a corresponding method
|
8
|
+
# is defined on the `Papercraft::Renderer` class that creates an extension
|
9
|
+
# proxy that gives access to the different extension methods.
|
10
|
+
class ExtensionProxy
|
11
|
+
|
12
|
+
# Initializes a new ExtensionProxy.
|
13
|
+
# @param renderer [Papercraft::Renderer] renderer to proxy to
|
14
|
+
# @param mod [Module] extension module
|
15
|
+
# @return [void]
|
16
|
+
def initialize(renderer, mod)
|
17
|
+
@renderer = renderer
|
18
|
+
extend(mod)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Proxies missing methods to the renderer
|
22
|
+
# @param sym [Symbol] method name
|
23
|
+
# @param *args [Array] arguments
|
24
|
+
# @param &block [Proc] block
|
25
|
+
# @return void
|
26
|
+
def method_missing(sym, *args, &block)
|
27
|
+
@renderer.send(sym, *args, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/papercraft/html.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'kramdown'
|
4
|
+
require 'rouge'
|
5
|
+
require 'kramdown-parser-gfm'
|
4
6
|
|
5
|
-
module Papercraft
|
6
|
-
# Markup extensions
|
7
|
+
module Papercraft
|
8
|
+
# HTML Markup extensions
|
7
9
|
module HTML
|
8
10
|
# Emits the p tag (overrides Object#p)
|
11
|
+
#
|
9
12
|
# @param text [String] text content of tag
|
10
13
|
# @param props [Hash] tag attributes
|
11
14
|
# @para block [Proc] nested HTML block
|
@@ -16,7 +19,8 @@ module Papercraft
|
|
16
19
|
|
17
20
|
S_HTML5_DOCTYPE = '<!DOCTYPE html>'
|
18
21
|
|
19
|
-
# Emits an HTML5 doctype tag and an html tag with the given block
|
22
|
+
# Emits an HTML5 doctype tag and an html tag with the given block.
|
23
|
+
#
|
20
24
|
# @param block [Proc] nested HTML block
|
21
25
|
# @return [void]
|
22
26
|
def html5(&block)
|
@@ -24,6 +28,11 @@ module Papercraft
|
|
24
28
|
self.html(&block)
|
25
29
|
end
|
26
30
|
|
31
|
+
# Emits a link element with a stylesheet.
|
32
|
+
#
|
33
|
+
# @param href [String] stylesheet URL
|
34
|
+
# @param custom_attributes [Hash] optional custom attributes for the link element
|
35
|
+
# @return [void]
|
27
36
|
def link_stylesheet(href, custom_attributes = nil)
|
28
37
|
attributes = {
|
29
38
|
rel: 'stylesheet',
|
@@ -34,5 +43,28 @@ module Papercraft
|
|
34
43
|
end
|
35
44
|
link(**attributes)
|
36
45
|
end
|
46
|
+
|
47
|
+
def emit_markdown(markdown, **opts)
|
48
|
+
emit Kramdown::Document.new(markdown, **kramdown_options(opts)).to_html
|
49
|
+
end
|
50
|
+
|
51
|
+
def kramdown_options(opts)
|
52
|
+
HTML.kramdown_options.merge(**opts)
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def kramdown_options
|
57
|
+
@kramdown_options ||= {
|
58
|
+
entity_output: :numeric,
|
59
|
+
syntax_highlighter: :rouge,
|
60
|
+
input: 'GFM',
|
61
|
+
hard_wrap: false
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def kramdown_options=(opts)
|
66
|
+
@kramdown_options = opts
|
67
|
+
end
|
68
|
+
end
|
37
69
|
end
|
38
70
|
end
|
data/lib/papercraft/renderer.rb
CHANGED
@@ -1,11 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative './html'
|
4
|
+
require_relative './extension_proxy'
|
4
5
|
|
5
6
|
module Papercraft
|
7
|
+
|
6
8
|
# A Renderer renders a Papercraft component into a string
|
7
9
|
class Renderer
|
10
|
+
|
8
11
|
class << self
|
12
|
+
|
13
|
+
# Verifies that the given template proc can be called with the given
|
14
|
+
# arguments and named arguments. If the proc demands named argument keys
|
15
|
+
# that do not exist in `named_args`, `Papercraft::Error` is raised.
|
16
|
+
#
|
17
|
+
# @param template [Proc] proc to verify
|
18
|
+
# @param args [Array<any>] arguments passed to proc
|
19
|
+
# @param named_args [Hash] named arguments passed to proc
|
9
20
|
def verify_proc_parameters(template, args, named_args)
|
10
21
|
param_count = 0
|
11
22
|
template.parameters.each do |(type, name)|
|
@@ -21,40 +32,58 @@ module Papercraft
|
|
21
32
|
if param_count > args.size
|
22
33
|
raise Papercraft::Error, "Missing template parameters"
|
23
34
|
end
|
24
|
-
end
|
25
|
-
end
|
35
|
+
end
|
26
36
|
|
27
|
-
|
37
|
+
# Installs the given extensions, mapping a method name to the extension
|
38
|
+
# module.
|
39
|
+
# @param map [Hash] hash mapping methods to extension modules
|
40
|
+
# @return [void]
|
41
|
+
def extension(map)
|
42
|
+
map.each do |sym, mod|
|
43
|
+
define_extension_method(sym, mod)
|
44
|
+
end
|
45
|
+
end
|
28
46
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
47
|
+
private
|
48
|
+
|
49
|
+
# Defines a method returning an extension proxy for the given module
|
50
|
+
# @param sym [Symbol] method name
|
51
|
+
# @param mod [Module] extension module
|
52
|
+
# @return [void]
|
53
|
+
def define_extension_method(sym, mod)
|
54
|
+
define_method(sym) do
|
55
|
+
(@extension_proxies ||= {})[mod] ||= ExtensionProxy.new(self, mod)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Initializes the renderer and evaulates the given template in the
|
61
|
+
# renderer's scope.
|
62
|
+
#
|
63
|
+
# @param &template [Proc] template block
|
33
64
|
def initialize(&template)
|
34
65
|
@buffer = +''
|
35
66
|
instance_eval(&template)
|
36
67
|
end
|
37
68
|
|
38
|
-
# Returns the
|
69
|
+
# Returns the rendered template.
|
70
|
+
#
|
39
71
|
# @return [String]
|
40
72
|
def to_s
|
41
73
|
@buffer
|
42
74
|
end
|
43
75
|
|
44
|
-
def escape_text(text)
|
45
|
-
raise NotImplementedError
|
46
|
-
end
|
47
|
-
|
48
|
-
def escape_uri(uri)
|
49
|
-
EscapeUtils.escape_uri(v)
|
50
|
-
end
|
51
|
-
|
52
76
|
S_TAG_METHOD_LINE = __LINE__ + 1
|
53
77
|
S_TAG_METHOD = <<~EOF
|
54
78
|
S_TAG_%<TAG>s_PRE = '<%<tag>s'.tr('_', '-')
|
55
79
|
S_TAG_%<TAG>s_CLOSE = '</%<tag>s>'.tr('_', '-')
|
56
80
|
|
57
81
|
def %<tag>s(text = nil, **props, &block)
|
82
|
+
if text.is_a?(Hash) && props.empty?
|
83
|
+
props = text
|
84
|
+
text = nil
|
85
|
+
end
|
86
|
+
|
58
87
|
@buffer << S_TAG_%<TAG>s_PRE
|
59
88
|
emit_props(props) unless props.empty?
|
60
89
|
|
@@ -74,33 +103,42 @@ module Papercraft
|
|
74
103
|
end
|
75
104
|
EOF
|
76
105
|
|
77
|
-
# Catches undefined tag method call and handles
|
106
|
+
# Catches undefined tag method call and handles it by defining the method.
|
107
|
+
#
|
78
108
|
# @param sym [Symbol] HTML tag or component identifier
|
79
|
-
# @param args [Array] method
|
80
|
-
# @param
|
109
|
+
# @param args [Array] method arguments
|
110
|
+
# @param opts [Hash] named method arguments
|
111
|
+
# @param &block [Proc] block passed to method
|
81
112
|
# @return [void]
|
82
113
|
def method_missing(sym, *args, **opts, &block)
|
83
|
-
value = @local && @local[sym]
|
84
|
-
return value if value
|
85
|
-
|
86
114
|
tag = sym.to_s
|
87
115
|
code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
|
88
116
|
self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
|
89
117
|
send(sym, *args, **opts, &block)
|
90
118
|
end
|
91
119
|
|
92
|
-
# Emits the given object into the rendering buffer
|
120
|
+
# Emits the given object into the rendering buffer. If the given object is a
|
121
|
+
# proc or a component, `emit` will passes any additional arguments and named
|
122
|
+
# arguments to the object when rendering it. If the given object is nil,
|
123
|
+
# nothing is emitted. Otherwise, the object is converted into a string using
|
124
|
+
# `#to_s` which is then added to the rendering buffer, without any escaping.
|
125
|
+
#
|
126
|
+
# greeter = proc { |name| h1 "Hello, #{name}!" }
|
127
|
+
# H { emit(greeter, 'world') }.render #=> "<h1>Hello, world!</h1>"
|
128
|
+
#
|
129
|
+
# H { emit 'hi&<bye>' }.render #=> "hi&<bye>"
|
130
|
+
#
|
131
|
+
# H { emit nil }.render #=> ""
|
132
|
+
#
|
93
133
|
# @param o [Proc, Papercraft::Component, String] emitted object
|
134
|
+
# @param *a [Array<any>] arguments to pass to a proc
|
135
|
+
# @param **b [Hash] named arguments to pass to a proc
|
94
136
|
# @return [void]
|
95
137
|
def emit(o, *a, **b)
|
96
138
|
case o
|
97
139
|
when ::Proc
|
98
140
|
Renderer.verify_proc_parameters(o, a, b)
|
99
141
|
instance_exec(*a, **b, &o)
|
100
|
-
# when Papercraft::Component
|
101
|
-
# o = o.template
|
102
|
-
# Renderer.verify_proc_parameters(o, a, b)
|
103
|
-
# instance_exec(*a, **b, &o)
|
104
142
|
when nil
|
105
143
|
else
|
106
144
|
@buffer << o.to_s
|
@@ -108,14 +146,15 @@ module Papercraft
|
|
108
146
|
end
|
109
147
|
alias_method :e, :emit
|
110
148
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
149
|
+
# Emits a block supplied using `Component#apply` or `Component#render`.
|
150
|
+
#
|
151
|
+
# div_wrap = H { |*args| div { emit_yield(*args) } }
|
152
|
+
# greeter = div_wrap.apply { |name| h1 "Hello, #{name}!" }
|
153
|
+
# greeter.render('world') #=> "<div><h1>Hello, world!</h1></div>"
|
154
|
+
#
|
155
|
+
# @param *a [Array<any>] arguments to pass to a proc
|
156
|
+
# @param **b [Hash] named arguments to pass to a proc
|
157
|
+
# @return [void]
|
119
158
|
def emit_yield(*a, **b)
|
120
159
|
raise Papercraft::Error, "No block given" unless @inner_block
|
121
160
|
|
@@ -131,6 +170,31 @@ module Papercraft
|
|
131
170
|
S_EQUAL_QUOTE = '="'
|
132
171
|
S_QUOTE = '"'
|
133
172
|
|
173
|
+
# Emits text into the rendering buffer, escaping any special characters to
|
174
|
+
# the respective HTML entities.
|
175
|
+
#
|
176
|
+
# @param data [String] text
|
177
|
+
# @return [void]
|
178
|
+
def text(data)
|
179
|
+
@buffer << escape_text(data)
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
# Escapes text. This method must be overriden in descendant classes.
|
185
|
+
def escape_text(text)
|
186
|
+
raise NotImplementedError
|
187
|
+
end
|
188
|
+
|
189
|
+
# Sets up a block to be called with `#emit_yield`
|
190
|
+
def with_block(block, &run_block)
|
191
|
+
old_block = @inner_block
|
192
|
+
@inner_block = block
|
193
|
+
instance_eval(&run_block)
|
194
|
+
ensure
|
195
|
+
@inner_block = old_block
|
196
|
+
end
|
197
|
+
|
134
198
|
# Emits tag attributes into the rendering buffer
|
135
199
|
# @param props [Hash] tag attributes
|
136
200
|
# @return [void]
|
@@ -153,23 +217,25 @@ module Papercraft
|
|
153
217
|
end
|
154
218
|
}
|
155
219
|
end
|
156
|
-
|
157
|
-
# Emits text into the rendering buffer
|
158
|
-
# @param data [String] text
|
159
|
-
def text(data)
|
160
|
-
@buffer << escape_text(data)
|
161
|
-
end
|
162
220
|
end
|
163
221
|
|
222
|
+
# Implements an HTML renderer
|
164
223
|
class HTMLRenderer < Renderer
|
165
224
|
include HTML
|
166
225
|
|
226
|
+
private
|
227
|
+
|
228
|
+
# Escapes the given text using HTML entities.
|
167
229
|
def escape_text(text)
|
168
230
|
EscapeUtils.escape_html(text.to_s)
|
169
231
|
end
|
170
232
|
end
|
171
233
|
|
234
|
+
# Implements an XML renderer
|
172
235
|
class XMLRenderer < Renderer
|
236
|
+
private
|
237
|
+
|
238
|
+
# Escapes the given text using XML entities.
|
173
239
|
def escape_text(text)
|
174
240
|
EscapeUtils.escape_xml(text.to_s)
|
175
241
|
end
|
data/lib/papercraft/version.rb
CHANGED
data/lib/papercraft.rb
CHANGED
@@ -11,24 +11,44 @@ require_relative 'papercraft/encoding'
|
|
11
11
|
module Papercraft
|
12
12
|
# Exception class used to signal templating-related errors
|
13
13
|
class Error < RuntimeError; end
|
14
|
+
|
15
|
+
# Installs one or more extensions. Extensions enhance templating capabilities
|
16
|
+
# by adding namespaced methods to emplates. An extension is implemented as a
|
17
|
+
# Ruby module containing one or more methods. Each method in the extension
|
18
|
+
# module can be used to render a specific HTML element or a set of elements.
|
19
|
+
def self.extension(map)
|
20
|
+
Renderer.extension(map)
|
21
|
+
end
|
14
22
|
end
|
15
23
|
|
16
24
|
# Kernel extensions
|
17
25
|
module ::Kernel
|
18
|
-
|
19
|
-
#
|
26
|
+
|
27
|
+
# Creates a new papercraft component. `#H` can take either a proc argument or
|
28
|
+
# a block. In both cases, the proc is converted to a `Papercraft::Component`.
|
29
|
+
#
|
30
|
+
# H(proc { h1 'hi' }).render #=> "<h1>hi</h1>"
|
31
|
+
# H { h1 'hi' }.render #=> "<h1>hi</h1>"
|
32
|
+
#
|
20
33
|
# @param template [Proc] template block
|
21
|
-
# @return [Papercraft] Papercraft
|
22
|
-
def H(&template)
|
23
|
-
Papercraft::Component
|
34
|
+
# @return [Papercraft::Component] Papercraft component
|
35
|
+
def H(o = nil, &template)
|
36
|
+
return o if o.is_a?(Papercraft::Component)
|
37
|
+
template ||= o
|
38
|
+
Papercraft::Component.new(mode: :html, &template)
|
24
39
|
end
|
25
40
|
|
26
|
-
|
41
|
+
# Creates a new papercraft component in XML mode. `#X` can take either a proc argument or
|
42
|
+
# a block. In both cases, the proc is converted to a `Papercraft::Component`.
|
43
|
+
#
|
44
|
+
# X(proc { item 'foo' }).render #=> "<item>foo</item>"
|
45
|
+
# X { item 'foo' }.render #=> "<item>foo</item>"
|
46
|
+
#
|
47
|
+
# @param template [Proc] template block
|
48
|
+
# @return [Papercraft::Component] Papercraft component
|
49
|
+
def X(o = nil, &template)
|
50
|
+
return o if o.is_a?(Papercraft::Component)
|
51
|
+
template ||= o
|
27
52
|
Papercraft::Component.new(mode: :xml, &template)
|
28
53
|
end
|
29
54
|
end
|
30
|
-
|
31
|
-
# Object extensions
|
32
|
-
class Object
|
33
|
-
include Papercraft::Encoding
|
34
|
-
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: papercraft
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: escape_utils
|
@@ -24,6 +24,48 @@ dependencies:
|
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.2.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: kramdown
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.3.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rouge
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.26.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.26.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: kramdown-parser-gfm
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.1.0
|
27
69
|
- !ruby/object:Gem::Dependency
|
28
70
|
name: minitest
|
29
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,7 +122,7 @@ dependencies:
|
|
80
122
|
- - '='
|
81
123
|
- !ruby/object:Gem::Version
|
82
124
|
version: 2.0.9
|
83
|
-
description:
|
125
|
+
description:
|
84
126
|
email: sharon@noteflakes.com
|
85
127
|
executables: []
|
86
128
|
extensions: []
|
@@ -93,6 +135,7 @@ files:
|
|
93
135
|
- lib/papercraft/compiler.rb
|
94
136
|
- lib/papercraft/component.rb
|
95
137
|
- lib/papercraft/encoding.rb
|
138
|
+
- lib/papercraft/extension_proxy.rb
|
96
139
|
- lib/papercraft/html.rb
|
97
140
|
- lib/papercraft/renderer.rb
|
98
141
|
- lib/papercraft/version.rb
|
@@ -104,7 +147,7 @@ metadata:
|
|
104
147
|
documentation_uri: https://www.rubydoc.info/gems/papercraft
|
105
148
|
homepage_uri: https://github.com/digital-fabric/papercraft
|
106
149
|
changelog_uri: https://github.com/digital-fabric/papercraft/blob/master/CHANGELOG.md
|
107
|
-
post_install_message:
|
150
|
+
post_install_message:
|
108
151
|
rdoc_options:
|
109
152
|
- "--title"
|
110
153
|
- Papercraft
|
@@ -123,8 +166,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
166
|
- !ruby/object:Gem::Version
|
124
167
|
version: '0'
|
125
168
|
requirements: []
|
126
|
-
rubygems_version: 3.
|
127
|
-
signing_key:
|
169
|
+
rubygems_version: 3.2.32
|
170
|
+
signing_key:
|
128
171
|
specification_version: 4
|
129
172
|
summary: 'Papercraft: component-based HTML templating for Ruby'
|
130
173
|
test_files: []
|