papercraft 0.8

Sign up to get free protection for your applications and to get access to all the features.
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: []