papercraft 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +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: []
|