papercraft 0.18 → 0.22

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,7 +101,7 @@ 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]
@@ -230,8 +112,9 @@ module Papercraft
230
112
  push_emit_yield_block(block) if block
231
113
  instance_exec(*a, **b, &o)
232
114
  when nil
115
+ # do nothing
233
116
  else
234
- @buffer << o.to_s
117
+ emit_object(o)
235
118
  end
236
119
  end
237
120
  alias_method :e, :emit
@@ -251,140 +134,28 @@ module Papercraft
251
134
 
252
135
  instance_exec(*a, **b, &block)
253
136
  end
254
-
255
- # Defers the given block to be evaluated later. Deferred evaluation allows
256
- # Papercraft components to inject state into sibling components, regardless
257
- # of the component's order in the container component. For example, a nested
258
- # component may set an instance variable used by another component. This is
259
- # an elegant solution to the problem of setting the HTML page's title, or
260
- # adding elements to the `<head>` section. Here's how a title can be
261
- # controlled from a nested component:
262
- #
263
- # layout = Papercraft.html {
264
- # html {
265
- # head {
266
- # defer { title @title }
267
- # }
268
- # body {
269
- # emit_yield
270
- # }
271
- # }
272
- # }
273
- #
274
- # html.render {
275
- # @title = 'My super page'
276
- # h1 'content'
277
- # }
278
- #
279
- # @param &block [Proc] Deferred block to be emitted
280
- # @return [void]
281
- def defer(&block)
282
- if !@parts
283
- @parts = [@buffer, block]
284
- else
285
- @parts << @buffer unless @buffer.empty?
286
- @parts << block
287
- end
288
- @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
289
- end
290
137
 
291
- S_LT = '<'
292
- S_GT = '>'
293
- S_LT_SLASH = '</'
294
- S_SPACE_LT_SLASH = ' </'
295
- S_SLASH_GT = '/>'
296
- S_SPACE = ' '
297
- S_EQUAL_QUOTE = '="'
298
- S_QUOTE = '"'
299
-
300
- # Emits text into the rendering buffer, escaping any special characters to
301
- # the respective HTML entities.
302
- #
303
- # @param data [String] text
304
- # @return [void]
305
- def text(data)
306
- @buffer << escape_text(data)
307
- end
308
-
309
138
  private
310
139
 
311
- # Escapes text. This method must be overriden in descendant classes.
312
- #
313
- # @param text [String] text to be escaped
314
- def escape_text(text)
315
- raise NotImplementedError
316
- end
317
-
318
140
  # Pushes the given block onto the emit_yield stack.
319
141
  #
320
142
  # @param block [Proc] block
321
143
  def push_emit_yield_block(block)
322
144
  (@emit_yield_stack ||= []) << block
323
145
  end
324
-
325
- # Emits tag attributes into the rendering buffer
326
- # @param props [Hash] tag attributes
327
- # @return [void]
328
- def emit_props(props)
329
- props.each { |k, v|
330
- case k
331
- when :src, :href
332
- @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
333
- EscapeUtils.escape_uri(v) << S_QUOTE
334
- else
335
- case v
336
- when true
337
- @buffer << S_SPACE << k.to_s.tr('_', '-')
338
- when false, nil
339
- # emit nothing
340
- else
341
- @buffer << S_SPACE << k.to_s.tr('_', '-') <<
342
- S_EQUAL_QUOTE << v << S_QUOTE
343
- end
344
- end
345
- }
346
- end
347
-
348
- # Renders a deferred proc by evaluating it, then adding the rendered result
349
- # to the buffer.
350
- #
351
- # @param &block [Proc] deferred proc
352
- # @return [void]
353
- def render_deferred_proc(&block)
354
- old_buffer = @buffer
355
-
356
- @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
357
- @parts = nil
358
-
359
- instance_eval(&block)
360
-
361
- old_buffer << to_s
362
- @buffer = old_buffer
363
- end
364
146
  end
365
147
 
