philiprehberger-html_builder 0.5.0 → 0.7.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: c951cd5a9e74ab6642a9181b01235a4a5a63b39015fd05cf5c41ab0157bc6106
4
- data.tar.gz: ab129d21a86612912a83f460cd49424b4005ac95fe6f7c8c6471f3f5309a6640
3
+ metadata.gz: 9a1ed5d63b82c40002240f74e0f808a5e62926efd8b55a0c52b482d804a23bab
4
+ data.tar.gz: 814fd3cf20cf2cf358f25e38d14dcf9f02e84e32827a584c0c94a947d4404f48
5
5
  SHA512:
6
- metadata.gz: c6ceba75c99ca6620ba8434521b02703cb42d0113f662fe0789928dec1b1e38f4e3057c4fd44495eefd30bd53d08127d6b95daa470e4d67bf15ed56f61e085a9
7
- data.tar.gz: c70fd51be8e402a223a7a1114a0f0950dd66267f2cf20506a1437a32905ba3185634a980fa314fd9785c985600a947890e05768b35dccdaabdbb40a395cb0191
6
+ metadata.gz: 0e0214af812ff0754349ee79c494f544f34b6b5a16852387d3c51c30b1580c7e88f75d0392f4761dacf754c91722cc83015f95131f065c60eaa073df64453217
7
+ data.tar.gz: eb761b219b3a40319752ba84628ed2a7a88f0ea423581619a641a98932a3c39f8deabb83edb077be8c0319436d60f7ccac2f53f5acaa6e63cb5d469e4d04b271
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.7.0] - 2026-04-26
11
+
12
+ ### Added
13
+ - `merge_attrs(*hashes)` helper for merging attribute hashes; concatenates `:class` (space-separated) and `:style` (semicolon-separated) values rather than overwriting
14
+ - `aria(**pairs)` helper for building ARIA attribute hashes from snake_case keyword pairs (converted to `aria-kebab-case` string keys; nil values omitted)
15
+
16
+ ## [0.6.0] - 2026-04-16
17
+
18
+ ### Added
19
+ - HTML5 `<!DOCTYPE html>` helper via `Builder#doctype` and `HtmlBuilder.document`
20
+
10
21
  ## [0.5.0] - 2026-04-15
11
22
 
12
23
  ### Added
data/README.md CHANGED
@@ -163,6 +163,28 @@ end
163
163
  # => '<ul><li><strong>Alice</strong></li><li><strong>Bob</strong></li></ul>'
