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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/release.yml +8 -3
- data/.gitignore +11 -0
- data/.rubocop.yml +11 -6
- data/.rubocop_todo.yml +270 -0
- data/CHANGELOG.md +317 -0
- data/CLAUDE.md +139 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +11 -10
- data/README.adoc +1929 -0
- data/Rakefile +11 -3
- data/docs/Gemfile +8 -0
- data/docs/_config.yml +23 -0
- data/docs/getting-started/index.adoc +75 -0
- data/docs/guides/error-handling.adoc +137 -0
- data/docs/guides/external-references.adoc +128 -0
- data/docs/guides/index.adoc +24 -0
- data/docs/guides/parsing-rnc.adoc +141 -0
- data/docs/guides/parsing-rng-xml.adoc +81 -0
- data/docs/guides/rng-to-rnc.adoc +101 -0
- data/docs/guides/validation.adoc +85 -0
- data/docs/index.adoc +52 -0
- data/docs/reference/api.adoc +126 -0
- data/docs/reference/cli.adoc +182 -0
- data/docs/understanding/architecture.adoc +58 -0
- data/docs/understanding/rng-vs-rnc.adoc +118 -0
- data/exe/rng +5 -0
- data/lib/rng/any_name.rb +28 -0
- data/lib/rng/attribute.rb +61 -5
- data/lib/rng/choice.rb +60 -0
- data/lib/rng/cli.rb +607 -0
- data/lib/rng/data.rb +32 -0
- data/lib/rng/datatype_declaration.rb +26 -0
- data/lib/rng/define.rb +56 -5
- data/lib/rng/div.rb +36 -0
- data/lib/rng/documentation.rb +9 -0
- data/lib/rng/element.rb +66 -18
- data/lib/rng/empty.rb +23 -0
- data/lib/rng/except.rb +62 -0
- data/lib/rng/external_ref.rb +28 -0
- data/lib/rng/external_ref_resolver.rb +582 -0
- data/lib/rng/foreign_attribute.rb +26 -0
- data/lib/rng/foreign_element.rb +33 -0
- data/lib/rng/grammar.rb +38 -0
- data/lib/rng/group.rb +62 -0
- data/lib/rng/include.rb +23 -0
- data/lib/rng/include_processor.rb +461 -0
- data/lib/rng/interleave.rb +58 -0
- data/lib/rng/list.rb +56 -0
- data/lib/rng/mixed.rb +58 -0
- data/lib/rng/name.rb +28 -0
- data/lib/rng/namespace_declaration.rb +47 -0
- data/lib/rng/namespaces.rb +15 -0
- data/lib/rng/not_allowed.rb +23 -0
- data/lib/rng/ns_name.rb +31 -0
- data/lib/rng/one_or_more.rb +58 -0
- data/lib/rng/optional.rb +58 -0
- data/lib/rng/param.rb +30 -0
- data/lib/rng/parent_ref.rb +28 -0
- data/lib/rng/parse_rnc.rb +26 -0
- data/lib/rng/parse_tree_processor.rb +695 -0
- data/lib/rng/pattern.rb +24 -0
- data/lib/rng/ref.rb +28 -0
- data/lib/rng/rnc_builder.rb +927 -0
- data/lib/rng/rnc_parser.rb +672 -115
- data/lib/rng/rnc_to_rng_converter.rb +1408 -0
- data/lib/rng/schema_preamble.rb +73 -0
- data/lib/rng/schema_validator.rb +1622 -0
- data/lib/rng/start.rb +57 -6
- data/lib/rng/test_suite_parser.rb +168 -0
- data/lib/rng/text.rb +29 -0
- data/lib/rng/to_rnc.rb +24 -0
- data/lib/rng/value.rb +28 -0
- data/lib/rng/version.rb +1 -1
- data/lib/rng/zero_or_more.rb +58 -0
- data/lib/rng.rb +80 -5
- data/rng.gemspec +19 -19
- data/scripts/extract_spectest_resources.rb +96 -0
- data/spec/fixtures/compacttest.xml +2511 -0
- data/spec/fixtures/external/circular_a.rng +7 -0
- data/spec/fixtures/external/circular_b.rng +7 -0
- data/spec/fixtures/external/circular_main.rng +7 -0
- data/spec/fixtures/external/external_ref_lib.rng +7 -0
- data/spec/fixtures/external/external_ref_main.rng +7 -0
- data/spec/fixtures/external/include_lib.rng +7 -0
- data/spec/fixtures/external/include_main.rng +3 -0
- data/spec/fixtures/external/nested_chain.rng +6 -0
- data/spec/fixtures/external/nested_leaf.rng +7 -0
- data/spec/fixtures/external/nested_mid.rng +8 -0
- data/spec/fixtures/metanorma/3gpp.rnc +35 -0
- data/spec/fixtures/metanorma/3gpp.rng +105 -0
- data/spec/fixtures/metanorma/basicdoc.rnc +11 -0
- data/spec/fixtures/metanorma/bipm.rnc +148 -0
- data/spec/fixtures/metanorma/bipm.rng +376 -0
- data/spec/fixtures/metanorma/bsi.rnc +104 -0
- data/spec/fixtures/metanorma/bsi.rng +332 -0
- data/spec/fixtures/metanorma/csa.rnc +45 -0
- data/spec/fixtures/metanorma/csa.rng +131 -0
- data/spec/fixtures/metanorma/csd.rnc +43 -0
- data/spec/fixtures/metanorma/csd.rng +132 -0
- data/spec/fixtures/metanorma/gbstandard.rnc +99 -0
- data/spec/fixtures/metanorma/gbstandard.rng +316 -0
- data/spec/fixtures/metanorma/iec.rnc +49 -0
- data/spec/fixtures/metanorma/iec.rng +193 -0
- data/spec/fixtures/metanorma/ietf.rnc +275 -0
- data/spec/fixtures/metanorma/ietf.rng +925 -0
- data/spec/fixtures/metanorma/iho.rnc +58 -0
- data/spec/fixtures/metanorma/iho.rng +179 -0
- data/spec/fixtures/metanorma/isodoc.rnc +873 -0
- data/spec/fixtures/metanorma/isodoc.rng +2704 -0
- data/spec/fixtures/metanorma/isostandard-amd.rnc +43 -0
- data/spec/fixtures/metanorma/isostandard-amd.rng +108 -0
- data/spec/fixtures/metanorma/isostandard.rnc +166 -0
- data/spec/fixtures/metanorma/isostandard.rng +494 -0
- data/spec/fixtures/metanorma/itu.rnc +122 -0
- data/spec/fixtures/metanorma/itu.rng +377 -0
- data/spec/fixtures/metanorma/m3d.rnc +41 -0
- data/spec/fixtures/metanorma/m3d.rng +122 -0
- data/spec/fixtures/metanorma/mpfd.rnc +36 -0
- data/spec/fixtures/metanorma/mpfd.rng +95 -0
- data/spec/fixtures/metanorma/nist.rnc +77 -0
- data/spec/fixtures/metanorma/nist.rng +216 -0
- data/spec/fixtures/metanorma/ogc.rnc +51 -0
- data/spec/fixtures/metanorma/ogc.rng +151 -0
- data/spec/fixtures/metanorma/reqt.rnc +6 -0
- data/spec/fixtures/metanorma/rsd.rnc +36 -0
- data/spec/fixtures/metanorma/rsd.rng +95 -0
- data/spec/fixtures/metanorma/un.rnc +103 -0
- data/spec/fixtures/metanorma/un.rng +367 -0
- data/spec/fixtures/rnc/address_book.rnc +10 -0
- data/spec/fixtures/rnc/base.rnc +4 -0
- data/spec/fixtures/rnc/complex_example.rnc +61 -0
- data/spec/fixtures/rnc/grammar_with_trailing.rnc +8 -0
- data/spec/fixtures/rnc/main_include_trailing.rnc +3 -0
- data/spec/fixtures/rnc/main_with_include.rnc +5 -0
- data/spec/fixtures/rnc/test_augment.rnc +10 -0
- data/spec/fixtures/rnc/test_isodoc_simple.rnc +9 -0
- data/spec/fixtures/rnc/top_level_include.rnc +8 -0
- data/spec/fixtures/rng/address_book.rng +20 -0
- data/spec/fixtures/rng/relaxng.rng +335 -0
- data/spec/fixtures/rng/testSuite.rng +163 -0
- data/spec/fixtures/spectest.xml +6845 -0
- data/spec/fixtures/spectest_external/case_10_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_10_4.7/y +7 -0
- data/spec/fixtures/spectest_external/case_11_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_12_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_13_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_13_4.7/y +3 -0
- data/spec/fixtures/spectest_external/case_14_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_15_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_16_4.7/x +5 -0
- data/spec/fixtures/spectest_external/case_17_4.7/x +5 -0
- data/spec/fixtures/spectest_external/case_18_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_19_4.7/level1.rng +9 -0
- data/spec/fixtures/spectest_external/case_19_4.7/level2.rng +7 -0
- data/spec/fixtures/spectest_external/case_1_4.5/sub1/x +3 -0
- data/spec/fixtures/spectest_external/case_1_4.5/sub3/x +3 -0
- data/spec/fixtures/spectest_external/case_1_4.5/x +3 -0
- data/spec/fixtures/spectest_external/case_20_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_2_4.5/x +3 -0
- data/spec/fixtures/spectest_external/case_3_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_4_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_5_4.6/x +1 -0
- data/spec/fixtures/spectest_external/case_6_4.6/x +5 -0
- data/spec/fixtures/spectest_external/case_7_4.6/x +1 -0
- data/spec/fixtures/spectest_external/case_7_4.6/y +1 -0
- data/spec/fixtures/spectest_external/case_8_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_9_4.7/x +7 -0
- data/spec/fixtures/spectest_external/resources.json +149 -0
- data/spec/rng/advanced_rnc_spec.rb +101 -0
- data/spec/rng/compacttest_spec.rb +197 -0
- data/spec/rng/datatype_declaration_spec.rb +28 -0
- data/spec/rng/div_spec.rb +207 -0
- data/spec/rng/external_ref_resolver_spec.rb +122 -0
- data/spec/rng/metanorma_conversion_spec.rb +159 -0
- data/spec/rng/namespace_declaration_spec.rb +60 -0
- data/spec/rng/namespace_support_spec.rb +199 -0
- data/spec/rng/rnc_parser_spec.rb +501 -23
- data/spec/rng/rnc_roundtrip_spec.rb +135 -0
- data/spec/rng/rng_generation_spec.rb +288 -0
- data/spec/rng/roundtrip_spec.rb +342 -0
- data/spec/rng/schema_preamble_spec.rb +145 -0
- data/spec/rng/schema_spec.rb +125 -172
- data/spec/rng/spectest_spec.rb +273 -0
- data/spec/rng_spec.rb +2 -2
- data/spec/spec_helper.rb +7 -9
- metadata +188 -8
- data/lib/rng/builder.rb +0 -158
- data/lib/rng/rng_parser.rb +0 -107
- data/lib/rng/schema.rb +0 -18
- 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
|
data/lib/rng/grammar.rb
ADDED
|
@@ -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
|