philiprehberger-xml_builder 0.3.0 → 0.5.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: 3fb9d2343ef89b02de12422a8ca7632e3d16634876c8d602f93966f35f4f8287
4
- data.tar.gz: 7a21d2d73ddfe6d7d2305527fc1305425965197eb5950809d3168308ed4a7d4e
3
+ metadata.gz: c3469bf0da4d625250b3e99c2659f1c5299d344a7b6b7f60f24af0cc9fd07f71
4
+ data.tar.gz: c806ec833b1d176a5907fb977c39c54b3e35f1103314cd2abfd335b260faff39
5
5
  SHA512:
6
- metadata.gz: 7c0d1c868566984cc1656049d2705a2b7af30293e567b0d66ff065edeeea004d257e5d3164b2e288949dfcc0654f0dddc3dea2d81a0c64ccebd1cd05c14afbb8
7
- data.tar.gz: 8094d77d65803ac2ad438e1eee6c9c32218e60e6548a09d18874796bad2a17ba891178aa6f0198b6d756ea4d067abcca2a8492a391b7e8d8409d381fcf652df2
6
+ metadata.gz: 9d8b31224c876a05e5d71619d80ad32a6cff4b7df989b21344d6b7063313e5a7454f1fb58ad0a506f3b49205258d5c9a4937231368e37f82572c8dbaf47230d2
7
+ data.tar.gz: fe9aba2f34358d9f54fce39e6894fa3959aa1c8dc29588fcb568112c24eaf432e84909cec5c78716f149b8803c8cdff080ab23241060af8a0a67b20c4e3e4c19
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-05-07
11
+
12
+ ### Added
13
+ - `Document#pretty(indent: 2)` — convenience renderer for pretty-printed XML, equivalent to `to_xml(indent: 2)` but with sane defaults.
14
+
15
+ ## [0.4.0] - 2026-04-16
16
+
17
+ ### Added
18
+ - Processing instruction (PI) node support via `Document#processing_instruction` / `#pi`
19
+
10
20
  ## [0.3.0] - 2026-04-10
11
21
 
12
22
  ### Added
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"')
@@ -90,6 +104,16 @@ puts doc.to_xml(indent: 2)
90
104
  # </root>
91
105
  ```
92
106
 
107
+ Or use `#pretty` for the same output with sane defaults:
108
+
109
+ ```ruby
110
+ puts doc.pretty
111
+ # Equivalent to doc.to_xml(indent: 2)
112
+
113
+ puts doc.pretty(indent: 4)
114
+ # Equivalent to doc.to_xml(indent: 4)
115
+ ```
116
+
93
117
  ### Raw XML
94
118
 
95
119
  ```ruby
@@ -209,6 +233,7 @@ end
209
233
  | `#cdata(content)` | Add a CDATA section |
210
234
  | `#comment(text)` | Add an XML comment |
211
235
  | `#processing_instruction(target, content)` | Add a processing instruction |
236
+ | `#processing_instruction(target, **attrs)` | Append an XML processing instruction (alias: `#pi`) |
212
237
  | `#raw(string)` | Add raw unescaped XML |
213
238
  | `#namespace(prefix, uri)` | Register an XML namespace prefix and URI |
214
239
  | `#namespace_tag(prefix, name, attributes = {}) { ... }` | Add a namespace-prefixed element with auto xmlns |
@@ -217,6 +242,7 @@ end
217
242
  | `#insert_fragment(xml_string)` | Insert a raw XML fragment string |
218
243
  | `#to_s` | Render compact XML string |
219
244
  | `#to_xml(indent: nil)` | Render XML with optional indentation |
245
+ | `#pretty(indent: 2)` | Renders with default 2-space indentation |
220
246
 
221
247
  ### `Escaper`
222
248
 
@@ -66,15 +66,33 @@ module Philiprehberger
66
66
  current_parent.push("<!-- #{text} -->")
67
67
  end
68
68
 
69
+ # Valid PI target pattern per the XML spec (simplified).
70
+ PI_TARGET_PATTERN = /\A[A-Za-z_][\w.-]*\z/
71
+
69
72
  # Add a processing instruction.
70
73
  #
71
- # @param target [String] the PI target
72
- # @param content [String] the PI content
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)
73
81
  # @return [void]
74
- def processing_instruction(target, content)
75
- current_parent.push("<?#{target} #{content}?>")
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
76
91
  end
77
92
 
93
+ # Alias for #processing_instruction.
94
+ alias pi processing_instruction
95
+
78
96
  # Add raw XML content without escaping.
79
97
  #
80
98
  # @param string [String] raw XML string
@@ -90,6 +108,14 @@ module Philiprehberger
90
108
  to_xml
91
109
  end
92
110
 
111
+ # Render the document as a pretty-printed XML string with default 2-space indentation.
112
+ #
113
+ # @param indent [Integer] number of spaces per indentation level (default 2)
114
+ # @return [String] the rendered XML document
115
+ def pretty(indent: 2)
116
+ to_xml(indent: indent)
117
+ end
118
+
93
119
  # Render the document as an XML string with optional indentation.
94
120
  #
95
121
  # @param indent [Integer, nil] number of spaces per indentation level, or nil for compact output
@@ -236,10 +262,18 @@ module Philiprehberger
236
262
  @node_stack.last&.children || @children
237
263
  end
238
264
 
265
+ def validate_pi_target!(target)
266
+ raise ArgumentError, 'PI target must be a non-empty String' unless target.is_a?(String) && !target.empty?
267
+ raise ArgumentError, 'PI target "xml" is reserved by the XML specification' if target.casecmp('xml').zero?
268
+ raise ArgumentError, "Invalid PI target: #{target.inspect}" unless target.match?(PI_TARGET_PATTERN)
269
+ end
270
+
239
271
  def render_child(child, indent:, level:)
240
272
  case child
241
273
  when Node
242
274
  child.render(indent: indent, level: level)
275
+ when ProcessingInstruction
276
+ child.to_xml(indent: indent, level: level, pretty: !indent.nil?)
243
277
  when String
244
278
  if indent
245
279
  "#{' ' * (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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module XmlBuilder
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  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
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.3.0
4
+ version: 0.5.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-11 00:00:00.000000000 Z
11
+ date: 2026-05-07 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,6 +25,7 @@ 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
30
  homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-xml_builder
30
31
  licenses: