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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dbf0a37df1293a248006f06e88aa73e80fb4ee09bd526e69927cc872d64c3dc
4
- data.tar.gz: cbf9e1056bacaf1013e4c1f42fe256d9abb75ac127b06296c12eb6d9cc1eb12a
3
+ metadata.gz: 595807de03e97765d4e856e8c1948bf1bf5a2d47ab75fd847589ae3fb2751e95
4
+ data.tar.gz: 6783b0d074ef48345176cf2dc69ea05a502eba3ab217399f0485d506e43789e3
5
5
  SHA512:
6
- metadata.gz: 8465923f3f60987cd19ee14c21393c1d6043b11a53fb559d406bb654969a09b97df3afbd4bd4f4b68d649366bc9d021a0f730dc6f4ba115c9a5de25116cfa30a
7
- data.tar.gz: afd25e6739d13c758c15d8373a66695e1fe83c74bbf4bb144dbec8fba7840f89f0b60d12baf5adc31324e8c2f764e68422aa3fe6a7e1c2674d9c7ec13348037a
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
- [![Tests](https://github.com/philiprehberger/rb-html-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-html-builder/actions/workflows/ci.yml)
4
- [![Gem Version](https://badge.fury.io/rb/philiprehberger-html_builder.svg)](https://rubygems.org/gems/philiprehberger-html_builder)
5
- [![License](https://img.shields.io/github/license/philiprehberger/rb-html-builder)](LICENSE)
3
+ [![Tests](https://github.com/philiprehberger/rb-html-builder/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-html-builder/actions/workflows/ci.yml) [![Gem Version](https://img.shields.io/gem/v/philiprehberger-html_builder)](https://rubygems.org/gems/philiprehberger-html_builder) [![GitHub release](https://img.shields.io/github/v/release/philiprehberger/rb-html-builder)](https://github.com/philiprehberger/rb-html-builder/releases) [![GitHub last commit](https://img.shields.io/github/last-commit/philiprehberger/rb-html-builder)](https://github.com/philiprehberger/rb-html-builder/commits/main) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Bug Reports](https://img.shields.io/badge/bug-reports-red.svg)](https://github.com/philiprehberger/rb-html-builder/issues) [![Feature Requests](https://img.shields.io/badge/feature-requests-blue.svg)](https://github.com/philiprehberger/rb-html-builder/issues) [![GitHub Sponsors](https://img.shields.io/badge/sponsor-philiprehberger-ea4aaa.svg?logo=github)](https://github.com/sponsors/philiprehberger)
6
4
 
7
- Programmatic HTML builder with tag DSL and auto-escaping
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
- | `Builder#to_html` | Render the builder contents to an HTML string |
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
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-Philip%20Rehberger-blue?logo=linkedin)](https://linkedin.com/in/philiprehberger) [![More Packages](https://img.shields.io/badge/more-packages-blue.svg)](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
- @html
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
- if void_element?
37
- "<#{tag}#{render_attributes}>"
38
- elsif children.empty?
39
- "<#{tag}#{render_attributes}></#{tag}>"
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
- inner = children.map { |c| c.respond_to?(:to_html) ? c.to_html : Escape.html(c.to_s) }.join
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
- # @return [String] rendered attribute string
56
- def render_attributes
57
- return '' if attributes.empty?
58
-
59
- attrs = attributes.map do |key, value|
60
- if value == true
61
- " #{key}"
62
- elsif value == false || value.nil?
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
- " #{key}=\"#{Escape.html(value)}\""
68
+ result[key] = value
66
69
  end
67
- end.join
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module HtmlBuilder
5
- VERSION = '0.1.3'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  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.1.3
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-25 00:00:00.000000000 Z
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.