papercraft 0.8.1 → 0.10

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: 944fb4023202a5fb15158b4dcd455eb12f37c5abff353f5ea63e3b605204f52b
4
- data.tar.gz: 618aea8b154aff252af187bca43cdc569fae80a6c7a3ec6489b168d61a5ab7e4
3
+ metadata.gz: a7d34aa76efd449fc6a63182195e8353886887e13963b79439a19dc3b0f9a9d2
4
+ data.tar.gz: e199245d969426b75627401b7c57fe1a01e7121a656ece8cd97b004a22b3ab2e
5
5
  SHA512:
6
- metadata.gz: d622166425334b096158ee425e75f4e4ad9c16c466db5aa7f45b51774137def33b4e4f891425bc917ee56b81fb91e39adc15c6f2476e009f3a4be4a73b295c74
7
- data.tar.gz: 8546f39f7f6b08cc49049fae52db77891f7664b6ac6d8bce9c0a1a41f19bc0f8a92c59909ab4778cf15d6ef958f756bc6b6f392aa0c028a8d9ddd54bbee67afa
6
+ metadata.gz: 75d5dd94c26d1ee15b11f8a439ff1c26cd7336c60ce609e334f3c0e823e8813f67923b79e8f023e9d1bcc3f2af20b5fffc93196ba5731d1cea42b2e0f75f3102
7
+ data.tar.gz: 7bdcbfa67dabec451c00f006256b16b9875d60fc726d295135db5de94a5b8854e3bdf648410d6ef6e7a7b76766c6c07d21ebb4da9788cab45fcb67a8c9d6b221
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.10 2021-12-25
2
+
3
+ - Add support for extensions
4
+
5
+ ## 0.9 2021-12-23
6
+
7
+ - Add support for emitting Markdown
8
+ - Add support for passing proc as argument to `#H` and `#X`
9
+ - Deprecate `Encoding` module
10
+
1
11
  ## 0.8.1 2021-12-22
2
12
 
3
13
  - 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).
@@ -3,18 +3,19 @@
3
3
  require_relative './html'
4
4
 
5
5
  module Papercraft
6
+
6
7
  # Component represents a distinct, reusable HTML template. A component can
7
8
  # include other components, and also be nested inside other components.
8
9
  #
9
10
  # Since in Papercraft HTML is expressed using blocks (or procs,) the Component
10
11
  # class is simply a special kind of Proc, which has some enhanced
11
12
  # capabilities, allowing it to be easily composed in a variety of ways.
12
-
13
+ #
13
14
  # Components are usually created using the global methods `H` or `X`, for HTML
14
15
  # or XML templates, respectively:
15
16
  #
16
- # greeter = H { |name| h1 "Hello, #{name}!" } greeter.render('world') #=>
17
- # "<h1>Hello, world!</h1>"
17
+ # greeter = H { |name| h1 "Hello, #{name}!" }
18
+ # greeter.render('world') #=> "<h1>Hello, world!</h1>"
18
19
  #
19
20
  # Components can also be created using the normal constructor:
20
21
  #
@@ -23,9 +24,71 @@ module Papercraft
23
24
  #
24
25
  # In the component block, HTML elements are created by simply calling
25
26
  # unqualified methods:
27
+ #
28
+ # page_layout = H {
29
+ # html5 {
30
+ # head {
31
+ # title 'foo'
32
+ # }
33
+ # body {
34
+ # h1 "Hello, world!"
35
+ # }
36
+ # }
37
+ # }
38
+ #
39
+ # Papercraft components can take explicit parameters in order to render
40
+ # dynamic content. This can be in the form of regular or named parameters. The
41
+ # `greeter` template shown above takes a single `name` parameter. Here's how a
42
+ # anchor component could be implemented with named parameters:
43
+ #
44
+ # anchor = H { |uri: , text: | a(text, href: uri) }
45
+ #
46
+ # The above component could later be rendered by passing the needed arguments:
47
+ #
48
+ # anchor.render(uri: 'https://example.com', text: 'Example')
49
+ #
50
+ # ## Component Composition
51
+ #
52
+ # A component can be included in another component using the `emit` method:
53
+ #
54
+ # links = H {
55
+ # emit anchor, uri: '/posts', text: 'Posts'
56
+ # emit anchor, uri: '/archive', text: 'Archive'
57
+ # emit anchor, uri: '/about', text: 'About'
58
+ # }
59
+ #
60
+ # Another way of composing components is to pass the components themselves as
61
+ # parameters:
62
+ #
63
+ # links = H { |anchors|
64
+ # anchors.each { |a| emit a }
65
+ # }
66
+ # links.render([
67
+ # anchor.apply(uri: '/posts', text: 'Posts'),
68
+ # anchor.apply(uri: '/archive', text: 'Archive'),
69
+ # anchor.apply(uri: '/about', text: 'About')
70
+ # ])
71
+ #
72
+ # The `#apply` method creates a new component, applying the given parameters
73
+ # such that the component can be rendered without parameters:
74
+ #
75
+ # links_with_anchors = links.apply([
76
+ # anchor.apply(uri: '/posts', text: 'Posts'),
77
+ # anchor.apply(uri: '/archive', text: 'Archive'),
78
+ # anchor.apply(uri: '/about', text: 'About')
79
+ # ])
80
+ # links_with_anchors.render
81
+ #
26
82
  class Component < Proc
