papercraft 0.17 → 0.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -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