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
@@ -0,0 +1,927 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rng
4
+ # RncBuilder converts RNG Grammar objects to RNC (RELAX NG Compact Syntax) text format.
5
+ #
6
+ # This class traverses the RNG object model and generates readable RNC syntax strings.
7
+ # It handles all major RELAX NG patterns including:
8
+ # - Elements and attributes
9
+ # - Named pattern definitions
10
+ # - Occurrence markers (*, +, ?)
11
+ # - Choice and group patterns
12
+ # - Mixed content
13
+ # - Value literals and datatypes
14
+ # - Namespace declarations
15
+ #
16
+ # @example Convert a Grammar object to RNC
17
+ # grammar = Rng::Grammar.new
18
+ # # ... populate grammar ...
19
+ # builder = Rng::RncBuilder.new
20
+ # rnc_text = builder.build(grammar)
21
+ class RncBuilder
22
+ # Build RNC text from a Grammar object or Element
23
+ #
24
+ # @param schema [Rng::Grammar, Rng::Element] The schema to convert
25
+ # @return [String] RNC text representation
26
+ def build(schema)
27
+ @datatype_prefix = nil
28
+ @datatype_library_uri = nil
29
+ # Handle simple element (direct Element object, not Grammar)
30
+ return build_element(schema) if schema.is_a?(Element)
31
+
32
+ # Grammar with start and/or named patterns
33
+ result = []
34
+
35
+ # Collect datatype libraries from data elements
36
+ collect_datatype_libraries(schema)
37
+
38
+ # Add namespace declaration if present
39
+ if schema.ns && !schema.ns.empty? && schema.ns != 'omitted' && schema.ns != 'empty'
40
+ result << "default namespace = \"#{schema.ns}\""
41
+ result << ''
42
+ end
43
+
44
+ # Add datatype library if present (grammar-level or from data elements)
45
+ dt_lib = schema.datatypeLibrary if schema.datatypeLibrary &&
46
+ !%w[omitted empty].include?(schema.datatypeLibrary)
47
+ dt_lib ||= @datatype_library_uri
48
+ if dt_lib
49
+ result << "datatypes xsd = \"#{dt_lib}\""
50
+ @datatype_prefix = 'xsd'
51
+ result << ''
52
+ end
53
+
54
+ # Process start pattern
55
+ if schema.start && !schema.start.empty?
56
+ start = schema.start.first
57
+ # Add documentation if present
58
+ result << build_documentation(start.documentation).chomp if start.documentation && !start.documentation.empty?
59
+ start_pattern = build_pattern(start)
60
+ result << "start = #{start_pattern}"
61
+ result << ''
62
+ end
63
+
64
+ # Process named patterns (define elements)
65
+ if schema.define && !schema.define.empty?
66
+ schema.define.each do |define|
67
+ # Add documentation if present
68
+ result << build_documentation(define.documentation).chomp if define.documentation && !define.documentation.empty?
69
+ pattern = build_pattern(define)
70
+ result << "#{define.name} = #{pattern}"
71
+ result << ''
72
+ end
73
+ end
74
+
75
+ # Process div elements (grouping containers)
76
+ if schema.div && !schema.div.empty?
77
+ schema.div.each do |div|
78
+ div_content = build_div(div)
79
+ result << div_content
80
+ result << ''
81
+ end
82
+ end
83
+
84
+ result.join("\n")
85
+ end
86
+
87
+ # Escape a string value for use in RNC double-quoted string literals.
88
+ # RNC string escape sequences: \\ (backslash), \" (double quote),
89
+ # \n (newline), \r (carriage return), \t (tab)
90
+ #
91
+ # @param str [String] The string to escape
92
+ # @return [String] The escaped string
93
+ def escape_rnc_string(str)
94
+ str.gsub('\\') { '\\\\' }
95
+ .gsub('"') { '\\"' }
96
+ .gsub("\n") { '\\n' }
97
+ .gsub("\r") { '\\r' }
98
+ .gsub("\t") { '\\t' }
99
+ end
100
+
101
+ # Build RNC syntax for a div grouping construct
102
+ #
103
+ # @param div [Rng::Div] The div to convert
104
+ # @return [String] RNC div syntax
105
+ def build_div(div)
106
+ result = 'div {'
107
+ parts = []
108
+
109
+ # Process divs within div
110
+ if div.div && !div.div.empty?
111
+ div.div.each do |nested_div|
112
+ parts << build_div(nested_div)
113
+ end
114
+ end
115
+
116
+ # Process start within div
117
+ if div.start && !div.start.empty?
118
+ div.start.each do |start|
119
+ parts << "start = #{build_pattern(start)}"
120
+ end
121
+ end
122
+
123
+ # Process defines within div
124
+ if div.define && !div.define.empty?
125
+ div.define.each do |define|
126
+ parts << "#{define.name} = #{build_pattern(define)}"
127
+ end
128
+ end
129
+
130
+ if parts.empty?
131
+ "#{result} }"
132
+ else
133
+ "#{result}\n #{parts.join("\n ")}\n}"
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ # Build RNC syntax for documentation comments
140
+ #
141
+ # @param documentation [String, nil] Documentation text
142
+ # @return [String] RNC documentation comment lines
143
+ def build_documentation(documentation)
144
+ return '' unless documentation && !documentation.empty?
145
+
146
+ lines = documentation.split("\n")
147
+ lines.map { |line| "## #{line}" }.join("\n") + "\n"
148
+ end
149
+
150
+ # Build RNC syntax for an element
151
+ #
152
+ # @param element [Rng::Element] The element to convert
153
+ # @return [String] RNC element syntax
154
+ def build_element(element)
155
+ result = ''
156
+
157
+ # Add documentation if present
158
+ result += build_documentation(element.documentation) if element.documentation
159
+
160
+ # Extract element name - handle multiple cases:
161
+ # 1. attr_name as string (from XML attribute)
162
+ # 2. attr_name as hash (raw parse tree - shouldn't happen but be defensive)
163
+ # 3. name as Name object (from XML child element)
164
+ element_name = if element.attr_name.is_a?(String) && !element.attr_name.empty?
165
+ element.attr_name
166
+ elsif element.attr_name.is_a?(Hash)
167
+ # Raw parse tree leaked through - extract name
168
+ name_data = element.attr_name
169
+ identifier = if name_data.dig(:name, :local_name,
170
+ :identifier)
171
+ name_data.dig(:name, :local_name,
172
+ :identifier)
173
+ elsif name_data.dig(:local_name,
174
+ :identifier)
175
+ name_data.dig(:local_name, :identifier)
176
+ else
177
+ name_data
178
+ end
179
+ # Handle Parslet::Slice (has position info like "doc"@16)
180
+ extract_string(identifier)
181
+ elsif element.anyName || element.nsName ||
182
+ (element.name.is_a?(Name) && element.name.value)
183
+ build_name_class(element)
184
+ else
185
+ ''
186
+ end
187
+
188
+ result += "element #{element_name} {\n"
189
+ result += " #{build_content(element)}\n"
190
+ result += '}'
191
+ result
192
+ end
193
+
194
+ # Build RNC syntax for element content
195
+ #
196
+ # @param node [Object] The node containing content (attributes, elements, text, etc.)
197
+ # @return [String] RNC content syntax
198
+ def build_content(node)
199
+ content_parts = []
200
+
201
+ # Process attributes
202
+ if node.attribute
203
+ if node.attribute.is_a?(Array)
204
+ node.attribute.each do |attr|
205
+ content_parts << build_attribute(attr)
206
+ end
207
+ else
208
+ content_parts << build_attribute(node.attribute)
209
+ end
210
+ end
211
+
212
+ # Process child elements
213
+ if node.element && !node.element.empty?
214
+ if node.element.is_a?(Array)
215
+ node.element.each do |elem|
216
+ content_parts << build_element(elem)
217
+ end
218
+ else
219
+ content_parts << build_element(node.element)
220
+ end
221
+ end
222
+
223
+ # Process text
224
+ content_parts << 'text' if node.text
225
+
226
+ # Process empty
227
+ content_parts << 'empty' if node.empty
228
+
229
+ # Process value literals (Value object has .value attribute)
230
+ if node.value
231
+ value_str = node.value.is_a?(Value) ? node.value.value : node.value.to_s
232
+ content_parts << "\"#{value_str}\""
233
+ end
234
+
235
+ # Process mixed content
236
+ if node.mixed
237
+ mixed_items = node.mixed.is_a?(Array) ? node.mixed : [node.mixed]
238
+ mixed_items.each do |m|
239
+ mixed_inner = build_mixed_content(m)
240
+ content_parts << "mixed {\n #{mixed_inner}\n}"
241
+ end
242
+ end
243
+ if node.choice && !(node.choice.is_a?(Array) && node.choice.empty?)
244
+ choices = node.choice.is_a?(Array) ? node.choice : [node.choice]
245
+ choices.each { |c| content_parts << build_pattern(c) }
246
+ end
247
+ if node.group && !(node.group.is_a?(Array) && node.group.empty?)
248
+ groups = node.group.is_a?(Array) ? node.group : [node.group]
249
+ groups.each { |g| content_parts << build_pattern(g) }
250
+ end
251
+ # Process ref
252
+ if node.ref && !node.ref.empty?
253
+ if node.ref.is_a?(Array)
254
+ node.ref.each do |ref|
255
+ content_parts << ref.name
256
+ end
257
+ else
258
+ content_parts << node.ref.name
259
+ end
260
+ end
261
+
262
+ # Process oneOrMore
263
+ if node.oneOrMore
264
+ if node.oneOrMore.is_a?(Array)
265
+ node.oneOrMore.each do |pattern|
266
+ content_parts << build_pattern(pattern)
267
+ end
268
+ else
269
+ content_parts << build_pattern(node.oneOrMore)
270
+ end
271
+ end
272
+
273
+ # Process optional
274
+ if node.optional
275
+ if node.optional.is_a?(Array)
276
+ node.optional.each do |pattern|
277
+ content_parts << build_pattern(pattern)
278
+ end
279
+ else
280
+ content_parts << build_pattern(node.optional)
281
+ end
282
+ end
283
+
284
+ # Process zeroOrMore
285
+ if node.zeroOrMore
286
+ if node.zeroOrMore.is_a?(Array)
287
+ node.zeroOrMore.each do |pattern|
288
+ content_parts << build_pattern(pattern)
289
+ end
290
+ else
291
+ content_parts << build_pattern(node.zeroOrMore)
292
+ end
293
+ end
294
+
295
+ # Process interleave
296
+ if node.interleave && !(node.interleave.is_a?(Array) && node.interleave.empty?)
297
+ interleaves = node.interleave.is_a?(Array) ? node.interleave : [node.interleave]
298
+ interleaves.each do |il|
299
+ content_parts << build_interleave(il)
300
+ end
301
+ end
302
+
303
+ # Process data
304
+ content_parts << build_data(node.data) if node.data
305
+
306
+ # Process list
307
+ content_parts << build_list(node.list) if node.list
308
+
309
+ # Process notAllowed
310
+ content_parts << 'notAllowed' if node.notAllowed
311
+
312
+ parts = content_parts.reject(&:empty?)
313
+ if parts.length > 1
314
+ # Wrap choice/interleave in parentheses when part of a sequence
315
+ parts.map! do |p|
316
+ if p.include?(' | ') || p.include?(' & ')
317
+ "(#{p})"
318
+ else
319
+ p
320
+ end
321
+ end
322
+ end
323
+ parts.join(",\n ")
324
+ end
325
+
326
+ # Build RNC syntax for mixed content
327
+ #
328
+ # @param mixed [Object] The mixed content node
329
+ # @return [String] RNC mixed content syntax
330
+ def build_mixed_content(mixed)
331
+ # Mixed contains collections of patterns
332
+ content_parts = []
333
+
334
+ # Process all pattern types in mixed
335
+ if mixed.element && !mixed.element.empty?
336
+ mixed.element.each do |elem|
337
+ content_parts << build_element(elem)
338
+ end
339
+ end
340
+
341
+ if mixed.oneOrMore && !mixed.oneOrMore.empty?
342
+ mixed.oneOrMore.each do |pattern|
343
+ content_parts << "#{build_pattern(pattern)}+"
344
+ end
345
+ end
346
+
347
+ if mixed.zeroOrMore && !mixed.zeroOrMore.empty?
348
+ mixed.zeroOrMore.each do |pattern|
349
+ content_parts << "#{build_pattern(pattern)}*"
350
+ end
351
+ end
352
+
353
+ if mixed.optional && !mixed.optional.empty?
354
+ mixed.optional.each do |pattern|
355
+ content_parts << "#{build_pattern(pattern)}?"
356
+ end
357
+ end
358
+
359
+ if mixed.ref && !mixed.ref.empty?
360
+ mixed.ref.each do |ref|
361
+ content_parts << ref.name
362
+ end
363
+ end
364
+
365
+ if mixed.choice && !mixed.choice.empty?
366
+ mixed.choice.each do |choice|
367
+ content_parts << build_pattern(choice)
368
+ end
369
+ end
370
+
371
+ if mixed.group && !mixed.group.empty?
372
+ mixed.group.each do |group|
373
+ content_parts << build_pattern(group)
374
+ end
375
+ end
376
+
377
+ content_parts << 'text' if mixed.text && !mixed.text.empty?
378
+ content_parts << 'empty' if mixed.empty && !mixed.empty.empty?
379
+
380
+ content_parts.reject(&:empty?).join(",\n ")
381
+ end
382
+
383
+ # Build RNC syntax for an attribute
384
+ #
385
+ # @param attr [Rng::Attribute] The attribute to convert
386
+ # @return [String] RNC attribute syntax
387
+ def build_attribute(attr)
388
+ result = ''
389
+
390
+ # Add documentation if present (indented for use within element)
391
+ if attr.documentation && !attr.documentation.empty?
392
+ doc_lines = attr.documentation.split("\n")
393
+ result += doc_lines.map { |line| " ## #{line}\n" }.join
394
+ end
395
+
396
+ # Extract attribute name - handle multiple cases:
397
+ # 1. attr_name as string (from XML attribute)
398
+ # 2. attr_name as hash (raw parse tree - shouldn't happen but be defensive)
399
+ # 3. name as Name object (from XML child element)
400
+ attr_name = if attr.attr_name.is_a?(String) && !attr.attr_name.empty?
401
+ attr.attr_name
402
+ elsif attr.attr_name.is_a?(Hash)
403
+ # Raw parse tree leaked through - extract name
404
+ name_data = attr.attr_name
405
+ identifier = if name_data.dig(:name, :local_name,
406
+ :identifier)
407
+ name_data.dig(:name, :local_name,
408
+ :identifier)
409
+ elsif name_data.dig(:local_name, :identifier)
410
+ name_data.dig(:local_name, :identifier)
411
+ else
412
+ name_data
413
+ end
414
+ # Handle Parslet::Slice (has position info like "doc"@16)
415
+ extract_string(identifier)
416
+ elsif attr.anyName || attr.nsName ||
417
+ (attr.name.is_a?(Name) && attr.name.value)
418
+ build_name_class(attr)
419
+ else
420
+ ''
421
+ end
422
+
423
+ result += "attribute #{attr_name} { "
424
+
425
+ # Check for value literal (Value object)
426
+ if attr.value
427
+ value_str = attr.value.is_a?(Value) ? attr.value.value : attr.value.to_s
428
+ result += "\"#{value_str}\""
429
+ # Check for choice of values
430
+ elsif attr.choice
431
+ # Check if choice contains Value objects
432
+ choice_obj = attr.choice
433
+ if choice_obj.value.is_a?(Array)
434
+ # Choice with array of Value objects
435
+ values = choice_obj.value.map do |v|
436
+ v_str = v.is_a?(Value) ? v.value : v.to_s
437
+ "\"#{escape_rnc_string(v_str)}\""
438
+ end
439
+ result += values.join(' | ')
440
+ else
441
+ # Other choice patterns
442
+ result += build_pattern(attr.choice)
443
+ end
444
+ # Check for datatype
445
+ elsif attr.data
446
+ result += if attr.data.type
447
+ "xsd:#{attr.data.type}"
448
+ else
449
+ 'text'
450
+ end
451
+ else
452
+ result += 'text'
453
+ end
454
+
455
+ result += ' }'
456
+ result
457
+ end
458
+
459
+ # Build RNC syntax for a pattern (recursive)
460
+ #
461
+ # This method handles all pattern types including:
462
+ # - Define (named pattern definitions)
463
+ # - OneOrMore, ZeroOrMore, Optional (occurrence wrappers)
464
+ # - Element (direct elements)
465
+ # - Choice, Group (pattern combinations)
466
+ # - Ref (pattern references)
467
+ # - Value, Text, Empty (leaf patterns)
468
+ # - Mixed (mixed content)
469
+ #
470
+ # @param node [Object] The pattern node to convert
471
+ # @return [String] RNC pattern syntax
472
+ def build_pattern(node)
473
+ # Handle Define objects by extracting their pattern content
474
+ if node.is_a?(Define)
475
+ # Define is a container, extract actual pattern content
476
+ # All pattern attributes are collections
477
+ if node.group && !node.group.empty?
478
+ return node.group.map { |g| build_pattern(g) }.join(', ')
479
+ elsif node.element && !node.element.empty?
480
+ return node.element.map { |elem| build_element(elem) }.join(', ')
481
+ elsif node.choice && !node.choice.empty?
482
+ return node.choice.map { |c| build_pattern(c) }.join(' | ')
483
+ elsif node.ref && !node.ref.empty?
484
+ return node.ref.map(&:name).join(' | ')
485
+ elsif node.optional && !node.optional.empty?
486
+ return node.optional.map { |o| build_pattern(o) }.join(', ')
487
+ elsif node.zeroOrMore && !node.zeroOrMore.empty?
488
+ return node.zeroOrMore.map { |z| build_pattern(z) }.join(', ')
489
+ elsif node.oneOrMore && !node.oneOrMore.empty?
490
+ return node.oneOrMore.map { |o| build_pattern(o) }.join(', ')
491
+ elsif node.interleave && !node.interleave.empty?
492
+ return node.interleave.map { |i| build_pattern(i) }.join(' & ')
493
+ elsif node.text && !node.text.empty?
494
+ return 'text'
495
+ elsif node.empty && !node.empty.empty?
496
+ return 'empty'
497
+ elsif node.value && !node.value.empty?
498
+ values = node.value.map do |v|
499
+ v.is_a?(Value) ? "\"#{escape_rnc_string(v.value)}\"" : "\"#{escape_rnc_string(v.to_s)}\""
500
+ end
501
+ return values.join(', ')
502
+ elsif node.data && !node.data.empty?
503
+ return node.data.map { |d| build_data(d) }.join(', ')
504
+ elsif node.attribute && !node.attribute.empty?
505
+ return node.attribute.map { |a| build_attribute(a) }.join(', ')
506
+ elsif node.list && !node.list.empty?
507
+ return node.list.map { |l| build_list(l) }.join(', ')
508
+ elsif node.mixed && !node.mixed.empty?
509
+ return node.mixed.map { |m| build_mixed(m) }.join(', ')
510
+ elsif node.notAllowed && !node.notAllowed.empty?
511
+ return 'notAllowed'
512
+ elsif node.grammar && !node.grammar.empty?
513
+ return node.grammar.map { |g| build(g) }.join(', ')
514
+ end
515
+ end
516
+
517
+ # Handle OneOrMore, ZeroOrMore, and Optional wrapper objects
518
+ if node.is_a?(OneOrMore) || node.is_a?(ZeroOrMore) || node.is_a?(Optional)
519
+ occurrence = case node.class.name
520
+ when 'Rng::OneOrMore' then '+'
521
+ when 'Rng::ZeroOrMore' then '*'
522
+ when 'Rng::Optional' then '?'
523
+ end
524
+
525
+ # Extract content from wrapper
526
+ if node.group && !(node.group.is_a?(Array) && node.group.empty?)
527
+ inner = if node.group.is_a?(Array)
528
+ node.group.map { |g| build_pattern(g) }.join(', ')
529
+ else
530
+ build_pattern(node.group)
531
+ end
532
+ return "(#{inner})#{occurrence}"
533
+ elsif node.choice && !(node.choice.is_a?(Array) && node.choice.empty?)
534
+ inner = if node.choice.is_a?(Array)
535
+ node.choice.map { |c| build_pattern(c) }.join(' | ')
536
+ else
537
+ build_pattern(node.choice)
538
+ end
539
+ return "(#{inner})#{occurrence}"
540
+ elsif node.element && !(node.element.is_a?(Array) && node.element.empty?)
541
+ return "#{build_element(node.element)}#{occurrence}" unless node.element.is_a?(Array)
542
+ return "#{build_element(node.element.first)}#{occurrence}" if node.element.length == 1
543
+
544
+ inner = node.element.map { |e| build_element(e) }.join(', ')
545
+ return "(#{inner})#{occurrence}"
546
+
547
+ elsif node.ref && !(node.ref.is_a?(Array) && node.ref.empty?)
548
+ return "(#{node.ref.map(&:name).join(' | ')})#{occurrence}" if node.ref.is_a?(Array)
549
+
550
+ return "#{node.ref.name}#{occurrence}"
551
+
552
+ elsif node.attribute && !(node.attribute.is_a?(Array) && node.attribute.empty?)
553
+ # Handle attribute content inside wrapper (e.g., attribute acronym { text }?)
554
+ attr_parts = if node.attribute.is_a?(Array)
555
+ node.attribute.map { |a| build_attribute(a) }
556
+ else
557
+ [build_attribute(node.attribute)]
558
+ end
559
+ inner = attr_parts.join(', ')
560
+ return "#{inner}#{occurrence}"
561
+ elsif node.text && !(node.text.is_a?(Array) && node.text.empty?)
562
+ # Handle text inside wrapper
563
+ return "text#{occurrence}"
564
+ elsif node.optional && !(node.optional.is_a?(Array) && node.optional.empty?)
565
+ inner = if node.optional.is_a?(Array)
566
+ node.optional.map { |o| build_pattern(o) }.join(', ')
567
+ else
568
+ build_pattern(node.optional)
569
+ end
570
+ return "(#{inner})#{occurrence}"
571
+ elsif node.zeroOrMore && !(node.zeroOrMore.is_a?(Array) && node.zeroOrMore.empty?)
572
+ inner = if node.zeroOrMore.is_a?(Array)
573
+ node.zeroOrMore.map { |z| build_pattern(z) }.join(', ')
574
+ else
575
+ build_pattern(node.zeroOrMore)
576
+ end
577
+ return "(#{inner})#{occurrence}"
578
+ elsif node.oneOrMore && !(node.oneOrMore.is_a?(Array) && node.oneOrMore.empty?)
579
+ inner = if node.oneOrMore.is_a?(Array)
580
+ node.oneOrMore.map { |o| build_pattern(o) }.join(', ')
581
+ else
582
+ build_pattern(node.oneOrMore)
583
+ end
584
+ return "(#{inner})#{occurrence}"
585
+ end
586
+ end
587
+
588
+ # Handle Element directly
589
+ return build_element(node) if node.is_a?(Element)
590
+
591
+ # Handle various pattern types
592
+ if node.element && !(node.element.is_a?(Array) && node.element.empty?)
593
+ # element can be an array or single element
594
+ if node.element.is_a?(Array)
595
+ if node.element.length == 1
596
+ build_element(node.element.first)
597
+ else
598
+ # Multiple elements - wrap in group
599
+ node.element.map { |elem| build_element(elem) }.join(', ')
600
+ end
601
+ else
602
+ build_element(node.element)
603
+ end
604
+ elsif node.is_a?(Choice)
605
+ build_choice(node)
606
+ elsif node.is_a?(Group) || node.is_a?(Start)
607
+ # Handle Group and Start objects directly
608
+ # They are container types with pattern attributes
609
+ group_parts = []
610
+ if node.group && !(node.group.is_a?(Array) && node.group.empty?)
611
+ group_parts = if node.group.is_a?(Array)
612
+ node.group.map { |g| build_pattern(g) }
613
+ else
614
+ [build_pattern(node.group)]
615
+ end
616
+ elsif node.choice && !(node.choice.is_a?(Array) && node.choice.empty?)
617
+ group_parts = if node.choice.is_a?(Array)
618
+ node.choice.map { |c| build_pattern(c) }
619
+ else
620
+ [build_pattern(node.choice)]
621
+ end
622
+ elsif node.element && !(node.element.is_a?(Array) && node.element.empty?)
623
+ group_parts = if node.element.is_a?(Array)
624
+ node.element.map { |e| build_element(e) }
625
+ else
626
+ [build_element(node.element)]
627
+ end
628
+ elsif node.ref && !(node.ref.is_a?(Array) && node.ref.empty?)
629
+ group_parts = if node.ref.is_a?(Array)
630
+ node.ref.map(&:name)
631
+ else
632
+ [node.ref.name]
633
+ end
634
+ elsif node.optional && !(node.optional.is_a?(Array) && node.optional.empty?)
635
+ group_parts = if node.optional.is_a?(Array)
636
+ node.optional.map { |p| build_pattern(p) }
637
+ else
638
+ [build_pattern(node.optional)]
639
+ end
640
+ elsif node.zeroOrMore && !(node.zeroOrMore.is_a?(Array) && node.zeroOrMore.empty?)
641
+ group_parts = if node.zeroOrMore.is_a?(Array)
642
+ node.zeroOrMore.map { |p| build_pattern(p) }
643
+ else
644
+ [build_pattern(node.zeroOrMore)]
645
+ end
646
+ elsif node.oneOrMore && !(node.oneOrMore.is_a?(Array) && node.oneOrMore.empty?)
647
+ group_parts = if node.oneOrMore.is_a?(Array)
648
+ node.oneOrMore.map { |p| build_pattern(p) }
649
+ else
650
+ [build_pattern(node.oneOrMore)]
651
+ end
652
+ end
653
+ "(#{group_parts.join(', ')})"
654
+ elsif node.ref
655
+ if node.ref.is_a?(Array)
656
+ # Array of refs - format as choice
657
+ node.ref.map(&:name).join(' | ')
658
+ else
659
+ node.ref.name
660
+ end
661
+ elsif node.value
662
+ value_str = node.value.is_a?(Value) ? node.value.value : node.value.to_s
663
+ "\"#{value_str}\""
664
+ elsif node.mixed
665
+ mixed_inner = build_mixed_content(node.mixed)
666
+ "mixed {\n #{mixed_inner}\n}"
667
+ elsif node.zeroOrMore
668
+ "#{build_pattern(node.zeroOrMore)}*"
669
+ elsif node.oneOrMore
670
+ if node.oneOrMore.is_a?(Array)
671
+ node.oneOrMore.map { |p| "#{build_pattern(p)}+" }.join(', ')
672
+ else
673
+ "#{build_pattern(node.oneOrMore)}+"
674
+ end
675
+ elsif node.optional
676
+ "#{build_pattern(node.optional)}?"
677
+ elsif node.text
678
+ 'text'
679
+ elsif node.empty
680
+ 'empty'
681
+ else
682
+ # Default case
683
+ ''
684
+ end
685
+ end
686
+
687
+ # Build RNC name class syntax for AnyName, NsName, Name objects
688
+ #
689
+ # @param node [Object] Element or Attribute with name class
690
+ # @return [String] RNC name class syntax
691
+ def build_name_class(node)
692
+ if node.anyName
693
+ result = '*'
694
+ result += " - #{build_except(node.anyName.except)}" if node.anyName.except
695
+ result
696
+ elsif node.nsName
697
+ ns_uri = node.nsName.ns
698
+ # If ns is nil, use the default namespace from the grammar
699
+ ns_uri = node.lutaml_root.ns if ns_uri.nil? && node.lutaml_root
700
+ # Output as namespace prefix if we have one, otherwise use literal URI
701
+ if ns_uri
702
+ # Look up the prefix for this URI from the grammar's namespace declarations
703
+ prefix = find_prefix_for_uri(node, ns_uri)
704
+ result = prefix ? "#{prefix}:*" : "\"#{ns_uri}\":*"
705
+ else
706
+ # No namespace info available - this shouldn't happen but be safe
707
+ result = '*'
708
+ end
709
+ result += " - #{build_except(node.nsName.except)}" if node.nsName.except
710
+ result
711
+ elsif node.name.is_a?(Name) && node.name.value
712
+ node.name.value
713
+ else
714
+ ''
715
+ end
716
+ end
717
+
718
+ # Find the namespace prefix for a given URI
719
+ def find_prefix_for_uri(node, uri)
720
+ return nil unless node.lutaml_root
721
+
722
+ grammar = node.lutaml_root
723
+ # Check if the grammar has namespace declarations
724
+ if grammar.respond_to?(:namespace) && grammar.namespace
725
+ ns = grammar.namespace
726
+ if ns.is_a?(Array)
727
+ ns.each do |n|
728
+ return n.prefix if n.uri == uri
729
+ end
730
+ elsif ns.respond_to?(:uri) && ns.uri == uri
731
+ return ns.prefix
732
+ end
733
+ end
734
+ nil
735
+ end
736
+
737
+ # Build RNC except syntax
738
+ #
739
+ # @param except [Rng::Except] The except clause
740
+ # @return [String] RNC except syntax
741
+ def build_except(except)
742
+ items = []
743
+ except.name&.each { |n| items << n.value }
744
+ except.ns_name&.each do |ns|
745
+ name = ns.ns ? "#{ns.ns}:*" : 'default:*'
746
+ items << name
747
+ end
748
+ except.choice&.each { |c| items << build_choice(c) }
749
+
750
+ if items.length == 1
751
+ items.first
752
+ else
753
+ "(#{items.join(' | ')})"
754
+ end
755
+ end
756
+
757
+ # Extract clean string from Parslet::Slice or other objects
758
+ #
759
+ # @param obj [Object] The object to extract string from
760
+ # @return [String] Clean string without position markers
761
+ def extract_string(obj)
762
+ if obj.respond_to?(:str)
763
+ # Parslet::Slice - use .str to get clean string
764
+ obj.str
765
+ elsif obj.is_a?(String)
766
+ # Already a string - remove position marker if present
767
+ obj.sub(/@\d+$/, '')
768
+ else
769
+ # Fallback
770
+ obj.to_s.sub(/@\d+$/, '')
771
+ end
772
+ end
773
+
774
+ # Build a data pattern from a Data object
775
+ #
776
+ # @param data [Data] Data object
777
+ # @return [String] RNC data pattern
778
+ def build_data(data)
779
+ type = data.type.to_s
780
+ # Prefix with datatype prefix if available (e.g., xsd:string)
781
+ result = @datatype_prefix ? "#{@datatype_prefix}:#{type}" : type
782
+ if data.param && !data.param.empty?
783
+ params = data.param.map { |p| "#{p.name} = \"#{escape_rnc_string(p.value)}\"" }
784
+ result += " { #{params.join(', ')} }"
785
+ end
786
+ result
787
+ end
788
+
789
+ # Build a list pattern from a List object
790
+ #
791
+ # @param list [List] List object
792
+ # @return [String] RNC list pattern
793
+ def build_list(list)
794
+ content_parts = collect_list_items(list)
795
+ content = content_parts.join(', ')
796
+ "list { #{content} }"
797
+ end
798
+
799
+ # Collect all pattern items from a List node
800
+ #
801
+ # @param list [List] List node
802
+ # @return [Array<String>] Array of pattern strings
803
+ def collect_list_items(list)
804
+ parts = []
805
+ list.element&.each { |e| parts << build_element(e) unless e.nil? }
806
+ list.attribute&.each { |a| parts << build_attribute(a) unless a.nil? }
807
+ list.ref&.each { |r| parts << r.name unless r.nil? }
808
+ list.text&.each { |_t| parts << 'text' }
809
+ list.empty&.each { |_e| parts << 'empty' }
810
+ list.value&.each { |v| parts << "\"#{escape_rnc_string(v.value)}\"" unless v.nil? }
811
+ list.data&.each { |d| parts << build_data(d) unless d.nil? }
812
+ list.notAllowed&.each { |_n| parts << 'notAllowed' }
813
+ list.choice&.each { |c| parts << build_choice(c) unless c.nil? }
814
+ list.group&.each { |g| parts << "(#{build_pattern(g)})" unless g.nil? }
815
+ list.zeroOrMore&.each { |z| parts << build_pattern(z) unless z.nil? }
816
+ list.oneOrMore&.each { |o| parts << build_pattern(o) unless o.nil? }
817
+ list.optional&.each { |o| parts << build_pattern(o) unless o.nil? }
818
+ list.mixed&.each { |m| parts << build_mixed_content(m) unless m.nil? }
819
+ list.interleave&.each { |i| parts << build_interleave(i) unless i.nil? }
820
+ parts
821
+ end
822
+
823
+ # Build a choice pattern from a Choice object or array of Choices
824
+ #
825
+ # Collects all children (element, ref, value, text, empty, data, etc.)
826
+ # and builds each as a pattern, joined with " | ".
827
+ #
828
+ # @param choices [Choice, Array<Choice>] Choice object(s)
829
+ # @return [String] RNC choice pattern
830
+ def build_choice(choices)
831
+ items = choices.is_a?(Array) ? choices : [choices]
832
+ items.flat_map { |choice| collect_choice_items(choice) }.join(' | ')
833
+ end
834
+
835
+ # Build an interleave pattern from an Interleave object
836
+ #
837
+ # In RNC, interleave uses the & operator to separate child patterns.
838
+ #
839
+ # @param interleave [Interleave] Interleave object
840
+ # @return [String] RNC interleave pattern
841
+ def build_interleave(interleave)
842
+ parts = collect_interleave_items(interleave)
843
+ parts.join(' & ')
844
+ end
845
+
846
+ # Collect datatype library URIs from all data elements in a grammar
847
+ def collect_datatype_libraries(schema)
848
+ return unless schema.respond_to?(:define)
849
+
850
+ schema.define.each do |d|
851
+ collect_dt_from_node(d)
852
+ end
853
+ end
854
+
855
+ def collect_dt_from_node(node)
856
+ return unless node
857
+
858
+ if node.respond_to?(:data) && node.data
859
+ [node.data].flatten.each do |d|
860
+ next unless d.datatypeLibrary && !d.datatypeLibrary.empty? &&
861
+ d.datatypeLibrary != 'omitted' && d.datatypeLibrary != 'empty'
862
+
863
+ @datatype_library_uri ||= d.datatypeLibrary
864
+ end
865
+ end
866
+ # Recurse into common pattern attributes (only for LUTAML model objects)
867
+ return unless node.respond_to?(:element_order) # LUTAML model check
868
+
869
+ %i[data element attribute choice group interleave optional zeroOrMore oneOrMore
870
+ mixed list ref].each do |attr|
871
+ val = node.send(attr) if node.respond_to?(attr)
872
+ next unless val && !(val.respond_to?(:empty?) && val.empty?)
873
+
874
+ [val].flatten.each { |v| collect_dt_from_node(v) }
875
+ end
876
+ end
877
+
878
+ # Collect all pattern items from an Interleave node
879
+ #
880
+ # @param interleave [Interleave] Interleave node
881
+ # @return [Array<String>] Array of pattern strings
882
+ def collect_interleave_items(interleave)
883
+ parts = []
884
+ interleave.element&.each { |e| parts << build_element(e) unless e.nil? }
885
+ interleave.attribute&.each { |a| parts << build_attribute(a) unless a.nil? }
886
+ interleave.ref&.each { |r| parts << r.name unless r.nil? }
887
+ interleave.text&.each { |_t| parts << 'text' }
888
+ interleave.empty&.each { |_e| parts << 'empty' }
889
+ interleave.value&.each { |v| parts << "\"#{escape_rnc_string(v.value)}\"" unless v.nil? }
890
+ interleave.data&.each { |d| parts << build_data(d) unless d.nil? }
891
+ interleave.list&.each { |l| parts << build_list(l) unless l.nil? }
892
+ interleave.notAllowed&.each { |_n| parts << 'notAllowed' }
893
+ interleave.choice&.each { |c| parts << build_choice(c) unless c.nil? }
894
+ interleave.group&.each { |g| parts << "(#{build_pattern(g)})" unless g.nil? }
895
+ interleave.zeroOrMore&.each { |z| parts << build_pattern(z) unless z.nil? }
896
+ interleave.oneOrMore&.each { |o| parts << build_pattern(o) unless o.nil? }
897
+ interleave.optional&.each { |o| parts << build_pattern(o) unless o.nil? }
898
+ interleave.mixed&.each { |m| parts << build_mixed_content(m) unless m.nil? }
899
+ interleave.interleave&.each { |i| parts << "(#{build_interleave(i)})" unless i.nil? }
900
+ parts
901
+ end
902
+
903
+ # Collect all pattern items from a single Choice node
904
+ #
905
+ # @param choice [Choice] Choice node
906
+ # @return [Array<String>] Array of pattern strings
907
+ def collect_choice_items(choice)
908
+ parts = []
909
+ choice.element&.each { |e| parts << build_element(e) }
910
+ choice.attribute&.each { |a| parts << build_attribute(a) }
911
+ choice.ref&.each { |r| parts << r.name }
912
+ choice.value&.each { |v| parts << "\"#{escape_rnc_string(v.value)}\"" }
913
+ choice.text&.each { |_t| parts << 'text' }
914
+ choice.empty&.each { |_e| parts << 'empty' }
915
+ choice.data&.each { |d| parts << build_data(d) }
916
+ choice.list&.each { |l| parts << build_list(l) }
917
+ choice.notAllowed&.each { |_n| parts << 'notAllowed' }
918
+ choice.choice&.each { |c| parts << build_choice(c) }
919
+ choice.group&.each { |g| parts << "(#{build_pattern(g)})" }
920
+ choice.zeroOrMore&.each { |z| parts << build_pattern(z) }
921
+ choice.oneOrMore&.each { |o| parts << build_pattern(o) }
922
+ choice.optional&.each { |o| parts << build_pattern(o) }
923
+ choice.mixed&.each { |m| parts << build_mixed_content(m) }
924
+ parts
925
+ end
926
+ end
927
+ end