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 +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/lib/philiprehberger/html_builder/builder.rb +92 -0
- data/lib/philiprehberger/html_builder/escape.rb +26 -0
- data/lib/philiprehberger/html_builder/node.rb +73 -0
- data/lib/philiprehberger/html_builder/version.rb +7 -0
- data/lib/philiprehberger/html_builder.rb +25 -0
- metadata +57 -0
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
|
+
[](https://github.com/philiprehberger/rb-html-builder/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/philiprehberger-html_builder)
|
|
5
|
+
[](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><script>alert("xss")</script></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
|
+
'&' => '&',
|
|
9
|
+
'<' => '<',
|
|
10
|
+
'>' => '>',
|
|
11
|
+
'"' => '"',
|
|
12
|
+
"'" => '''
|
|
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,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: []
|