366
148
  # Implements an HTML renderer
367
149
  class HTMLRenderer < Renderer
368
150
  include HTML
369
-
370
- private
371
-
372
- # Escapes the given text using HTML entities.
373
- def escape_text(text)
374
- EscapeUtils.escape_html(text.to_s)
375
- end
376
151
  end
377
152
 
378
153
  # Implements an XML renderer
379
154
  class XMLRenderer < Renderer
380
- private
381
-
382
- # Escapes the given text using XML entities.
383
- def escape_text(text)
384
- EscapeUtils.escape_xml(text.to_s)
385
- end
155
+ include XML
386
156
  end
387
157
 
158
+ # Implements a JSON renderer
388
159
  class JSONRenderer < Renderer
389
160
  include JSON
390
161
  end
@@ -0,0 +1,260 @@
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
+ tag = sym.to_s
164
+ repr = tag_repr(tag)
165
+ code = S_TAG_METHOD % {
166
+ tag: tag,
167
+ TAG: tag.upcase,
168
+ tag_pre: "<#{repr}".inspect,
169
+ tag_close: "</#{repr}>".inspect
170
+ }
171
+ self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
172
+ send(sym, *args, **opts, &block)
173
+ end
174
+
175
+ # Emits text into the rendering buffer, escaping any special characters to
176
+ # the respective XML entities.
177
+ #
178
+ # @param data [String] text
179
+ # @return [void]
180
+ def text(data)
181
+ @buffer << escape_text(data)
182
+ end
183
+
184
+ private
185
+
186
+ # Emits an arbitrary object by converting it to string, then adding it to
187
+ # the internal buffer. This method is called internally by `Renderer#emit`.
188
+ #
189
+ # @param obj [Object] emitted object
190
+ # @return [void]
191
+ def emit_object(obj)
192
+ @buffer << obj.to_s
193
+ end
194
+
195
+ # Renders a deferred proc by evaluating it, then adding the rendered result
196
+ # to the buffer.
197
+ #
198
+ # @param &block [Proc] deferred proc
199
+ # @return [void]
200
+ def render_deferred_proc(&block)
201
+ old_buffer = @buffer
202
+
203
+ @buffer = String.new(capacity: INITIAL_BUFFER_CAPACITY)
204
+ @parts = nil
205
+
206
+ instance_eval(&block)
207
+
208
+ old_buffer << to_s
209
+ @buffer = old_buffer
210
+ end
211
+
212
+ # Escapes text. This method must be overriden in Renderers which include
213
+ # this module.
214
+ #
215
+ # @param text [String] text to be escaped
216
+ def escape_text(text)
217
+ raise NotImplementedError
218
+ end
219
+
220
+ # Converts a tag to its string representation. This method must be overriden
221
+ # in Renderers which include this module.
222
+ #
223
+ # @param tag [Symbol, String] tag
224
+ def tag_repr(tag)
225
+ raise NotImplementedError
226
+ end
227
+
228
+ # Converts an attribute to its string representation. This method must be
229
+ # overriden in Renderers which include this module.
230
+ #
231
+ # @param att [Symbol, String] attribute
232
+ def att_repr(att)
233
+ raise NotImplementedError
234
+ end
235
+
236
+ # Emits tag attributes into the rendering buffer.
237
+ #
238
+ # @param props [Hash] tag attributes
239
+ # @return [void]
240
+ def emit_props(props)
241
+ props.each { |k, v|
242
+ case k
243
+ when :src, :href
244
+ @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
245
+ EscapeUtils.escape_uri(v) << S_QUOTE
246
+ else
247
+ case v
248
+ when true
249
+ @buffer << S_SPACE << att_repr(k)
250
+ when false, nil
251
+ # emit nothing
252
+ else
253
+ @buffer << S_SPACE << att_repr(k) <<
254
+ S_EQUAL_QUOTE << v << S_QUOTE
255
+ end
256
+ end
257
+ }
258
+ end
259
+ end
260
+ end