|
| 1 | +/** |
| 2 | + * `<Slice>` — show a window of completed rendered text |
| 3 | + * (specs/executable-mdx-spec.md §6.21). |
| 4 | + * |
| 5 | + * The content renders once, in full, and the window is taken from the string it |
| 6 | + * produced. Reaching the requested number of characters is not a reason to stop |
| 7 | + * the work behind them: this bounds what a reader is shown rather than what a |
| 8 | + * producer does, so a log the document asked for is still written whole and a |
| 9 | + * command it ran still finishes. |
| 10 | + * |
| 11 | + * Positions count Unicode code points rather than UTF-16 code units or bytes, |
| 12 | + * so an astral character is one position and a window can never land between |
| 13 | + * the two halves of one. |
| 14 | + * |
| 15 | + * A partial window carries a note saying which interval of which total it is, |
| 16 | + * because an excerpt that did not say so could not be told from the whole of a |
| 17 | + * shorter text — and an empty window could not be told from work that produced |
| 18 | + * nothing at all. The note is added after the selection and sits outside it, so |
| 19 | + * an outer `<Slice>` reads it as the ordinary text it is and may cut it. |
| 20 | + */ |
| 21 | + |
| 22 | +import type { Operation } from "effection"; |
| 23 | +import { content } from "../component-api.ts"; |
| 24 | +import type { Json } from "../types.ts"; |
| 25 | + |
| 26 | +export const props = { |
| 27 | + type: "object", |
| 28 | + properties: { |
| 29 | + offset: { type: "integer", default: 0 }, |
| 30 | + length: { type: "integer", minimum: 1, default: 4096 }, |
| 31 | + }, |
| 32 | + additionalProperties: false, |
| 33 | +}; |
| 34 | + |
| 35 | +/** What a partial window says about itself, in the document's own text. */ |
| 36 | +function note(start: number, end: number, total: number): string { |
| 37 | + return `Text excerpt: code points [${start}, ${end}) of ${total} (zero-based, end exclusive).`; |
| 38 | +} |
| 39 | + |
| 40 | +export default function* Slice(props: Record<string, Json>): Operation<string> { |
| 41 | + const offset = Number(props.offset); |
| 42 | + const length = Number(props.length); |
| 43 | + |
| 44 | + // Once, and completely. A negative offset is a position from the end, so the |
| 45 | + // total has to be known before any window can be resolved — and waiting for |
| 46 | + // the same completion when the offset is positive is what keeps one component |
| 47 | + // from answering two different questions depending on how it was written. |
| 48 | + const body = yield* content(); |
| 49 | + |
| 50 | + const points = Array.from(body); |
| 51 | + const total = points.length; |
| 52 | + const from = offset < 0 ? total + offset : offset; |
| 53 | + const start = Math.min(Math.max(from, 0), total); |
| 54 | + const end = Math.min(total, start + length); |
| 55 | + |
| 56 | + // A window that omits nothing has nothing to report, so the body is handed |
| 57 | + // back exactly as it arrived rather than rebuilt out of its code points. |
| 58 | + if (start === 0 && end === total) { |
| 59 | + return body; |
| 60 | + } |
| 61 | + return `${note(start, end, total)}\n${points.slice(start, end).join("")}`; |
| 62 | +} |
0 commit comments