papercraft 0.17 → 0.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31c80224c2a9dc3a6d594d923e9a16ccd8127284bce501f1d7f1dc25386bf25c
4
- data.tar.gz: 45dc43d353bc1f3c06e62d1b439751570ce7d46a915b0a72f1ce3ea182b0c225
3
+ metadata.gz: a23bf614d84747a18eecf84545604c9c2f58710dc3fa0e0c651a863a53c919b0
4
+ data.tar.gz: 6b1ca8f4a90801beff11807dd357ce801b7526b5d114d5cca5f41f41aec69958
5
5
  SHA512:
6
- metadata.gz: 67529d1336456dded5d911548941c1accc695bd017b889b378ed008e177707b100e11e90b1adbe887d45530cfb94950808e21257955999d795b388dbeb13206e
7
- data.tar.gz: 1caca448eb2a317b1616a529e82803f6571e26e8b0f36a19e771b2b4a9a96cabb8695ad5a01148acfa7481b016742e9c546c794fd2a9017ec54d299184d36c94
6
+ metadata.gz: dcd7eada8280cb0fcd68528eaea0d2500ff99ac149c8495dd5a3955a93c3f181301a4decc9eeb4d7efdd8cfe2c4f6047c9f838e5084b413c1118a76d6abc4252
7
+ data.tar.gz: 840ce0fc2a83de9b5a83bb10a5f4ed98986f7201737d6e31a4e89877aeb6bb22b2f2449c6efa08302473c073e26c830cfbe9e267456b450a56222b57a429fd15
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 0.21 2022-02-13
2
+
3
+ - Refactor and improve documentation
4
+
5
+ ## 0.20 2022-02-13
6
+
7
+ - Add support for XML namespaced tags and attributes (#9)
8
+ - Move and refactor HTML/XML common code to Tags module
9
+
10
+ ## 0.19 2022-02-05
11
+
12
+ - Rename `Papercraft::Component` to `Papercraft::Template`
13
+
14
+ ## 0.18 2022-02-04
15
+
16
+ - Cleanup and update examples
17
+ - Fix behaviour of #emit with block
18
+ - Improve README
19
+
1
20
  ## 0.17 2022-01-23
2
21
 
3
22
  - Refactor markdown code, add `Papercraft.markdown` method (#8)
data/README.md CHANGED
@@ -24,35 +24,63 @@
24
24
 
25
25
  ## What is Papercraft?
26
26
 
27
+ Papercraft is a templating engine for dynamically producing HTML, XML or JSON.
28
+ Papercraft templates are expressed in plain Ruby, leading to easier debugging,
29
+ better protection against HTML/XML injection attacks, and better code reuse.
30
+
31
+ Papercraft templates can be composed in a variety of ways, facilitating the
32
+ usage of layout templates, and enabling a component-oriented approach to
33
+ building complex web interfaces.
34
+
35
+ In Papercraft, dynamic data is passed explicitly to the template as block
36
+ arguments, making the data flow easy to follow and understand. Papercraft also
37
+ lets developers create derivative templates using full or partial parameter
38
+ application.
39
+
40
+ Papercraft includes built-in support for rendering Markdown (using
41
+ [Kramdown](https://github.com/gettalong/kramdown/)), as well as support for
42
+ creating template extensions in order to allow the creation of component
43
+ libraries.
44
+
27
45
  ```ruby
28
46
  require 'papercraft'
29
47
 
30
48
  page = Papercraft.html { |*args|
31
49
  html {
32
- head { }
50
+ head { title 'Title' }
33
51
  body { emit_yield *args }
34
52
  }
35
53
  }
36
54
  page.render { p 'foo' }
37
- #=> "<html><head/><body><p>foo</p></body></html>"
55
+ #=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
38
56
 
39
57
  hello = page.apply { |name| h1 "Hello, #{name}!" }
40
58
  hello.render('world')
41
- #=> "<html><head/><body><h1>Hello, world!</h1></body></html>"
42
- ```
43
-
44
- Papercraft is a templating engine for Ruby that offers the following features:
45
-
46
- - HTML, XML and JSON templating using plain Ruby syntax
47
- - Minimal boilerplate
48
- - Mix logic and tags freely
49
- - Automatic HTML and XML escaping
50
- - Composable components
51
- - Standard or custom MIME types
52
- - Explicit parameter passing to nested components
53
- - Higher order components
54
- - Built-in support for rendering [Markdown](#emitting-markdown)
55
- - Support for namespaced extensions
59
+ #=> "<html><head><title>Title</title></head><body><h1>Hello, world!</h1></body></html>"
60
+ ```
61
+
62
+ ## Table of content
63
+
64
+ - [Installing papercraft](#installing-papercraft)
65
+ - [Basic usage](#basic-usage)
66
+ - [Adding tags](#adding-tags)
67
+ - [Template parameters](#template-parameters)
68
+ - [Template logic](#template-logic)
69
+ - [Template blocks](#template-blocks)
70
+ - [Plain procs as templates](#plain-procs-as-templates)
71
+ - [Template composition](#template-composition)
72
+ - [Parameter and block application](#parameter-and-block-application)
73
+ - [Higher-order templates](#higher-order-templates)
74
+ - [Layout template composition](#layout-template-composition)
75
+ - [Emitting raw HTML](#emitting-raw-html)
76
+ - [Emitting a string with HTML Encoding](#emitting-a-string-with-html-encoding)
77
+ - [Emitting Markdown](#emitting-markdown)
78
+ - [Working with MIME types](#working-with-mime-types)
79
+ - [Deferred evaluation](#deferred-evaluation)
80
+ - [Papercraft extensions](#papercraft-extensions)
81
+ - [XML templates](#xml-templates)
82
+ - [JSON templates](#json-templates)
83
+ - [API Reference](#api-reference)
56
84
 
57
85
  ## Installing Papercraft
58
86
 
@@ -68,9 +96,9 @@ Or manually:
68
96
  $ gem install papercraft
69
97
  ```
70
98
 
71
- ## Getting started
99
+ ## Basic usage
72
100
 
73
- To create a template use the global method `Kernel#H`:
101
+ To create an HTML template use `Papercraft.html`:
74
102
 
75
103
  ```ruby
76
104
  require 'papercraft'
@@ -80,13 +108,16 @@ html = Papercraft.html {
80
108
  }
81
109
  ```
82
110
 
111
+ (You can also use `Papercraft.xml` and `Papercraft.json` to create XML and JSON
112
+ templates, respectively.)
113
+
83
114
  Rendering a template is done using `#render`:
84
115
 
85
116
  ```ruby
86
117
  html.render #=> "<div id="greeter"><p>Hello!</p></div>"
87
118
  ```
88
119
 
89
- ## All about tags
120
+ ## Adding tags
90
121
 
91
122
  Tags are added using unqualified method calls, and can be nested using blocks:
92
123
 
@@ -141,7 +172,7 @@ greeting = Papercraft.html { |name:| h1 "Hello, #{name}!" }
141
172
  greeting.render(name: 'world') #=> "<h1>Hello, world!</h1>"
142
173
  ```
143
174
 
144
- ## Logic in templates
175
+ ## Template logic
145
176
 
146
177
  Since Papercraft templates are just a bunch of Ruby, you can easily write your
147
178
  view logic right in the template:
@@ -171,10 +202,10 @@ page = Papercraft.html {
171
202
  page.render { h1 'hi' }
172
203
  ```
173
204
 
174
- ## Plain procs as components
205
+ ## Plain procs as templates
175
206
 
176
207
  With Papercraft you can write a template as a plain Ruby proc, and later render
177
- it by passing it as a block to `H`:
208
+ it by passing it as a block to `Papercraft.html`:
178
209
 
179
210
  ```ruby
180
211
  greeting = proc { |name| h1 "Hello, #{name}!" }
@@ -188,10 +219,10 @@ greeting = ->(name) { h1 "Hello, #{name}!" }
188
219
  Papercraft.html(&greeting).render('world')
189
220
  ```
190
221
 
191
- ## Component composition
222
+ ## Template composition
192
223
 
193
- Papercraft makes it easy to compose multiple components into a whole HTML
194
- document. A Papercraft component can contain other components, as the following
224
+ Papercraft makes it easy to compose multiple templates into a whole HTML
225
+ document. A Papercraft template can contain other templates, as the following
195
226
  example shows.
196
227
 
197
228
  ```ruby
@@ -219,14 +250,14 @@ page = Papercraft.html { |title, items|
219
250
  }
220
251
  }
221
252
 
222
- page.render('Hello from components', [
253
+ page.render('Hello from composed templates', [
223
254
  { id: 1, text: 'foo', checked: false },
224
255
  { id: 2, text: 'bar', checked: true }
225
256
  ])
226
257
  ```
227
258
 
228
- In addition to using components defined as constants, you can also use
229
- non-constant components by invoking the `#emit` method:
259
+ In addition to using templates defined as constants, you can also use
260
+ non-constant templates by invoking the `#emit` method:
230
261
 
231
262
  ```ruby
232
263
  greeting = -> { span "Hello, world" }
@@ -241,11 +272,11 @@ Papercraft.html {
241
272
  ## Parameter and block application
242
273
 
243
274
  Parameters and blocks can be applied to a template without it being rendered, by
244
- using `#apply`. This mechanism is what allows component composition and the
245
- creation of higher-order components.
275
+ using `#apply`. This mechanism is what allows template composition and the
276
+ creation of higher-order templates.
246
277
 
247
- The `#apply` method returns a new component which applies the given parameters and
248
- or block to the original component:
278
+ The `#apply` method returns a new template which applies the given parameters and
279
+ or block to the original template:
249
280
 
250
281
  ```ruby
251
282
  # parameter application
@@ -258,19 +289,19 @@ div_wrap = Papercraft.html { div { emit_yield } }
258
289
  wrapped_h1 = div_wrap.apply { h1 'hi' }
259
290
  wrapped_h1.render #=> "<div><h1>hi</h1></div>"
260
291
 
261
- # wrap a component
292
+ # wrap a template
262
293
  wrapped_hello_world = div_wrap.apply(&hello_world)
263
294
  wrapped_hello_world.render #=> "<div><h1>Hello, world!</h1></div>"
264
295
  ```
265
296
 
266
- ## Higher-order components
297
+ ## Higher-order templates
267
298
 
268
- Papercraft also lets you create higher-order components (HOCs), that is,
269
- components that take other components as parameters, or as blocks. Higher-order
270
- components are handy for creating layouts, wrapping components in arbitrary
271
- markup, enhancing components or injecting component parameters.
299
+ Papercraft also lets you create higher-order templates, that is,
300
+ templates that take other templates as parameters, or as blocks. Higher-order
301
+ templates are handy for creating layouts, wrapping templates in arbitrary
302
+ markup, enhancing templates or injecting template parameters.
272
303
 
273
- Here is a HOC that takes a component as parameter:
304
+ Here is a higher-order template that takes a template as parameter:
274
305
 
275
306
  ```ruby
276
307
  div_wrap = Papercraft.html { |inner| div { emit inner } }
@@ -279,7 +310,7 @@ wrapped_greeter = div_wrap.apply(greeter)
279
310
  wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
280
311
  ```
281
312
 
282
- The inner component can also be passed as a block, as shown above:
313
+ The inner template can also be passed as a block, as shown above:
283
314
 
284
315
  ```ruby
285
316
  div_wrap = Papercraft.html { div { emit_yield } }
@@ -289,7 +320,7 @@ wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
289
320
 
290
321
  ## Layout template composition
291
322
 
292
- One of the principal uses of higher-order components is the creation of nested
323
+ One of the principal uses of higher-order templates is the creation of nested
293
324
  layouts. Suppose we have a website with a number of different layouts, and we'd
294
325
  like to avoid having to repeat the same code in the different layouts. We can do
295
326
  this by creating a `default` page template that takes a block, then use `#apply`
@@ -384,11 +415,27 @@ The deafult options can be configured by accessing
384
415
  Papercraft.default_kramdown_options[:auto_ids] = false
385
416
  ```
386
417
 
418
+ ## Working with MIME types
419
+
420
+ Papercraft lets you set and interrogate a template's MIME type, in order to be
421
+ able to dynamically set the `Content-Type` HTTP response header. A template's
422
+ MIME type can be set when creating the template, e.g. `Papercraft.xml(mime_type:
423
+ 'application/rss+xml')`. You can interrogate the template's MIME type using
424
+ `#mime_type`:
425
+
426
+ ```ruby
427
+ # using Qeweney (https://github.com/digital-fabric/qeweney)
428
+ def serve_template(req, template)
429
+ body = template.render
430
+ respond(body, 'Content-Type' => template.mime_type)
431
+ end
432
+ ```
433
+
387
434
  ## Deferred evaluation
388
435
 
389
436
  Deferred evaluation allows deferring the rendering of parts of a template until
390
- the last moment, thus allowing an inner component to manipulate the state of the
391
- outer component. To in order to defer a part of a template, use `#defer`, and
437
+ the last moment, thus allowing an inner template to manipulate the state of the
438
+ outer template. To in order to defer a part of a template, use `#defer`, and
392
439
  include any markup in the provided block. This technique, in in conjunction with
393
440
  holding state in instance variables, is an alternative to passing parameters,
394
441
  which can be limiting in some situations.
@@ -397,11 +444,11 @@ A few use cases for deferred evaulation come to mind:
397
444
 
398
445
  - Setting the page title.
399
446
  - Adding a flash message to a page.
400
- - Using components that dynamically add static dependencies (JS and CSS) to the
447
+ - Using templates that dynamically add static dependencies (JS and CSS) to the
401
448
  page.
402
449
 
403
450
  The last use case is particularly interesting. Imagine a `DependencyMananger`
404
- class that can collect JS and CSS dependencies from the different components
451
+ class that can collect JS and CSS dependencies from the different templates
405
452
  integrated into the page, and adds them to the page's `<head>` element:
406
453
 
407
454
  ```ruby
@@ -502,11 +549,47 @@ Papercraft.html {
502
549
  }
503
550
  ```
504
551
 
505
- ## JSON templating
506
552
 
507
- You can create a JSON template using the same API used for HTML and XML
508
- templating. The only difference is that for adding array items you'll need to
509
- use the `#item` method:
553
+
554
+ ## XML templates
555
+
556
+ XML templates behave largely the same as HTML templates, with a few minor
557
+ differences. XML templates employ a different encoding algorithm, and lack some
558
+ specific HTML functionality, such as emitting Markdown.
559
+
560
+ Here's an example showing how to create an RSS feed:
561
+
562
+ ```ruby
563
+ rss = Papercraft.xml(mime_type: 'text/xml; charset=utf-8') { |resource:, **props|
564
+ rss(version: '2.0', 'xmlns:atom' => 'http://www.w3.org/2005/Atom') {
565
+ channel {
566
+ title 'Noteflakes'
567
+ link 'https://noteflakes.com/'
568
+ description 'A website by Sharon Rosner'
569
+ language 'en-us'
570
+ pubDate Time.now.httpdate
571
+ emit '<atom:link href="https://noteflakes.com/feeds/rss" rel="self" type="application/rss+xml" />'
572
+
573
+ article_entries = resource.page_list('/articles').reverse
574
+
575
+ article_entries.each { |e|
576
+ item {
577
+ title e[:title]
578
+ link "https://noteflakes.com#{e[:url]}"
579
+ guid "https://noteflakes.com#{e[:url]}"
580
+ pubDate e[:date].to_time.httpdate
581
+ description e[:html_content]
582
+ }
583
+ }
584
+ }
585
+ }
586
+ }
587
+ ```
588
+
589
+ ## JSON templates
590
+
591
+ JSON templates behave largely the same as HTML and XML templates. The only major
592
+ difference is that for adding array items you'll need to use the `#item` method:
510
593
 
511
594
  ```ruby
512
595
  Papercraft.json {
@@ -531,7 +614,8 @@ Papercraft.json {
531
614
  }.render #=> "{\"foo\":{\"bar\":[null,true,123.456]}}"
532
615
  ```
533
616
 
534
- Papercraft uses the [JSON gem](https://rubyapi.org/3.1/o/json) under the hood.
617
+ Papercraft uses the [JSON gem](https://rubyapi.org/3.1/o/json) under the hood in
618
+ order to generate actual JSON.
535
619
 
536
620
  ## API Reference
537
621
 
@@ -10,6 +10,7 @@ module Papercraft
10
10
  class ExtensionProxy
11
11
 
12
12
  # Initializes a new ExtensionProxy.
13
+ #
13
14
  # @param renderer [Papercraft::Renderer] renderer to proxy to
14
15
  # @param mod [Module] extension module
15
16
  # @return [void]
@@ -18,7 +19,8 @@ module Papercraft
18
19
  extend(mod)
19
20
  end
20
21
 
21
- # Proxies missing methods to the renderer
22
+ # Proxies missing methods to the renderer.
23
+ #
22
24
  # @param sym [Symbol] method name
23
25
  # @param *args [Array] arguments
24
26
  # @param &block [Proc] block
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './tags'
4
+
3
5
  module Papercraft
4
6
  # HTML Markup extensions
5
7
  module HTML
8
+ include Tags
9
+
6
10
  # Emits the p tag (overrides Object#p)
7
11
  #
8
12
  # @param text [String] text content of tag
@@ -81,5 +85,33 @@ module Papercraft
81
85
  def emit_markdown(markdown, **opts)
82
86
  emit Papercraft.markdown(markdown, **opts)
83
87
  end
88
+
89
+ private
90
+
91
+ # Escapes the given text using HTML entities.
92
+ #
93
+ # @param text [String] text
94
+ # @return [String] escaped text
95
+ def escape_text(text)
96
+ EscapeUtils.escape_html(text.to_s)
97
+ end
98
+
99
+ # Converts a tag to its string representation. Underscores will be converted
100
+ # to dashes.
101
+ #
102
+ # @param tag [Symbol, String] tag
103
+ # @return [String] tag string
104
+ def tag_repr(tag)
105
+ tag.to_s.tr('_', '-')
106
+ end
107
+
108
+ # Converts an attribute to its string representation. Underscores will be
109
+ # converted to dashes.
110
+ #
111
+ # @param att [Symbol, String] attribute
112
+ # @return [String] attribute string
113
+ def att_repr(att)
114
+ att.to_s.tr('_', '-')
115
+ end
84
116
  end
85
117
  end
@@ -5,69 +5,122 @@ require 'json'
5
5
  module Papercraft
6
6
  # JSON renderer extensions
7
7
  module JSON
8
- def object_stack
9
- @object_stack ||= [nil]
8
+ # Initializes a JSON renderer, setting up an object stack.
9
+ def initialize(&template)
10
+ @object_stack = [nil]
11
+ super
10
12
  end
11
13
 
14
+ # Adds an array item to the current object target. If a block is given, the
15
+ # block is evaulated against a new object target, then added to the current
16
+ # array.
17
+ #
18
+ # @param value [Object] item
19
+ # @param &block [Proc] template block
20
+ # @return [void]
21
+ def item(value = nil, &block)
22
+ verify_array_target
23
+ if block
24
+ value = enter_object(&block)
25
+ end
26
+ push_array_item(value)
27
+ end
28
+
29
+ # Adds a key-value item to the current object target. If a block is given,
30
+ # the block is evaulated against a new object target, then used as the
31
+ # value.
32
+ #
33
+ # @param key [Object] key
34
+ # @param value [Object] value
35
+ # @param &block [Proc] template block
36
+ # @return [void]
37
+ def kv(key, value = nil, &block)
38
+ verify_hash_target
39
+ if block
40
+ value = enter_object(&block)
41
+ end
42
+ push_kv_item(key, value)
43
+ end
44
+
45
+ # Intercepts method calls by adding key-value pairs to the current object
46
+ # target.
47
+ #
48
+ # @param key [Object] key
49
+ # @param value [Object] value
50
+ # @param &block [Proc] template block
51
+ # @return [void]
52
+ def method_missing(sym, value = nil, &block)
53
+ kv(sym, value, &block)
54
+ end
55
+
56
+ # Converts the root object target to JSON.
57
+ #
58
+ # @return [String] JSON template result
59
+ def to_s
60
+ @object_stack[0].to_json
61
+ end
62
+
63
+ private
64
+
65
+ # Adds a new entry to the object stack and evaluates the given block.
66
+ #
67
+ # @param &block [Proc] template block
68
+ # @return [void]
12
69
  def with_object(&block)
13
- object_stack << nil
70
+ @object_stack << nil
14
71
  instance_eval(&block)
15
72
  end
16
73
 
74
+ # Verifies that the current object target is not a hash.
75
+ #
76
+ # @return [bool]
17
77
  def verify_array_target
18
- case object_stack[-1]
78
+ case @object_stack[-1]
19
79
  when nil
20
- object_stack[-1] = []
80
+ @object_stack[-1] = []
21
81
  when Hash
22
82
  raise "Mixing array and hash values"
23
83
  end
24
84
  end
25
85
 
86
+ # Verifies that the current object target is not an array.
87
+ #
88
+ # @return [bool]
26
89
  def verify_hash_target
27
- case object_stack[-1]
90
+ case @object_stack[-1]
28
91
  when nil
29
- object_stack[-1] = {}
92
+ @object_stack[-1] = {}
30
93
  when Array
31
94
  raise "Mixing array and hash values"
32
95
  end
33
96
  end
34
97
 
98
+ # Pushes an array item to the current object target.
99
+ #
100
+ # @param value [Object] item
101
+ # @return [void]
35
102
  def push_array_item(value)
36
- object_stack[-1] << value
103
+ @object_stack[-1] << value
37
104
  end
38
105
 
106
+ # Pushes a key value into the current object target.
107
+ #
108
+ # @param key [Object] key
109
+ # @param value [Object] value
110
+ # @return [void]
39
111
  def push_kv_item(key, value)
40
- object_stack[-1][key] = value
112
+ @object_stack[-1][key] = value
41
113
  end
42
114
 
115
+ # Adds a new object to the object stack, evaluates the given template block,
116
+ # then pops the object off the stack.
117
+ #
118
+ # @param &block [Proc] template block
119
+ # @return [void]
43
120
  def enter_object(&block)
44
- object_stack << nil
121
+ @object_stack << nil
45
122
  instance_eval(&block)
46
- object_stack.pop
47
- end
48
-
49
- def item(value = nil, &block)
50
- verify_array_target
51
- if block
52
- value = enter_object(&block)
53
- end
54
- push_array_item(value)
55
- end
56
-
57
- def kv(key, value, &block)
58
- verify_hash_target
59
- if block
60
- value = enter_object(&block)
61
- end
62
- push_kv_item(key, value)
63
- end
64
-
65
- def method_missing(sym, value = nil, &block)
66
- kv(sym, value, &block)
67
- end
68
-
69
- def to_s
70
- object_stack[0].to_json
71
- end
123
+ @object_stack.pop
124
+ end
72
125
  end
73
126
  end