ChemPal Documentation - v1.6.0
    Preparing search index...

    Function findFormulaInText

    • Finds the first chemical-formula-like substring in text and returns it with <sub>/<sup> tags converted to Unicode sub/superscript glyphs, or undefined if none is found. Unlike findFormulaInHtml (which only understands <sub>/<sup> markup), this recognizes a subscript/superscript number written in any of four representations, so it works on raw scraped text regardless of how the source encoded it:

      • literal glyphs — H₂O, ;
      • \u escape text — H₂O (e.g. unparsed JSON);
      • HTML numeric entities — H&#8322;O, H&#x2082;O (decimal or hex);
      • <sub>/<sup> tags — H<sub>2</sub>O.

      All four forms are matched so the formula is found; for the returned string only <sub>/<sup> tags are rewritten to glyphs. Glyph input already carries glyphs and passes through unchanged; entity- and \u-escape-encoded numbers are returned in their source encoding. Untagged inline digits (e.g. a salt/hydrate coefficient like ·12H₂O, or 2K after a separator) are matched but left as regular digits.

      Also handles salts/hydrates joined by ·//* (or a tight .) with optional integer, fraction, or variable (x/n) coefficients, and trailing ionic charge signs. Element symbols gate the match — within a longer string it needs at least two element/bracket "units" (so KBr counts as K+Br) or a single element carrying a subscript/superscript, so ordinary prose isn't mistaken for a formula. A lone element (e.g. Na, K+) is accepted only when it is the entire trimmed input, so a bare symbol is never pulled out of a sentence.

      Parameters

      • text: string

        The text string to search for a formula

      Returns undefined | string

      The formula with <sub>/<sup> tags converted to glyphs, or undefined if none is found

      findFormulaInText("C₃₃H₂₅N₃O₁₂S • ₄K")                 // "C₃₃H₂₅N₃O₁₂S • ₄K"
      findFormulaInText("K<sub>2</sub>SO<sub>4</sub>") // "K₂SO₄"
      findFormulaInText("Here is a formula: C₁₀H₇KN₆O·xH₂O") // "C₁₀H₇KN₆O·xH₂O"
      findFormulaInText("Na") // "Na" (lone element, whole input)
      findFormulaInText("I love Nature") // undefined (not pulled from prose)

      https://regex101.com/r/h3ZnXX/4 - Regex pattern explanation

      export const findFormulaInText = (text: string): string | undefined => {
      // A sub/superscript "number" (no leading zero), in each accepted representation.
      // Characters are enumerated rather than ranged so engines that don't compute
      // Unicode ranges (byte-oriented / non-Unicode modes) still match them.
      const glyphSub = '[₁₂₃₄₅₆₇₈₉][₀₁₂₃₄₅₆₇₈₉]*';
      const glyphSup = '[¹²³⁴⁵⁶⁷⁸⁹][⁰¹²³⁴⁵⁶⁷⁸⁹]*';
      // \u escape text (literal backslash-u-XXXX, e.g. before JSON.parse).
      const escSub = String.raw`\\u208[1-9](?:\\u208[0-9])*`;
      const escSup = String.raw`(?:\\u00[bB][239]|\\u207[4-9])(?:\\u2070|\\u00[bB][239]|\\u207[4-9])*`;
      // HTML numeric entities, decimal or hex, with optional leading zeros.
      const entSub = '&#(?:0*832[1-9]|[xX]0*208[1-9]);(?:&#(?:0*832[0-9]|[xX]0*208[0-9]);)*';
      const entSup =
      '&#(?:0*(?:178|179|185|830[89]|831[0-3])|[xX]0*(?:[bB][239]|207[4-9]));(?:&#(?:0*(?:178|179|185|8304|830[89]|831[0-3])|[xX]0*(?:2070|[bB][239]|207[4-9]));)*';
      // <sub>2</sub> / <sup>2</sup> tags.
      const tag = '<su[bp]>[1-9][0-9]*</su[bp]>';
      const subSup = `(?:${glyphSub}|${glyphSup}|${escSub}|${escSup}|${entSub}|${entSup}|${tag})`;

      // Polymer / repeat-unit index: a variable subscript (n, m, x), not a number — as in "(C₃H₃NaO₂)ₙ".
      // Glyph form (ₙ ₘ ₓ) or the <sub>n</sub> HTML form. Unambiguous → no prose-word risk.
      const repeatIndex = '(?:[ₙₘₓ]|<su[bp]>[nmx]</su[bp]>)';
      // Everything that can trail a unit as a "count": a real sub/sup number, or a repeat index.
      const trailingCount = `(?:${subSup}|${repeatIndex})`;

      // A lone element (e.g. "Na", "K+") is a valid formula, but only when it is the entire trimmed
      // input — anchoring keeps a bare symbol from being pulled out of prose (e.g. "I" from "I love …").
      const trimmed = text.trim();
      if (new RegExp(`^${FORMULA_ELEMENT_PATTERN}(?:[+-])?$`).test(trimmed)) {
      return trimmed;
      }

      // Collect every candidate and keep the most likely one, so a real formula isn't shadowed by a
      // two-letter coincidence earlier in the text (e.g. "IN"/"CS" inside "EINECS 243-669-6").
      const best = pickBestFormula(
      [...text.matchAll(buildFormulaPattern(trailingCount))].map((m) => m[0]),
      );
      if (best === undefined) {
      return;
      }
      return (
      best
      .replace(/<sub>(\d+)<\/sub>/g, (_match, p1) => subscript(p1 || ''))
      .replace(/<sup>(\d+)<\/sup>/g, (_match, p1) => superscript(p1 || ''))
      // Repeat-index tag → glyph, mirroring the digit-tag handling above.
      .replace(/<su[bp]>([nmx])<\/su[bp]>/g, (_match, p1: string) => REPEAT_INDEX_GLYPHS[p1] ?? p1)
      .replace(/\\u208[0-9](?:\\u208[0-9])*/g, (match) => subscriptGlyph(match))
      .replace(/(?:\\u00[bB][239]|\\u207[4-9])(?:\\u2070|\\u00[bB][239]|\\u207[4-9])*/g, (match) =>
      superscriptGlyph(match),
      )
      );
      };