moxml 0.5.131 → 0.5.132

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: 2de6081e549de3acc9fe0fe2697d0f44e137673eaa311dc9beec4de6346dfb54
4
- data.tar.gz: 026a76efc77132825449057e48559626e3f3ec4166d54fb5ea817d51accb830e
3
+ metadata.gz: 6e9c4fec61c2e21eec26368889b888c8cdf1b3c553161bafc0334861cef93529
4
+ data.tar.gz: 0d32dcd44ab21957f09078af75e147005d1da6a67a72a17626d488757c6a305c
5
5
  SHA512:
6
- metadata.gz: 369b3b0ca0527711f87e4b18f9b268c503612c9aed6aa8ea4fcfcb6dd9ad835be5cbb80b9ff00b0606b3df908bb150a0ef22bba6041157bfdb41a0bd8264a153
7
- data.tar.gz: a86491ce96b541420ddf36fc2cbe1324c2f283d212d0271cb2eb61129bca15258dd7d92dbe7a022c6e3e4055140fb395111c96e8518ebd5b5d882a036186ae33
6
+ metadata.gz: 1d8da6f131247d4a9b61bc119e85ec19096bdc8769e35ec545f35da657d23556fb22b9d7b8e00c68684aa875a46134fb3d8845526d123f7d4379ef174ff70a8e
7
+ data.tar.gz: a8d0a826db5dd0de0febc05189048a86f7c23a2f8cb028395d6add3500f331b18d104498a1880c7e28e8786b81aab8beb2b9af8ae9e101b1c13755a3fdbe03ac
data/lib/moxml/builder.rb CHANGED
@@ -7,7 +7,7 @@ module Moxml
7
7
  attr_reader :document
8
8
  alias_method :doc, :document
9
9
 
10
- def initialize(context)
10
+ def initialize(context, strict: nil)
11
11
  @context = context
12
12
  @current = @document = context.create_document
13
13
  @namespaces = {}
@@ -22,6 +22,22 @@ module Moxml
22
22
  @face_counts = Hash.new(0)
23
23
  @face_deferred = []
24
24
  @face_entity_markers = false
25
+ # #371: strict mode — unknown methods raise instead of
26
+ # silently becoming empty elements. Per-build override
27
+ # (Context#build(strict:)), else the context's
28
+ # config.builder_strict.
29
+ @strict = strict.nil? ? context.config.builder_strict : strict
30
+ end
31
+
32
+ def strict?
33
+ @strict == true
34
+ end
35
+
36
+ # The tag-creation face NamespacedTags calls — the private
37
+ # create_element_node stays private.
38
+ def create_tag(tag_name, attrs = {}, text_content: nil, block: nil)
39
+ create_element_node(tag_name, attrs, text_content: text_content,
40
+ block: block, eval_block: false)
25
41
  end
26
42
 
27
43
  def build(&block)
@@ -30,6 +46,61 @@ module Moxml
30
46
  @document
31
47
  end
32
48
 