27
- # Initializes a component with the given block
28
- # @param mode [Symbol] local context
83
+
84
+ # Determines the rendering mode: `:html` or `:xml`.
85
+ attr_accessor :mode
86
+
87
+ # Initializes a component with the given block. The rendering mode (HTML or
88
+ # XML) can be passed in the `mode:` parameter. If `mode:` is not specified,
89
+ # the component defaults to HTML.
90
+ #
91
+ # @param mode [:html, :xml] rendering mode
29
92
  # @param block [Proc] nested HTML block
30
93
  def initialize(mode: :html, &block)
31
94
  @mode = mode
@@ -34,7 +97,9 @@ module Papercraft
34
97
 
35
98
  H_EMPTY = {}.freeze
36
99
 
37
- # Renders the associated block and returns the string result
100
+ # Renders the template with the given parameters and or block, and returns
101
+ # the string result.
102
+ #
38
103
  # @param context [Hash] context
39
104
  # @return [String]
40
105
  def render(*a, **b, &block)
@@ -51,6 +116,24 @@ module Papercraft
51
116
  raise Papercraft::Error, e.message
52
117
  end
53
118
 
119
+ # Creates a new component, applying the given parameters and or block to the
120
+ # current one. Application is one of the principal methods of composing
121
+ # components, particularly when passing inner components as blocks:
122
+ #
123
+ # article_wrapper = H {
124
+ # article {
125
+ # emit_yield
126
+ # }
127
+ # }
128
+ # wrapped_article = article_wrapper.apply {
129
+ # h1 'Article title'
130
+ # }
131
+ # wrapped_article.render #=> "<article><h1>Article title</h1></article>"
132
+ #
133
+ # @param *a [<any>] normal parameters
134
+ # @param **b [Hash] named parameters
135
+ # @param &block [Proc] inner block
136
+ # @return [Papercraft::Component] applied component
54
137
  def apply(*a, **b, &block)
55
138
  template = self
56
139
  if block
@@ -64,6 +147,10 @@ module Papercraft
64
147
  end
65
148
  end
66
149
 
150
+ # Returns the Renderer class used for rendering the templates, according to
151
+ # the component's mode.
152
+ #
153
+ # @return [Papercraft::Renderer] Renderer used for rendering the component
67
154
  def renderer_class
68
155
  case @mode
69
156
  when :html
@@ -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,34 +32,47 @@ 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
36
+
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
26
46
 
27
- attr_reader :context
47
+ private
28
48
 
29
- # Initializes attributes and renders the given block
30
- # @param context [Hash] rendering context
31
- # @param block [Proc] template block
32
- # @return [void]
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('_', '-')
@@ -74,33 +98,42 @@ module Papercraft
74
98
  end
75
99
  EOF
76
100
 
77
- # Catches undefined tag method call and handles them by defining the method
101
+ # Catches undefined tag method call and handles it by defining the method.
102
+ #
78
103
  # @param sym [Symbol] HTML tag or component identifier
79
- # @param args [Array] method call arguments
80
- # @param block [Proc] block passed to method call
104
+ # @param args [Array] method arguments
105
+ # @param opts [Hash] named method arguments
106
+ # @param &block [Proc] block passed to method
81
107
  # @return [void]
82
108
  def method_missing(sym, *args, **opts, &block)
