Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 180x 180x 180x 180x 180x 180x 180x 180x 180x 180x 180x 180x 180x 180x 180x 180x 1x 1x 1x 120x 120x 120x 120x 1x 1x 1x 181x 181x 181x 116x 116x 113x 113x 113x 116x 181x 181x | /**
* @group Helpers
* @groupDescription Shared building blocks for chemical-formula detection, used by both
* `findFormulaInText` and `findFormulaInHtml`. They differ only in how subscripts are
* represented (unicode/entity/escape/tag for plain text vs `<sub>`/`<sup>` tags for HTML)
* and in post-match conversion; the element gating, charge/separator/coefficient grammar,
* and "pick the most likely of several matches" logic live here so they aren't duplicated.
* @source
*/
/**
* @summary Full formula pattern:
* @document ../../REGEXP_PATTERNS.md
*/
/**
* Element-symbol alternation (all 118 symbols, folded into per-first-letter character classes).
* Gates formula matching so ordinary prose isn't read as a formula.
* @category Science Helpers
* @group Regex Patterns
* @source
*/
export const FORMULA_ELEMENT_PATTERN =
'(?:H[eogsf]?|L[iavru]|B[eahkri]?|C[arofmusenld]?|N[eiahopdb]?|O[sg]?|F[rlem]?|M[godtcn]|A[lrsgutmc]|S[icerngmb]?|P[uabotmrd]?|Kr?|T[icebmsalh]|V|Z[nr]|G[ade]|R[buhenagf]|Yb?|I[nr]?|Xe|E[urs]|D[ysb]|W|U)';
/**
* Builds the chemical-formula matching regex, parameterized by the subscript token for the
* source format. Everything else is shared: element gating, an optional ionic charge,
* salt/hydrate separators with optional coefficients, and a "head" that must look like a
* formula — either ≥2 element/bracket units, or a single element carrying a real subscript.
*
* The returned regex is **global**, so callers can collect every candidate (via `matchAll`)
* and choose the most likely one with {@link pickBestFormula}.
* @category Science Helpers
* @document ./CHEMICAL_FORMULA_PATTERN.md
* @see https://regex101.com/r/YBovln/4
* @param subToken - Regex source matching one real subscript/superscript in the source format
* (e.g. `<su[bp]>…</su[bp]>` for HTML, or the union of glyph/escape/entity/tag forms for text).
* @returns A global RegExp that matches formula candidates.
* @example
* ```typescript
* const re = buildFormulaPattern("<su[bp]>[1-9][0-9]*</su[bp]>");
* [...("H<sub>2</sub>O and CO<sub>2</sub>".matchAll(re))].map((m) => m[0]);
* // ["H<sub>2</sub>O", "CO<sub>2</sub>"]
* ```
* @source
*/
export function buildFormulaPattern(subToken: string): RegExp {
const element = FORMULA_ELEMENT_PATTERN;
const subPart = `(?:${subToken}|[1-9][0-9]*)`;
const unit = `(?:(?:${element}|[()\\[\\]])+(?:${subPart})*)`;
const head = `(?:${unit})+`;
const charge = '(?:[+-](?![A-Za-z0-9]))?';
const separator = '(?:\\s*[·•‧∙⋅・・*]\\s*|\\.(?=[A-Za-z(\\[]))';
const coefficient = `(?:${subToken}|[1-9][0-9]*(?:/[1-9][0-9]*)?|[xn])`;
// Preceding char must be whitespace or start-of-string; the double-negative
// form makes start/end fall through for free and covers \r\n, \t, etc.
const leftBoundary = '(?<![^\\s>])';
// Following char must be whitespace or end-of-string.
const rightBoundary = '(?![^\\s\\.<])';
return new RegExp(
`${leftBoundary}((?![^<>]*>)${head}${charge}(?:${separator}(?:${coefficient})?${unit}+${charge})*)${rightBoundary}`,
'g',
);
}
/**
* Scores how "formula-like" a candidate is, so the best of several matches can be chosen.
* Favors more element symbols first (a real formula packs several), then the presence of a
* subscript/count, then raw length — so `NaOSOCH3` outranks a two-letter coincidence such as
* `IN` (from "EINECS") or `CS` pulled out of surrounding codes/prose.
*
* @category Science Helpers
* @param candidate - A raw formula match.
* @returns A numeric score; higher is more likely to be a real formula.
* @example
* ```typescript
* scoreFormula("NaOSOCH3") > scoreFormula("IN"); // true
* ```
* @source
*/
export function scoreFormula(candidate: string): number {
const elementCount = candidate.match(new RegExp(FORMULA_ELEMENT_PATTERN, 'g'))?.length ?? 0;
const hasCount = /[0-9₀-₉⁰-⁹]/.test(candidate);
return elementCount * 100 + (hasCount ? 50 : 0) + candidate.length;
}
/**
* Returns the most-likely-correct formula from a list of raw matches (highest {@link scoreFormula},
* first match winning ties), or `undefined` when the list is empty.
*
* @category Science Helpers
* @param candidates - Raw formula matches, e.g. from `matchAll`.
* @returns The best candidate, or `undefined`.
* @example
* ```typescript
* pickBestFormula(["IN", "CS", "NaOSOCH3"]); // "NaOSOCH3"
* ```
* @source
*/
export function pickBestFormula(candidates: string[]): string | undefined {
let best: string | undefined;
let bestScore = -Infinity;
for (const candidate of candidates) {
const score = scoreFormula(candidate);
if (score > bestScore) {
bestScore = score;
best = candidate;
}
}
return best;
}
|