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 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&amp;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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Papercraft
4
+ VERSION = '0.8'
5
+ 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: []