papercraft 0.8.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 309174da790e301142b03cc8aad3e966c6b1482d514da8d3910c2e84f6981af6
4
- data.tar.gz: 64fb5b791fce05c0347ba21ccf70160323f70ad708ece9c14222b8544efdf8e0
3
+ metadata.gz: 757137ea8d3a2b5d5d329b7e841bad13cc26ea3486ebcb216f1ef016d7d3ff4a
4
+ data.tar.gz: 281ee3b2911b1f4e1ff3337dbefef407ce07b17e21acb949dce158a93d8758a5
5
5
  SHA512:
6
- metadata.gz: d1f56c25e4854cb6b0ffce1d149d831d51d4141159a44fd6a962c27d9e0878b33e9438d9ad621c0c30a42dfa853b96a26b20c6ebe42243844be2b42d4e2901ef
7
- data.tar.gz: 46952658d3e237f784cfd9b608c82b001fdd188b788c931f0a1a3669d5800d8ed3ef89623f88ff9a617237fbfbbd3a0f9f5b30bfc693b2e6f0b5659f66afa41b
6
+ metadata.gz: 22225498fce014a17962541bac3c17f20243239b709e191e75fa2f1abbc0f7f7fdef3b0c4d110753f0de1c2ca4efc19f516cb797a02cbafa70af95085449096e
7
+ data.tar.gz: 57bb7aae102900de14ba9d8f4a0e2be83978e5106ff098ffda5acf39431273fd76be646d012da72094715ad8a3da7fd81a485e51151c74df94091a91e72866f4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.10.1 2021-12-25
2
+
3
+ - Fix tag rendering with empty text in Ruby 3.0
4
+
5
+ ## 0.10 2021-12-25
6
+
7
+ - Add support for extensions
8
+
9
+ ## 0.9 2021-12-23
10
+
11
+ - Add support for emitting Markdown
12
+ - Add support for passing proc as argument to `#H` and `#X`
13
+ - Deprecate `Encoding` module
14
+
1
15
  ## 0.8.1 2021-12-22
2
16
 
3
17
  - Fix gemspec
data/README.md CHANGED
@@ -1,30 +1,56 @@
1
- # Papercraft - Composable HTML templating for Ruby
1
+ <h1 align="center">
2
+ Papercraft
3
+ </h1>
4
+
5
+ <h4 align="center">Composable HTML templating for Ruby</h4>
6
+
7
+ <p align="center">
8
+ <a href="http://rubygems.org/gems/papercraft">
9
+ <img src="https://badge.fury.io/rb/papercraft.svg" alt="Ruby gem">
10
+ </a>
11
+ <a href="https://github.com/digital-fabric/papercraft/actions?query=workflow%3ATests">
12
+ <img src="https://github.com/digital-fabric/papercraft/workflows/Tests/badge.svg" alt="Tests">
13
+ </a>
14
+ <a href="https://github.com/digital-fabric/papercraft/blob/master/LICENSE">
15
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
16
+ </a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://www.rubydoc.info/gems/papercraft">API reference</a>
21
+ </p>
2
22
 
