rng 0.1.2 → 0.3.4

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 (180) 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 +10 -7
  6. data/.rubocop_todo.yml +229 -23
  7. data/CHANGELOG.md +317 -0
  8. data/CLAUDE.md +139 -0
  9. data/Gemfile +11 -12
  10. data/README.adoc +1538 -11
  11. data/Rakefile +11 -3
  12. data/docs/Gemfile +8 -0
  13. data/docs/_config.yml +23 -0
  14. data/docs/getting-started/index.adoc +75 -0
  15. data/docs/guides/error-handling.adoc +137 -0
  16. data/docs/guides/external-references.adoc +128 -0
  17. data/docs/guides/index.adoc +24 -0
  18. data/docs/guides/parsing-rnc.adoc +141 -0
  19. data/docs/guides/parsing-rng-xml.adoc +81 -0
  20. data/docs/guides/rng-to-rnc.adoc +101 -0
  21. data/docs/guides/validation.adoc +85 -0
  22. data/docs/index.adoc +52 -0
  23. data/docs/reference/api.adoc +126 -0
  24. data/docs/reference/cli.adoc +182 -0
  25. data/docs/understanding/architecture.adoc +58 -0
  26. data/docs/understanding/rng-vs-rnc.adoc +118 -0
  27. data/exe/rng +5 -0
  28. data/lib/rng/any_name.rb +10 -8
  29. data/lib/rng/attribute.rb +28 -26
  30. data/lib/rng/choice.rb +24 -24
  31. data/lib/rng/cli.rb +607 -0
  32. data/lib/rng/data.rb +10 -10
  33. data/lib/rng/datatype_declaration.rb +26 -0
  34. data/lib/rng/define.rb +44 -41
  35. data/lib/rng/div.rb +36 -0
  36. data/lib/rng/documentation.rb +9 -0
  37. data/lib/rng/element.rb +39 -37
  38. data/lib/rng/empty.rb +7 -7
  39. data/lib/rng/except.rb +25 -25
  40. data/lib/rng/external_ref.rb +8 -8
  41. data/lib/rng/external_ref_resolver.rb +602 -0
  42. data/lib/rng/foreign_attribute.rb +26 -0
  43. data/lib/rng/foreign_element.rb +33 -0
  44. data/lib/rng/grammar.rb +14 -12
  45. data/lib/rng/group.rb +26 -24
  46. data/lib/rng/include.rb +5 -6
  47. data/lib/rng/include_processor.rb +461 -0
  48. data/lib/rng/interleave.rb +23 -23
  49. data/lib/rng/list.rb +22 -22
  50. data/lib/rng/mixed.rb +23 -23
  51. data/lib/rng/name.rb +6 -7
  52. data/lib/rng/namespace_declaration.rb +47 -0
  53. data/lib/rng/namespaces.rb +15 -0
  54. data/lib/rng/not_allowed.rb +7 -7
  55. data/lib/rng/ns_name.rb +9 -9
  56. data/lib/rng/one_or_more.rb +23 -23
  57. data/lib/rng/optional.rb +23 -23
  58. data/lib/rng/param.rb +7 -8
  59. data/lib/rng/parent_ref.rb +8 -8
  60. data/lib/rng/parse_tree_processor.rb +695 -0
  61. data/lib/rng/pattern.rb +7 -7
  62. data/lib/rng/ref.rb +8 -8
  63. data/lib/rng/rnc_builder.rb +927 -0
  64. data/lib/rng/rnc_parser.rb +605 -305
  65. data/lib/rng/rnc_to_rng_converter.rb +1408 -0
  66. data/lib/rng/schema_preamble.rb +73 -0
  67. data/lib/rng/schema_validator.rb +1622 -0
  68. data/lib/rng/start.rb +27 -25
  69. data/lib/rng/test_suite_parser.rb +168 -0
  70. data/lib/rng/text.rb +11 -8
  71. data/lib/rng/to_rnc.rb +4 -35
  72. data/lib/rng/value.rb +6 -7
  73. data/lib/rng/version.rb +1 -1
  74. data/lib/rng/zero_or_more.rb +23 -23
  75. data/lib/rng.rb +68 -17
  76. data/rng.gemspec +18 -19
  77. data/scripts/extract_spectest_resources.rb +96 -0
  78. data/spec/fixtures/compacttest.xml +2511 -0
  79. data/spec/fixtures/external/circular_a.rng +7 -0
  80. data/spec/fixtures/external/circular_b.rng +7 -0
  81. data/spec/fixtures/external/circular_main.rng +7 -0
  82. data/spec/fixtures/external/external_ref_lib.rng +7 -0
  83. data/spec/fixtures/external/external_ref_main.rng +7 -0
  84. data/spec/fixtures/external/include_lib.rng +7 -0
  85. data/spec/fixtures/external/include_main.rng +3 -0
  86. data/spec/fixtures/external/nested_chain.rng +6 -0
  87. data/spec/fixtures/external/nested_leaf.rng +7 -0
  88. data/spec/fixtures/external/nested_mid.rng +8 -0
  89. data/spec/fixtures/metanorma/3gpp.rnc +35 -0
  90. data/spec/fixtures/metanorma/3gpp.rng +105 -0
  91. data/spec/fixtures/metanorma/basicdoc.rnc +11 -0
  92. data/spec/fixtures/metanorma/bipm.rnc +148 -0
  93. data/spec/fixtures/metanorma/bipm.rng +376 -0
  94. data/spec/fixtures/metanorma/bsi.rnc +104 -0
  95. data/spec/fixtures/metanorma/bsi.rng +332 -0
  96. data/spec/fixtures/metanorma/csa.rnc +45 -0
  97. data/spec/fixtures/metanorma/csa.rng +131 -0
  98. data/spec/fixtures/metanorma/csd.rnc +43 -0
  99. data/spec/fixtures/metanorma/csd.rng +132 -0
  100. data/spec/fixtures/metanorma/gbstandard.rnc +99 -0
  101. data/spec/fixtures/metanorma/gbstandard.rng +316 -0
  102. data/spec/fixtures/metanorma/iec.rnc +49 -0
  103. data/spec/fixtures/metanorma/iec.rng +193 -0
  104. data/spec/fixtures/metanorma/ietf.rnc +275 -0
  105. data/spec/fixtures/metanorma/ietf.rng +925 -0
  106. data/spec/fixtures/metanorma/iho.rnc +58 -0
  107. data/spec/fixtures/metanorma/iho.rng +179 -0
  108. data/spec/fixtures/metanorma/isodoc.rnc +873 -0
  109. data/spec/fixtures/metanorma/isodoc.rng +2704 -0
  110. data/spec/fixtures/metanorma/isostandard-amd.rnc +43 -0
  111. data/spec/fixtures/metanorma/isostandard-amd.rng +108 -0
  112. data/spec/fixtures/metanorma/isostandard.rnc +166 -0
  113. data/spec/fixtures/metanorma/isostandard.rng +494 -0
  114. data/spec/fixtures/metanorma/itu.rnc +122 -0
  115. data/spec/fixtures/metanorma/itu.rng +377 -0
  116. data/spec/fixtures/metanorma/m3d.rnc +41 -0
  117. data/spec/fixtures/metanorma/m3d.rng +122 -0
  118. data/spec/fixtures/metanorma/mpfd.rnc +36 -0
  119. data/spec/fixtures/metanorma/mpfd.rng +95 -0
  120. data/spec/fixtures/metanorma/nist.rnc +77 -0
  121. data/spec/fixtures/metanorma/nist.rng +216 -0
  122. data/spec/fixtures/metanorma/ogc.rnc +51 -0
  123. data/spec/fixtures/metanorma/ogc.rng +151 -0
  124. data/spec/fixtures/metanorma/reqt.rnc +6 -0
  125. data/spec/fixtures/metanorma/rsd.rnc +36 -0
  126. data/spec/fixtures/metanorma/rsd.rng +95 -0
  127. data/spec/fixtures/metanorma/un.rnc +103 -0
  128. data/spec/fixtures/metanorma/un.rng +367 -0
  129. data/spec/fixtures/rnc/base.rnc +4 -0
  130. data/spec/fixtures/rnc/grammar_with_trailing.rnc +8 -0
  131. data/spec/fixtures/rnc/main_include_trailing.rnc +3 -0
  132. data/spec/fixtures/rnc/main_with_include.rnc +5 -0
  133. data/spec/fixtures/rnc/test_augment.rnc +10 -0
  134. data/spec/fixtures/rnc/test_isodoc_simple.rnc +9 -0
  135. data/spec/fixtures/rnc/top_level_include.rnc +8 -0
  136. data/spec/fixtures/spectest_external/case_10_4.7/x +3 -0
  137. data/spec/fixtures/spectest_external/case_10_4.7/y +7 -0
  138. data/spec/fixtures/spectest_external/case_11_4.7/x +3 -0
  139. data/spec/fixtures/spectest_external/case_12_4.7/x +3 -0
  140. data/spec/fixtures/spectest_external/case_13_4.7/x +3 -0
  141. data/spec/fixtures/spectest_external/case_13_4.7/y +3 -0
  142. data/spec/fixtures/spectest_external/case_14_4.7/x +7 -0
  143. data/spec/fixtures/spectest_external/case_15_4.7/x +7 -0
  144. data/spec/fixtures/spectest_external/case_16_4.7/x +5 -0
  145. data/spec/fixtures/spectest_external/case_17_4.7/x +5 -0
  146. data/spec/fixtures/spectest_external/case_18_4.7/x +7 -0
  147. data/spec/fixtures/spectest_external/case_19_4.7/level1.rng +9 -0
  148. data/spec/fixtures/spectest_external/case_19_4.7/level2.rng +7 -0
  149. data/spec/fixtures/spectest_external/case_1_4.5/sub1/x +3 -0
  150. data/spec/fixtures/spectest_external/case_1_4.5/sub3/x +3 -0
  151. data/spec/fixtures/spectest_external/case_1_4.5/x +3 -0
  152. data/spec/fixtures/spectest_external/case_20_4.6/x +3 -0
  153. data/spec/fixtures/spectest_external/case_2_4.5/x +3 -0
  154. data/spec/fixtures/spectest_external/case_3_4.6/x +3 -0
  155. data/spec/fixtures/spectest_external/case_4_4.6/x +3 -0
  156. data/spec/fixtures/spectest_external/case_5_4.6/x +1 -0
  157. data/spec/fixtures/spectest_external/case_6_4.6/x +5 -0
  158. data/spec/fixtures/spectest_external/case_7_4.6/x +1 -0
  159. data/spec/fixtures/spectest_external/case_7_4.6/y +1 -0
  160. data/spec/fixtures/spectest_external/case_8_4.7/x +7 -0
  161. data/spec/fixtures/spectest_external/case_9_4.7/x +7 -0
  162. data/spec/fixtures/spectest_external/resources.json +149 -0
  163. data/spec/rng/advanced_rnc_spec.rb +101 -0
  164. data/spec/rng/compacttest_spec.rb +197 -0
  165. data/spec/rng/datatype_declaration_spec.rb +28 -0
  166. data/spec/rng/div_spec.rb +207 -0
  167. data/spec/rng/external_ref_resolver_spec.rb +122 -0
  168. data/spec/rng/metanorma_conversion_spec.rb +159 -0
  169. data/spec/rng/namespace_declaration_spec.rb +60 -0
  170. data/spec/rng/namespace_support_spec.rb +199 -0
  171. data/spec/rng/rnc_parser_spec.rb +498 -22
  172. data/spec/rng/rnc_roundtrip_spec.rb +96 -82
  173. data/spec/rng/rng_generation_spec.rb +288 -0
  174. data/spec/rng/roundtrip_spec.rb +342 -0
  175. data/spec/rng/schema_preamble_spec.rb +145 -0
  176. data/spec/rng/schema_spec.rb +68 -64
  177. data/spec/rng/spectest_spec.rb +168 -90
  178. data/spec/rng_spec.rb +2 -2
  179. data/spec/spec_helper.rb +7 -42
  180. metadata +141 -8
