papercraft 0.17 → 0.21

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.
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'escape_utils'
4
-
5
3
  require_relative './html'
4
+ require_relative './xml'
6
5
  require_relative './json'
7
6
  require_relative './extension_proxy'
8
7
 
9
8
  module Papercraft
10
9
 
11
- # A Renderer renders a Papercraft component into a string
10
+ # A Renderer renders a Papercraft template into a string
12
11
  class Renderer
13
-
12
+
14
13
  class << self
15
14
 
16
15
  # Verifies that the given template proc can be called with the given
@@ -43,9 +42,9 @@ module Papercraft
43
42
  #
44
43
  # Installs the given extensions, passed in the form of a Ruby hash mapping
45
44
  # methods to extension modules. The methods will be available to all
46
- # Papercraft components. Extension methods are executed in the context of
45
+ # Papercraft templates. Extension methods are executed in the context of
47
46
  # the the renderer instance, so they can look just like normal proc
48
- # components. In cases where method names in the module clash with HTML
47
+ # components. In cases where method names in the module clash with XML
49
48
  # tag names, you can use the `#tag` method to emit the relevant tag.
50
49
  #
51
50
  # module ComponentLibrary
@@ -81,131 +80,14 @@ module Papercraft
81
80
  end
82
81
  end
83
82
 
84
- INITIAL_BUFFER_CAPACITY = 8192
85
-
86
83
  # Initializes the renderer and evaulates the given template in the
87
84
  # renderer's scope.
88
85
  #
89
86
  # @param &template [Proc] template block
90
87
  def initialize(&template)
91
- @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
92
88
  instance_eval(&template)
93
89
  end
94
90
 
95
- # Returns the rendered template.
96
- #
97
- # @return [String]
98
- def to_s
99
- if @parts
100
- last = @buffer
101
- @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
102
- parts = @parts
103
- @parts = nil
104
- parts.each do |p|
105
- if Proc === p
106
- render_deferred_proc(&p)
107
- else
108
- @buffer << p
109
- end
110
- end
111
- @buffer << last unless last.empty?
112
- end
113
- @buffer
114
- end
115
-
116
- # The tag method template below is optimized for performance. Do not touch!
117
-
118
- S_TAG_METHOD_LINE = __LINE__ + 2
119
- S_TAG_METHOD = <<~EOF
120
- S_TAG_%<TAG>s_PRE = %<tag_pre>s
121
- S_TAG_%<TAG>s_CLOSE = %<tag_close>s
122
-
123
- def %<tag>s(text = nil, **props, &block)
124
- if text.is_a?(Hash) && props.empty?
125
- props = text
126
- text = nil
127
- end
128
-
129
- @buffer << S_TAG_%<TAG>s_PRE
130
- emit_props(props) unless props.empty?
131
-
132
- if block
133
- @buffer << S_GT
134
- instance_eval(&block)
135
- @buffer << S_TAG_%<TAG>s_CLOSE
136
- elsif Proc === text
137
- @buffer << S_GT
138
- emit(text)
139
- @buffer << S_TAG_%<TAG>s_CLOSE
140
- elsif text
141
- @buffer << S_GT << escape_text(text.to_s) << S_TAG_%<TAG>s_CLOSE
142
- else
143
- @buffer << S_SLASH_GT
144
- end
145
- end
146
- EOF
147
-
148
- # Emits an HTML tag with the given content, properties and optional block.
149
- # This method is an alternative to emitting HTML tags using dynamically
150
- # created methods. This is particularly useful when using extensions that
151
- # have method names that clash with HTML tags, such as `button` or `a`, or
152
- # when you need to override the behaviour of a particular HTML tag.
153
- #
154
- # The following two method calls have the same effect:
155
- #
156
- # button 'text', id: 'button1'
157
- # tag :button, 'text', id: 'button1'
158
- #
159
- # @param sym [Symbol, String] HTML tag
160
- # @param text [String, nil] tag content
161
- # @param **props [Hash] tag attributes
162
- # @param &block [Proc] optional inner HTML
163
- # @return [void]
164
- def tag(sym, text = nil, **props, &block)
165
- if text.is_a?(Hash) && props.empty?
166
- props = text
167
- text = nil
168
- end
169
-
170
- tag = sym.to_s.tr('_', '-')
171
-
172
- @buffer << S_LT << tag
173
- emit_props(props) unless props.empty?
174
-
175
- if block
176
- @buffer << S_GT
177
- instance_eval(&block)
178
- @buffer << S_LT_SLASH << tag << S_GT
179
- elsif Proc === text
180
- @buffer << S_GT
181
- emit(text)
182
- @buffer << S_LT_SLASH << tag << S_GT
183
- elsif text
184
- @buffer << S_GT << escape_text(text.to_s) << S_LT_SLASH << tag << S_GT
185
- else
186
- @buffer << S_SLASH_GT
187
- end
188
- end
189
-
190
- # Catches undefined tag method call and handles it by defining the method.
191
- #
192
- # @param sym [Symbol] HTML tag or component identifier
193
- # @param args [Array] method arguments
194
- # @param opts [Hash] named method arguments
195
- # @param &block [Proc] block passed to method
196
- # @return [void]
197
- def method_missing(sym, *args, **opts, &block)
198
- tag = sym.to_s
199
- code = S_TAG_METHOD % {
200
- tag: tag,
201
- TAG: tag.upcase,
202
- tag_pre: "<#{tag.tr('_', '-')}".inspect,
203
- tag_close: "</#{tag.tr('_', '-')}>".inspect
204
- }
205
- self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
206
- send(sym, *args, **opts, &block)
207
- end
208
-
209
91
  # Emits the given object into the rendering buffer. If the given object is a
