rng 0.1.2 → 0.3.4

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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/release.yml +8 -3
  4. data/.gitignore +11 -0
  5. data/.rubocop.yml +10 -7
  6. data/.rubocop_todo.yml +229 -23
  7. data/CHANGELOG.md +317 -0
  8. data/CLAUDE.md +139 -0
  9. data/Gemfile +11 -12
  10. data/README.adoc +1538 -11
  11. data/Rakefile +11 -3
  12. data/docs/Gemfile +8 -0
  13. data/docs/_config.yml +23 -0
  14. data/docs/getting-started/index.adoc +75 -0
  15. data/docs/guides/error-handling.adoc +137 -0
  16. data/docs/guides/external-references.adoc +128 -0
  17. data/docs/guides/index.adoc +24 -0
  18. data/docs/guides/parsing-rnc.adoc +141 -0
  19. data/docs/guides/parsing-rng-xml.adoc +81 -0
  20. data/docs/guides/rng-to-rnc.adoc +101 -0
  21. data/docs/guides/validation.adoc +85 -0
  22. data/docs/index.adoc +52 -0
  23. data/docs/reference/api.adoc +126 -0
  24. data/docs/reference/cli.adoc +182 -0
  25. data/docs/understanding/architecture.adoc +58 -0
  26. data/docs/understanding/rng-vs-rnc.adoc +118 -0
  27. data/exe/rng +5 -0
  28. data/lib/rng/any_name.rb +10 -8
  29. data/lib/rng/attribute.rb +28 -26
  30. data/lib/rng/choice.rb +24 -24
  31. data/lib/rng/cli.rb +607 -0
  32. data/lib/rng/data.rb +10 -10
  33. data/lib/rng/datatype_declaration.rb +26 -0
  34. data/lib/rng/define.rb +44 -41
  35. data/lib/rng/div.rb +36 -0
  36. data/lib/rng/documentation.rb +9 -0
  37. data/lib/rng/element.rb +39 -37
  38. data/lib/rng/empty.rb +7 -7
  39. data/lib/rng/except.rb +25 -25
  40. data/lib/rng/external_ref.rb +8 -8
  41. data/lib/rng/external_ref_resolver.rb +602 -0
  42. data/lib/rng/foreign_attribute.rb +26 -0
  43. data/lib/rng/foreign_element.rb +33 -0
  44. data/lib/rng/grammar.rb +14 -12
  45. data/lib/rng/group.rb +26 -24
  46. data/lib/rng/include.rb +5 -6
  47. data/lib/rng/include_processor.rb +461 -0
  48. data/lib/rng/interleave.rb +23 -23
  49. data/lib/rng/list.rb +22 -22
  50. data/lib/rng/mixed.rb +23 -23
  51. data/lib/rng/name.rb +6 -7
  52. data/lib/rng/namespace_declaration.rb +47 -0
  53. data/lib/rng/namespaces.rb +15 -0
  54. data/lib/rng/not_allowed.rb +7 -7
  55. data/lib/rng/ns_name.rb +9 -9
  56. data/lib/rng/one_or_more.rb +23 -23
  57. data/lib/rng/optional.rb +23 -23
  58. data/lib/rng/param.rb +7 -8
  59. data/lib/rng/parent_ref.rb +8 -8
  60. data/lib/rng/parse_tree_processor.rb +695 -0
  61. data/lib/rng/pattern.rb +7 -7
  62. data/lib/rng/ref.rb +8 -8
  63. data/lib/rng/rnc_builder.rb +927 -0
  64. data/lib/rng/rnc_parser.rb +605 -305
  65. data/lib/rng/rnc_to_rng_converter.rb +1408 -0
  66. data/lib/rng/schema_preamble.rb +73 -0
  67. data/lib/rng/schema_validator.rb +1622 -0
  68. data/lib/rng/start.rb +27 -25
  69. data/lib/rng/test_suite_parser.rb +168 -0
  70. data/lib/rng/text.rb +11 -8
  71. data/lib/rng/to_rnc.rb +4 -35
  72. data/lib/rng/value.rb +6 -7
  73. data/lib/rng/version.rb +1 -1
  74. data/lib/rng/zero_or_more.rb +23 -23
  75. data/lib/rng.rb +68 -17
  76. data/rng.gemspec +18 -19
  77. data/scripts/extract_spectest_resources.rb +96 -0
  78. data/spec/fixtures/compacttest.xml +2511 -0
  79. data/spec/fixtures/external/circular_a.rng +7 -0
  80. data/spec/fixtures/external/circular_b.rng +7 -0
  81. data/spec/fixtures/external/circular_main.rng +7 -0
  82. data/spec/fixtures/external/external_ref_lib.rng +7 -0
  83. data/spec/fixtures/external/external_ref_main.rng +7 -0
  84. data/spec/fixtures/external/include_lib.rng +7 -0
  85. data/spec/fixtures/external/include_main.rng +3 -0
  86. data/spec/fixtures/external/nested_chain.rng +6 -0
  87. data/spec/fixtures/external/nested_leaf.rng +7 -0
  88. data/spec/fixtures/external/nested_mid.rng +8 -0
  89. data/spec/fixtures/metanorma/3gpp.rnc +35 -0
  90. data/spec/fixtures/metanorma/3gpp.rng +105 -0
  91. data/spec/fixtures/metanorma/basicdoc.rnc +11 -0
  92. data/spec/fixtures/metanorma/bipm.rnc +148 -0
  93. data/spec/fixtures/metanorma/bipm.rng +376 -0
  94. data/spec/fixtures/metanorma/bsi.rnc +104 -0
  95. data/spec/fixtures/metanorma/bsi.rng +332 -0
  96. data/spec/fixtures/metanorma/csa.rnc +45 -0
  97. data/spec/fixtures/metanorma/csa.rng +131 -0
  98. data/spec/fixtures/metanorma/csd.rnc +43 -0
  99. data/spec/fixtures/metanorma/csd.rng +132 -0
  100. data/spec/fixtures/metanorma/gbstandard.rnc +99 -0
  101. data/spec/fixtures/metanorma/gbstandard.rng +316 -0
  102. data/spec/fixtures/metanorma/iec.rnc +49 -0
  103. data/spec/fixtures/metanorma/iec.rng +193 -0
  104. data/spec/fixtures/metanorma/ietf.rnc +275 -0
  105. data/spec/fixtures/metanorma/ietf.rng +925 -0
  106. data/spec/fixtures/metanorma/iho.rnc +58 -0
  107. data/spec/fixtures/metanorma/iho.rng +179 -0
  108. data/spec/fixtures/metanorma/isodoc.rnc +873 -0
  109. data/spec/fixtures/metanorma/isodoc.rng +2704 -0
  110. data/spec/fixtures/metanorma/isostandard-amd.rnc +43 -0
  111. data/spec/fixtures/metanorma/isostandard-amd.rng +108 -0
  112. data/spec/fixtures/metanorma/isostandard.rnc +166 -0
  113. data/spec/fixtures/metanorma/isostandard.rng +494 -0
  114. data/spec/fixtures/metanorma/itu.rnc +122 -0
  115. data/spec/fixtures/metanorma/itu.rng +377 -0
  116. data/spec/fixtures/metanorma/m3d.rnc +41 -0
  117. data/spec/fixtures/metanorma/m3d.rng +122 -0
  118. data/spec/fixtures/metanorma/mpfd.rnc +36 -0
  119. data/spec/fixtures/metanorma/mpfd.rng +95 -0
  120. data/spec/fixtures/metanorma/nist.rnc +77 -0
  121. data/spec/fixtures/metanorma/nist.rng +216 -0
  122. data/spec/fixtures/metanorma/ogc.rnc +51 -0
  123. data/spec/fixtures/metanorma/ogc.rng +151 -0
  124. data/spec/fixtures/metanorma/reqt.rnc +6 -0
  125. data/spec/fixtures/metanorma/rsd.rnc +36 -0
  126. data/spec/fixtures/metanorma/rsd.rng +95 -0
  127. data/spec/fixtures/metanorma/un.rnc +103 -0
  128. data/spec/fixtures/metanorma/un.rng +367 -0
  129. data/spec/fixtures/rnc/base.rnc +4 -0
  130. data/spec/fixtures/rnc/grammar_with_trailing.rnc +8 -0
  131. data/spec/fixtures/rnc/main_include_trailing.rnc +3 -0
  132. data/spec/fixtures/rnc/main_with_include.rnc +5 -0
  133. data/spec/fixtures/rnc/test_augment.rnc +10 -0
  134. data/spec/fixtures/rnc/test_isodoc_simple.rnc +9 -0
  135. data/spec/fixtures/rnc/top_level_include.rnc +8 -0
  136. data/spec/fixtures/spectest_external/case_10_4.7/x +3 -0
  137. data/spec/fixtures/spectest_external/case_10_4.7/y +7 -0
  138. data/spec/fixtures/spectest_external/case_11_4.7/x +3 -0
  139. data/spec/fixtures/spectest_external/case_12_4.7/x +3 -0
  140. data/spec/fixtures/spectest_external/case_13_4.7/x +3 -0
  141. data/spec/fixtures/spectest_external/case_13_4.7/y +3 -0
  142. data/spec/fixtures/spectest_external/case_14_4.7/x +7 -0
  143. data/spec/fixtures/spectest_external/case_15_4.7/x +7 -0
  144. data/spec/fixtures/spectest_external/case_16_4.7/x +5 -0
  145. data/spec/fixtures/spectest_external/case_17_4.7/x +5 -0
  146. data/spec/fixtures/spectest_external/case_18_4.7/x +7 -0
  147. data/spec/fixtures/spectest_external/case_19_4.7/level1.rng +9 -0
  148. data/spec/fixtures/spectest_external/case_19_4.7/level2.rng +7 -0
  149. data/spec/fixtures/spectest_external/case_1_4.5/sub1/x +3 -0
  150. data/spec/fixtures/spectest_external/case_1_4.5/sub3/x +3 -0
  151. data/spec/fixtures/spectest_external/case_1_4.5/x +3 -0
  152. data/spec/fixtures/spectest_external/case_20_4.6/x +3 -0
  153. data/spec/fixtures/spectest_external/case_2_4.5/x +3 -0
  154. data/spec/fixtures/spectest_external/case_3_4.6/x +3 -0
  155. data/spec/fixtures/spectest_external/case_4_4.6/x +3 -0
  156. data/spec/fixtures/spectest_external/case_5_4.6/x +1 -0
  157. data/spec/fixtures/spectest_external/case_6_4.6/x +5 -0
  158. data/spec/fixtures/spectest_external/case_7_4.6/x +1 -0
  159. data/spec/fixtures/spectest_external/case_7_4.6/y +1 -0
  160. data/spec/fixtures/spectest_external/case_8_4.7/x +7 -0
  161. data/spec/fixtures/spectest_external/case_9_4.7/x +7 -0
  162. data/spec/fixtures/spectest_external/resources.json +149 -0
  163. data/spec/rng/advanced_rnc_spec.rb +101 -0
  164. data/spec/rng/compacttest_spec.rb +197 -0
  165. data/spec/rng/datatype_declaration_spec.rb +28 -0
  166. data/spec/rng/div_spec.rb +207 -0
  167. data/spec/rng/external_ref_resolver_spec.rb +122 -0
  168. data/spec/rng/metanorma_conversion_spec.rb +159 -0
  169. data/spec/rng/namespace_declaration_spec.rb +60 -0
  170. data/spec/rng/namespace_support_spec.rb +199 -0
  171. data/spec/rng/rnc_parser_spec.rb +498 -22
  172. data/spec/rng/rnc_roundtrip_spec.rb +96 -82
  173. data/spec/rng/rng_generation_spec.rb +288 -0
  174. data/spec/rng/roundtrip_spec.rb +342 -0
  175. data/spec/rng/schema_preamble_spec.rb +145 -0
  176. data/spec/rng/schema_spec.rb +68 -64
  177. data/spec/rng/spectest_spec.rb +168 -90
  178. data/spec/rng_spec.rb +2 -2
  179. data/spec/spec_helper.rb +7 -42
  180. metadata +141 -8
