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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/release.yml +8 -3
- data/.gitignore +11 -0
- data/.rubocop.yml +10 -7
- data/.rubocop_todo.yml +229 -23
- data/CHANGELOG.md +317 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +11 -12
- data/README.adoc +1538 -11
- data/Rakefile +11 -3
- data/docs/Gemfile +8 -0
- data/docs/_config.yml +23 -0
- data/docs/getting-started/index.adoc +75 -0
- data/docs/guides/error-handling.adoc +137 -0
- data/docs/guides/external-references.adoc +128 -0
- data/docs/guides/index.adoc +24 -0
- data/docs/guides/parsing-rnc.adoc +141 -0
- data/docs/guides/parsing-rng-xml.adoc +81 -0
- data/docs/guides/rng-to-rnc.adoc +101 -0
- data/docs/guides/validation.adoc +85 -0
- data/docs/index.adoc +52 -0
- data/docs/reference/api.adoc +126 -0
- data/docs/reference/cli.adoc +182 -0
- data/docs/understanding/architecture.adoc +58 -0
- data/docs/understanding/rng-vs-rnc.adoc +118 -0
- data/exe/rng +5 -0
- data/lib/rng/any_name.rb +10 -8
- data/lib/rng/attribute.rb +28 -26
- data/lib/rng/choice.rb +24 -24
- data/lib/rng/cli.rb +607 -0
- data/lib/rng/data.rb +10 -10
- data/lib/rng/datatype_declaration.rb +26 -0
- data/lib/rng/define.rb +44 -41
- data/lib/rng/div.rb +36 -0
- data/lib/rng/documentation.rb +9 -0
- data/lib/rng/element.rb +39 -37
- data/lib/rng/empty.rb +7 -7
- data/lib/rng/except.rb +25 -25
- data/lib/rng/external_ref.rb +8 -8
- data/lib/rng/external_ref_resolver.rb +602 -0
- data/lib/rng/foreign_attribute.rb +26 -0
- data/lib/rng/foreign_element.rb +33 -0
- data/lib/rng/grammar.rb +14 -12
- data/lib/rng/group.rb +26 -24
- data/lib/rng/include.rb +5 -6
- data/lib/rng/include_processor.rb +461 -0
- data/lib/rng/interleave.rb +23 -23
- data/lib/rng/list.rb +22 -22
- data/lib/rng/mixed.rb +23 -23
- data/lib/rng/name.rb +6 -7
- data/lib/rng/namespace_declaration.rb +47 -0
- data/lib/rng/namespaces.rb +15 -0
- data/lib/rng/not_allowed.rb +7 -7
- data/lib/rng/ns_name.rb +9 -9
- data/lib/rng/one_or_more.rb +23 -23
- data/lib/rng/optional.rb +23 -23
- data/lib/rng/param.rb +7 -8
- data/lib/rng/parent_ref.rb +8 -8
- data/lib/rng/parse_tree_processor.rb +695 -0
- data/lib/rng/pattern.rb +7 -7
- data/lib/rng/ref.rb +8 -8
- data/lib/rng/rnc_builder.rb +927 -0
- data/lib/rng/rnc_parser.rb +605 -305
- data/lib/rng/rnc_to_rng_converter.rb +1408 -0
- data/lib/rng/schema_preamble.rb +73 -0
- data/lib/rng/schema_validator.rb +1622 -0
- data/lib/rng/start.rb +27 -25
- data/lib/rng/test_suite_parser.rb +168 -0
- data/lib/rng/text.rb +11 -8
- data/lib/rng/to_rnc.rb +4 -35
- data/lib/rng/value.rb +6 -7
- data/lib/rng/version.rb +1 -1
- data/lib/rng/zero_or_more.rb +23 -23
- data/lib/rng.rb +68 -17
- data/rng.gemspec +18 -19
- data/scripts/extract_spectest_resources.rb +96 -0
- data/spec/fixtures/compacttest.xml +2511 -0
- data/spec/fixtures/external/circular_a.rng +7 -0
- data/spec/fixtures/external/circular_b.rng +7 -0
- data/spec/fixtures/external/circular_main.rng +7 -0
- data/spec/fixtures/external/external_ref_lib.rng +7 -0
- data/spec/fixtures/external/external_ref_main.rng +7 -0
- data/spec/fixtures/external/include_lib.rng +7 -0
- data/spec/fixtures/external/include_main.rng +3 -0
- data/spec/fixtures/external/nested_chain.rng +6 -0
- data/spec/fixtures/external/nested_leaf.rng +7 -0
- data/spec/fixtures/external/nested_mid.rng +8 -0
- data/spec/fixtures/metanorma/3gpp.rnc +35 -0
- data/spec/fixtures/metanorma/3gpp.rng +105 -0
- data/spec/fixtures/metanorma/basicdoc.rnc +11 -0
- data/spec/fixtures/metanorma/bipm.rnc +148 -0
- data/spec/fixtures/metanorma/bipm.rng +376 -0
- data/spec/fixtures/metanorma/bsi.rnc +104 -0
- data/spec/fixtures/metanorma/bsi.rng +332 -0
- data/spec/fixtures/metanorma/csa.rnc +45 -0
- data/spec/fixtures/metanorma/csa.rng +131 -0
- data/spec/fixtures/metanorma/csd.rnc +43 -0
- data/spec/fixtures/metanorma/csd.rng +132 -0
- data/spec/fixtures/metanorma/gbstandard.rnc +99 -0
- data/spec/fixtures/metanorma/gbstandard.rng +316 -0
- data/spec/fixtures/metanorma/iec.rnc +49 -0
- data/spec/fixtures/metanorma/iec.rng +193 -0
- data/spec/fixtures/metanorma/ietf.rnc +275 -0
- data/spec/fixtures/metanorma/ietf.rng +925 -0
- data/spec/fixtures/metanorma/iho.rnc +58 -0
- data/spec/fixtures/metanorma/iho.rng +179 -0
- data/spec/fixtures/metanorma/isodoc.rnc +873 -0
- data/spec/fixtures/metanorma/isodoc.rng +2704 -0
- data/spec/fixtures/metanorma/isostandard-amd.rnc +43 -0
- data/spec/fixtures/metanorma/isostandard-amd.rng +108 -0
- data/spec/fixtures/metanorma/isostandard.rnc +166 -0
- data/spec/fixtures/metanorma/isostandard.rng +494 -0
- data/spec/fixtures/metanorma/itu.rnc +122 -0
- data/spec/fixtures/metanorma/itu.rng +377 -0
- data/spec/fixtures/metanorma/m3d.rnc +41 -0
- data/spec/fixtures/metanorma/m3d.rng +122 -0
- data/spec/fixtures/metanorma/mpfd.rnc +36 -0
- data/spec/fixtures/metanorma/mpfd.rng +95 -0
- data/spec/fixtures/metanorma/nist.rnc +77 -0
- data/spec/fixtures/metanorma/nist.rng +216 -0
- data/spec/fixtures/metanorma/ogc.rnc +51 -0
- data/spec/fixtures/metanorma/ogc.rng +151 -0
- data/spec/fixtures/metanorma/reqt.rnc +6 -0
- data/spec/fixtures/metanorma/rsd.rnc +36 -0
- data/spec/fixtures/metanorma/rsd.rng +95 -0
- data/spec/fixtures/metanorma/un.rnc +103 -0
- data/spec/fixtures/metanorma/un.rng +367 -0
- data/spec/fixtures/rnc/base.rnc +4 -0
- data/spec/fixtures/rnc/grammar_with_trailing.rnc +8 -0
- data/spec/fixtures/rnc/main_include_trailing.rnc +3 -0
- data/spec/fixtures/rnc/main_with_include.rnc +5 -0
- data/spec/fixtures/rnc/test_augment.rnc +10 -0
- data/spec/fixtures/rnc/test_isodoc_simple.rnc +9 -0
- data/spec/fixtures/rnc/top_level_include.rnc +8 -0
- data/spec/fixtures/spectest_external/case_10_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_10_4.7/y +7 -0
- data/spec/fixtures/spectest_external/case_11_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_12_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_13_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_13_4.7/y +3 -0
- data/spec/fixtures/spectest_external/case_14_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_15_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_16_4.7/x +5 -0
- data/spec/fixtures/spectest_external/case_17_4.7/x +5 -0
- data/spec/fixtures/spectest_external/case_18_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_19_4.7/level1.rng +9 -0
- data/spec/fixtures/spectest_external/case_19_4.7/level2.rng +7 -0
- data/spec/fixtures/spectest_external/case_1_4.5/sub1/x +3 -0
- data/spec/fixtures/spectest_external/case_1_4.5/sub3/x +3 -0
- data/spec/fixtures/spectest_external/case_1_4.5/x +3 -0
- data/spec/fixtures/spectest_external/case_20_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_2_4.5/x +3 -0
- data/spec/fixtures/spectest_external/case_3_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_4_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_5_4.6/x +1 -0
- data/spec/fixtures/spectest_external/case_6_4.6/x +5 -0
- data/spec/fixtures/spectest_external/case_7_4.6/x +1 -0
- data/spec/fixtures/spectest_external/case_7_4.6/y +1 -0
- data/spec/fixtures/spectest_external/case_8_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_9_4.7/x +7 -0
- data/spec/fixtures/spectest_external/resources.json +149 -0
- data/spec/rng/advanced_rnc_spec.rb +101 -0
- data/spec/rng/compacttest_spec.rb +197 -0
- data/spec/rng/datatype_declaration_spec.rb +28 -0
- data/spec/rng/div_spec.rb +207 -0
- data/spec/rng/external_ref_resolver_spec.rb +122 -0
- data/spec/rng/metanorma_conversion_spec.rb +159 -0
- data/spec/rng/namespace_declaration_spec.rb +60 -0
- data/spec/rng/namespace_support_spec.rb +199 -0
- data/spec/rng/rnc_parser_spec.rb +498 -22
- data/spec/rng/rnc_roundtrip_spec.rb +96 -82
- data/spec/rng/rng_generation_spec.rb +288 -0
- data/spec/rng/roundtrip_spec.rb +342 -0
- data/spec/rng/schema_preamble_spec.rb +145 -0
- data/spec/rng/schema_spec.rb +68 -64
- data/spec/rng/spectest_spec.rb +168 -90
- data/spec/rng_spec.rb +2 -2
- data/spec/spec_helper.rb +7 -42
- 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
|