philiprehberger-xml_builder 0.2.1 → 0.4.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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +30 -2
- data/lib/philiprehberger/xml_builder/document.rb +42 -7
- data/lib/philiprehberger/xml_builder/processing_instruction.rb +49 -0
- data/lib/philiprehberger/xml_builder/version.rb +1 -1
- data/lib/philiprehberger/xml_builder.rb +5 -4
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 720c31767f0706544320e00c6fb4ed6e3dd946f601c68c947c6eaf409c324dd2
|
|
4
|
+
data.tar.gz: a8dafaef9908d5f485cdf57fc65654a8b11610e93575a27f78b668f6b9e342e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 466443cf83706000e5ae81f7a8fd5cf673cb7aad9306a4de70d189182af0459e75a415b047063af46feaffe227342f3c1b51ac8288e312cca5ba2983efd38233
|
|
7
|
+
data.tar.gz: 5c904af4d1da2f0899771115ac35632ebd0c871c65236e24a98001522821a23e93d003723c4b4b98470da8579d8267a52443ba91243e16e46571a76fd6164515
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Processing instruction (PI) node support via `Document#processing_instruction` / `#pi`
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2026-04-10
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Add `declaration:` option to `build`, `build_soap`, and `Document.new` to omit the XML declaration when building fragments
|
|
19
|
+
- Add CDATA content validation — raises `Error` if content contains `]]>`
|
|
20
|
+
- Add comment text validation — raises `Error` if text contains `--`
|
|
21
|
+
- Add Ruby 3.4 to CI test matrix
|
|
22
|
+
|
|
23
|
+
## [0.2.2] - 2026-03-31
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Add GitHub issue templates, dependabot config, and PR template
|
|
27
|
+
|
|
10
28
|
## [0.2.1] - 2026-03-31
|
|
11
29
|
|
|
12
30
|
### Changed
|
data/README.md
CHANGED
|
@@ -68,6 +68,20 @@ end
|
|
|
68
68
|
|
|
69
69
|
### Processing Instructions
|
|
70
70
|
|
|
71
|
+
Pass keyword attributes to emit a structured PI with XML-escaped values:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
xml = Philiprehberger::XmlBuilder.build do |doc|
|
|
75
|
+
doc.pi("xml-stylesheet", href: "style.xsl", type: "text/xsl")
|
|
76
|
+
doc.tag(:root) { doc.text("content") }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts xml
|
|
80
|
+
# <?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="style.xsl" type="text/xsl"?><root>content</root>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A legacy positional content string is also supported:
|
|
84
|
+
|
|
71
85
|
```ruby
|
|
72
86
|
xml = Philiprehberger::XmlBuilder.build do |doc|
|
|
73
87
|
doc.processing_instruction("xml-stylesheet", 'type="text/xsl" href="style.xsl"')
|
|
@@ -98,6 +112,19 @@ xml = Philiprehberger::XmlBuilder.build do |doc|
|
|
|
98
112
|
end
|
|
99
113
|
```
|
|
100
114
|
|
|
115
|
+
### Without XML Declaration
|
|
116
|
+
|
|
117
|
+
Omit the `<?xml ... ?>` declaration when building fragments:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
xml = Philiprehberger::XmlBuilder.build(declaration: false) do |doc|
|
|
121
|
+
doc.tag(:item, id: "1") { doc.text("fragment") }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
puts xml
|
|
125
|
+
# <item id="1">fragment</item>
|
|
126
|
+
```
|
|
127
|
+
|
|
101
128
|
### XML Namespaces
|
|
102
129
|
|
|
103
130
|
Register namespace prefixes and create namespace-aware elements:
|
|
@@ -184,8 +211,8 @@ end
|
|
|
184
211
|
|
|
185
212
|
| Method | Description |
|
|
186
213
|
|--------|-------------|
|
|
187
|
-
| `.build(encoding: "UTF-8", version: "1.0") { \|doc\| ... }` | Build an XML document and return the string |
|
|
188
|
-
| `.build_soap(soap_version: "1.1", encoding: "UTF-8", version: "1.0") { \|header, body\| ... }` | Build a SOAP envelope document |
|
|
214
|
+
| `.build(encoding: "UTF-8", version: "1.0", declaration: true) { \|doc\| ... }` | Build an XML document and return the string |
|
|
215
|
+
| `.build_soap(soap_version: "1.1", encoding: "UTF-8", version: "1.0", declaration: true) { \|header, body\| ... }` | Build a SOAP envelope document |
|
|
189
216
|
|
|
190
217
|
### `Document`
|
|
191
218
|
|
|
@@ -196,6 +223,7 @@ end
|
|
|
196
223
|
| `#cdata(content)` | Add a CDATA section |
|
|
197
224
|
| `#comment(text)` | Add an XML comment |
|
|
198
225
|
| `#processing_instruction(target, content)` | Add a processing instruction |
|
|
226
|
+
| `#processing_instruction(target, **attrs)` | Append an XML processing instruction (alias: `#pi`) |
|
|
199
227
|
| `#raw(string)` | Add raw unescaped XML |
|
|
200
228
|
| `#namespace(prefix, uri)` | Register an XML namespace prefix and URI |
|
|
201
229
|
| `#namespace_tag(prefix, name, attributes = {}) { ... }` | Add a namespace-prefixed element with auto xmlns |
|
|
@@ -10,9 +10,10 @@ module Philiprehberger
|
|
|
10
10
|
|
|
11
11
|
# @param version [String] XML version for the declaration
|
|
12
12
|
# @param encoding [String] XML encoding for the declaration
|
|
13
|
-
def initialize(version: '1.0', encoding: 'UTF-8')
|
|
13
|
+
def initialize(version: '1.0', encoding: 'UTF-8', declaration: true)
|
|
14
14
|
@version = version
|
|
15
15
|
@encoding = encoding
|
|
16
|
+
@declaration = declaration
|
|
16
17
|
@children = []
|
|
17
18
|
@node_stack = []
|
|
18
19
|
@namespaces = {}
|
|
@@ -50,6 +51,8 @@ module Philiprehberger
|
|
|
50
51
|
# @param content [String] the CDATA content (must not contain "]]>")
|
|
51
52
|
# @return [void]
|
|
52
53
|
def cdata(content)
|
|
54
|
+
raise Error, 'CDATA content must not contain "]]>"' if content.to_s.include?(']]>')
|
|
55
|
+
|
|
53
56
|
current_parent.push("<![CDATA[#{content}]]>")
|
|
54
57
|
end
|
|
55
58
|
|
|
@@ -58,18 +61,38 @@ module Philiprehberger
|
|
|
58
61
|
# @param text [String] the comment text
|
|
59
62
|
# @return [void]
|
|
60
63
|
def comment(text)
|
|
64
|
+
raise Error, 'Comment text must not contain "--"' if text.to_s.include?('--')
|
|
65
|
+
|
|
61
66
|
current_parent.push("<!-- #{text} -->")
|
|
62
67
|
end
|
|
63
68
|
|
|
69
|
+
# Valid PI target pattern per the XML spec (simplified).
|
|
70
|
+
PI_TARGET_PATTERN = /\A[A-Za-z_][\w.-]*\z/
|
|
71
|
+
|
|
64
72
|
# Add a processing instruction.
|
|
65
73
|
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
74
|
+
# Accepts either a legacy positional content string or keyword attributes.
|
|
75
|
+
# When attrs are given, renders as <?target key="value" key2="value2"?>.
|
|
76
|
+
# When a content string is given, renders as <?target content?>.
|
|
77
|
+
#
|
|
78
|
+
# @param target [String] the PI target (must match PI_TARGET_PATTERN; "xml" is forbidden)
|
|
79
|
+
# @param content [String, nil] optional raw PI content string (legacy)
|
|
80
|
+
# @param attrs [Hash] attribute key/value pairs (XML-escaped)
|
|
68
81
|
# @return [void]
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
# @raise [ArgumentError] if target is empty, invalid, or equal to "xml" (case-insensitive)
|
|
83
|
+
def processing_instruction(target, content = nil, **attrs)
|
|
84
|
+
validate_pi_target!(target)
|
|
85
|
+
|
|
86
|
+
if content.is_a?(String)
|
|
87
|
+
current_parent.push("<?#{target} #{content}?>")
|
|
88
|
+
else
|
|
89
|
+
current_parent.push(ProcessingInstruction.new(target, attrs))
|
|
90
|
+
end
|
|
71
91
|
end
|
|
72
92
|
|
|
93
|
+
# Alias for #processing_instruction.
|
|
94
|
+
alias pi processing_instruction
|
|
95
|
+
|
|
73
96
|
# Add raw XML content without escaping.
|
|
74
97
|
#
|
|
75
98
|
# @param string [String] raw XML string
|
|
@@ -90,8 +113,12 @@ module Philiprehberger
|
|
|
90
113
|
# @param indent [Integer, nil] number of spaces per indentation level, or nil for compact output
|
|
91
114
|
# @return [String] the rendered XML document
|
|
92
115
|
def to_xml(indent: nil)
|
|
93
|
-
parts = [
|
|
94
|
-
|
|
116
|
+
parts = []
|
|
117
|
+
|
|
118
|
+
if @declaration
|
|
119
|
+
parts << "<?xml version=\"#{@version}\" encoding=\"#{@encoding}\"?>"
|
|
120
|
+
parts << (indent ? "\n" : '')
|
|
121
|
+
end
|
|
95
122
|
|
|
96
123
|
@children.each do |child|
|
|
97
124
|
parts << render_child(child, indent: indent, level: 0)
|
|
@@ -227,10 +254,18 @@ module Philiprehberger
|
|
|
227
254
|
@node_stack.last&.children || @children
|
|
228
255
|
end
|
|
229
256
|
|
|
257
|
+
def validate_pi_target!(target)
|
|
258
|
+
raise ArgumentError, 'PI target must be a non-empty String' unless target.is_a?(String) && !target.empty?
|
|
259
|
+
raise ArgumentError, 'PI target "xml" is reserved by the XML specification' if target.casecmp('xml').zero?
|
|
260
|
+
raise ArgumentError, "Invalid PI target: #{target.inspect}" unless target.match?(PI_TARGET_PATTERN)
|
|
261
|
+
end
|
|
262
|
+
|
|
230
263
|
def render_child(child, indent:, level:)
|
|
231
264
|
case child
|
|
232
265
|
when Node
|
|
233
266
|
child.render(indent: indent, level: level)
|
|
267
|
+
when ProcessingInstruction
|
|
268
|
+
child.to_xml(indent: indent, level: level, pretty: !indent.nil?)
|
|
234
269
|
when String
|
|
235
270
|
if indent
|
|
236
271
|
"#{' ' * (indent * level)}#{child}\n"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module XmlBuilder
|
|
5
|
+
# Represents an XML processing instruction (PI).
|
|
6
|
+
#
|
|
7
|
+
# Renders as <?target key="value" key2="value2"?>, with attribute
|
|
8
|
+
# values escaped via Escaper.
|
|
9
|
+
class ProcessingInstruction
|
|
10
|
+
attr_reader :target, :attributes
|
|
11
|
+
|
|
12
|
+
# @param target [String] the PI target name
|
|
13
|
+
# @param attributes [Hash] attribute key/value pairs
|
|
14
|
+
def initialize(target, attributes = {})
|
|
15
|
+
@target = target.to_s
|
|
16
|
+
@attributes = attributes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Render this processing instruction as an XML string.
|
|
20
|
+
#
|
|
21
|
+
# @param indent [Integer, nil] number of spaces per indentation level, or nil for compact output
|
|
22
|
+
# @param level [Integer] current nesting depth (used internally)
|
|
23
|
+
# @param pretty [Boolean] whether to apply pretty-print formatting
|
|
24
|
+
# @return [String] the rendered processing instruction
|
|
25
|
+
def to_xml(indent: nil, level: 0, pretty: false)
|
|
26
|
+
prefix = indent && pretty ? ' ' * (indent * level) : ''
|
|
27
|
+
newline = indent && pretty ? "\n" : ''
|
|
28
|
+
"#{prefix}<?#{@target}#{render_attributes}?>#{newline}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Alias for to_xml to match Node#render semantics.
|
|
32
|
+
def render(indent: nil, level: 0)
|
|
33
|
+
to_xml(indent: indent, level: level, pretty: !indent.nil?)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def render_attributes
|
|
39
|
+
return '' if @attributes.empty?
|
|
40
|
+
|
|
41
|
+
pairs = @attributes.map do |key, value|
|
|
42
|
+
attr_name = key.to_s.gsub('__', ':')
|
|
43
|
+
" #{attr_name}=\"#{Escaper.escape(value)}\""
|
|
44
|
+
end
|
|
45
|
+
pairs.join
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'xml_builder/version'
|
|
4
4
|
require_relative 'xml_builder/escaper'
|
|
5
5
|
require_relative 'xml_builder/node'
|
|
6
|
+
require_relative 'xml_builder/processing_instruction'
|
|
6
7
|
require_relative 'xml_builder/document'
|
|
7
8
|
|
|
8
9
|
module Philiprehberger
|
|
@@ -15,8 +16,8 @@ module Philiprehberger
|
|
|
15
16
|
# @param version [String] XML version declaration (default: "1.0")
|
|
16
17
|
# @yield [Document] the document builder
|
|
17
18
|
# @return [String] the rendered XML string
|
|
18
|
-
def self.build(encoding: 'UTF-8', version: '1.0', &block)
|
|
19
|
-
doc = Document.new(version: version, encoding: encoding)
|
|
19
|
+
def self.build(encoding: 'UTF-8', version: '1.0', declaration: true, &block)
|
|
20
|
+
doc = Document.new(version: version, encoding: encoding, declaration: declaration)
|
|
20
21
|
block.call(doc)
|
|
21
22
|
doc.to_s
|
|
22
23
|
end
|
|
@@ -31,8 +32,8 @@ module Philiprehberger
|
|
|
31
32
|
# @param version [String] XML version declaration (default: "1.0")
|
|
32
33
|
# @yield [header, body] yields two arrays; push lambdas that accept a doc
|
|
33
34
|
# @return [String] the rendered SOAP XML string
|
|
34
|
-
def self.build_soap(soap_version: '1.1', encoding: 'UTF-8', version: '1.0', &block)
|
|
35
|
-
doc = Document.new(version: version, encoding: encoding)
|
|
35
|
+
def self.build_soap(soap_version: '1.1', encoding: 'UTF-8', version: '1.0', declaration: true, &block)
|
|
36
|
+
doc = Document.new(version: version, encoding: encoding, declaration: declaration)
|
|
36
37
|
doc.soap_envelope(version: soap_version, &block)
|
|
37
38
|
doc.to_s
|
|
38
39
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-xml_builder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.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-
|
|
11
|
+
date: 2026-04-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Programmatic XML construction with a clean DSL, auto-escaping, CDATA,
|
|
14
14
|
comments, processing instructions, and pretty printing. Zero dependencies.
|
|
@@ -25,12 +25,13 @@ files:
|
|
|
25
25
|
- lib/philiprehberger/xml_builder/document.rb
|
|
26
26
|
- lib/philiprehberger/xml_builder/escaper.rb
|
|
27
27
|
- lib/philiprehberger/xml_builder/node.rb
|
|
28
|
+
- lib/philiprehberger/xml_builder/processing_instruction.rb
|
|
28
29
|
- lib/philiprehberger/xml_builder/version.rb
|
|
29
|
-
homepage: https://
|
|
30
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-xml_builder
|
|
30
31
|
licenses:
|
|
31
32
|
- MIT
|
|
32
33
|
metadata:
|
|
33
|
-
homepage_uri: https://
|
|
34
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-xml_builder
|
|
34
35
|
source_code_uri: https://github.com/philiprehberger/rb-xml-builder
|
|
35
36
|
changelog_uri: https://github.com/philiprehberger/rb-xml-builder/blob/main/CHANGELOG.md
|
|
36
37
|
bug_tracker_uri: https://github.com/philiprehberger/rb-xml-builder/issues
|