philiprehberger-html_builder 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3c6da441bb1020a1c99449e57883c95c26bac54e7c3d40cd087d8eb39878d16b
4
+ data.tar.gz: b446dc852ecb4f57d9da26e759eef7b529fadfcc828eb0adede1521aedae9680
5
+ SHA512:
6
+ metadata.gz: d0e59c8ddaf4c5dd5b9db5ad6f5b1f1c69710c71075667e1dc7d388d37c11dd8b391d2425bc2a0be7dbe49a98f78e89e6b3961f56cbc1a57b96917a411a7d6a8
7
+ data.tar.gz: 5fac133068612fe23b7a9be0aea9ae411d91f884263497153fe1cff5745a31218f8eed3e20f10af996679e136a0c4b353d2b37be0d70f5b6f758041b4ab13642
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-03-22
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Tag DSL with nested block syntax for building HTML
15
+ - Auto-escaping of text content and attribute values
16
+ - Void element support (br, hr, img, input, meta, link, and others)
17
+ - Attributes hash support on all elements
18
+ - Raw HTML insertion for pre-rendered content
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philiprehberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # philiprehberger-html_builder
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)
6
+
7
+ Programmatic HTML builder with tag DSL and auto-escaping
8
+
9
+ ## Requirements
10
+
11
+ - Ruby >= 3.1
12
+
13
+ ## Installation
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "philiprehberger-html_builder"
19
+ ```
20
+
21
+ Or install directly:
22
+
23
+ ```bash
24
+ gem install philiprehberger-html_builder
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ require "philiprehberger/html_builder"
31
+
32
+ html = Philiprehberger::HtmlBuilder.build do
33
+ div(class: 'card') do
34
+ h1 'Title'
35
+ p 'Content'
36
+ end
37
+ end
38
+ # => '<div class="card"><h1>Title</h1><p>Content</p></div>'
39
+ ```
40
+
41
+ ### Auto-Escaping
42
+
43
+ Text content and attribute values are automatically escaped:
44
+
45
+ ```ruby
46
+ Philiprehberger::HtmlBuilder.build { p '<script>alert("xss")</script>' }
47
+ # => '<p>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p>'
48
+ ```
49
+
50
+ ### Void Elements
51
+
52
+ Self-closing elements like `br`, `hr`, `img`, `input`, `meta`, and `link` render without closing tags:
53
+
54
+ ```ruby
55
+ Philiprehberger::HtmlBuilder.build do
56
+ img(src: 'photo.jpg', alt: 'Photo')
57
+ br
58
+ input(type: 'text', name: 'email')
59
+ end
60
+ # => '<img src="photo.jpg" alt="Photo"><br><input type="text" name="email">'
61
+ ```
62
+
63
+ ### Attributes
64
+
65
+ Pass attributes as keyword arguments to any tag:
66
+
67
+ ```ruby
68
+ Philiprehberger::HtmlBuilder.build do
69
+ a(href: '/about', class: 'nav-link') { text 'About' }
70
+ input(type: 'checkbox', checked: true, disabled: false)
71
+ end
72
+ ```
73
+
74
+ ### Raw HTML
75
+
76
+ Insert pre-rendered HTML without escaping:
77
+
78
+ ```ruby
79
+ Philiprehberger::HtmlBuilder.build do
80
+ div { raw '<em>pre-rendered</em>' }
81
+ end
82
+ ```
83
+
84
+ ## API
85
+
86
+ | Method | Description |
87
+ |--------|-------------|
88
+ | `HtmlBuilder.build { ... }` | Build HTML using the tag DSL, returns a string |
89
+ | `Builder#to_html` | Render the builder contents to an HTML string |
90
+ | `Builder#text(content)` | Add escaped text content to the current element |
91
+ | `Builder#raw(html)` | Add raw HTML without escaping |
92
+ | `Escape.html(value)` | Escape HTML special characters in a string |
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ bundle install
98
+ bundle exec rspec # Run tests
99
+ bundle exec rubocop # Check code style
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module HtmlBuilder
5
+ # DSL-based HTML builder that creates a tree of nodes
6
+ class Builder
7
+ STANDARD_TAGS = %i[
8
+ a abbr address article aside audio b bdi bdo blockquote body button
9
+ canvas caption cite code colgroup data datalist dd del details dfn
10
+ dialog div dl dt em fieldset figcaption figure footer form
11
+ h1 h2 h3 h4 h5 h6 head header hgroup html i iframe ins kbd label
12
+ legend li main map mark menu meter nav noscript object ol optgroup
13
+ option output p picture pre progress q rp rt ruby s samp script
14
+ section select slot small span strong style sub summary sup table
15
+ tbody td template textarea tfoot th thead time title tr u ul var video
16
+ ].freeze
17
+
18
+ VOID_TAGS = %i[area base br col embed hr img input link meta param source track wbr].freeze
19
+
20
+ ALL_TAGS = (STANDARD_TAGS + VOID_TAGS).freeze
21
+
22
+ def initialize
23
+ @root_children = []
24
+ @stack = []
25
+ end
26
+
27
+ # Render all root-level nodes to HTML
28
+ #
29
+ # @return [String] the rendered HTML
30
+ def to_html
31
+ @root_children.map { |c| c.respond_to?(:to_html) ? c.to_html : Escape.html(c.to_s) }.join
32
+ end
33
+
34
+ ALL_TAGS.each do |tag_name|
35
+ define_method(tag_name) do |content = nil, **attrs, &block|
36
+ node = Node.new(tag_name, attributes: attrs)
37
+ node.add_child(content.to_s) if content
38
+ current_children << node
39
+
40
+ if block
41
+ @stack.push(node)
42
+ instance_eval(&block)
43
+ @stack.pop
44
+ end
45
+
46
+ node
47
+ end
48
+ end
49
+
50
+ # Add raw text content to the current context
51
+ #
52
+ # @param content [String] the text content (will be escaped)
53
+ # @return [void]
54
+ def text(content)
55
+ current_children << content.to_s
56
+ end
57
+
58
+ # Add raw HTML content without escaping
59
+ #
60
+ # @param html [String] the raw HTML string
61
+ # @return [void]
62
+ def raw(html)
63
+ node = RawNode.new(html)
64
+ current_children << node
65
+ end
66
+
67
+ private
68
+
69
+ # @return [Array] the children array for the current context
70
+ def current_children
71
+ if @stack.empty?
72
+ @root_children
73
+ else
74
+ @stack.last.children
75
+ end
76
+ end
77
+ end
78
+
79
+ # A node that renders raw HTML without escaping
80
+ class RawNode
81
+ # @param html [String] the raw HTML
82
+ def initialize(html)
83
+ @html = html
84
+ end
85
+
86
+ # @return [String] the raw HTML
87
+ def to_html
88
+ @html
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module HtmlBuilder
5
+ # HTML entity escaping utilities
6
+ module Escape
7
+ ENTITIES = {
8
+ '&' => '&amp;',
9
+ '<' => '&lt;',
10
+ '>' => '&gt;',
11
+ '"' => '&quot;',
12
+ "'" => '&#39;'
13
+ }.freeze
14
+
15
+ ENTITY_PATTERN = Regexp.union(ENTITIES.keys).freeze
16
+
17
+ # Escape HTML special characters in a string
18
+ #
19
+ # @param value [String] the string to escape
20
+ # @return [String] the escaped string
21
+ def self.html(value)
22
+ value.to_s.gsub(ENTITY_PATTERN, ENTITIES)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module HtmlBuilder
5
+ # Represents an HTML element node with tag, attributes, and children
6
+ class Node
7
+ # @return [Symbol] the tag name
8
+ attr_reader :tag
9
+
10
+ # @return [Hash] the element attributes
11
+ attr_reader :attributes
12
+
13
+ # @return [Array] the child nodes
14
+ attr_reader :children
15
+
16
+ # @param tag [Symbol] the HTML tag name
17
+ # @param attributes [Hash] HTML attributes
18
+ def initialize(tag, attributes: {})
19
+ @tag = tag
20
+ @attributes = attributes
21
+ @children = []
22
+ end
23
+
24
+ # Add a child node
25
+ #
26
+ # @param child [Node, String] child node or text content
27
+ # @return [void]
28
+ def add_child(child)
29
+ @children << child
30
+ end
31
+
32
+ # Render the node to an HTML string
33
+ #
34
+ # @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}>"
40
+ 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
+ end
44
+ end
45
+
46
+ private
47
+
48
+ VOID_ELEMENTS = %i[area base br col embed hr img input link meta param source track wbr].freeze
49
+
50
+ # @return [Boolean] true if this is a void (self-closing) element
51
+ def void_element?
52
+ VOID_ELEMENTS.include?(tag)
53
+ end
54
+
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
+ ''
64
+ else
65
+ " #{key}=\"#{Escape.html(value)}\""
66
+ end
67
+ end.join
68
+
69
+ attrs
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module HtmlBuilder
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'html_builder/version'
4
+ require_relative 'html_builder/escape'
5
+ require_relative 'html_builder/node'
6
+ require_relative 'html_builder/builder'
7
+
8
+ module Philiprehberger
9
+ module HtmlBuilder
10
+ class Error < StandardError; end
11
+
12
+ # Build HTML using a tag DSL
13
+ #
14
+ # @yield [Builder] the builder instance for DSL evaluation
15
+ # @return [String] the rendered HTML string
16
+ # @raise [Error] if no block is given
17
+ def self.build(&block)
18
+ raise Error, 'a block is required' unless block
19
+
20
+ builder = Builder.new
21
+ builder.instance_eval(&block)
22
+ builder.to_html
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philiprehberger-html_builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Philip Rehberger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Build HTML programmatically using a clean tag DSL with nested blocks,
14
+ automatic content escaping, void element support, and attribute hashes.
15
+ email:
16
+ - me@philiprehberger.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - lib/philiprehberger/html_builder.rb
25
+ - lib/philiprehberger/html_builder/builder.rb
26
+ - lib/philiprehberger/html_builder/escape.rb
27
+ - lib/philiprehberger/html_builder/node.rb
28
+ - lib/philiprehberger/html_builder/version.rb
29
+ homepage: https://github.com/philiprehberger/rb-html-builder
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/philiprehberger/rb-html-builder
34
+ source_code_uri: https://github.com/philiprehberger/rb-html-builder
35
+ changelog_uri: https://github.com/philiprehberger/rb-html-builder/blob/main/CHANGELOG.md
36
+ bug_tracker_uri: https://github.com/philiprehberger/rb-html-builder/issues
37
+ rubygems_mfa_required: 'true'
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.1.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.5.22
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Programmatic HTML builder with tag DSL and auto-escaping
57
+ test_files: []