164
164
  ```
165
165
 
166
+ ### Attribute Helpers
167
+
168
+ Merge multiple attribute hashes — concatenating `:class` (single space) and `:style` (`'; '`) values rather than overwriting — and build ARIA attribute hashes from snake_case keyword pairs:
169
+
170
+ ```ruby
171
+ Philiprehberger::HtmlBuilder.build do
172
+ base = { class: 'btn', style: 'color: red' }
173
+ variant = { class: 'btn-primary', style: 'font-weight: bold' }
174
+ attrs = merge_attrs(base, variant)
175
+
176
+ button('Save', **attrs)
177
+ end
178
+ # => '<button class="btn btn-primary" style="color: red; font-weight: bold">Save</button>'
179
+
180
+ Philiprehberger::HtmlBuilder.build do
181
+ aria(label: 'Save', expanded: false, describedby: nil)
182
+ end
183
+ # => { 'aria-label' => 'Save', 'aria-expanded' => 'false' }
184
+ ```
185
+
186
+ `merge_attrs` joins `:class` values with a single space and `:style` values with `'; '`. Other keys follow last-write-wins, and input hashes are not mutated. `aria` converts snake_case keys to `aria-kebab-case` string keys, stringifies values, and omits keys whose value is `nil`.
187
+
166
188
  ### CSS Class Helpers
167
189
 
168
190
  Build conditional CSS class strings from mixed arguments. Strings are included as-is, hash keys are included when their value is truthy:
@@ -227,6 +249,31 @@ end
227
249
 
228
250
  Components without parameters use a simple block with no arguments. Components with parameters receive a hash of locals.
229
251
 
252
+ ### HTML5 Documents
253
+
254
+ Emit a standards-compliant `<!DOCTYPE html>` declaration via the `doctype` DSL helper, or use `HtmlBuilder.document` for a full HTML5 document shortcut that prefixes the doctype automatically. The block decides the root element, so no hardcoded `<html>` wrapper is added:
255
+
256
+ ```ruby
257
+ Philiprehberger::HtmlBuilder.build do
258
+ doctype
259
+ html { head { title 'Home' } }
260
+ end
261
+ # => '<!DOCTYPE html><html><head><title>Home</title></head></html>'
262
+
263
+ Philiprehberger::HtmlBuilder.document do
264
+ html do
265
+ head { title 'Home' }
266
+ body { h1 'Welcome' }
267
+ end
268
+ end
269
+ # => "<!DOCTYPE html>\n<html><head><title>Home</title></head><body><h1>Welcome</h1></body></html>"
270
+
271
+ Philiprehberger::HtmlBuilder.document(pretty: true) do
272
+ html { head { title 'Home' } }
273
+ end
274
+ # pretty-printed with the doctype on its own line
275
+ ```
276
+
230
277
  ### Output Modes
231
278
 
232
279
  Choose between minified and pretty-printed output:
@@ -279,12 +326,14 @@ Philiprehberger::HtmlBuilder.merge(header, body, footer)
279
326
  | `HtmlBuilder.build { ... }` | Build minified HTML using the tag DSL, returns a string |
280
327
  | `HtmlBuilder.build_pretty { ... }` | Build pretty-printed HTML with indentation |
281
328
  | `HtmlBuilder.build_minified { ... }` | Alias for `build`, explicitly produces minified output |
329
+ | `HtmlBuilder.document(pretty:, indent_size:) { ... }` | Build an HTML5 document; prefixes `<!DOCTYPE html>` before the block output |
282
330
  | `HtmlBuilder.merge(*fragments)` | Merge multiple HTML fragment strings into one |
283
331
  | `HtmlBuilder.escape(value)` | Escape HTML special characters in a string using the DSL's escaper |
284
332
  | `Builder#to_html` | Render builder contents to a minified HTML string |
285
333
  | `Builder#to_pretty_html` | Render builder contents to a pretty-printed HTML string |
286
334
  | `Builder#text(content)` | Add escaped text content to the current element |
287
335
  | `Builder#raw(html)` | Add raw HTML without escaping |
336
+ | `Builder#doctype` | Emit an HTML5 `<!DOCTYPE html>` declaration |
288
337
  | `Builder#render_if(condition) { ... }` | Conditionally render a block if condition is truthy |
289
338
  | `Builder#render_unless(condition) { ... }` | Conditionally render a block if condition is falsy |
290
339
  | `Builder#define_component(name) { ... }` | Define a reusable named block |
@@ -297,6 +346,8 @@ Philiprehberger::HtmlBuilder.merge(header, body, footer)
297
346
  | `Builder#submit(text, **attrs)` | Generate a submit button (default text "Submit") |
298
347
  | `Builder#list(items, ordered:, **attrs, &block)` | Build a `<ul>` or `<ol>` from an array of items |
299
348
  | `Builder#class_names(*args)` | Build a conditional CSS class string from strings and hashes |
349
+ | `Builder#merge_attrs(*hashes)` | Merge attribute hashes, concatenating `:class` (space) and `:style` (`'; '`) values |
350
+ | `Builder#aria(**pairs)` | Build an ARIA attribute hash from snake_case keys (rendered as `aria-kebab-case`); omits nil values |
300
351
  | `Builder#cache(key) { ... }` | Cache rendered block output by key; return cached HTML on repeat calls |
301
352
  | `Escape.html(value)` | Escape HTML special characters in a string |
302
353
 
@@ -80,6 +80,16 @@ module Philiprehberger
80
80
  current_children << node
81
81
  end
82
82
 
83
+ # Emit an HTML5 doctype declaration (`<!DOCTYPE html>`)
84
+ #
85
+ # Has no children and no attributes. In pretty mode the declaration is
86
+ # rendered on its own line at the current indentation.
87
+ #
88
+ # @return [void]
89
+ def doctype
90
+ current_children << DoctypeNode.new
91
+ end
92
+
83
93
  # Conditionally render a block if the condition is truthy
