rng 0.1.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/release.yml +8 -3
  4. data/.gitignore +11 -0
  5. data/.rubocop.yml +11 -6
  6. data/.rubocop_todo.yml +270 -0
  7. data/CHANGELOG.md +317 -0
  8. data/CLAUDE.md +139 -0
  9. data/CODE_OF_CONDUCT.md +132 -0
  10. data/Gemfile +11 -10
  11. data/README.adoc +1929 -0
  12. data/Rakefile +11 -3
  13. data/docs/Gemfile +8 -0
  14. data/docs/_config.yml +23 -0
  15. data/docs/getting-started/index.adoc +75 -0
  16. data/docs/guides/error-handling.adoc +137 -0
  17. data/docs/guides/external-references.adoc +128 -0
  18. data/docs/guides/index.adoc +24 -0
  19. data/docs/guides/parsing-rnc.adoc +141 -0
  20. data/docs/guides/parsing-rng-xml.adoc +81 -0
  21. data/docs/guides/rng-to-rnc.adoc +101 -0
  22. data/docs/guides/validation.adoc +85 -0
  23. data/docs/index.adoc +52 -0
  24. data/docs/reference/api.adoc +126 -0
  25. data/docs/reference/cli.adoc +182 -0
  26. data/docs/understanding/architecture.adoc +58 -0
  27. data/docs/understanding/rng-vs-rnc.adoc +118 -0
  28. data/exe/rng +5 -0
  29. data/lib/rng/any_name.rb +28 -0
  30. data/lib/rng/attribute.rb +61 -5
  31. data/lib/rng/choice.rb +60 -0
  32. data/lib/rng/cli.rb +607 -0
  33. data/lib/rng/data.rb +32 -0
  34. data/lib/rng/datatype_declaration.rb +26 -0
  35. data/lib/rng/define.rb +56 -5
  36. data/lib/rng/div.rb +36 -0
  37. data/lib/rng/documentation.rb +9 -0
  38. data/lib/rng/element.rb +66 -18
  39. data/lib/rng/empty.rb +23 -0
  40. data/lib/rng/except.rb +62 -0
  41. data/lib/rng/external_ref.rb +28 -0
  42. data/lib/rng/external_ref_resolver.rb +582 -0
  43. data/lib/rng/foreign_attribute.rb +26 -0
  44. data/lib/rng/foreign_element.rb +33 -0
  45. data/lib/rng/grammar.rb +38 -0
  46. data/lib/rng/group.rb +62 -0
  47. data/lib/rng/include.rb +23 -0
  48. data/lib/rng/include_processor.rb +461 -0
  49. data/lib/rng/interleave.rb +58 -0
  50. data/lib/rng/list.rb +56 -0
  51. data/lib/rng/mixed.rb +58 -0
  52. data/lib/rng/name.rb +28 -0
  53. data/lib/rng/namespace_declaration.rb +47 -0
  54. data/lib/rng/namespaces.rb +15 -0
  55. data/lib/rng/not_allowed.rb +23 -0
  56. data/lib/rng/ns_name.rb +31 -0
  57. data/lib/rng/one_or_more.rb +58 -0
  58. data/lib/rng/optional.rb +58 -0
  59. data/lib/rng/param.rb +30 -0
  60. data/lib/rng/parent_ref.rb +28 -0
  61. data/lib/rng/parse_rnc.rb +26 -0
  62. data/lib/rng/parse_tree_processor.rb +695 -0
  63. data/lib/rng/pattern.rb +24 -0
  64. data/lib/rng/ref.rb +28 -0
  65. data/lib/rng/rnc_builder.rb +927 -0
  66. data/lib/rng/rnc_parser.rb +672 -115
  67. data/lib/rng/rnc_to_rng_converter.rb +1408 -0
  68. data/lib/rng/schema_preamble.rb +73 -0
  69. data/lib/rng/schema_validator.rb +1622 -0
  70. data/lib/rng/start.rb +57 -6
  71. data/lib/rng/test_suite_parser.rb +168 -0
  72. data/lib/rng/text.rb +29 -0
  73. data/lib/rng/to_rnc.rb +24 -0
  74. data/lib/rng/value.rb +28 -0
  75. data/lib/rng/version.rb +1 -1
  76. data/lib/rng/zero_or_more.rb +58 -0
  77. data/lib/rng.rb +80 -5
  78. data/rng.gemspec +19 -19
  79. data/scripts/extract_spectest_resources.rb +96 -0
  80. data/spec/fixtures/compacttest.xml +2511 -0
  81. data/spec/fixtures/external/circular_a.rng +7 -0
  82. data/spec/fixtures/external/circular_b.rng +7 -0
  83. data/spec/fixtures/external/circular_main.rng +7 -0
  84. data/spec/fixtures/external/external_ref_lib.rng +7 -0
  85. data/spec/fixtures/external/external_ref_main.rng +7 -0
  86. data/spec/fixtures/external/include_lib.rng +7 -0
  87. data/spec/fixtures/external/include_main.rng +3 -0
  88. data/spec/fixtures/external/nested_chain.rng +6 -0
  89. data/spec/fixtures/external/nested_leaf.rng +7 -0
  90. data/spec/fixtures/external/nested_mid.rng +8 -0
  91. data/spec/fixtures/metanorma/3gpp.rnc +35 -0
  92. data/spec/fixtures/metanorma/3gpp.rng +105 -0
  93. data/spec/fixtures/metanorma/basicdoc.rnc +11 -0
  94. data/spec/fixtures/metanorma/bipm.rnc +148 -0
  95. data/spec/fixtures/metanorma/bipm.rng +376 -0
  96. data/spec/fixtures/metanorma/bsi.rnc +104 -0
  97. data/spec/fixtures/metanorma/bsi.rng +332 -0
  98. data/spec/fixtures/metanorma/csa.rnc +45 -0
  99. data/spec/fixtures/metanorma/csa.rng +131 -0
  100. data/spec/fixtures/metanorma/csd.rnc +43 -0
  101. data/spec/fixtures/metanorma/csd.rng +132 -0
  102. data/spec/fixtures/metanorma/gbstandard.rnc +99 -0
  103. data/spec/fixtures/metanorma/gbstandard.rng +316 -0
  104. data/spec/fixtures/metanorma/iec.rnc +49 -0
  105. data/spec/fixtures/metanorma/iec.rng +193 -0
  106. data/spec/fixtures/metanorma/ietf.rnc +275 -0
  107. data/spec/fixtures/metanorma/ietf.rng +925 -0
  108. data/spec/fixtures/metanorma/iho.rnc +58 -0
  109. data/spec/fixtures/metanorma/iho.rng +179 -0
  110. data/spec/fixtures/metanorma/isodoc.rnc +873 -0
  111. data/spec/fixtures/metanorma/isodoc.rng +2704 -0
  112. data/spec/fixtures/metanorma/isostandard-amd.rnc +43 -0
  113. data/spec/fixtures/metanorma/isostandard-amd.rng +108 -0
  114. data/spec/fixtures/metanorma/isostandard.rnc +166 -0
  115. data/spec/fixtures/metanorma/isostandard.rng +494 -0
  116. data/spec/fixtures/metanorma/itu.rnc +122 -0
  117. data/spec/fixtures/metanorma/itu.rng +377 -0
  118. data/spec/fixtures/metanorma/m3d.rnc +41 -0
  119. data/spec/fixtures/metanorma/m3d.rng +122 -0
  120. data/spec/fixtures/metanorma/mpfd.rnc +36 -0
  121. data/spec/fixtures/metanorma/mpfd.rng +95 -0
  122. data/spec/fixtures/metanorma/nist.rnc +77 -0
  123. data/spec/fixtures/metanorma/nist.rng +216 -0
  124. data/spec/fixtures/metanorma/ogc.rnc +51 -0
  125. data/spec/fixtures/metanorma/ogc.rng +151 -0
  126. data/spec/fixtures/metanorma/reqt.rnc +6 -0
  127. data/spec/fixtures/metanorma/rsd.rnc +36 -0
  128. data/spec/fixtures/metanorma/rsd.rng +95 -0
  129. data/spec/fixtures/metanorma/un.rnc +103 -0
  130. data/spec/fixtures/metanorma/un.rng +367 -0
  131. data/spec/fixtures/rnc/address_book.rnc +10 -0
  132. data/spec/fixtures/rnc/base.rnc +4 -0
  133. data/spec/fixtures/rnc/complex_example.rnc +61 -0
  134. data/spec/fixtures/rnc/grammar_with_trailing.rnc +8 -0
  135. data/spec/fixtures/rnc/main_include_trailing.rnc +3 -0
  136. data/spec/fixtures/rnc/main_with_include.rnc +5 -0
  137. data/spec/fixtures/rnc/test_augment.rnc +10 -0
  138. data/spec/fixtures/rnc/test_isodoc_simple.rnc +9 -0
  139. data/spec/fixtures/rnc/top_level_include.rnc +8 -0
  140. data/spec/fixtures/rng/address_book.rng +20 -0
  141. data/spec/fixtures/rng/relaxng.rng +335 -0
  142. data/spec/fixtures/rng/testSuite.rng +163 -0
  143. data/spec/fixtures/spectest.xml +6845 -0
  144. data/spec/fixtures/spectest_external/case_10_4.7/x +3 -0
  145. data/spec/fixtures/spectest_external/case_10_4.7/y +7 -0
  146. data/spec/fixtures/spectest_external/case_11_4.7/x +3 -0
  147. data/spec/fixtures/spectest_external/case_12_4.7/x +3 -0
  148. data/spec/fixtures/spectest_external/case_13_4.7/x +3 -0
  149. data/spec/fixtures/spectest_external/case_13_4.7/y +3 -0
  150. data/spec/fixtures/spectest_external/case_14_4.7/x +7 -0
  151. data/spec/fixtures/spectest_external/case_15_4.7/x +7 -0
  152. data/spec/fixtures/spectest_external/case_16_4.7/x +5 -0
  153. data/spec/fixtures/spectest_external/case_17_4.7/x +5 -0
  154. data/spec/fixtures/spectest_external/case_18_4.7/x +7 -0
  155. data/spec/fixtures/spectest_external/case_19_4.7/level1.rng +9 -0
  156. data/spec/fixtures/spectest_external/case_19_4.7/level2.rng +7 -0
  157. data/spec/fixtures/spectest_external/case_1_4.5/sub1/x +3 -0
  158. data/spec/fixtures/spectest_external/case_1_4.5/sub3/x +3 -0
  159. data/spec/fixtures/spectest_external/case_1_4.5/x +3 -0
  160. data/spec/fixtures/spectest_external/case_20_4.6/x +3 -0
  161. data/spec/fixtures/spectest_external/case_2_4.5/x +3 -0
  162. data/spec/fixtures/spectest_external/case_3_4.6/x +3 -0
  163. data/spec/fixtures/spectest_external/case_4_4.6/x +3 -0
  164. data/spec/fixtures/spectest_external/case_5_4.6/x +1 -0
  165. data/spec/fixtures/spectest_external/case_6_4.6/x +5 -0
  166. data/spec/fixtures/spectest_external/case_7_4.6/x +1 -0
  167. data/spec/fixtures/spectest_external/case_7_4.6/y +1 -0
  168. data/spec/fixtures/spectest_external/case_8_4.7/x +7 -0
  169. data/spec/fixtures/spectest_external/case_9_4.7/x +7 -0
  170. data/spec/fixtures/spectest_external/resources.json +149 -0
  171. data/spec/rng/advanced_rnc_spec.rb +101 -0
  172. data/spec/rng/compacttest_spec.rb +197 -0
  173. data/spec/rng/datatype_declaration_spec.rb +28 -0
  174. data/spec/rng/div_spec.rb +207 -0
  175. data/spec/rng/external_ref_resolver_spec.rb +122 -0
  176. data/spec/rng/metanorma_conversion_spec.rb +159 -0
  177. data/spec/rng/namespace_declaration_spec.rb +60 -0
  178. data/spec/rng/namespace_support_spec.rb +199 -0
  179. data/spec/rng/rnc_parser_spec.rb +501 -23
  180. data/spec/rng/rnc_roundtrip_spec.rb +135 -0
  181. data/spec/rng/rng_generation_spec.rb +288 -0
  182. data/spec/rng/roundtrip_spec.rb +342 -0
  183. data/spec/rng/schema_preamble_spec.rb +145 -0
  184. data/spec/rng/schema_spec.rb +125 -172
  185. data/spec/rng/spectest_spec.rb +273 -0
  186. data/spec/rng_spec.rb +2 -2
  187. data/spec/spec_helper.rb +7 -9
  188. metadata +188 -8
  189. data/lib/rng/builder.rb +0 -158
  190. data/lib/rng/rng_parser.rb +0 -107
  191. data/lib/rng/schema.rb +0 -18
  192. data/spec/rng/rng_parser_spec.rb +0 -102
