papercraft 0.10.1 → 0.14

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: 757137ea8d3a2b5d5d329b7e841bad13cc26ea3486ebcb216f1ef016d7d3ff4a
4
- data.tar.gz: 281ee3b2911b1f4e1ff3337dbefef407ce07b17e21acb949dce158a93d8758a5
3
+ metadata.gz: 8ecbcde7e33f35958acc4ca0c5c0623468563f14322675a3c7315ed92e0b289c
4
+ data.tar.gz: 5f6bdcdee15ef9ca04bc982405bd07ea3da49c4a43fbd8fd95a70494e0c43dab
5
5
  SHA512:
6
- metadata.gz: 22225498fce014a17962541bac3c17f20243239b709e191e75fa2f1abbc0f7f7fdef3b0c4d110753f0de1c2ca4efc19f516cb797a02cbafa70af95085449096e
7
- data.tar.gz: 57bb7aae102900de14ba9d8f4a0e2be83978e5106ff098ffda5acf39431273fd76be646d012da72094715ad8a3da7fd81a485e51151c74df94091a91e72866f4
6
+ metadata.gz: 7496b82113125d5f20287cb8b2f6607acfe7c16dda2a4670152867a175c877c33865f85ad155e733509b55e36067d0c3ea60578bf55f041e95bb46e3c1fa84f9
7
+ data.tar.gz: 91d5a7059eaf4b760412d9440f8ae08b48486c6ca6fc423a1ce32eb61a9a376385b4830b88b8a2ca1721971c48e4525a99d732d7650522fa71a55654c63aae86
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 0.14 2022-01-19
2
+
3
+ - Add support for #emit_yield in applied component (#4)
4
+
5
+ ## 0.13 2022-01-19
6
+
7
+ - Add support for partial parameter application (#3)
8
+
9
+ ## 0.12 2022-01-06
10
+
11
+ - Improve documentation
12
+ - Add `Renderer#tag` method
13
+ - Add `HTML#style`, `HTML#script` methods
14
+
15
+ ## 0.11 2022-01-04
16
+
17
+ - Add deferred evaluation
18
+
1
19
  ## 0.10.1 2021-12-25
2
20
 
3
21
  - Fix tag rendering with empty text in Ruby 3.0
data/README.md CHANGED
@@ -20,8 +20,6 @@
20
20
  <a href="https://www.rubydoc.info/gems/papercraft">API reference</a>
21
21
  </p>
22
22
 
23
- # Papercraft - Composable HTML templating for Ruby
24
-
25
23
  ## What is Papercraft?
26
24
 
27
25
  ```ruby
@@ -368,17 +366,69 @@ The default Kramdown options are:
368
366
  ```
369
367
 
370
368
  The deafult options can be configured by accessing
371
- `Papercraft::HTML.kramdown_options`:
369
+ `Papercraft::HTML.kramdown_options`, e.g.:
372
370
 
373
371
  ```ruby
374
372
  Papercraft::HTML.kramdown_options[:auto_ids] = false
375
373
  ```
376
374
 
375
+ ## Deferred evaluation
376
+
377
+ Deferred evaluation allows deferring the rendering of parts of a template until
378
+ the last moment, thus allowing an inner component to manipulate the state of the
379
+ outer component. To in order to defer a part of a template, use `#defer`, and
380
+ include any markup in the provided block. This technique, in in conjunction with
381
+ holding state in instance variables, is an alternative to passing parameters,
382
+ which can be limiting in some situations.
383
+
384
+ A few use cases for deferred evaulation come to mind:
385
+
386
+ - Setting the page title.
387
+ - Adding a flash message to a page.
388
+ - Using components that dynamically add static dependencies (JS and CSS) to the
389
+ page.
390
+
391
+ The last use case is particularly interesting. Imagine a `DependencyMananger`
392
+ class that can collect JS and CSS dependencies from the different components
393
+ integrated into the page, and adds them to the page's `<head>` element:
394
+
395
+ ```ruby
396
+ default_layout = H { |**args|
397
+ @dependencies = DependencyMananger.new
398
+ head {
399
+ defer { emit @dependencies.head_markup }
400
+ }
401
+ body { emit_yield **args }
402
+ }
403
+
404
+ button = proc { |text, onclick|
405
+ @dependencies.js '/static/js/button.js'
406
+ @dependencies.css '/static/css/button.css'
407
+
408
+ button text, onclick: onclick
409
+ }
410
+
411
+ heading = proc { |text|
412
+ @dependencies.js '/static/js/heading.js'
413
+ @dependencies.css '/static/css/heading.css'
414
+
415
+ h1 text
416
+ }
417
+
418
+ page = default_layout.apply {
419
+ emit heading, "What's your favorite cheese?"
420
+
421
+ emit button, 'Beaufort', 'eat_beaufort()'
422
+ emit button, 'Mont d''or', 'eat_montdor()'
423
+ emit button, 'Époisses', 'eat_epoisses()'
424
+ }
425
+ ```
426
+
377
427
  ## Papercraft extensions
378
428
 
379
429
  Papercraft extensions are modules that contain one or more methods that can be
380
430
  used to render complex HTML components. Extension modules can be used by
381
- installing them as a namespaced extension using `Papercraft.extension`.
431
+ installing them as a namespaced extension using `Papercraft::extension`.
382
432
  Extensions are particularly useful when you work with CSS frameworks such as
383
433
  [Bootstrap](https://getbootstrap.com/), [Tailwind](https://tailwindui.com/) or
384
434
  [Primer](https://primer.style/).
@@ -424,7 +474,7 @@ end
424
474
  Papercraft.extension(bootstrap: BootstrapComponents)
425
475
  ```
426
476
 
427
- The call to `Papercraft.extension` lets us access the different methods of
477
+ The call to `Papercraft::extension` lets us access the different methods of
428
478
  `BootstrapComponents` by calling `#bootstrap` inside a template. With this,
429
479
  we'll be able to express the above markup as follows:
430
480
 
@@ -106,11 +106,8 @@ module Papercraft
106
106
  template = self
107
107
  Renderer.verify_proc_parameters(template, a, b)
108
108
  renderer_class.new do
109
- if block
110
- with_block(block) { instance_exec(*a, **b, &template) }
111
- else
112
- instance_exec(*a, **b, &template)
113
- end
109
+ push_emit_yield_block(block) if block
110
+ instance_exec(*a, **b, &template)
114
111
  end.to_s
115
112
  rescue ArgumentError => e
116
113
  raise Papercraft::Error, e.message
@@ -136,15 +133,10 @@ module Papercraft
136
133
  # @return [Papercraft::Component] applied component
137
134
  def apply(*a, **b, &block)
138
135
  template = self
139
- if block
140
- Component.new(&proc do |*x, **y|
141
- with_block(block) { instance_exec(*x, **y, &template) }
142
- end)
143
- else
144
- Component.new(&proc do |*x, **y|
145
- instance_exec(*a, **b, &template)
146
- end)
147
- end
136
+ Component.new(&proc do |*x, **y|
137
+ push_emit_yield_block(block) if block
138
+ instance_exec(*a, *x, **b, **y, &template)
139
+ end)
148
140
  end
149
141
 
150
142
  # Returns the Renderer class used for rendering the templates, according to
@@ -44,15 +44,53 @@ module Papercraft
44
44
  link(**attributes)
45
45
  end
46
46
 
47
- def emit_markdown(markdown, **opts)
48
- emit Kramdown::Document.new(markdown, **kramdown_options(opts)).to_html
47
+ # Emits an inline CSS style element.
48
+ #
49
+ # @param css [String] CSS code
50
+ # @param **props [Hash] optional element attributes
51
+ # @return [void]
52
+ def style(css, **props, &block)
53
+ @buffer << '<style'
54
+ emit_props(props) unless props.empty?
55
+
56
+ @buffer << '>' << css << '</style>'
49
57
  end
58
+
59
+ # Emits an inline JS script element.
60
+ #
61
+ # @param js [String, nil] Javascript code
62
+ # @param **props [Hash] optional element attributes
63
+ # @return [void]
64
+ def script(js = nil, **props, &block)
65
+ @buffer << '<script'
66
+ emit_props(props) unless props.empty?
50
67
 
51
- def kramdown_options(opts)
52
- HTML.kramdown_options.merge(**opts)
68
+ if js
69
+ @buffer << '>' << js << '</script>'
70
+ else
71
+ @buffer << '></script>'
72
+ end
73
+ end
74
+
75
+ # Converts and emits the given markdown. Papercraft uses
76
+ # [Kramdown](https://github.com/gettalong/kramdown/) to do the Markdown to
77
+ # HTML conversion. Optional Kramdown settings can be provided in order to
78
+ # control the conversion. Those are merged with the default Kramdown
79
+ # settings, which can be controlled using
80
+ # `Papercraft::HTML.kramdown_options`.
81
+ #
82
+ # @param markdown [String] Markdown content
83
+ # @param **opts [Hash] Kramdown options
84
+ # @return [void]
85
+ def emit_markdown(markdown, **opts)
86
+ emit Kramdown::Document.new(markdown, **kramdown_options(opts)).to_html
53
87
  end
54
88
 
55
89
  class << self
90
+ # Returns the default Kramdown options used for converting Markdown to
91
+ # HTML.
92
+ #
93
+ # @return [Hash] Default Kramdown options
56
94
  def kramdown_options
57
95
  @kramdown_options ||= {
58
96
  entity_output: :numeric,
@@ -62,9 +100,24 @@ module Papercraft
62
100
  }
63
101
  end
64
102
 
103
+ # Sets the default Kramdown options used for converting Markdown to
104
+ # HTML.
105
+ #
106
+ # @param opts [Hash] New deafult Kramdown options
107
+ # @return [Hash] New default Kramdown options
65
108
  def kramdown_options=(opts)
66
109
  @kramdown_options = opts
67
110
  end
68
111
  end
112
+
113
+ private
114
+
115
+ # Returns the default Kramdown options, merged with the given overrides.
116
+ #
117
+ # @param opts [Hash] Kramdown option overrides
118
+ # @return [Hash] Merged Kramdown options
119
+ def kramdown_options(opts)
120
+ HTML.kramdown_options.merge(**opts)
121
+ end
69
122
  end
70
123
  end
@@ -34,8 +34,29 @@ module Papercraft
34
34
  end
35
35
  end
36
36
 
37
- # Installs the given extensions, mapping a method name to the extension
38
- # module.
37
+ # call_seq:
38
+ # Papercraft::Renderer.extension(name => mod, ...)
39
+ # Papercraft.extension(name => mod, ...)
40
+ #
41
+ # Installs the given extensions, passed in the form of a Ruby hash mapping
42
+ # methods to extension modules. The methods will be available to all
43
+ # Papercraft components. Extension methods are executed in the context of
44
+ # the the renderer instance, so they can look just like normal proc
45
+ # components. In cases where method names in the module clash with HTML
46
+ # tag names, you can use the `#tag` method to emit the relevant tag.
47
+ #
48
+ # module ComponentLibrary
49
+ # def card(title, content)
50
+ # div(class: 'card') {
51
+ # h3 title
52
+ # div(class: 'card-content') { emit_markdown content }
53
+ # }
54
+ # end
55
+ # end
56
+ #
57
+ # Papercraft.extension(components: ComponentLibrary)
58
+ # H { components.card('Foo', '**Bar**') }
59
+ #
39
60
  # @param map [Hash] hash mapping methods to extension modules
40
61
  # @return [void]
41
62
  def extension(map)
@@ -57,12 +78,14 @@ module Papercraft
57
78
  end
58
79
  end
59
80
 
81
+ INITIAL_BUFFER_CAPACITY = 8192
82
+
60
83
  # Initializes the renderer and evaulates the given template in the
61
84
  # renderer's scope.
62
85
  #
63
86
  # @param &template [Proc] template block
64
87
  def initialize(&template)
65
- @buffer = +''
88
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
66
89
  instance_eval(&template)
67
90
  end
68
91
 
@@ -70,13 +93,29 @@ module Papercraft
70
93
  #
71
94
  # @return [String]
72
95
  def to_s
96
+ if @parts
97
+ last = @buffer
98
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
99
+ parts = @parts
100
+ @parts = nil
101
+ parts.each do |p|
102
+ if Proc === p
103
+ render_deferred_proc(&p)
104
+ else
105
+ @buffer << p
106
+ end
107
+ end
108
+ @buffer << last unless last.empty?
109
+ end
73
110
  @buffer
74
111
  end
75
112
 
113
+ # The tag method template below is optimized for performance. Do not touch!
114
+
76
115
  S_TAG_METHOD_LINE = __LINE__ + 1
77
116
  S_TAG_METHOD = <<~EOF
78
- S_TAG_%<TAG>s_PRE = '<%<tag>s'.tr('_', '-')
79
- S_TAG_%<TAG>s_CLOSE = '</%<tag>s>'.tr('_', '-')
117
+ S_TAG_%<TAG>s_PRE = %<tag_pre>s
118
+ S_TAG_%<TAG>s_CLOSE = %<tag_close>s
80
119
 
81
120
  def %<tag>s(text = nil, **props, &block)
82
121
  if text.is_a?(Hash) && props.empty?
@@ -103,6 +142,48 @@ module Papercraft
103
142
  end
104
143
  EOF
105
144
 
145
+ # Emits an HTML tag with the given content, properties and optional block.
146
+ # This method is an alternative to emitting HTML tags using dynamically
147
+ # created methods. This is particularly useful when using extensions that
148
+ # have method names that clash with HTML tags, such as `button` or `a`, or
149
+ # when you need to override the behaviour of a particular HTML tag.
150
+ #
151
+ # The following two method calls have the same effect:
152
+ #
153
+ # button 'text', id: 'button1'
154
+ # tag :button, 'text', id: 'button1'
155
+ #
156
+ # @param sym [Symbol, String] HTML tag
157
+ # @param text [String, nil] tag content
158
+ # @param **props [Hash] tag attributes
159
+ # @param &block [Proc] optional inner HTML
160
+ # @return [void]
161
+ def tag(sym, text = nil, **props, &block)
162
+ if text.is_a?(Hash) && props.empty?
163
+ props = text
164
+ text = nil
165
+ end
166
+
167
+ tag = sym.to_s.tr('_', '-')
168
+
169
+ @buffer << S_LT << tag
170
+ emit_props(props) unless props.empty?
171
+
172
+ if block
173
+ @buffer << S_GT
174
+ instance_eval(&block)
175
+ @buffer << S_LT_SLASH << tag << S_GT
176
+ elsif Proc === text
177
+ @buffer << S_GT
178
+ emit(text)
179
+ @buffer << S_LT_SLASH << tag << S_GT
180
+ elsif text
181
+ @buffer << S_GT << escape_text(text.to_s) << S_LT_SLASH << tag << S_GT
182
+ else
183
+ @buffer << S_SLASH_GT
184
+ end
185
+ end
186
+
106
187
  # Catches undefined tag method call and handles it by defining the method.
107
188
  #
108
189
  # @param sym [Symbol] HTML tag or component identifier
@@ -112,7 +193,12 @@ module Papercraft
112
193
  # @return [void]
113
194
  def method_missing(sym, *args, **opts, &block)
114
195
  tag = sym.to_s
115
- code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
196
+ code = S_TAG_METHOD % {
197
+ tag: tag,
198
+ TAG: tag.upcase,
199
+ tag_pre: "<#{tag.tr('_', '-')}".inspect,
200
+ tag_close: "</#{tag.tr('_', '-')}>".inspect
201
+ }
116
202
  self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
117
203
  send(sym, *args, **opts, &block)
118
204
  end
@@ -156,9 +242,46 @@ module Papercraft
156
242
  # @param **b [Hash] named arguments to pass to a proc
157
243
  # @return [void]
158
244
  def emit_yield(*a, **b)
159
- raise Papercraft::Error, "No block given" unless @inner_block
245
+ block = @emit_yield_stack&.pop
246
+ raise Papercraft::Error, "No block given" unless block
160
247
 
161
- instance_exec(*a, **b, &@inner_block)
248
+ instance_exec(*a, **b, &block)
249
+ end
250
+
251
+ # Defers the given block to be evaluated later. Deferred evaluation allows
252
+ # Papercraft components to inject state into sibling components, regardless
253
+ # of the component's order in the container component. For example, a nested
254
+ # component may set an instance variable used by another component. This is
255
+ # an elegant solution to the problem of setting the HTML page's title, or
256
+ # adding elements to the `<head>` section. Here's how a title can be
257
+ # controlled from a nested component:
258
+ #
259
+ # layout = H {
260
+ # html {
261
+ # head {
262
+ # defer { title @title }
263
+ # }
264
+ # body {
265
+ # emit_yield
266
+ # }
267
+ # }
268
+ # }
269
+ #
270
+ # html.render {
271
+ # @title = 'My super page'
272
+ # h1 'content'
273
+ # }
274
+ #
275
+ # @param &block [Proc] Deferred block to be emitted
276
+ # @return [void]
277
+ def defer(&block)
278
+ if !@parts
279
+ @parts = [@buffer, block]
280
+ else
281
+ @parts << @buffer unless @buffer.empty?
282
+ @parts << block
283
+ end
284
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
162
285
  end
163
286
 
164
287
  S_LT = '<'
@@ -182,19 +305,19 @@ module Papercraft
182
305
  private
183
306
 
184
307
  # Escapes text. This method must be overriden in descendant classes.
308
+ #
309
+ # @param text [String] text to be escaped
185
310
  def escape_text(text)
186
311
  raise NotImplementedError
187
312
  end
188
313
 
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
314
+ # Pushes the given block onto the emit_yield stack.
315
+ #
316
+ # @param block [Proc] block
317
+ def push_emit_yield_block(block)
318
+ (@emit_yield_stack ||= []) << block
196
319
  end
197
-
320
+
198
321
  # Emits tag attributes into the rendering buffer
199
322
  # @param props [Hash] tag attributes
200
323
  # @return [void]
@@ -217,6 +340,23 @@ module Papercraft
217
340
  end
218
341
  }
219
342
  end
343
+
344
+ # Renders a deferred proc by evaluating it, then adding the rendered result
345
+ # to the buffer.
346
+ #
347
+ # @param &block [Proc] deferred proc
348
+ # @return [void]
349
+ def render_deferred_proc(&block)
350
+ old_buffer = @buffer
351
+
352
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
353
+ @parts = nil
354
+
355
+ instance_eval(&block)
356
+
357
+ old_buffer << to_s
358
+ @buffer = old_buffer
359
+ end
220
360
  end
221
361
 
222
362
  # Implements an HTML renderer
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Papercraft
4
- VERSION = '0.10.1'
4
+ VERSION = '0.14'
5
5
  end
data/lib/papercraft.rb CHANGED
@@ -16,6 +16,12 @@ module Papercraft
16
16
  # by adding namespaced methods to emplates. An extension is implemented as a
17
17
  # Ruby module containing one or more methods. Each method in the extension
18
18
  # module can be used to render a specific HTML element or a set of elements.
19
+ #
20
+ # This is a convenience method. For more information on using Papercraft
21
+ # extensions, see `Papercraft::Renderer::extension`
22
+ #
23
+ # @param map [Hash] hash mapping methods to extension modules
24
+ # @return [void]
19
25
  def self.extension(map)
20
26
  Renderer.extension(map)
21
27
  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.10.1
4
+ version: '0.14'
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-25 00:00:00.000000000 Z
11
+ date: 2022-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: escape_utils
@@ -166,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  - !ruby/object:Gem::Version
167
167
  version: '0'
168
168
  requirements: []
169
- rubygems_version: 3.2.32
169
+ rubygems_version: 3.3.3
170
170
  signing_key:
171
171
  specification_version: 4
172
172
  summary: 'Papercraft: component-based HTML templating for Ruby'