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/rnc_parser.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'parslet'
|
|
4
|
-
require 'nokogiri'
|
|
5
4
|
require 'set'
|
|
6
5
|
|
|
7
6
|
module Rng
|
|
@@ -261,6 +260,23 @@ module Rng
|
|
|
261
260
|
(whitespace >> annotation).repeat(1)
|
|
262
261
|
end
|
|
263
262
|
|
|
263
|
+
# Match a `[ ... ]` annotation WITHOUT capturing anything (produces a raw
|
|
264
|
+
# slice that is discarded in a sequence). Strings and nested brackets are
|
|
265
|
+
# matched as raw characters so no `.as` capture turns the surrounding
|
|
266
|
+
# content item into an array. Used to drop a content-position annotation.
|
|
267
|
+
rule(:annotation_skip) do
|
|
268
|
+
str('[') >> annotation_skip_inner.repeat >> str(']')
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
rule(:annotation_skip_inner) do
|
|
272
|
+
(str('"""') >> (str('"""').absent? >> any).repeat >> str('"""')) |
|
|
273
|
+
(str("'''") >> (str("'''").absent? >> any).repeat >> str("'''")) |
|
|
274
|
+
(str('"') >> ((str('\\') >> any) | (str('"').absent? >> match('[\u0000-\u001F\u007F]').absent? >> any)).repeat >> str('"')) |
|
|
275
|
+
(str("'") >> ((str('\\') >> any) | (str("'").absent? >> match('[\u0000-\u001F\u007F]').absent? >> any)).repeat >> str("'")) |
|
|
276
|
+
(str('[') >> annotation_skip_inner.repeat >> str(']')) |
|
|
277
|
+
(str('[').absent? >> str(']').absent? >> str('"').absent? >> str("'").absent? >> any)
|
|
278
|
+
end
|
|
279
|
+
|
|
264
280
|
# Notation/annotation: [ key = "value" ] or just [ ... ]
|
|
265
281
|
# Notations are only valid when attached to patterns using >>, not as standalone preamble items
|
|
266
282
|
rule(:notation) do
|
|
@@ -312,14 +328,18 @@ module Rng
|
|
|
312
328
|
value_literal >>
|
|
313
329
|
(whitespace >> str('|') >> whitespace >> value_literal).repeat(1) >>
|
|
314
330
|
whitespace >> str(')')).as(:datatype_except) |
|
|
315
|
-
# Non-parenthesized choice of
|
|
316
|
-
(
|
|
331
|
+
# Non-parenthesized choice of items: "a" | "b", text | "a", "a" | ref, text | ref
|
|
332
|
+
(attribute_choice_item >> (whitespace >> str('|') >> whitespace >> attribute_choice_item).repeat(1).as(:attribute_choice)) |
|
|
317
333
|
value_literal |
|
|
318
334
|
datatype_ref |
|
|
319
335
|
keyword('text').as(:text_type) |
|
|
320
336
|
identifier.as(:ref)
|
|
321
337
|
end
|
|
322
338
|
|
|
339
|
+
rule(:attribute_choice_item) do
|
|
340
|
+
keyword('text').as(:text_type) | value_literal | identifier.as(:ref)
|
|
341
|
+
end
|
|
342
|
+
|
|
323
343
|
rule(:datatype_ref) do
|
|
324
344
|
identifier.as(:prefix) >> str(':') >> identifier.as(:type) >>
|
|
325
345
|
(whitespace >> str('{') >> whitespace >>
|
|
@@ -381,7 +401,7 @@ module Rng
|
|
|
381
401
|
# Named pattern definition (e.g., "myPattern = element foo { text }")
|
|
382
402
|
rule(:named_pattern) do
|
|
383
403
|
(doc_comments >> whitespace).maybe.as(:docs) >>
|
|
384
|
-
|
|
404
|
+
(annotation_skip >> whitespace).repeat >>
|
|
385
405
|
identifier.as(:name) >> whitespace >>
|
|
386
406
|
(str('|=') | str('&=') | str('=')).as(:operator) >> whitespace >>
|
|
387
407
|
pattern_list.as(:pattern)
|
|
@@ -402,14 +422,18 @@ module Rng
|
|
|
402
422
|
(
|
|
403
423
|
(whitespace >> str('&') >> whitespace >> content_item).repeat(1).as(:interleave_items) |
|
|
404
424
|
(whitespace >> str('|') >> whitespace >> content_item).repeat(1).as(:choice_items) |
|
|
405
|
-
|
|
425
|
+
# A comma is required between top-level sequence items; a comma-less
|
|
426
|
+
# continuation would greedily swallow the following definition.
|
|
427
|
+
(whitespace >> comma >> whitespace >> content_item).repeat(1).as(:sequence_items)
|
|
406
428
|
).maybe
|
|
407
429
|
end
|
|
408
430
|
|
|
409
431
|
# Choice is handled at content level, not as separate pattern
|
|
410
432
|
rule(:content_item) do
|
|
411
433
|
(doc_comments >> whitespace).maybe >>
|
|
412
|
-
|
|
434
|
+
# Drop any content-position bracket annotation(s) before the item so the
|
|
435
|
+
# following pattern still matches (annotations are not modelled here).
|
|
436
|
+
(annotation_skip >> whitespace).repeat >>
|
|
413
437
|
(element_def | attribute_def |
|
|
414
438
|
# Datatype subtraction: identifier - ( value|identifier|choice|annotated )
|
|
415
439
|
(identifier.as(:datatype_name) >>
|
|
@@ -668,18 +692,13 @@ module Rng
|
|
|
668
692
|
result
|
|
669
693
|
end
|
|
670
694
|
|
|
695
|
+
# Parse RNC content into a Grammar.
|
|
696
|
+
#
|
|
697
|
+
# Delegates to {IncludeProcessor#parse_content} so that this entry point
|
|
698
|
+
# and {Rng.parse_rnc} share a single canonical pipeline. With no location,
|
|
699
|
+
# include directives are not resolved.
|
|
671
700
|
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)
|
|
701
|
+
IncludeProcessor.new.parse_content(input)
|
|
683
702
|
end
|
|
684
703
|
|
|
685
704
|
# Convert RNG schema to RNC
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'moxml'
|
|
4
4
|
require 'set'
|
|
5
5
|
|
|
6
6
|
module Rng
|
|
7
7
|
# RncToRngConverter converts RNC parse trees to RNG XML format.
|
|
8
8
|
#
|
|
9
9
|
# This class takes the parse tree output from the Parslet RNC parser and
|
|
10
|
-
# converts it to RNG XML using
|
|
10
|
+
# converts it to RNG XML using Moxml's Nokogiri-compatible XML builder.
|
|
11
|
+
# The resulting XML
|
|
11
12
|
# can then be deserialized into Grammar objects using Lutaml::Model.
|
|
12
13
|
#
|
|
13
14
|
# @example Convert a parse tree to RNG XML
|
|
@@ -17,6 +18,8 @@ module Rng
|
|
|
17
18
|
# grammar = Rng::Grammar.from_xml(rng_xml)
|
|
18
19
|
class RncToRngConverter
|
|
19
20
|
RNG_NAMESPACE = 'http://relaxng.org/ns/structure/1.0'
|
|
21
|
+
OCCURRENCE_TAGS = { '*' => 'zeroOrMore', '+' => 'oneOrMore', '?' => 'optional' }.freeze
|
|
22
|
+
private_constant :OCCURRENCE_TAGS
|
|
20
23
|
|
|
21
24
|
# Convert a parse tree to RNG XML
|
|
22
25
|
#
|
|
@@ -36,65 +39,37 @@ module Rng
|
|
|
36
39
|
# Validate that element notations in preamble annotations are only used with element/attribute patterns
|
|
37
40
|
validate_preamble_element_notation_usage(tree[:preamble_items], tree[:definitions])
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
# Build without Context#build: its block is instance_eval'd, which
|
|
43
|
+
# would re-bind self and turn every converter helper call into a
|
|
44
|
+
# builder method_missing tag. A lambda preserves self; Moxml's tag
|
|
45
|
+
# blocks use block.call, so caller context survives down the tree.
|
|
46
|
+
builder = Moxml::Builder.new(Moxml::Context.new)
|
|
47
|
+
emit = lambda do |xml|
|
|
48
|
+
xml.declaration
|
|
40
49
|
grammar_attrs = { xmlns: 'http://relaxng.org/ns/structure/1.0' }
|
|
41
50
|
if @has_documentation
|
|
42
51
|
grammar_attrs[:'xmlns:a'] =
|
|
43
52
|
'http://relaxng.org/ns/compatibility/annotations/1.0'
|
|
44
53
|
end
|
|
54
|
+
if tree[:namespace]
|
|
55
|
+
grammar_attrs[:ns] =
|
|
56
|
+
process_string_literal(tree[:namespace][:namespace_uri])
|
|
57
|
+
end
|
|
58
|
+
if tree[:datatype_library]
|
|
59
|
+
grammar_attrs[:datatypeLibrary] =
|
|
60
|
+
process_string_literal(tree[:datatype_library][:uri])
|
|
61
|
+
elsif tree[:datatype_map] && !tree[:datatype_map].empty?
|
|
62
|
+
# From ParseTreeProcessor - datatype_map is {prefix => uri}
|
|
63
|
+
# Use the first (typically only) datatype library
|
|
64
|
+
grammar_attrs[:datatypeLibrary] = tree[:datatype_map].values.first
|
|
65
|
+
end
|
|
45
66
|
|
|
46
67
|
xml.grammar(grammar_attrs) do
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Add datatype library if present
|
|
54
|
-
if tree[:datatype_library]
|
|
55
|
-
xml.parent[:datatypeLibrary] =
|
|
56
|
-
process_string_literal(tree[:datatype_library][:uri])
|
|
57
|
-
elsif tree[:datatype_map] && !tree[:datatype_map].empty?
|
|
58
|
-
# From ParseTreeProcessor - datatype_map is {prefix => uri}
|
|
59
|
-
# Use the first (typically only) datatype library
|
|
60
|
-
xml.parent[:datatypeLibrary] = tree[:datatype_map].values.first
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# If no explicit start but we have top-level elements, wrap them in start
|
|
64
|
-
has_explicit_start = tree[:start] && tree[:start][:start_pattern]
|
|
65
|
-
has_top_elements = tree[:definitions]&.any? do |d|
|
|
66
|
-
d.key?(:top_element)
|
|
67
|
-
end
|
|
68
|
-
has_top_choice = tree[:definitions]&.any? do |d|
|
|
69
|
-
d.key?(:top_choice)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
if has_explicit_start
|
|
73
|
-
# Process explicit start pattern
|
|
74
|
-
xml.start do
|
|
75
|
-
add_documentation(xml, tree[:start]) if tree[:start][:docs]
|
|
76
|
-
process_pattern_list(xml, tree[:start][:start_pattern])
|
|
77
|
-
end
|
|
78
|
-
elsif has_top_choice
|
|
79
|
-
# No explicit start, but has top-level choice - wrap in start with choice
|
|
80
|
-
first_choice = tree[:definitions].find { |d| d.key?(:top_choice) }
|
|
81
|
-
xml.start do
|
|
82
|
-
xml.choice do
|
|
83
|
-
process_content_item(xml, first_choice[:top_choice][:first])
|
|
84
|
-
first_choice[:top_choice][:choice_items]&.each do |item|
|
|
85
|
-
process_content_item(xml, item)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
elsif has_top_elements
|
|
90
|
-
# No explicit start, but has top-level elements - wrap first one in start
|
|
91
|
-
xml.start do
|
|
92
|
-
first_element = tree[:definitions].find do |d|
|
|
93
|
-
d.key?(:top_element)
|
|
94
|
-
end
|
|
95
|
-
process_content_item(xml, first_element[:top_element])
|
|
96
|
-
end
|
|
97
|
-
end
|
|
68
|
+
# A leading `start` (tree[:start]) or a non-leading one (emitted as
|
|
69
|
+
# <start> in the definitions loop) both suppress auto-wrapping.
|
|
70
|
+
has_explicit_start =
|
|
71
|
+
(tree[:start] && tree[:start][:start_pattern]) || nonleading_start?(tree)
|
|
72
|
+
emit_grammar_start(xml, tree)
|
|
98
73
|
|
|
99
74
|
# Process named patterns and remaining top-level elements
|
|
100
75
|
tree[:definitions]&.each_with_index do |def_item, idx|
|
|
@@ -162,6 +137,8 @@ module Rng
|
|
|
162
137
|
# Include without override block
|
|
163
138
|
xml.include(href: href)
|
|
164
139
|
end
|
|
140
|
+
elsif def_item.key?(:name) && keyword_start_name?(def_item[:name])
|
|
141
|
+
emit_nonleading_start(xml, def_item)
|
|
165
142
|
elsif def_item.key?(:name)
|
|
166
143
|
# Named pattern - handle augmentation operators
|
|
167
144
|
name = process_identifier(def_item[:name])
|
|
@@ -204,7 +181,8 @@ module Rng
|
|
|
204
181
|
end
|
|
205
182
|
end
|
|
206
183
|
|
|
207
|
-
builder
|
|
184
|
+
emit.call(builder)
|
|
185
|
+
builder.document.to_xml
|
|
208
186
|
end
|
|
209
187
|
|
|
210
188
|
private
|
|
@@ -256,6 +234,11 @@ module Rng
|
|
|
256
234
|
end
|
|
257
235
|
|
|
258
236
|
# Add annotations (foreign attributes and elements) if present
|
|
237
|
+
#
|
|
238
|
+
# NOTE: currently uncalled; kept for upcoming annotation emission. The
|
|
239
|
+
# foreign-attribute branch below needs the enclosing element, which the
|
|
240
|
+
# Moxml builder does not expose yet — port it together with that hook
|
|
241
|
+
# rather than reintroducing Nokogiri-style parent mutation.
|
|
259
242
|
def add_annotations(xml, item, processor = nil)
|
|
260
243
|
return unless item[:annotations]
|
|
261
244
|
|
|
@@ -440,6 +423,66 @@ module Rng
|
|
|
440
423
|
end
|
|
441
424
|
end
|
|
442
425
|
|
|
426
|
+
# Emit the grammar's <start>: an explicit leading start, or (when there is
|
|
427
|
+
# none) auto-wrap the first top-level choice/element. A non-leading start is
|
|
428
|
+
# emitted by the definitions loop, so here it only suppresses auto-wrapping.
|
|
429
|
+
def emit_grammar_start(xml, tree)
|
|
430
|
+
if tree[:start] && tree[:start][:start_pattern]
|
|
431
|
+
xml.start do
|
|
432
|
+
add_documentation(xml, tree[:start]) if tree[:start][:docs]
|
|
433
|
+
process_pattern_list(xml, tree[:start][:start_pattern])
|
|
434
|
+
end
|
|
435
|
+
elsif nonleading_start?(tree)
|
|
436
|
+
# Emitted as <start> by the definitions loop.
|
|
437
|
+
elsif (choice = tree[:definitions]&.find { |d| d.key?(:top_choice) })
|
|
438
|
+
emit_start_from_top_choice(xml, choice)
|
|
439
|
+
elsif (element = tree[:definitions]&.find { |d| d.key?(:top_element) })
|
|
440
|
+
xml.start { process_content_item(xml, element[:top_element]) }
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def emit_start_from_top_choice(xml, first_choice)
|
|
445
|
+
xml.start do
|
|
446
|
+
xml.choice do
|
|
447
|
+
process_content_item(xml, first_choice[:top_choice][:first])
|
|
448
|
+
first_choice[:top_choice][:choice_items]&.each do |item|
|
|
449
|
+
process_content_item(xml, item)
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# True when a `start = X` / `start |= X` appears outside the leading
|
|
456
|
+
# position and was parsed as a named pattern named "start".
|
|
457
|
+
def nonleading_start?(tree)
|
|
458
|
+
tree[:definitions]&.any? do |d|
|
|
459
|
+
d.key?(:name) && keyword_start_name?(d[:name])
|
|
460
|
+
end || false
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# True when a named-pattern name is the `start` keyword, and NOT an escaped
|
|
464
|
+
# identifier `\start` (a define literally named "start", which must stay a
|
|
465
|
+
# <define>). The escape survives as a backslash_escape identifier part.
|
|
466
|
+
def keyword_start_name?(name_node)
|
|
467
|
+
process_identifier(name_node) == 'start' && !escaped_identifier?(name_node)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def escaped_identifier?(name_node)
|
|
471
|
+
parts = name_node.is_a?(Hash) ? name_node[:identifier_parts] : nil
|
|
472
|
+
Array(parts).any? { |p| p.is_a?(Hash) && p.key?(:backslash_escape) }
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Emit a real <start> for a non-leading `start = X` / `start |= X` that was
|
|
476
|
+
# parsed as a named pattern (a <define name="start"> would be wrong).
|
|
477
|
+
def emit_nonleading_start(xml, def_item)
|
|
478
|
+
op = def_item[:operator] ? extract_string(def_item[:operator]) : '='
|
|
479
|
+
combine = { '|=' => 'choice', '&=' => 'interleave' }[op]
|
|
480
|
+
xml.start(combine ? { combine: combine } : {}) do
|
|
481
|
+
add_documentation(xml, def_item) if def_item[:docs]
|
|
482
|
+
process_pattern_list(xml, def_item[:pattern])
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
443
486
|
# Collect prefixed namespace declarations from preamble items
|
|
444
487
|
def collect_namespace_prefixes(preamble_items)
|
|
445
488
|
return unless preamble_items
|
|
@@ -707,7 +750,7 @@ module Rng
|
|
|
707
750
|
# Bare anyName wildcard: *
|
|
708
751
|
any_name_info = item[:bare_any_name]
|
|
709
752
|
except_clause = any_name_info[:any_name_except]
|
|
710
|
-
xml.element do
|
|
753
|
+
xml.element({}) do
|
|
711
754
|
xml.anyName do
|
|
712
755
|
if except_clause
|
|
713
756
|
xml.except_ do
|
|
@@ -722,6 +765,13 @@ module Rng
|
|
|
722
765
|
end
|
|
723
766
|
end
|
|
724
767
|
|
|
768
|
+
def wrap_with_occurrence(xml, occurrence, &block)
|
|
769
|
+
tag = OCCURRENCE_TAGS[occurrence.to_s] if occurrence
|
|
770
|
+
return yield unless tag
|
|
771
|
+
|
|
772
|
+
xml.send(tag, &block)
|
|
773
|
+
end
|
|
774
|
+
|
|
725
775
|
def process_content_item(xml, item)
|
|
726
776
|
if item.key?(:type) && item.key?(:name)
|
|
727
777
|
# Attribute definition (has both :type and :name keys)
|
|
@@ -821,19 +871,7 @@ module Rng
|
|
|
821
871
|
end
|
|
822
872
|
end
|
|
823
873
|
|
|
824
|
-
|
|
825
|
-
# Wrap in occurrence element
|
|
826
|
-
occurrence_tag = case occurrence.to_s
|
|
827
|
-
when '*' then 'zeroOrMore'
|
|
828
|
-
when '+' then 'oneOrMore'
|
|
829
|
-
when '?' then 'optional'
|
|
830
|
-
end
|
|
831
|
-
xml.send(occurrence_tag) do
|
|
832
|
-
attribute_block.call(xml)
|
|
833
|
-
end
|
|
834
|
-
else
|
|
835
|
-
attribute_block.call(xml)
|
|
836
|
-
end
|
|
874
|
+
wrap_with_occurrence(xml, occurrence) { attribute_block.call(xml) }
|
|
837
875
|
elsif item.key?(:name)
|
|
838
876
|
# Element definition
|
|
839
877
|
attrs = {}
|
|
@@ -886,7 +924,7 @@ module Rng
|
|
|
886
924
|
case element_name_type
|
|
887
925
|
when :any_name
|
|
888
926
|
# Generate <anyName> with optional <except>
|
|
889
|
-
xml_ctx.element do
|
|
927
|
+
xml_ctx.element({}) do
|
|
890
928
|
add_documentation(xml_ctx, item) if item[:docs]
|
|
891
929
|
xml_ctx.anyName do
|
|
892
930
|
if except_clause
|
|
@@ -899,7 +937,7 @@ module Rng
|
|
|
899
937
|
end
|
|
900
938
|
when :ns_name
|
|
901
939
|
# Generate <nsName> with ns attribute and optional <except>
|
|
902
|
-
xml_ctx.element do
|
|
940
|
+
xml_ctx.element({}) do
|
|
903
941
|
add_documentation(xml_ctx, item) if item[:docs]
|
|
904
942
|
xml_ctx.nsName(ns: ns_prefix) do
|
|
905
943
|
if except_clause
|
|
@@ -912,7 +950,7 @@ module Rng
|
|
|
912
950
|
end
|
|
913
951
|
when :name_choice
|
|
914
952
|
# Element with choice of names (e.g., element foo|bar { ... })
|
|
915
|
-
xml_ctx.element do
|
|
953
|
+
xml_ctx.element({}) do
|
|
916
954
|
add_documentation(xml_ctx, item) if item[:docs]
|
|
917
955
|
xml_ctx.choice do
|
|
918
956
|
choice_names.each do |name_part|
|
|
@@ -939,26 +977,19 @@ module Rng
|
|
|
939
977
|
end
|
|
940
978
|
end
|
|
941
979
|
|
|
942
|
-
|
|
943
|
-
# Wrap in occurrence element
|
|
944
|
-
occurrence_tag = case occurrence.to_s
|
|
945
|
-
when '*' then 'zeroOrMore'
|
|
946
|
-
when '+' then 'oneOrMore'
|
|
947
|
-
when '?' then 'optional'
|
|
948
|
-
end
|
|
949
|
-
|
|
950
|
-
xml.send(occurrence_tag) do
|
|
951
|
-
element_block.call(xml)
|
|
952
|
-
end
|
|
953
|
-
else
|
|
954
|
-
element_block.call(xml)
|
|
955
|
-
end
|
|
980
|
+
wrap_with_occurrence(xml, occurrence) { element_block.call(xml) }
|
|
956
981
|
elsif item.key?(:text)
|
|
957
|
-
xml
|
|
982
|
+
wrap_with_occurrence(xml, item[:occurrence]) do
|
|
983
|
+
xml.text_
|
|
984
|
+
end
|
|
958
985
|
elsif item.key?(:empty)
|
|
959
|
-
xml
|
|
986
|
+
wrap_with_occurrence(xml, item[:occurrence]) do
|
|
987
|
+
xml.empty_
|
|
988
|
+
end
|
|
960
989
|
elsif item.key?(:not_allowed)
|
|
961
|
-
xml
|
|
990
|
+
wrap_with_occurrence(xml, item[:occurrence]) do
|
|
991
|
+
xml.notAllowed_
|
|
992
|
+
end
|
|
962
993
|
elsif item.key?(:list_content)
|
|
963
994
|
xml.list do
|
|
964
995
|
process_list_content(xml, item[:list_content])
|
|
@@ -968,21 +999,7 @@ module Rng
|
|
|
968
999
|
elsif item.key?(:external_href)
|
|
969
1000
|
xml.externalRef(href: process_string_literal(item[:external_href]))
|
|
970
1001
|
elsif item.key?(:group)
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
if occurrence
|
|
974
|
-
occurrence_tag = case occurrence.to_s
|
|
975
|
-
when '*' then 'zeroOrMore'
|
|
976
|
-
when '+' then 'oneOrMore'
|
|
977
|
-
when '?' then 'optional'
|
|
978
|
-
end
|
|
979
|
-
|
|
980
|
-
xml.send(occurrence_tag) do
|
|
981
|
-
xml.group do
|
|
982
|
-
process_element_content(xml, item[:group])
|
|
983
|
-
end
|
|
984
|
-
end
|
|
985
|
-
else
|
|
1002
|
+
wrap_with_occurrence(xml, item[:occurrence]) do
|
|
986
1003
|
xml.group do
|
|
987
1004
|
process_element_content(xml, item[:group])
|
|
988
1005
|
end
|
|
@@ -997,28 +1014,34 @@ module Rng
|
|
|
997
1014
|
ref_name = process_identifier(item[:ref])
|
|
998
1015
|
raise StandardError, "subtraction operator '-' cannot be used as a pattern" if ref_name == '-'
|
|
999
1016
|
|
|
1000
|
-
xml
|
|
1017
|
+
wrap_with_occurrence(xml, item[:occurrence]) do
|
|
1018
|
+
xml.ref(name: ref_name)
|
|
1019
|
+
end
|
|
1001
1020
|
elsif item.key?(:prefix) && item.key?(:type)
|
|
1002
1021
|
# Datatype reference (e.g., xsd:string { maxLength = "100" })
|
|
1003
1022
|
data_attrs = {
|
|
1004
1023
|
type: process_identifier(item[:type]),
|
|
1005
1024
|
datatypeLibrary: 'http://www.w3.org/2001/XMLSchema-datatypes'
|
|
1006
1025
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1026
|
+
wrap_with_occurrence(xml, item[:occurrence]) do
|
|
1027
|
+
if item[:params]
|
|
1028
|
+
xml.data(data_attrs) do
|
|
1029
|
+
params = item[:params].is_a?(Array) ? item[:params] : [item[:params]]
|
|
1030
|
+
params.each do |param|
|
|
1031
|
+
param_name = process_identifier(param[:param_name])
|
|
1032
|
+
param_value = process_string_literal(param[:param_value])
|
|
1033
|
+
xml.param(param_value, name: param_name)
|
|
1034
|
+
end
|
|
1014
1035
|
end
|
|
1036
|
+
else
|
|
1037
|
+
xml.data(data_attrs)
|
|
1015
1038
|
end
|
|
1016
|
-
else
|
|
1017
|
-
xml.data(data_attrs)
|
|
1018
1039
|
end
|
|
1019
1040
|
elsif item.key?(:value)
|
|
1020
1041
|
# Value literal (string) in element content
|
|
1021
|
-
xml
|
|
1042
|
+
wrap_with_occurrence(xml, item[:occurrence]) do
|
|
1043
|
+
xml.value(process_string_literal(item[:value]))
|
|
1044
|
+
end
|
|
1022
1045
|
elsif item.key?(:grammar_block)
|
|
1023
1046
|
# Inline grammar block
|
|
1024
1047
|
grammar_data = item[:grammar_block]
|
|
@@ -1158,11 +1181,9 @@ module Rng
|
|
|
1158
1181
|
|
|
1159
1182
|
# Process a single list content item (datatype or text with optional occurrence)
|
|
1160
1183
|
def process_list_item(xml, item)
|
|
1161
|
-
occurrence = item[:occurrence]&.to_s
|
|
1162
|
-
|
|
1163
1184
|
item_block = lambda do |xml_ctx|
|
|
1164
1185
|
if item.key?(:text)
|
|
1165
|
-
xml_ctx.
|
|
1186
|
+
xml_ctx.text_
|
|
1166
1187
|
elsif item.key?(:prefix)
|
|
1167
1188
|
# Datatype reference
|
|
1168
1189
|
data_attrs = {
|
|
@@ -1179,18 +1200,7 @@ module Rng
|
|
|
1179
1200
|
end
|
|
1180
1201
|
end
|
|
1181
1202
|
|
|
1182
|
-
|
|
1183
|
-
occurrence_tag = case occurrence
|
|
1184
|
-
when '*' then 'zeroOrMore'
|
|
1185
|
-
when '+' then 'oneOrMore'
|
|
1186
|
-
when '?' then 'optional'
|
|
1187
|
-
end
|
|
1188
|
-
xml.send(occurrence_tag) do
|
|
1189
|
-
item_block.call(xml)
|
|
1190
|
-
end
|
|
1191
|
-
else
|
|
1192
|
-
item_block.call(xml)
|
|
1193
|
-
end
|
|
1203
|
+
wrap_with_occurrence(xml, item[:occurrence]) { item_block.call(xml) }
|
|
1194
1204
|
end
|
|
1195
1205
|
|
|
1196
1206
|
# Process name except clause for wildcards
|
|
@@ -1243,18 +1253,17 @@ module Rng
|
|
|
1243
1253
|
|
|
1244
1254
|
# Process attribute type content (factored out for wildcard support)
|
|
1245
1255
|
def process_attribute_type(xml, type_info)
|
|
1246
|
-
if type_info
|
|
1247
|
-
|
|
1248
|
-
elsif type_info.is_a?(Hash) && type_info.key?(:value_choice)
|
|
1249
|
-
# Choice of value literals
|
|
1256
|
+
if type_info.is_a?(Hash) && type_info.key?(:attribute_choice)
|
|
1257
|
+
# Choice of attribute items (text / value literals / refs)
|
|
1250
1258
|
xml.choice do
|
|
1251
|
-
#
|
|
1252
|
-
xml
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
xml.value(process_string_literal(val[:value]))
|
|
1259
|
+
# The first item is hoisted alongside the rest of the choice
|
|
1260
|
+
emit_attribute_choice_item(xml, type_info)
|
|
1261
|
+
type_info[:attribute_choice].each do |item|
|
|
1262
|
+
emit_attribute_choice_item(xml, item)
|
|
1256
1263
|
end
|
|
1257
1264
|
end
|
|
1265
|
+
elsif type_info == 'text' || (type_info.is_a?(Hash) && type_info.key?(:text_type))
|
|
1266
|
+
xml.text_
|
|
1258
1267
|
elsif type_info.is_a?(Hash) && type_info.key?(:value)
|
|
1259
1268
|
# Single value literal
|
|
1260
1269
|
xml.value(process_string_literal(type_info[:value]))
|
|
@@ -1282,6 +1291,16 @@ module Rng
|
|
|
1282
1291
|
end
|
|
1283
1292
|
end
|
|
1284
1293
|
|
|
1294
|
+
def emit_attribute_choice_item(xml, item)
|
|
1295
|
+
if item.key?(:text_type)
|
|
1296
|
+
xml.text_
|
|
1297
|
+
elsif item.key?(:value)
|
|
1298
|
+
xml.value(process_string_literal(item[:value]))
|
|
1299
|
+
elsif item.key?(:ref)
|
|
1300
|
+
xml.ref(name: process_identifier(item[:ref]))
|
|
1301
|
+
end
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1285
1304
|
# Process data parameters (e.g., maxLength = "100")
|
|
1286
1305
|
def process_data_params(xml, params)
|
|
1287
1306
|
params = [params] unless params.is_a?(Array)
|
|
@@ -1402,7 +1421,7 @@ module Rng
|
|
|
1402
1421
|
end
|
|
1403
1422
|
|
|
1404
1423
|
# Emit as an empty foreign element (content is annotation-only, ignored in RNG)
|
|
1405
|
-
xml.
|
|
1424
|
+
xml.create_tag(element_name)
|
|
1406
1425
|
end
|
|
1407
1426
|
end
|
|
1408
1427
|
end
|