Files
openclaw/packages/markdown-core/src/chunk-text.ts
Peter Steinberger e686693732 feat(tooling): adopt noUncheckedIndexedAccess via a strict-ratchet lane (phase 1) (#104577)
* feat(tooling): add noUncheckedIndexedAccess strict-ratchet lane

* fix(packages): make indexed access explicit across ratchet packages

Burns down all 65 noUncheckedIndexedAccess errors in the seven ratchet
packages with behavior-identical restructuring (iteration, charAt/slice,
regex-group guards, validated hextet tuple). net-policy invariant
violations now throw instead of failing open. Adds
@openclaw/normalization-core/expect (expectDefined/first/last) with
subpath export and tests. memory-host-sdk fixes kept but the package
stays out of the lane: it re-exports core src/** so the flag would apply
transitively to all of core.

* fix(lint): keep .oxlintrc.json strict-JSON parseable for extension lint wrappers
2026-07-11 10:26:12 -07:00

99 lines
2.7 KiB
TypeScript

// Markdown Core module implements chunk text behavior.
function resolveChunkEarlyReturn(text: string, limit: number): string[] | undefined {
if (!text) {
return [];
}
if (limit <= 0) {
return [text];
}
if (text.length <= limit) {
return [text];
}
return undefined;
}
function scanParenAwareBreakpoints(text: string): { lastNewline: number; lastWhitespace: number } {
let lastNewline = -1;
let lastWhitespace = -1;
let depth = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
// Parenthesized spans often contain rewritten links or file references;
// avoid splitting them unless the window has no safer outside break.
if (char === "(") {
depth += 1;
continue;
}
if (char === ")" && depth > 0) {
depth -= 1;
continue;
}
if (depth !== 0) {
continue;
}
if (char === "\n") {
lastNewline = i;
} else if (/\s/.test(char)) {
lastWhitespace = i;
}
}
return { lastNewline, lastWhitespace };
}
/**
* Keeps UTF-16 chunk boundaries from separating a supplementary-plane character.
* A one-unit positive limit still needs to emit an entire surrogate pair.
*/
export function avoidTrailingHighSurrogateBreak(text: string, start: number, end: number): number {
if (
end >= text.length ||
text.charCodeAt(end - 1) < 0xd800 ||
text.charCodeAt(end - 1) > 0xdbff ||
text.charCodeAt(end) < 0xdc00 ||
text.charCodeAt(end) > 0xdfff
) {
return end;
}
return end - 1 > start ? end - 1 : end + 1;
}
/**
* Splits plain text into size-bounded chunks at readable boundaries.
*
* Returns the original text as one chunk when the limit is non-positive.
*/
export function chunkText(text: string, limit: number): string[] {
const early = resolveChunkEarlyReturn(text, limit);
if (early) {
return early;
}
const chunks: string[] = [];
let cursor = 0;
while (cursor < text.length) {
if (text.length - cursor <= limit) {
chunks.push(text.slice(cursor));
break;
}
const windowEnd = Math.min(text.length, cursor + limit);
const window = text.slice(cursor, windowEnd);
const { lastNewline, lastWhitespace } = scanParenAwareBreakpoints(window);
// Prefer block boundaries, then spaces, then a hard size cut when no
// readable breakpoint exists inside this window.
const breakOffset = lastNewline > 0 ? lastNewline : lastWhitespace;
const end = avoidTrailingHighSurrogateBreak(
text,
cursor,
breakOffset > 0 ? cursor + breakOffset : windowEnd,
);
chunks.push(text.slice(cursor, end));
cursor = end;
while (cursor < text.length && /\s/.test(text[cursor] ?? "")) {
cursor += 1;
}
}
return chunks;
}