data/lib/rng/grammar.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
4
-
5
3
  module Rng
6
4
  # This represents the RNG schema
7
5
  class Grammar < Lutaml::Model::Serializable
@@ -10,27 +8,31 @@ module Rng
10
8
  attribute :datatypeLibrary, :string
11
9
  attribute :start, Start, collection: true
12
10
  attribute :define, Define, collection: true, initialize_empty: true
13
- attribute :element, Element, collection: true
11
+ attribute :element, Element, collection: true, initialize_empty: true
14
12
  attribute :include, Include, collection: true
13
+ attribute :div, Div, collection: true, initialize_empty: true
15
14
 
16
15
  xml do
17
- root "grammar", ordered: true
18
- namespace "http://relaxng.org/ns/structure/1.0"
16
+ element 'grammar'
17
+ ordered
18
+
19
+ namespace ::Rng::Namespaces::RngNamespace
19
20
 
20
- map_attribute "datatypeLibrary", to: :datatypeLibrary, value_map: {
21
+ map_attribute 'datatypeLibrary', to: :datatypeLibrary, value_map: {
21
22
  from: { empty: :empty, omitted: :omitted, nil: :nil },
22
23
  to: { empty: :empty, omitted: :omitted, nil: :nil }
23
24
  }
24
- map_attribute "ns", to: :ns, value_map: {
25
+ map_attribute 'ns', to: :ns, value_map: {
25
26
  from: { empty: :empty, omitted: :omitted, nil: :nil },
26
27
  to: { empty: :empty, omitted: :omitted, nil: :nil }
27
28
  }
28
- map_attribute "id", to: :id
29
+ map_attribute 'id', to: :id
29
30
 
30
- map_element "start", to: :start
31
- map_element "define", to: :define
32
- map_element "element", to: :element
33
- map_element "include", to: :include
31
+ map_element 'start', to: :start
32
+ map_element 'define', to: :define
33
+ map_element 'element', to: :element
34
+ map_element 'include', to: :include
35
+ map_element 'div', to: :div
34
36
  end
35
37
  end
36
38
  end
data/lib/rng/group.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
4
-
5
3
  module Rng
6
4
  class Group < Lutaml::Model::Serializable
7
5
  attribute :id, :string
@@ -24,37 +22,41 @@ module Rng
24
22
  attribute :data, Data, collection: true, initialize_empty: true
25
23
  attribute :list, List, collection: true, initialize_empty: true
26
24
  attribute :notAllowed, NotAllowed, collection: true, initialize_empty: true
25
+ attribute :base, Lutaml::Xml::W3c::XmlBaseType
27
26
 
28
27
  xml do
29
- root "group", ordered: true
30
- namespace "http://relaxng.org/ns/structure/1.0"
28
+ element 'group'
29
+ ordered
30
+
31
+ namespace ::Rng::Namespaces::RngNamespace
31
32
 
32
- map_attribute "id", to: :id
33
- map_attribute "ns", to: :ns, value_map: {
33
+ map_attribute 'id', to: :id
34
+ w3c_attributes :base
35
+ map_attribute 'ns', to: :ns, value_map: {
34
36
  from: { empty: :empty, omitted: :omitted, nil: :nil },
35
37
  to: { empty: :empty, omitted: :omitted, nil: :nil }
36
38
  }
37
- map_attribute "datatypeLibrary", to: :datatypeLibrary, value_map: {
39
+ map_attribute 'datatypeLibrary', to: :datatypeLibrary, value_map: {
38
40
  from: { empty: :empty, omitted: :omitted, nil: :nil },
39
41
  to: { empty: :empty, omitted: :omitted, nil: :nil }
40
42
  }
41
- map_element "element", to: :element
42
- map_element "attribute", to: :attribute
43
- map_element "ref", to: :ref
44
- map_element "choice", to: :choice
45
- map_element "group", to: :group
46
- map_element "interleave", to: :interleave
47
- map_element "mixed", to: :mixed
48
- map_element "optional", to: :optional
49
- map_element "zeroOrMore", to: :zeroOrMore
50
- map_element "oneOrMore", to: :oneOrMore
51
- map_element "text", to: :text
52
- map_element "empty", to: :empty
53
- map_element "value", to: :value
54
- map_element "data", to: :data
55
- map_element "list", to: :list
56
- map_element "notAllowed", to: :notAllowed
57
- map_element "externalRef", to: :externalRef
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
58
60
  end
59
61
  end
60
62
  end
data/lib/rng/include.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
4
-
5
3
  module Rng
6
4
  class Include < Lutaml::Model::Serializable
7
5
  attribute :href, :string
@@ -10,15 +8,16 @@ module Rng
10
8
  attribute :grammar, Grammar
11
9
 
12
10
  xml do
13
- root "include"
11
+ element 'include'
12
+ namespace ::Rng::Namespaces::RngNamespace
14
13
 
15
- map_attribute "href", to: :href
16
- map_attribute "ns", to: :ns, value_map: {
14
+ map_attribute 'href', to: :href
15
+ map_attribute 'ns', to: :ns, value_map: {
17
16
  from: { empty: :empty, omitted: :omitted, nil: :nil },
18
17
  to: { empty: :empty, omitted: :omitted, nil: :nil }
19
18
  }
20
19
  map_content to: :grammar
21
- map_element "define", to: :define
20
+ map_element 'define', to: :define
22
21
  end
23
22
  end
24
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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
4
-
5
3
  module Rng
6
4
  class Interleave < Lutaml::Model::Serializable
7
5
  attribute :id, :string
@@ -25,34 +23,36 @@ module Rng
25
23
  attribute :notAllowed, NotAllowed, collection: true, initialize_empty: true
26
24
 
27
25
  xml do
28
- root "interleave", ordered: true
29
- namespace "http://relaxng.org/ns/structure/1.0"
26
+ element 'interleave'
27
+ ordered
28
+
29
+ namespace ::Rng::Namespaces::RngNamespace
30
30
 
31
- map_attribute "id", to: :id
32
- map_attribute "ns", to: :ns, value_map: {
31
+ map_attribute 'id', to: :id
32
+ map_attribute 'ns', to: :ns, value_map: {
33
33
  from: { empty: :empty, omitted: :omitted, nil: :nil },
34
34
  to: { empty: :empty, omitted: :omitted, nil: :nil }
35
35
  }
36
- map_attribute "datatypeLibrary", to: :datatypeLibrary, value_map: {
36
+ map_attribute 'datatypeLibrary', to: :datatypeLibrary, value_map: {
37
37
  from: { empty: :empty, omitted: :omitted, nil: :nil },
38
38
  to: { empty: :empty, omitted: :omitted, nil: :nil }
39
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
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
56
  end
57
57
  end
58
58
  end