210
92
  # proc or a component, `emit` will passes any additional arguments and named
211
93
  # arguments to the object when rendering it. If the given object is nil,
@@ -219,18 +101,20 @@ module Papercraft
219
101
  #
220
102
  # Papercraft.html { emit nil }.render #=> ""
221
103
  #
222
- # @param o [Proc, Papercraft::Component, String] emitted object
104
+ # @param o [Proc, Papercraft::Template, String] emitted object
223
105
  # @param *a [Array<any>] arguments to pass to a proc
224
106
  # @param **b [Hash] named arguments to pass to a proc
225
107
  # @return [void]
226
- def emit(o, *a, **b)
108
+ def emit(o, *a, **b, &block)
227
109
  case o
228
110
  when ::Proc
229
111
  Renderer.verify_proc_parameters(o, a, b)
112
+ push_emit_yield_block(block) if block
230
113
  instance_exec(*a, **b, &o)
231
114
  when nil
115
+ # do nothing
232
116
  else
233
- @buffer << o.to_s
117
+ emit_object(o)
234
118
  end
235
119
  end
236
120
  alias_method :e, :emit
@@ -250,140 +134,28 @@ module Papercraft
250
134
 
251
135
  instance_exec(*a, **b, &block)
252
136
  end
253
-
254
- # Defers the given block to be evaluated later. Deferred evaluation allows
255
- # Papercraft components to inject state into sibling components, regardless
256
- # of the component's order in the container component. For example, a nested
257
- # component may set an instance variable used by another component. This is
258
- # an elegant solution to the problem of setting the HTML page's title, or
259
- # adding elements to the `<head>` section. Here's how a title can be
260
- # controlled from a nested component:
261
- #
262
- # layout = Papercraft.html {
263
- # html {
264
- # head {
265
- # defer { title @title }
266
- # }
267
- # body {
268
- # emit_yield
269
- # }
270
- # }
271
- # }
272
- #
273
- # html.render {
274
- # @title = 'My super page'
275
- # h1 'content'
276
- # }
277
- #
278
- # @param &block [Proc] Deferred block to be emitted
279
- # @return [void]
280
- def defer(&block)
281
- if !@parts
282
- @parts = [@buffer, block]
283
- else
284
- @parts << @buffer unless @buffer.empty?
285
- @parts << block
286
- end
287
- @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
288
- end
289
137
 
