rng 0.3.7 → 0.3.8
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/lib/rng/external_ref_resolver.rb +86 -157
- data/lib/rng/grammar.rb +8 -0
- data/lib/rng/include_processor.rb +64 -90
- data/lib/rng/rnc_builder.rb +3 -14
- data/lib/rng/rnc_parser.rb +36 -16
- data/lib/rng/rnc_to_rng_converter.rb +124 -114
- data/lib/rng/version.rb +1 -1
- data/lib/rng.rb +7 -5
- data/rng.gemspec +1 -1
- 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/parse_rnc_spec.rb +75 -0
- data/spec/rng/rnc_parser_spec.rb +48 -0
- data/spec/rng/rnc_roundtrip_spec.rb +77 -0
- data/spec/rng/schema_spec.rb +18 -0
- metadata +7 -5
- data/lib/rng/parse_rnc.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e689f7178c2c8a2e691e1327e99c1b74e7cea638ec80a12d5d557ad0c7a8003
|
|
4
|
+
data.tar.gz: 2d768848f695fab05f1f8808e44124178a5498b81bc12b277cc40b787fb862b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b982725dbada931908431e146118e403f750ed9232d477491f568455a674b2f3d4efaeda73c3bbbd5b4fc0de49e1fab886f20fad6daf74dcdd283d20ff305569
|
|
7
|
+
data.tar.gz: 78f9da1bc7d6b3335af3e251954172e3d6d548f2a875c7d49b4f6c8cfe7a5cbd98dbe3f62d61b1effb2fd44088cd0f993a3b845fa014aaa87f2904f60be92e3e
|
|
@@ -22,6 +22,41 @@ module Rng
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Scan a grammar for unresolved external references.
|
|
26
|
+
#
|
|
27
|
+
# Walks the object graph forward-only via lutaml attribute reflection
|
|
28
|
+
# and collects a descriptive message for every Include, ExternalRef, and
|
|
29
|
+
# ParentRef found, including its href/name so callers can match on it.
|
|
30
|
+
#
|
|
31
|
+
# @param grammar [Grammar] The grammar to scan
|
|
32
|
+
# @return [Array<String>] Descriptive messages for unresolved references
|
|
33
|
+
def self.unresolved_ref_hrefs(grammar)
|
|
34
|
+
messages = []
|
|
35
|
+
collect_unresolved_refs(grammar, messages, [])
|
|
36
|
+
messages
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @api private
|
|
40
|
+
def self.collect_unresolved_refs(obj, messages, seen)
|
|
41
|
+
case obj
|
|
42
|
+
when Include
|
|
43
|
+
messages << "Unresolved include href: #{obj.href}" if obj.href
|
|
44
|
+
when ExternalRef
|
|
45
|
+
messages << "Unresolved externalRef href: #{obj.href}" if obj.href
|
|
46
|
+
when ParentRef
|
|
47
|
+
messages << "Unresolved parentRef name: #{obj.name}" if obj.name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return unless obj.is_a?(Lutaml::Model::Serializable)
|
|
51
|
+
return if seen.include?(obj.object_id)
|
|
52
|
+
|
|
53
|
+
seen << obj.object_id
|
|
54
|
+
obj.class.attributes.each_key do |attr_name|
|
|
55
|
+
value = obj.public_send(attr_name)
|
|
56
|
+
Array(value).each { |child| collect_unresolved_refs(child, messages, seen) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
25
60
|
# Initialize the resolver
|
|
26
61
|
#
|
|
27
62
|
# @param grammar [Grammar] The grammar to resolve external refs in
|
|
@@ -83,18 +118,14 @@ module Rng
|
|
|
83
118
|
# Copy start pattern
|
|
84
119
|
if grammar.start && !grammar.start.empty?
|
|
85
120
|
new_grammar.start = grammar.start.filter_map do |s|
|
|
86
|
-
|
|
87
|
-
clear_element_order!(resolved)
|
|
88
|
-
resolved
|
|
121
|
+
resolve_pattern(deep_dup(s), base_dir, visited_files)
|
|
89
122
|
end
|
|
90
123
|
end
|
|
91
124
|
|
|
92
125
|
# Copy define patterns
|
|
93
126
|
if grammar.define && !grammar.define.empty?
|
|
94
127
|
new_grammar.define = grammar.define.filter_map do |d|
|
|
95
|
-
|
|
96
|
-
clear_element_order!(resolved)
|
|
97
|
-
resolved
|
|
128
|
+
resolve_pattern(deep_dup(d), base_dir, visited_files)
|
|
98
129
|
end
|
|
99
130
|
end
|
|
100
131
|
|
|
@@ -102,68 +133,10 @@ module Rng
|
|
|
102
133
|
return unless grammar.div && !grammar.div.empty?
|
|
103
134
|
|
|
104
135
|
new_grammar.div = grammar.div.filter_map do |div|
|
|
105
|
-
|
|
106
|
-
clear_element_order!(resolved_div)
|
|
107
|
-
resolved_div
|
|
136
|
+
resolve_div(deep_dup(div), base_dir, visited_files)
|
|
108
137
|
end
|
|
109
138
|
end
|
|
110
139
|
|
|
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
140
|
# Resolve includes and return array of resolved grammars/content
|
|
168
141
|
#
|
|
169
142
|
# @param grammar [Grammar] Grammar containing includes
|
|
@@ -230,6 +203,7 @@ module Rng
|
|
|
230
203
|
value = obj.public_send(attr_name)
|
|
231
204
|
result.public_send(:"#{attr_name}=", deep_dup(value))
|
|
232
205
|
end
|
|
206
|
+
copy_order_ivars!(obj, result)
|
|
233
207
|
result
|
|
234
208
|
when Array
|
|
235
209
|
obj.map { |o| deep_dup(o) }
|
|
@@ -243,6 +217,21 @@ module Rng
|
|
|
243
217
|
end
|
|
244
218
|
end
|
|
245
219
|
|
|
220
|
+
# Carry lutaml's document-order ivars onto a deep-dup'd object so to_xml
|
|
221
|
+
# emits children in source order. These hold Lutaml::Xml::Element value
|
|
222
|
+
# objects (name/type), not live Nokogiri nodes.
|
|
223
|
+
#
|
|
224
|
+
# @param source [Object] Original object
|
|
225
|
+
# @param result [Object] Deep-dup'd copy
|
|
226
|
+
def copy_order_ivars!(source, result)
|
|
227
|
+
%i[@element_order @attribute_order].each do |ivar|
|
|
228
|
+
next unless source.instance_variable_defined?(ivar)
|
|
229
|
+
|
|
230
|
+
order = source.instance_variable_get(ivar)
|
|
231
|
+
result.instance_variable_set(ivar, order&.dup)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
246
235
|
# Resolve a single include directive
|
|
247
236
|
#
|
|
248
237
|
# @param include_directive [Include] Include element with href
|
|
@@ -310,32 +299,41 @@ module Rng
|
|
|
310
299
|
def resolve_pattern(pattern, base_dir, visited_files)
|
|
311
300
|
return unless pattern
|
|
312
301
|
|
|
313
|
-
# Handle Element pattern
|
|
314
302
|
case pattern
|
|
315
303
|
when Element
|
|
316
304
|
resolve_element_external_ref!(pattern, base_dir, visited_files)
|
|
317
|
-
|
|
318
|
-
# Recursively resolve in Element's children
|
|
319
|
-
resolve_element_children!(pattern, base_dir, visited_files)
|
|
320
|
-
# Handle Group pattern
|
|
305
|
+
resolve_child_patterns!(pattern, base_dir, visited_files)
|
|
321
306
|
when Group
|
|
322
307
|
resolve_group_external_ref!(pattern, base_dir, visited_files)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
# Handle Define pattern
|
|
327
|
-
when Define
|
|
328
|
-
resolve_define_children!(pattern, base_dir, visited_files)
|
|
329
|
-
# Handle Start pattern
|
|
330
|
-
when Start
|
|
331
|
-
resolve_start_children!(pattern, base_dir, visited_files)
|
|
332
|
-
when Start
|
|
333
|
-
resolve_pattern_children!(pattern, base_dir, visited_files)
|
|
308
|
+
resolve_child_patterns!(pattern, base_dir, visited_files)
|
|
309
|
+
when Define, Start
|
|
310
|
+
resolve_child_patterns!(pattern, base_dir, visited_files)
|
|
334
311
|
end
|
|
335
312
|
|
|
336
313
|
pattern
|
|
337
314
|
end
|
|
338
315
|
|
|
316
|
+
# Recursively resolve external refs in a pattern's child patterns.
|
|
317
|
+
#
|
|
318
|
+
# Walks forward via lutaml attribute reflection (same approach as
|
|
319
|
+
# collect_unresolved_refs) instead of curated per-type attribute lists.
|
|
320
|
+
# Scalars are skipped by the Serializable guard. Only Element/Group/Define/
|
|
321
|
+
# Start dispatch into externalRef handling in resolve_pattern, so every
|
|
322
|
+
# other Serializable child is a no-op and the recursion stays bounded.
|
|
323
|
+
#
|
|
324
|
+
# @param pattern [Object] Pattern whose children to resolve
|
|
325
|
+
# @param base_dir [String] Base directory for href resolution
|
|
326
|
+
# @param visited_files [Set] Set of visited file paths
|
|
327
|
+
def resolve_child_patterns!(pattern, base_dir, visited_files)
|
|
328
|
+
pattern.class.attributes.each_key do |attr_name|
|
|
329
|
+
Array(pattern.public_send(attr_name)).each do |child|
|
|
330
|
+
next unless child.is_a?(Lutaml::Model::Serializable)
|
|
331
|
+
|
|
332
|
+
resolve_pattern(child, base_dir, visited_files)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
339
337
|
# Resolve external ref in an Element
|
|
340
338
|
#
|
|
341
339
|
# @param element [Element] Element with external_ref
|
|
@@ -386,9 +384,16 @@ module Rng
|
|
|
386
384
|
|
|
387
385
|
# Copy pattern content from source to target
|
|
388
386
|
#
|
|
387
|
+
# Rebuilds target's content from attributes, so its own document-order
|
|
388
|
+
# ivars no longer describe what it holds (e.g. an externalRef entry was
|
|
389
|
+
# replaced). Clear them on target only; children keep their own order.
|
|
390
|
+
#
|
|
389
391
|
# @param target [Object] Target pattern (Element, Group, etc.)
|
|
390
392
|
# @param source [Object] Source pattern
|
|
391
393
|
def copy_pattern_content(target, source)
|
|
394
|
+
target.instance_variable_set(:@element_order, nil)
|
|
395
|
+
target.instance_variable_set(:@attribute_order, nil)
|
|
396
|
+
|
|
392
397
|
case source
|
|
393
398
|
when Start
|
|
394
399
|
# Start pattern - copy its content (element, choice, group, etc.)
|
|
@@ -407,8 +412,6 @@ module Rng
|
|
|
407
412
|
value data list notAllowed externalRef])
|
|
408
413
|
when Choice
|
|
409
414
|
target.choice = source.choice if source.choice
|
|
410
|
-
when Group
|
|
411
|
-
target.group = source.group if source.group
|
|
412
415
|
when Interleave
|
|
413
416
|
target.interleave = source.interleave if source.interleave
|
|
414
417
|
when Optional
|
|
@@ -491,80 +494,6 @@ module Rng
|
|
|
491
494
|
copy_pattern_content(group, start_pattern)
|
|
492
495
|
end
|
|
493
496
|
|
|
494
|
-
# Recursively resolve children of an Element
|
|
495
|
-
#
|
|
496
|
-
# @param element [Element] Element to resolve children in
|
|
497
|
-
# @param base_dir [String] Base directory
|
|
498
|
-
# @param visited_files [Set] Set of visited file paths
|
|
499
|
-
def resolve_element_children!(element, base_dir, visited_files)
|
|
500
|
-
%i[attribute ref choice group interleave mixed optional zeroOrMore
|
|
501
|
-
oneOrMore anyName text empty value data list notAllowed element grammar].each do |attr|
|
|
502
|
-
children = element.send(attr)
|
|
503
|
-
next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
|
|
504
|
-
|
|
505
|
-
Array(children).each do |child|
|
|
506
|
-
resolve_pattern(child, base_dir, visited_files)
|
|
507
|
-
end
|
|
508
|
-
end
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
# Recursively resolve children of a Group
|
|
512
|
-
#
|
|
513
|
-
# @param group [Group] Group to resolve children in
|
|
514
|
-
# @param base_dir [String] Base directory
|
|
515
|
-
# @param visited_files [Set] Set of visited file paths
|
|
516
|
-
def resolve_group_children!(group, base_dir, visited_files)
|
|
517
|
-
%i[attribute ref choice group interleave mixed optional zeroOrMore
|
|
518
|
-
oneOrMore text empty value data list notAllowed].each do |attr|
|
|
519
|
-
children = group.send(attr)
|
|
520
|
-
next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
|
|
521
|
-
|
|
522
|
-
Array(children).each do |child|
|
|
523
|
-
resolve_pattern(child, base_dir, visited_files)
|
|
524
|
-
end
|
|
525
|
-
end
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
# Recursively resolve children of a Start pattern
|
|
529
|
-
#
|
|
530
|
-
# @param start [Start] Start pattern to resolve children in
|
|
531
|
-
# @param base_dir [String] Base directory
|
|
532
|
-
# @param visited_files [Set] Set of visited file paths
|
|
533
|
-
def resolve_start_children!(start, base_dir, visited_files)
|
|
534
|
-
# Start has: element, choice, group, interleave, mixed, optional,
|
|
535
|
-
# zeroOrMore, oneOrMore, text, empty, value, data, list,
|
|
536
|
-
# parentRef, notAllowed, grammar
|
|
537
|
-
%i[element choice group interleave mixed optional zeroOrMore
|
|
538
|
-
oneOrMore text empty value data list parentRef notAllowed grammar].each do |attr|
|
|
539
|
-
children = start.send(attr)
|
|
540
|
-
next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
|
|
541
|
-
|
|
542
|
-
Array(children).each do |child|
|
|
543
|
-
resolve_pattern(child, base_dir, visited_files)
|
|
544
|
-
end
|
|
545
|
-
end
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
# Recursively resolve children of a Define
|
|
549
|
-
#
|
|
550
|
-
# @param define [Define] Define to resolve children in
|
|
551
|
-
# @param base_dir [String] Base directory
|
|
552
|
-
# @param visited_files [Set] Set of visited file paths
|
|
553
|
-
def resolve_define_children!(define, base_dir, visited_files)
|
|
554
|
-
# Define has: ref, element, choice, group, interleave, mixed, optional,
|
|
555
|
-
# zeroOrMore, oneOrMore, text, empty, value, data, list,
|
|
556
|
-
# notAllowed, attribute, grammar
|
|
557
|
-
%i[ref element choice group interleave mixed optional zeroOrMore
|
|
558
|
-
oneOrMore text empty value data list notAllowed attribute grammar].each do |attr|
|
|
559
|
-
children = define.send(attr)
|
|
560
|
-
next if children.nil? || (children.respond_to?(:empty?) && children.empty?)
|
|
561
|
-
|
|
562
|
-
Array(children).each do |child|
|
|
563
|
-
resolve_pattern(child, base_dir, visited_files)
|
|
564
|
-
end
|
|
565
|
-
end
|
|
566
|
-
end
|
|
567
|
-
|
|
568
497
|
# Resolve href to absolute path with cycle detection
|
|
569
498
|
#
|
|
570
499
|
# @param href [String] Relative or absolute href
|
data/lib/rng/grammar.rb
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
module Rng
|
|
4
4
|
# This represents the RNG schema
|
|
5
5
|
class Grammar < Lutaml::Model::Serializable
|
|
6
|
+
# Non-serialized diagnostics, e.g. unresolved external-reference hrefs
|
|
7
|
+
# collected when parsing without resolve_external.
|
|
8
|
+
attr_writer :warnings
|
|
9
|
+
|
|
10
|
+
def warnings
|
|
11
|
+
@warnings ||= []
|
|
12
|
+
end
|
|
13
|
+
|
|
6
14
|
attribute :id, :string
|
|
7
15
|
attribute :ns, :string
|
|
8
16
|
attribute :datatypeLibrary, :string
|
|
@@ -29,69 +29,74 @@ module Rng
|
|
|
29
29
|
# @param visited_files [Set] Set of already visited file paths (for circular detection)
|
|
30
30
|
# @return [Grammar] Parsed grammar object
|
|
31
31
|
def parse_file(file_path, base_dir = nil, visited_files = Set.new)
|
|
32
|
-
|
|
32
|
+
abs_path = track_visited_file(file_path, base_dir, visited_files)
|
|
33
33
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
process_raw_nodes!(tree)
|
|
37
|
-
|
|
38
|
-
# Process any includes in the tree (top-level or grammar-level)
|
|
39
|
-
process_includes(tree, base_dir || File.dirname(File.expand_path(file_path)),
|
|
40
|
-
visited_files)
|
|
41
|
-
|
|
42
|
-
# Extract namespace from wrapper level if present
|
|
43
|
-
namespace = tree[:namespace]
|
|
34
|
+
# Read file content
|
|
35
|
+
raise "Include file not found: #{abs_path}" unless File.exist?(abs_path)
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
grammar_tree[:namespace] = namespace if namespace
|
|
37
|
+
parse_tree_to_grammar(File.read(abs_path), File.dirname(abs_path), visited_files)
|
|
38
|
+
end
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
# Parse in-memory RNC content with optional include resolution.
|
|
41
|
+
#
|
|
42
|
+
# When +location+ is nil, includes are not resolved and the pipeline matches
|
|
43
|
+
# {RncParser.parse} exactly. When +location+ names an existing directory it
|
|
44
|
+
# is used directly as the base directory for relative +include+ directives.
|
|
45
|
+
# Otherwise +location+ is treated as a source file path and includes are
|
|
46
|
+
# resolved against that file's directory.
|
|
47
|
+
#
|
|
48
|
+
# @param content [String] RNC content
|
|
49
|
+
# @param location [String, nil] Base directory or source file path used to resolve relative includes
|
|
50
|
+
# @param visited_files [Set] Set of already visited file paths (for circular detection)
|
|
51
|
+
# @return [Grammar] Parsed grammar object
|
|
52
|
+
def parse_content(content, location: nil, visited_files: Set.new)
|
|
53
|
+
parse_tree_to_grammar(content, resolve_base_dir(location, visited_files), visited_files)
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
private
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# @param base_dir [String, nil] Base directory for resolving relative paths
|
|
60
|
-
# @param visited_files [Set] Set of already visited file paths
|
|
61
|
-
# @return [Hash] Parse tree
|
|
62
|
-
def parse_file_to_tree(file_path, base_dir = nil, visited_files = Set.new)
|
|
63
|
-
# Resolve absolute path to prevent circular includes
|
|
64
|
-
abs_path = File.expand_path(file_path, base_dir)
|
|
58
|
+
def resolve_base_dir(location, visited_files)
|
|
59
|
+
return nil if location.nil?
|
|
60
|
+
return location if File.directory?(location)
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
File.dirname(track_visited_file(location, nil, visited_files))
|
|
63
|
+
end
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
def parse_tree_to_grammar(content, base_dir, visited_files)
|
|
66
|
+
tree = parse_content_to_tree(content)
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
if base_dir
|
|
69
|
+
# process_includes runs before normalize and needs raw_override /
|
|
70
|
+
# raw_grammar / raw_patterns expanded so it can walk the parsed
|
|
71
|
+
# subtrees. normalize would do this on the grammar_tree later, but
|
|
72
|
+
# by then process_includes has already executed.
|
|
73
|
+
process_raw_nodes!(tree)
|
|
74
|
+
process_includes(tree, base_dir, visited_files)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
rng_xml = @converter.convert(build_grammar_tree(tree))
|
|
78
|
+
Grammar.from_xml(rng_xml)
|
|
79
|
+
end
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
def track_visited_file(file_path, base_dir, visited_files)
|
|
82
|
+
abs_path = File.expand_path(file_path, base_dir)
|
|
83
|
+
raise "Circular include detected: #{abs_path}" if visited_files.include?(abs_path)
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
visited_files.add(abs_path)
|
|
86
|
+
abs_path
|
|
79
87
|
end
|
|
80
88
|
|
|
81
|
-
# Parse RNC content
|
|
89
|
+
# Parse RNC content into a raw parse tree.
|
|
90
|
+
#
|
|
91
|
+
# Applies hex-escape preprocessing so callers get the same parse tree
|
|
92
|
+
# they would from {RncParser.parse}. Preprocessing is idempotent on input
|
|
93
|
+
# that contains no hex escapes outside string literals, so it is safe to
|
|
94
|
+
# apply to file content as well.
|
|
82
95
|
#
|
|
83
96
|
# @param content [String] RNC content
|
|
84
|
-
# @
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def parse_with_includes(content, base_dir = nil, visited_files = Set.new)
|
|
88
|
-
parser = RncParser.new
|
|
89
|
-
tree = parser.parse(content.strip)
|
|
90
|
-
|
|
91
|
-
# Process includes in the tree
|
|
92
|
-
process_includes(tree, base_dir, visited_files)
|
|
93
|
-
|
|
94
|
-
tree
|
|
97
|
+
# @return [Hash] Raw parse tree
|
|
98
|
+
def parse_content_to_tree(content)
|
|
99
|
+
RncParser.new.parse(RncParser.preprocess_hex_escapes(content.strip))
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
# Process include directives by recursively parsing included files
|
|
@@ -140,27 +145,7 @@ module Rng
|
|
|
140
145
|
href = extract_string_literal(include_item[:href])
|
|
141
146
|
override = parse_override(include_item[:override])
|
|
142
147
|
|
|
143
|
-
|
|
144
|
-
included_file_path = base_dir ? File.join(base_dir, href) : href
|
|
145
|
-
|
|
146
|
-
# Parse included file recursively
|
|
147
|
-
included_tree = parse_file_to_tree(included_file_path, base_dir,
|
|
148
|
-
visited_files.dup)
|
|
149
|
-
|
|
150
|
-
# Process raw grammar/override/patterns before extracting
|
|
151
|
-
process_raw_nodes!(included_tree)
|
|
152
|
-
|
|
153
|
-
# Extract grammar from included tree
|
|
154
|
-
included_grammar = extract_grammar_tree(included_tree)
|
|
155
|
-
|
|
156
|
-
# Recursively process includes in the included file
|
|
157
|
-
if included_grammar[:includes] && !included_grammar[:includes].empty?
|
|
158
|
-
process_includes(included_tree, File.dirname(included_file_path),
|
|
159
|
-
visited_files.dup)
|
|
160
|
-
|
|
161
|
-
# Re-extract grammar after processing its includes
|
|
162
|
-
included_grammar = extract_grammar_tree(included_tree)
|
|
163
|
-
end
|
|
148
|
+
included_grammar = resolve_included_grammar(href, base_dir, visited_files.dup)
|
|
164
149
|
|
|
165
150
|
# Merge included definitions into temporary grammar_tree
|
|
166
151
|
merge_included_grammar(grammar_tree, included_grammar, override)
|
|
@@ -191,28 +176,7 @@ module Rng
|
|
|
191
176
|
href = extract_string_literal(include_item[:href])
|
|
192
177
|
override = parse_override(include_item[:override])
|
|
193
178
|
|
|
194
|
-
|
|
195
|
-
included_file_path = base_dir ? File.join(base_dir, href) : href
|
|
196
|
-
|
|
197
|
-
# Parse included file recursively
|
|
198
|
-
included_tree = parse_file_to_tree(included_file_path, base_dir,
|
|
199
|
-
visited_files.dup)
|
|
200
|
-
|
|
201
|
-
# Process raw grammar/override/patterns before extracting
|
|
202
|
-
process_raw_nodes!(included_tree)
|
|
203
|
-
|
|
204
|
-
# Extract grammar from included tree
|
|
205
|
-
included_grammar = extract_grammar_tree(included_tree)
|
|
206
|
-
|
|
207
|
-
# Recursively process includes in the included file
|
|
208
|
-
if included_grammar[:includes] && !included_grammar[:includes].empty?
|
|
209
|
-
# Process includes in the included file
|
|
210
|
-
process_includes(included_tree, File.dirname(included_file_path),
|
|
211
|
-
visited_files.dup)
|
|
212
|
-
|
|
213
|
-
# Re-extract grammar after processing its includes
|
|
214
|
-
included_grammar = extract_grammar_tree(included_tree)
|
|
215
|
-
end
|
|
179
|
+
included_grammar = resolve_included_grammar(href, base_dir, visited_files.dup)
|
|
216
180
|
|
|
217
181
|
# Merge included definitions into current tree
|
|
218
182
|
merge_included_grammar(grammar_tree, included_grammar, override)
|
|
@@ -230,6 +194,16 @@ module Rng
|
|
|
230
194
|
end
|
|
231
195
|
end
|
|
232
196
|
|
|
197
|
+
def resolve_included_grammar(file_path, base_dir, visited_files)
|
|
198
|
+
abs_path = track_visited_file(file_path, base_dir, visited_files)
|
|
199
|
+
raise "Include file not found: #{abs_path}" unless File.exist?(abs_path)
|
|
200
|
+
|
|
201
|
+
tree = parse_content_to_tree(File.read(abs_path))
|
|
202
|
+
process_raw_nodes!(tree)
|
|
203
|
+
process_includes(tree, File.dirname(abs_path), visited_files)
|
|
204
|
+
build_grammar_tree(tree)
|
|
205
|
+
end
|
|
206
|
+
|
|
233
207
|
# Process raw_grammar/raw_override/raw_patterns nodes in-place
|
|
234
208
|
#
|
|
235
209
|
# This is needed when included files contain grammar blocks or
|
data/lib/rng/rnc_builder.rb
CHANGED
|
@@ -426,21 +426,10 @@ module Rng
|
|
|
426
426
|
if attr.value
|
|
427
427
|
value_str = attr.value.is_a?(Value) ? attr.value.value : attr.value.to_s
|
|
428
428
|
result += "\"#{value_str}\""
|
|
429
|
-
# Check for choice
|
|
429
|
+
# Check for choice (values, text, refs, etc.) — delegate to the
|
|
430
|
+
# canonical choice builder so every member type is preserved
|
|
430
431
|
elsif attr.choice
|
|
431
|
-
|
|
432
|
-
choice_obj = attr.choice
|
|
433
|
-
if choice_obj.value.is_a?(Array)
|
|
434
|
-
# Choice with array of Value objects
|
|
435
|
-
values = choice_obj.value.map do |v|
|
|
436
|
-
v_str = v.is_a?(Value) ? v.value : v.to_s
|
|
437
|
-
"\"#{escape_rnc_string(v_str)}\""
|
|
438
|
-
end
|
|
439
|
-
result += values.join(' | ')
|
|
440
|
-
else
|
|
441
|
-
# Other choice patterns
|
|
442
|
-
result += build_pattern(attr.choice)
|
|
443
|
-
end
|
|
432
|
+
result += build_choice(attr.choice)
|
|
444
433
|
# Check for datatype
|
|
445
434
|
elsif attr.data
|
|
446
435
|
result += if attr.data.type
|
data/lib/rng/rnc_parser.rb
CHANGED
|
@@ -261,6 +261,23 @@ module Rng
|
|
|
261
261
|
(whitespace >> annotation).repeat(1)
|
|
262
262
|
end
|
|
263
263
|
|
|
264
|
+
# Match a `[ ... ]` annotation WITHOUT capturing anything (produces a raw
|
|
265
|
+
# slice that is discarded in a sequence). Strings and nested brackets are
|
|
266
|
+
# matched as raw characters so no `.as` capture turns the surrounding
|
|
267
|
+
# content item into an array. Used to drop a content-position annotation.
|
|
268
|
+
rule(:annotation_skip) do
|
|
269
|
+
str('[') >> annotation_skip_inner.repeat >> str(']')
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
rule(:annotation_skip_inner) do
|
|
273
|
+
(str('"""') >> (str('"""').absent? >> any).repeat >> str('"""')) |
|
|
274
|
+
(str("'''") >> (str("'''").absent? >> any).repeat >> str("'''")) |
|
|
275
|
+
(str('"') >> ((str('\\') >> any) | (str('"').absent? >> match('[\u0000-\u001F\u007F]').absent? >> any)).repeat >> str('"')) |
|
|
276
|
+
(str("'") >> ((str('\\') >> any) | (str("'").absent? >> match('[\u0000-\u001F\u007F]').absent? >> any)).repeat >> str("'")) |
|
|
277
|
+
(str('[') >> annotation_skip_inner.repeat >> str(']')) |
|
|
278
|
+
(str('[').absent? >> str(']').absent? >> str('"').absent? >> str("'").absent? >> any)
|
|
279
|
+
end
|
|
280
|
+
|
|
264
281
|
# Notation/annotation: [ key = "value" ] or just [ ... ]
|
|
265
282
|
# Notations are only valid when attached to patterns using >>, not as standalone preamble items
|
|
266
283
|
rule(:notation) do
|
|
@@ -312,14 +329,18 @@ module Rng
|
|
|
312
329
|
value_literal >>
|
|
313
330
|
(whitespace >> str('|') >> whitespace >> value_literal).repeat(1) >>
|
|
314
331
|
whitespace >> str(')')).as(:datatype_except) |
|
|
315
|
-
# Non-parenthesized choice of
|
|
316
|
-
(
|
|
332
|
+
# Non-parenthesized choice of items: "a" | "b", text | "a", "a" | ref, text | ref
|
|
333
|
+
(attribute_choice_item >> (whitespace >> str('|') >> whitespace >> attribute_choice_item).repeat(1).as(:attribute_choice)) |
|
|
317
334
|
value_literal |
|
|
318
335
|
datatype_ref |
|
|
319
336
|
keyword('text').as(:text_type) |
|
|
320
337
|
identifier.as(:ref)
|
|
321
338
|
end
|
|
322
339
|
|
|
340
|
+
rule(:attribute_choice_item) do
|
|
341
|
+
keyword('text').as(:text_type) | value_literal | identifier.as(:ref)
|
|
342
|
+
end
|
|
343
|
+
|
|
323
344
|
rule(:datatype_ref) do
|
|
324
345
|
identifier.as(:prefix) >> str(':') >> identifier.as(:type) >>
|
|
325
346
|
(whitespace >> str('{') >> whitespace >>
|
|
@@ -381,7 +402,7 @@ module Rng
|
|
|
381
402
|
# Named pattern definition (e.g., "myPattern = element foo { text }")
|
|
382
403
|
rule(:named_pattern) do
|
|
383
404
|
(doc_comments >> whitespace).maybe.as(:docs) >>
|
|
384
|
-
|
|
405
|
+
(annotation_skip >> whitespace).repeat >>
|
|
385
406
|
identifier.as(:name) >> whitespace >>
|
|
386
407
|
(str('|=') | str('&=') | str('=')).as(:operator) >> whitespace >>
|
|
387
408
|
pattern_list.as(:pattern)
|
|
@@ -402,14 +423,18 @@ module Rng
|
|
|
402
423
|
(
|
|
403
424
|
(whitespace >> str('&') >> whitespace >> content_item).repeat(1).as(:interleave_items) |
|
|
404
425
|
(whitespace >> str('|') >> whitespace >> content_item).repeat(1).as(:choice_items) |
|
|
405
|
-
|
|
426
|
+
# A comma is required between top-level sequence items; a comma-less
|
|
427
|
+
# continuation would greedily swallow the following definition.
|
|
428
|
+
(whitespace >> comma >> whitespace >> content_item).repeat(1).as(:sequence_items)
|
|
406
429
|
).maybe
|
|
407
430
|
end
|
|
408
431
|
|
|
409
432
|
# Choice is handled at content level, not as separate pattern
|
|
410
433
|
rule(:content_item) do
|
|
411
434
|
(doc_comments >> whitespace).maybe >>
|
|
412
|
-
|
|
435
|
+
# Drop any content-position bracket annotation(s) before the item so the
|
|
436
|
+
# following pattern still matches (annotations are not modelled here).
|
|
437
|
+
(annotation_skip >> whitespace).repeat >>
|
|
413
438
|
(element_def | attribute_def |
|
|
414
439
|
# Datatype subtraction: identifier - ( value|identifier|choice|annotated )
|
|
415
440
|
(identifier.as(:datatype_name) >>
|
|
@@ -668,18 +693,13 @@ module Rng
|
|
|
668
693
|
result
|
|
669
694
|
end
|
|
670
695
|
|
|
696
|
+
# Parse RNC content into a Grammar.
|
|
697
|
+
#
|
|
698
|
+
# Delegates to {IncludeProcessor#parse_content} so that this entry point
|
|
699
|
+
# and {Rng.parse_rnc} share a single canonical pipeline. With no location,
|
|
700
|
+
# include directives are not resolved.
|
|
671
701
|
def self.parse(input)
|
|
672
|
-
|
|
673
|
-
preprocessed = preprocess_hex_escapes(input.strip)
|
|
674
|
-
tree = parser.parse(preprocessed)
|
|
675
|
-
|
|
676
|
-
# Normalize parse tree
|
|
677
|
-
processor = ParseTreeProcessor.new(tree)
|
|
678
|
-
normalized = processor.normalize
|
|
679
|
-
|
|
680
|
-
# Convert to RNG XML and Grammar object
|
|
681
|
-
rng_xml = convert_to_rng(normalized.grammar_tree)
|
|
682
|
-
Grammar.from_xml(rng_xml)
|
|
702
|
+
IncludeProcessor.new.parse_content(input)
|
|
683
703
|
end
|
|
684
704
|
|
|
685
705
|
# Convert RNG schema to RNC
|