84
94
  #
85
95
  # @param condition [Object] the condition to evaluate
@@ -255,6 +265,49 @@ module Philiprehberger
255
265
  result.join(' ')
256
266
  end
257
267
 
268
+ # Merge multiple attribute hashes into one
269
+ #
270
+ # `:class` values are concatenated with a single space, and `:style` values
271
+ # are concatenated with a semicolon and space. All other keys follow last-write-wins
272
+ # semantics. Input hashes are not mutated.
273
+ #
274
+ # @param hashes [Array<Hash>] one or more attribute hashes to merge
275
+ # @return [Hash] the merged attribute hash
276
+ def merge_attrs(*hashes)
277
+ result = {}
278
+ hashes.each do |h|
279
+ next if h.nil?
280
+
281
+ h.each do |key, value|
282
+ if key == :class && result.key?(:class)
283
+ result[:class] = "#{result[:class]} #{value}"
284
+ elsif key == :style && result.key?(:style)
285
+ result[:style] = "#{result[:style]}; #{value}"
286
+ else
287
+ result[key] = value
288
+ end
289
+ end
290
+ end
291
+ result
292
+ end
293
+
294
+ # Build an ARIA attribute hash from keyword pairs
295
+ #
296
+ # snake_case keys are converted to `aria-kebab-case` string keys. Values are
297
+ # converted to strings. Keys whose value is `nil` are omitted from the result.
298
+ #
299
+ # @param pairs [Hash] keyword pairs to convert into ARIA attributes
300
+ # @return [Hash<String, String>] hash with `"aria-*"` string keys and string values
301
+ def aria(**pairs)
302
+ result = {}
303
+ pairs.each do |key, value|
304
+ next if value.nil?
305
+
306
+ result["aria-#{key.to_s.tr('_', '-')}"] = value.to_s
307
+ end
308
+ result
309
+ end
310
+
258
311
  # Cache a rendered block result by key
259
312
  #
260
313
  # On the first call with a given key, the block is executed, its rendered
@@ -306,5 +359,19 @@ module Philiprehberger
306
359
  end
307
360
  end
308
361
  end
362
+
363
+ # A node that renders the HTML5 doctype declaration
364
+ class DoctypeNode
365
+ DECLARATION = '<!DOCTYPE html>'
366
+
367
+ # @return [String] the doctype declaration
368
+ def to_html(indent: nil, indent_size: 2)
369
+ if indent
370
+ "#{' ' * (indent * indent_size)}#{DECLARATION}"
371
+ else
372
+ DECLARATION
373
+ end
374
+ end
375
+ end
309
376
  end
310
377
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module HtmlBuilder
5
- VERSION = '0.5.0'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  end
@@ -45,6 +45,27 @@ module Philiprehberger
45
45
  build(&)
46
46
  end
47
47
 
48
+ # Build a full HTML5 document: emits `<!DOCTYPE html>` followed by the
49
+ # rendered block, separated by a newline.
50
+ #
51
+ # The block is evaluated at the root level exactly like `.build` / `.build_pretty`,
52
+ # so the caller decides whether to add an `<html>` wrapper. When `pretty: true`,
53
+ # output is pretty-printed with the given indent size.
54
+ #
55
+ # @param pretty [Boolean] whether to pretty-print the block output (default false)
56
+ # @param indent_size [Integer] number of spaces per indent level when pretty (default 2)
57
+ # @yield [Builder] the builder instance for DSL evaluation
58
+ # @return [String] the rendered HTML document string
59
+ # @raise [Error] if no block is given
60
+ def self.document(pretty: false, indent_size: 2, &block)
61
+ raise Error, 'a block is required' unless block
62
+
63
+ builder = Builder.new
64
+ builder.instance_eval(&block)
65
+ body = pretty ? builder.to_pretty_html(indent_size: indent_size) : builder.to_html
66
+ "#{DoctypeNode::DECLARATION}\n#{body}"
67
+ end
68
+
48
69
  # Merge multiple HTML fragment strings into one
49
70
  #
50
71
  # @param fragments [Array<String>] HTML fragments to merge
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.5.0
4
+ version: 0.7.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-04-16 00:00:00.000000000 Z
11
+ date: 2026-04-27 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.