rng 0.3.7 → 0.3.9
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/Gemfile +2 -1
- data/lib/rng/external_ref_resolver.rb +87 -158
- data/lib/rng/grammar.rb +8 -0
- data/lib/rng/include_processor.rb +76 -91
- data/lib/rng/parse_tree_processor.rb +8 -2
- data/lib/rng/rnc_builder.rb +3 -14
- data/lib/rng/rnc_parser.rb +36 -17
- data/lib/rng/rnc_to_rng_converter.rb +159 -140
- data/lib/rng/schema_validator.rb +33 -37
- data/lib/rng/test_suite_parser.rb +5 -5
- data/lib/rng/version.rb +1 -1
- data/lib/rng.rb +7 -5
- data/rng.gemspec +2 -2
- data/spec/fixtures/external/order_lib.rng +13 -0
- data/spec/fixtures/external/order_main.rng +3 -0
- data/spec/rng/advanced_rnc_spec.rb +123 -0
- data/spec/rng/external_ref_resolver_spec.rb +23 -0
- data/spec/rng/include_processor_spec.rb +61 -0
- data/spec/rng/parse_rnc_spec.rb +75 -0
- data/spec/rng/rnc_parser_spec.rb +56 -4
- data/spec/rng/rnc_roundtrip_spec.rb +77 -0
- data/spec/rng/schema_spec.rb +18 -0
- metadata +9 -6
- data/lib/rng/parse_rnc.rb +0 -26
data/lib/rng/schema_validator.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'moxml'
|
|
4
4
|
require 'uri'
|
|
5
5
|
|
|
6
6
|
module Rng
|
|
@@ -22,7 +22,7 @@ module Rng
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Validates raw XML against RELAX NG structural rules.
|
|
25
|
-
# Operates on the
|
|
25
|
+
# Operates on the Moxml XML tree BEFORE Lutaml deserialization,
|
|
26
26
|
# because Lutaml silently drops unmapped content.
|
|
27
27
|
class SchemaValidator
|
|
28
28
|
RNG_NS = 'http://relaxng.org/ns/structure/1.0'
|
|
@@ -31,6 +31,8 @@ module Rng
|
|
|
31
31
|
LEAF_ELEMENTS = %w[empty text notAllowed ref parentRef value].freeze
|
|
32
32
|
OBSOLETE_ELEMENTS = %w[not difference key keyRef].freeze
|
|
33
33
|
OBSOLETE_ATTRS = %w[key keyRef global].freeze
|
|
34
|
+
OBSOLETE_ATTR_CONTEXTS = [%w[start name], %w[attribute global], %w[data key],
|
|
35
|
+
%w[data keyRef]].freeze
|
|
34
36
|
NAME_REQUIRED = %w[element attribute].freeze
|
|
35
37
|
CONTAINER_ELEMENTS = %w[group choice interleave optional zeroOrMore oneOrMore list mixed define start].freeze
|
|
36
38
|
VALID_ROOT_ELEMENTS = %w[grammar element group choice interleave notAllowed externalRef data].freeze
|
|
@@ -66,7 +68,7 @@ module Rng
|
|
|
66
68
|
|
|
67
69
|
class << self
|
|
68
70
|
def validate(xml_input)
|
|
69
|
-
doc =
|
|
71
|
+
doc = Moxml::Context.new.parse(xml_input)
|
|
70
72
|
root = doc.root
|
|
71
73
|
report_error('Document has no root element') unless root
|
|
72
74
|
validator = new
|
|
@@ -75,7 +77,7 @@ module Rng
|
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
def validate_all(xml_input)
|
|
78
|
-
doc =
|
|
80
|
+
doc = Moxml::Context.new.parse(xml_input)
|
|
79
81
|
root = doc.root
|
|
80
82
|
return [SchemaValidationError.new('Document has no root element')] unless root
|
|
81
83
|
|
|
@@ -89,7 +91,7 @@ module Rng
|
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
def valid?(xml_input)
|
|
92
|
-
doc =
|
|
94
|
+
doc = Moxml::Context.new.parse(xml_input)
|
|
93
95
|
root = doc.root
|
|
94
96
|
return false unless root
|
|
95
97
|
|
|
@@ -113,9 +115,9 @@ module Rng
|
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
def validate_node(node, parent_context: nil)
|
|
116
|
-
return unless node.is_a?(
|
|
118
|
+
return unless node.is_a?(Moxml::Element)
|
|
117
119
|
|
|
118
|
-
ns = node.namespace&.
|
|
120
|
+
ns = node.namespace&.uri
|
|
119
121
|
local_name = node.name
|
|
120
122
|
is_root = node.parent&.document&.root == node
|
|
121
123
|
xpath = node.path.delete_prefix('/')
|
|
@@ -192,15 +194,15 @@ module Rng
|
|
|
192
194
|
validate_oneOrMore_infinite_attribute_name(local_name, node, xpath)
|
|
193
195
|
|
|
194
196
|
# Recurse
|
|
195
|
-
node.
|
|
196
|
-
next if child.namespace&.
|
|
197
|
+
node.children.select(&:element?).each do |child|
|
|
198
|
+
next if child.namespace&.uri == ANNOTATIONS_NS
|
|
197
199
|
|
|
198
200
|
validate_node(child, parent_context: context_for_child(local_name, node))
|
|
199
201
|
end
|
|
200
202
|
end
|
|
201
203
|
|
|
202
204
|
def report_error(message, xpath: nil, node: nil)
|
|
203
|
-
line = node&.
|
|
205
|
+
line = node&.line_number
|
|
204
206
|
error = SchemaValidationError.new(message, xpath: xpath, line: line)
|
|
205
207
|
raise error unless @collect_all
|
|
206
208
|
|
|
@@ -211,7 +213,7 @@ module Rng
|
|
|
211
213
|
private
|
|
212
214
|
|
|
213
215
|
def rng_ns_children(node)
|
|
214
|
-
node.
|
|
216
|
+
node.children.select(&:element?).select { |c| (c.namespace&.uri == RNG_NS) || (c.namespace.nil? && !c.name.empty?) }
|
|
215
217
|
end
|
|
216
218
|
|
|
217
219
|
def context_for_child(parent_name, _parent_node)
|
|
@@ -227,7 +229,7 @@ module Rng
|
|
|
227
229
|
current = node.parent
|
|
228
230
|
while current
|
|
229
231
|
return current if current.name == name &&
|
|
230
|
-
(current.namespace&.
|
|
232
|
+
(current.namespace&.uri == RNG_NS || (current.namespace.nil? && !current.name.empty?))
|
|
231
233
|
|
|
232
234
|
current = current.parent
|
|
233
235
|
end
|
|
@@ -436,21 +438,13 @@ module Rng
|
|
|
436
438
|
end
|
|
437
439
|
|
|
438
440
|
def validate_obsolete_attrs(node, xpath)
|
|
439
|
-
node.attributes.each do |
|
|
440
|
-
attr_ns = attr.namespace&.
|
|
441
|
-
if attr_ns == RNG_NS && OBSOLETE_ATTRS.include?(name)
|
|
442
|
-
report_error("Attribute '#{name}' on '#{node.name}' is obsolete",
|
|
443
|
-
xpath: xpath)
|
|
444
|
-
end
|
|
441
|
+
node.attributes.each do |attr|
|
|
442
|
+
attr_ns = attr.namespace&.uri
|
|
443
|
+
report_error("Attribute '#{attr.name}' on '#{node.name}' is obsolete", xpath: xpath) if attr_ns == RNG_NS && OBSOLETE_ATTRS.include?(attr.name)
|
|
445
444
|
next unless attr_ns.nil? && attr.namespace&.prefix.nil?
|
|
445
|
+
next unless OBSOLETE_ATTR_CONTEXTS.include?([node.name, attr.name])
|
|
446
446
|
|
|
447
|
-
report_error("Attribute 'name' on '
|
|
448
|
-
if name == 'global' && node.name == 'attribute'
|
|
449
|
-
report_error("Attribute 'global' on 'attribute' is obsolete",
|
|
450
|
-
xpath: xpath)
|
|
451
|
-
end
|
|
452
|
-
report_error("Attribute '#{name}' on 'data' is obsolete", xpath: xpath) if %w[key
|
|
453
|
-
keyRef].include?(name) && node.name == 'data'
|
|
447
|
+
report_error("Attribute '#{attr.name}' on '#{node.name}' is obsolete", xpath: xpath)
|
|
454
448
|
end
|
|
455
449
|
end
|
|
456
450
|
|
|
@@ -529,7 +523,7 @@ module Rng
|
|
|
529
523
|
return false unless node
|
|
530
524
|
|
|
531
525
|
current = node
|
|
532
|
-
while current.is_a?(
|
|
526
|
+
while current.is_a?(Moxml::Element)
|
|
533
527
|
current.namespace_definitions.each do |ns|
|
|
534
528
|
return true if ns.prefix == prefix
|
|
535
529
|
end
|
|
@@ -758,7 +752,7 @@ module Rng
|
|
|
758
752
|
def validate_name_value_purity(local_name, node, xpath)
|
|
759
753
|
return unless %w[name value].include?(local_name)
|
|
760
754
|
|
|
761
|
-
return unless node.
|
|
755
|
+
return unless node.children.any?(&:element?)
|
|
762
756
|
|
|
763
757
|
report_error("'#{local_name}' must not contain child elements", xpath: xpath)
|
|
764
758
|
end
|
|
@@ -827,8 +821,9 @@ module Rng
|
|
|
827
821
|
def validate_leaf_no_attrs(local_name, node, xpath)
|
|
828
822
|
return unless NO_ATTR_LEAF_ELEMENTS.include?(local_name)
|
|
829
823
|
|
|
830
|
-
node.attributes.each do |
|
|
831
|
-
|
|
824
|
+
node.attributes.each do |attr|
|
|
825
|
+
attr_name = attr.name
|
|
826
|
+
attr_ns = attr.namespace&.uri
|
|
832
827
|
next if attr_ns && attr_ns != RNG_NS
|
|
833
828
|
next if attr_name == 'xmlns' || attr_name.start_with?('xmlns:')
|
|
834
829
|
|
|
@@ -838,10 +833,11 @@ module Rng
|
|
|
838
833
|
end
|
|
839
834
|
|
|
840
835
|
def validate_unknown_attrs(node, xpath)
|
|
841
|
-
node.attributes.each do |
|
|
836
|
+
node.attributes.each do |attr|
|
|
837
|
+
attr_name = attr.name
|
|
842
838
|
next if attr_name == 'xmlns'
|
|
843
839
|
|
|
844
|
-
attr_ns = attr.namespace&.
|
|
840
|
+
attr_ns = attr.namespace&.uri
|
|
845
841
|
if (attr_ns == RNG_NS) && !known_rng_attr?(
|
|
846
842
|
node.name, attr_name
|
|
847
843
|
)
|
|
@@ -1407,9 +1403,9 @@ module Rng
|
|
|
1407
1403
|
|
|
1408
1404
|
def collect_all_refs(node)
|
|
1409
1405
|
refs = []
|
|
1410
|
-
return refs unless node.is_a?(
|
|
1406
|
+
return refs unless node.is_a?(Moxml::Element)
|
|
1411
1407
|
|
|
1412
|
-
ns = node.namespace&.
|
|
1408
|
+
ns = node.namespace&.uri
|
|
1413
1409
|
local = node.name
|
|
1414
1410
|
if ns == RNG_NS || (ns.nil? && !local.empty?)
|
|
1415
1411
|
case local
|
|
@@ -1418,7 +1414,7 @@ module Rng
|
|
|
1418
1414
|
when 'parentRef'
|
|
1419
1415
|
refs << { name: node['name'], type: 'parentRef' } if node['name']
|
|
1420
1416
|
end
|
|
1421
|
-
node.
|
|
1417
|
+
node.children.select(&:element?).each do |child|
|
|
1422
1418
|
refs.concat(collect_all_refs(child))
|
|
1423
1419
|
end
|
|
1424
1420
|
end
|
|
@@ -1444,9 +1440,9 @@ module Rng
|
|
|
1444
1440
|
|
|
1445
1441
|
def collect_direct_refs(node)
|
|
1446
1442
|
refs = []
|
|
1447
|
-
return refs unless node.is_a?(
|
|
1443
|
+
return refs unless node.is_a?(Moxml::Element)
|
|
1448
1444
|
|
|
1449
|
-
ns = node.namespace&.
|
|
1445
|
+
ns = node.namespace&.uri
|
|
1450
1446
|
local = node.name
|
|
1451
1447
|
if ns == RNG_NS || (ns.nil? && !local.empty?)
|
|
1452
1448
|
case local
|
|
@@ -1455,7 +1451,7 @@ module Rng
|
|
|
1455
1451
|
when 'parentRef'
|
|
1456
1452
|
refs << node['name'] if node['name']
|
|
1457
1453
|
end
|
|
1458
|
-
node.
|
|
1454
|
+
node.children.select(&:element?).each do |child|
|
|
1459
1455
|
next if child.name == 'define' # Don't recurse into nested grammar's defines
|
|
1460
1456
|
|
|
1461
1457
|
refs.concat(collect_direct_refs(child))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'moxml'
|
|
4
4
|
|
|
5
5
|
module Rng
|
|
6
6
|
# Parses RELAX NG test suite XML files (like compacttest.xml)
|
|
@@ -53,7 +53,7 @@ module Rng
|
|
|
53
53
|
attr_reader :test_cases
|
|
54
54
|
|
|
55
55
|
def initialize(xml_content)
|
|
56
|
-
@doc =
|
|
56
|
+
@doc = Moxml::Context.new.parse(xml_content)
|
|
57
57
|
@test_cases = []
|
|
58
58
|
parse_test_cases
|
|
59
59
|
end
|
|
@@ -138,7 +138,7 @@ module Rng
|
|
|
138
138
|
xml_node.xpath('resource').each do |resource_node|
|
|
139
139
|
name = resource_node['name']
|
|
140
140
|
# Get the first element child as RNG content
|
|
141
|
-
content_node = resource_node.
|
|
141
|
+
content_node = resource_node.children.find(&:element?)
|
|
142
142
|
content = content_node ? content_node.to_xml : ''
|
|
143
143
|
test_case.xml_resources[name] = content
|
|
144
144
|
end
|
|
@@ -147,7 +147,7 @@ module Rng
|
|
|
147
147
|
correct_node = xml_node.at_xpath('correct')
|
|
148
148
|
if correct_node
|
|
149
149
|
# Get the first element child as RNG content
|
|
150
|
-
rng_node = correct_node.
|
|
150
|
+
rng_node = correct_node.children.find(&:element?)
|
|
151
151
|
test_case.instance_variable_set(
|
|
152
152
|
:@xml_correct,
|
|
153
153
|
rng_node ? rng_node.to_xml : ''
|
|
@@ -158,7 +158,7 @@ module Rng
|
|
|
158
158
|
incorrect_node = xml_node.at_xpath('incorrect')
|
|
159
159
|
return unless incorrect_node
|
|
160
160
|
|
|
161
|
-
rng_node = incorrect_node.
|
|
161
|
+
rng_node = incorrect_node.children.find(&:element?)
|
|
162
162
|
test_case.instance_variable_set(
|
|
163
163
|
:@xml_incorrect,
|
|
164
164
|
rng_node ? rng_node.to_xml : ''
|
data/lib/rng/version.rb
CHANGED
data/lib/rng.rb
CHANGED
|
@@ -39,7 +39,6 @@ module Rng
|
|
|
39
39
|
autoload :Grammar, File.expand_path('rng/grammar', __dir__)
|
|
40
40
|
autoload :Documentation, File.expand_path('rng/documentation', __dir__)
|
|
41
41
|
autoload :TestSuiteParser, File.expand_path('rng/test_suite_parser', __dir__)
|
|
42
|
-
autoload :Value, File.expand_path('rng/value', __dir__)
|
|
43
42
|
|
|
44
43
|
# Autoload parser support classes
|
|
45
44
|
autoload :ParseTreeProcessor, File.expand_path('rng/parse_tree_processor', __dir__)
|
|
@@ -57,7 +56,6 @@ module Rng
|
|
|
57
56
|
|
|
58
57
|
# Autoload parsers and builders
|
|
59
58
|
autoload :RncParser, File.expand_path('rng/rnc_parser', __dir__)
|
|
60
|
-
autoload :ParseRnc, File.expand_path('rng/parse_rnc', __dir__)
|
|
61
59
|
autoload :ToRnc, File.expand_path('rng/to_rnc', __dir__)
|
|
62
60
|
autoload :RncBuilder, File.expand_path('rng/rnc_builder', __dir__)
|
|
63
61
|
autoload :IncludeProcessor, File.expand_path('rng/include_processor', __dir__)
|
|
@@ -67,13 +65,17 @@ module Rng
|
|
|
67
65
|
def parse(rng, location: nil, nested_schema: false, validate: false, resolve_external: false)
|
|
68
66
|
SchemaValidator.validate(rng) if validate && !nested_schema
|
|
69
67
|
grammar = Grammar.from_xml(rng)
|
|
70
|
-
|
|
68
|
+
|
|
69
|
+
unless resolve_external
|
|
70
|
+
grammar.warnings = ExternalRefResolver.unresolved_ref_hrefs(grammar)
|
|
71
|
+
return grammar
|
|
72
|
+
end
|
|
71
73
|
|
|
72
74
|
ExternalRefResolver.new(grammar, location: location).resolve
|
|
73
75
|
end
|
|
74
76
|
|
|
75
|
-
def self.parse_rnc(rnc)
|
|
76
|
-
|
|
77
|
+
def self.parse_rnc(rnc, location: nil)
|
|
78
|
+
IncludeProcessor.new.parse_content(rnc, location: location)
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def self.parse_file(file_path)
|
data/rng.gemspec
CHANGED
|
@@ -29,8 +29,8 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
30
|
spec.require_paths = ['lib']
|
|
31
31
|
|
|
32
|
-
spec.add_dependency 'lutaml-model', '~> 0.8.
|
|
33
|
-
spec.add_dependency '
|
|
32
|
+
spec.add_dependency 'lutaml-model', '~> 0.8.114'
|
|
33
|
+
spec.add_dependency 'moxml'
|
|
34
34
|
spec.add_dependency 'parslet'
|
|
35
35
|
|
|
36
36
|
spec.add_dependency 'thor'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
require 'rng'
|
|
5
|
+
require 'parslet'
|
|
5
6
|
|
|
6
7
|
RSpec.describe 'Advanced RNC features' do
|
|
7
8
|
describe 'Name wildcards' do
|
|
@@ -98,4 +99,126 @@ RSpec.describe 'Advanced RNC features' do
|
|
|
98
99
|
expect { Rng.parse_rnc(rnc) }.not_to raise_error
|
|
99
100
|
end
|
|
100
101
|
end
|
|
102
|
+
|
|
103
|
+
describe 'Content-position annotations' do
|
|
104
|
+
it 'parses a bracket annotation before an attribute in content' do
|
|
105
|
+
rnc = 'start = element n { [ a:defaultValue = "x" ] attribute foo { text } }'
|
|
106
|
+
expect { Rng.parse_rnc(rnc) }.not_to raise_error
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'parses a bracket annotation before a nested element in content' do
|
|
110
|
+
rnc = 'start = element n { [ a:documentation [ "hi" ] ] element c { text } }'
|
|
111
|
+
expect { Rng.parse_rnc(rnc) }.not_to raise_error
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'parses an annotated attribute within a sequence' do
|
|
115
|
+
rnc = 'start = element n { [ a:x = "1" ] attribute foo { text }, ' \
|
|
116
|
+
'attribute bar { text } }'
|
|
117
|
+
grammar = Rng.parse_rnc(rnc)
|
|
118
|
+
attrs = grammar.start.first.element.attribute.map(&:attr_name)
|
|
119
|
+
expect(attrs).to eq(%w[foo bar])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'leaves un-annotated content parsing unchanged' do
|
|
123
|
+
rnc = 'start = element n { attribute foo { text } }'
|
|
124
|
+
expect { Rng.parse_rnc(rnc) }.not_to raise_error
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'does not let brackets inside an annotation string close it early' do
|
|
128
|
+
rnc = 'start = element n { [ a:x = "a]b[c" ] attribute foo { text }, ' \
|
|
129
|
+
'attribute bar { text } }'
|
|
130
|
+
grammar = Rng.parse_rnc(rnc)
|
|
131
|
+
attrs = grammar.start.first.element.attribute.map(&:attr_name)
|
|
132
|
+
expect(attrs).to eq(%w[foo bar])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'ignores brackets inside a single-quoted annotation string' do
|
|
136
|
+
rnc = "start = element n { [ a:x = 'a]b[c' ] attribute foo { text }, " \
|
|
137
|
+
'attribute bar { text } }'
|
|
138
|
+
grammar = Rng.parse_rnc(rnc)
|
|
139
|
+
attrs = grammar.start.first.element.attribute.map(&:attr_name)
|
|
140
|
+
expect(attrs).to eq(%w[foo bar])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'ignores brackets inside a triple-quoted annotation string' do
|
|
144
|
+
rnc = 'start = element n { [ a:doc [ """multi]line[x""" ] ] ' \
|
|
145
|
+
'attribute foo { text }, attribute bar { text } }'
|
|
146
|
+
grammar = Rng.parse_rnc(rnc)
|
|
147
|
+
attrs = grammar.start.first.element.attribute.map(&:attr_name)
|
|
148
|
+
expect(attrs).to eq(%w[foo bar])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'drops an annotation before a non-leading definition without corrupting it' do
|
|
152
|
+
rnc = "foo = element foo { text }\n" \
|
|
153
|
+
"[ a:defaultValue = \"x\" ]\n" \
|
|
154
|
+
"bar = element bar { text }\n" \
|
|
155
|
+
'start = foo'
|
|
156
|
+
grammar = Rng.parse_rnc(rnc)
|
|
157
|
+
|
|
158
|
+
expect(grammar.define.map(&:name)).to contain_exactly('foo', 'bar')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'rejects a control character inside an annotation string, like a value string' do
|
|
162
|
+
rnc = "start = element n { [ a:x = \"#{1.chr}\" ] " \
|
|
163
|
+
'attribute foo { text } }'
|
|
164
|
+
expect { Rng.parse_rnc(rnc) }.to raise_error(Parslet::ParseFailed)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe 'Sequence separators' do
|
|
169
|
+
it 'rejects comma-less sequence items so they cannot swallow the next definition' do
|
|
170
|
+
rnc = 'start = element n { attribute foo { text } attribute bar { text } }'
|
|
171
|
+
expect { Rng.parse_rnc(rnc) }.to raise_error(Parslet::ParseFailed)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'parses comma-separated sequence items in element content' do
|
|
175
|
+
rnc = 'start = element n { attribute foo { text }, attribute bar { text } }'
|
|
176
|
+
grammar = Rng.parse_rnc(rnc)
|
|
177
|
+
attrs = grammar.start.first.element.attribute.map(&:attr_name)
|
|
178
|
+
expect(attrs).to eq(%w[foo bar])
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe 'Non-leading start' do
|
|
183
|
+
it 'produces a real <start> for a start = element after other defines' do
|
|
184
|
+
rnc = "item = element item { text }\nstart = element root { item* }"
|
|
185
|
+
grammar = Rng.parse_rnc(rnc)
|
|
186
|
+
|
|
187
|
+
expect(grammar.start.first.element.attr_name).to eq('root')
|
|
188
|
+
expect(grammar.define.map(&:name)).to eq(['item'])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'produces a real <start> for a non-leading start = ref' do
|
|
192
|
+
rnc = "library = element library { text }\nstart = library"
|
|
193
|
+
grammar = Rng.parse_rnc(rnc)
|
|
194
|
+
|
|
195
|
+
expect(grammar.start.first.ref.name).to eq('library')
|
|
196
|
+
expect(grammar.define.map(&:name)).not_to include('start')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'collects multiple start |= definitions as combining starts' do
|
|
200
|
+
rnc = "a = element a { empty }\nb = element b { empty }\n" \
|
|
201
|
+
"start |= a\nstart |= b"
|
|
202
|
+
grammar = Rng.parse_rnc(rnc)
|
|
203
|
+
|
|
204
|
+
expect(grammar.start.map(&:combine)).to eq(%w[choice choice])
|
|
205
|
+
expect(grammar.start.map { |s| s.ref.name }).to contain_exactly('a', 'b')
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'maps start &= to interleave-combining starts' do
|
|
209
|
+
rnc = "a = element a { empty }\nb = element b { empty }\n" \
|
|
210
|
+
"start &= a\nstart &= b"
|
|
211
|
+
grammar = Rng.parse_rnc(rnc)
|
|
212
|
+
|
|
213
|
+
expect(grammar.start.map(&:combine)).to eq(%w[interleave interleave])
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'keeps an escaped \\start as a define, not the grammar start' do
|
|
217
|
+
rnc = "x = element x { text }\n\\start = element a { text }\nstart = x"
|
|
218
|
+
grammar = Rng.parse_rnc(rnc)
|
|
219
|
+
|
|
220
|
+
expect(grammar.define.map(&:name)).to include('start')
|
|
221
|
+
expect(grammar.start.first.ref.name).to eq('x')
|
|
222
|
+
end
|
|
223
|
+
end
|
|
101
224
|
end
|
|
@@ -69,6 +69,22 @@ RSpec.describe Rng::ExternalRefResolver do
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
context 'with interleaved children reached through an include' do
|
|
73
|
+
let(:order_main) { File.read('spec/fixtures/external/order_main.rng') }
|
|
74
|
+
|
|
75
|
+
it 'preserves element_order through external ref resolution' do
|
|
76
|
+
grammar = Rng::Grammar.from_xml(order_main)
|
|
77
|
+
resolved = described_class.new(grammar, location: 'spec/fixtures/external/order_main.rng').resolve
|
|
78
|
+
|
|
79
|
+
xml = resolved.to_xml
|
|
80
|
+
expect(xml.index('first')).to be < xml.index('mid')
|
|
81
|
+
expect(xml.index('mid')).to be < xml.index('last')
|
|
82
|
+
|
|
83
|
+
root = resolved.start.first.element
|
|
84
|
+
expect(root.instance_variable_get(:@element_order)).not_to be_nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
72
88
|
context 'with non-existent external file' do
|
|
73
89
|
it 'does not raise error but warns when external file not found' do
|
|
74
90
|
# Create a grammar with a non-existent include
|
|
@@ -110,6 +126,13 @@ RSpec.describe Rng::ExternalRefResolver do
|
|
|
110
126
|
# Without resolve_external, the grammar is returned as-is
|
|
111
127
|
expect(grammar).to be_a(Rng::Grammar)
|
|
112
128
|
end
|
|
129
|
+
|
|
130
|
+
it 'warns about unresolved external references when not resolving' do
|
|
131
|
+
main_rng = File.read('spec/fixtures/external/include_main.rng')
|
|
132
|
+
grammar = Rng.parse(main_rng)
|
|
133
|
+
|
|
134
|
+
expect(grammar.warnings).to include(a_string_matching(/include_lib\.rng/))
|
|
135
|
+
end
|
|
113
136
|
end
|
|
114
137
|
|
|
115
138
|
describe Rng::ExternalRefResolver::ExternalRefResolutionError do
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Rng::IncludeProcessor do
|
|
7
|
+
# A grammar-block RNC that includes another grammar and then defines its
|
|
8
|
+
# own named patterns after the include. The merged grammar must carry BOTH
|
|
9
|
+
# the included grammar's definitions and the including file's own patterns.
|
|
10
|
+
# ParseTreeProcessor used to overwrite :definitions (the include merge
|
|
11
|
+
# result) with only :includes + :patterns, dropping every included define.
|
|
12
|
+
it 'keeps included definitions and own patterns in a grammar block file' do
|
|
13
|
+
Dir.mktmpdir do |dir|
|
|
14
|
+
File.write(File.join(dir, 'base.rnc'), <<~RNC)
|
|
15
|
+
start = doc
|
|
16
|
+
doc = element doc { sections }
|
|
17
|
+
sections = element sections { element a { text } }
|
|
18
|
+
RNC
|
|
19
|
+
|
|
20
|
+
File.write(File.join(dir, 'derived.rnc'), <<~RNC)
|
|
21
|
+
include "base.rnc" {
|
|
22
|
+
doc = element doc { sections }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
start = derived
|
|
26
|
+
derived = element derived { sections }
|
|
27
|
+
note = element note { text }
|
|
28
|
+
RNC
|
|
29
|
+
|
|
30
|
+
grammar = Rng.parse_file(File.join(dir, 'derived.rnc'))
|
|
31
|
+
names = grammar.define.map(&:name)
|
|
32
|
+
|
|
33
|
+
expect(names).to include('sections')
|
|
34
|
+
expect(names).to include('note')
|
|
35
|
+
expect(names).to include('derived')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'applies include-block overrides, replacing the base definition' do
|
|
40
|
+
Dir.mktmpdir do |dir|
|
|
41
|
+
File.write(File.join(dir, 'base.rnc'), <<~RNC)
|
|
42
|
+
start = doc
|
|
43
|
+
doc = element doc { sections }
|
|
44
|
+
sections = element sections { element a { text } }
|
|
45
|
+
RNC
|
|
46
|
+
|
|
47
|
+
File.write(File.join(dir, 'derived.rnc'), <<~RNC)
|
|
48
|
+
include "base.rnc" {
|
|
49
|
+
sections = element sections { element b { text } }
|
|
50
|
+
}
|
|
51
|
+
RNC
|
|
52
|
+
|
|
53
|
+
grammar = Rng.parse_file(File.join(dir, 'derived.rnc'))
|
|
54
|
+
sections = grammar.define.find { |d| d.name == 'sections' }
|
|
55
|
+
|
|
56
|
+
expect(sections).not_to be_nil
|
|
57
|
+
expect(sections.to_xml).to include('name="b"')
|
|
58
|
+
expect(sections.to_xml).not_to include('name="a"')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'ParseRncSpec' do
|
|
7
|
+
describe '.parse_rnc' do
|
|
8
|
+
let(:simple_rnc) { 'element foo { text }' }
|
|
9
|
+
|
|
10
|
+
it 'returns a Grammar for include-free RNC without a location' do
|
|
11
|
+
parsed = Rng.parse_rnc(simple_rnc)
|
|
12
|
+
|
|
13
|
+
expect(parsed).to be_a(Rng::Grammar)
|
|
14
|
+
expect(parsed.start.first.element.attr_name).to eq('foo')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'produces equivalent output with and without location for hex-escaped RNC' do
|
|
18
|
+
# \x{72}oot decodes to "root". Hex preprocessing must run on the
|
|
19
|
+
# with-location path or the parse tree differs from the no-location path
|
|
20
|
+
# for the same input — which is the divergence this spec guards against.
|
|
21
|
+
rnc = 'element \\x{72}oot { text }'
|
|
22
|
+
|
|
23
|
+
Dir.mktmpdir do |dir|
|
|
24
|
+
scratch = File.join(dir, 'scratch.rnc')
|
|
25
|
+
|
|
26
|
+
without_location = Rng.parse_rnc(rnc)
|
|
27
|
+
with_location = Rng.parse_rnc(rnc, location: scratch)
|
|
28
|
+
|
|
29
|
+
expect(with_location.to_xml).to be_xml_equivalent_to(without_location.to_xml)
|
|
30
|
+
expect(with_location.start.first.element.attr_name).to eq('root')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'resolves relative includes when location is the source directory' do
|
|
35
|
+
Dir.mktmpdir do |dir|
|
|
36
|
+
File.write(File.join(dir, 'child.rnc'), <<~RNC)
|
|
37
|
+
ChildPattern = element child { text }
|
|
38
|
+
start = element wrapped { ChildPattern }
|
|
39
|
+
RNC
|
|
40
|
+
|
|
41
|
+
parent_path = File.join(dir, 'parent.rnc')
|
|
42
|
+
File.write(parent_path, <<~RNC)
|
|
43
|
+
include "child.rnc" {
|
|
44
|
+
start = element root { ChildPattern }
|
|
45
|
+
}
|
|
46
|
+
RNC
|
|
47
|
+
|
|
48
|
+
parsed = Rng.parse_rnc(File.read(parent_path), location: dir)
|
|
49
|
+
|
|
50
|
+
expect(parsed.define.map(&:name)).to include('ChildPattern')
|
|
51
|
+
expect(parsed.start.first.element.attr_name).to eq('root')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'also accepts a source file path as location' do
|
|
56
|
+
Dir.mktmpdir do |dir|
|
|
57
|
+
File.write(File.join(dir, 'child.rnc'), <<~RNC)
|
|
58
|
+
ChildPattern = element child { text }
|
|
59
|
+
RNC
|
|
60
|
+
|
|
61
|
+
parent_path = File.join(dir, 'parent.rnc')
|
|
62
|
+
File.write(parent_path, <<~RNC)
|
|
63
|
+
include "child.rnc" {
|
|
64
|
+
start = element root { ChildPattern }
|
|
65
|
+
}
|
|
66
|
+
RNC
|
|
67
|
+
|
|
68
|
+
parsed = Rng.parse_rnc(File.read(parent_path), location: parent_path)
|
|
69
|
+
|
|
70
|
+
expect(parsed.define.map(&:name)).to include('ChildPattern')
|
|
71
|
+
expect(parsed.start.first.element.attr_name).to eq('root')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|