data/lib/rng/group.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ class Group < Lutaml::Model::Serializable
5
+ attribute :id, :string
6
+ attribute :ns, :string
7
+ attribute :datatypeLibrary, :string
8
+ attribute :externalRef, ExternalRef
9
+ attribute :element, Element, collection: true, initialize_empty: true
10
+ attribute :attribute, Attribute, collection: true, initialize_empty: true
11
+ attribute :ref, Ref, collection: true, initialize_empty: true
12
+ attribute :choice, Choice, collection: true, initialize_empty: true
13
+ attribute :group, Group, collection: true, initialize_empty: true
14
+ attribute :interleave, Interleave, collection: true, initialize_empty: true
15
+ attribute :mixed, Mixed, collection: true, initialize_empty: true
16
+ attribute :optional, Optional, collection: true, initialize_empty: true
17
+ attribute :zeroOrMore, ZeroOrMore, collection: true, initialize_empty: true
18
+ attribute :oneOrMore, OneOrMore, collection: true, initialize_empty: true
19
+ attribute :text, Text, collection: true, initialize_empty: true
20
+ attribute :empty, Empty, collection: true, initialize_empty: true
21
+ attribute :value, Value, collection: true, initialize_empty: true
22
+ attribute :data, Data, collection: true, initialize_empty: true
23
+ attribute :list, List, collection: true, initialize_empty: true
24
+ attribute :notAllowed, NotAllowed, collection: true, initialize_empty: true
25
+ attribute :base, Lutaml::Xml::W3c::XmlBaseType
26
+
27
+ xml do
28
+ element 'group'
29
+ ordered
30
+
31
+ namespace ::Rng::Namespaces::RngNamespace
32
+
33
+ map_attribute 'id', to: :id
34
+ w3c_attributes :base
35
+ map_attribute 'ns', to: :ns, value_map: {
36
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
37
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
38
+ }
39
+ map_attribute 'datatypeLibrary', to: :datatypeLibrary, value_map: {
40
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
41
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
42
+ }
43
+ map_element 'element', to: :element
44
+ map_element 'attribute', to: :attribute
45
+ map_element 'ref', to: :ref
46
+ map_element 'choice', to: :choice
47
+ map_element 'group', to: :group
48
+ map_element 'interleave', to: :interleave
49
+ map_element 'mixed', to: :mixed
50
+ map_element 'optional', to: :optional
51
+ map_element 'zeroOrMore', to: :zeroOrMore
52
+ map_element 'oneOrMore', to: :oneOrMore
53
+ map_element 'text', to: :text
54
+ map_element 'empty', to: :empty
55
+ map_element 'value', to: :value
56
+ map_element 'data', to: :data
57
+ map_element 'list', to: :list
58
+ map_element 'notAllowed', to: :notAllowed
59
+ map_element 'externalRef', to: :externalRef
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ class Include < Lutaml::Model::Serializable
5
+ attribute :href, :string
6
+ attribute :ns, :string
7
+ attribute :define, Define
8
+ attribute :grammar, Grammar
9
+
10
+ xml do
11
+ element 'include'
12
+ namespace ::Rng::Namespaces::RngNamespace
13
+
14
+ map_attribute 'href', to: :href
15
+ map_attribute 'ns', to: :ns, value_map: {
16
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
17
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
18
+ }
19
+ map_content to: :grammar
20
+ map_element 'define', to: :define
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,461 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Rng
6
+ # Handles RNC file inclusion and grammar merging
7
+ #
8
+ # This class processes include directives in RNC files, resolving them
9
+ # recursively while preventing circular includes. It supports both:
10
+ # - Grammar-level includes (inside grammar blocks)
11
+ # - Top-level includes (Metanorma-style schemas)
12
+ #
13
+ # @example Parse a file with includes
14
+ # processor = Rng::IncludeProcessor.new
15
+ # grammar = processor.parse_file("schema.rnc")
16
+ #
17
+ class IncludeProcessor
18
+ # Initialize with optional converter
19
+ #
20
+ # @param converter [RncToRngConverter] Converter for parse tree to RNG XML
21
+ def initialize(converter = RncToRngConverter.new)
22
+ @converter = converter
23
+ end
24
+
25
+ # Parse a file with include resolution
26
+ #
27
+ # @param file_path [String] Path to RNC file
28
+ # @param base_dir [String, nil] Base directory for resolving relative paths
29
+ # @param visited_files [Set] Set of already visited file paths (for circular detection)
30
+ # @return [Grammar] Parsed grammar object
31
+ def parse_file(file_path, base_dir = nil, visited_files = Set.new)
32
+ tree = parse_file_to_tree(file_path, base_dir, visited_files)
33
+
34
+ # Process raw_grammar/raw_override/raw_patterns first (before include resolution)
35
+ # The include processor needs parsed content in the tree
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]
44
+
45
+ # Build grammar tree from different structures
46
+ grammar_tree = build_grammar_tree(tree)
47
+ grammar_tree[:namespace] = namespace if namespace
48
+
49
+ # Convert to RNG XML and then to Grammar object
50
+ rng_xml = @converter.convert(grammar_tree)
51
+ Grammar.from_xml(rng_xml)
52
+ end
53
+
54
+ private
55
+
56
+ # Parse file to parse tree (not Grammar object)
57
+ #
58
+ # @param file_path [String] Path to RNC file
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)
65
+
66
+ # Check for circular includes
67
+ raise "Circular include detected: #{abs_path}" if visited_files.include?(abs_path)
68
+
69
+ # Mark file as visited
70
+ visited_files.add(abs_path)
71
+
72
+ # Read file content
73
+ raise "Include file not found: #{abs_path}" unless File.exist?(abs_path)
74
+
75
+ content = File.read(abs_path)
76
+
77
+ # Parse with includes, passing the directory for relative path resolution
78
+ parse_with_includes(content, File.dirname(abs_path), visited_files)
79
+ end
80
+
81
+ # Parse RNC content and resolve includes
82
+ #
83
+ # @param content [String] RNC content
84
+ # @param base_dir [String, nil] Base directory for resolving relative paths
85
+ # @param visited_files [Set] Set of already visited file paths
86
+ # @return [Hash] Parse tree with includes resolved
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
95
+ end
96
+
97
+ # Process include directives by recursively parsing included files
98
+ #
99
+ # @param tree [Hash] Parse tree
100
+ # @param base_dir [String] Base directory for resolving relative paths
101
+ # @param visited_files [Set] Set of already visited file paths
102
+ def process_includes(tree, base_dir, visited_files)
103
+ # Handle top-level includes first (Metanorma-style schemas)
104
+ if tree.key?(:top_includes)
105
+ process_top_level_includes(tree, base_dir, visited_files)
106
+ return
107
+ end
108
+
109
+ return if tree[:includes] && tree[:includes].empty?
110
+
111
+ # Handle grammar-level includes (existing logic)
112
+ grammar_tree = extract_grammar_tree(tree)
113
+
114
+ return unless grammar_tree[:includes] && !grammar_tree[:includes].empty?
115
+
116
+ # Process each include
117
+ grammar_tree[:includes].each do |include_item|
118
+ process_single_include(grammar_tree, include_item, base_dir,
119
+ visited_files)
120
+ end
121
+
122
+ # Remove includes array after processing (no longer needed in conversion)
123
+ grammar_tree.delete(:includes)
124
+ end
125
+
126
+ # Process top-level includes (Metanorma-style schemas)
127
+ #
128
+ # @param tree [Hash] Parse tree
129
+ # @param base_dir [String] Base directory for resolving relative paths
130
+ # @param visited_files [Set] Set of already visited file paths
131
+ def process_top_level_includes(tree, base_dir, visited_files)
132
+ # Create a temporary grammar_tree to hold merged content
133
+ grammar_tree = {
134
+ start: nil,
135
+ definitions: []
136
+ }
137
+
138
+ # Process each top-level include
139
+ tree[:top_includes].each do |include_item|
140
+ href = extract_string_literal(include_item[:href])
141
+ override = parse_override(include_item[:override])
142
+
143
+ # Resolve file path relative to base_dir
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
164
+
165
+ # Merge included definitions into temporary grammar_tree
166
+ merge_included_grammar(grammar_tree, included_grammar, override)
167
+ end
168
+
169
+ # Merge the resolved grammar_tree back into the main tree
170
+ tree[:start] = grammar_tree[:start] if grammar_tree[:start]
171
+ if grammar_tree[:definitions]
172
+ tree[:definitions] =
173
+ grammar_tree[:definitions]
174
+ end
175
+
176
+ # Process raw_trailing if present (named patterns after includes)
177
+ process_raw_trailing!(tree, grammar_tree) if tree[:raw_trailing]
178
+
179
+ # Clean up - remove top_includes key as it's been processed
180
+ tree.delete(:top_includes)
181
+ end
182
+
183
+ # Process a single include directive
184
+ #
185
+ # @param grammar_tree [Hash] Grammar tree to merge into
186
+ # @param include_item [Hash] Include item from parse tree
187
+ # @param base_dir [String] Base directory for resolving relative paths
188
+ # @param visited_files [Set] Set of already visited file paths
189
+ def process_single_include(grammar_tree, include_item, base_dir,
190
+ visited_files)
191
+ href = extract_string_literal(include_item[:href])
192
+ override = parse_override(include_item[:override])
193
+
194
+ # Resolve file path relative to base_dir
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
216
+
217
+ # Merge included definitions into current tree
218
+ merge_included_grammar(grammar_tree, included_grammar, override)
219
+ end
220
+
221
+ # Extract grammar tree from parse tree
222
+ #
223
+ # @param tree [Hash] Parse tree
224
+ # @return [Hash] Grammar tree
225
+ def extract_grammar_tree(tree)
226
+ if tree.key?(:inner_grammar)
227
+ tree[:inner_grammar]
228
+ else
229
+ tree
230
+ end
231
+ end
232
+
233
+ # Process raw_grammar/raw_override/raw_patterns nodes in-place
234
+ #
235
+ # This is needed when included files contain grammar blocks or
236
+ # overrides that are captured as raw text by the parser.
237
+ #
238
+ # @param tree [Hash] Parse tree to process in-place
239
+ def process_raw_nodes!(tree)
240
+ ParseTreeProcessor.new(tree).send(:process_raw_overrides!, tree)
241
+ end
242
+
243
+ # Process raw_trailing content (named patterns after top-level includes)
244
+ #
245
+ # For schemas like ietf.rnc where include directives are followed by
246
+ # named pattern definitions, the trailing content is captured as raw_trailing.
247
+ # This method parses that content and adds definitions to grammar_tree.
248
+ #
249
+ # @param tree [Hash] Parse tree containing raw_trailing
250
+ # @param grammar_tree [Hash] Grammar tree to merge definitions into
251
+ def process_raw_trailing!(tree, grammar_tree)
252
+ raw = tree[:raw_trailing]
253
+ return unless raw
254
+
255
+ text = if raw.is_a?(Array)
256
+ raw.map { |r| r.respond_to?(:str) ? r.str : r.to_s }.join
257
+ else
258
+ (raw.respond_to?(:str) ? raw.str : raw.to_s)
259
+ end
260
+ return if text.strip.empty?
261
+
262
+ # Parse raw_trailing as a grammar (which handles named patterns)
263
+ parser = Rng::RncParser.new
264
+ begin
265
+ parsed = parser.grammar.parse(text.strip)
266
+ patterns = parsed[:patterns] || []
267
+ grammar_tree[:definitions] ||= []
268
+ grammar_tree[:definitions].concat(patterns)
269
+ rescue Parslet::ParseFailed => e
270
+ warn "Warning: Failed to parse trailing content: #{e.message}" if ENV['RNG_VERBOSE']
271
+ ensure
272
+ tree.delete(:raw_trailing)
273
+ end
274
+ end
275
+
276
+ # Build grammar tree from different tree structures
277
+ #
278
+ # Handles:
279
+ # - Top-level includes (Metanorma style)
280
+ # - Grammar block wrapper
281
+ # - Flat grammar
282
+ #
283
+ # @param tree [Hash] Parse tree
284
+ # @return [Hash] Normalized grammar tree
285
+ def build_grammar_tree(tree)
286
+ ParseTreeProcessor.new(tree).normalize.grammar_tree
287
+ end
288
+
289
+ # Merge included grammar into current grammar, applying overrides
290
+ #
291
+ # @param target_tree [Hash] Target grammar tree to merge into
292
+ # @param source_tree [Hash] Source grammar tree to merge from
293
+ # @param override [Hash, nil] Override definitions from include directive
294
+ def merge_included_grammar(target_tree, source_tree, override)
295
+ # Merge datatype library if not already set
296
+ if source_tree[:datatype_library] && !target_tree[:datatype_library]
297
+ target_tree[:datatype_library] =
298
+ source_tree[:datatype_library]
299
+ end
300
+
301
+ # Merge start pattern if not overridden
302
+ if source_tree[:start] && !override
303
+ # If target has no start, use source start
304
+ target_tree[:start] ||= source_tree[:start]
305
+ elsif override && override[:start]
306
+ # Use override start
307
+ target_tree[:start] = override[:start]
308
+ elsif source_tree[:start] && !target_tree[:start]
309
+ target_tree[:start] = source_tree[:start]
310
+ end
311
+
312
+ # Initialize definitions array if needed
313
+ target_tree[:definitions] ||= []
314
+
315
+ # Merge definitions from source
316
+ source_tree[:definitions]&.each do |source_def|
317
+ # Check if this definition is overridden
318
+ overridden = false
319
+
320
+ if override && override[:definitions]
321
+ override[:definitions].each do |override_def|
322
+ # Check if names match
323
+ next unless source_def[:name] && override_def[:name] &&
324
+ extract_string(source_def[:name][:identifier]) == extract_string(override_def[:name][:identifier])
325
+
326
+ # Use override instead of source
327
+ target_tree[:definitions] << override_def
328
+ overridden = true
329
+ break
330
+ end
331
+ end
332
+
333
+ # If not overridden, add source definition
334
+ target_tree[:definitions] << source_def unless overridden
335
+ end
336
+
337
+ # Add any override definitions not matched with source
338
+ return unless override && override[:definitions]
339
+
340
+ override[:definitions].each do |override_def|
341
+ # Check if this override matched any source definition
342
+ matched = false
343
+
344
+ source_tree[:definitions]&.each do |source_def|
345
+ next unless source_def[:name] && override_def[:name] &&
346
+ extract_string(source_def[:name][:identifier]) == extract_string(override_def[:name][:identifier])
347
+
348
+ matched = true
349
+ break
350
+ end
351
+
352
+ # If no match, this is a new definition - add it
353
+ target_tree[:definitions] << override_def unless matched
354
+ end
355
+ end
356
+
357
+ # Helper method to extract clean string without Parslet position markers
358
+ #
359
+ # @param obj [Object] Parslet::Slice or String
360
+ # @return [String] Clean string
361
+ def extract_string(obj)
362
+ RncParser.extract_string(obj)
363
+ end
364
+
365
+ # Parse raw override string into structured override hash
366
+ #
367
+ # @param override [Hash, nil] Override hash potentially containing :raw_override
368
+ # @return [Hash, nil] Parsed override with :start and :definitions, or nil
369
+ def parse_override(override)
370
+ return nil unless override
371
+ return override unless override[:raw_override]
372
+
373
+ raw = extract_string(override[:raw_override])
374
+ return nil if raw.nil? || raw.strip.empty?
375
+
376
+ # Parse the raw override content as RNC directly (top-level style)
377
+ parser = RncParser.new
378
+ begin
379
+ parsed = parser.parse(raw.strip)
380
+ # Normalize the parsed override
381
+ processor = ParseTreeProcessor.new(parsed)
382
+ normalized = processor.normalize
383
+ tree = normalized.grammar_tree
384
+ result = {}
385
+ result[:start] = tree[:start] if tree[:start]
386
+ result[:definitions] = tree[:definitions] if tree[:definitions] && !tree[:definitions].empty?
387
+ result.empty? ? nil : result
388
+ rescue Parslet::ParseFailed
389
+ nil
390
+ end
391
+ end
392
+
393
+ # Helper method to extract string literal with concatenations
394
+ #
395
+ # @param lit [Hash] String literal with :string_parts and :concatenations
396
+ # @return [String] Extracted string
397
+ def extract_string_literal(lit)
398
+ return '' unless lit
399
+
400
+ # Extract main string parts
401
+ result = extract_string_parts(lit[:string_parts])
402
+
403
+ # Handle concatenations if present
404
+ if lit[:concatenations].is_a?(Array)
405
+ lit[:concatenations].each do |concat|
406
+ result += extract_string_parts(concat[:concat_string_parts])
407
+ end
408
+ end
409
+
410
+ result
411
+ end
412
+
413
+ # Extract string from string_parts array
414
+ #
415
+ # @param parts [Array, String] String parts
416
+ # @return [String] Extracted string
417
+ def extract_string_parts(parts)
418
+ return '' unless parts
419
+ return parts if parts.is_a?(String)
420
+ return parts.str if parts.respond_to?(:str)
421
+
422
+ return '' unless parts.is_a?(Array)
423
+
424
+ parts.map do |part|
425
+ if part.is_a?(String)
426
+ part
427
+ elsif part.respond_to?(:str)
428
+ part.str
429
+ elsif part[:hex_escape]
430
+ # Handle \x{HEX}
431
+ hex_str = part[:hex_escape][:hex]
432
+ hex_str = hex_str.str if hex_str.respond_to?(:str)
433
+ [hex_str.to_i(16)].pack('U')
434
+ elsif part[:char_escape]
435
+ # Handle \", \\, \n, \r, \t, and RELAX NG class escapes \i, \c, \d, \w
436
+ char = part[:char_escape][:char]
437
+ char = char.str if char.respond_to?(:str)
438
+ case char
439
+ when '"' then '"'
440
+ when '\\' then '\\'
441
+ when 'n' then "\n"
442
+ when 'r' then "\r"
443
+ when 't' then "\t"
444
+ when 'i' then '\\i'
445
+ when 'c' then '\\c'
446
+ when 'd' then '\\d'
447
+ when 'w' then '\\w'
448
+ else char
449
+ end
450
+ elsif part[:char]
451
+ # Regular character (plain char in string literal)
452
+ c = part[:char]
453
+ c = c.str if c.respond_to?(:str)
454
+ c.to_s
455
+ else
456
+ part.to_s
457
+ end
458
+ end.join
459
+ end
460
+ end
461
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ class Interleave < Lutaml::Model::Serializable
5
+ attribute :id, :string
6
+ attribute :ns, :string
7
+ attribute :datatypeLibrary, :string
8
+ attribute :element, Element, collection: true, initialize_empty: true
9
+ attribute :attribute, Attribute, collection: true, initialize_empty: true
10
+ attribute :ref, Ref, collection: true, initialize_empty: true
11
+ attribute :choice, Choice, collection: true, initialize_empty: true
12
+ attribute :group, Group, collection: true, initialize_empty: true
13
+ attribute :interleave, Interleave, collection: true, initialize_empty: true
14
+ attribute :mixed, Mixed, collection: true, initialize_empty: true
15
+ attribute :optional, Optional, collection: true, initialize_empty: true
16
+ attribute :zeroOrMore, ZeroOrMore, collection: true, initialize_empty: true
17
+ attribute :oneOrMore, OneOrMore, collection: true, initialize_empty: true
18
+ attribute :text, Text, collection: true, initialize_empty: true
19
+ attribute :empty, Empty, collection: true, initialize_empty: true
20
+ attribute :value, Value, collection: true, initialize_empty: true
21
+ attribute :data, Data, collection: true, initialize_empty: true
22
+ attribute :list, List, collection: true, initialize_empty: true
23
+ attribute :notAllowed, NotAllowed, collection: true, initialize_empty: true
24
+
25
+ xml do
26
+ element 'interleave'
27
+ ordered
28
+
29
+ namespace ::Rng::Namespaces::RngNamespace
30
+
31
+ map_attribute 'id', to: :id
32
+ map_attribute 'ns', to: :ns, value_map: {
33
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
34
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
35
+ }
36
+ map_attribute 'datatypeLibrary', to: :datatypeLibrary, value_map: {
37
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
38
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
39
+ }
40
+ map_element 'element', to: :element
41
+ map_element 'attribute', to: :attribute
42
+ map_element 'ref', to: :ref
43
+ map_element 'choice', to: :choice
44
+ map_element 'group', to: :group
45
+ map_element 'interleave', to: :interleave
46
+ map_element 'mixed', to: :mixed
47
+ map_element 'optional', to: :optional
48
+ map_element 'zeroOrMore', to: :zeroOrMore
49
+ map_element 'oneOrMore', to: :oneOrMore
50
+ map_element 'text', to: :text
51
+ map_element 'empty', to: :empty
52
+ map_element 'value', to: :value
53
+ map_element 'data', to: :data
54
+ map_element 'list', to: :list
55
+ map_element 'notAllowed', to: :notAllowed
56
+ end
57
+ end
58
+ end
data/lib/rng/list.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ class List < Lutaml::Model::Serializable
5
+ attribute :id, :string
6
+ attribute :ns, :string
7
+ attribute :datatypeLibrary, :string
8
+ attribute :element, Element, collection: true, initialize_empty: true
9
+ attribute :attribute, Attribute, collection: true, initialize_empty: true
10
+ attribute :ref, Ref, collection: true, initialize_empty: true
11
+ attribute :choice, Choice, collection: true, initialize_empty: true
12
+ attribute :group, Group, collection: true, initialize_empty: true
13
+ attribute :interleave, Interleave, collection: true, initialize_empty: true
14
+ attribute :mixed, Mixed, collection: true, initialize_empty: true
15
+ attribute :optional, Optional, collection: true, initialize_empty: true
16
+ attribute :zeroOrMore, ZeroOrMore, collection: true, initialize_empty: true
17
+ attribute :oneOrMore, OneOrMore, collection: true, initialize_empty: true
18
+ attribute :text, Text, collection: true, initialize_empty: true
19
+ attribute :empty, Empty, collection: true, initialize_empty: true
20
+ attribute :value, Value, collection: true, initialize_empty: true
21
+ attribute :data, Data, collection: true, initialize_empty: true
22
+ attribute :notAllowed, NotAllowed, collection: true, initialize_empty: true
23
+
24
+ xml do
25
+ element 'list'
26
+ ordered
27
+
28
+ namespace ::Rng::Namespaces::RngNamespace
29
+
30
+ map_attribute 'id', to: :id
31
+ map_attribute 'ns', to: :ns, value_map: {
32
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
33
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
34
+ }
35
+ map_attribute 'datatypeLibrary', to: :datatypeLibrary, value_map: {
36
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
37
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
38
+ }
39
+ map_element 'element', to: :element
40
+ map_element 'attribute', to: :attribute
41
+ map_element 'ref', to: :ref
42
+ map_element 'choice', to: :choice
43
+ map_element 'group', to: :group
44
+ map_element 'interleave', to: :interleave
45
+ map_element 'mixed', to: :mixed
46
+ map_element 'optional', to: :optional
47
+ map_element 'zeroOrMore', to: :zeroOrMore
48
+ map_element 'oneOrMore', to: :oneOrMore
49
+ map_element 'text', to: :text
50
+ map_element 'empty', to: :empty
51
+ map_element 'value', to: :value
52
+ map_element 'data', to: :data
53
+ map_element 'notAllowed', to: :notAllowed
54
+ end
55
+ end
56
+ end