papercraft 0.8.1 → 0.10

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: 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