philiprehberger-html_builder 0.6.0 → 0.8.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: 2422583ee49313a76a8eb70b50d6d8de0040b3f8ea9a9e22722013b0ab7537f5
4
- data.tar.gz: b61ca71f0b42e279fdd9fa4375b7d67c7c9c8db7a85cc4c6575d5e9ba455b87e
3
+ metadata.gz: ae55b58a30868dfa5bc7afd1f3300723c88c8f05e97470571a02619e07dd3dfd
4
+ data.tar.gz: 39f947b3d37ac2a914da3fa42f99dd9a3fddd9d43b09498d548b03eae269b694
5
5
  SHA512:
6
- metadata.gz: 16b555765bd466535c05261da722bdacd670ee72bae7fc635e09de789a02eed4ad6f9020e17404fcb140e34060e1c1be2c5bfff05674e8c5d3867e4f9c8917ed
7
- data.tar.gz: 75d90b85a6a865213dda114f6e3880d1f059a96ebbfbe348ad872b702fd644c5eece3e86bb56bb1237eae573b74c721dcbf004d3377a05503fce95319e200b69
6
+ metadata.gz: e4147ae709bc12f9c0d422169e2a8183bc90a92d3ae9b19cda6c62ad7fa927923b436d0a68f1c496eee6de843d9585a26a60786fee3611c3bcf8c765cf0365bd
7
+ data.tar.gz: 37cbec88f3da3293458bdbd8c42270c44d7f6f1b6f1e4b7052154968df53de570f9921dd27f9dde0a4221c56cf1ade5902168c2b98bd7af1f32327134b0b08f2
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-05-20
11
+
12
+ ### Added
13
+ - `Builder#capture { ... }` — render a block in an isolated scope and return the HTML as a string without appending to the current document. Inherits previously defined components, so component reuse works inside captured fragments. Pairs naturally with `Builder#raw` for inserting the captured HTML into the document.
14
+ - Card image reference in the README for registry-side rendering
15
+
16
+ ## [0.7.0] - 2026-04-26
17
+
18
+ ### Added
19
+ - `merge_attrs(*hashes)` helper for merging attribute hashes; concatenates `:class` (space-separated) and `:style` (semicolon-separated) values rather than overwriting
20
+ - `aria(**pairs)` helper for building ARIA attribute hashes from snake_case keyword pairs (converted to `aria-kebab-case` string keys; nil values omitted)
21
+
10
22
  ## [0.6.0] - 2026-04-16
11
23
 
