37 lines
1.5 KiB
TypeScript
37 lines
1.5 KiB
TypeScript
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<string, { normal: Range[]; emergency: Range[] }>();
|
||
|
|
|
||
|
|
/** 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"];
|
||
|
|
}
|