rng 0.1.1 → 0.3.3

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