Files
makelore/electron/coding-teacher/read-page.ts

59 lines
2.5 KiB
TypeScript

export function readInteger(value: unknown, fallback: number, maximum = Number.MAX_SAFE_INTEGER): number {
const result = value ?? fallback;
if (typeof result !== 'number' || !Number.isSafeInteger(result) || result < 1 || result > maximum)
throw new Error('Invalid read range');
return result;
}
export function textChunks(text: string): string[] {
return [text.replaceAll('\r\n', '\n')];
}
// JSON contains contiguous original text; columns count Unicode code points, starting at 1.
export async function readPage(
source: string,
chunks: AsyncIterable<string> | Iterable<string>,
args: Record<string, unknown>,
maxBytes: number,
): Promise<{ content: string; truncated: boolean }> {
const start = readInteger(args.start_line, 1);
const column = readInteger(args.start_column, 1);
const count = readInteger(args.line_count, 120, 400);
const chars: string[] = [];
let lineNumber = 1, position = 1, more = false;
outer: for await (const chunk of chunks) {
for (const char of chunk) {
if (lineNumber === start && char === '\n' && position < column)
throw new Error('Column is past the line');
if (lineNumber > start || (lineNumber === start && position >= column)) {
if (lineNumber >= start + count || chars.length >= maxBytes) { more = true; break outer; }
chars.push(char);
}
if (char === '\n') { lineNumber++; position = 1; } else position++;
}
}
if (lineNumber < start || (lineNumber === start && position < column))
throw new Error('Read position is past the source');
const render = (length: number) => {
let nextLine = start, nextColumn = column;
for (let i = 0; i < length; i++) {
if (chars[i] === '\n') { nextLine++; nextColumn = 1; } else nextColumn++;
}
const next = more || length < chars.length ? { start_line: nextLine, start_column: nextColumn } : null;
return { content: JSON.stringify({ source, start_line: start, start_column: column,
text: chars.slice(0, length).join(''), next }), truncated: next !== null };
};
const complete = render(chars.length);
if (Buffer.byteLength(complete.content) <= maxBytes) return complete;
let low = 0, high = chars.length;
while (low < high) {
const middle = Math.ceil((low + high) / 2);
if (Buffer.byteLength(render(middle).content) <= maxBytes) low = middle;
else high = middle - 1;
}
const page = render(low);
if ((chars.length && !low) || Buffer.byteLength(page.content) > maxBytes)
throw new Error('Read budget too small for a page');
return page;
}