@@ -0,0 +1,602 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ # Resolves external href references in RNG schemas.
5
+ #
6
+ # This class handles two types of external references:
7
+ # 1. `<include href="uri"/>` at grammar level - merges definitions from external grammar
8
+ # 2. `<externalRef href="uri"/>` at pattern level - replaces ref with external pattern
9
+ #
10
+ # @example Parse with external resolution
11
+ # Rng.parse(rng_xml, location: "/path/to/schema.rng", resolve_external: true)
12
+ #
13
+ class ExternalRefResolver
14
+ # Error raised when external reference resolution fails
15
+ class ExternalRefResolutionError < Error
16
+ attr_reader :href, :cause
17
+
18
+ def initialize(message, href: nil, cause: nil)
19
+ super(message)
20
+ @href = href
21
+ @cause = cause
22
+ end
23
+ end
24
+
25
+ # Initialize the resolver
26
+ #
27
+ # @param grammar [Grammar] The grammar to resolve external refs in
28
+ # @param location [String, nil] Base location for resolving relative hrefs
29
+ def initialize(grammar, location: nil)
30
+ @grammar = grammar
31
+ @location = location
32
+ end
33
+
34
+ # Resolve all external references in the grammar
35
+ #
36
+ # @return [Grammar] The resolved grammar
37
+ def resolve
38
+ visited_files = Set.new
39
+ build_resolved_grammar(@grammar, @location, visited_files)
40
+ end
41
+
42
+ private
43
+
44
+ # Build a new resolved grammar (doesn't modify original)
45
+ #
46
+ # @param grammar [Grammar] Grammar to resolve
47
+ # @param location [String, nil] Base location for href resolution
48
+ # @param visited_files [Set] Set of visited file paths for cycle detection
49
+ # @return [Grammar] New grammar with resolved external refs
50
+ def build_resolved_grammar(grammar, location, visited_files)
51
+ return grammar unless grammar
52
+
53
+ base_dir = location ? File.dirname(File.expand_path(location)) : Dir.pwd
54
+
55
+ # Create new grammar with namespace and datatypeLibrary
56
+ new_grammar = Grammar.new
57
+ new_grammar.ns = grammar.ns if grammar.ns && grammar.ns != :omitted
58
+ new_grammar.datatypeLibrary = grammar.datatypeLibrary if grammar.datatypeLibrary
59
+
60
+ # Process includes and build the new grammar's content
61
+ include_results = resolve_includes!(grammar, base_dir, visited_files)
62
+
63
+ if include_results.empty?
64
+ # No includes - copy original content with externalRef resolution
65
+ copy_grammar_content!(new_grammar, grammar, base_dir, visited_files)
66
+ else
67
+ # Has includes - merge the resolved included content
68
+ include_results.each do |resolved|
69
+ merge_grammar_content!(new_grammar, resolved, base_dir, visited_files)
70
+ end
71
+ end
72
+
73
+ new_grammar
74
+ end
75
+
76
+ # Copy grammar content when there are no includes
77
+ #
78
+ # @param new_grammar [Grammar] Target grammar to copy into
79
+ # @param grammar [Grammar] Source grammar
80
+ # @param base_dir [String] Base directory
81
+ # @param visited_files [Set] Set of visited file paths
82
+ def copy_grammar_content!(new_grammar, grammar, base_dir, visited_files)
83
+ # Copy start pattern
84
+ if grammar.start && !grammar.start.empty?
85
+ new_grammar.start = grammar.start.filter_map do |s|
86
+ resolved = resolve_pattern(deep_dup(s), base_dir, visited_files)
87
+ clear_element_order!(resolved)
88
+ resolved
89
+ end
90
+ end
91
+
92
+ # Copy define patterns
93
+ if grammar.define && !grammar.define.empty?
94
+ new_grammar.define = grammar.define.filter_map do |d|
95
+ resolved = resolve_pattern(deep_dup(d), base_dir, visited_files)
96
+ clear_element_order!(resolved)
97
+ resolved
98
+ end
99
+ end
100
+
101
+ # Copy div elements
102
+ return unless grammar.div && !grammar.div.empty?
103
+
104
+ new_grammar.div = grammar.div.filter_map do |div|
105
+ resolved_div = resolve_div(deep_dup(div), base_dir, visited_files)
106
+ clear_element_order!(resolved_div)
107
+ resolved_div
108
+ end
109
+ end
110
+
111
+ # Recursively clear element_order on an object and its children
112
+ # This forces to_xml to use Ruby attributes instead of stale XML nodes
113
+ #
114
+ # @param obj [Object] Object to clear element_order on
115
+ def clear_element_order!(obj)
116
+ return obj unless obj
117
+
118
+ obj.instance_variable_set(:@element_order, nil)
119
+
120
+ # Recursively clear on children based on type
121
+ case obj
122
+ when Start
123
+ %i[element choice group interleave mixed optional zeroOrMore
124
+ oneOrMore text empty value data list parentRef notAllowed grammar].each do |attr|
125
+ children = obj.send(attr)
126
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
127
+
128
+ Array(children).each { |c| clear_element_order!(c) }
129
+ end
130
+ when Define
131
+ %i[ref element choice group interleave mixed optional zeroOrMore
132
+ oneOrMore text empty value data list notAllowed attribute grammar].each do |attr|
133
+ children = obj.send(attr)
134
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
135
+
136
+ Array(children).each { |c| clear_element_order!(c) }
137
+ end
138
+ when Element
139
+ %i[attribute ref choice group interleave mixed optional zeroOrMore
140
+ oneOrMore anyName text empty value data list notAllowed element grammar].each do |attr|
141
+ children = obj.send(attr)
142
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
143
+
144
+ Array(children).each { |c| clear_element_order!(c) }
145
+ end
146
+ when Group
147
+ %i[attribute ref choice group interleave mixed optional zeroOrMore
148
+ oneOrMore text empty value data list notAllowed].each do |attr|
149
+ children = obj.send(attr)
150
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
151
+
152
+ Array(children).each { |c| clear_element_order!(c) }
153
+ end
154
+ when Div
155
+ obj.div&.each { |d| clear_element_order!(d) }
156
+ %i[start define].each do |attr|
157
+ children = obj.send(attr)
158
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
159
+
160
+ Array(children).each { |c| clear_element_order!(c) }
161
+ end
162
+ end
163
+
164
+ obj
165
+ end
166
+
167
+ # Resolve includes and return array of resolved grammars/content
168
+ #
169
+ # @param grammar [Grammar] Grammar containing includes
170
+ # @param base_dir [String] Base directory for relative path resolution
171
+ # @param visited_files [Set] Set of visited file paths
172
+ # @return [Array] Array of content to merge (grammars or defines)
173
+ def resolve_includes!(grammar, base_dir, visited_files)
174
+ return [] unless grammar.include && !grammar.include.empty?
175
+
176
+ results = []
177
+ grammar.include.each do |include_directive|
178
+ next unless include_directive.href
179
+
180
+ begin
181
+ resolved = resolve_include(include_directive, base_dir, visited_files)
182
+ results << resolved if resolved
183
+ rescue ExternalRefResolutionError => e
184
+ warn "Warning: Failed to resolve include '#{include_directive.href}': #{e.message}" if ENV['RNG_VERBOSE']
185
+ end
186
+ end
187
+ results
188
+ end
189
+
190
+ # Merge content into new grammar
191
+ #
192
+ # @param new_grammar [Grammar] Target grammar to merge into
193
+ # @param resolved [Grammar] Resolved included grammar
194
+ # @param base_dir [String] Base directory
195
+ # @param visited_files [Set] Set of visited file paths
196
+ def merge_grammar_content!(new_grammar, resolved, _base_dir, _visited_files)
197
+ return unless resolved
198
+
199
+ # Merge datatypeLibrary if not set
200
+ new_grammar.datatypeLibrary = resolved.datatypeLibrary if new_grammar.datatypeLibrary.nil? || new_grammar.datatypeLibrary == :omitted
201
+
202
+ # Merge start pattern if new_grammar has no start
203
+ if (new_grammar.start.nil? || new_grammar.start.empty?) && resolved.start && !resolved.start.empty?
204
+ new_grammar.start = resolved.start.map do |s|
205
+ deep_dup(s)
206
+ end
207
+ end
208
+
209
+ # Merge definitions
210
+ return unless resolved.define
211
+
212
+ resolved.define.each do |ext_define|
213
+ add_or_replace_define(new_grammar, deep_dup(ext_define))
214
+ end
215
+ end
216
+
217
+ # Deep dup a pattern object
218
+ #
219
+ # Uses recursive copying instead of Marshal to handle objects
220
+ # containing Nokogiri::XML::Element nodes (stored in element_order
221
+ # by lutaml-model), which cannot be serialized by Marshal.
222
+ #
223
+ # @param obj [Object] Object to deep copy
224
+ # @return [Object] Deep copy of object
225
+ def deep_dup(obj)
226
+ case obj
227
+ when Lutaml::Model::Serializable
228
+ result = obj.class.new
229
+ obj.class.attributes.each_key do |attr_name|
230
+ value = obj.public_send(attr_name)
231
+ result.public_send(:"#{attr_name}=", deep_dup(value))
232
+ end
233
+ result
234
+ when Array
235
+ obj.map { |o| deep_dup(o) }
236
+ when Hash
237
+ obj.each_with_object({}) { |(k, v), h| h[deep_dup(k)] = deep_dup(v) }
238
+ when NilClass, Symbol, Numeric, TrueClass, FalseClass
239
+ obj
240
+ else
241
+ obj.dup
242
+ end
243
+ end
244
+
245
+ # Resolve a single include directive
246
+ #
247
+ # @param include_directive [Include] Include element with href
248
+ # @param base_dir [String] Base directory for resolution
249
+ # @param visited_files [Set] Set of visited file paths
250
+ # @return [Grammar, nil] Resolved grammar or nil on error
251
+ def resolve_include(include_directive, base_dir, visited_files)
252
+ href = include_directive.href
253
+ resolved_path = resolve_href(href, base_dir, visited_files)
254
+
255
+ # Mark this file as visited BEFORE processing to detect circular refs
256
+ visited_files << resolved_path
257
+
258
+ # Parse the external grammar file
259
+ external_grammar = Grammar.from_xml(File.read(resolved_path))
260
+
261
+ # Recursively resolve external refs in the included grammar
262
+ build_resolved_grammar(external_grammar, resolved_path, visited_files)
263
+ end
264
+
265
+ # Add or replace a definition in grammar
266
+ #
267
+ # @param grammar [Grammar] Grammar to modify
268
+ # @param define [Define] Definition to add or replace
269
+ def add_or_replace_define(grammar, define)
270
+ return unless define&.name
271
+
272
+ grammar.define ||= []
273
+ existing = grammar.define.find { |d| d.name == define.name }
274
+ if existing
275
+ # Replace existing definition
276
+ idx = grammar.define.index(existing)
277
+ grammar.define[idx] = define
278
+ else
279
+ # Add new definition
280
+ grammar.define << define
281
+ end
282
+ end
283
+
284
+ # Resolve external refs in a div element
285
+ #
286
+ # @param div [Div] Div element
287
+ # @param base_dir [String] Base directory for resolution
288
+ # @param visited_files [Set] Set of visited file paths
289
+ def resolve_div(div, base_dir, visited_files)
290
+ return unless div
291
+
292
+ # Resolve includes within div
293
+ div.div&.each do |nested_div|
294
+ resolve_div(nested_div, base_dir, visited_files)
295
+ end
296
+
297
+ div.start&.each { |s| resolve_pattern(s, base_dir, visited_files) if s }
298
+
299
+ return unless div.define
300
+
301
+ div.define.each { |d| resolve_pattern(d, base_dir, visited_files) if d }
302
+ end
303
+
304
+ # Resolve external refs in a pattern
305
+ #
306
+ # @param pattern [Object] Pattern object (Start, Define, Element, Group, etc.)
307
+ # @param base_dir [String] Base directory for resolution
308
+ # @param visited_files [Set] Set of visited file paths
309
+ def resolve_pattern(pattern, base_dir, visited_files)
310
+ return unless pattern
311
+
312
+ # Handle Element pattern
313
+ case pattern
314
+ when Element
315
+ resolve_element_external_ref!(pattern, base_dir, visited_files)
316
+
317
+ # Recursively resolve in Element's children
318
+ resolve_element_children!(pattern, base_dir, visited_files)
319
+ # Handle Group pattern
320
+ when Group
321
+ resolve_group_external_ref!(pattern, base_dir, visited_files)
322
+
323
+ # Recursively resolve in Group's children
324
+ resolve_group_children!(pattern, base_dir, visited_files)
325
+ # Handle Define pattern
326
+ when Define
327
+ resolve_define_children!(pattern, base_dir, visited_files)
328
+ # Handle Start pattern
329
+ when Start
330
+ resolve_start_children!(pattern, base_dir, visited_files)
331
+ when Start
332
+ resolve_pattern_children!(pattern, base_dir, visited_files)
333
+ end
334
+
335
+ pattern
336
+ end
337
+
338
+ # Resolve external ref in an Element
339
+ #
340
+ # @param element [Element] Element with external_ref
341
+ # @param base_dir [String] Base directory
342
+ # @param visited_files [Set] Set of visited file paths
343
+ def resolve_element_external_ref!(element, base_dir, visited_files)
344
+ return unless element.external_ref
345
+
346
+ href = element.external_ref.href
347
+ return unless href
348
+
349
+ begin
350
+ resolved_path = resolve_href(href, base_dir, visited_files)
351
+ external_grammar = Grammar.from_xml(File.read(resolved_path))
352
+ resolved_grammar = build_resolved_grammar(external_grammar, resolved_path, visited_files)
353
+
354
+ # Get the start pattern from the external grammar
355
+ if resolved_grammar.start && !resolved_grammar.start.empty?
356
+ start_pattern = resolved_grammar.start.first
357
+
358
+ # Copy attributes from external ref's ns to override namespace
359
+ if element.external_ref.ns && !element.external_ref.ns.empty? &&
360
+ element.external_ref.ns != :omitted && element.external_ref.ns != :empty && element.external_ref.ns != :empty
361
+ start_pattern.ns = element.external_ref.ns
362
+ end
363
+
364
+ # Replace the external_ref with the start pattern's content
365
+ replace_element_external_ref!(element, start_pattern)
366
+ end
367
+ rescue ExternalRefResolutionError => e
368
+ warn "Warning: Failed to resolve externalRef '#{href}': #{e.message}" if ENV['RNG_VERBOSE']
369
+ rescue StandardError => e
370
+ warn "Warning: Error resolving externalRef '#{href}': #{e.message}" if ENV['RNG_VERBOSE']
371
+ end
372
+ end
373
+
374
+ # Replace external_ref in element with resolved pattern
375
+ #
376
+ # @param element [Element] Element containing external_ref
377
+ # @param start_pattern [Object] Resolved start pattern
378
+ def replace_element_external_ref!(element, start_pattern)
379
+ # Clear external_ref
380
+ element.external_ref = nil
381
+
382
+ # Copy all pattern content from start_pattern to element
383
+ copy_pattern_content(element, start_pattern)
384
+ end
385
+
386
+ # Copy pattern content from source to target
387
+ #
388
+ # @param target [Object] Target pattern (Element, Group, etc.)
389
+ # @param source [Object] Source pattern
390
+ def copy_pattern_content(target, source)
391
+ case source
392
+ when Start
393
+ # Start pattern - copy its content (element, choice, group, etc.)
394
+ copy_children(target, source, %i[element choice group interleave mixed optional
395
+ zeroOrMore oneOrMore text empty value data
396
+ list parentRef notAllowed grammar])
397
+ when Element
398
+ target.attr_name = source.attr_name if source.attr_name
399
+ target.ns = source.ns if source.ns
400
+ copy_children(target, source, %i[attribute ref choice group interleave mixed
401
+ optional zeroOrMore oneOrMore anyName
402
+ text empty value data list notAllowed element])
403
+ when Group
404
+ copy_children(target, source, %i[attribute ref choice group interleave mixed
405
+ optional zeroOrMore oneOrMore text empty
406
+ value data list notAllowed externalRef])
407
+ when Choice
408
+ target.choice = source.choice if source.choice
409
+ when Group
410
+ target.group = source.group if source.group
411
+ when Interleave
412
+ target.interleave = source.interleave if source.interleave
413
+ when Optional
414
+ target.optional = source.optional if source.optional
415
+ when ZeroOrMore
416
+ target.zeroOrMore = source.zeroOrMore if source.zeroOrMore
417
+ when OneOrMore
418
+ target.oneOrMore = source.oneOrMore if source.oneOrMore
419
+ when Mixed
420
+ target.mixed = source.mixed if source.mixed
421
+ when Text
422
+ target.text = source.text if source.text
423
+ when Empty
424
+ target.empty = source.empty if source.empty
425
+ when Value
426
+ target.value = source.value if source.value
427
+ when Data
428
+ target.data = source.data if source.data
429
+ when List
430
+ target.list = source.list if source.list
431
+ when NotAllowed
432
+ target.notAllowed = source.notAllowed if source.notAllowed
433
+ end
434
+ end
435
+
436
+ # Copy child collections from source to target
437
+ #
438
+ # @param target [Object] Target pattern
439
+ # @param source [Object] Source pattern
440
+ # @param attrs [Array<Symbol>] Attribute names to copy
441
+ def copy_children(target, source, attrs)
442
+ attrs.each do |attr|
443
+ value = source.send(attr)
444
+ next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
445
+
446
+ target.send("#{attr}=", value) if target.respond_to?("#{attr}=")
447
+ end
448
+ end
449
+
450
+ # Resolve external ref in a Group
451
+ #
452
+ # @param group [Group] Group with externalRef
453
+ # @param base_dir [String] Base directory
454
+ # @param visited_files [Set] Set of visited file paths
455
+ def resolve_group_external_ref!(group, base_dir, visited_files)
456
+ return unless group.externalRef
457
+
458
+ href = group.externalRef.href
459
+ return unless href
460
+
461
+ begin
462
+ resolved_path = resolve_href(href, base_dir, visited_files)
463
+ external_grammar = Grammar.from_xml(File.read(resolved_path))
464
+ resolved_grammar = build_resolved_grammar(external_grammar, resolved_path, visited_files)
465
+
466
+ if resolved_grammar.start && !resolved_grammar.start.empty?
467
+ start_pattern = resolved_grammar.start.first
468
+
469
+ # Handle ns attribute override
470
+ if group.externalRef.ns && !group.externalRef.ns.empty? &&
471
+ group.externalRef.ns != :omitted && group.externalRef.ns != :empty && group.externalRef.ns != :empty
472
+ start_pattern.ns = group.externalRef.ns
473
+ end
474
+
475
+ replace_group_external_ref!(group, start_pattern)
476
+ end
477
+ rescue ExternalRefResolutionError => e
478
+ warn "Warning: Failed to resolve externalRef '#{href}': #{e.message}" if ENV['RNG_VERBOSE']
479
+ rescue StandardError => e
480
+ warn "Warning: Error resolving externalRef '#{href}': #{e.message}" if ENV['RNG_VERBOSE']
481
+ end
482
+ end
483
+
484
+ # Replace externalRef in group with resolved pattern
485
+ #
486
+ # @param group [Group] Group containing externalRef
487
+ # @param start_pattern [Object] Resolved start pattern
488
+ def replace_group_external_ref!(group, start_pattern)
489
+ group.externalRef = nil
490
+ copy_pattern_content(group, start_pattern)
491
+ end
492
+
493
+ # Recursively resolve children of an Element
494
+ #
495
+ # @param element [Element] Element to resolve children in
496
+ # @param base_dir [String] Base directory
497
+ # @param visited_files [Set] Set of visited file paths
498
+ def resolve_element_children!(element, base_dir, visited_files)
499
+ %i[attribute ref choice group interleave mixed optional zeroOrMore
500
+ oneOrMore anyName text empty value data list notAllowed element grammar].each do |attr|
501
+ children = element.send(attr)
502
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
503
+
504
+ Array(children).each do |child|
505
+ resolve_pattern(child, base_dir, visited_files)
506
+ end
507
+ end
508
+ end
509
+
510
+ # Recursively resolve children of a Group
511
+ #
512
+ # @param group [Group] Group to resolve children in
513
+ # @param base_dir [String] Base directory
514
+ # @param visited_files [Set] Set of visited file paths
515
+ def resolve_group_children!(group, base_dir, visited_files)
516
+ %i[attribute ref choice group interleave mixed optional zeroOrMore
517
+ oneOrMore text empty value data list notAllowed].each do |attr|
518
+ children = group.send(attr)
519
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
520
+
521
+ Array(children).each do |child|
522
+ resolve_pattern(child, base_dir, visited_files)
523
+ end
524
+ end
525
+ end
526
+
527
+ # Recursively resolve children of a Start pattern
528
+ #
529
+ # @param start [Start] Start pattern to resolve children in
530
+ # @param base_dir [String] Base directory
531
+ # @param visited_files [Set] Set of visited file paths
532
+ def resolve_start_children!(start, base_dir, visited_files)
533
+ # Start has: element, choice, group, interleave, mixed, optional,
534
+ # zeroOrMore, oneOrMore, text, empty, value, data, list,
535
+ # parentRef, notAllowed, grammar
536
+ %i[element choice group interleave mixed optional zeroOrMore
537
+ oneOrMore text empty value data list parentRef notAllowed grammar].each do |attr|
538
+ children = start.send(attr)
539
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
540
+
541
+ Array(children).each do |child|
542
+ resolve_pattern(child, base_dir, visited_files)
543
+ end
544
+ end
545
+ end
546
+
547
+ # Recursively resolve children of a Define
548
+ #
549
+ # @param define [Define] Define to resolve children in
550
+ # @param base_dir [String] Base directory
551
+ # @param visited_files [Set] Set of visited file paths
552
+ def resolve_define_children!(define, base_dir, visited_files)
553
+ # Define has: ref, element, choice, group, interleave, mixed, optional,
554
+ # zeroOrMore, oneOrMore, text, empty, value, data, list,
555
+ # notAllowed, attribute, grammar
556
+ %i[ref element choice group interleave mixed optional zeroOrMore
557
+ oneOrMore text empty value data list notAllowed attribute grammar].each do |attr|
558
+ children = define.send(attr)
559
+ next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
560
+
561
+ Array(children).each do |child|
562
+ resolve_pattern(child, base_dir, visited_files)
563
+ end
564
+ end
565
+ end
566
+
567
+ # Resolve href to absolute path with cycle detection
568
+ #
569
+ # @param href [String] Relative or absolute href
570
+ # @param base_dir [String] Base directory for relative resolution
571
+ # @param visited_files [Set] Set of visited file paths for cycle detection
572
+ # @return [String] Absolute path
573
+ def resolve_href(href, base_dir, visited_files)
574
+ # Resolve relative to base_dir
575
+ resolved = if base_dir && !base_dir.empty?
576
+ File.expand_path(href, base_dir)
577
+ else
578
+ File.expand_path(href)
579
+ end
580
+
581
+ # Check for cycle
582
+ if visited_files.include?(resolved)
583
+ raise ExternalRefResolutionError.new(
584
+ "Circular reference detected: #{href}",
585
+ href: href,
586
+ cause: :circular
587
+ )
588
+ end
589
+
590
+ # Check file exists
591
+ unless File.exist?(resolved)
592
+ raise ExternalRefResolutionError.new(
593
+ "External file not found: #{href}",
594
+ href: href,
595
+ cause: Errno::ENOENT
596
+ )
597
+ end
598
+
599
+ resolved
600
+ end
601
+ end
602
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ # Represents a foreign attribute (from a non-RELAX NG namespace)
5
+ # Used in annotation blocks like [eg:foo = "value"]
6
+ class ForeignAttribute < Lutaml::Model::Serializable
7
+ attribute :name, :string
8
+ attribute :namespace, :string
9
+ attribute :value, :string
10
+
11
+ xml do
12
+ element 'attribute'
13
+ namespace ::Rng::Namespaces::RngNamespace
14
+
15
+ map_attribute 'name', to: :name
16
+ map_attribute 'namespace', to: :namespace
17
+ map_content to: :value
18
+ end
19
+
20
+ def initialize(name: nil, namespace: nil, value: nil)
21
+ @name = name
22
+ @namespace = namespace
23
+ @value = value
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ # Represents a foreign element (from a non-RELAX NG namespace)
5
+ # Used in annotation blocks like [eg:foo [ "content" ]]
6
+ class ForeignElement < Lutaml::Model::Serializable
7
+ attribute :name, :string
8
+ attribute :namespace, :string
9
+ attribute :content, :string
10
+ attribute :attributes, ForeignAttribute, collection: true
11
+ attribute :elements, ForeignElement, collection: true
12
+
13
+ xml do
14
+ element 'element'
15
+ namespace ::Rng::Namespaces::RngNamespace
16
+
17
+ map_attribute 'name', to: :name
18
+ map_attribute 'namespace', to: :namespace
19
+ map_content to: :content
20
+ map_element 'attribute', to: :attributes
21
+ map_element 'element', to: :elements
22
+ end
23
+
24
+ def initialize(name: nil, namespace: nil, content: nil,
25
+ attributes: [], elements: [])
26
+ @name = name
27
+ @namespace = namespace
28
+ @content = content
29
+ @attributes = attributes
30
+ @elements = elements
31
+ end
32
+ end
33
+ end