3
- [INSTALL](#installing-papercraft) |
4
- [TUTORIAL](#getting-started) |
5
- [EXAMPLES](examples) |
6
- [REFERENCE](#api-reference)
23
+ # Papercraft - Composable HTML templating for Ruby
7
24
 
8
25
  ## What is Papercraft?
9
26
 
27
+ ```ruby
28
+ require 'papercraft'
29
+
30
+ page = H { |*args|
31
+ html {
32
+ head { }
33
+ body { emit_yield *args }
34
+ }
35
+ }
36
+
37
+ hello = H.apply { |name| h1 "Hello, #{name}!" }
38
+ hello.render('world')
39
+ #=> "<html><head/><body><h1>Hello, world!</h1></body></html>"
40
+ ```
41
+
10
42
  Papercraft is an HTML templating engine for Ruby that offers the following
11
43
  features:
12
44
 
13
- - HTML templating using plain Ruby syntax
45
+ - HTML and XML templating using plain Ruby syntax
14
46
  - Minimal boilerplate
15
47
  - Mix logic and tags freely
16
- - Use global and local contexts to pass values to reusable components
17
- - Automatic HTML escaping
48
+ - Automatic HTML and XML escaping
18
49
  - Composable components
50
+ - Explicit parameter passing to nested components
19
51
  - 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.
52
+ - Built-in support for rendering [Markdown](#emitting-markdown)
53
+ - Support for namespaced extensions
28
54
 
29
55
  ## Installing Papercraft
30
56
 
@@ -42,42 +68,25 @@ $ gem install papercraft
42
68
 
43
69
  ## Getting started
44
70
 
45
- To use Papercraft in your code just require it:
71
+ To create a template use the global method `Kernel#H`:
46
72
 
47
73
  ```ruby
48
74
  require 'papercraft'
49
- ```
50
-
51
- To create a template use `Papercraft.new` or the global method `Kernel#H`:
52
75
 
53
- ```ruby
54
- # can also use Papercraft.new
55
76
  html = H {
56
- div { p 'hello' }
77
+ div(id: 'greeter') { p 'Hello!' }
57
78
  }
58
79
  ```
59
80
 
60
- ## Rendering a template
61
-
62
- To render a Papercraft template use the `#render` method:
81
+ Rendering a template is done using `#render`:
63
82
 
64
83
  ```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>"
84
+ html.render #=> "<div id="greeter"><p>Hello!</p></div>"
76
85
  ```
77
86
 
78
87
  ## All about tags
79
88
 
80
- Tags are added using unqualified method calls, and are nested using blocks:
89
+ Tags are added using unqualified method calls, and can be nested using blocks:
81
90
 
82
91
  ```ruby
83
92
  H {
@@ -114,8 +123,9 @@ H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</
114
123
 
115
124
  ## Template parameters
116
125
 
117
- Template parameters are specified as block parameters, and are passed to the
118
- template on rendering:
126
+ In Papercraft, parameters are always passed explicitly. This means that template
127
+ parameters are specified as block parameters, and are passed to the template on
128
+ rendering:
119
129
 
120
130
  ```ruby
121
131
  greeting = H { |name| h1 "Hello, #{name}!" }
@@ -328,110 +338,109 @@ H { str 'hi&lo' }.render #=> "hi&amp;lo"
328
338
 
329
339
  ## Emitting Markdown
330
340
 
331
- To emit Markdown, use `#emit_markdown`:
341
+ Markdown is rendered using the
342
+ [Kramdown](https://kramdown.gettalong.org/index.html) gem. To emit Markdown, use
343
+ `#emit_markdown`:
332
344
 
333
345
  ```ruby
334
346
  template = H { |md| div { emit_markdown md } }
335
- template.render("Here's some *Markdown*") #=> "<div>Here's some <em>Markdown</em></div>"
347
+ template.render("Here's some *Markdown*") #=> "<div><p>Here's some <em>Markdown</em><p>\n</div>"
336
348
  ```
337
349
 
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!
350
+ [Kramdown
351
+ options](https://kramdown.gettalong.org/options.html#available-options) can be
352
+ specified by adding them to the `#emit_markdown` call:
342
353
 
343
- ### A higher-order list component
354
+ ```ruby
355
+ template = H { |md| div { emit_markdown md, auto_ids: false } }
356
+ template.render("# title") #=> "<div><h1>title</h1></div>"
357
+ ```
344
358
 
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:
359
+ The default Kramdown options are:
348
360
 
349
361
  ```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'
362
+ {
363
+ entity_output: :numeric,
364
+ syntax_highlighter: :rouge,
365
+ input: 'GFM',
366
+ hard_wrap: false
364
367
  }
365
-
366
- def todo_list(items)
367
- H {
368
- div { List(items, TodoItem) }
369
- }
370
- end
371
368
  ```
372
369
 
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
370
+ The deafult options can be configured by accessing
371
+ `Papercraft::HTML.kramdown_options`:
410
372
 
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.
373
+ ```ruby
374
+ Papercraft::HTML.kramdown_options[:auto_ids] = false
375
+ ```
415
376
 
416
- #### `#context`
377
+ ## Papercraft extensions
378
+
379
+ Papercraft extensions are modules that contain one or more methods that can be
380
+ used to render complex HTML components. Extension modules can be used by
381
+ installing them as a namespaced extension using `Papercraft.extension`.
382
+ Extensions are particularly useful when you work with CSS frameworks such as
383
+ [Bootstrap](https://getbootstrap.com/), [Tailwind](https://tailwindui.com/) or
384
+ [Primer](https://primer.style/).
385
+
386
+ For example, to create a Bootstrap card component, the following HTML markup is
387
+ needed (example taken from the [Bootstrap
388
+ docs](https://getbootstrap.com/docs/5.1/components/card/#titles-text-and-links)):
389
+
390
+ ```html
391
+ <div class="card" style="width: 18rem;">
392
+ <div class="card-body">
393
+ <h5 class="card-title">Card title</h5>
394
+ <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
395
+ <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
396
+ <a href="#" class="card-link">Card link</a>
397
+ <a href="#" class="card-link">Another link</a>
398
+ </div>
399
+ </div>
400
+ ```
417
401
 
418
- Accesses the [global context](#global-context).
402
+ With Papercraft, we could create a `Bootstrap` extension with a `#card` method
403
+ and other associated methods:
419
404
 
420
- #### `#emit(object)` a.k.a. `#e(object)`
405
+ ```ruby
406
+ module BootstrapComponents
407
+ ...
421
408
 
422
- - `object`: `Proc`, `Papercraft` instance or `String`
409
+ def card(**props)
410
+ div(class: 'card', **props) {
411
+ div(class: 'card-body') {
412
+ emit_yield
413
+ }
414
+ }
415
+ end
423
416
 
424
- Adds the given object to the current template. If a `String` is given, it is
425
- rendered verbatim, i.e. without escaping.
417
+ def card_title(title)
418
+ h5 title, class: 'card-title'
419
+ end
426
420
 
427
- #### `html5(&block)`
421
+ ...
422
+ end
428
423
 
429
- - `block`: inner HTML block
424
+ Papercraft.extension(bootstrap: BootstrapComponents)
425
+ ```
430
426
 
431
- Adds an HTML5 `doctype` tag, followed by an `html` tag with the given block.
427
+ The call to `Papercraft.extension` lets us access the different methods of
428
+ `BootstrapComponents` by calling `#bootstrap` inside a template. With this,
429
+ we'll be able to express the above markup as follows:
432
430
 
433
- #### `#text(data)`
431
+ ```ruby
432
+ H {
433
+ bootstrap.card(style: 'width: 18rem') {
434
+ bootstrap.card_title 'Card title'
435
+ bootstrap.card_subtitle 'Card subtitle'
436
+ bootstrap.card_text 'Some quick example text to build on the card title and make up the bulk of the card''s content.'
437
+ bootstrap.card_link '#', 'Card link'
438
+ bootstrap.card_link '#', 'Another link'
439
+ }
440
+ }
441
+ ```
434
442
 
435
- - `data` - text to add
443
+ ## API Reference
436
444
 
437
- Adds text without wrapping it in a tag. The text will be escaped.
445
+ The API reference for this library can be found
446
+ [here](https://www.rubydoc.info/gems/papercraft).
@@ -14,8 +14,8 @@ module Papercraft
14
14
  # Components are usually created using the global methods `H` or `X`, for HTML
15
15
  # or XML templates, respectively:
16
16
  #
17
- # greeter = H { |name| h1 "Hello, #{name}!" } greeter.render('world') #=>
18
- # "<h1>Hello, world!</h1>"
17
+ # greeter = H { |name| h1 "Hello, #{name}!" }
18
+ # greeter.render('world') #=> "<h1>Hello, world!</h1>"
19
19
  #
20
20
  # Components can also be created using the normal constructor:
21
21
  #
@@ -25,7 +25,10 @@ module Papercraft
25
25
  # In the component block, HTML elements are created by simply calling
26
26
  # unqualified methods:
27
27
  #
28
- # page_layout = H { html5 { head { title 'foo'
28
+ # page_layout = H {
29
+ # html5 {
30
+ # head {
31
+ # title 'foo'
29
32
  # }
30
33
  # body {
31
34
  # h1 "Hello, world!"
@@ -75,7 +78,11 @@ module Papercraft
75
78
  # anchor.apply(uri: '/about', text: 'About')
76
79
  # ])
77
80
  # links_with_anchors.render
81
+ #
78
82
  class Component < Proc
83
+
84
+ # Determines the rendering mode: `:html` or `:xml`.
85
+ attr_accessor :mode
79
86
 
80
87
  # Initializes a component with the given block. The rendering mode (HTML or
81
88
  # XML) can be passed in the `mode:` parameter. If `mode:` is not specified,
@@ -113,15 +120,15 @@ module Papercraft
113
120
  # current one. Application is one of the principal methods of composing
114
121
  # components, particularly when passing inner components as blocks:
115
122
  #
116
- # article_wrapper = H {
117
- # article {
118
- # emit_yield
123
+ # article_wrapper = H {
124
+ # article {
125
+ # emit_yield
126
+ # }
127
+ # }
128
+ # wrapped_article = article_wrapper.apply {
129
+ # h1 'Article title'
119
130
  # }
120
- # }
121
- # wrapped_article = article_wrapper.apply {
122
- # h1 'Article title'
123
- # }
124
- # wrapped_article.render #=> "<article><h1>Article title</h1></article>"
131
+ # wrapped_article.render #=> "<article><h1>Article title</h1></article>"
125
132
  #
126
133
  # @param *a [<any>] normal parameters
127
134
  # @param **b [Hash] named parameters
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Papercraft
4
+
5
+ # An ExtensionProxy proxies method calls to a renderer. Extension proxies are
6
+ # used to provide an namespaced interface to Papercraft extensions. When an
7
+ # extension is installed using `Papercraft.extension`, a corresponding method
8
+ # is defined on the `Papercraft::Renderer` class that creates an extension
9
+ # proxy that gives access to the different extension methods.
10
+ class ExtensionProxy
11
+
12
+ # Initializes a new ExtensionProxy.
13
+ # @param renderer [Papercraft::Renderer] renderer to proxy to
14
+ # @param mod [Module] extension module
15
+ # @return [void]
16
+ def initialize(renderer, mod)
17
+ @renderer = renderer
18
+ extend(mod)
19
+ end
20
+
21
+ # Proxies missing methods to the renderer
22
+ # @param sym [Symbol] method name
23
+ # @param *args [Array] arguments
24
+ # @param &block [Proc] block
25
+ # @return void
26
+ def method_missing(sym, *args, &block)
27
+ @renderer.send(sym, *args, &block)
28
+ end
29
+ end
30
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './html'
3
+ require 'kramdown'
4
+ require 'rouge'
5
+ require 'kramdown-parser-gfm'
4
6
 
5
- module Papercraft
6
- # Markup extensions
7
+ module Papercraft
8
+ # HTML Markup extensions
7
9
  module HTML
8
10
  # Emits the p tag (overrides Object#p)
11
+ #
9
12
  # @param text [String] text content of tag
10
13
  # @param props [Hash] tag attributes
11
14
  # @para block [Proc] nested HTML block
@@ -16,7 +19,8 @@ module Papercraft
16
19
 
17
20
  S_HTML5_DOCTYPE = '<!DOCTYPE html>'
18
21
 
19
- # Emits an HTML5 doctype tag and an html tag with the given block
22
+ # Emits an HTML5 doctype tag and an html tag with the given block.
23
+ #
20
24
  # @param block [Proc] nested HTML block
21
25
  # @return [void]
22
26
  def html5(&block)
@@ -24,6 +28,11 @@ module Papercraft
24
28
  self.html(&block)
25
29
  end
26
30
 
31
+ # Emits a link element with a stylesheet.
32
+ #
33
+ # @param href [String] stylesheet URL
34
+ # @param custom_attributes [Hash] optional custom attributes for the link element
35
+ # @return [void]
27
36
  def link_stylesheet(href, custom_attributes = nil)
28
37
  attributes = {
29
38
  rel: 'stylesheet',
@@ -34,5 +43,28 @@ module Papercraft
34
43
  end
35
44
  link(**attributes)
36
45
  end
46
+
47
+ def emit_markdown(markdown, **opts)
48
+ emit Kramdown::Document.new(markdown, **kramdown_options(opts)).to_html
49
+ end
50
+
51
+ def kramdown_options(opts)
52
+ HTML.kramdown_options.merge(**opts)
53
+ end
54
+
55
+ class << self
56
+ def kramdown_options
57
+ @kramdown_options ||= {
58
+ entity_output: :numeric,
59
+ syntax_highlighter: :rouge,
60
+ input: 'GFM',
61
+ hard_wrap: false
62
+ }
63
+ end
64
+
65
+ def kramdown_options=(opts)
66
+ @kramdown_options = opts
67
+ end
68
+ end
37
69
  end
38
70
  end
@@ -1,11 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative './html'
4
+ require_relative './extension_proxy'
4
5
 
5
6
  module Papercraft
7
+
6
8
  # A Renderer renders a Papercraft component into a string
7
9
  class Renderer
10
+
8
11
  class << self
12
+
13
+ # Verifies that the given template proc can be called with the given
14
+ # arguments and named arguments. If the proc demands named argument keys
15
+ # that do not exist in `named_args`, `Papercraft::Error` is raised.
16
+ #
17
+ # @param template [Proc] proc to verify
18
+ # @param args [Array<any>] arguments passed to proc
19
+ # @param named_args [Hash] named arguments passed to proc
9
20
  def verify_proc_parameters(template, args, named_args)
10
21
  param_count = 0
11
22
  template.parameters.each do |(type, name)|
@@ -21,40 +32,58 @@ module Papercraft
21
32
  if param_count > args.size
22
33
  raise Papercraft::Error, "Missing template parameters"
23
34
  end
24
- end
25
- end
35
+ end
26
36
 
27
- attr_reader :context
37
+ # Installs the given extensions, mapping a method name to the extension
38
+ # module.
39
+ # @param map [Hash] hash mapping methods to extension modules
40
+ # @return [void]
41
+ def extension(map)
42
+ map.each do |sym, mod|
43
+ define_extension_method(sym, mod)
44
+ end
45
+ end
28
46
 
29
- # Initializes attributes and renders the given block
30
- # @param context [Hash] rendering context
31
- # @param block [Proc] template block
32
- # @return [void]
47
+ private
48
+
49
+ # Defines a method returning an extension proxy for the given module
50
+ # @param sym [Symbol] method name
51
+ # @param mod [Module] extension module
52
+ # @return [void]
53
+ def define_extension_method(sym, mod)
54
+ define_method(sym) do
55
+ (@extension_proxies ||= {})[mod] ||= ExtensionProxy.new(self, mod)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Initializes the renderer and evaulates the given template in the
61
+ # renderer's scope.
62
+ #
63
+ # @param &template [Proc] template block
33
64
  def initialize(&template)
34
65
  @buffer = +''
35
66
  instance_eval(&template)
36
67
  end
37
68
 
38
- # Returns the result of the rendering
69
+ # Returns the rendered template.
70
+ #
39
71
  # @return [String]
40
72
  def to_s
41
73
  @buffer
42
74
  end
43
75
 
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
76
  S_TAG_METHOD_LINE = __LINE__ + 1
53
77
  S_TAG_METHOD = <<~EOF
54
78
  S_TAG_%<TAG>s_PRE = '<%<tag>s'.tr('_', '-')
55
79
  S_TAG_%<TAG>s_CLOSE = '</%<tag>s>'.tr('_', '-')
56
80
 
57
81
  def %<tag>s(text = nil, **props, &block)
82
+ if text.is_a?(Hash) && props.empty?
83
+ props = text
84
+ text = nil
85
+ end
86
+
58
87
  @buffer << S_TAG_%<TAG>s_PRE
59
88
  emit_props(props) unless props.empty?
60
89
 
@@ -74,33 +103,42 @@ module Papercraft
74
103
  end
75
104
  EOF
76
105
 
77
- # Catches undefined tag method call and handles them by defining the method
106
+ # Catches undefined tag method call and handles it by defining the method.
107
+ #
78
108
  # @param sym [Symbol] HTML tag or component identifier
79
- # @param args [Array] method call arguments
80
- # @param block [Proc] block passed to method call
109
+ # @param args [Array] method arguments
110
+ # @param opts [Hash] named method arguments
111
+ # @param &block [Proc] block passed to method
81
112
  # @return [void]
82
113
  def method_missing(sym, *args, **opts, &block)
83
- value = @local && @local[sym]
84
- return value if value
85
-
86
114
  tag = sym.to_s
87
115
  code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
88
116
  self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
89
117
  send(sym, *args, **opts, &block)
90
118
  end
91
119
 
92
- # Emits the given object into the rendering buffer
120
+ # Emits the given object into the rendering buffer. If the given object is a
121
+ # proc or a component, `emit` will passes any additional arguments and named
122
+ # arguments to the object when rendering it. If the given object is nil,
123
+ # nothing is emitted. Otherwise, the object is converted into a string using
124
+ # `#to_s` which is then added to the rendering buffer, without any escaping.
125
+ #
126
+ # greeter = proc { |name| h1 "Hello, #{name}!" }
127
+ # H { emit(greeter, 'world') }.render #=> "<h1>Hello, world!</h1>"
128
+ #
129
+ # H { emit 'hi&<bye>' }.render #=> "hi&<bye>"
130
+ #
131
+ # H { emit nil }.render #=> ""
132
+ #
93
133
  # @param o [Proc, Papercraft::Component, String] emitted object
134
+ # @param *a [Array<any>] arguments to pass to a proc
135
+ # @param **b [Hash] named arguments to pass to a proc
94
136
  # @return [void]
95
137
  def emit(o, *a, **b)
96
138
  case o
97
139
  when ::Proc
98
140
  Renderer.verify_proc_parameters(o, a, b)
99
141
  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
142
  when nil
105
143
  else
106
144
  @buffer << o.to_s
@@ -108,14 +146,15 @@ module Papercraft
108
146
  end
109
147
  alias_method :e, :emit
110
148
 
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
-
149
+ # Emits a block supplied using `Component#apply` or `Component#render`.
150
+ #
151
+ # div_wrap = H { |*args| div { emit_yield(*args) } }
152
+ # greeter = div_wrap.apply { |name| h1 "Hello, #{name}!" }
153
+ # greeter.render('world') #=> "<div><h1>Hello, world!</h1></div>"
154
+ #
155
+ # @param *a [Array<any>] arguments to pass to a proc
156
+ # @param **b [Hash] named arguments to pass to a proc
157
+ # @return [void]
119
158
  def emit_yield(*a, **b)
120
159
  raise Papercraft::Error, "No block given" unless @inner_block
121
160
 
@@ -131,6 +170,31 @@ module Papercraft
131
170
  S_EQUAL_QUOTE = '="'
132
171
  S_QUOTE = '"'
133
172
 
173
+ # Emits text into the rendering buffer, escaping any special characters to
174
+ # the respective HTML entities.
175
+ #
176
+ # @param data [String] text
177
+ # @return [void]
178
+ def text(data)
179
+ @buffer << escape_text(data)
180
+ end
181
+
182
+ private
183
+
184
+ # Escapes text. This method must be overriden in descendant classes.
185
+ def escape_text(text)
186
+ raise NotImplementedError
187
+ end
188
+
189
+ # Sets up a block to be called with `#emit_yield`
190
+ def with_block(block, &run_block)
191
+ old_block = @inner_block
192
+ @inner_block = block
193
+ instance_eval(&run_block)
194
+ ensure
195
+ @inner_block = old_block
196
+ end
197
+
134
198
  # Emits tag attributes into the rendering buffer
135
199
  # @param props [Hash] tag attributes
136
200
  # @return [void]
@@ -153,23 +217,25 @@ module Papercraft
153
217
  end
154
218
  }
155
219
  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
220
  end
163
221
 
222
+ # Implements an HTML renderer
164
223
  class HTMLRenderer < Renderer
165
224
  include HTML
166
225
 
226
+ private
227
+
228
+ # Escapes the given text using HTML entities.
167
229
  def escape_text(text)
168
230
  EscapeUtils.escape_html(text.to_s)
169
231
  end
170
232
  end
171
233
 
234
+ # Implements an XML renderer
172
235
  class XMLRenderer < Renderer
236
+ private
237
+
238
+ # Escapes the given text using XML entities.
173
239
  def escape_text(text)
174
240
  EscapeUtils.escape_xml(text.to_s)
175
241
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Papercraft
4
- VERSION = '0.8.2'
4
+ VERSION = '0.10.1'
5
5
  end
data/lib/papercraft.rb CHANGED
@@ -11,24 +11,44 @@ require_relative 'papercraft/encoding'
11
11
  module Papercraft
12
12
  # Exception class used to signal templating-related errors
13
13
  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
+ def self.extension(map)
20
+ Renderer.extension(map)
21
+ end
14
22
  end
15
23
 
16
24
  # Kernel extensions
17
25
  module ::Kernel
18
- # Convenience method for creating a new Papercraft
19
- # @param ctx [Hash] local context
26
+
27
+ # Creates a new papercraft component. `#H` can take either a proc argument or
28
+ # a block. In both cases, the proc is converted to a `Papercraft::Component`.
29
+ #
30
+ # H(proc { h1 'hi' }).render #=> "<h1>hi</h1>"
31
+ # H { h1 'hi' }.render #=> "<h1>hi</h1>"
32
+ #
20
33
  # @param template [Proc] template block
21
- # @return [Papercraft] Papercraft template
22
- def H(&template)
23
- Papercraft::Component.new(&template)
34
+ # @return [Papercraft::Component] Papercraft component
35
+ def H(o = nil, &template)
36
+ return o if o.is_a?(Papercraft::Component)
37
+ template ||= o
38
+ Papercraft::Component.new(mode: :html, &template)
24
39
  end
25
40
 
26
- def X(&template)
41
+ # Creates a new papercraft component in XML mode. `#X` can take either a proc argument or
42
+ # a block. In both cases, the proc is converted to a `Papercraft::Component`.
43
+ #
44
+ # X(proc { item 'foo' }).render #=> "<item>foo</item>"
45
+ # X { item 'foo' }.render #=> "<item>foo</item>"
46
+ #
47
+ # @param template [Proc] template block
48
+ # @return [Papercraft::Component] Papercraft component
49
+ def X(o = nil, &template)
50
+ return o if o.is_a?(Papercraft::Component)
51
+ template ||= o
27
52
  Papercraft::Component.new(mode: :xml, &template)
28
53
  end
29
54
  end
30
-
31
- # Object extensions
32
- class Object
33
- include Papercraft::Encoding
34
- end
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.8.2
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-23 00:00:00.000000000 Z
11
+ date: 2021-12-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: escape_utils
@@ -24,6 +24,48 @@ dependencies:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: kramdown
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.3.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.3.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rouge
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.26.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.26.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: kramdown-parser-gfm
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.1.0
27
69
  - !ruby/object:Gem::Dependency
28
70
  name: minitest
29
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +122,7 @@ dependencies:
80
122
  - - '='
81
123
  - !ruby/object:Gem::Version
82
124
  version: 2.0.9
83
- description:
125
+ description:
84
126
  email: sharon@noteflakes.com
85
127
  executables: []
86
128
  extensions: []
@@ -93,6 +135,7 @@ files:
93
135
  - lib/papercraft/compiler.rb
94
136
  - lib/papercraft/component.rb
95
137
  - lib/papercraft/encoding.rb
138
+ - lib/papercraft/extension_proxy.rb
96
139
  - lib/papercraft/html.rb
97
140
  - lib/papercraft/renderer.rb
98
141
  - lib/papercraft/version.rb
@@ -104,7 +147,7 @@ metadata:
104
147
  documentation_uri: https://www.rubydoc.info/gems/papercraft
105
148
  homepage_uri: https://github.com/digital-fabric/papercraft
106
149
  changelog_uri: https://github.com/digital-fabric/papercraft/blob/master/CHANGELOG.md
107
- post_install_message:
150
+ post_install_message:
108
151
  rdoc_options:
109
152
  - "--title"
110
153
  - Papercraft
@@ -123,8 +166,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
166
  - !ruby/object:Gem::Version
124
167
  version: '0'
125
168
  requirements: []
126
- rubygems_version: 3.1.6
127
- signing_key:
169
+ rubygems_version: 3.2.32
170
+ signing_key:
128
171
  specification_version: 4
129
172
  summary: 'Papercraft: component-based HTML templating for Ruby'
130
173
  test_files: []