philiprehberger-html_builder 0.1.3 → 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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +122 -6
- data/lib/philiprehberger/html_builder/builder.rb +140 -3
- data/lib/philiprehberger/html_builder/node.rb +86 -19
- data/lib/philiprehberger/html_builder/version.rb +1 -1
- data/lib/philiprehberger/html_builder.rb +32 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 595807de03e97765d4e856e8c1948bf1bf5a2d47ab75fd847589ae3fb2751e95
|
|
4
|
+
data.tar.gz: 6783b0d074ef48345176cf2dc69ea05a502eba3ab217399f0485d506e43789e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e36fc11adf732b544ad9f42d43a91fbc2b230ed5bc5803ab2c16f8515ae7ccfe691dfc40a301e604a1cb10cf0aa8e6e2c82513a4e926b42e2ab2c35d8a5239ca
|
|
7
|
+
data.tar.gz: b0ac790fd555a82e537f1b023a8f74f3de1d330ccfe3113fb4e11996d252f2d00221ac7c1109cf171080f693a283362d36b7abf6330aa8decf275c74a73588b4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-03-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Form builder helpers (`form_for`, `field`, `select_field`, `textarea_field`) for streamlined form construction
|
|
14
|
+
- HTML5 data/aria attribute support via hash syntax (`data: { id: 1 }` renders as `data-id="1"`)
|
|
15
|
+
- Conditional rendering with `render_if` and `render_unless` blocks
|
|
16
|
+
- Component/partial system with `define_component` and `use_component` for reusable named blocks
|
|
17
|
+
- Pretty-printed output mode via `build_pretty` with configurable indentation
|
|
18
|
+
- Minified output mode via `build_minified` (alias for `build`)
|
|
19
|
+
- HTML fragment merging via `HtmlBuilder.merge` to combine multiple builder outputs
|
|
20
|
+
|
|
10
21
|
## [0.1.3] - 2026-03-24
|
|
11
22
|
|
|
12
23
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# philiprehberger-html_builder
|
|
2
2
|
|
|
3
|
-
[](https://github.com/philiprehberger/rb-html-builder/actions/workflows/ci.yml)
|
|
4
|
-
[](https://rubygems.org/gems/philiprehberger-html_builder)
|
|
5
|
-
[](LICENSE)
|
|
3
|
+
[](https://github.com/philiprehberger/rb-html-builder/actions/workflows/ci.yml) [](https://rubygems.org/gems/philiprehberger-html_builder) [](https://github.com/philiprehberger/rb-html-builder/releases) [](https://github.com/philiprehberger/rb-html-builder/commits/main) [](LICENSE) [](https://github.com/philiprehberger/rb-html-builder/issues) [](https://github.com/philiprehberger/rb-html-builder/issues) [](https://github.com/sponsors/philiprehberger)
|
|
6
4
|
|
|
7
|
-
Programmatic HTML builder with tag DSL
|
|
5
|
+
Programmatic HTML builder with tag DSL, auto-escaping, form helpers, components, and output formatting.
|
|
8
6
|
|
|
9
7
|
## Requirements
|
|
10
8
|
|
|
@@ -71,6 +69,19 @@ Philiprehberger::HtmlBuilder.build do
|
|
|
71
69
|
end
|
|
72
70
|
```
|
|
73
71
|
|
|
72
|
+
### Data and Aria Attributes
|
|
73
|
+
|
|
74
|
+
Use hash syntax for HTML5 `data-*` and `aria-*` attributes:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Philiprehberger::HtmlBuilder.build do
|
|
78
|
+
div(data: { id: 1, action: 'click' }, aria: { label: 'Panel' }) do
|
|
79
|
+
button('Toggle', aria: { expanded: 'false' })
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
# => '<div data-id="1" data-action="click" aria-label="Panel"><button aria-expanded="false">Toggle</button></div>'
|
|
83
|
+
```
|
|
84
|
+
|
|
74
85
|
### Raw HTML
|
|
75
86
|
|
|
76
87
|
Insert pre-rendered HTML without escaping:
|
|
@@ -81,14 +92,115 @@ Philiprehberger::HtmlBuilder.build do
|
|
|
81
92
|
end
|
|
82
93
|
```
|
|
83
94
|
|
|
95
|
+
### Form Builder Helpers
|
|
96
|
+
|
|
97
|
+
Streamlined helpers for building forms with automatic label generation:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
Philiprehberger::HtmlBuilder.build do
|
|
101
|
+
form_for('/signup', class: 'form') do
|
|
102
|
+
field(:email, type: 'email')
|
|
103
|
+
field(:first_name)
|
|
104
|
+
select_field(:country, [%w[USA us], %w[Canada ca]], selected: 'us')
|
|
105
|
+
textarea_field(:bio, rows: '5')
|
|
106
|
+
button 'Submit', type: 'submit'
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `field` helper generates a `<label>` and `<input>` pair. The `select_field` helper generates a `<label>` and `<select>` with `<option>` tags. The `textarea_field` helper generates a `<label>` and `<textarea>`. Label text is auto-generated from the field name (underscores become spaces, words are capitalized).
|
|
112
|
+
|
|
113
|
+
### Conditional Rendering
|
|
114
|
+
|
|
115
|
+
Render blocks based on conditions:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
logged_in = true
|
|
119
|
+
admin = false
|
|
120
|
+
|
|
121
|
+
Philiprehberger::HtmlBuilder.build do
|
|
122
|
+
render_if(logged_in) { p 'Welcome back!' }
|
|
123
|
+
render_unless(admin) { p 'Standard user' }
|
|
124
|
+
end
|
|
125
|
+
# => '<p>Welcome back!</p><p>Standard user</p>'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Components
|
|
129
|
+
|
|
130
|
+
Define reusable named blocks and render them anywhere:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
Philiprehberger::HtmlBuilder.build do
|
|
134
|
+
define_component(:card) do |locals|
|
|
135
|
+
div(class: 'card') do
|
|
136
|
+
h2 locals[:title]
|
|
137
|
+
p locals[:body]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
use_component(:card, title: 'First', body: 'Content 1')
|
|
142
|
+
use_component(:card, title: 'Second', body: 'Content 2')
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Components without parameters use a simple block with no arguments. Components with parameters receive a hash of locals.
|
|
147
|
+
|
|
148
|
+
### Output Modes
|
|
149
|
+
|
|
150
|
+
Choose between minified and pretty-printed output:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# Minified (default)
|
|
154
|
+
Philiprehberger::HtmlBuilder.build do
|
|
155
|
+
div { p 'Hello' }
|
|
156
|
+
end
|
|
157
|
+
# => '<div><p>Hello</p></div>'
|
|
158
|
+
|
|
159
|
+
# Pretty-printed
|
|
160
|
+
Philiprehberger::HtmlBuilder.build_pretty do
|
|
161
|
+
div { p 'Hello' }
|
|
162
|
+
end
|
|
163
|
+
# => "<div>\n <p>Hello</p>\n</div>"
|
|
164
|
+
|
|
165
|
+
# Pretty-printed with custom indent
|
|
166
|
+
Philiprehberger::HtmlBuilder.build_pretty(indent_size: 4) do
|
|
167
|
+
div { p 'Hello' }
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Fragment Merging
|
|
172
|
+
|
|
173
|
+
Combine multiple builder outputs into a single HTML string:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
header = Philiprehberger::HtmlBuilder.build { header { h1 'Title' } }
|
|
177
|
+
body = Philiprehberger::HtmlBuilder.build { main { p 'Content' } }
|
|
178
|
+
footer = Philiprehberger::HtmlBuilder.build { footer { p 'Copyright' } }
|
|
179
|
+
|
|
180
|
+
Philiprehberger::HtmlBuilder.merge(header, body, footer)
|
|
181
|
+
# => '<header><h1>Title</h1></header><main><p>Content</p></main><footer><p>Copyright</p></footer>'
|
|
182
|
+
```
|
|
183
|
+
|
|
84
184
|
## API
|
|
85
185
|
|
|
86
186
|
| Method | Description |
|
|
87
187
|
|--------|-------------|
|
|
88
|
-
| `HtmlBuilder.build { ... }` | Build HTML using the tag DSL, returns a string |
|
|
89
|
-
| `
|
|
188
|
+
| `HtmlBuilder.build { ... }` | Build minified HTML using the tag DSL, returns a string |
|
|
189
|
+
| `HtmlBuilder.build_pretty { ... }` | Build pretty-printed HTML with indentation |
|
|
190
|
+
| `HtmlBuilder.build_minified { ... }` | Alias for `build`, explicitly produces minified output |
|
|
191
|
+
| `HtmlBuilder.merge(*fragments)` | Merge multiple HTML fragment strings into one |
|
|
192
|
+
| `Builder#to_html` | Render builder contents to a minified HTML string |
|
|
193
|
+
| `Builder#to_pretty_html` | Render builder contents to a pretty-printed HTML string |
|
|
90
194
|
| `Builder#text(content)` | Add escaped text content to the current element |
|
|
91
195
|
| `Builder#raw(html)` | Add raw HTML without escaping |
|
|
196
|
+
| `Builder#render_if(condition) { ... }` | Conditionally render a block if condition is truthy |
|
|
197
|
+
| `Builder#render_unless(condition) { ... }` | Conditionally render a block if condition is falsy |
|
|
198
|
+
| `Builder#define_component(name) { ... }` | Define a reusable named block |
|
|
199
|
+
| `Builder#use_component(name, **locals)` | Render a previously defined component |
|
|
200
|
+
| `Builder#form_for(action, method_type:, **attrs) { ... }` | Build a form tag with common defaults |
|
|
201
|
+
| `Builder#field(name, label_text:, type:, **attrs)` | Build a label + input pair |
|
|
202
|
+
| `Builder#select_field(name, options, label_text:, selected:, **attrs)` | Build a label + select with options |
|
|
203
|
+
| `Builder#textarea_field(name, content, label_text:, **attrs)` | Build a label + textarea |
|
|
92
204
|
| `Escape.html(value)` | Escape HTML special characters in a string |
|
|
93
205
|
|
|
94
206
|
## Development
|
|
@@ -99,6 +211,10 @@ bundle exec rspec
|
|
|
99
211
|
bundle exec rubocop
|
|
100
212
|
```
|
|
101
213
|
|
|
214
|
+
## Support
|
|
215
|
+
|
|
216
|
+
[](https://linkedin.com/in/philiprehberger) [](https://github.com/philiprehberger?tab=repositories)
|
|
217
|
+
|
|
102
218
|
## License
|
|
103
219
|
|
|
104
220
|
MIT
|
|
@@ -22,15 +22,30 @@ module Philiprehberger
|
|
|
22
22
|
def initialize
|
|
23
23
|
@root_children = []
|
|
24
24
|
@stack = []
|
|
25
|
+
@components = {}
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
# Render all root-level nodes to HTML
|
|
28
|
+
# Render all root-level nodes to HTML (minified)
|
|
28
29
|
#
|
|
29
30
|
# @return [String] the rendered HTML
|
|
30
31
|
def to_html
|
|
31
32
|
@root_children.map { |c| c.respond_to?(:to_html) ? c.to_html : Escape.html(c.to_s) }.join
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
# Render all root-level nodes to pretty-printed HTML with indentation
|
|
36
|
+
#
|
|
37
|
+
# @param indent_size [Integer] number of spaces per indent level (default 2)
|
|
38
|
+
# @return [String] the pretty-printed HTML
|
|
39
|
+
def to_pretty_html(indent_size: 2)
|
|
40
|
+
@root_children.map do |c|
|
|
41
|
+
if c.respond_to?(:to_html)
|
|
42
|
+
c.to_html(indent: 0, indent_size: indent_size)
|
|
43
|
+
else
|
|
44
|
+
Escape.html(c.to_s)
|
|
45
|
+
end
|
|
46
|
+
end.join("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
34
49
|
ALL_TAGS.each do |tag_name|
|
|
35
50
|
define_method(tag_name) do |content = nil, **attrs, &block|
|
|
36
51
|
node = Node.new(tag_name, attributes: attrs)
|
|
@@ -64,6 +79,124 @@ module Philiprehberger
|
|
|
64
79
|
current_children << node
|
|
65
80
|
end
|
|
66
81
|
|
|
82
|
+
# Conditionally render a block if the condition is truthy
|
|
83
|
+
#
|
|
84
|
+
# @param condition [Object] the condition to evaluate
|
|
85
|
+
# @yield the block to render if condition is truthy
|
|
86
|
+
# @return [void]
|
|
87
|
+
def render_if(condition, &block)
|
|
88
|
+
return unless condition
|
|
89
|
+
raise Error, 'a block is required for render_if' unless block
|
|
90
|
+
|
|
91
|
+
instance_eval(&block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Conditionally render a block if the condition is falsy
|
|
95
|
+
#
|
|
96
|
+
# @param condition [Object] the condition to evaluate
|
|
97
|
+
# @yield the block to render if condition is falsy
|
|
98
|
+
# @return [void]
|
|
99
|
+
def render_unless(condition, &block)
|
|
100
|
+
return if condition
|
|
101
|
+
raise Error, 'a block is required for render_unless' unless block
|
|
102
|
+
|
|
103
|
+
instance_eval(&block)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Define a reusable named component
|
|
107
|
+
#
|
|
108
|
+
# @param name [Symbol, String] the component name
|
|
109
|
+
# @yield the block that defines the component's HTML
|
|
110
|
+
# @return [void]
|
|
111
|
+
def define_component(name, &block)
|
|
112
|
+
raise Error, 'a block is required for define_component' unless block
|
|
113
|
+
|
|
114
|
+
@components[name.to_sym] = block
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Render a previously defined component
|
|
118
|
+
#
|
|
119
|
+
# @param name [Symbol, String] the component name
|
|
120
|
+
# @param locals [Hash] local variables passed to the component block
|
|
121
|
+
# @return [void]
|
|
122
|
+
def use_component(name, **locals)
|
|
123
|
+
block = @components[name.to_sym]
|
|
124
|
+
raise Error, "undefined component: #{name}" unless block
|
|
125
|
+
|
|
126
|
+
if block.arity.zero? || (block.arity.negative? && locals.empty?)
|
|
127
|
+
instance_eval(&block)
|
|
128
|
+
else
|
|
129
|
+
instance_exec(locals, &block)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Form builder helper: builds a form tag with common defaults
|
|
134
|
+
#
|
|
135
|
+
# @param action [String] the form action URL
|
|
136
|
+
# @param method_type [String] the HTTP method (default "post")
|
|
137
|
+
# @param attrs [Hash] additional attributes
|
|
138
|
+
# @yield the form contents
|
|
139
|
+
# @return [Node]
|
|
140
|
+
def form_for(action, method_type: 'post', **attrs, &block)
|
|
141
|
+
form(action: action, method: method_type, **attrs, &block)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Form builder helper: builds a label + input pair
|
|
145
|
+
#
|
|
146
|
+
# @param name [String, Symbol] the field name
|
|
147
|
+
# @param label_text [String] the label text
|
|
148
|
+
# @param type [String] the input type (default "text")
|
|
149
|
+
# @param attrs [Hash] additional input attributes
|
|
150
|
+
# @return [void]
|
|
151
|
+
def field(name, label_text: nil, type: 'text', **attrs)
|
|
152
|
+
field_id = attrs.delete(:id) || name.to_s.tr('_', '-')
|
|
153
|
+
label_str = label_text || name.to_s.gsub('_', ' ').split.map(&:capitalize).join(' ')
|
|
154
|
+
label label_str, for: field_id
|
|
155
|
+
input(type: type, name: name.to_s, id: field_id, **attrs)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Form builder helper: builds a label + select with options
|
|
159
|
+
#
|
|
160
|
+
# @param name [String, Symbol] the field name
|
|
161
|
+
# @param options_list [Array<Array, String>] list of [text, value] pairs or plain strings
|
|
162
|
+
# @param label_text [String] the label text
|
|
163
|
+
# @param selected [String, nil] the selected value
|
|
164
|
+
# @param attrs [Hash] additional select attributes
|
|
165
|
+
# @return [void]
|
|
166
|
+
def select_field(name, options_list, label_text: nil, selected: nil, **attrs)
|
|
167
|
+
field_id = attrs.delete(:id) || name.to_s.tr('_', '-')
|
|
168
|
+
label_str = label_text || name.to_s.gsub('_', ' ').split.map(&:capitalize).join(' ')
|
|
169
|
+
label label_str, for: field_id
|
|
170
|
+
select(name: name.to_s, id: field_id, **attrs) do
|
|
171
|
+
options_list.each do |opt|
|
|
172
|
+
if opt.is_a?(Array)
|
|
173
|
+
opt_text, opt_value = opt
|
|
174
|
+
option_attrs = { value: opt_value.to_s }
|
|
175
|
+
option_attrs[:selected] = true if opt_value.to_s == selected.to_s
|
|
176
|
+
option opt_text, **option_attrs
|
|
177
|
+
else
|
|
178
|
+
option_attrs = { value: opt.to_s }
|
|
179
|
+
option_attrs[:selected] = true if opt.to_s == selected.to_s
|
|
180
|
+
option opt.to_s, **option_attrs
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Form builder helper: builds a label + textarea
|
|
187
|
+
#
|
|
188
|
+
# @param name [String, Symbol] the field name
|
|
189
|
+
# @param content [String, nil] the textarea content
|
|
190
|
+
# @param label_text [String] the label text
|
|
191
|
+
# @param attrs [Hash] additional textarea attributes
|
|
192
|
+
# @return [void]
|
|
193
|
+
def textarea_field(name, content = nil, label_text: nil, **attrs)
|
|
194
|
+
field_id = attrs.delete(:id) || name.to_s.tr('_', '-')
|
|
195
|
+
label_str = label_text || name.to_s.gsub('_', ' ').split.map(&:capitalize).join(' ')
|
|
196
|
+
label label_str, for: field_id
|
|
197
|
+
textarea(content, name: name.to_s, id: field_id, **attrs)
|
|
198
|
+
end
|
|
199
|
+
|
|
67
200
|
private
|
|
68
201
|
|
|
69
202
|
# @return [Array] the children array for the current context
|
|
@@ -84,8 +217,12 @@ module Philiprehberger
|
|
|
84
217
|
end
|
|
85
218
|
|
|
86
219
|
# @return [String] the raw HTML
|
|
87
|
-
def to_html
|
|
88
|
-
|
|
220
|
+
def to_html(indent: nil, indent_size: 2)
|
|
221
|
+
if indent
|
|
222
|
+
"#{' ' * (indent * indent_size)}#{@html}"
|
|
223
|
+
else
|
|
224
|
+
@html
|
|
225
|
+
end
|
|
89
226
|
end
|
|
90
227
|
end
|
|
91
228
|
end
|
|
@@ -31,15 +31,16 @@ module Philiprehberger
|
|
|
31
31
|
|
|
32
32
|
# Render the node to an HTML string
|
|
33
33
|
#
|
|
34
|
+
# @param indent [Integer, nil] current indentation level (nil for minified)
|
|
35
|
+
# @param indent_size [Integer] number of spaces per indent level
|
|
34
36
|
# @return [String] the rendered HTML
|
|
35
|
-
def to_html
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
def to_html(indent: nil, indent_size: 2)
|
|
38
|
+
flat_attrs = flatten_attributes(attributes)
|
|
39
|
+
|
|
40
|
+
if indent
|
|
41
|
+
render_pretty(flat_attrs, indent: indent, indent_size: indent_size)
|
|
40
42
|
else
|
|
41
|
-
|
|
42
|
-
"<#{tag}#{render_attributes}>#{inner}</#{tag}>"
|
|
43
|
+
render_inline(flat_attrs)
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -52,21 +53,87 @@ module Philiprehberger
|
|
|
52
53
|
VOID_ELEMENTS.include?(tag)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
# Flatten nested hash attributes (data, aria) into hyphenated keys
|
|
57
|
+
#
|
|
58
|
+
# @param attrs [Hash] the attributes hash
|
|
59
|
+
# @return [Hash] flattened attributes
|
|
60
|
+
def flatten_attributes(attrs)
|
|
61
|
+
result = {}
|
|
62
|
+
attrs.each do |key, value|
|
|
63
|
+
if value.is_a?(Hash)
|
|
64
|
+
value.each do |sub_key, sub_value|
|
|
65
|
+
result[:"#{key}-#{sub_key}"] = sub_value
|
|
66
|
+
end
|
|
64
67
|
else
|
|
65
|
-
|
|
68
|
+
result[key] = value
|
|
66
69
|
end
|
|
67
|
-
end
|
|
70
|
+
end
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Render a single attribute pair
|
|
75
|
+
#
|
|
76
|
+
# @param key [Symbol] attribute name
|
|
77
|
+
# @param value [Object] attribute value
|
|
78
|
+
# @return [String] rendered attribute string
|
|
79
|
+
def render_attribute(key, value)
|
|
80
|
+
if value == true
|
|
81
|
+
" #{key}"
|
|
82
|
+
elsif value == false || value.nil?
|
|
83
|
+
''
|
|
84
|
+
else
|
|
85
|
+
" #{key}=\"#{Escape.html(value)}\""
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [String] rendered attribute string
|
|
90
|
+
def render_attributes(attrs)
|
|
91
|
+
return '' if attrs.empty?
|
|
68
92
|
|
|
69
|
-
attrs
|
|
93
|
+
attrs.map { |key, value| render_attribute(key, value) }.join
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Render inline (minified) HTML
|
|
97
|
+
def render_inline(flat_attrs)
|
|
98
|
+
attr_str = render_attributes(flat_attrs)
|
|
99
|
+
if void_element?
|
|
100
|
+
"<#{tag}#{attr_str}>"
|
|
101
|
+
elsif children.empty?
|
|
102
|
+
"<#{tag}#{attr_str}></#{tag}>"
|
|
103
|
+
else
|
|
104
|
+
inner = children.map { |c| c.respond_to?(:to_html) ? c.to_html : Escape.html(c.to_s) }.join
|
|
105
|
+
"<#{tag}#{attr_str}>#{inner}</#{tag}>"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Render pretty-printed HTML with indentation
|
|
110
|
+
def render_pretty(flat_attrs, indent:, indent_size:)
|
|
111
|
+
attr_str = render_attributes(flat_attrs)
|
|
112
|
+
pad = ' ' * (indent * indent_size)
|
|
113
|
+
|
|
114
|
+
if void_element?
|
|
115
|
+
"#{pad}<#{tag}#{attr_str}>"
|
|
116
|
+
elsif children.empty?
|
|
117
|
+
"#{pad}<#{tag}#{attr_str}></#{tag}>"
|
|
118
|
+
else
|
|
119
|
+
# Check if all children are text-only (no Node children)
|
|
120
|
+
all_text = children.none? { |c| c.respond_to?(:to_html) }
|
|
121
|
+
if all_text
|
|
122
|
+
inner = children.map { |c| Escape.html(c.to_s) }.join
|
|
123
|
+
"#{pad}<#{tag}#{attr_str}>#{inner}</#{tag}>"
|
|
124
|
+
else
|
|
125
|
+
lines = ["#{pad}<#{tag}#{attr_str}>"]
|
|
126
|
+
children.each do |c|
|
|
127
|
+
lines << if c.respond_to?(:to_html)
|
|
128
|
+
c.to_html(indent: indent + 1, indent_size: indent_size)
|
|
129
|
+
else
|
|
130
|
+
"#{' ' * ((indent + 1) * indent_size)}#{Escape.html(c.to_s)}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
lines << "#{pad}</#{tag}>"
|
|
134
|
+
lines.join("\n")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
70
137
|
end
|
|
71
138
|
end
|
|
72
139
|
end
|
|
@@ -9,7 +9,7 @@ module Philiprehberger
|
|
|
9
9
|
module HtmlBuilder
|
|
10
10
|
class Error < StandardError; end
|
|
11
11
|
|
|
12
|
-
# Build HTML using a tag DSL
|
|
12
|
+
# Build HTML using a tag DSL (minified output)
|
|
13
13
|
#
|
|
14
14
|
# @yield [Builder] the builder instance for DSL evaluation
|
|
15
15
|
# @return [String] the rendered HTML string
|
|
@@ -21,5 +21,36 @@ module Philiprehberger
|
|
|
21
21
|
builder.instance_eval(&block)
|
|
22
22
|
builder.to_html
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# Build pretty-printed HTML using a tag DSL
|
|
26
|
+
#
|
|
27
|
+
# @param indent_size [Integer] number of spaces per indent level (default 2)
|
|
28
|
+
# @yield [Builder] the builder instance for DSL evaluation
|
|
29
|
+
# @return [String] the pretty-printed HTML string
|
|
30
|
+
# @raise [Error] if no block is given
|
|
31
|
+
def self.build_pretty(indent_size: 2, &block)
|
|
32
|
+
raise Error, 'a block is required' unless block
|
|
33
|
+
|
|
34
|
+
builder = Builder.new
|
|
35
|
+
builder.instance_eval(&block)
|
|
36
|
+
builder.to_pretty_html(indent_size: indent_size)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Build minified HTML (alias for build)
|
|
40
|
+
#
|
|
41
|
+
# @yield [Builder] the builder instance for DSL evaluation
|
|
42
|
+
# @return [String] the rendered HTML string
|
|
43
|
+
# @raise [Error] if no block is given
|
|
44
|
+
def self.build_minified(&)
|
|
45
|
+
build(&)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Merge multiple HTML fragment strings into one
|
|
49
|
+
#
|
|
50
|
+
# @param fragments [Array<String>] HTML fragments to merge
|
|
51
|
+
# @return [String] the merged HTML string
|
|
52
|
+
def self.merge(*fragments)
|
|
53
|
+
fragments.join
|
|
54
|
+
end
|
|
24
55
|
end
|
|
25
56
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-html_builder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Build HTML programmatically using a clean tag DSL with nested blocks,
|
|
14
14
|
automatic content escaping, void element support, and attribute hashes.
|