roda-tags 0.1.1

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.
@@ -0,0 +1,452 @@
1
+ require 'roda'
2
+ require_relative '../../core_ext/hash' unless {}.respond_to?(:to_html_attributes)
3
+ require_relative '../../core_ext/blank' unless Object.new.respond_to?(:blank?)
4
+
5
+
6
+ class Roda
7
+
8
+ #
9
+ module RodaPlugins
10
+
11
+ # TODO: Add documentation here
12
+ #
13
+ #
14
+ module RodaTags
15
+
16
+ # default options
17
+ OPTS = {
18
+ # toggle for XHTML formatted output in case of legacy
19
+ tag_output_format_is_xhtml: false,
20
+ # toggle for adding newlines after output
21
+ tag_add_newlines_after_tags: true
22
+ }.freeze
23
+
24
+ # Tags that should be rendered in multiple lines, like...
25
+ #
26
+ # <body>
27
+ # <snip...>
28
+ # </body>
29
+ #
30
+ MULTI_LINE_TAGS = %w(
31
+ a address applet bdo big blockquote body button caption center
32
+ colgroup dd dir div dl dt fieldset form frameset head html iframe
33
+ map noframes noscript object ol optgroup pre script section select small
34
+ style table tbody td tfoot th thead title tr tt ul
35
+ )
36
+
37
+ # Self closing tags, like...
38
+ #
39
+ # <hr> or <hr />
40
+ #
41
+ SELF_CLOSING_TAGS = %w( area base br col frame hr img input link meta param )
42
+
43
+ # Tags that should be rendered in a single line, like...
44
+ #
45
+ # <h1>Header</h1>
46
+ #
47
+ SINGLE_LINE_TAGS = %w(
48
+ abbr acronym b cite code del dfn em h1 h2 h3 h4 h5 h6 i kbd
49
+ label legend li option p q samp span strong sub sup var
50
+ )
51
+
52
+ # Boolean attributes, ie: attributes like...
53
+ #
54
+ # <option value="a" selected="selected">A</option>
55
+ #
56
+ BOOLEAN_ATTRIBUTES = %w(autofocus checked disabled multiple readonly required selected)
57
+
58
+
59
+ # Depend on the render plugin, since this plugin only makes
60
+ # sense when the render plugin is used.
61
+ def self.load_dependencies(app, opts = OPTS)
62
+ app.plugin :render
63
+ end
64
+
65
+ def self.configure(app, opts = {})
66
+ if app.opts[:tags]
67
+ opts = app.opts[:tags][:orig_opts].merge(opts)
68
+ else
69
+ opts = OPTS.merge(opts)
70
+ end
71
+
72
+ app.opts[:tags] = opts.dup
73
+ app.opts[:tags][:orig_opts] = opts
74
+ end
75
+
76
+ #
77
+ module ClassMethods
78
+
79
+ # Return the uitags options for this class.
80
+ def tags_opts
81
+ opts[:tags]
82
+ end
83
+
84
+ end
85
+
86
+ #
87
+ module InstanceMethods
88
+
89
+ # Returns markup for tag _name_.
90
+ #
91
+ # Optionally _contents_ may be passed, which is literal content for spanning tags such as
92
+ # <tt>textarea</tt>, etc.
93
+ #
94
+ # A hash of _attrs_ may be passed as the *second* or *third* argument.
95
+ #
96
+ # Self closing tags such as <tt><br/></tt>, <tt><input/></tt>, etc are automatically closed
97
+ # depending on output format, HTML vs XHTML.
98
+ #
99
+ # Boolean attributes like "<tt>selected</tt>", "<tt>checked</tt>" etc, are mirrored or
100
+ # removed when <tt>true</tt> or <tt>false</tt>.
101
+ #
102
+ # ==== Examples
103
+ #
104
+ # Self closing tags:
105
+ #
106
+ # tag(:br)
107
+ # # => <br> or <br/> if XHTML
108
+ #
109
+ # tag(:hr, class: "space")
110
+ # # => <hr class="space">
111
+ #
112
+ # Multi line tags:
113
+ #
114
+ # tag(:div)
115
+ # # => <div></div>
116
+ #
117
+ # tag(:div, 'content')
118
+ # # => <div>content</div>
119
+ #
120
+ # tag(:div, 'content', id: 'comment')
121
+ # # => <div id="comment">content</div>
122
+ #
123
+ # tag(:div, id: 'comment') # NB! no content
124
+ # # => <div id="comment"></div>
125
+ #
126
+ # Single line tags:
127
+ #
128
+ # tag(:h1,'Header')
129
+ # # => <h1>Header</h1>
130
+ #
131
+ # tag(:abbr, 'WHO', :title => "World Health Organization")
132
+ # # => <abbr title="World Health Organization">WHO</abbr>
133
+ #
134
+ # Working with blocks
135
+ #
136
+ # tag(:div) do
137
+ # tag(:p, 'Hello World')
138
+ # end
139
+ # # => <div><p>Hello World</p></div>
140
+ #
141
+ # <% tag(:div) do %>
142
+ # <p>Paragraph 1</p>
143
+ # <%= tag(:p, 'Paragraph 2') %>
144
+ # <p>Paragraph 3</p>
145
+ # <% end %>
146
+ # # =>
147
+ # <div>
148
+ # <p>Paragraph 1</p>
149
+ # <p>Paragraph 2</p>
150
+ # <p>Paragraph 3</p>
151
+ # </div>
152
+ #
153
+ #
154
+ # # NB! ignored tag contents if given a block
155
+ # <% tag(:div, 'ignored tag-content') do %>
156
+ # <%= tag(:label, 'Comments:', for: :comments) %>
157
+ # <%= tag(:textarea,'textarea contents', id: :comments) %>
158
+ # <% end %>
159
+ # # =>
160
+ # <div>
161
+ # <label for="comments">Comments:</label>
162
+ # <textarea id="comments">
163
+ # textarea contents
164
+ # </textarea>
165
+ # </div>
166
+ #
167
+ #
168
+ #
169
+ # Boolean attributes:
170
+ #
171
+ # tag(:input, type: :checkbox, checked: true)
172
+ # # => <input type="checkbox" checked="checked">
173
+ #
174
+ # tag(:option, 'Sinatra', value: "1", selected: true)
175
+ # # => <option value="1" selected>Sinatra</option>
176
+ #
177
+ # tag(:option, 'PHP', value: "0", selected: false)
178
+ # # => <option value="0">PHP</option>
179
+ #
180
+ def tag(*args, &block)
181
+ name = args.first
182
+ attrs = args.last.is_a?(Hash) ? args.pop : {}
183
+ newline = attrs[:newline] # save before it gets tainted
184
+
185
+ tag_content = block_given? ? capture_html(&block) : args[1] # content
186
+
187
+ if self_closing_tag?(name)
188
+ tag_html = self_closing_tag(name, attrs)
189
+ else
190
+ tag_html = "#{open_tag(name, attrs)}#{tag_contents_for(name, tag_content, newline)}"
191
+ tag_html << closing_tag(name)
192
+ end
193
+ block_is_template?(block) ? concat_content(tag_html) : tag_html
194
+ end
195
+
196
+ # Update the +:class+ entry in the +attr+ hash with the given +classes+ and returns +attr+.
197
+ #
198
+ # attr = { class: 'alert', id: :idval }
199
+ #
200
+ # merge_attr_classes(attr, 'alert-info') #=> { class: 'alert alert-info', id: :idval }
201
+ #
202
+ # merge_attr_classes(attr, [:alert, 'alert-info'])
203
+ # #=> { class: 'alert alert-info', id: :idval }
204
+ #
205
+ def merge_attr_classes(attr, *classes)
206
+ attr[:class] = [] if attr[:class].blank?
207
+ attr[:class] = merge_classes(attr[:class], *classes)
208
+ attr[:class] = nil if attr[:class] == '' # set to nil to remove from tag output
209
+ attr
210
+ end
211
+
212
+ # Return an alphabetized string that includes all given class values.
213
+ #
214
+ # Handles a combination of arrays, strings & symbols being passed in.
215
+ #
216
+ # attr = { class: 'alert', id: :idval }
217
+ #
218
+ # merge_classes(attr[:class], ['alert', 'alert-info']) #=> 'alert alert-info'
219
+ #
220
+ # merge_classes(attr[:class], :text) #=> 'alert text'
221
+ #
222
+ # merge_classes(attr[:class], [:text, :'alert-info']) #=> 'alert alert-info text'
223
+ #
224
+ #
225
+ def merge_classes(*classes)
226
+ klasses = []
227
+ classes.each do |c|
228
+ klasses << c.to_s if c.is_a?(Symbol)
229
+ c.split(/\s+/).each { |x| klasses << x.to_s } if c.is_a?(String)
230
+ c.each { |i| klasses << i.to_s } if c.is_a?(Array)
231
+ end
232
+ klasses.compact.uniq.sort.join(' ').strip
233
+ end
234
+
235
+
236
+ ## HELPERS
237
+
238
+
239
+ #
240
+ def capture(block = '') # :nodoc:
241
+ buf_was = @output
242
+ @output = block.is_a?(Proc) ? (eval('@_out_buf', block.binding) || @output) : block
243
+ yield
244
+ ret = @output
245
+ @output = buf_was
246
+ ret
247
+ end
248
+
249
+ # Captures the html from a block of template code for erb or haml
250
+ #
251
+ # ==== Examples
252
+ #
253
+ # capture_html(&block) => "...html..."
254
+ #
255
+ def capture_html(*args, &block)
256
+ if self.respond_to?(:is_haml?) && is_haml?
257
+ block_is_haml?(block) ? capture_haml(*args, &block) : block.call
258
+ elsif erb_buffer?
259
+ result_text = capture_block(*args, &block)
260
+ result_text.present? ? result_text : (block_given? && block.call(*args))
261
+ else # theres no template to capture, invoke the block directly
262
+ block.call(*args)
263
+ end
264
+ end
265
+
266
+ # Outputs the given text to the templates buffer directly.
267
+ #
268
+ # ==== Examples
269
+ #
270
+ # concat_content("This will be output to the template buffer in erb or haml")
271
+ #
272
+ def concat_content(text = '')
273
+ if self.respond_to?(:is_haml?) && is_haml?
274
+ haml_concat(text)
275
+ elsif :erb_buffer?
276
+ buffer_concat(text)
277
+ else # theres no template to concat, return the text directly
278
+ text
279
+ end
280
+ end
281
+
282
+ # Returns true if the block is from an ERB or HAML template; false otherwise.
283
+ # Used to determine if html should be returned or concatenated to a view.
284
+ #
285
+ # ==== Examples
286
+ #
287
+ # block_is_template?(block)
288
+ #
289
+ def block_is_template?(block)
290
+ block && (erb_block?(block) ||
291
+ (self.respond_to?(:block_is_haml?) && block_is_haml?(block)))
292
+ end
293
+
294
+ #
295
+ def output_is_xhtml?
296
+ opts[:tags][:tag_output_format_is_xhtml]
297
+ end
298
+
299
+
300
+ private
301
+
302
+
303
+ # Return an opening tag of _name_, with _attrs_.
304
+ def open_tag(name, attrs = {})
305
+ "<#{name}#{normalize_html_attributes(attrs)}>"
306
+ end
307
+
308
+ # Return closing tag of _name_.
309
+ def closing_tag(name)
310
+ "</#{name}>#{add_newline?}"
311
+ end
312
+
313
+ # Creates a self closing tag. Like <br/> or <img src="..."/>
314
+ #
315
+ # ==== Options
316
+ # +name+ : the name of the tag to create
317
+ # +attrs+ : a hash where all members will be mapped to key="value"
318
+ #
319
+ def self_closing_tag(name, attrs = {})
320
+ newline = (attrs[:newline].nil?) ? nil : attrs.delete(:newline)
321
+ "<#{name}#{normalize_html_attributes(attrs)}#{is_xhtml?}#{add_newline?(newline)}"
322
+ end
323
+
324
+ # Based upon the context, wraps the tag content in '\n' (newlines)
325
+ #
326
+ # ==== Examples
327
+ #
328
+ # tag_contents_for(:div, 'content', nil)
329
+ # # => <div>content</div>
330
+ #
331
+ # tag_contents_for(:div, 'content', false)
332
+ # # => <div>content</div>
333
+ #
334
+ # Single line tag
335
+ # tag_contents_for(:option, 'content', true)
336
+ # # => <option...>\ncontent\n</option>
337
+ #
338
+ def tag_contents_for(name, content, newline = nil)
339
+ if multi_line_tag?(name)
340
+ "#{add_newline?(newline)}#{content}#{add_newline?(newline)}".gsub(/\n\n/, "\n")
341
+ elsif single_line_tag?(name) && newline == true
342
+ "#{add_newline?(newline)}#{content}#{add_newline?(newline)}"
343
+ else
344
+ content.to_s
345
+ end
346
+ end
347
+
348
+ # Normalize _attrs_, replacing boolean keys with their mirrored values.
349
+ def normalize_html_attributes(attrs = {})
350
+ return if attrs.blank?
351
+ attrs.delete(:newline) # remove newline from attributes
352
+ # look for data attrs
353
+ if value = attrs.delete(:data)
354
+ # NB!! convert key to symbol for [].sort
355
+ value.each { |k, v| attrs[:"data-#{k.to_s}"] = v }
356
+ end
357
+ attrs.each do |name, val|
358
+ if boolean_attribute?(name)
359
+ val == true ? attrs[name] = name : attrs.delete(name)
360
+ end
361
+ end
362
+ return attrs.empty? ? '' : ' ' + attrs.to_html_attributes
363
+ end
364
+
365
+ # Check if _name_ is a boolean attribute.
366
+ def boolean_attribute?(name)
367
+ BOOLEAN_ATTRIBUTES.include?(name.to_s)
368
+ end
369
+
370
+ # Check if tag _name_ is a self-closing tag.
371
+ def self_closing_tag?(name)
372
+ SELF_CLOSING_TAGS.include?(name.to_s)
373
+ end
374
+
375
+ # Check if tag _name_ is a single line tag.
376
+ def single_line_tag?(name)
377
+ SINGLE_LINE_TAGS.include?(name.to_s)
378
+ end
379
+
380
+ # Check if tag _name_ is a multi line tag.
381
+ def multi_line_tag?(name)
382
+ MULTI_LINE_TAGS.include?(name.to_s)
383
+ end
384
+
385
+ # Returns a '>' or ' />' string based on the output format used, ie: HTML vs XHTML
386
+ def xhtml?
387
+ opts[:tags][:tag_output_format_is_xhtml] ? ' />' : '>'
388
+ end
389
+ alias_method :is_xhtml?, :xhtml?
390
+
391
+ #
392
+ def add_newline?(add_override = nil)
393
+ add = (add_override.nil?) ? opts[:tags][:tag_add_newlines_after_tags] : add_override
394
+ add == true ? "\n" : ''
395
+ end
396
+
397
+ # concat contents to the buffer if present
398
+ #
399
+ # ==== Examples
400
+ #
401
+ # buffer_concat("Direct to buffer")
402
+ #
403
+ def buffer_concat(txt)
404
+ @_out_buf << txt if buffer?
405
+ end
406
+ alias_method :erb_concat, :buffer_concat
407
+
408
+ # Used to capture the contents of html/ERB block
409
+ #
410
+ # ==== Examples
411
+ #
412
+ # capture_block(&block) => '...html...'
413
+ #
414
+ def capture_block(*args, &block)
415
+ with_output_buffer { block_given? && block.call(*args) }
416
+ end
417
+ alias_method :capture_erb, :capture_block
418
+
419
+ # Used to direct the buffer for the erb capture
420
+ def with_output_buffer(buf = '')
421
+ @_out_buf, old_buffer = buf, @_out_buf
422
+ yield
423
+ @_out_buf
424
+ ensure
425
+ @_out_buf = old_buffer
426
+ end
427
+ alias_method :erb_with_output_buffer, :with_output_buffer
428
+
429
+ # returns true if the buffer is not empty
430
+ def buffer?
431
+ !@_out_buf.nil?
432
+ end
433
+ alias_method :have_buffer?, :buffer?
434
+ alias_method :erb_buffer?, :buffer?
435
+
436
+ #
437
+ def erb_block?(block)
438
+ have_buffer? || block && eval('defined? __in_erb_template', block.binding)
439
+ end
440
+ alias_method :is_erb_block?, :erb_block?
441
+ alias_method :is_erb_template?, :erb_block?
442
+
443
+
444
+ end # /InstanceMethods
445
+
446
+ end # /RodaTags
447
+
448
+ register_plugin(:tags, RodaTags)
449
+
450
+ end # /RodaPlugins
451
+
452
+ end
data/lib/roda/tags.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'roda'
2
+ require 'roda/tags/version'
3
+ require 'roda/plugins/tags'