import { PASS, Rules } from "@cto.af/linebreak"; import type { Range } from "./types.js"; import { graphemes } from "./text.js"; const normal = new Rules(); // Emergency wrapping may split an oversized word or number, but still obeys // the punctuation, bracket, combining-mark and nonbreaking-space rules. This // is UAX #14 tailoring, not a list of special cases for a particular language. const emergency = new Rules(); for (const rule of ["LB23", "LB25", "LB28", "LB29"]) emergency.replaceRule(rule, () => PASS); const cached = new Map(); /** Legal UTF-16 source spans for line wrapping. Intl.Segmenter still owns word * targeting; line breaking is a separate Unicode operation. */ export function lineSpans(text: string, emergencyWrap = false): Range[] { let entry = cached.get(text); if (!entry) { const spans = (rules: Rules): Range[] => { let start = 0; const boundaries = new Set(graphemes(text).map((unit) => unit.end)); return Array.from(rules.breaks(text)) .filter(({ position }) => boundaries.has(position)) .map(({ position }) => { const range = { start, end: position }; start = position; return range; }); }; entry = { normal: spans(normal), emergency: spans(emergency) }; if (text.length >= 256 && text.length <= 120000) { if (cached.size >= 4) cached.delete(cached.keys().next().value!); cached.set(text, entry); } } return entry[emergencyWrap ? "emergency" : "normal"]; }