roda-tags 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +24 -0
- data/.github/workflows/ruby.yml +45 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +33 -0
- data/.rubocop_todo.yml +7 -0
- data/CODE_OF_CONDUCT.md +22 -16
- data/Gemfile +86 -0
- data/Guardfile +25 -0
- data/README.md +234 -236
- data/Rakefile +14 -11
- data/lib/core_ext/blank.rb +37 -36
- data/lib/core_ext/hash.rb +39 -16
- data/lib/core_ext/object.rb +2 -2
- data/lib/core_ext/string.rb +290 -116
- data/lib/roda/plugins/tag_helpers.rb +795 -575
- data/lib/roda/plugins/tags.rb +532 -276
- data/lib/roda/tags/version.rb +2 -3
- data/lib/roda/tags.rb +2 -0
- data/roda-tags.gemspec +30 -32
- metadata +44 -128
- data/.travis.yml +0 -4
data/lib/roda/plugins/tags.rb
CHANGED
@@ -1,18 +1,17 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'roda'
|
4
|
+
|
2
5
|
require_relative '../../core_ext/hash' unless {}.respond_to?(:to_html_attributes)
|
3
6
|
require_relative '../../core_ext/blank' unless Object.new.respond_to?(:blank?)
|
4
7
|
|
5
|
-
|
6
8
|
class Roda
|
7
|
-
|
8
|
-
#
|
9
|
+
# add module documentation
|
9
10
|
module RodaPlugins
|
10
|
-
|
11
11
|
# TODO: Add documentation here
|
12
12
|
#
|
13
13
|
#
|
14
14
|
module RodaTags
|
15
|
-
|
16
15
|
# default options
|
17
16
|
OPTS = {
|
18
17
|
# toggle for XHTML formatted output in case of legacy
|
@@ -20,433 +19,690 @@ class Roda
|
|
20
19
|
# toggle for adding newlines after output
|
21
20
|
tag_add_newlines_after_tags: true
|
22
21
|
}.freeze
|
23
|
-
|
22
|
+
|
24
23
|
# Tags that should be rendered in multiple lines, like...
|
25
|
-
#
|
24
|
+
#
|
26
25
|
# <body>
|
27
26
|
# <snip...>
|
28
27
|
# </body>
|
29
28
|
#
|
30
|
-
MULTI_LINE_TAGS = %w
|
31
|
-
a address applet bdo big blockquote body button caption center
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
29
|
+
MULTI_LINE_TAGS = %w[
|
30
|
+
a address applet bdo big blockquote body button caption center colgroup dd dir div dl dt
|
31
|
+
fieldset form frameset head html iframe map noframes noscript object ol optgroup pre
|
32
|
+
script section select small style table tbody td tfoot th thead title tr tt ul
|
33
|
+
].freeze
|
34
|
+
|
37
35
|
# Self closing tags, like...
|
38
|
-
#
|
36
|
+
#
|
39
37
|
# <hr> or <hr />
|
40
38
|
#
|
41
|
-
SELF_CLOSING_TAGS = %w
|
42
|
-
|
39
|
+
SELF_CLOSING_TAGS = %w[
|
40
|
+
area base br col frame hr img input link meta param
|
41
|
+
].freeze
|
42
|
+
|
43
43
|
# Tags that should be rendered in a single line, like...
|
44
|
-
#
|
44
|
+
#
|
45
45
|
# <h1>Header</h1>
|
46
46
|
#
|
47
|
-
SINGLE_LINE_TAGS = %w
|
48
|
-
abbr acronym b cite code del dfn em h1 h2 h3 h4 h5 h6 i kbd
|
47
|
+
SINGLE_LINE_TAGS = %w[
|
48
|
+
abbr acronym b cite code del dfn em h1 h2 h3 h4 h5 h6 i kbd
|
49
49
|
label legend li option p q samp span strong sub sup var
|
50
|
-
|
51
|
-
|
50
|
+
].freeze
|
51
|
+
|
52
52
|
# Boolean attributes, ie: attributes like...
|
53
|
-
#
|
53
|
+
#
|
54
54
|
# <option value="a" selected="selected">A</option>
|
55
55
|
#
|
56
|
-
BOOLEAN_ATTRIBUTES = %w
|
57
|
-
|
58
|
-
|
56
|
+
BOOLEAN_ATTRIBUTES = %w[
|
57
|
+
autofocus checked disabled multiple readonly required selected
|
58
|
+
].freeze
|
59
|
+
|
59
60
|
# Depend on the render plugin, since this plugin only makes
|
60
61
|
# sense when the render plugin is used.
|
61
|
-
def self.load_dependencies(app,
|
62
|
+
def self.load_dependencies(app, _opts = OPTS)
|
62
63
|
app.plugin :render
|
63
64
|
end
|
64
|
-
|
65
|
+
|
65
66
|
def self.configure(app, opts = {})
|
66
|
-
if app.opts[:tags]
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
opts = if app.opts[:tags]
|
68
|
+
app.opts[:tags][:orig_opts].merge(opts)
|
69
|
+
else
|
70
|
+
OPTS.merge(opts)
|
71
|
+
end
|
72
|
+
|
72
73
|
app.opts[:tags] = opts.dup
|
73
74
|
app.opts[:tags][:orig_opts] = opts
|
74
75
|
end
|
75
|
-
|
76
|
-
#
|
76
|
+
|
77
|
+
# add module documentation
|
77
78
|
module ClassMethods
|
78
|
-
|
79
|
-
#
|
79
|
+
# Returns the tags options hash for the current Roda class instance.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# tags_opts
|
83
|
+
# #=> { tag_output_format_is_xhtml: false, tag_add_newlines_after_tags: true }
|
84
|
+
#
|
80
85
|
def tags_opts
|
81
86
|
opts[:tags]
|
82
87
|
end
|
83
|
-
|
84
88
|
end
|
85
|
-
|
86
|
-
#
|
89
|
+
|
90
|
+
# add module documentation
|
91
|
+
# rubocop:disable Metrics/ModuleLength
|
87
92
|
module InstanceMethods
|
88
|
-
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
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>
|
93
|
+
# Generates HTML tag markup based on the given name, content, and attributes
|
94
|
+
#
|
95
|
+
# @param name [String,Symbol] The tag name to generate (e.g. 'div', :span)
|
96
|
+
# @param content [String,nil] Optional content for the tag (ignored if block given)
|
97
|
+
# @param attrs [Hash] Optional HTML attributes hash that may include :newline toggle
|
98
|
+
# @yield Optional block providing content for the tag
|
99
|
+
#
|
100
|
+
# @return [String] The generated HTML tag markup
|
101
|
+
#
|
102
|
+
# @example Basic tag
|
103
|
+
# tag(:div) #=> <div></div>
|
104
|
+
#
|
105
|
+
# @example Tag with content
|
106
|
+
# tag(:p, "Hello") #=> <p>Hello</p>
|
107
|
+
#
|
108
|
+
# @example Tag with attributes
|
109
|
+
# tag(:div, class: 'btn', id: 'submit')
|
110
|
+
# #=> <div class="btn" id="submit"></div>
|
111
|
+
#
|
112
|
+
# @example Self closing tags:
|
113
|
+
# tag(:br) # => <br> / <br/>
|
114
|
+
#
|
115
|
+
# tag(:hr, class: "space") # => <hr class="space">
|
116
|
+
#
|
117
|
+
# @example Multi line tags:
|
118
|
+
# tag(:div, 'content') # => <div>content</div>
|
119
119
|
#
|
120
120
|
# tag(:div, 'content', id: 'comment')
|
121
|
-
#
|
121
|
+
# # => <div id="comment">content</div>
|
122
122
|
#
|
123
123
|
# tag(:div, id: 'comment') # NB! no content
|
124
|
-
#
|
124
|
+
# # => <div id="comment"></div>
|
125
125
|
#
|
126
|
-
# Single line tags:
|
127
|
-
#
|
126
|
+
# @example Single line tags:
|
128
127
|
# tag(:h1,'Header')
|
129
|
-
#
|
130
|
-
#
|
128
|
+
# # => <h1>Header</h1>
|
129
|
+
#
|
131
130
|
# tag(:abbr, 'WHO', :title => "World Health Organization")
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
131
|
+
# # => <abbr title="World Health Organization">WHO</abbr>
|
132
|
+
#
|
133
|
+
# @example Tag with block
|
134
|
+
# tag(:div) { tag(:p, "Content") } #=> "<div><p>Content</p></div>"
|
135
|
+
#
|
136
|
+
# @example Working with blocks
|
136
137
|
# tag(:div) do
|
137
138
|
# tag(:p, 'Hello World')
|
138
139
|
# end
|
139
|
-
#
|
140
|
-
#
|
140
|
+
# # => <div><p>Hello World</p></div>
|
141
|
+
#
|
141
142
|
# <% tag(:div) do %>
|
142
143
|
# <p>Paragraph 1</p>
|
143
144
|
# <%= tag(:p, 'Paragraph 2') %>
|
144
145
|
# <p>Paragraph 3</p>
|
145
146
|
# <% end %>
|
146
|
-
#
|
147
|
-
# <
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
# # NB! ignored tag contents if given a block
|
147
|
+
# # => <div>
|
148
|
+
# # <p>Paragraph 1</p>
|
149
|
+
# # <p>Paragraph 2</p>
|
150
|
+
# # <p>Paragraph 3</p>
|
151
|
+
# # </div>
|
152
|
+
#
|
153
|
+
# NOTE! ignored tag contents if given a block
|
154
|
+
#
|
155
155
|
# <% tag(:div, 'ignored tag-content') do %>
|
156
156
|
# <%= tag(:label, 'Comments:', for: :comments) %>
|
157
157
|
# <%= tag(:textarea,'textarea contents', id: :comments) %>
|
158
158
|
# <% end %>
|
159
|
-
#
|
160
|
-
# <
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
#
|
168
|
-
#
|
169
|
-
# Boolean attributes:
|
170
|
-
#
|
159
|
+
# # => <div>
|
160
|
+
# # <label for="comments">Comments:</label>
|
161
|
+
# # <textarea id="comments">
|
162
|
+
# # textarea contents
|
163
|
+
# # </textarea>
|
164
|
+
# # </div>
|
165
|
+
#
|
166
|
+
# @example Boolean attributes
|
171
167
|
# tag(:input, type: :checkbox, checked: true)
|
172
|
-
#
|
173
|
-
#
|
168
|
+
# # => <input type="checkbox" checked="checked">
|
169
|
+
#
|
174
170
|
# tag(:option, 'Sinatra', value: "1", selected: true)
|
175
|
-
#
|
176
|
-
#
|
171
|
+
# # => <option value="1" selected>Sinatra</option>
|
172
|
+
#
|
177
173
|
# tag(:option, 'PHP', value: "0", selected: false)
|
178
|
-
#
|
179
|
-
#
|
174
|
+
# # => <option value="0">PHP</option>
|
175
|
+
#
|
176
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
180
177
|
def tag(*args, &block)
|
181
178
|
name = args.first
|
182
179
|
attrs = args.last.is_a?(Hash) ? args.pop : {}
|
183
180
|
newline = attrs[:newline] # save before it gets tainted
|
184
|
-
|
185
|
-
tag_content = block_given? ? capture_html(&block) : args[1]
|
186
|
-
|
181
|
+
|
182
|
+
tag_content = block_given? ? capture_html(&block) : args[1] # content
|
183
|
+
|
187
184
|
if self_closing_tag?(name)
|
188
185
|
tag_html = self_closing_tag(name, attrs)
|
189
186
|
else
|
190
|
-
tag_html = "#{open_tag(name, attrs)}#{tag_contents_for(name, tag_content, newline)}"
|
187
|
+
tag_html = "#{open_tag(name, attrs)}#{tag_contents_for(name, tag_content, newline)}"
|
191
188
|
tag_html << closing_tag(name)
|
192
189
|
end
|
193
190
|
block_is_template?(block) ? concat_content(tag_html) : tag_html
|
194
191
|
end
|
195
|
-
|
196
|
-
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
200
|
-
#
|
201
|
-
#
|
202
|
-
#
|
203
|
-
#
|
204
|
-
#
|
192
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
193
|
+
|
194
|
+
# Updates the :class attribute in the given hash with additional class values.
|
195
|
+
# Takes a hash and any number of class values as arguments.
|
196
|
+
# Returns the modified hash with merged class values.
|
197
|
+
#
|
198
|
+
# @param attr [Hash] The attributes hash to modify
|
199
|
+
# @param classes [Array<String,Symbol,Array>] Additional class values to merge
|
200
|
+
#
|
201
|
+
# @return [Hash] The modified attributes hash
|
202
|
+
#
|
203
|
+
# @example
|
204
|
+
# attr = { class: 'btn', id: 'submit' }
|
205
|
+
# merge_attr_classes(attr, 'primary', 'large')
|
206
|
+
# #=> { class: 'btn large primary', id: 'submit' }
|
207
|
+
#
|
208
|
+
# attr = { id: 'submit' }
|
209
|
+
# merge_attr_classes(attr, ['btn', 'primary'])
|
210
|
+
# #=> { class: 'btn primary', id: 'submit' }
|
211
|
+
#
|
205
212
|
def merge_attr_classes(attr, *classes)
|
206
213
|
attr[:class] = [] if attr[:class].blank?
|
207
214
|
attr[:class] = merge_classes(attr[:class], *classes)
|
208
215
|
attr[:class] = nil if attr[:class] == '' # set to nil to remove from tag output
|
209
216
|
attr
|
210
217
|
end
|
211
|
-
|
212
|
-
#
|
213
|
-
#
|
214
|
-
#
|
215
|
-
#
|
218
|
+
|
219
|
+
# Merges class values from multiple sources into a single sorted string
|
220
|
+
#
|
221
|
+
# @param classes [Array<String,Symbol,Array>] Class values to merge, which can be:
|
222
|
+
# - Symbols: Converted directly to strings
|
223
|
+
# - Strings: Split on whitespace into multiple classes
|
224
|
+
# - Arrays: Each element converted to string
|
225
|
+
#
|
226
|
+
# @return [String] Space-separated string of unique, sorted class names
|
227
|
+
#
|
228
|
+
# @example Passing a hash
|
216
229
|
# attr = { class: 'alert', id: :idval }
|
217
|
-
#
|
218
230
|
# merge_classes(attr[:class], ['alert', 'alert-info']) #=> 'alert alert-info'
|
219
|
-
#
|
231
|
+
#
|
220
232
|
# merge_classes(attr[:class], :text) #=> 'alert text'
|
221
|
-
#
|
222
|
-
#
|
223
|
-
#
|
224
|
-
#
|
233
|
+
#
|
234
|
+
# @example Passing a string, an array & symbol
|
235
|
+
# merge_classes('btn', ['primary', 'large'], :active) #=> "active btn large primary"
|
236
|
+
#
|
237
|
+
# @example Passing a string & :symbol
|
238
|
+
# merge_classes('alert alert-info', :text) #=> "alert alert-info text"
|
239
|
+
#
|
240
|
+
# rubocop:disable Metrics/AbcSize
|
225
241
|
def merge_classes(*classes)
|
226
242
|
klasses = []
|
227
243
|
classes.each do |c|
|
228
244
|
klasses << c.to_s if c.is_a?(Symbol)
|
229
|
-
c.split(/\s+/).each { |x| klasses << x.to_s
|
245
|
+
c.split(/\s+/).each { |x| klasses << x.to_s } if c.is_a?(String)
|
230
246
|
c.each { |i| klasses << i.to_s } if c.is_a?(Array)
|
231
247
|
end
|
232
248
|
klasses.compact.uniq.sort.join(' ').strip
|
233
249
|
end
|
234
|
-
|
235
|
-
|
250
|
+
# rubocop:enable Metrics/AbcSize
|
251
|
+
|
236
252
|
## HELPERS
|
237
|
-
|
238
|
-
|
239
|
-
#
|
253
|
+
|
254
|
+
# Captures the content of a block with proper buffer handling
|
255
|
+
#
|
256
|
+
# @param block [String, Proc] The block to capture, defaults to empty string
|
257
|
+
#
|
258
|
+
# @return The captured content from the block
|
259
|
+
#
|
260
|
+
# @example Capturing a block's content
|
261
|
+
# capture { tag(:div, "content") } # => <div>content</div>
|
262
|
+
#
|
263
|
+
# @example Capturing with explicit block parameter
|
264
|
+
# capture(some_block) { yield } # => captured block content
|
265
|
+
#
|
240
266
|
def capture(block = '') # :nodoc:
|
241
267
|
buf_was = @output
|
242
|
-
@output = block.is_a?(Proc)
|
268
|
+
@output = if block.is_a?(Proc)
|
269
|
+
eval('@_out_buf', block.binding, __FILE__, __LINE__ - 1) || @output
|
270
|
+
else
|
271
|
+
block
|
272
|
+
end
|
243
273
|
yield
|
244
274
|
ret = @output
|
245
275
|
@output = buf_was
|
246
276
|
ret
|
247
277
|
end
|
248
278
|
|
249
|
-
# Captures the
|
250
|
-
#
|
251
|
-
#
|
252
|
-
#
|
253
|
-
#
|
254
|
-
#
|
255
|
-
|
256
|
-
|
257
|
-
|
279
|
+
# Captures the content of a template block for Haml or ERB templates,
|
280
|
+
# returning the captured HTML
|
281
|
+
#
|
282
|
+
# @param args [Array] Arguments to pass to the block
|
283
|
+
# @param block [Proc] The template block to capture
|
284
|
+
#
|
285
|
+
# @return [String] The captured HTML content
|
286
|
+
#
|
287
|
+
# @example Capturing Haml content
|
288
|
+
# capture_html { tag :div, "Content" } # => <div>Content</div>
|
289
|
+
#
|
290
|
+
# @example Capturing ERB content
|
291
|
+
# capture_html { tag :p, "Content" } # => <p>Content</p>\n
|
292
|
+
#
|
293
|
+
# @example Direct block yield
|
294
|
+
# capture_html { "Content" } # => Content
|
295
|
+
#
|
296
|
+
def capture_html(*args, &block)
|
297
|
+
if respond_to?(:is_haml?) && is_haml?
|
298
|
+
block_is_haml?(block) ? capture_haml(*args, &block) : yield
|
258
299
|
elsif erb_buffer?
|
259
300
|
result_text = capture_block(*args, &block)
|
260
|
-
result_text.present? ? result_text : (block_given? &&
|
301
|
+
result_text.present? ? result_text : (block_given? && yield(*args))
|
261
302
|
else # theres no template to capture, invoke the block directly
|
262
|
-
|
303
|
+
yield(*args)
|
263
304
|
end
|
264
305
|
end
|
265
306
|
|
266
|
-
# Outputs the given text to the
|
267
|
-
#
|
268
|
-
#
|
269
|
-
#
|
270
|
-
#
|
271
|
-
#
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
307
|
+
# Outputs the given text to the template buffer based on the template engine in use.
|
308
|
+
# For Haml templates, uses `haml_concat`. For ERB templates, uses `buffer_concat`.
|
309
|
+
# If no template engine is active, returns the text directly.
|
310
|
+
#
|
311
|
+
# @param text [String] The text to output, defaults to empty string
|
312
|
+
#
|
313
|
+
# @return [String] The text if no template engine is active
|
314
|
+
#
|
315
|
+
# @example With Haml template
|
316
|
+
# concat_content("Hello") # => Outputs "Hello" to Haml buffer
|
317
|
+
#
|
318
|
+
# @example With ERB template
|
319
|
+
# concat_content("World") # => Outputs "World" to ERB buffer
|
320
|
+
#
|
321
|
+
# @example With no template
|
322
|
+
# concat_content("Test") # => Returns "Test" string
|
323
|
+
#
|
324
|
+
def concat_content(text = '')
|
325
|
+
if respond_to?(:is_haml?) && is_haml?
|
326
|
+
haml_concat(text.to_s)
|
327
|
+
elsif erb_buffer?
|
328
|
+
buffer_concat(text.to_s)
|
277
329
|
else # theres no template to concat, return the text directly
|
278
|
-
text
|
330
|
+
text.to_s
|
279
331
|
end
|
280
332
|
end
|
281
333
|
|
282
|
-
# Returns true if the block is from an ERB or HAML template
|
283
|
-
#
|
284
|
-
#
|
285
|
-
#
|
286
|
-
#
|
287
|
-
#
|
288
|
-
#
|
289
|
-
|
290
|
-
|
291
|
-
|
334
|
+
# Returns true if the given block is from an ERB or HAML template
|
335
|
+
#
|
336
|
+
# @param block [Proc] The block to check
|
337
|
+
#
|
338
|
+
# @return [Boolean] true if block is from an ERB/HAML template, false otherwise
|
339
|
+
#
|
340
|
+
# @example
|
341
|
+
# # if block is from ERB template
|
342
|
+
# block_is_template?(some_block) #=> true
|
343
|
+
#
|
344
|
+
# # if block is from HAML template
|
345
|
+
# block_is_template?(some_block) #=> true
|
346
|
+
#
|
347
|
+
# block_is_template?(regular_block) #=> false
|
348
|
+
#
|
349
|
+
def block_is_template?(block)
|
350
|
+
block && (erb_block?(block) || (respond_to?(:block_is_haml?) && block_is_haml?(block)))
|
292
351
|
end
|
293
|
-
|
294
|
-
#
|
352
|
+
|
353
|
+
# Returns whether the current output format is XHTML based on tag configuration
|
354
|
+
#
|
355
|
+
# @return [Boolean] true if XHTML output is enabled, false for HTML output
|
356
|
+
#
|
357
|
+
# @example
|
358
|
+
# # if :tag_output_format_is_xhtml is true in config
|
359
|
+
# output_is_xhtml? #=> true
|
360
|
+
#
|
361
|
+
# # if :tag_output_format_is_xhtml is false in config
|
362
|
+
# output_is_xhtml? #=> false
|
363
|
+
#
|
295
364
|
def output_is_xhtml?
|
296
365
|
opts[:tags][:tag_output_format_is_xhtml]
|
297
366
|
end
|
298
|
-
|
299
|
-
|
367
|
+
|
300
368
|
private
|
301
|
-
|
302
|
-
|
303
|
-
#
|
304
|
-
|
369
|
+
|
370
|
+
# Returns an opening HTML tag string with the given name and optional attributes
|
371
|
+
#
|
372
|
+
# @param name [String,Symbol] The tag name (e.g. 'div', :span)
|
373
|
+
# @param attrs [Hash] Optional HTML attributes hash (e.g. {class: 'btn'})
|
374
|
+
#
|
375
|
+
# @return [String] The opening tag string (e.g. '<div class="btn">')
|
376
|
+
#
|
377
|
+
# @example Basic tag
|
378
|
+
# open_tag(:div) #=> "<div>"
|
379
|
+
#
|
380
|
+
# @example Tag with attributes
|
381
|
+
# open_tag(:div, class: 'btn', id: 'submit')
|
382
|
+
# #=> <div class="btn" id="submit">
|
383
|
+
#
|
384
|
+
def open_tag(name, attrs = {})
|
305
385
|
"<#{name}#{normalize_html_attributes(attrs)}>"
|
306
386
|
end
|
307
|
-
|
308
|
-
#
|
309
|
-
|
387
|
+
|
388
|
+
# Returns a closing HTML tag string for the given tag name
|
389
|
+
#
|
390
|
+
# @param name [String,Symbol] The tag name to close (e.g. 'div', :span)
|
391
|
+
#
|
392
|
+
# @return [String] The closing tag string with optional newline
|
393
|
+
#
|
394
|
+
# @example Basic closing tag
|
395
|
+
# closing_tag(:div) #=> "</div>\n" # with newlines enabled
|
396
|
+
#
|
397
|
+
# @example Without newlines
|
398
|
+
# closing_tag(:span) #=> "</span>" # with newlines disabled
|
399
|
+
#
|
400
|
+
def closing_tag(name)
|
310
401
|
"</#{name}>#{add_newline?}"
|
311
402
|
end
|
312
|
-
|
313
|
-
# Creates a self
|
314
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
#
|
318
|
-
#
|
319
|
-
|
320
|
-
|
403
|
+
|
404
|
+
# Creates a self-closing HTML tag with optional attributes and newlines
|
405
|
+
#
|
406
|
+
# @param name [String,Symbol] The tag name (e.g. 'br', :img)
|
407
|
+
# @param attrs [Hash] Optional attrs hash including `{ newline: true } toggle
|
408
|
+
#
|
409
|
+
# @return [String] The self-closing tag string (e.g. '<br>' or '<br />' for XHTML)
|
410
|
+
#
|
411
|
+
# @example Basic self-closing tag
|
412
|
+
# self_closing_tag(:br) #=> "<br>" / "<br />" in XHTML
|
413
|
+
#
|
414
|
+
# @example With attributes and newlines
|
415
|
+
# self_closing_tag(:img, src: 'test.jpg', newline: true)
|
416
|
+
# #=> '<img src="test.jpg">\n' / '<img src="test.jpg" />\n'
|
417
|
+
#
|
418
|
+
def self_closing_tag(name, attrs = {})
|
419
|
+
newline = attrs[:newline].nil? ? nil : attrs.delete(:newline)
|
321
420
|
"<#{name}#{normalize_html_attributes(attrs)}#{is_xhtml?}#{add_newline?(newline)}"
|
322
421
|
end
|
323
|
-
|
324
|
-
#
|
325
|
-
#
|
326
|
-
#
|
327
|
-
#
|
328
|
-
#
|
329
|
-
#
|
330
|
-
#
|
331
|
-
#
|
332
|
-
#
|
333
|
-
#
|
334
|
-
#
|
335
|
-
#
|
336
|
-
#
|
337
|
-
#
|
422
|
+
|
423
|
+
# Formats tag contents with appropriate newlines based on tag type and options
|
424
|
+
#
|
425
|
+
# @param name [String,Symbol] The tag name to check format rules against
|
426
|
+
# @param content [String] The content to be wrapped in the tag
|
427
|
+
# @param newline [Boolean,nil] Override flag for newline insertion, nil uses default
|
428
|
+
#
|
429
|
+
# @return [String] The formatted content with appropriate newlines
|
430
|
+
#
|
431
|
+
# @example Multi-line tag
|
432
|
+
# tag_contents_for(:div, 'content') #=> "\ncontent\n"
|
433
|
+
#
|
434
|
+
# @example Single-line tag with newlines
|
435
|
+
# tag_contents_for(:span, 'text', true) #=> "\ntext\n"
|
436
|
+
#
|
437
|
+
# @example Basic content
|
438
|
+
# tag_contents_for(:p, 'text') #=> "text"
|
439
|
+
#
|
338
440
|
def tag_contents_for(name, content, newline = nil)
|
339
441
|
if multi_line_tag?(name)
|
340
|
-
"#{add_newline?(newline)}#{content}#{add_newline?(newline)}".gsub(
|
442
|
+
"#{add_newline?(newline)}#{content}#{add_newline?(newline)}".gsub("\n\n", "\n")
|
341
443
|
elsif single_line_tag?(name) && newline == true
|
342
444
|
"#{add_newline?(newline)}#{content}#{add_newline?(newline)}"
|
343
445
|
else
|
344
446
|
content.to_s
|
345
447
|
end
|
346
448
|
end
|
347
|
-
|
348
|
-
#
|
349
|
-
|
449
|
+
|
450
|
+
# Normalizes HTML attributes handling special cases like data-* attributes
|
451
|
+
# and boolean attributes
|
452
|
+
#
|
453
|
+
# @param attrs [Hash] Hash of HTML attributes to normalize
|
454
|
+
#
|
455
|
+
# @return [String, nil] Normalized attributes string with leading space, nil if attrs empty
|
456
|
+
#
|
457
|
+
# @example Basic attributes
|
458
|
+
# normalize_html_attributes(class: 'btn', id: 'submit') #=> ' class="btn" id="submit"'
|
459
|
+
#
|
460
|
+
# @example Data attributes
|
461
|
+
# normalize_html_attributes(data: { value: 123 }) #=> ' data-value="123"'
|
462
|
+
#
|
463
|
+
# @example Boolean attributes
|
464
|
+
# normalize_html_attributes(checked: true, disabled: false) #=> ' checked="checked"'
|
465
|
+
#
|
466
|
+
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
467
|
+
def normalize_html_attributes(attrs = {})
|
350
468
|
return if attrs.blank?
|
469
|
+
|
351
470
|
attrs.delete(:newline) # remove newline from attributes
|
352
471
|
# look for data attrs
|
353
|
-
if value = attrs.delete(:data)
|
472
|
+
if (value = attrs.delete(:data))
|
354
473
|
# NB!! convert key to symbol for [].sort
|
355
|
-
value.each { |k, v| attrs[:"data-#{k
|
474
|
+
value.each { |k, v| attrs[:"data-#{k}"] = v }
|
356
475
|
end
|
357
476
|
attrs.each do |name, val|
|
358
477
|
if boolean_attribute?(name)
|
359
478
|
val == true ? attrs[name] = name : attrs.delete(name)
|
360
479
|
end
|
361
480
|
end
|
362
|
-
|
481
|
+
attrs.empty? ? '' : " #{attrs.to_html_attributes}"
|
363
482
|
end
|
364
|
-
|
365
|
-
|
366
|
-
|
483
|
+
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
484
|
+
|
485
|
+
# Checks if the given attribute name is a boolean HTML attribute like checked, disabled, etc
|
486
|
+
#
|
487
|
+
# @param name [String,Symbol] The attribute name to check
|
488
|
+
#
|
489
|
+
# @return [Boolean] true if attribute is boolean, false otherwise
|
490
|
+
#
|
491
|
+
# @example Boolean attribute
|
492
|
+
# boolean_attribute?(:checked) #=> true
|
493
|
+
#
|
494
|
+
# @example Regular attribute
|
495
|
+
# boolean_attribute?(:class) #=> false
|
496
|
+
#
|
497
|
+
def boolean_attribute?(name)
|
367
498
|
BOOLEAN_ATTRIBUTES.include?(name.to_s)
|
368
499
|
end
|
369
|
-
|
370
|
-
#
|
371
|
-
|
500
|
+
|
501
|
+
# Checks if the given tag name is a self-closing tag like <br>, <img>, etc
|
502
|
+
#
|
503
|
+
# @param name [String,Symbol] The tag name to check
|
504
|
+
#
|
505
|
+
# @return [Boolean] true if tag is self-closing, false otherwise
|
506
|
+
#
|
507
|
+
# @example Self-closing tag
|
508
|
+
# self_closing_tag?(:br) #=> true
|
509
|
+
#
|
510
|
+
# @example Regular tag
|
511
|
+
# self_closing_tag?(:div) #=> false
|
512
|
+
#
|
513
|
+
def self_closing_tag?(name)
|
372
514
|
SELF_CLOSING_TAGS.include?(name.to_s)
|
373
515
|
end
|
374
|
-
|
375
|
-
#
|
376
|
-
|
516
|
+
|
517
|
+
# Checks if the given tag name is a single-line tag that should be rendered without newlines
|
518
|
+
#
|
519
|
+
# @param name [String,Symbol] The tag name to check
|
520
|
+
#
|
521
|
+
# @return [Boolean] true if tag should be rendered on a single line, false otherwise
|
522
|
+
#
|
523
|
+
# @example Single-line tag
|
524
|
+
# single_line_tag?(:span) #=> true
|
525
|
+
#
|
526
|
+
# @example Multi-line tag
|
527
|
+
# single_line_tag?(:div) #=> false
|
528
|
+
#
|
529
|
+
def single_line_tag?(name)
|
377
530
|
SINGLE_LINE_TAGS.include?(name.to_s)
|
378
531
|
end
|
379
|
-
|
380
|
-
#
|
381
|
-
|
532
|
+
|
533
|
+
# Checks if the given tag name is a multi-line tag that should be rendered with newlines
|
534
|
+
#
|
535
|
+
# @param name [String,Symbol] The tag name to check
|
536
|
+
#
|
537
|
+
# @return [Boolean] true if tag should be rendered with multiple lines, false otherwise
|
538
|
+
#
|
539
|
+
# @example Multi-line tag
|
540
|
+
# multi_line_tag?(:div) #=> true
|
541
|
+
#
|
542
|
+
# @example Single-line tag
|
543
|
+
# multi_line_tag?(:span) #=> false
|
544
|
+
#
|
545
|
+
def multi_line_tag?(name)
|
382
546
|
MULTI_LINE_TAGS.include?(name.to_s)
|
383
547
|
end
|
384
|
-
|
385
|
-
# Returns a
|
386
|
-
|
548
|
+
|
549
|
+
# Returns a string for closing self-closing tags based on HTML/XHTML format setting
|
550
|
+
#
|
551
|
+
# @return [String] Returns ' />' for XHTML format, '>' for HTML format
|
552
|
+
#
|
553
|
+
# @example With XHTML format
|
554
|
+
# # When tag_output_format_is_xhtml is true
|
555
|
+
# xhtml? #=> ' />'
|
556
|
+
#
|
557
|
+
# @example With HTML format
|
558
|
+
# # When tag_output_format_is_xhtml is false
|
559
|
+
# xhtml? #=> '>'
|
560
|
+
#
|
561
|
+
def xhtml?
|
387
562
|
opts[:tags][:tag_output_format_is_xhtml] ? ' />' : '>'
|
388
563
|
end
|
389
|
-
|
390
|
-
|
391
|
-
#
|
392
|
-
|
393
|
-
|
564
|
+
alias is_xhtml? xhtml?
|
565
|
+
|
566
|
+
# Determines whether to add a newline based on override flag or default configuration
|
567
|
+
#
|
568
|
+
# @param add_override [Boolean, nil] Optional flag to override default newline behavior
|
569
|
+
# - When nil: Uses configured :tag_add_newlines_after_tags setting
|
570
|
+
# - When true/false: Uses override value directly
|
571
|
+
#
|
572
|
+
# @return [String] Returns "\n" for true, empty string for false
|
573
|
+
#
|
574
|
+
# @example Using default configuration
|
575
|
+
# add_newline? #=> "\n" # When tag_add_newlines_after_tags is true
|
576
|
+
#
|
577
|
+
# @example With override
|
578
|
+
# add_newline?(false) #=> "" # Forces no newline
|
579
|
+
#
|
580
|
+
def add_newline?(add_override = nil)
|
581
|
+
add = add_override.nil? ? opts[:tags][:tag_add_newlines_after_tags] : add_override
|
394
582
|
add == true ? "\n" : ''
|
395
583
|
end
|
396
|
-
|
397
|
-
#
|
398
|
-
#
|
399
|
-
#
|
400
|
-
#
|
401
|
-
#
|
402
|
-
#
|
584
|
+
|
585
|
+
# Appends text to the ERB output buffer if one exists
|
586
|
+
#
|
587
|
+
# @param txt [String] The text to append to the buffer
|
588
|
+
#
|
589
|
+
# @return [String, nil] The appended text if buffer exists, nil otherwise
|
590
|
+
#
|
591
|
+
# @example With active buffer
|
592
|
+
# buffer_concat("Hello") #=> "Hello" # Added to @_out_buf
|
593
|
+
#
|
594
|
+
# @example With no buffer
|
595
|
+
# buffer_concat("Hello") #=> nil # No buffer to append to
|
596
|
+
#
|
403
597
|
def buffer_concat(txt)
|
404
598
|
@_out_buf << txt if buffer?
|
405
599
|
end
|
406
|
-
|
407
|
-
|
408
|
-
#
|
409
|
-
#
|
410
|
-
#
|
411
|
-
#
|
412
|
-
#
|
413
|
-
#
|
414
|
-
|
415
|
-
|
600
|
+
alias erb_concat buffer_concat
|
601
|
+
|
602
|
+
# Captures the contents of a given block by executing it with a temporary output buffer
|
603
|
+
#
|
604
|
+
# @param args [Array] Arguments to pass through to the yielded block
|
605
|
+
# @yield [*args] The block to capture contents from
|
606
|
+
#
|
607
|
+
# @return [String] The captured contents of the block, or nil if no block given
|
608
|
+
#
|
609
|
+
# @example Basic capture
|
610
|
+
# capture_block { "<div>content</div>" }
|
611
|
+
# #=> "<div>content</div>"
|
612
|
+
#
|
613
|
+
# @example With arguments
|
614
|
+
# capture_block("arg1", "arg2") { |a,b| "#{a} #{b}" }
|
615
|
+
# #=> "arg1 arg2"
|
616
|
+
#
|
617
|
+
def capture_block(*args)
|
618
|
+
with_output_buffer { block_given? && yield(*args) }
|
416
619
|
end
|
417
|
-
|
418
|
-
|
419
|
-
#
|
620
|
+
alias capture_erb capture_block
|
621
|
+
|
622
|
+
# Temporarily swaps the output buffer with a new one during block execution
|
623
|
+
#
|
624
|
+
# @param buf [String] The new buffer to use temporarily, defaults to empty string
|
625
|
+
# @yield The block to execute with the temporary buffer
|
626
|
+
#
|
627
|
+
# @return [String] The contents of the temporary buffer after block execution
|
628
|
+
#
|
629
|
+
# @example Using temporary buffer
|
630
|
+
# with_output_buffer { @_out_buf << "content" } #=> "content"
|
631
|
+
#
|
632
|
+
# @example Restoring original buffer
|
633
|
+
# old_buf = @_out_buf
|
634
|
+
# with_output_buffer { "content" }
|
635
|
+
# @_out_buf == old_buf #=> true
|
636
|
+
#
|
420
637
|
def with_output_buffer(buf = '')
|
421
|
-
|
638
|
+
old_buffer = @_out_buf
|
639
|
+
@_out_buf = buf
|
422
640
|
yield
|
423
641
|
@_out_buf
|
424
642
|
ensure
|
425
643
|
@_out_buf = old_buffer
|
426
644
|
end
|
427
|
-
|
645
|
+
alias erb_with_output_buffer with_output_buffer
|
428
646
|
|
429
|
-
#
|
430
|
-
|
647
|
+
# Checks if an output buffer exists for template rendering
|
648
|
+
#
|
649
|
+
# @return [Boolean] true if @_out_buf is not nil, false otherwise
|
650
|
+
#
|
651
|
+
# @example With active buffer
|
652
|
+
# buffer? #=> true # when @_out_buf exists
|
653
|
+
#
|
654
|
+
# @example With no buffer
|
655
|
+
# buffer? #=> false # when @_out_buf is nil
|
656
|
+
#
|
657
|
+
def buffer?
|
431
658
|
!@_out_buf.nil?
|
432
659
|
end
|
433
|
-
|
434
|
-
|
660
|
+
alias have_buffer? buffer?
|
661
|
+
alias erb_buffer? buffer?
|
435
662
|
|
663
|
+
# Checks if the given block is from an ERB template
|
664
|
+
#
|
665
|
+
# @param block [Proc] The block to check
|
666
|
+
#
|
667
|
+
# @return [Boolean] true if block is from ERB template or buffer exists, false otherwise
|
668
|
+
#
|
669
|
+
# @example With ERB template block
|
670
|
+
# erb_block?(erb_block) #=> true
|
671
|
+
#
|
672
|
+
# @example With regular block
|
673
|
+
# erb_block?(regular_block) #=> false
|
436
674
|
#
|
437
675
|
def erb_block?(block)
|
438
|
-
have_buffer? ||
|
676
|
+
have_buffer? ||
|
677
|
+
(block && eval('defined? __in_erb_template', block.binding, __FILE__, __LINE__ - 1))
|
439
678
|
end
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
679
|
+
alias is_erb_block? erb_block?
|
680
|
+
alias is_erb_template? erb_block?
|
681
|
+
|
682
|
+
# Checks if the given block is from a HAML template
|
683
|
+
#
|
684
|
+
# @param block [Proc] The block to check
|
685
|
+
#
|
686
|
+
# @return [Boolean] true if block is from HAML template, false otherwise
|
687
|
+
#
|
688
|
+
# @example With HAML template block
|
689
|
+
# haml_block?(haml_block) #=> true
|
690
|
+
#
|
691
|
+
# @example With regular block
|
692
|
+
# haml_block?(regular_block) #=> false
|
693
|
+
#
|
694
|
+
def haml_block?(block)
|
695
|
+
block && eval('defined? _hamlout', block.binding, __FILE__, __LINE__ - 1)
|
696
|
+
end
|
697
|
+
alias is_haml_block? haml_block?
|
698
|
+
alias is_haml_template? haml_block?
|
699
|
+
end
|
700
|
+
# rubocop:enable Metrics/ModuleLength
|
701
|
+
# /InstanceMethods
|
702
|
+
end
|
703
|
+
# /RodaTags
|
704
|
+
|
448
705
|
register_plugin(:tags, RodaTags)
|
449
|
-
|
450
|
-
|
451
|
-
|
706
|
+
end
|
707
|
+
# /RodaPlugins
|
452
708
|
end
|