philiprehberger-xml_builder 0.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fb9d2343ef89b02de12422a8ca7632e3d16634876c8d602f93966f35f4f8287
4
- data.tar.gz: 7a21d2d73ddfe6d7d2305527fc1305425965197eb5950809d3168308ed4a7d4e
3
+ metadata.gz: 720c31767f0706544320e00c6fb4ed6e3dd946f601c68c947c6eaf409c324dd2
4
+ data.tar.gz: a8dafaef9908d5f485cdf57fc65654a8b11610e93575a27f78b668f6b9e342e4
5
5
  SHA512:
6
- metadata.gz: 7c0d1c868566984cc1656049d2705a2b7af30293e567b0d66ff065edeeea004d257e5d3164b2e288949dfcc0654f0dddc3dea2d81a0c64ccebd1cd05c14afbb8
7
- data.tar.gz: 8094d77d65803ac2ad438e1eee6c9c32218e60e6548a09d18874796bad2a17ba891178aa6f0198b6d756ea4d067abcca2a8492a391b7e8d8409d381fcf652df2
6
+ metadata.gz: 466443cf83706000e5ae81f7a8fd5cf673cb7aad9306a4de70d189182af0459e75a415b047063af46feaffe227342f3c1b51ac8288e312cca5ba2983efd38233
7
+ data.tar.gz: 5c904af4d1da2f0899771115ac35632ebd0c871c65236e24a98001522821a23e93d003723c4b4b98470da8579d8267a52443ba91243e16e46571a76fd6164515
data/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ 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
+
10
15
  ## [0.3.0] - 2026-04-10
11
16
 
12
17
  ### 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"')
@@ -209,6 +223,7 @@ end
209
223
  | `#cdata(content)` | Add a CDATA section |
210
224
  | `#comment(text)` | Add an XML comment |
211
225
  | `#processing_instruction(target, content)` | Add a processing instruction |
226
+ | `#processing_instruction(target, **attrs)` | Append an XML processing instruction (alias: `#pi`) |
212
227
  | `#raw(string)` | Add raw unescaped XML |
213
228
  | `#namespace(prefix, uri)` | Register an XML namespace prefix and URI |
214
229
  | `#namespace_tag(prefix, name, attributes = {}) { ... }` | Add a namespace-prefixed element with auto xmlns |
@@ -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
@@ -236,10 +254,18 @@ module Philiprehberger
236
254
  @node_stack.last&.children || @children
237
255
  end
238
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
+
239
263
  def render_child(child, indent:, level:)
240
264
  case child
241
265
  when Node
242
266
  child.render(indent: indent, level: level)
267
+ when ProcessingInstruction
268
+ child.to_xml(indent: indent, level: level, pretty: !indent.nil?)
243
269
  when String
244
270
  if indent
245
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module XmlBuilder
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.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.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-04-11 00:00:00.000000000 Z
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,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: