moxml 0.5.142 → 0.5.143

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: 2c55a66ba735b6d8f170ad9f83a95faeb16717520d2fd30f767e1dc56914914b
4
- data.tar.gz: 74f3c07cdccc077e78fbef88d9d8e8f83310f5f9cdc1175221bf719a3a864146
3
+ metadata.gz: c7ef456b1833d080915877e15238d23fd1e3d38bd638a434711bc98d423b78fc
4
+ data.tar.gz: 680fe1bbe7097895d5fdb46c29e5e2c624b13ed29792be2ffa941ef3c8bbe3bb
5
5
  SHA512:
6
- metadata.gz: 2b8a163b9601f4da9d2c9a96b1d5ef35eb0d874c27ec5d6c608283cd7ca27c9d801fa9065724a8989406423bd0844100d914cb2ee0de07a2a8f51f0212eb4ab4
7
- data.tar.gz: 153852b30839c08744fbd29558af04ba07a1c7c27979fa5cfd0ab6fbf48185a1186a33b7989b4aaf07e477942a3c9c215aa6b25b9225dff5670def94a71aa563
6
+ metadata.gz: 01e458bb31733bbb8a9bf379d37ba30b4d5b30d1670eceffbfad5ae953afa85f1dc272ce3cc99cf719808417a13035dadf00e14fdb6a01d5cce3f4bcc855e8ac
7
+ data.tar.gz: e0eae403141507dcc8dd23e3b6962935cac3481382ef268a41105ae919e014fc53e657e4b44d261cdb8e59799678dc0a2f1f103937580ee6bb7b8f4e93372667
@@ -89,6 +89,15 @@ module Moxml
89
89
  # no declaration. indent:/indent_text: thread the
90
90
  # pretty-print knobs where the adapter's engine carries
91
91
  # them. Adapters without an HTML serializer raise.
92
+ # XHTML-shaped serialization (issue #390): void elements
93
+ # self-closed, non-void paired, indent knobs composing.
94
+ # Adapters with an HTML engine answer this; the default
95
+ # raises so consumers get a named error, not mangled bytes.
96
+ def serialize_xhtml(_node, _options = {})
97
+ raise Moxml::NotImplementedError,
98
+ "XHTML serialization requires an adapter with an HTML engine"
99
+ end
100
+
92
101
  def serialize_html(_node, _options = {})
93
102
  raise Moxml::AdapterError.new(
94
103
  "HTML serialization is not supported by the #{name.split('::').last} adapter",
@@ -1212,6 +1212,16 @@ module Moxml
1212
1212
  node.to_html(**kwargs)
1213
1213
  end
1214
1214
 
1215
+ # XHTML shape (issue #390): the binding's XML method on an
1216
+ # HTML document — void elements short-form (<br/>), non-void
1217
+ # paired, raw text escaped, no declaration. The indent knobs
1218
+ # thread the #354 machinery.
1219
+ def serialize_xhtml(node, options = {})
1220
+ node.to_xml(no_decl: true, **options.slice(:indent,
1221
+ :indent_text, :encoding)
1222
+ .compact)
1223
+ end
1224
+
1215
1225
  def parse_html_fragment(html, context_tag = :body, _context = nil)
1216
1226
  unless HTML_FRAGMENT
1217
1227
  raise Moxml::AdapterError.new(
@@ -189,6 +189,22 @@ module Moxml
189
189
  node.to_html(**kwargs)
190
190
  end
191
191
 
192
+ # Nokogiri's AS_XHTML save flag is the byte reference for the
193
+ # XHTML shape (issue #390): FORMAT layout, self-closed voids,
194
+ # no declaration.
195
+ def serialize_xhtml(node, options = {})
196
+ save_with = ::Nokogiri::XML::Node::SaveOptions::FORMAT |
197
+ ::Nokogiri::XML::Node::SaveOptions::AS_XHTML |
198
+ ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
199
+ io = StringIO.new
200
+ kwargs = { save_with: save_with }
201
+ kwargs[:indent] = options[:indent] if options[:indent]
202
+ kwargs[:indent_text] = options[:indent_text] if options[:indent_text]
203
+ kwargs[:encoding] = options[:encoding] if options[:encoding]
204
+ node.write_to(io, **kwargs)
205
+ io.string
206
+ end
207
+
192
208
  # WHATWG fragment parse through Nokogiri::HTML5.fragment
193
209
  # (issue #353) — the same tree construction the leptris
194
210
  # lane composes over its WHATWG engine.
@@ -129,12 +129,30 @@ module Moxml
129
129
  # (leptris >= the #354 ride; nokogiri passes them to the
130
130
  # engine). Adapters without the knob raise Moxml::AdapterError.
131
131
  def to_html(options = {})
132
+ return to_xhtml(options) if options[:xhtml]
133
+
132
134
  result = adapter.serialize_html(@native, options)
133
135
  apply_line_ending(result,
134
136
  options.fetch(:line_ending, Config::LINE_ENDING_LF))
135
137
  result
136
138
  end
137
139
 
140
+ # XHTML-shaped serialization (issue #390): void elements
141
+ # self-closed (<br/>), non-void elements paired, raw text
142
+ # escaped (the XML method's §16.2-compatible escaping), no
143
+ # declaration unless asked. Composes with the indent knobs.
144
+ # Also selected by to_html(xhtml: true).
145
+ #
146
+ # Byte reference: Nokogiri's FORMAT | AS_XHTML | NO_DECLARATION
147
+ # save flag; canon pins engine-vs-Nokogiri parity on its display
148
+ # corpus before flipping its lane.
149
+ def to_xhtml(options = {})
150
+ result = adapter.serialize_xhtml(@native, options)
151
+ apply_line_ending(result,
152
+ options.fetch(:line_ending, Config::LINE_ENDING_LF))
153
+ result
154
+ end
155
+
138
156
  def create_element(name)
139
157
  Wrappers::Element.new(adapter.create_element(name, owner_doc: @native), context)
140
158
  end
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.142"
4
+ VERSION = "0.5.143"
5
5
  end
@@ -13,7 +13,7 @@ SUPPORTED = %i[nokogiri leptris].select do |name|
13
13
  Moxml::Adapter::AVAILABLE_ADAPTERS.include?(name)
14
14
  end.freeze
15
15
 
16
- CORPUS = {
16
+ XSLT_CORPUS = {
17
17
  "value-of with count" => [
18
18
  <<~XSL,
19
19
  <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
@@ -82,7 +82,7 @@ RSpec.describe "Moxml::XSLT cross-engine agreement" do
82
82
  output.sub(/\A<\?xml[^>]*\?>\s*/, "")
83
83
  end
84
84
 
85
- CORPUS.each do |label, (sheet_xml, input, expected_re)|
85
+ XSLT_CORPUS.each do |label, (sheet_xml, input, expected_re)|
86
86
  it "transforms #{label}" do
87
87
  sheet = ctx.xslt(sheet_xml)
88
88
  doc = ctx.parse(input)
@@ -97,7 +97,7 @@ RSpec.describe "Moxml::XSLT cross-engine agreement" do
97
97
  return unless SUPPORTED.size == 2
98
98
 
99
99
  first, second = SUPPORTED.map { |name| Moxml.new(name) }
100
- CORPUS.each do |label, (sheet_xml, input, _)|
100
+ XSLT_CORPUS.each do |label, (sheet_xml, input, _)|
101
101
  a = first.xslt(sheet_xml).apply_to_string(first.parse(input))
102
102
  b = second.xslt(sheet_xml).apply_to_string(second.parse(input))
103
103
  expect(Moxml::C14n.equivalent?(a, b))
@@ -3,7 +3,7 @@
3
3
  require "spec_helper"
4
4
  require "moxml/c14n"
5
5
 
6
- CORPUS = {
6
+ C14N_DELEGATION_CORPUS = {
7
7
  "cdata content becomes escaped text" => [
8
8
  "<doc><![CDATA[a < b]]>tail</doc>",
9
9
  "<doc>a &lt; btail</doc>",
@@ -45,7 +45,7 @@ RSpec.describe "Moxml::C14n cross-adapter canonicalization" do
45
45
  context "with the #{adapter_name} adapter" do
46
46
  let(:ctx) { Moxml.new(adapter_name) }
47
47
 
48
- CORPUS.each do |label, (input, expected)|
48
+ C14N_DELEGATION_CORPUS.each do |label, (input, expected)|
49
49
  it "canonicalizes #{label}" do
50
50
  root = ctx.parse(input).root
51
51
  expect(Moxml::C14n.canonicalize_inclusive10(root)).to eq(expected)
@@ -9,7 +9,7 @@ ENVELOPED_DOC = %(<env><payload id="p1">data &amp; more</payload>#{SIG}<tail/></
9
9
  ENVELOPED_EXCLUDE = '//*[not(ancestor-or-self::*[local-name()="Signature"])]'
10
10
  SIGNATURE_XPATH = '//*[local-name()="Signature"]'
11
11
 
12
- CORPUS = [
12
+ C14N_SUBSET_CORPUS = [
13
13
  ["apex attraction + text",
14
14
  %(<a xml:lang="en"><b id="1"><c>t</c></b><d/></a>), "//b",
15
15
  %(<b id="1" xml:lang="en"></b>)],
@@ -59,7 +59,7 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
59
59
  context "with the #{adapter_name} adapter" do
60
60
  let(:ctx) { Moxml.new(adapter_name) }
61
61
 
62
- CORPUS.each do |label, xml, xpath, expected|
62
+ C14N_SUBSET_CORPUS.each do |label, xml, xpath, expected|
63
63
  next if label == "text selection" && adapter_name == :rexml
64
64
 
65
65
  # rexml's adapter xpath does not return text nodes for
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Issue #390: XHTML-shaped serialization — void elements self-closed
6
+ # (<br/>), non-void elements paired, raw text escaped, no XML
7
+ # declaration. Composes with the #354 indent knobs. Nokogiri's
8
+ # FORMAT | AS_XHTML | NO_DECLARATION save flag is the byte
9
+ # reference; canon's fixture_ready display lane rides the face.
10
+ RSpec.describe "XHTML output mode" do
11
+ let(:doc) do
12
+ Moxml.new(ENV.fetch("XHTML_ADAPTER", :leptris)).parse_html(
13
+ '<!DOCTYPE html><html><body><p>a<br>b</p><img src="x.png">' \
14
+ "<hr></body></html>",
15
+ )
16
+ end
17
+
18
+ describe "Document#to_xhtml" do
19
+ it "self-closes void elements with the XHTML slash" do
20
+ out = doc.to_xhtml
21
+ expect(out).not_to include("<br>")
22
+ expect(out).to include("<br/>")
23
+ expect(out).to include('<img src="x.png"/>')
24
+ expect(out).to include("<hr/>")
25
+ end
26
+
27
+ it "pairs non-void elements" do
28
+ out = doc.to_xhtml
29
+ expect(out).to include("<p>a<br/>b</p>")
30
+ expect(out).not_to include("<p/>")
31
+ end
32
+
33
+ it "omits the XML declaration" do
34
+ expect(doc.to_xhtml).not_to start_with("<?xml")
35
+ end
36
+
37
+ it "keeps the doctype as document content" do
38
+ expect(doc.to_xhtml).to include("<!DOCTYPE html>")
39
+ end
40
+
41
+ it "composes with the indent knobs" do
42
+ out = doc.to_xhtml(indent: 2)
43
+ expect(out).to include("\n <body>")
44
+ expect(out).to include("\n <p>a<br/>b</p>")
45
+ end
46
+
47
+ it "escapes raw text per the XML method" do
48
+ doc = Moxml.new(ENV.fetch("XHTML_ADAPTER", :leptris)).parse_html(
49
+ "<!DOCTYPE html><html><body><script>if (a < b) { x(); }</script>" \
50
+ "</body></html>",
51
+ )
52
+ out = doc.to_xhtml
53
+ expect(out).not_to include("a < b")
54
+ expect(out).to include("&lt;") if out.include?("script")
55
+ end
56
+ end
57
+
58
+ describe "Document#to_html(xhtml: true)" do
59
+ it "selects the same shape" do
60
+ expect(doc.to_html(xhtml: true)).to eq(doc.to_xhtml)
61
+ end
62
+
63
+ it "still serves the HTML shape without the flag" do
64
+ expect(doc.to_html).to include("<br>")
65
+ expect(doc.to_html).not_to include("<br/>")
66
+ end
67
+ end
68
+
69
+ describe "adapter coverage" do
70
+ it "serves the nokogiri lane (the byte reference)" do
71
+ doc = Moxml.new(:nokogiri).parse_html(
72
+ "<!DOCTYPE html><html><body><p>a<br>b</p></body></html>",
73
+ )
74
+ out = doc.to_xhtml
75
+ expect(out).to include("<br/>")
76
+ expect(out).to include("<p>a<br/>b</p>")
77
+ end
78
+ end
79
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.142
4
+ version: 0.5.143
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-18 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -560,6 +560,7 @@ files:
560
560
  - spec/moxml/text_spec.rb
561
561
  - spec/moxml/to_html_spec.rb
562
562
  - spec/moxml/version_spec.rb
563
+ - spec/moxml/xhtml_output_spec.rb
563
564
  - spec/moxml/xml_emitter_spec.rb
564
565
  - spec/moxml/xml_utils/.gitkeep
565
566
  - spec/moxml/xml_utils/encoder_spec.rb