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

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