49
+ # Namespace-scoped tag syntax (#371, Nokogiri's xml["p"].tag
50
+ # parity): xml["w"].p creates <w:p>. The proxy creates through
51
+ # the same face as plain method_missing — caller-context
52
+ # blocks, text/attributes arguments, trailing-underscore
53
+ # escape — and honors the strict mode.
54
+ def [](namespace_prefix)
55
+ raise ArgumentError, "namespace prefix required" if namespace_prefix.nil? || namespace_prefix.to_s.empty?
56
+
57
+ NamespacedTags.new(self, namespace_prefix.to_s)
58
+ end
59
+
60
+ # Tag proxy bound to one namespace prefix. method_missing maps
61
+ # tag names to prefixed element creation; explicit Builder
62
+ # methods (text, element, ...) stay reachable by holding the
63
+ # builder — the proxy is tags-only by design.
64
+ class NamespacedTags
65
+ def initialize(builder, prefix)
66
+ @builder = builder
67
+ @prefix = prefix
68
+ end
69
+
70
+ def [](nested_prefix)
71
+ NamespacedTags.new(@builder, "#{@prefix}:#{nested_prefix}")
72
+ end
73
+
74
+ def method_missing(method_name, *args, &block)
75
+ # Trailing-underscore escape, same as plain Builder tags
76
+ # (type_ → type under the prefix).
77
+ tag = "#{@prefix}:#{method_name.to_s.chomp('_')}"
78
+ if @builder.strict?
79
+ raise NoMethodError,
80
+ "builder strict mode: unknown tag #{tag.inspect} " \
81
+ "would have silently created an empty element (#371)"
82
+ end
83
+
84
+ text_content = args.first.is_a?(String) ? args.shift : nil
85
+ attrs = args.first.is_a?(Hash) ? args.shift : {}
86
+ unless args.empty?
87
+ raise ArgumentError,
88
+ "unexpected arguments for #{tag}: #{args.inspect}"
89
+ end
90
+ if text_content && block
91
+ raise ArgumentError,
92
+ "#{tag}: cannot combine text content with a block"
93
+ end
94
+
95
+ @builder.create_tag(tag, attrs,
96
+ text_content: text_content, block: block)
97
+ end
98
+
99
+ def respond_to_missing?(_method_name, _include_private = false)
100
+ true
101
+ end
102
+ end
103
+
33
104
  # Element ops inside a face-accumulated subtree cannot touch the
34
105
  # tree yet (nothing exists until the flush): PIs ride the raw
35
106
  # hatch in exact position; namespace/entity/declaration/doctype
@@ -139,6 +210,13 @@ module Moxml
139
210
  def method_missing(method_name, *args, &block)
140
211
  return super if RESERVED_METHOD_PATTERN.match?(method_name.to_s)
141
212
 
213
+ if @strict
214
+ raise NoMethodError,
215
+ "builder strict mode: unknown method #{method_name.inspect} " \
216
+ "would have silently created an empty <#{method_name}> element " \
217
+ "(#371); call a Builder method or use the explicit element face"
218
+ end
219
+
142
220
  text_content = args.first.is_a?(String) ? args.shift : nil
143
221
  attrs = args.first.is_a?(Hash) ? args.shift : {}
144
222
 
data/lib/moxml/config.rb CHANGED
@@ -114,6 +114,7 @@ module Moxml
114
114
 
115
115
  attr_reader :adapter_name, :default_line_ending
116
116
  attr_accessor :strict_parsing,
117
+ :builder_strict,
117
118
  :default_encoding,
118
119
  :default_indent,
119
120
  :restore_entities,
@@ -173,6 +174,11 @@ module Moxml
173
174
  @entity_provider = nil
174
175
  @namespace_validation_mode = :strict
175
176
  @entity_restoration_mode = :lenient
177
+ # #371: Builder method_missing creates elements for unknown
178
+ # methods (the DSL contract). builder_strict flips that to
179
+ # raising NoMethodError — external helper calls stop
180
+ # silently becoming empty elements.
181
+ @builder_strict = false
176
182
  @entity_mode = :expand
177
183
  end
178
184
 
data/lib/moxml/context.rb CHANGED
@@ -275,8 +275,11 @@ module Moxml
275
275
  config.adapter.sax_parse(xml, handler)
276
276
  end
277
277
 
278
- def build(&block)
279
- Builder.new(self).build(&block)
278
+ # strict: (issue #371) flips unknown method_missing calls to
279
+ # NoMethodError instead of silently creating empty elements —
280
+ # nil defers to config.builder_strict.
281
+ def build(strict: nil, &block)
282
+ Builder.new(self, strict: strict).build(&block)
280
283
  end
281
284
 
