{"version":3,"file":"cif-to-json.umd.min.js","sources":["../src/cifParser.js","../src/moleculeFromCifJson.js","../src/moleculeFromCif.js"],"sourcesContent":["/**\n * Parse a CIF (Crystallographic Information File) string into a JSON object.\n * Supports single-quoted, double-quoted, and semicolon-delimited text fields.\n * Handles both classic crystallographic CIF and mmCIF / CCD format (dotted tags\n * like `_chem_comp_atom.atom_id`).\n * @param {string} input - CIF file content as a string.\n * @returns {object} Parsed CIF data. Scalar fields are top-level keys; loop\n *   sections become arrays keyed on the common tag prefix (trailing `_` or `.`\n *   stripped). Each array row uses the full tag name as its property key.\n */\nexport function cifParser(input) {\n  return buildJson(tokenize(input));\n}\n\n/**\n * Tokenize CIF content into a flat array of typed tokens.\n * @param {string} input - Raw CIF text to tokenize.\n * @returns {Array<{type: 'data'|'loop'|'tag'|'value', value?: string|null}>} Flat token stream.\n */\nfunction tokenize(input) {\n  const tokens = [];\n  const lines = input.split(/\\r?\\n/);\n\n  let inSemicolon = false;\n  let semicolonLines = [];\n\n  for (const rawLine of lines) {\n    // --- Semicolon-delimited text block ---\n    if (inSemicolon) {\n      if (rawLine.startsWith(';')) {\n        // Closing semicolon: emit the accumulated text\n        tokens.push({ type: 'value', value: semicolonLines.join('\\n') });\n        inSemicolon = false;\n        semicolonLines = [];\n      } else {\n        semicolonLines.push(rawLine);\n      }\n      continue;\n    }\n\n    const line = rawLine.trimEnd();\n\n    if (!line.trim()) continue;\n    if (line.trimStart().startsWith('#')) continue;\n\n    // Opening semicolon (must be the very first character of the line)\n    if (line.startsWith(';')) {\n      inSemicolon = true;\n      // Text may start on the same line as the opening semicolon;\n      // if the semicolon is alone on its line the remainder is empty — skip it.\n      const afterSemi = line.slice(1);\n      semicolonLines = afterSemi ? [afterSemi] : [];\n      continue;\n    }\n\n    // Inline tokenization\n    let i = 0;\n    while (i < line.length) {\n      // Skip whitespace\n      while (i < line.length && (line[i] === ' ' || line[i] === '\\t')) i++;\n      if (i >= line.length) break;\n\n      const ch = line[i];\n\n      // Inline comment\n      if (ch === '#') break;\n\n      // Quoted string (single or double)\n      if (ch === '\"' || ch === \"'\") {\n        const closeIdx = line.indexOf(ch, i + 1);\n        if (closeIdx === -1) {\n          // Unclosed quote — consume rest of line\n          tokens.push({ type: 'value', value: line.slice(i + 1) });\n          break;\n        }\n        tokens.push({ type: 'value', value: line.slice(i + 1, closeIdx) });\n        i = closeIdx + 1;\n        continue;\n      }\n\n      // Unquoted token — read until whitespace or comment\n      let end = i;\n      while (\n        end < line.length &&\n        line[end] !== ' ' &&\n        line[end] !== '\\t' &&\n        line[end] !== '#'\n      ) {\n        end++;\n      }\n      const token = line.slice(i, end);\n      i = end;\n\n      if (/^loop_$/i.test(token)) {\n        tokens.push({ type: 'loop' });\n      } else if (token.startsWith('data_')) {\n        tokens.push({ type: 'data', value: token.slice(5) });\n      } else if (token.startsWith('_')) {\n        tokens.push({ type: 'tag', value: token });\n      } else if (token === '.' || token === '?') {\n        // CIF missing / unknown value → empty string (backward-compatible)\n        tokens.push({ type: 'value', value: '' });\n      } else {\n        tokens.push({ type: 'value', value: token });\n      }\n    }\n  }\n\n  // Unclosed semicolon block (malformed CIF) — emit what we have\n  if (inSemicolon && semicolonLines.length > 0) {\n    tokens.push({ type: 'value', value: semicolonLines.join('\\n') });\n  }\n\n  return tokens;\n}\n\n/**\n * Build a plain JSON object from the token stream.\n * @param {Array<{type: string, value?: string|null}>} tokens - Flat token stream from {@link tokenize}.\n * @returns {object} Parsed CIF data as a plain object.\n */\nfunction buildJson(tokens) {\n  const result = {};\n  let i = 0;\n\n  while (i < tokens.length) {\n    const token = tokens[i];\n\n    // data_ header — skip (block name is not surfaced as a key)\n    if (token.type === 'data') {\n      i++;\n      continue;\n    }\n\n    if (token.type === 'loop') {\n      i++;\n\n      // Collect column headers\n      const headers = [];\n      while (i < tokens.length && tokens[i].type === 'tag') {\n        headers.push(tokens[i].value);\n        i++;\n      }\n\n      if (headers.length === 0) continue;\n\n      // Determine the shared prefix, stripping a trailing `_` or `.`\n      const common = getCommonBeginning(headers).replace(/[_.]$/, '');\n\n      // Collect data rows\n      const rows = [];\n      while (i < tokens.length && tokens[i].type === 'value') {\n        const row = {};\n        for (let h = 0; h < headers.length; h++) {\n          if (i >= tokens.length || tokens[i].type !== 'value') break;\n          row[headers[h]] = tokens[i].value;\n          i++;\n        }\n        if (Object.keys(row).length === headers.length) {\n          rows.push(row);\n        }\n      }\n\n      result[common] = rows;\n      continue;\n    }\n\n    if (token.type === 'tag') {\n      const tag = token.value;\n      i++;\n      if (i < tokens.length && tokens[i].type === 'value') {\n        result[tag] = tokens[i].value;\n        i++;\n      }\n      continue;\n    }\n\n    // Stray value token (should not happen in well-formed CIF)\n    i++;\n  }\n\n  return result;\n}\n\n/**\n * Return the longest common prefix shared by all strings.\n * @param {string[]} strings - Array of strings to compare.\n * @returns {string} Longest common prefix.\n */\nfunction getCommonBeginning(strings) {\n  if (strings.length === 0) return '';\n  let common = '';\n  for (let pos = 0; pos < strings[0].length; pos++) {\n    const ch = strings[0][pos];\n    for (let k = 1; k < strings.length; k++) {\n      if (strings[k][pos] !== ch) return common;\n    }\n    common += ch;\n  }\n  return common;\n}\n","/**\n * Normalize a CCD element symbol to the mixed-case form OCL expects.\n * CCD writes `C`, `Cl`, `FE`, …; OCL requires `C`, `Cl`, `Fe`, …\n * @param {string} symbol - Raw element symbol from CCD.\n * @returns {string} Mixed-case element symbol.\n */\nfunction normalizeSymbol(symbol) {\n  if (!symbol) return '';\n  return symbol[0].toUpperCase() + symbol.slice(1).toLowerCase();\n}\n\n/**\n * Look up a value in a CDD loop row, treating CIF missing-value markers\n * (`.` and `?`, both mapped to `''` by cifParser) as absent.\n * @param {Record<string,string>} row - Parsed CIF loop row object.\n * @param {string} key - Full tag name to look up.\n * @returns {string} The string value, or `''` if absent or missing.\n */\nfunction get(row, key) {\n  return row[key] ?? '';\n}\n\n/**\n * Build an OpenChemLib `Molecule` from the parsed JSON of a single wwPDB\n * Chemical Component Dictionary (CCD) block.\n *\n * The JSON is expected to come from `cifParser` applied to a single\n * `data_XXX` CCD block. Atoms are added with their ideal Cartesian\n * coordinates (`pdbx_model_Cartn_*_ideal`); this gives OCL enough 3D\n * information to encode tetrahedral and double-bond stereochemistry\n * directly in the resulting idCode — no additional stereo-detection call\n * is required.\n *\n * Returns `null` for entries that cannot be represented as a small\n * molecule: single-atom ions (< 2 atoms), unknown element symbols, or\n * bonds that reference non-existent atom IDs.\n * @param {object} json - Output of `cifParser` for one CCD block.\n * @param {object} Molecule - OpenChemLib `Molecule` class constructor (peer dependency).\n * @returns {{ molecule: object, code: string, name: string, type: string, formula: string, nbAtoms: number } | null} Parsed result or `null` when the entry cannot be represented.\n */\nexport function moleculeFromCifJson(json, Molecule) {\n  const atomRows = /** @type {Array<Record<string,string>>} */ (\n    json._chem_comp_atom ?? []\n  );\n  const bondRows = /** @type {Array<Record<string,string>>} */ (\n    json._chem_comp_bond ?? []\n  );\n\n  if (atomRows.length < 2) return null;\n\n  const code =\n    json['_chem_comp.id'] ?? json['_chem_comp.three_letter_code'] ?? '';\n  const name = json['_chem_comp.name'] ?? '';\n  const type = json['_chem_comp.type'] ?? '';\n  const formula = json['_chem_comp.formula'] ?? '';\n\n  const molecule = new Molecule(atomRows.length, bondRows.length);\n  /** @type {Map<string, number>} */\n  const atomIndex = new Map();\n\n  for (const row of atomRows) {\n    const atomId = get(row, '_chem_comp_atom.atom_id');\n    const rawSymbol = get(row, '_chem_comp_atom.type_symbol');\n    if (!atomId || !rawSymbol) return null;\n\n    const symbol = normalizeSymbol(rawSymbol);\n    const atomicNo = Molecule.getAtomicNoFromLabel(symbol);\n    if (!atomicNo) return null;\n\n    const i = molecule.addAtom(atomicNo);\n\n    // Prefer ideal coordinates; fall back to model coordinates.\n    const xIdeal = get(row, '_chem_comp_atom.pdbx_model_Cartn_x_ideal');\n    const yIdeal = get(row, '_chem_comp_atom.pdbx_model_Cartn_y_ideal');\n    const zIdeal = get(row, '_chem_comp_atom.pdbx_model_Cartn_z_ideal');\n    const xModel = get(row, '_chem_comp_atom.model_Cartn_x');\n    const yModel = get(row, '_chem_comp_atom.model_Cartn_y');\n    const zModel = get(row, '_chem_comp_atom.model_Cartn_z');\n\n    molecule.setAtomX(i, Number.parseFloat(xIdeal || xModel || '0'));\n    molecule.setAtomY(i, Number.parseFloat(yIdeal || yModel || '0'));\n    molecule.setAtomZ(i, Number.parseFloat(zIdeal || zModel || '0'));\n\n    const charge = Number.parseInt(\n      get(row, '_chem_comp_atom.charge') || '0',\n      10,\n    );\n    if (charge) molecule.setAtomCharge(i, charge);\n\n    atomIndex.set(atomId, i);\n  }\n\n  for (const row of bondRows) {\n    const atom1 = get(row, '_chem_comp_bond.atom_id_1');\n    const atom2 = get(row, '_chem_comp_bond.atom_id_2');\n    if (!atom1 || !atom2) return null;\n\n    const a = atomIndex.get(atom1);\n    const b = atomIndex.get(atom2);\n    if (a === undefined || b === undefined) return null;\n\n    const bondIdx = molecule.addBond(a, b);\n    const order = (\n      get(row, '_chem_comp_bond.value_order') || 'SING'\n    ).toUpperCase();\n    molecule.setBondType(bondIdx, bondTypeConstant(order, Molecule));\n  }\n\n  return { molecule, code, name, type, formula, nbAtoms: atomRows.length };\n}\n\n/**\n * Map a CCD `value_order` string to the corresponding OCL bond-type constant.\n * @param {string} order - CCD bond order (e.g. `'SING'`, `'DOUB'`, `'TRIP'`, `'AROM'`).\n * @param {object} Molecule - OCL Molecule class (for bond-type constants).\n * @returns {number} OCL bond type constant.\n */\nfunction bondTypeConstant(order, Molecule) {\n  switch (order) {\n    case 'DOUB':\n      return Molecule.cBondTypeDouble;\n    case 'TRIP':\n      return Molecule.cBondTypeTriple;\n    case 'AROM':\n    case 'AROMATIC':\n      return Molecule.cBondTypeDelocalized;\n    default:\n      return Molecule.cBondTypeSingle;\n  }\n}\n","import { cifParser } from './cifParser.js';\nimport { moleculeFromCifJson } from './moleculeFromCifJson.js';\n\n/**\n * Parse a CIF block and build an OpenChemLib `Molecule` from it.\n *\n * Parses the CIF text with `cifParser` and delegates to\n * `moleculeFromCifJson` which handles the wwPDB Chemical Component\n * Dictionary (CCD) mmCIF schema (`_chem_comp_atom` / `_chem_comp_bond`).\n * When processing the full `components.cif` file, the caller is responsible\n * for splitting the stream into individual `data_XXX` blocks before calling\n * this function.\n * @param {string} cifText - Raw CIF text for a single block.\n * @param {object} Molecule - OpenChemLib `Molecule` class constructor (peer dependency).\n * @returns {{ molecule: object, code: string, name: string, type: string, formula: string, nbAtoms: number } | null}\n *   Parsed result, or `null` when the entry cannot be represented as a\n *   small molecule (single-atom ions, unknown elements, malformed bonds).\n */\nexport function moleculeFromCif(cifText, Molecule) {\n  const json = cifParser(cifText);\n  return moleculeFromCifJson(json, Molecule);\n}\n"],"names":["cifParser","input","tokens","result","i","length","token","type","headers","push","value","common","getCommonBeginning","replace","rows","row","h","Object","keys","tag","buildJson","lines","split","inSemicolon","semicolonLines","rawLine","startsWith","join","line","trimEnd","trim","trimStart","afterSemi","slice","ch","closeIdx","indexOf","end","test","tokenize","strings","pos","k","normalizeSymbol","symbol","toUpperCase","toLowerCase","get","key","moleculeFromCifJson","json","Molecule","atomRows","_chem_comp_atom","bondRows","_chem_comp_bond","code","name","formula","molecule","atomIndex","Map","atomId","rawSymbol","atomicNo","getAtomicNoFromLabel","addAtom","xIdeal","yIdeal","zIdeal","xModel","yModel","zModel","setAtomX","Number","parseFloat","setAtomY","setAtomZ","charge","parseInt","setAtomCharge","set","atom1","atom2","a","b","undefined","bondIdx","addBond","order","setBondType","bondTypeConstant","nbAtoms","cBondTypeDouble","cBondTypeTriple","cBondTypeDelocalized","cBondTypeSingle","cifText"],"mappings":"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"}