290
- S_LT = '<'
291
- S_GT = '>'
292
- S_LT_SLASH = '</'
293
- S_SPACE_LT_SLASH = ' </'
294
- S_SLASH_GT = '/>'
295
- S_SPACE = ' '
296
- S_EQUAL_QUOTE = '="'
297
- S_QUOTE = '"'
298
-
299
- # Emits text into the rendering buffer, escaping any special characters to
300
- # the respective HTML entities.
301
- #
302
- # @param data [String] text
303
- # @return [void]
304
- def text(data)
305
- @buffer << escape_text(data)
306
- end
307
-
308
138
  private
309
139
 
310
- # Escapes text. This method must be overriden in descendant classes.
311
- #
312
- # @param text [String] text to be escaped
313
- def escape_text(text)
314
- raise NotImplementedError
315
- end
316
-
317
140
  # Pushes the given block onto the emit_yield stack.
318
141
  #
319
142
  # @param block [Proc] block
320
143
  def push_emit_yield_block(block)
321
144
  (@emit_yield_stack ||= []) << block
322
145
  end
323
-
324
- # Emits tag attributes into the rendering buffer
325
- # @param props [Hash] tag attributes
326
- # @return [void]
327
- def emit_props(props)
328
- props.each { |k, v|
329
- case k
330
- when :src, :href
331
- @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
332
- EscapeUtils.escape_uri(v) << S_QUOTE
333
- else
334
- case v
335
- when true
336
- @buffer << S_SPACE << k.to_s.tr('_', '-')
337
- when false, nil
338
- # emit nothing
339
- else
340
- @buffer << S_SPACE << k.to_s.tr('_', '-') <<
341
- S_EQUAL_QUOTE << v << S_QUOTE
342
- end
343
- end
344
- }
345
- end
346
-
347
- # Renders a deferred proc by evaluating it, then adding the rendered result
348
- # to the buffer.
349
- #
350
- # @param &block [Proc] deferred proc
351
- # @return [void]
352
- def render_deferred_proc(&block)
353
- old_buffer = @buffer
354
-
355
- @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
356
- @parts = nil
357
-
358
- instance_eval(&block)
359
-
360
- old_buffer << to_s
361
- @buffer = old_buffer
362
- end
363
146
  end
364
147
 
365
148
  # Implements an HTML renderer
366
149
  class HTMLRenderer < Renderer
367
150
  include HTML
368
-
369
- private
370
-
371
- # Escapes the given text using HTML entities.
372
- def escape_text(text)
373
- EscapeUtils.escape_html(text.to_s)
374
- end
375
151
  end
376
152
 
377
153
  # Implements an XML renderer
378
154
  class XMLRenderer < Renderer
379
- private
380
-
381
- # Escapes the given text using XML entities.
382
- def escape_text(text)
383
- EscapeUtils.escape_xml(text.to_s)
384
- end
155
+ include XML
385
156
  end
386
157
 
158
+ # Implements a JSON renderer
387
159
  class JSONRenderer < Renderer
388
160
  include JSON
389
161
  end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Papercraft
