roda-tags 0.1.1 → 0.2.0
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.
- 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
|