papercraft 0.15 → 0.19

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: 2200153b1b339d33c7e9e81c45c23bd4c117c274a52b1d1bfc97f83995727697
4
- data.tar.gz: 3dae3cf3b6ea2530df8c498cdafb876e06d45d527b6e2bbbcb6302e6ef0842e9
3
+ metadata.gz: f0f4910bd26625ca0349b1c471338fb4d13e8055e88367b5cad16f91cdee7531
4
+ data.tar.gz: 38e43978278ba069e3767abd94d83c676f42692f5eb55f1be4b4067fae2f935e
5
5
  SHA512:
6
- metadata.gz: 5f9727de69aac6d48de53ca174280c68dce7825b9a4aa6f8c955b6122f30d9f4541a7d47e7ed7caa319feda5659b14b73a3ad343db2733b4bb9518e9ade94f8c
7
- data.tar.gz: 765e786959a6fdda6c25cddda11f5f987837d6853245e5bc8533dbe7b904792b09fc39e726371c81981275ef0abdb8d1d95de63a76889af34b037f34d01c6c3a
6
+ metadata.gz: b096452cebe7b6bb34a428f4920a5940e5b745c86b54188a984a77df6175145cf6645753e6ae58b3df89813919920a17a96610e010724bc50077c6f9accc7293
7
+ data.tar.gz: 8d1cd5b2d6b70ee9ef0a748a064bbd6d1630cfa926663aded95a01c26ce289a159e2548068bf8210c5e8d37ba26c9671eb4a00fe4790a47bec6dd822b4de5d5d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 0.19 2022-02-05
2
+
3
+ - Rename `Papercraft::Component` to `Papercraft::Template`
4
+
5
+ ## 0.18 2022-02-04
6
+
7
+ - Cleanup and update examples
8
+ - Fix behaviour of #emit with block
9
+ - Improve README
10
+
11
+ ## 0.17 2022-01-23
12
+
13
+ - Refactor markdown code, add `Papercraft.markdown` method (#8)
14
+
15
+ ## 0.16 2022-01-23
16
+
17
+ - Implement JSON templating (#7)
18
+ - Add support for MIME types (#6)
19
+ - Change entrypoint from `Kernel#H`, `Kernel#X` to `Papercraft.html`, `.xml` (#5)
20
+
1
21
  ## 0.15 2022-01-20
2
22
 
3
23
  - Fix tag method line reference
@@ -42,7 +62,7 @@
42
62
  ## 0.8 2021-12-22
43
63
 
44
64
  - Cleanup and refactor code
45
- - Add X global method for XML templates
65
+ - Add Papercraft.xml global method for XML templates
46
66
  - Make `Component` a descendant of `Proc`
47
67
  - Introduce new component API
48
68
  - Rename Rubyoshka to Papercraft
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  Papercraft
5
5
  </h1>
6
6
 
7
- <h4 align="center">Composable HTML templating for Ruby</h4>
7
+ <h4 align="center">Composable templating for Ruby</h4>
8
8
 
9
9
  <p align="center">
10
10
  <a href="http://rubygems.org/gems/papercraft">
@@ -24,33 +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
- page = H { |*args|
48
+ page = Papercraft.html { |*args|
31
49
  html {
32
- head { }
50
+ head { title 'Title' }
33
51
  body { emit_yield *args }
34
52
  }
35
53
  }
54
+ page.render { p 'foo' }
55
+ #=> "<html><head><title>Title</title></head><body><p>foo</p></body></html>"
36
56
 
37
- hello = H.apply { |name| h1 "Hello, #{name}!" }
57
+ hello = page.apply { |name| h1 "Hello, #{name}!" }
38
58
  hello.render('world')
39
- #=> "<html><head/><body><h1>Hello, world!</h1></body></html>"
59
+ #=> "<html><head><title>Title</title></head><body><h1>Hello, world!</h1></body></html>"
40
60
  ```
41
61
 
42
- Papercraft is an HTML templating engine for Ruby that offers the following
43
- features:
44
-
45
- - HTML and XML templating using plain Ruby syntax
46
- - Minimal boilerplate
47
- - Mix logic and tags freely
48
- - Automatic HTML and XML escaping
49
- - Composable components
50
- - Explicit parameter passing to nested components
51
- - Higher order components
52
- - Built-in support for rendering [Markdown](#emitting-markdown)
53
- - Support for namespaced extensions
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)
54
84
 
55
85
  ## Installing Papercraft
56
86
 
@@ -66,30 +96,33 @@ Or manually:
66
96
  $ gem install papercraft
67
97
  ```
68
98
 
69
- ## Getting started
99
+ ## Basic usage
70
100
 
71
- To create a template use the global method `Kernel#H`:
101
+ To create an HTML template use `Papercraft.html`:
72
102
 
73
103
  ```ruby
74
104
  require 'papercraft'
75
105
 
76
- html = H {
106
+ html = Papercraft.html {
77
107
  div(id: 'greeter') { p 'Hello!' }
78
108
  }
79
109
  ```
80
110
 
111
+ (You can also use `Papercraft.xml` and `Papercraft.json` to create XML and JSON
112
+ templates, respectively.)
113
+
81
114
  Rendering a template is done using `#render`:
82
115
 
83
116
  ```ruby
84
117
  html.render #=> "<div id="greeter"><p>Hello!</p></div>"
85
118
  ```
86
119
 
87
- ## All about tags
120
+ ## Adding tags
88
121
 
89
122
  Tags are added using unqualified method calls, and can be nested using blocks:
90
123
 
91
124
  ```ruby
92
- H {
125
+ Papercraft.html {
93
126
  html {
94
127
  head {
95
128
  title 'page title'
@@ -106,19 +139,19 @@ H {
106
139
  Tag methods accept a string argument, a block, or no argument at all:
107
140
 
108
141
  ```ruby
109
- H { p 'hello' }.render #=> "<p>hello</p>"
142
+ Papercraft.html { p 'hello' }.render #=> "<p>hello</p>"
110
143
 
111
- H { p { span '1'; span '2' } }.render #=> "<p><span>1</span><span>2</span></p>"
144
+ Papercraft.html { p { span '1'; span '2' } }.render #=> "<p><span>1</span><span>2</span></p>"
112
145
 
113
- H { hr() }.render #=> "<hr/>"
146
+ Papercraft.html { hr() }.render #=> "<hr/>"
114
147
  ```
115
148
 
116
149
  Tag methods also accept tag attributes, given as a hash:
117
150
 
118
151
  ```ruby
119
- H { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>
152
+ Papercraft.html { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>
120
153
 
121
- H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
154
+ Papercraft.html { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
122
155
  ```
123
156
 
124
157
  ## Template parameters
@@ -128,24 +161,24 @@ parameters are specified as block parameters, and are passed to the template on
128
161
  rendering:
129
162
 
130
163
  ```ruby
131
- greeting = H { |name| h1 "Hello, #{name}!" }
164
+ greeting = Papercraft.html { |name| h1 "Hello, #{name}!" }
132
165
  greeting.render('world') #=> "<h1>Hello, world!</h1>"
133
166
  ```
134
167
 
135
168
  Templates can also accept named parameters:
136
169
 
137
170
  ```ruby
138
- greeting = H { |name:| h1 "Hello, #{name}!" }
171
+ greeting = Papercraft.html { |name:| h1 "Hello, #{name}!" }
139
172
  greeting.render(name: 'world') #=> "<h1>Hello, world!</h1>"
140
173
  ```
141
174
 
142
- ## Logic in templates
175
+ ## Template logic
143
176
 
144
177
  Since Papercraft templates are just a bunch of Ruby, you can easily write your
145
178
  view logic right in the template:
146
179
 
147
180
  ```ruby
148
- H { |user = nil|
181
+ Papercraft.html { |user = nil|
149
182
  if user
150
183
  span "Hello, #{user.name}!"
151
184
  else
@@ -159,7 +192,7 @@ H { |user = nil|
159
192
  Templates can also accept and render blocks by using `emit_yield`:
160
193
 
161
194
  ```ruby
162
- page = H {
195
+ page = Papercraft.html {
163
196
  html {
164
197
  body { emit_yield }
165
198
  }
@@ -169,27 +202,27 @@ page = H {
169
202
  page.render { h1 'hi' }
170
203
  ```
171
204
 
172
- ## Plain procs as components
205
+ ## Plain procs as templates
173
206
 
174
207
  With Papercraft you can write a template as a plain Ruby proc, and later render
175
- it by passing it as a block to `H`:
208
+ it by passing it as a block to `Papercraft.html`:
176
209
 
177
210
  ```ruby
178
211
  greeting = proc { |name| h1 "Hello, #{name}!" }
179
- H(&greeting).render('world')
212
+ Papercraft.html(&greeting).render('world')
180
213
  ```
181
214
 
182
215
  Components can also be expressed using lambda notation:
183
216
 
184
217
  ```ruby
185
218
  greeting = ->(name) { h1 "Hello, #{name}!" }
186
- H(&greeting).render('world')
219
+ Papercraft.html(&greeting).render('world')
187
220
  ```
188
221
 
189
- ## Component composition
222
+ ## Template composition
190
223
 
191
- Papercraft makes it easy to compose multiple components into a whole HTML
192
- 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
193
226
  example shows.
194
227
 
195
228
  ```ruby
@@ -210,26 +243,26 @@ ItemList = ->(items) {
210
243
  }
211
244
  }
212
245
 
213
- page = H { |title, items|
246
+ page = Papercraft.html { |title, items|
214
247
  html5 {
215
248
  head { Title(title) }
216
249
  body { ItemList(items) }
217
250
  }
218
251
  }
219
252
 
220
- page.render('Hello from components', [
253
+ page.render('Hello from composed templates', [
221
254
  { id: 1, text: 'foo', checked: false },
222
255
  { id: 2, text: 'bar', checked: true }
223
256
  ])
224
257
  ```
225
258
 
226
- In addition to using components defined as constants, you can also use
227
- 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:
228
261
 
229
262
  ```ruby
230
263
  greeting = -> { span "Hello, world" }
231
264
 
232
- H {
265
+ Papercraft.html {
233
266
  div {
234
267
  emit greeting
235
268
  }
@@ -239,62 +272,62 @@ H {
239
272
  ## Parameter and block application
240
273
 
241
274
  Parameters and blocks can be applied to a template without it being rendered, by
242
- using `#apply`. This mechanism is what allows component composition and the
243
- creation of higher-order components.
275
+ using `#apply`. This mechanism is what allows template composition and the
276
+ creation of higher-order templates.
244
277
 
245
- The `#apply` method returns a new component which applies the given parameters and
246
- 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:
247
280
 
248
281
  ```ruby
249
282
  # parameter application
250
- hello = H { |name| h1 "Hello, #{name}!" }
283
+ hello = Papercraft.html { |name| h1 "Hello, #{name}!" }
251
284
  hello_world = hello.apply('world')
252
285
  hello_world.render #=> "<h1>Hello, world!</h1>"
253
286
 
254
287
  # block application
255
- div_wrap = H { div { emit_yield } }
288
+ div_wrap = Papercraft.html { div { emit_yield } }
256
289
  wrapped_h1 = div_wrap.apply { h1 'hi' }
257
290
  wrapped_h1.render #=> "<div><h1>hi</h1></div>"
258
291
 
259
- # wrap a component
292
+ # wrap a template
260
293
  wrapped_hello_world = div_wrap.apply(&hello_world)
261
294
  wrapped_hello_world.render #=> "<div><h1>Hello, world!</h1></div>"
262
295
  ```
263
296
 
264
- ## Higher-order components
297
+ ## Higher-order templates
265
298
 
266
- Papercraft also lets you create higher-order components (HOCs), that is,
267
- components that take other components as parameters, or as blocks. Higher-order
268
- components are handy for creating layouts, wrapping components in arbitrary
269
- 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.
270
303
 
271
- Here is a HOC that takes a component as parameter:
304
+ Here is a higher-order template that takes a template as parameter:
272
305
 
273
306
  ```ruby
274
- div_wrap = H { |inner| div { emit inner } }
275
- greeter = H { h1 'hi' }
307
+ div_wrap = Papercraft.html { |inner| div { emit inner } }
308
+ greeter = Papercraft.html { h1 'hi' }
276
309
  wrapped_greeter = div_wrap.apply(greeter)
277
310
  wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
278
311
  ```
279
312
 
280
- 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:
281
314
 
282
315
  ```ruby
283
- div_wrap = H { div { emit_yield } }
316
+ div_wrap = Papercraft.html { div { emit_yield } }
284
317
  wrapped_greeter = div_wrap.apply { h1 'hi' }
285
318
  wrapped_greeter.render #=> "<div><h1>hi</h1></div>"
286
319
  ```
287
320
 
288
321
  ## Layout template composition
289
322
 
290
- 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
291
324
  layouts. Suppose we have a website with a number of different layouts, and we'd
292
325
  like to avoid having to repeat the same code in the different layouts. We can do
293
326
  this by creating a `default` page template that takes a block, then use `#apply`
294
327
  to create the other templates:
295
328
 
296
329
  ```ruby
297
- default_layout = H { |**params|
330
+ default_layout = Papercraft.html { |**params|
298
331
  html5 {
299
332
  head {
300
333
  title: params[:title]
@@ -323,7 +356,7 @@ article_layout.render(
323
356
  Raw HTML can be emitted using `#emit`:
324
357
 
325
358
  ```ruby
326
- wrapped = H { |html| div { emit html } }
359
+ wrapped = Papercraft.html { |html| div { emit html } }
327
360
  wrapped.render("<h1>hi</h1>") #=> "<div><h1>hi</h1></div>"
328
361
  ```
329
362
 
@@ -333,7 +366,7 @@ To emit a string with proper HTML encoding, without wrapping it in an HTML
333
366
  element, use `#text`:
334
367
 
335
368
  ```ruby
336
- H { str 'hi&lo' }.render #=> "hi&amp;lo"
369
+ Papercraft.html { str 'hi&lo' }.render #=> "hi&amp;lo"
337
370
  ```
338
371
 
339
372
  ## Emitting Markdown
@@ -343,7 +376,7 @@ Markdown is rendered using the
343
376
  `#emit_markdown`:
344
377
 
345
378
  ```ruby
346
- template = H { |md| div { emit_markdown md } }
379
+ template = Papercraft.html { |md| div { emit_markdown md } }
347
380
  template.render("Here's some *Markdown*") #=> "<div><p>Here's some <em>Markdown</em><p>\n</div>"
348
381
  ```
349
382
 
@@ -352,10 +385,18 @@ options](https://kramdown.gettalong.org/options.html#available-options) can be
352
385
  specified by adding them to the `#emit_markdown` call:
353
386
 
354
387
  ```ruby
355
- template = H { |md| div { emit_markdown md, auto_ids: false } }
388
+ template = Papercraft.html { |md| div { emit_markdown md, auto_ids: false } }
356
389
  template.render("# title") #=> "<div><h1>title</h1></div>"
357
390
  ```
358
391
 
392
+ The `#emit_markdown` method is available only to HTML templates. If you need to
393
+ render markdown in XML or JSON templates (usually for implementing RSS or JSON
394
+ feeds), you can use `Papercraft.markdown` directly:
395
+
396
+ ```ruby
397
+ Papercraft.markdown('# title') #=> "<h1>title</h1>"
398
+ ```
399
+
359
400
  The default Kramdown options are:
360
401
 
361
402
  ```ruby
@@ -368,17 +409,33 @@ The default Kramdown options are:
368
409
  ```
369
410
 
370
411
  The deafult options can be configured by accessing
371
- `Papercraft::HTML.kramdown_options`, e.g.:
412
+ `Papercraft.default_kramdown_options`, e.g.:
372
413
 
373
414
  ```ruby
374
- Papercraft::HTML.kramdown_options[:auto_ids] = false
415
+ Papercraft.default_kramdown_options[:auto_ids] = false
416
+ ```
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
375
432
  ```
376
433
 
377
434
  ## Deferred evaluation
378
435
 
379
436
  Deferred evaluation allows deferring the rendering of parts of a template until
380
- the last moment, thus allowing an inner component to manipulate the state of the
381
- 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
382
439
  include any markup in the provided block. This technique, in in conjunction with
383
440
  holding state in instance variables, is an alternative to passing parameters,
384
441
  which can be limiting in some situations.
@@ -387,15 +444,15 @@ A few use cases for deferred evaulation come to mind:
387
444
 
388
445
  - Setting the page title.
389
446
  - Adding a flash message to a page.
390
- - 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
391
448
  page.
392
449
 
393
450
  The last use case is particularly interesting. Imagine a `DependencyMananger`
394
- 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
395
452
  integrated into the page, and adds them to the page's `<head>` element:
396
453
 
397
454
  ```ruby
398
- default_layout = H { |**args|
455
+ default_layout = Papercraft.html { |**args|
399
456
  @dependencies = DependencyMananger.new
400
457
  head {
401
458
  defer { emit @dependencies.head_markup }
@@ -481,7 +538,7 @@ The call to `Papercraft::extension` lets us access the different methods of
481
538
  we'll be able to express the above markup as follows:
482
539
 
483
540
  ```ruby
484
- H {
541
+ Papercraft.html {
485
542
  bootstrap.card(style: 'width: 18rem') {
486
543
  bootstrap.card_title 'Card title'
487
544
  bootstrap.card_subtitle 'Card subtitle'
@@ -492,6 +549,74 @@ H {
492
549
  }
493
550
  ```
494
551
 
552
+
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:
593
+
594
+ ```ruby
595
+ Papercraft.json {
596
+ item 1
597
+ item 2
598
+ item 3
599
+ }.render #=> "[1,2,3]"
600
+ ```
601
+
602
+ Otherwise, you can create arbitrarily complex JSON structures by mixing hashes
603
+ and arrays:
604
+
605
+ ```Ruby
606
+ Papercraft.json {
607
+ foo {
608
+ bar {
609
+ item nil
610
+ item true
611
+ item 123.456
612
+ }
613
+ }
614
+ }.render #=> "{\"foo\":{\"bar\":[null,true,123.456]}}"
615
+ ```
616
+
617
+ Papercraft uses the [JSON gem](https://rubyapi.org/3.1/o/json) under the hood in
618
+ order to generate actual JSON.
619
+
495
620
  ## API Reference
496
621
 
497
622
  The API reference for this library can be found
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kramdown'
4
- require 'rouge'
5
- require 'kramdown-parser-gfm'
6
-
7
3
  module Papercraft
8
4
  # HTML Markup extensions
9
5
  module HTML
@@ -83,41 +79,7 @@ module Papercraft
83
79
  # @param **opts [Hash] Kramdown options
84
80
  # @return [void]
85
81
  def emit_markdown(markdown, **opts)
86
- emit Kramdown::Document.new(markdown, **kramdown_options(opts)).to_html
87
- end
88
-
89
- class << self
90
- # Returns the default Kramdown options used for converting Markdown to
91
- # HTML.
92
- #
93
- # @return [Hash] Default Kramdown options
94
- def kramdown_options
95
- @kramdown_options ||= {
96
- entity_output: :numeric,
97
- syntax_highlighter: :rouge,
98
- input: 'GFM',
99
- hard_wrap: false
100
- }
101
- end
102
-
103
- # Sets the default Kramdown options used for converting Markdown to
104
- # HTML.
105
- #
106
- # @param opts [Hash] New deafult Kramdown options
107
- # @return [Hash] New default Kramdown options
108
- def kramdown_options=(opts)
109
- @kramdown_options = opts
110
- end
111
- end
112
-
113
- private
114
-
115
- # Returns the default Kramdown options, merged with the given overrides.
116
- #
117
- # @param opts [Hash] Kramdown option overrides
118
- # @return [Hash] Merged Kramdown options
119
- def kramdown_options(opts)
120
- HTML.kramdown_options.merge(**opts)
82
+ emit Papercraft.markdown(markdown, **opts)
121
83
  end
122
84
  end
123
85
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Papercraft
6
+ # JSON renderer extensions
7
+ module JSON
8
+ def object_stack
9
+ @object_stack ||= [nil]
10
+ end
11
+
12
+ def with_object(&block)
13
+ object_stack << nil
14
+ instance_eval(&block)
15
+ end
16
+
17
+ def verify_array_target
18
+ case object_stack[-1]
19
+ when nil
20
+ object_stack[-1] = []
21
+ when Hash
22
+ raise "Mixing array and hash values"
23
+ end
24
+ end
25
+
26
+ def verify_hash_target
27
+ case object_stack[-1]
28
+ when nil
29
+ object_stack[-1] = {}
30
+ when Array
31
+ raise "Mixing array and hash values"
32
+ end
33
+ end
34
+
35
+ def push_array_item(value)
36
+ object_stack[-1] << value
37
+ end
38
+
39
+ def push_kv_item(key, value)
40
+ object_stack[-1][key] = value
41
+ end
42
+
43
+ def enter_object(&block)
44
+ object_stack << nil
45
+ 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
72
+ end
73
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'escape_utils'
4
+
3
5
  require_relative './html'
6
+ require_relative './json'
4
7
  require_relative './extension_proxy'
5
8
 
6
9
  module Papercraft
7
10
 
8
- # A Renderer renders a Papercraft component into a string
11
+ # A Renderer renders a Papercraft template into a string
9
12
  class Renderer
10
13
 
11
14
  class << self
@@ -40,7 +43,7 @@ module Papercraft
40
43
  #
41
44
  # Installs the given extensions, passed in the form of a Ruby hash mapping
42
45
  # methods to extension modules. The methods will be available to all
43
- # Papercraft components. Extension methods are executed in the context of
46
+ # Papercraft templates. Extension methods are executed in the context of
44
47
  # the the renderer instance, so they can look just like normal proc
45
48
  # components. In cases where method names in the module clash with HTML
46
49
  # tag names, you can use the `#tag` method to emit the relevant tag.
@@ -55,7 +58,7 @@ module Papercraft
55
58
  # end
56
59
  #
57
60
  # Papercraft.extension(components: ComponentLibrary)
58
- # H { components.card('Foo', '**Bar**') }
61
+ # Papercraft.html { components.card('Foo', '**Bar**') }
59
62
  #
60
63
  # @param map [Hash] hash mapping methods to extension modules
61
64
  # @return [void]
@@ -210,20 +213,21 @@ module Papercraft
210
213
  # `#to_s` which is then added to the rendering buffer, without any escaping.
211
214
  #
212
215
  # greeter = proc { |name| h1 "Hello, #{name}!" }
213
- # H { emit(greeter, 'world') }.render #=> "<h1>Hello, world!</h1>"
216
+ # Papercraft.html { emit(greeter, 'world') }.render #=> "<h1>Hello, world!</h1>"
214
217
  #
215
- # H { emit 'hi&<bye>' }.render #=> "hi&<bye>"
218
+ # Papercraft.html { emit 'hi&<bye>' }.render #=> "hi&<bye>"
216
219
  #
217
- # H { emit nil }.render #=> ""
220
+ # Papercraft.html { emit nil }.render #=> ""
218
221
  #
219
- # @param o [Proc, Papercraft::Component, String] emitted object
222
+ # @param o [Proc, Papercraft::Template, String] emitted object
220
223
  # @param *a [Array<any>] arguments to pass to a proc
221
224
  # @param **b [Hash] named arguments to pass to a proc
222
225
  # @return [void]
223
- def emit(o, *a, **b)
226
+ def emit(o, *a, **b, &block)
224
227
  case o
225
228
  when ::Proc
226
229
  Renderer.verify_proc_parameters(o, a, b)
230
+ push_emit_yield_block(block) if block
227
231
  instance_exec(*a, **b, &o)
228
232
  when nil
229
233
  else
@@ -234,7 +238,7 @@ module Papercraft
234
238
 
235
239
  # Emits a block supplied using `Component#apply` or `Component#render`.
236
240
  #
237
- # div_wrap = H { |*args| div { emit_yield(*args) } }
241
+ # div_wrap = Papercraft.html { |*args| div { emit_yield(*args) } }
238
242
  # greeter = div_wrap.apply { |name| h1 "Hello, #{name}!" }
239
243
  # greeter.render('world') #=> "<div><h1>Hello, world!</h1></div>"
240
244
  #
@@ -249,14 +253,14 @@ module Papercraft
249
253
  end
250
254
 
251
255
  # Defers the given block to be evaluated later. Deferred evaluation allows
252
- # Papercraft components to inject state into sibling components, regardless
256
+ # Papercraft templates to inject state into sibling components, regardless
253
257
  # of the component's order in the container component. For example, a nested
254
258
  # component may set an instance variable used by another component. This is
255
259
  # an elegant solution to the problem of setting the HTML page's title, or
256
260
  # adding elements to the `<head>` section. Here's how a title can be
257
261
  # controlled from a nested component:
258
262
  #
259
- # layout = H {
263
+ # layout = Papercraft.html {
260
264
  # html {
261
265
  # head {
262
266
  # defer { title @title }
@@ -380,4 +384,8 @@ module Papercraft
380
384
  EscapeUtils.escape_xml(text.to_s)
381
385
  end
382
386
  end
387
+
388
+ class JSONRenderer < Renderer
389
+ include JSON
390
+ end
383
391
  end
@@ -4,28 +4,33 @@ require_relative './html'
4
4
 
5
5
  module Papercraft
6
6
 
7
- # Component represents a distinct, reusable HTML template. A component can
8
- # include other components, and also be nested inside other components.
7
+ # Template represents a distinct, reusable HTML template. A template can
8
+ # include other templates, and also be nested inside other templates.
9
9
  #
10
- # Since in Papercraft HTML is expressed using blocks (or procs,) the Component
10
+ # Since in Papercraft HTML is expressed using blocks (or procs,) the Template
11
11
  # class is simply a special kind of Proc, which has some enhanced
12
12
  # capabilities, allowing it to be easily composed in a variety of ways.
13
13
  #
14
- # Components are usually created using the global methods `H` or `X`, for HTML
15
- # or XML templates, respectively:
14
+ # Templates are usually created using the class methods `html`, `xml` or
15
+ # `json`, for HTML, XML or JSON templates, respectively:
16
16
  #
17
- # greeter = H { |name| h1 "Hello, #{name}!" }
17
+ # greeter = Papercraft.html { |name| h1 "Hello, #{name}!" }
18
18
  # greeter.render('world') #=> "<h1>Hello, world!</h1>"
19
19
  #
20
- # Components can also be created using the normal constructor:
20
+ # Templates can also be created using the normal constructor:
21
21
  #
22
- # greeter = Papercraft::Component.new { |name| h1 "Hello, #{name}!" }
22
+ # greeter = Papercraft::Template.new(mode: :html) { |name| h1 "Hello, #{name}!" }
23
23
  # greeter.render('world') #=> "<h1>Hello, world!</h1>"
24
24
  #
25
- # In the component block, HTML elements are created by simply calling
25
+ # The different methods for creating templates can also take a custom MIME
26
+ # type, by passing a `mime_type` named argument:
27
+ #
28
+ # json = Papercraft.json(mime_type: 'application/feed+json') { ... }
29
+ #
30
+ # In the template block, HTML elements are created by simply calling
26
31
  # unqualified methods:
27
32
  #
28
- # page_layout = H {
33
+ # page_layout = Papercraft.html {
29
34
  # html5 {
30
35
  # head {
31
36
  # title 'foo'
@@ -36,31 +41,31 @@ module Papercraft
36
41
  # }
37
42
  # }
38
43
  #
39
- # Papercraft components can take explicit parameters in order to render
44
+ # Papercraft templates can take explicit parameters in order to render
40
45
  # dynamic content. This can be in the form of regular or named parameters. The
41
46
  # `greeter` template shown above takes a single `name` parameter. Here's how a
42
- # anchor component could be implemented with named parameters:
47
+ # anchor template could be implemented with named parameters:
43
48
  #
44
- # anchor = H { |uri: , text: | a(text, href: uri) }
49
+ # anchor = Papercraft.html { |uri: , text: | a(text, href: uri) }
45
50
  #
46
- # The above component could later be rendered by passing the needed arguments:
51
+ # The above template could later be rendered by passing the needed arguments:
47
52
  #
48
53
  # anchor.render(uri: 'https://example.com', text: 'Example')
49
54
  #
50
- # ## Component Composition
55
+ # ## Template Composition
51
56
  #
52
- # A component can be included in another component using the `emit` method:
57
+ # A template can be included in another template using the `emit` method:
53
58
  #
54
- # links = H {
59
+ # links = Papercraft.html {
55
60
  # emit anchor, uri: '/posts', text: 'Posts'
56
61
  # emit anchor, uri: '/archive', text: 'Archive'
57
62
  # emit anchor, uri: '/about', text: 'About'
58
63
  # }
59
64
  #
60
- # Another way of composing components is to pass the components themselves as
65
+ # Another way of composing templates is to pass the templates themselves as
61
66
  # parameters:
62
67
  #
63
- # links = H { |anchors|
68
+ # links = Papercraft.html { |anchors|
64
69
  # anchors.each { |a| emit a }
65
70
  # }
66
71
  # links.render([
@@ -69,8 +74,8 @@ module Papercraft
69
74
  # anchor.apply(uri: '/about', text: 'About')
70
75
  # ])
71
76
  #
72
- # The `#apply` method creates a new component, applying the given parameters
73
- # such that the component can be rendered without parameters:
77
+ # The `#apply` method creates a new template, applying the given parameters
78
+ # such that the template can be rendered without parameters:
74
79
  #
75
80
  # links_with_anchors = links.apply([
76
81
  # anchor.apply(uri: '/posts', text: 'Posts'),
@@ -79,19 +84,27 @@ module Papercraft
79
84
  # ])
80
85
  # links_with_anchors.render
81
86
  #
82
- class Component < Proc
87
+ class Template < Proc
83
88
 
84
89
  # Determines the rendering mode: `:html` or `:xml`.
85
90
  attr_accessor :mode
86
91
 
87
- # Initializes a component with the given block. The rendering mode (HTML or
92
+ STOCK_MIME_TYPE = {
93
+ html: 'text/html',
94
+ xml: 'application/xml',
95
+ json: 'application/json'
96
+ }.freeze
97
+
98
+ # Initializes a template with the given block. The rendering mode (HTML or
88
99
  # XML) can be passed in the `mode:` parameter. If `mode:` is not specified,
89
- # the component defaults to HTML.
100
+ # the template defaults to HTML.
90
101
  #
91
102
  # @param mode [:html, :xml] rendering mode
103
+ # @param mime_type [String, nil] the template's mime type (nil for default)
92
104
  # @param block [Proc] nested HTML block
93
- def initialize(mode: :html, &block)
105
+ def initialize(mode: :html, mime_type: nil, &block)
94
106
  @mode = mode
107
+ @mime_type = mime_type || STOCK_MIME_TYPE[mode]
95
108
  super(&block)
96
109
  end
97
110
 
@@ -111,11 +124,11 @@ module Papercraft
111
124
  end.to_s
112
125
  end
113
126
 
114
- # Creates a new component, applying the given parameters and or block to the
127
+ # Creates a new template, applying the given parameters and or block to the
115
128
  # current one. Application is one of the principal methods of composing
116
- # components, particularly when passing inner components as blocks:
129
+ # templates, particularly when passing inner templates as blocks:
117
130
  #
118
- # article_wrapper = H {
131
+ # article_wrapper = Papercraft.html {
119
132
  # article {
120
133
  # emit_yield
121
134
  # }
@@ -128,29 +141,35 @@ module Papercraft
128
141
  # @param *a [<any>] normal parameters
129
142
  # @param **b [Hash] named parameters
130
143
  # @param &block [Proc] inner block
131
- # @return [Papercraft::Component] applied component
144
+ # @return [Papercraft::Template] applied template
132
145
  def apply(*a, **b, &block)
133
146
  template = self
134
- Component.new(&proc do |*x, **y|
147
+ Template.new(mode: @mode, mime_type: @mime_type, &proc do |*x, **y|
135
148
  push_emit_yield_block(block) if block
136
149
  instance_exec(*a, *x, **b, **y, &template)
137
150
  end)
138
151
  end
139
152
 
140
153
  # Returns the Renderer class used for rendering the templates, according to
141
- # the component's mode.
154
+ # the template's mode.
142
155
  #
143
- # @return [Papercraft::Renderer] Renderer used for rendering the component
156
+ # @return [Papercraft::Renderer] Renderer used for rendering the template
144
157
  def renderer_class
145
158
  case @mode
146
159
  when :html
147
160
  HTMLRenderer
148
161
  when :xml
149
162
  XMLRenderer
163
+ when :json
164
+ JSONRenderer
150
165
  else
151
166
  raise "Invalid mode #{@mode.inspect}"
152
167
  end
153
168
  end
169
+
170
+ def mime_type
171
+ @mime_type
172
+ end
154
173
 
155
174
  # def compile
156
175
  # Papercraft::Compiler.new.compile(self)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Papercraft
4
- VERSION = '0.15'
4
+ VERSION = '0.19'
5
5
  end
data/lib/papercraft.rb CHANGED
@@ -1,60 +1,110 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'escape_utils'
3
+ require 'kramdown'
4
+ require 'rouge'
5
+ require 'kramdown-parser-gfm'
4
6
 
5
- require_relative 'papercraft/component'
7
+ require_relative 'papercraft/template'
6
8
  require_relative 'papercraft/renderer'
7
9
  require_relative 'papercraft/encoding'
8
10
  # require_relative 'papercraft/compiler'
9
11
 
10
- # Papercraft is a component-based HTML templating library
12
+
13
+ # Papercraft is a composable templating library
11
14
  module Papercraft
12
15
  # Exception class used to signal templating-related errors
13
16
  class Error < RuntimeError; end
14
-
15
- # Installs one or more extensions. Extensions enhance templating capabilities
16
- # by adding namespaced methods to emplates. An extension is implemented as a
17
- # Ruby module containing one or more methods. Each method in the extension
18
- # module can be used to render a specific HTML element or a set of elements.
19
- #
20
- # This is a convenience method. For more information on using Papercraft
21
- # extensions, see `Papercraft::Renderer::extension`
22
- #
23
- # @param map [Hash] hash mapping methods to extension modules
24
- # @return [void]
25
- def self.extension(map)
26
- Renderer.extension(map)
27
- end
28
- end
29
-
30
- # Kernel extensions
31
- module ::Kernel
32
17
 
33
- # Creates a new papercraft component. `#H` can take either a proc argument or
34
- # a block. In both cases, the proc is converted to a `Papercraft::Component`.
35
- #
36
- # H(proc { h1 'hi' }).render #=> "<h1>hi</h1>"
37
- # H { h1 'hi' }.render #=> "<h1>hi</h1>"
38
- #
39
- # @param template [Proc] template block
40
- # @return [Papercraft::Component] Papercraft component
41
- def H(o = nil, &template)
42
- return o if o.is_a?(Papercraft::Component)
43
- template ||= o
44
- Papercraft::Component.new(mode: :html, &template)
45
- end
18
+ class << self
19
+
20
+ # Installs one or more extensions. Extensions enhance templating capabilities
21
+ # by adding namespaced methods to emplates. An extension is implemented as a
22
+ # Ruby module containing one or more methods. Each method in the extension
23
+ # module can be used to render a specific HTML element or a set of elements.
24
+ #
25
+ # This is a convenience method. For more information on using Papercraft
26
+ # extensions, see `Papercraft::Renderer::extension`
27
+ #
28
+ # @param map [Hash] hash mapping methods to extension modules
29
+ # @return [void]
30
+ def extension(map)
31
+ Renderer.extension(map)
32
+ end
33
+
34
+ # Creates a new papercraft template. `Papercraft.html` can take either a proc
35
+ # argument or a block. In both cases, the proc is converted to a
36
+ # `Papercraft::Template`.
37
+ #
38
+ # Papercraft.html(proc { h1 'hi' }).render #=> "<h1>hi</h1>"
39
+ # Papercraft.html { h1 'hi' }.render #=> "<h1>hi</h1>"
40
+ #
41
+ # @param template [Proc] template block
42
+ # @return [Papercraft::Template] Papercraft template
43
+ def html(o = nil, mime_type: nil, &template)
44
+ return o if o.is_a?(Papercraft::Template)
45
+ template ||= o
46
+ Papercraft::Template.new(mode: :html, mime_type: mime_type, &template)
47
+ end
48
+
49
+ # Creates a new Papercraft template in XML mode. `Papercraft.xml` can take
50
+ # either a proc argument or a block. In both cases, the proc is converted to a
51
+ # `Papercraft::Template`.
52
+ #
53
+ # Papercraft.xml(proc { item 'foo' }).render #=> "<item>foo</item>"
54
+ # Papercraft.xml { item 'foo' }.render #=> "<item>foo</item>"
55
+ #
56
+ # @param template [Proc] template block
57
+ # @return [Papercraft::Template] Papercraft template
58
+ def xml(o = nil, mime_type: nil, &template)
59
+ return o if o.is_a?(Papercraft::Template)
60
+ template ||= o
61
+ Papercraft::Template.new(mode: :xml, mime_type: mime_type, &template)
62
+ end
63
+
64
+ # Creates a new Papercraft template in JSON mode. `Papercraft.json` can take
65
+ # either a proc argument or a block. In both cases, the proc is converted to a
66
+ # `Papercraft::Template`.
67
+ #
68
+ # Papercraft.json(proc { item 42 }).render #=> "[42]"
69
+ # Papercraft.json { foo 'bar' }.render #=> "{\"foo\": \"bar\"}"
70
+ #
71
+ # @param template [Proc] template block
72
+ # @return [Papercraft::Template] Papercraft template
73
+ def json(o = nil, mime_type: nil, &template)
74
+ return o if o.is_a?(Papercraft::Template)
75
+ template ||= o
76
+ Papercraft::Template.new(mode: :json, mime_type: mime_type, &template)
77
+ end
78
+
79
+ # Renders Markdown into HTML. The `opts` argument will be merged with the
80
+ # default Kramdown options in order to change the rendering behaviour.
81
+ #
82
+ # @param markdown [String] Markdown
83
+ # @param **opts [Hash] Kramdown option overrides
84
+ # @return [String] HTML
85
+ def markdown(markdown, **opts)
86
+ opts = default_kramdown_options.merge(opts)
87
+ Kramdown::Document.new(markdown, **opts).to_html
88
+ end
89
+
90
+ # Returns the default Kramdown options used for rendering Markdown.
91
+ #
92
+ # @return [Hash] Kramdown options
93
+ def default_kramdown_options
94
+ @default_kramdown_options ||= {
95
+ entity_output: :numeric,
96
+ syntax_highlighter: :rouge,
97
+ input: 'GFM',
98
+ hard_wrap: false
99
+ }
100
+ end
46
101
 
47
- # Creates a new papercraft component in XML mode. `#X` can take either a proc argument or
48
- # a block. In both cases, the proc is converted to a `Papercraft::Component`.
49
- #
50
- # X(proc { item 'foo' }).render #=> "<item>foo</item>"
51
- # X { item 'foo' }.render #=> "<item>foo</item>"
52
- #
53
- # @param template [Proc] template block
54
- # @return [Papercraft::Component] Papercraft component
55
- def X(o = nil, &template)
56
- return o if o.is_a?(Papercraft::Component)
57
- template ||= o
58
- Papercraft::Component.new(mode: :xml, &template)
102
+ # Sets the default Kramdown options used for rendering Markdown.
103
+ #
104
+ # @param opts [Hash] Kramdown options
105
+ # @return [void]
106
+ def default_kramdown_options=(opts)
107
+ @default_kramdown_options = opts
108
+ end
59
109
  end
60
110
  end
data/papercraft.png ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: papercraft
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.15'
4
+ version: '0.19'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-20 00:00:00.000000000 Z
11
+ date: 2022-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: escape_utils
@@ -128,17 +128,20 @@ executables: []
128
128
  extensions: []
129
129
  extra_rdoc_files:
130
130
  - README.md
131
+ - papercraft.png
131
132
  files:
132
133
  - CHANGELOG.md
133
134
  - README.md
134
135
  - lib/papercraft.rb
135
136
  - lib/papercraft/compiler.rb
136
- - lib/papercraft/component.rb
137
137
  - lib/papercraft/encoding.rb
138
138
  - lib/papercraft/extension_proxy.rb
139
139
  - lib/papercraft/html.rb
140
+ - lib/papercraft/json.rb
140
141
  - lib/papercraft/renderer.rb
142
+ - lib/papercraft/template.rb
141
143
  - lib/papercraft/version.rb
144
+ - papercraft.png
142
145
  homepage: http://github.com/digital-fabric/papercraft
143
146
  licenses:
144
147
  - MIT