4
+ # Markup (HTML/XML) extensions
5
+ module Tags
6
+ S_LT = '<'
7
+ S_GT = '>'
8
+ S_LT_SLASH = '</'
9
+ S_SPACE_LT_SLASH = ' </'
10
+ S_SLASH_GT = '/>'
11
+ S_SPACE = ' '
12
+ S_EQUAL_QUOTE = '="'
13
+ S_QUOTE = '"'
14
+
15
+ # The tag method template below is optimized for performance. Do not touch!
16
+
17
+ S_TAG_METHOD_LINE = __LINE__ + 2
18
+ S_TAG_METHOD = <<~EOF
19
+ S_TAG_%<TAG>s_PRE = %<tag_pre>s
20
+ S_TAG_%<TAG>s_CLOSE = %<tag_close>s
21
+
22
+ def %<tag>s(text = nil, **props, &block)
23
+ if text.is_a?(Hash) && props.empty?
24
+ props = text
25
+ text = nil
26
+ end
27
+
28
+ @buffer << S_TAG_%<TAG>s_PRE
29
+ emit_props(props) unless props.empty?
30
+
31
+ if block
32
+ @buffer << S_GT
33
+ instance_eval(&block)
34
+ @buffer << S_TAG_%<TAG>s_CLOSE
35
+ elsif Proc === text
36
+ @buffer << S_GT
37
+ emit(text)
38
+ @buffer << S_TAG_%<TAG>s_CLOSE
39
+ elsif text
40
+ @buffer << S_GT << escape_text(text.to_s) << S_TAG_%<TAG>s_CLOSE
41
+ else
42
+ @buffer << S_SLASH_GT
43
+ end
44
+ end
45
+ EOF
46
+
47
+ INITIAL_BUFFER_CAPACITY = 8192
48
+
49
+ # Initializes a tag renderer.
50
+ def initialize(&template)
51
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
52
+ super
53
+ end
54
+
55
+ # Returns the rendered template.
56
+ #
57
+ # @return [String]
58
+ def to_s
59
+ if @parts
60
+ last = @buffer
61
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
62
+ parts = @parts
63
+ @parts = nil
64
+ parts.each do |p|
65
+ if Proc === p
66
+ render_deferred_proc(&p)
67
+ else
68
+ @buffer << p
69
+ end
70
+ end
71
+ @buffer << last unless last.empty?
72
+ end
73
+ @buffer
74
+ end
75
+
76
+ # Defers the given block to be evaluated later. Deferred evaluation allows
77
+ # Papercraft templates to inject state into sibling components, regardless
78
+ # of the component's order in the container component. For example, a nested
79
+ # component may set an instance variable used by another component. This is
80
+ # an elegant solution to the problem of setting the XML page's title, or
81
+ # adding elements to the `<head>` section. Here's how a title can be
82
+ # controlled from a nested component:
83
+ #
84
+ # layout = Papercraft.html {
85
+ # html {
86
+ # head {
87
+ # defer { title @title }
88
+ # }
89
+ # body {
90
+ # emit_yield
91
+ # }
92
+ # }
93
+ # }
94
+ #
95
+ # html.render {
96
+ # @title = 'My super page'
97
+ # h1 'content'
98
+ # }
99
+ #
100
+ # @param &block [Proc] Deferred block to be emitted
101
+ # @return [void]
102
+ def defer(&block)
103
+ if !@parts
104
+ @parts = [@buffer, block]
105
+ else
106
+ @parts << @buffer unless @buffer.empty?
107
+ @parts << block
108
+ end
109
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
110
+ end
111
+
112
+
113
+ # Emits an XML tag with the given content, properties and optional block.
114
+ # This method is an alternative to emitting XML tags using dynamically
115
+ # created methods. This is particularly useful when using extensions that
116
+ # have method names that clash with XML tags, such as `button` or `a`, or
117
+ # when you need to override the behaviour of a particular XML tag.
118
+ #
119
+ # The following two method calls have the same effect:
120
+ #
121
+ # button 'text', id: 'button1'
122
+ # tag :button, 'text', id: 'button1'
123
+ #
124
+ # @param sym [Symbol, String] XML tag
125
+ # @param text [String, nil] tag content
126
+ # @param **props [Hash] tag attributes
127
+ # @param &block [Proc] optional inner XML
128
+ # @return [void]
129
+ def tag(sym, text = nil, **props, &block)
130
+ if text.is_a?(Hash) && props.empty?
131
+ props = text
132
+ text = nil
133
+ end
134
+
135
+ tag = tag_repr(sym)
136
+
137
+ @buffer << S_LT << tag
138
+ emit_props(props) unless props.empty?
139
+
140
+ if block
141
+ @buffer << S_GT
142
+ instance_eval(&block)
143
+ @buffer << S_LT_SLASH << tag << S_GT
144
+ elsif Proc === text
145
+ @buffer << S_GT
146
+ emit(text)
147
+ @buffer << S_LT_SLASH << tag << S_GT
148
+ elsif text
149
+ @buffer << S_GT << escape_text(text.to_s) << S_LT_SLASH << tag << S_GT
150
+ else
151
+ @buffer << S_SLASH_GT
152
+ end
153
+ end
154
+
155
+ # Catches undefined tag method call and handles it by defining the method.
156
+ #
157
+ # @param sym [Symbol] XML tag or component identifier
158
+ # @param args [Array] method arguments
159
+ # @param opts [Hash] named method arguments
160
+ # @param &block [Proc] block passed to method
161
+ # @return [void]
162
+ def method_missing(sym, *args, **opts, &block)
163
+ # p method_missing: sym, self: self
164
+ tag = sym.to_s
165
+ repr = tag_repr(tag)
166
+ code = S_TAG_METHOD % {
167
+ tag: tag,
168
+ TAG: tag.upcase,
169
+ tag_pre: "<#{repr}".inspect,
170
+ tag_close: "</#{repr}>".inspect
171
+ }
172
+ self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
173
+ send(sym, *args, **opts, &block)
174
+ end
175
+
176
+ # Emits text into the rendering buffer, escaping any special characters to
177
+ # the respective XML entities.
178
+ #
179
+ # @param data [String] text
180
+ # @return [void]
181
+ def text(data)
182
+ @buffer << escape_text(data)
183
+ end
184
+
185
+ private
186
+
187
+ # Emits an arbitrary object by converting it to string, then adding it to
188
+ # the internal buffer. This method is called internally by `Renderer#emit`.
189
+ #
190
+ # @param obj [Object] emitted object
191
+ # @return [void]
192
+ def emit_object(obj)
193
+ @buffer << obj.to_s
194
+ end
195
+
196
+ # Renders a deferred proc by evaluating it, then adding the rendered result
197
+ # to the buffer.
198
+ #
199
+ # @param &block [Proc] deferred proc
200
+ # @return [void]
201
+ def render_deferred_proc(&block)
202
+ old_buffer = @buffer
203
+
204
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
205
+ @parts = nil
206
+
207
+ instance_eval(&block)
208
+
209
+ old_buffer << to_s
210
+ @buffer = old_buffer
211
+ end
212
+
213
+ # Escapes text. This method must be overriden in Renderers which include
214
+ # this module.
215
+ #
216
+ # @param text [String] text to be escaped
217
+ def escape_text(text)
218
+ raise NotImplementedError
219
+ end
220
+
221
+ # Converts a tag to its string representation. This method must be overriden
222
+ # in Renderers which include this module.
223
+ #
224
+ # @param tag [Symbol, String] tag
225
+ def tag_repr(tag)
226
+ raise NotImplementedError
227
+ end
228
+
229
+ # Converts an attribute to its string representation. This method must be
230
+ # overriden in Renderers which include this module.
231
+ #
232
+ # @param att [Symbol, String] attribute
233
+ def att_repr(att)
234
+ raise NotImplementedError
235
+ end
236
+
237
+ # Emits tag attributes into the rendering buffer.
238
+ #
239
+ # @param props [Hash] tag attributes
240
+ # @return [void]
241
+ def emit_props(props)
242
+ props.each { |k, v|
243
+ case k
244
+ when :src, :href
245
+ @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
246
+ EscapeUtils.escape_uri(v) << S_QUOTE
247
+ else
248
+ case v
249
+ when true
250
+ @buffer << S_SPACE << att_repr(k)
251
+ when false, nil
252
+ # emit nothing
253
+ else
254
+ @buffer << S_SPACE << att_repr(k) <<
255
+ S_EQUAL_QUOTE << v << S_QUOTE
256
+ end
257
+ end
258
+ }
259
+ end
260
+ end
261
+ end