Class: Rng::RncToRngConverter
- Inherits:
-
Object
- Object
- Rng::RncToRngConverter
- Defined in:
- lib/rng/rnc_to_rng_converter.rb
Overview
RncToRngConverter converts RNC parse trees to RNG XML format.
This class takes the parse tree output from the Parslet RNC parser and converts it to RNG XML using Nokogiri’s XML builder. The resulting XML can then be deserialized into Grammar objects using Lutaml::Model.
Constant Summary collapse
- RNG_NAMESPACE =
'http://relaxng.org/ns/structure/1.0'
Instance Method Summary collapse
-
#convert(tree) ⇒ String
Convert a parse tree to RNG XML.
Instance Method Details
#convert(tree) ⇒ String
Convert a parse tree to RNG XML
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/rng/rnc_to_rng_converter.rb', line 25 def convert(tree) # Track defined names for augmentation support defined_names = Set.new # Check if we need the annotations namespace @has_documentation = has_documentation_comments?(tree) # Collect prefixed namespace declarations from preamble @namespace_prefixes = {} collect_namespace_prefixes(tree[:preamble_items]) # Validate that element notations in preamble annotations are only used with element/attribute patterns validate_preamble_element_notation_usage(tree[:preamble_items], tree[:definitions]) builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| grammar_attrs = { xmlns: 'http://relaxng.org/ns/structure/1.0' } if @has_documentation grammar_attrs[:'xmlns:a'] = 'http://relaxng.org/ns/compatibility/annotations/1.0' end xml.grammar(grammar_attrs) do # Add namespace if present if tree[:namespace] xml.parent[:ns] = process_string_literal(tree[:namespace][:namespace_uri]) end # Add datatype library if present if tree[:datatype_library] xml.parent[:datatypeLibrary] = process_string_literal(tree[:datatype_library][:uri]) elsif tree[:datatype_map] && !tree[:datatype_map].empty? # From ParseTreeProcessor - datatype_map is {prefix => uri} # Use the first (typically only) datatype library xml.parent[:datatypeLibrary] = tree[:datatype_map].values.first end # If no explicit start but we have top-level elements, wrap them in start has_explicit_start = tree[:start] && tree[:start][:start_pattern] has_top_elements = tree[:definitions]&.any? do |d| d.key?(:top_element) end has_top_choice = tree[:definitions]&.any? do |d| d.key?(:top_choice) end if has_explicit_start # Process explicit start pattern xml.start do add_documentation(xml, tree[:start]) if tree[:start][:docs] process_pattern_list(xml, tree[:start][:start_pattern]) end elsif has_top_choice # No explicit start, but has top-level choice - wrap in start with choice first_choice = tree[:definitions].find { |d| d.key?(:top_choice) } xml.start do xml.choice do process_content_item(xml, first_choice[:top_choice][:first]) first_choice[:top_choice][:choice_items]&.each do |item| process_content_item(xml, item) end end end elsif has_top_elements # No explicit start, but has top-level elements - wrap first one in start xml.start do first_element = tree[:definitions].find do |d| d.key?(:top_element) end process_content_item(xml, first_element[:top_element]) end end # Process named patterns and remaining top-level elements tree[:definitions]&.each_with_index do |def_item, idx| if def_item.key?(:href) # Include directive href = process_string_literal(def_item[:href]) if def_item[:override] # Include with override block - override is properly scoped override = def_item[:override] # Check if override has any content has_content = (override[:start] && override[:start][:start_pattern]) || (override[:patterns] && !override[:patterns].empty?) if has_content xml.include(href: href) do # Process override start if present if override[:start] && override[:start][:start_pattern] xml.start do process_pattern_list(xml, override[:start][:start_pattern]) end end # Process override patterns (named patterns, div blocks, and top-level elements) override[:patterns]&.each do |pattern_item| if pattern_item.key?(:name) # Named pattern in override name = process_identifier(pattern_item[:name]) operator = pattern_item[:operator] ? extract_string(pattern_item[:operator]) : '=' if operator == '=' xml.define(name: name) do if pattern_item[:docs] add_documentation(xml, pattern_item) end process_pattern_list(xml, pattern_item[:pattern]) end else combine_type = operator == '|=' ? 'choice' : 'interleave' xml.define(name: name, combine: combine_type) do if pattern_item[:docs] add_documentation(xml, pattern_item) end process_pattern_list(xml, pattern_item[:pattern]) end end elsif pattern_item.key?(:top_element) # Top-level element in override process_content_item(xml, pattern_item[:top_element]) elsif pattern_item.key?(:div) # Div block in override process_div_block(xml, pattern_item[:div]) end end end else # Empty override block xml.include(href: href) end else # Include without override block xml.include(href: href) end elsif def_item.key?(:name) # Named pattern - handle augmentation operators name = process_identifier(def_item[:name]) operator = def_item[:operator] ? extract_string(def_item[:operator]) : '=' if operator == '=' || !defined_names.include?(name) # First definition or normal definition xml.define(name: name) do add_documentation(xml, def_item) if def_item[:docs] process_pattern_list(xml, def_item[:pattern]) end defined_names.add(name) else # Augmentation - use combine attribute combine_type = operator == '|=' ? 'choice' : 'interleave' xml.define(name: name, combine: combine_type) do add_documentation(xml, def_item) if def_item[:docs] process_pattern_list(xml, def_item[:pattern]) end end elsif def_item.key?(:top_element) && has_explicit_start # Top-level element (only add if we already have explicit start) process_content_item(xml, def_item[:top_element]) elsif def_item.key?(:top_element) && !has_explicit_start && idx.positive? # Additional top-level elements after the first (which is in start) process_content_item(xml, def_item[:top_element]) elsif def_item.key?(:top_choice) && !has_explicit_start # Top-level choice already handled in start generation above, skip elsif def_item.key?(:div) # Div block for documentation and grouping process_div_block(xml, def_item[:div]) elsif def_item.key?(:standalone) # Standalone pattern (bare wildcard, etc.) process_standalone_pattern(xml, def_item[:standalone]) elsif def_item.key?(:foreign_name) # Foreign element annotation - emit as foreign element in RNG XML process_foreign_element(xml, def_item) end end end end builder.to_xml end |