83
- value = @local && @local[sym]
84
- return value if value
85
-
86
109
  tag = sym.to_s
87
110
  code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
88
111
  self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
89
112
  send(sym, *args, **opts, &block)
90
113
  end
91
114
 
92
- # Emits the given object into the rendering buffer
115
+ # Emits the given object into the rendering buffer. If the given object is a
116
+ # proc or a component, `emit` will passes any additional arguments and named
117
+ # arguments to the object when rendering it. If the given object is nil,
118
+ # nothing is emitted. Otherwise, the object is converted into a string using
119
+ # `#to_s` which is then added to the rendering buffer, without any escaping.
120
+ #
121
+ # greeter = proc { |name| h1 "Hello, #{name}!" }
122
+ # H { emit(greeter, 'world') }.render #=> "<h1>Hello, world!</h1>"
123
+ #
124
+ # H { emit 'hi&<bye>' }.render #=> "hi&<bye>"
125
+ #
126
+ # H { emit nil }.render #=> ""
127
+ #
93
128
  # @param o [Proc, Papercraft::Component, String] emitted object
129
+ # @param *a [Array<any>] arguments to pass to a proc
130
+ # @param **b [Hash] named arguments to pass to a proc
94
131
  # @return [void]
95
132
  def emit(o, *a, **b)
96
133
  case o
97
134
  when ::Proc
98
135
  Renderer.verify_proc_parameters(o, a, b)
99
136
  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
137
  when nil
105
138
  else
106
139
  @buffer << o.to_s
@@ -108,14 +141,15 @@ module Papercraft
108
141
  end
109
142
  alias_method :e, :emit
110
143
 
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
-
144
+ # Emits a block supplied using `Component#apply` or `Component#render`.
145
+ #
146
+ # div_wrap = H { |*args| div { emit_yield(*args) } }
147
+ # greeter = div_wrap.apply { |name| h1 "Hello, #{name}!" }
148
+ # greeter.render('world') #=> "<div><h1>Hello, world!</h1></div>"
149
+ #
150
+ # @param *a [Array<any>] arguments to pass to a proc
151
+ # @param **b [Hash] named arguments to pass to a proc
152
+ # @return [void]
119
153
  def emit_yield(*a, **b)
120
154
  raise Papercraft::Error, "No block given" unless @inner_block
121
155
 
@@ -131,6 +165,31 @@ module Papercraft
131
165
  S_EQUAL_QUOTE = '="'
132
166
  S_QUOTE = '"'
133
167
 
168
+ # Emits text into the rendering buffer, escaping any special characters to
169
+ # the respective HTML entities.
170
+ #
171
+ # @param data [String] text
172
+ # @return [void]
173
+ def text(data)
174
+ @buffer << escape_text(data)
175
+ end
176
+
177
+ private
178
+
179
+ # Escapes text. This method must be overriden in descendant classes.
180
+ def escape_text(text)
181
+ raise NotImplementedError
182
+ end
183
+
184
+ # Sets up a block to be called with `#emit_yield`
185
+ def with_block(block, &run_block)
186
+ old_block = @inner_block
187
+ @inner_block = block
188
+ instance_eval(&run_block)
189
+ ensure
190
+ @inner_block = old_block
191
+ end
192
+
134
193
  # Emits tag attributes into the rendering buffer
135
194
  # @param props [Hash] tag attributes
136
195
  # @return [void]
@@ -153,23 +212,25 @@ module Papercraft
153
212
  end
154
213
  }
155
214
  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
215
  end
163
216
 
217
+ # Implements an HTML renderer
164
218
  class HTMLRenderer < Renderer
165
219
  include HTML
166
220
 
221
+ private
222
+
223
+ # Escapes the given text using HTML entities.
167
224
  def escape_text(text)
168
225
  EscapeUtils.escape_html(text.to_s)
169
226
  end
170
227
  end
171
228
 
229
+ # Implements an XML renderer
172
230
  class XMLRenderer < Renderer
231
+ private
232
+
233
+ # Escapes the given text using XML entities.
173
234
  def escape_text(text)
174
235
  EscapeUtils.escape_xml(text.to_s)
175
236
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Papercraft
4
- VERSION = '0.8.1'
4
+ VERSION = '0.10'
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.1
4
+ version: '0.10'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-22 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
@@ -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