282
285
  # Frozen serialization defaults, memoized per context — Node#to_xml
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.131"
4
+ VERSION = "0.5.132"
5
5
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # #371: Builder sharp edges — the namespace-scoped tag proxy
6
+ # (Nokogiri's xml["p"].tag parity) and the strict mode where
7
+ # unknown methods raise instead of silently becoming empty
8
+ # elements.
9
+ RSpec.describe "Builder namespaces + strict mode (#371)" do
10
+ describe "Builder#[] namespace-scoped tags" do
11
+ it "creates prefixed elements with text and attributes" do
12
+ doc = Moxml.new.build do
13
+ element("root") do
14
+ self["w"].p("text", "a" => 1)
15
+ end
16
+ end
17
+ expect(doc.root.children.first.name).to eq("w:p")
18
+ expect(doc.to_xml).to include(%(<w:p a="1">text</w:p>))
19
+ end
20
+
21
+ it "nests blocks in the caller context" do
22
+ doc = Moxml.new.build do
23
+ element("root") do
24
+ self["w"].sub { self["w"].deep("x") }
25
+ end
26
+ end
27
+ sub = doc.root.children.first
28
+ expect(sub.name).to eq("w:sub")
29
+ expect(sub.children.first.name).to eq("w:deep")
30
+ expect(sub.children.first.text).to eq("x")
31
+ end
32
+
33
+ it "strips the trailing-underscore escape under the prefix" do
34
+ doc = Moxml.new.build do
35
+ element("root") { self["epub"].type_ }
36
+ end
37
+ expect(doc.to_xml).to include("<epub:type>")
38
+ end
39
+
40
+ it "composes prefixes through nested []" do
41
+ doc = Moxml.new.build do
42
+ element("root") { self["w"]["ext"].node }
43
+ end
44
+ expect(doc.to_xml).to include("<w:ext:node>")
45
+ end
46
+
47
+ it "rejects a missing prefix" do
48
+ expect { Moxml.new.build { self[nil] } }
49
+ .to raise_error(ArgumentError, /prefix/)
50
+ end
51
+ end
52
+
53
+ describe "strict mode" do
54
+ it "raises NoMethodError for unknown methods (the silent-corruption fix)" do
55
+ expect do
56
+ Moxml.new.build(strict: true) do
57
+ element("root") { build_chart_type_unused }
58
+ end
59
+ end.to raise_error(NoMethodError, /build_chart_type_unused/)
60
+ end
61
+
62
+ it "raises for unknown namespaced tags too" do
63
+ expect do
64
+ Moxml.new.build(strict: true) do
65
+ element("root") { self["w"].chart_helper }
66
+ end
67
+ end.to raise_error(NoMethodError, /w:chart_helper/)
68
+ end
69
+
70
+ it "still builds known tags under strict" do
71
+ doc = Moxml.new.build(strict: true) do
72
+ element("root") { text "ok" }
73
+ end
74
+ expect(doc.to_xml).to include("<root>ok</root>")
75
+ end
76
+
77
+ it "defaults off and honors config.builder_strict" do
78
+ expect(Moxml.new.config.builder_strict).to be(false)
79
+ ctx = Moxml.new
80
+ ctx.config.builder_strict = true
81
+ expect do
82
+ ctx.build { element("root") { helperish } }
83
+ end.to raise_error(NoMethodError)
84
+ end
85
+
86
+ it "a per-build false overrides a strict config" do
87
+ ctx = Moxml.new
88
+ ctx.config.builder_strict = true
89
+ doc = ctx.build(strict: false) do
90
+ element("root") { helperish }
91
+ end
92
+ expect(doc.to_xml).to include("<helperish>")
93
+ end
94
+ end
95
+ 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.131
4
+ version: 0.5.132
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-12 00:00:00.000000000 Z
11
+ date: 2026-10-13 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
@@ -473,6 +473,7 @@ files:
473
473
  - spec/moxml/at_search_spec.rb
474
474
  - spec/moxml/attribute_resolver_spec.rb
475
475
  - spec/moxml/attribute_spec.rb
476
+ - spec/moxml/builder_namespaces_spec.rb
476
477
  - spec/moxml/builder_spec.rb
477
478
  - spec/moxml/c14n/adapter_delegation_spec.rb
478
479
  - spec/moxml/c14n/api_spec.rb