12
24
  ### Added
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-html_builder.svg)](https://rubygems.org/gems/philiprehberger-html_builder)
5
5
  [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-html-builder)](https://github.com/philiprehberger/rb-html-builder/commits/main)
6
6
 
7
+ ![philiprehberger-html_builder](https://raw.githubusercontent.com/philiprehberger/rb-html-builder/main/package-card.webp)
8
+
7
9
  Programmatic HTML builder with tag DSL, auto-escaping, form helpers, components, and output formatting
8
10
 
9
11
  ## Requirements
@@ -163,6 +165,28 @@ end
163
165
  # => '<ul><li><strong>Alice</strong></li><li><strong>Bob</strong></li></ul>'
164
166
  ```
165
167
 
168
+ ### Attribute Helpers
169
+
170
+ Merge multiple attribute hashes — concatenating `:class` (single space) and `:style` (`'; '`) values rather than overwriting — and build ARIA attribute hashes from snake_case keyword pairs:
171
+
172
+ ```ruby
173
+ Philiprehberger::HtmlBuilder.build do
174
+ base = { class: 'btn', style: 'color: red' }
175
+ variant = { class: 'btn-primary', style: 'font-weight: bold' }
176
+ attrs = merge_attrs(base, variant)
177
+
178
+ button('Save', **attrs)
179
+ end
180
+ # => '<button class="btn btn-primary" style="color: red; font-weight: bold">Save</button>'
181
+
182
+ Philiprehberger::HtmlBuilder.build do
183
+ aria(label: 'Save', expanded: false, describedby: nil)
184
+ end
185
+ # => { 'aria-label' => 'Save', 'aria-expanded' => 'false' }
186
+ ```
187
+
188
+ `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`.
189
+
166
190
  ### CSS Class Helpers
167
191
 
168
192
  Build conditional CSS class strings from mixed arguments. Strings are included as-is, hash keys are included when their value is truthy:
@@ -176,6 +200,25 @@ end
176
200
  # => <div class="btn btn-lg active">Click me</div>
177
201
  ```
178
202
 
203
+ ### Capturing Fragments
204
+
205
+ Render a block in a separate scope and capture the HTML as a string instead of appending it to the document. Useful when the same fragment is needed in more than one place or as a value:
206
+
207
+ ```ruby
208
+ Philiprehberger::HtmlBuilder.build do
209
+ badge = capture { strong 'NEW', class: 'badge' }
210
+ article do
211
+ h2 'Headline'
212
+ raw badge
213
+ p 'Body text'
214
+ raw badge
215
+ end
216
+ end
217
+ # => <article><h2>Headline</h2><strong class="badge">NEW</strong><p>Body text</p><strong class="badge">NEW</strong></article>
218
+ ```
219
+
220
+ `capture` inherits any components defined on the parent builder, so component reuse works inside captured fragments.
221
+
179
222
  ### Fragment Caching
180
223
 
181
224
  Cache rendered block results by key. On subsequent calls with the same key, the cached HTML is returned without re-executing the block:
@@ -324,7 +367,10 @@ Philiprehberger::HtmlBuilder.merge(header, body, footer)
324
367
  | `Builder#submit(text, **attrs)` | Generate a submit button (default text "Submit") |
325
368
  | `Builder#list(items, ordered:, **attrs, &block)` | Build a `<ul>` or `<ol>` from an array of items |
326
369
  | `Builder#class_names(*args)` | Build a conditional CSS class string from strings and hashes |
370
+ | `Builder#merge_attrs(*hashes)` | Merge attribute hashes, concatenating `:class` (space) and `:style` (`'; '`) values |
371
+ | `Builder#aria(**pairs)` | Build an ARIA attribute hash from snake_case keys (rendered as `aria-kebab-case`); omits nil values |
327
372
  | `Builder#cache(key) { ... }` | Cache rendered block output by key; return cached HTML on repeat calls |
373
+ | `Builder#capture { ... }` | Render the block in an isolated scope and return its HTML string (no append) |
328
374
  | `Escape.html(value)` | Escape HTML special characters in a string |
329
375
 
330
376
  ## Development
@@ -265,6 +265,71 @@ module Philiprehberger
265
265
  result.join(' ')
266
266
  end
267
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
+
311
+ # Render a block in an isolated builder scope and return the HTML string
312
+ # without appending anything to the current document.
313
+ #
314
+ # Useful for template helpers that need to compute a fragment once and
315
+ # then insert it in multiple places (e.g. via {#raw}) or pass it as a
316
+ # string attribute (e.g. ``title:`` containing a small piece of escaped
317
+ # markup) — none of which can be expressed with the regular tag DSL
318
+ # because every tag method side-effects into the current scope.
319
+ #
320
+ # @yield block evaluated against a fresh builder; the same DSL methods
321
+ # (tags, `text`, `raw`, components, etc.) are available
322
+ # @return [String] the captured HTML (minified)
323
+ # @raise [Error] if no block is given
324
+ def capture(&block)
325
+ raise Error, 'a block is required for capture' unless block
326
+
327
+ nested = Builder.new
328
+ @components.each { |name, b| nested.instance_variable_get(:@components)[name] = b }
329
+ nested.instance_eval(&block)
330
+ nested.to_html
331
+ end
332
+
268
333
  # Cache a rendered block result by key
269
334
  #
270
335
  # On the first call with a given key, the block is executed, its rendered
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module HtmlBuilder
5
- VERSION = '0.6.0'
5
+ VERSION = '0.8.0'
6
6
  end
7
7
  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.6.0
4
+ version: 0.8.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-05-20 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.