papercraft 0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +48 -0
- data/README.md +437 -0
- data/lib/papercraft/compiler.rb +428 -0
- data/lib/papercraft/component.rb +82 -0
- data/lib/papercraft/encoding.rb +26 -0
- data/lib/papercraft/html.rb +38 -0
- data/lib/papercraft/renderer.rb +177 -0
- data/lib/papercraft/version.rb +5 -0
- data/lib/papercraft.rb +34 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4404d5b6a84318eab12f0c7d13b4c3be4355c8ebb36b6a3fbd65c6f62af8f361
|
4
|
+
data.tar.gz: 175a60878f6770ae52d956b6e7c72326af5a0db9faefecae2bd2c3e2fb406f97
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 307d3e37cb946b0adb54fa2d6b2939ec0016cc81205f3b4d4dfb6c01d57b65b1f609024b0499f6b5717c858b6dc5aea472f0b82585df44f761f4ac9fdb8c2eae
|
7
|
+
data.tar.gz: 96162c1707785e3d54ce6cb55bd9a5801658b92d294e28b4168b78ee68e49027ec945eab13d47142382e458fdd33c6d26182a7a08bfc403038c170a8f119984f
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
## 0.8 2021-12-22
|
2
|
+
|
3
|
+
- Cleanup and refactor code
|
4
|
+
- Add X global method for XML templates
|
5
|
+
- Make `Component` a descendant of `Proc`
|
6
|
+
- Introduce new component API
|
7
|
+
- Rename Rubyoshka to Papercraft
|
8
|
+
- Convert underscores to dashes for tag and attribute names (@jaredcwhite)
|
9
|
+
|
10
|
+
## 0.7 2021-09-29
|
11
|
+
|
12
|
+
- Add `#emit_yield` for rendering layouts
|
13
|
+
- Add experimental template compilation (WIP)
|
14
|
+
|
15
|
+
## 0.6.1 2021-03-03
|
16
|
+
|
17
|
+
- Remove support for Ruby 2.6
|
18
|
+
|
19
|
+
## 0.6 2021-03-03
|
20
|
+
|
21
|
+
- Fix Rubyoshka on Ruby 3.0
|
22
|
+
- Refactor and add more tests
|
23
|
+
|
24
|
+
## 0.5 2021-02-27
|
25
|
+
|
26
|
+
- Add support for rendering XML
|
27
|
+
- Add Rubyoshka.component method
|
28
|
+
- Remove Modulation dependency
|
29
|
+
|
30
|
+
## 0.4 2019-02-05
|
31
|
+
|
32
|
+
- Add support for emitting component modules
|
33
|
+
|
34
|
+
## 0.3 2019-01-13
|
35
|
+
|
36
|
+
- Implement caching
|
37
|
+
- Improve performance
|
38
|
+
- Handle attributes with `false` value correctly
|
39
|
+
|
40
|
+
## 0.2 2019-01-07
|
41
|
+
|
42
|
+
- Better documentation
|
43
|
+
- Fix #text
|
44
|
+
- Add local context
|
45
|
+
|
46
|
+
## 0.1 2019-01-06
|
47
|
+
|
48
|
+
- First working version
|
data/README.md
ADDED
@@ -0,0 +1,437 @@
|
|
1
|
+
# Papercraft - Composable HTML templating for Ruby
|
2
|
+
|
3
|
+
[INSTALL](#installing-papercraft) |
|
4
|
+
[TUTORIAL](#getting-started) |
|
5
|
+
[EXAMPLES](examples) |
|
6
|
+
[REFERENCE](#api-reference)
|
7
|
+
|
8
|
+
## What is Papercraft?
|
9
|
+
|
10
|
+
Papercraft is an HTML templating engine for Ruby that offers the following
|
11
|
+
features:
|
12
|
+
|
13
|
+
- HTML templating using plain Ruby syntax
|
14
|
+
- Minimal boilerplate
|
15
|
+
- Mix logic and tags freely
|
16
|
+
- Use global and local contexts to pass values to reusable components
|
17
|
+
- Automatic HTML escaping
|
18
|
+
- Composable components
|
19
|
+
- 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.
|
28
|
+
|
29
|
+
## Installing Papercraft
|
30
|
+
|
31
|
+
Using bundler:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
gem 'papercraft'
|
35
|
+
```
|
36
|
+
|
37
|
+
Or manually:
|
38
|
+
|
39
|
+
```bash
|
40
|
+
$ gem install papercraft
|
41
|
+
```
|
42
|
+
|
43
|
+
## Getting started
|
44
|
+
|
45
|
+
To use Papercraft in your code just require it:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'papercraft'
|
49
|
+
```
|
50
|
+
|
51
|
+
To create a template use `Papercraft.new` or the global method `Kernel#H`:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# can also use Papercraft.new
|
55
|
+
html = H {
|
56
|
+
div { p 'hello' }
|
57
|
+
}
|
58
|
+
```
|
59
|
+
|
60
|
+
## Rendering a template
|
61
|
+
|
62
|
+
To render a Papercraft template use the `#render` method:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
H { span 'best span' }.render #=> "<span>best span</span>"
|
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>"
|
76
|
+
```
|
77
|
+
|
78
|
+
## All about tags
|
79
|
+
|
80
|
+
Tags are added using unqualified method calls, and are nested using blocks:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
H {
|
84
|
+
html {
|
85
|
+
head {
|
86
|
+
title 'page title'
|
87
|
+
}
|
88
|
+
body {
|
89
|
+
article {
|
90
|
+
h1 'article title'
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
```
|
96
|
+
|
97
|
+
Tag methods accept a string argument, a block, or no argument at all:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
H { p 'hello' }.render #=> "<p>hello</p>"
|
101
|
+
|
102
|
+
H { p { span '1'; span '2' } }.render #=> "<p><span>1</span><span>2</span></p>"
|
103
|
+
|
104
|
+
H { hr() }.render #=> "<hr/>"
|
105
|
+
```
|
106
|
+
|
107
|
+
Tag methods also accept tag attributes, given as a hash:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
H { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>
|
111
|
+
|
112
|
+
H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
|
113
|
+
```
|
114
|
+
|
115
|
+
## Template parameters
|
116
|
+
|
117
|
+
Template parameters are specified as block parameters, and are passed to the
|
118
|
+
template on rendering:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
greeting = H { |name| h1 "Hello, #{name}!" }
|
122
|
+
greeting.render('world') #=> "<h1>Hello, world!</h1>"
|
123
|
+
```
|
124
|
+
|
125
|
+
Templates can also accept named parameters:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
greeting = H { |name:| h1 "Hello, #{name}!" }
|
129
|
+
greeting.render(name: 'world') #=> "<h1>Hello, world!</h1>"
|
130
|
+
```
|
131
|
+
|
132
|
+
## Logic in templates
|
133
|
+
|
134
|
+
Since Papercraft templates are just a bunch of Ruby, you can easily write your
|
135
|
+
view logic right in the template:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
H { |user = nil|
|
139
|
+
if user
|
140
|
+
span "Hello, #{user.name}!"
|
141
|
+
else
|
142
|
+
span "Hello, guest!"
|
143
|
+
end
|
144
|
+
}
|
145
|
+
```
|
146
|
+
|
147
|
+
## Template blocks
|
148
|
+
|
149
|
+
Templates can also accept and render blocks by using `emit_yield`:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
page = H {
|
153
|
+
html {
|
154
|
+
body { emit_yield }
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
# we pass the inner HTML
|
159
|
+
page.render { h1 'hi' }
|
160
|
+
```
|
161
|
+
|
162
|
+
## Plain procs as components
|
163
|
+
|
164
|
+
With Papercraft you can write a template as a plain Ruby proc, and later render
|
165
|
+
it by passing it as a block to `H`:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
greeting = proc { |name| h1 "Hello, #{name}!" }
|
169
|
+
H(&greeting).render('world')
|
170
|
+
```
|
171
|
+
|
172
|
+
Components can also be expressed using lambda notation:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
greeting = ->(name) { h1 "Hello, #{name}!" }
|
176
|
+
H(&greeting).render('world')
|
177
|
+
```
|
178
|
+
|
179
|
+
## Component composition
|
180
|
+
|
181
|
+
Papercraft makes it easy to compose multiple components into a whole HTML
|
182
|
+
document. A Papercraft component can contain other components, as the following
|
183
|
+
example shows.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
Title = ->(title) { h1 title }
|
187
|
+
|
188
|
+
Item = ->(id:, text:, checked:) {
|
189
|
+
li {
|
190
|
+
input name: id, type: 'checkbox', checked: checked
|
191
|
+
label text, for: id
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
ItemList = ->(items) {
|
196
|
+
ul {
|
197
|
+
items.each { |i|
|
198
|
+
Item(**i)
|
199
|
+
}
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
page = H { |title, items|
|
204
|
+
html5 {
|
205
|
+
head { Title(title) }
|
206
|
+
body { ItemList(items) }
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
page.render('Hello from components', [
|
211
|
+
{ id: 1, text: 'foo', checked: false },
|
212
|
+
{ id: 2, text: 'bar', checked: true }
|
213
|
+
])
|
214
|
+
```
|
215
|
+
|
216
|
+
In addition to using components defined as constants, you can also use
|
217
|
+
non-constant components by invoking the `#emit` method:
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
greeting = -> { span "Hello, world" }
|
221
|
+
|
222
|
+
H {
|
223
|
+
div {
|
224
|
+
emit greeting
|
225
|
+
}
|
226
|
+
}
|
227
|
+
```
|
228
|
+
|
229
|
+
## Parameter and block application
|
230
|
+
|
231
|
+
Parameters and blocks can be applied to a template without it being rendered, by
|
232
|
+
using `#apply`. This mechanism is what allows component composition and the
|
233
|
+
creation of higher-order components.
|
234
|
+
|
235
|
+
The `#apply` method returns a new component which applies the given parameters and
|
236
|
+
or block to the original component:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
# parameter application
|
240
|
+
hello = H { |name| h1 "Hello, #{name}!" }
|
241
|
+
hello_world = hello.apply('world')
|
242
|
+
hello_world.render #=> "<h1>Hello, world!</h1>"
|
243
|
+
|
244
|
+
# block application
|
245
|
+
div_wrap = H { div { emit_yield } }
|
246
|
+
wrapped_h1 = div_wrap.apply { h1 'hi' }
|
247
|
+
wrapped_h1.render #=> "<div><h1>hi</h1></div>"
|
248
|
+
|
249
|
+
# wrap a component
|
250
|
+
wrapped_hello_world = div_wrap.apply(&hello_world)
|
251
|
+
wrapped_hello_world.render #=> "<div><h1>Hello, world!</h1></div>"
|
252
|
+
```
|
253
|
+
|
254
|
+
## Higher-order components
|
255
|
+
|
256
|
+
Papercraft also lets you create higher-order components (HOCs), that is,
|
257
|
+
components that take other components as parameters, or as blocks. Higher-order
|
258
|
+
components are handy for creating layouts, wrapping components in arbitrary
|
259
|
+
markup, enhancing components or injecting component parameters.
|
260
|
+
|
261
|
+
Here is a HOC that takes a component as parameter:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
div_wrap = H { |inner| div { emit inner } }
|
265
|
+
greeter = H { h1 'hi' }
|
266
|
+
wrapped_greeter = div_wrap.apply(greeter)
|
267
|
+
wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
|
268
|
+
```
|
269
|
+
|
270
|
+
The inner component can also be passed as a block, as shown above:
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
div_wrap = H { div { emit_yield } }
|
274
|
+
wrapped_greeter = div_wrap.apply { h1 'hi' }
|
275
|
+
wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
|
276
|
+
```
|
277
|
+
|
278
|
+
## Layout template composition
|
279
|
+
|
280
|
+
One of the principal uses of higher-order components is the creation of nested
|
281
|
+
layouts. Suppose we have a website with a number of different layouts, and we'd
|
282
|
+
like to avoid having to repeat the same code in the different layouts. We can do
|
283
|
+
this by creating a `default` page template that takes a block, then use `#apply`
|
284
|
+
to create the other templates:
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
default_layout = H { |**params|
|
288
|
+
html5 {
|
289
|
+
head {
|
290
|
+
title: params[:title]
|
291
|
+
}
|
292
|
+
body {
|
293
|
+
emit_yield(**params)
|
294
|
+
}
|
295
|
+
}
|
296
|
+
}
|
297
|
+
|
298
|
+
article_layout = default_layout.apply { |title:, body:|
|
299
|
+
article {
|
300
|
+
h1 title
|
301
|
+
emit_markdown body
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
article_layout.render(
|
306
|
+
title: 'This is a title',
|
307
|
+
body: 'Hello from *markdown body*'
|
308
|
+
)
|
309
|
+
```
|
310
|
+
|
311
|
+
## Emitting raw HTML
|
312
|
+
|
313
|
+
Raw HTML can be emitted using `#emit`:
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
wrapped = H { |html| div { emit html } }
|
317
|
+
wrapped.render("<h1>hi</h1>") #=> "<div><h1>hi</h1></div>"
|
318
|
+
```
|
319
|
+
|
320
|
+
## Emitting a string with HTML Encoding
|
321
|
+
|
322
|
+
To emit a string with proper HTML encoding, without wrapping it in an HTML
|
323
|
+
element, use `#text`:
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
H { str 'hi&lo' }.render #=> "hi&lo"
|
327
|
+
```
|
328
|
+
|
329
|
+
## Emitting Markdown
|
330
|
+
|
331
|
+
To emit Markdown, use `#emit_markdown`:
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
template = H { |md| div { emit_markdown md } }
|
335
|
+
template.render("Here's some *Markdown*") #=> "<div>Here's some <em>Markdown</em></div>"
|
336
|
+
```
|
337
|
+
|
338
|
+
## Some interesting use cases
|
339
|
+
|
340
|
+
Papercraft opens up all kinds of new possibilities when it comes to putting
|
341
|
+
together pieces of HTML. Feel free to explore the API!
|
342
|
+
|
343
|
+
### A higher-order list component
|
344
|
+
|
345
|
+
Here's another demonstration of a higher-order component, a list component that
|
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:
|
348
|
+
|
349
|
+
```ruby
|
350
|
+
List = ->(items, item_component) {
|
351
|
+
H {
|
352
|
+
ul {
|
353
|
+
items.each { |item|
|
354
|
+
with(item: item) {
|
355
|
+
li { emit item_component }
|
356
|
+
}
|
357
|
+
}
|
358
|
+
}
|
359
|
+
}
|
360
|
+
}
|
361
|
+
|
362
|
+
TodoItem = H {
|
363
|
+
span item.text, class: item.completed ? 'completed' : 'pending'
|
364
|
+
}
|
365
|
+
|
366
|
+
def todo_list(items)
|
367
|
+
H {
|
368
|
+
div { List(items, TodoItem) }
|
369
|
+
}
|
370
|
+
end
|
371
|
+
```
|
372
|
+
|
373
|
+
## API Reference
|
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
|
410
|
+
|
411
|
+
Caches the markup in the given block, storing it in the Papercraft cache store.
|
412
|
+
If a cache entry for the given block is found, it will be used instead of
|
413
|
+
invoking the block. If one or more variables given, those will be used to create
|
414
|
+
a separate cache entry.
|
415
|
+
|
416
|
+
#### `#context`
|
417
|
+
|
418
|
+
Accesses the [global context](#global-context).
|
419
|
+
|
420
|
+
#### `#emit(object)` a.k.a. `#e(object)`
|
421
|
+
|
422
|
+
- `object`: `Proc`, `Papercraft` instance or `String`
|
423
|
+
|
424
|
+
Adds the given object to the current template. If a `String` is given, it is
|
425
|
+
rendered verbatim, i.e. without escaping.
|
426
|
+
|
427
|
+
#### `html5(&block)`
|
428
|
+
|
429
|
+
- `block`: inner HTML block
|
430
|
+
|
431
|
+
Adds an HTML5 `doctype` tag, followed by an `html` tag with the given block.
|
432
|
+
|
433
|
+
#### `#text(data)`
|
434
|
+
|
435
|
+
- `data` - text to add
|
436
|
+
|
437
|
+
Adds text without wrapping it in a tag. The text will be escaped.
|
@@ -0,0 +1,428 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Papercraft
|
4
|
+
# The Compiler class compiles Papercraft templates
|
5
|
+
class Compiler
|
6
|
+
DEFAULT_CODE_BUFFER_CAPACITY = 8192
|
7
|
+
DEFAULT_EMIT_BUFFER_CAPACITY = 4096
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@level = 1
|
11
|
+
@code_buffer = String.new(capacity: DEFAULT_CODE_BUFFER_CAPACITY)
|
12
|
+
end
|
13
|
+
|
14
|
+
def emit_output
|
15
|
+
@output_mode = true
|
16
|
+
yield
|
17
|
+
@output_mode = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def emit_code_line_break
|
21
|
+
return if @code_buffer.empty?
|
22
|
+
|
23
|
+
@code_buffer << "\n" if @code_buffer[-1] != "\n"
|
24
|
+
@line_break = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def emit_literal(lit)
|
28
|
+
if @output_mode
|
29
|
+
emit_code_line_break if @line_break
|
30
|
+
@emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
|
31
|
+
@emit_buffer << lit
|
32
|
+
else
|
33
|
+
emit_code(lit)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def emit_text(str, encoding: :html)
|
38
|
+
emit_code_line_break if @line_break
|
39
|
+
@emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
|
40
|
+
@emit_buffer << encode(str, encoding).inspect[1..-2]
|
41
|
+
end
|
42
|
+
|
43
|
+
def encode(str, encoding)
|
44
|
+
case encoding
|
45
|
+
when :html
|
46
|
+
__html_encode__(str)
|
47
|
+
when :uri
|
48
|
+
__uri_encode__(str)
|
49
|
+
else
|
50
|
+
raise "Invalid encoding #{encoding.inspect}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def emit_expression
|
55
|
+
if @output_mode
|
56
|
+
emit_literal('#{__html_encode__(')
|
57
|
+
yield
|
58
|
+
emit_literal(')}')
|
59
|
+
else
|
60
|
+
yield
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def flush_emit_buffer
|
65
|
+
return if !@emit_buffer
|
66
|
+
|
67
|
+
@code_buffer << "#{' ' * @level}__buffer__ << \"#{@emit_buffer}\"\n"
|
68
|
+
@emit_buffer = nil
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def emit_code(code)
|
73
|
+
if flush_emit_buffer || @line_break
|
74
|
+
emit_code_line_break if @line_break
|
75
|
+
@code_buffer << "#{' ' * @level}#{code}"
|
76
|
+
else
|
77
|
+
if @code_buffer.empty? || (@code_buffer[-1] == "\n")
|
78
|
+
@code_buffer << "#{' ' * @level}#{code}"
|
79
|
+
else
|
80
|
+
@code_buffer << "#{code}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def compile(template)
|
86
|
+
@block = template.to_proc
|
87
|
+
ast = RubyVM::AbstractSyntaxTree.of(@block)
|
88
|
+
# Compiler.pp_ast(ast)
|
89
|
+
parse(ast)
|
90
|
+
flush_emit_buffer
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_reader :code_buffer
|
95
|
+
|
96
|
+
def to_code
|
97
|
+
"->(__buffer__, __context__) do\n#{@code_buffer}end"
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_proc
|
101
|
+
@block.binding.eval(to_code)
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse(node)
|
105
|
+
@line_break = @last_node && node.first_lineno != @last_node.first_lineno
|
106
|
+
@last_node = node
|
107
|
+
# puts "- parse(#{node.type}) (break: #{@line_break.inspect})"
|
108
|
+
send(:"parse_#{node.type.downcase}", node)
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_scope(node)
|
112
|
+
parse(node.children[2])
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_iter(node)
|
116
|
+
call, scope = node.children
|
117
|
+
if call.type == :FCALL
|
118
|
+
parse_fcall(call, scope)
|
119
|
+
else
|
120
|
+
parse(call)
|
121
|
+
emit_code(" do")
|
122
|
+
args = scope.children[0]
|
123
|
+
emit_code(" |#{args.join(', ')}|") if args
|
124
|
+
emit_code("\n")
|
125
|
+
@level += 1
|
126
|
+
parse(scope)
|
127
|
+
flush_emit_buffer
|
128
|
+
@level -= 1
|
129
|
+
emit_code("end\n")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_ivar(node)
|
134
|
+
ivar = node.children.first.match(/^@(.+)*/)[1]
|
135
|
+
emit_literal("__context__[:#{ivar}]")
|
136
|
+
end
|
137
|
+
|
138
|
+
def parse_fcall(node, block = nil)
|
139
|
+
tag, args = node.children
|
140
|
+
args = args.children.compact if args
|
141
|
+
text = fcall_inner_text_from_args(args)
|
142
|
+
atts = fcall_attributes_from_args(args)
|
143
|
+
if block
|
144
|
+
emit_tag(tag, atts) { parse(block) }
|
145
|
+
elsif text
|
146
|
+
emit_tag(tag, atts) do
|
147
|
+
emit_output do
|
148
|
+
if text.is_a?(String)
|
149
|
+
emit_text(text)
|
150
|
+
else
|
151
|
+
emit_expression { parse(text) }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
else
|
156
|
+
emit_tag(tag, atts)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def fcall_inner_text_from_args(args)
|
161
|
+
return nil if !args
|
162
|
+
|
163
|
+
first = args.first
|
164
|
+
case first.type
|
165
|
+
when :STR
|
166
|
+
first.children.first
|
167
|
+
when :LIT
|
168
|
+
first.children.first.to_s
|
169
|
+
when :HASH
|
170
|
+
nil
|
171
|
+
else
|
172
|
+
first
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def fcall_attributes_from_args(args)
|
177
|
+
return nil if !args
|
178
|
+
|
179
|
+
last = args.last
|
180
|
+
(last.type == :HASH) ? last : nil
|
181
|
+
end
|
182
|
+
|
183
|
+
def emit_tag(tag, atts, &block)
|
184
|
+
emit_output do
|
185
|
+
if atts
|
186
|
+
emit_literal("<#{tag}")
|
187
|
+
emit_tag_attributes(atts)
|
188
|
+
emit_literal(block ? '>' : '/>')
|
189
|
+
else
|
190
|
+
emit_literal(block ? "<#{tag}>" : "<#{tag}/>")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
if block
|
194
|
+
block.call
|
195
|
+
emit_output { emit_literal("</#{tag}>") }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def emit_tag_attributes(atts)
|
200
|
+
list = atts.children.first.children
|
201
|
+
while true
|
202
|
+
key = list.shift
|
203
|
+
break unless key
|
204
|
+
|
205
|
+
value = list.shift
|
206
|
+
value_type = value.type
|
207
|
+
case value_type
|
208
|
+
when :FALSE, :NIL
|
209
|
+
next
|
210
|
+
end
|
211
|
+
|
212
|
+
emit_literal(' ')
|
213
|
+
emit_tag_attribute_key(key)
|
214
|
+
next if value_type == :TRUE
|
215
|
+
|
216
|
+
emit_literal('=\"')
|
217
|
+
emit_tag_attribute_value(value, key)
|
218
|
+
emit_literal('\"')
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def emit_tag_attribute_key(key)
|
223
|
+
case key.type
|
224
|
+
when :STR
|
225
|
+
emit_literal(key.children.first)
|
226
|
+
when :LIT
|
227
|
+
emit_literal(key.children.first.to_s)
|
228
|
+
when :NIL
|
229
|
+
emit_literal('nil')
|
230
|
+
else
|
231
|
+
emit_expression { parse(key) }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def emit_tag_attribute_value(value, key)
|
236
|
+
case value.type
|
237
|
+
when :STR
|
238
|
+
encoding = (key.type == :LIT) && (key.children.first == :href) ? :uri : :html
|
239
|
+
emit_text(value.children.first, encoding: encoding)
|
240
|
+
when :LIT
|
241
|
+
emit_text(value.children.first.to_s)
|
242
|
+
else
|
243
|
+
parse(value)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def parse_call(node)
|
248
|
+
receiver, method, args = node.children
|
249
|
+
if receiver.type == :VCALL && receiver.children == [:context]
|
250
|
+
emit_literal('__context__')
|
251
|
+
else
|
252
|
+
parse(receiver)
|
253
|
+
end
|
254
|
+
if method == :[]
|
255
|
+
emit_literal('[')
|
256
|
+
args = args.children.compact
|
257
|
+
while true
|
258
|
+
arg = args.shift
|
259
|
+
break unless arg
|
260
|
+
|
261
|
+
parse(arg)
|
262
|
+
emit_literal(', ') if !args.empty?
|
263
|
+
end
|
264
|
+
emit_literal(']')
|
265
|
+
else
|
266
|
+
emit_literal('.')
|
267
|
+
emit_literal(method.to_s)
|
268
|
+
if args
|
269
|
+
emit_literal('(')
|
270
|
+
args = args.children.compact
|
271
|
+
while true
|
272
|
+
arg = args.shift
|
273
|
+
break unless arg
|
274
|
+
|
275
|
+
parse(arg)
|
276
|
+
emit_literal(', ') if !args.empty?
|
277
|
+
end
|
278
|
+
emit_literal(')')
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def parse_str(node)
|
284
|
+
str = node.children.first
|
285
|
+
emit_literal(str.inspect)
|
286
|
+
end
|
287
|
+
|
288
|
+
def parse_lit(node)
|
289
|
+
value = node.children.first
|
290
|
+
emit_literal(value.inspect)
|
291
|
+
end
|
292
|
+
|
293
|
+
def parse_true(node)
|
294
|
+
emit_expression { emit_literal('true') }
|
295
|
+
end
|
296
|
+
|
297
|
+
def parse_false(node)
|
298
|
+
emit_expression { emit_literal('true') }
|
299
|
+
end
|
300
|
+
|
301
|
+
def parse_list(node)
|
302
|
+
emit_literal('[')
|
303
|
+
items = node.children.compact
|
304
|
+
while true
|
305
|
+
item = items.shift
|
306
|
+
break unless item
|
307
|
+
|
308
|
+
parse(item)
|
309
|
+
emit_literal(', ') if !items.empty?
|
310
|
+
end
|
311
|
+
emit_literal(']')
|
312
|
+
end
|
313
|
+
|
314
|
+
def parse_vcall(node)
|
315
|
+
tag = node.children.first
|
316
|
+
emit_tag(tag, nil)
|
317
|
+
end
|
318
|
+
|
319
|
+
def parse_opcall(node)
|
320
|
+
left, op, right = node.children
|
321
|
+
parse(left)
|
322
|
+
emit_literal(" #{op} ")
|
323
|
+
right.children.compact.each { |c| parse(c) }
|
324
|
+
end
|
325
|
+
|
326
|
+
def parse_block(node)
|
327
|
+
node.children.each { |c| parse(c) }
|
328
|
+
end
|
329
|
+
|
330
|
+
def parse_if(node)
|
331
|
+
cond, then_branch, else_branch = node.children
|
332
|
+
if @output_mode
|
333
|
+
emit_if_output(cond, then_branch, else_branch)
|
334
|
+
else
|
335
|
+
emit_if_code(cond, then_branch, else_branch)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def parse_unless(node)
|
340
|
+
cond, then_branch, else_branch = node.children
|
341
|
+
if @output_mode
|
342
|
+
emit_unless_output(cond, then_branch, else_branch)
|
343
|
+
else
|
344
|
+
emit_unless_code(cond, then_branch, else_branch)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def emit_if_output(cond, then_branch, else_branch)
|
349
|
+
parse(cond)
|
350
|
+
emit_literal(" ? ")
|
351
|
+
parse(then_branch)
|
352
|
+
emit_literal(" : ")
|
353
|
+
if else_branch
|
354
|
+
parse(else_branch)
|
355
|
+
else
|
356
|
+
emit_literal(nil)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def emit_unless_output(cond, then_branch, else_branch)
|
361
|
+
parse(cond)
|
362
|
+
emit_literal(" ? ")
|
363
|
+
if else_branch
|
364
|
+
parse(else_branch)
|
365
|
+
else
|
366
|
+
emit_literal(nil)
|
367
|
+
end
|
368
|
+
emit_literal(" : ")
|
369
|
+
parse(then_branch)
|
370
|
+
end
|
371
|
+
|
372
|
+
def emit_if_code(cond, then_branch, else_branch)
|
373
|
+
emit_code('if ')
|
374
|
+
parse(cond)
|
375
|
+
emit_code("\n")
|
376
|
+
@level += 1
|
377
|
+
parse(then_branch)
|
378
|
+
flush_emit_buffer
|
379
|
+
@level -= 1
|
380
|
+
if else_branch
|
381
|
+
emit_code("else\n")
|
382
|
+
@level += 1
|
383
|
+
parse(else_branch)
|
384
|
+
flush_emit_buffer
|
385
|
+
@level -= 1
|
386
|
+
end
|
387
|
+
emit_code("end\n")
|
388
|
+
end
|
389
|
+
|
390
|
+
def emit_unless_code(cond, then_branch, else_branch)
|
391
|
+
emit_code('unless ')
|
392
|
+
parse(cond)
|
393
|
+
emit_code("\n")
|
394
|
+
@level += 1
|
395
|
+
parse(then_branch)
|
396
|
+
flush_emit_buffer
|
397
|
+
@level -= 1
|
398
|
+
if else_branch
|
399
|
+
emit_code("else\n")
|
400
|
+
@level += 1
|
401
|
+
parse(else_branch)
|
402
|
+
flush_emit_buffer
|
403
|
+
@level -= 1
|
404
|
+
end
|
405
|
+
emit_code("end\n")
|
406
|
+
end
|
407
|
+
|
408
|
+
def parse_dvar(node)
|
409
|
+
|
410
|
+
emit_literal(node.children.first.to_s)
|
411
|
+
end
|
412
|
+
|
413
|
+
def self.pp_ast(node, level = 0)
|
414
|
+
case node
|
415
|
+
when RubyVM::AbstractSyntaxTree::Node
|
416
|
+
puts "#{' ' * level}#{node.type.inspect}"
|
417
|
+
node.children.each { |c| pp_ast(c, level + 1) }
|
418
|
+
when Array
|
419
|
+
puts "#{' ' * level}["
|
420
|
+
node.each { |c| pp_ast(c, level + 1) }
|
421
|
+
puts "#{' ' * level}]"
|
422
|
+
else
|
423
|
+
puts "#{' ' * level}#{node.inspect}"
|
424
|
+
return
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './html'
|
4
|
+
|
5
|
+
module Papercraft
|
6
|
+
# Component represents a distinct, reusable HTML template. A component can
|
7
|
+
# include other components, and also be nested inside other components.
|
8
|
+
#
|
9
|
+
# Since in Papercraft HTML is expressed using blocks (or procs,) the Component
|
10
|
+
# class is simply a special kind of Proc, which has some enhanced
|
11
|
+
# capabilities, allowing it to be easily composed in a variety of ways.
|
12
|
+
|
13
|
+
# Components are usually created using the global methods `H` or `X`, for HTML
|
14
|
+
# or XML templates, respectively:
|
15
|
+
#
|
16
|
+
# greeter = H { |name| h1 "Hello, #{name}!" } greeter.render('world') #=>
|
17
|
+
# "<h1>Hello, world!</h1>"
|
18
|
+
#
|
19
|
+
# Components can also be created using the normal constructor:
|
20
|
+
#
|
21
|
+
# greeter = Papercraft::Component.new { |name| h1 "Hello, #{name}!" }
|
22
|
+
# greeter.render('world') #=> "<h1>Hello, world!</h1>"
|
23
|
+
#
|
24
|
+
# In the component block, HTML elements are created by simply calling
|
25
|
+
# unqualified methods:
|
26
|
+
class Component < Proc
|
27
|
+
# Initializes a component with the given block
|
28
|
+
# @param mode [Symbol] local context
|
29
|
+
# @param block [Proc] nested HTML block
|
30
|
+
def initialize(mode: :html, &block)
|
31
|
+
@mode = mode
|
32
|
+
super(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
H_EMPTY = {}.freeze
|
36
|
+
|
37
|
+
# Renders the associated block and returns the string result
|
38
|
+
# @param context [Hash] context
|
39
|
+
# @return [String]
|
40
|
+
def render(*a, **b, &block)
|
41
|
+
template = self
|
42
|
+
Renderer.verify_proc_parameters(template, a, b)
|
43
|
+
renderer_class.new do
|
44
|
+
if block
|
45
|
+
with_block(block) { instance_exec(*a, **b, &template) }
|
46
|
+
else
|
47
|
+
instance_exec(*a, **b, &template)
|
48
|
+
end
|
49
|
+
end.to_s
|
50
|
+
rescue ArgumentError => e
|
51
|
+
raise Papercraft::Error, e.message
|
52
|
+
end
|
53
|
+
|
54
|
+
def apply(*a, **b, &block)
|
55
|
+
template = self
|
56
|
+
if block
|
57
|
+
Component.new(&proc do |*x, **y|
|
58
|
+
with_block(block) { instance_exec(*x, **y, &template) }
|
59
|
+
end)
|
60
|
+
else
|
61
|
+
Component.new(&proc do |*x, **y|
|
62
|
+
instance_exec(*a, **b, &template)
|
63
|
+
end)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def renderer_class
|
68
|
+
case @mode
|
69
|
+
when :html
|
70
|
+
HTMLRenderer
|
71
|
+
when :xml
|
72
|
+
XMLRenderer
|
73
|
+
else
|
74
|
+
raise "Invalid mode #{@mode.inspect}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# def compile
|
79
|
+
# Papercraft::Compiler.new.compile(self)
|
80
|
+
# end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Papercraft
|
4
|
+
# Papercraft::Encoding includes common encoding methods
|
5
|
+
module Encoding
|
6
|
+
# Encodes the given string to safe HTML text, converting special characters
|
7
|
+
# into the respective HTML entities. If a non-string value is given, it is
|
8
|
+
# converted to `String` using `#to_s`.
|
9
|
+
#
|
10
|
+
# @param text [String] string to be encoded
|
11
|
+
# @return [String] HTML-encoded string
|
12
|
+
def __html_encode__(text)
|
13
|
+
EscapeUtils.escape_html(text.to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Encodes the given string to safe URI component, converting special
|
17
|
+
# characters to URI entities. If a non-string value is given, it is
|
18
|
+
# converted to `String` using `#to_s`.
|
19
|
+
#
|
20
|
+
# @param text [String] string to be encoded
|
21
|
+
# @return [String] URI-encoded string
|
22
|
+
def __uri_encode__(text)
|
23
|
+
EscapeUtils.escape_uri(text.to_s)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './html'
|
4
|
+
|
5
|
+
module Papercraft
|
6
|
+
# Markup extensions
|
7
|
+
module HTML
|
8
|
+
# Emits the p tag (overrides Object#p)
|
9
|
+
# @param text [String] text content of tag
|
10
|
+
# @param props [Hash] tag attributes
|
11
|
+
# @para block [Proc] nested HTML block
|
12
|
+
# @return [void]
|
13
|
+
def p(text = nil, **props, &block)
|
14
|
+
method_missing(:p, text, **props, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
S_HTML5_DOCTYPE = '<!DOCTYPE html>'
|
18
|
+
|
19
|
+
# Emits an HTML5 doctype tag and an html tag with the given block
|
20
|
+
# @param block [Proc] nested HTML block
|
21
|
+
# @return [void]
|
22
|
+
def html5(&block)
|
23
|
+
@buffer << S_HTML5_DOCTYPE
|
24
|
+
self.html(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def link_stylesheet(href, custom_attributes = nil)
|
28
|
+
attributes = {
|
29
|
+
rel: 'stylesheet',
|
30
|
+
href: href
|
31
|
+
}
|
32
|
+
if custom_attributes
|
33
|
+
attributes = custom_attributes.merge(attributes)
|
34
|
+
end
|
35
|
+
link(**attributes)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './html'
|
4
|
+
|
5
|
+
module Papercraft
|
6
|
+
# A Renderer renders a Papercraft component into a string
|
7
|
+
class Renderer
|
8
|
+
class << self
|
9
|
+
def verify_proc_parameters(template, args, named_args)
|
10
|
+
param_count = 0
|
11
|
+
template.parameters.each do |(type, name)|
|
12
|
+
case type
|
13
|
+
when :req
|
14
|
+
param_count += 1
|
15
|
+
when :keyreq
|
16
|
+
if !named_args.has_key?(name)
|
17
|
+
raise Papercraft::Error, "Missing template parameter #{name.inspect}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
if param_count > args.size
|
22
|
+
raise Papercraft::Error, "Missing template parameters"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :context
|
28
|
+
|
29
|
+
# Initializes attributes and renders the given block
|
30
|
+
# @param context [Hash] rendering context
|
31
|
+
# @param block [Proc] template block
|
32
|
+
# @return [void]
|
33
|
+
def initialize(&template)
|
34
|
+
@buffer = +''
|
35
|
+
instance_eval(&template)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the result of the rendering
|
39
|
+
# @return [String]
|
40
|
+
def to_s
|
41
|
+
@buffer
|
42
|
+
end
|
43
|
+
|
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
|
+
S_TAG_METHOD_LINE = __LINE__ + 1
|
53
|
+
S_TAG_METHOD = <<~EOF
|
54
|
+
S_TAG_%<TAG>s_PRE = '<%<tag>s'.tr('_', '-')
|
55
|
+
S_TAG_%<TAG>s_CLOSE = '</%<tag>s>'.tr('_', '-')
|
56
|
+
|
57
|
+
def %<tag>s(text = nil, **props, &block)
|
58
|
+
@buffer << S_TAG_%<TAG>s_PRE
|
59
|
+
emit_props(props) unless props.empty?
|
60
|
+
|
61
|
+
if block
|
62
|
+
@buffer << S_GT
|
63
|
+
instance_eval(&block)
|
64
|
+
@buffer << S_TAG_%<TAG>s_CLOSE
|
65
|
+
elsif Proc === text
|
66
|
+
@buffer << S_GT
|
67
|
+
emit(text)
|
68
|
+
@buffer << S_TAG_%<TAG>s_CLOSE
|
69
|
+
elsif text
|
70
|
+
@buffer << S_GT << escape_text(text.to_s) << S_TAG_%<TAG>s_CLOSE
|
71
|
+
else
|
72
|
+
@buffer << S_SLASH_GT
|
73
|
+
end
|
74
|
+
end
|
75
|
+
EOF
|
76
|
+
|
77
|
+
# Catches undefined tag method call and handles them by defining the method
|
78
|
+
# @param sym [Symbol] HTML tag or component identifier
|
79
|
+
# @param args [Array] method call arguments
|
80
|
+
# @param block [Proc] block passed to method call
|
81
|
+
# @return [void]
|
82
|
+
def method_missing(sym, *args, **opts, &block)
|
83
|
+
value = @local && @local[sym]
|
84
|
+
return value if value
|
85
|
+
|
86
|
+
tag = sym.to_s
|
87
|
+
code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
|
88
|
+
self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
|
89
|
+
send(sym, *args, **opts, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Emits the given object into the rendering buffer
|
93
|
+
# @param o [Proc, Papercraft::Component, String] emitted object
|
94
|
+
# @return [void]
|
95
|
+
def emit(o, *a, **b)
|
96
|
+
case o
|
97
|
+
when ::Proc
|
98
|
+
Renderer.verify_proc_parameters(o, a, b)
|
99
|
+
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
|
+
when nil
|
105
|
+
else
|
106
|
+
@buffer << o.to_s
|
107
|
+
end
|
108
|
+
end
|
109
|
+
alias_method :e, :emit
|
110
|
+
|
111
|
+
def with_block(block, &run_block)
|
112
|
+
old_block = @inner_block
|
113
|
+
@inner_block = block
|
114
|
+
instance_eval(&run_block)
|
115
|
+
ensure
|
116
|
+
@inner_block = old_block
|
117
|
+
end
|
118
|
+
|
119
|
+
def emit_yield(*a, **b)
|
120
|
+
raise Papercraft::Error, "No block given" unless @inner_block
|
121
|
+
|
122
|
+
instance_exec(*a, **b, &@inner_block)
|
123
|
+
end
|
124
|
+
|
125
|
+
S_LT = '<'
|
126
|
+
S_GT = '>'
|
127
|
+
S_LT_SLASH = '</'
|
128
|
+
S_SPACE_LT_SLASH = ' </'
|
129
|
+
S_SLASH_GT = '/>'
|
130
|
+
S_SPACE = ' '
|
131
|
+
S_EQUAL_QUOTE = '="'
|
132
|
+
S_QUOTE = '"'
|
133
|
+
|
134
|
+
# Emits tag attributes into the rendering buffer
|
135
|
+
# @param props [Hash] tag attributes
|
136
|
+
# @return [void]
|
137
|
+
def emit_props(props)
|
138
|
+
props.each { |k, v|
|
139
|
+
case k
|
140
|
+
when :src, :href
|
141
|
+
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
|
142
|
+
EscapeUtils.escape_uri(v) << S_QUOTE
|
143
|
+
else
|
144
|
+
case v
|
145
|
+
when true
|
146
|
+
@buffer << S_SPACE << k.to_s.tr('_', '-')
|
147
|
+
when false, nil
|
148
|
+
# emit nothing
|
149
|
+
else
|
150
|
+
@buffer << S_SPACE << k.to_s.tr('_', '-') <<
|
151
|
+
S_EQUAL_QUOTE << v << S_QUOTE
|
152
|
+
end
|
153
|
+
end
|
154
|
+
}
|
155
|
+
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
|
+
end
|
163
|
+
|
164
|
+
class HTMLRenderer < Renderer
|
165
|
+
include HTML
|
166
|
+
|
167
|
+
def escape_text(text)
|
168
|
+
EscapeUtils.escape_html(text.to_s)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class XMLRenderer < Renderer
|
173
|
+
def escape_text(text)
|
174
|
+
EscapeUtils.escape_xml(text.to_s)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
data/lib/papercraft.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'escape_utils'
|
4
|
+
|
5
|
+
require_relative 'papercraft/component'
|
6
|
+
require_relative 'papercraft/renderer'
|
7
|
+
require_relative 'papercraft/encoding'
|
8
|
+
# require_relative 'papercraft/compiler'
|
9
|
+
|
10
|
+
# Papercraft is a component-based HTML templating library
|
11
|
+
module Papercraft
|
12
|
+
# Exception class used to signal templating-related errors
|
13
|
+
class Error < RuntimeError; end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Kernel extensions
|
17
|
+
module ::Kernel
|
18
|
+
# Convenience method for creating a new Papercraft
|
19
|
+
# @param ctx [Hash] local context
|
20
|
+
# @param template [Proc] template block
|
21
|
+
# @return [Papercraft] Papercraft template
|
22
|
+
def H(&template)
|
23
|
+
Papercraft::Component.new(&template)
|
24
|
+
end
|
25
|
+
|
26
|
+
def X(&template)
|
27
|
+
Papercraft::Component.new(mode: :xml, &template)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Object extensions
|
32
|
+
class Object
|
33
|
+
include Papercraft::Encoding
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: papercraft
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.8'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sharon Rosner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-12-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: escape_utils
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.2.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.2.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.11.3
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.11.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: benchmark-ips
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.7.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.7.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: erubis
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.7.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.7.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tilt
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.0.9
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.0.9
|
83
|
+
description:
|
84
|
+
email: sharon@noteflakes.com
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files:
|
88
|
+
- README.md
|
89
|
+
files:
|
90
|
+
- CHANGELOG.md
|
91
|
+
- README.md
|
92
|
+
- lib/papercraft.rb
|
93
|
+
- lib/papercraft/compiler.rb
|
94
|
+
- lib/papercraft/component.rb
|
95
|
+
- lib/papercraft/encoding.rb
|
96
|
+
- lib/papercraft/html.rb
|
97
|
+
- lib/papercraft/renderer.rb
|
98
|
+
- lib/papercraft/version.rb
|
99
|
+
homepage: http://github.com/digital-fabric/papercraft
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
metadata:
|
103
|
+
source_code_uri: https://github.com/digital-fabric/papercraft
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options:
|
106
|
+
- "--title"
|
107
|
+
- Papercraft
|
108
|
+
- "--main"
|
109
|
+
- README.md
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '2.7'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubygems_version: 3.1.6
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: 'Papercraft: component-based HTML templating for Ruby'
|
127
|
+
test_files: []
|