Module: AsciiChem::Cml::GroupExtensions
- Defined in:
- lib/asciichem/cml/group_extensions.rb
Overview
Preserves AsciiChem::Model::Group structure through CML round-trip.
CML has no native group concept — the canonical model flattens
(OH)_2 to <atom elementType="O" count="2"/><atom elementType="H" count="2"/>.
The aci: namespace records the original grouping so AsciiChem
can rebuild it on parse:
The mechanism parallels Extensions (atom attributes) and the
top-level handlers, but operates at molecule scope: each
<aci:group> lives inside its parent <molecule> and references
atoms by ID. Nested groups in AsciiChem produce multiple
<aci:group> elements with overlapping atomRefs — the parent
group's refs include the inner group's atoms.
On restore, the canonical adapter has already applied the multiplicity to atom counts. GroupExtensions reverses this: divides each referenced atom's count by the group's multiplicity (rounding to nil when the result is 1) and wraps the atoms in an AsciiChem::Model::Group.
Constant Summary collapse
- NAMESPACE =
'https://asciichem.org/cml-ext'- PREFIX =
'aci'- CML_NS =
'http://www.xml-cml.org/schema'- BRACKET_TO_WIRE =
AsciiChem bracket symbols → wire strings.
{ paren: 'paren', square: 'square', brace: 'brace' }.freeze
- WIRE_TO_BRACKET =
BRACKET_TO_WIRE.invert.freeze
Class Method Summary collapse
-
.collect(groups_by_molecule) ⇒ Object
Build the group extensions map:
{ molecule_id => [group_record, ...] }. -
.extract(xml) ⇒ Object
Extract
<aci:group>elements, keyed by their parent molecule's ID. -
.inject(xml, groups_by_molecule) ⇒ Object
Inject
<aci:group>elements into each referenced molecule. -
.restore(formula, canonical_doc, groups_by_molecule) ⇒ Object
-- Round-trip restore ----------------------------------------.
Class Method Details
.collect(groups_by_molecule) ⇒ Object
Build the group extensions map: { molecule_id => [group_record, ...] }.
Molecules without groups are omitted (keeps the CML clean for
ungrouped molecules).
48 49 50 |
# File 'lib/asciichem/cml/group_extensions.rb', line 48 def self.collect(groups_by_molecule) groups_by_molecule.reject { |_, groups| groups.empty? } end |
.extract(xml) ⇒ Object
Extract <aci:group> elements, keyed by their parent
molecule's ID. Each value is an array of record hashes:
{ multiplicity:, bracket:, atom_ids: }.
90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/asciichem/cml/group_extensions.rb', line 90 def self.extract(xml) doc = Nokogiri::XML(xml) result = Hash.new { |h, k| h[k] = [] } doc.xpath('//cml:molecule', cml: CML_NS).each do |mol_el| group_els = mol_el.xpath("./#{PREFIX}:group", PREFIX => NAMESPACE) next if group_els.empty? mol_el['id']&.then do |molecule_id| group_els.each { |g| result[molecule_id] << read_group(g) } end end result end |
.inject(xml, groups_by_molecule) ⇒ Object
Inject <aci:group> elements into each referenced molecule.
No-op when the map is empty.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/asciichem/cml/group_extensions.rb', line 54 def self.inject(xml, groups_by_molecule) return xml if groups_by_molecule.empty? doc = Nokogiri::XML(xml) root = doc.root root.add_namespace(PREFIX, NAMESPACE) unless namespace_declared?(root) groups_by_molecule.each do |molecule_id, groups| molecule_el = root.at_xpath("//cml:molecule[@id='#{molecule_id}']", cml: CML_NS) next unless molecule_el groups.each { |record| molecule_el.add_child(build_group_element(doc, record)) } end doc.to_xml end |
.restore(formula, canonical_doc, groups_by_molecule) ⇒ Object
-- Round-trip restore ----------------------------------------
Walks the rebuilt AsciiChem::Model::Formula's molecules. For each molecule that has group records, rebuilds the Group nodes from the flattened atom list. Atom positions in the rebuilt molecule match the canonical order (a1, a2, ...), so the atom_ids in each record map directly to indices.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/asciichem/cml/group_extensions.rb', line 122 def self.restore(formula, canonical_doc, groups_by_molecule) return formula if groups_by_molecule.empty? canonical_molecules = flatten_canonical_molecules(canonical_doc) formula_molecules = flatten_formula_molecules(formula) canonical_molecules.each_with_index do |canon_mol, idx| groups = groups_by_molecule[canon_mol.id] next unless groups target = formula_molecules[idx] next unless target rebuild_groups_in_molecule(target, groups) end formula end |