@kud/ink-markdown 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -132,10 +132,11 @@ var append = (line, text, style) => {
|
|
|
132
132
|
line[line.length - 1] = { text: last.text + text, style };
|
|
133
133
|
else line.push(style ? { text, style } : { text });
|
|
134
134
|
};
|
|
135
|
-
var INLINE = /(\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*|_([^_]+)_|\[([^\]]+)\]\(([^)]+)\))/g;
|
|
135
|
+
var INLINE = /(\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*|_([^_]+)_|\[([^\]]+)\]\(([^)]+)\)|(https?:\/\/[^\s<>()]+?)(?=[.,;:!?]*(?:\s|$|\))))/g;
|
|
136
136
|
var parseInline = (text, theme) => {
|
|
137
137
|
const spans = [];
|
|
138
138
|
const re = new RegExp(INLINE);
|
|
139
|
+
const link = { color: theme.link, underline: true };
|
|
139
140
|
let last = 0;
|
|
140
141
|
let m;
|
|
141
142
|
while ((m = re.exec(text)) !== null) {
|
|
@@ -147,8 +148,8 @@ var parseInline = (text, theme) => {
|
|
|
147
148
|
spans.push({ text: m[4], style: { italic: true } });
|
|
148
149
|
else if (m[5] !== void 0)
|
|
149
150
|
spans.push({ text: m[5], style: { italic: true } });
|
|
150
|
-
else if (m[6] !== void 0)
|
|
151
|
-
|
|
151
|
+
else if (m[6] !== void 0) spans.push({ text: m[6], style: link });
|
|
152
|
+
else if (m[8] !== void 0) spans.push({ text: m[8], style: link });
|
|
152
153
|
last = m.index + m[0].length;
|
|
153
154
|
}
|
|
154
155
|
if (last < text.length) spans.push({ text: text.slice(last) });
|
|
@@ -317,7 +318,8 @@ var createMarkdownLayout = (document, source, options) => {
|
|
|
317
318
|
const lineOffsets = [];
|
|
318
319
|
let reused = 0;
|
|
319
320
|
let computed = 0;
|
|
320
|
-
|
|
321
|
+
document.blocks.forEach((block, i) => {
|
|
322
|
+
if (i > 0) lines.push(toLine(block.id, -1, []));
|
|
321
323
|
const key = `${block.id}:${width}:${themeId}`;
|
|
322
324
|
let blockLines = cache?.get(key);
|
|
323
325
|
if (blockLines) {
|
|
@@ -335,7 +337,7 @@ var createMarkdownLayout = (document, source, options) => {
|
|
|
335
337
|
lines: blockLines
|
|
336
338
|
});
|
|
337
339
|
for (const line of blockLines) lines.push(line);
|
|
338
|
-
}
|
|
340
|
+
});
|
|
339
341
|
return {
|
|
340
342
|
width,
|
|
341
343
|
themeId,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/hash.ts","../src/core/segment.ts","../src/core/document.ts","../src/core/spans.ts","../src/core/layout.ts","../src/ink/markdown-viewport.tsx","../src/ink/ansi.ts","../src/ink/use-markdown-scroll.ts"],"sourcesContent":["// FNV-1a — cheap, stable content hash used for block identity and layout-cache keys.\nexport const hash = (input: string): string => {\n let h = 0x811c9dc5\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(36)\n}\n","import { hash } from \"./hash.js\"\nimport type { MarkdownBlock, MarkdownBlockType } from \"./document.js\"\n\nconst FENCE = /^```(\\S*)\\s*$/\nconst HEADING = /^#{1,6}\\s+/\nconst THEMATIC_BREAK = /^(-{3,}|\\*{3,}|_{3,})\\s*$/\nconst QUOTE = /^>\\s?/\nconst LIST = /^(\\s*)([-*+]|\\d+\\.)\\s+/\n\n// Line-scan segmenter → renderer-independent top-level blocks with stable, content-based\n// identity. Minimal GFM-ish block set; inline parsing happens later in the layout stage.\n// An unterminated fence stays a single open block, which is what the streaming tail needs.\nexport const segment = (source: string): MarkdownBlock[] => {\n const lines = source.split(\"\\n\")\n const lineStart: number[] = []\n let offset = 0\n for (const line of lines) {\n lineStart.push(offset)\n offset += line.length + 1 // + newline\n }\n\n const blocks: MarkdownBlock[] = []\n const seen = new Map<string, number>()\n\n const push = (\n type: MarkdownBlockType,\n fromLine: number,\n toLine: number,\n lang?: string,\n ) => {\n const lastLine = toLine - 1\n const sourceStart = lineStart[fromLine]\n const sourceEnd = lineStart[lastLine] + lines[lastLine].length\n const text = lines.slice(fromLine, toLine).join(\"\\n\")\n const sourceHash = hash(text)\n // Content hash is the identity; disambiguate genuinely-identical blocks by occurrence so\n // React keys stay unique without reintroducing position sensitivity for unique content.\n const count = seen.get(sourceHash) ?? 0\n seen.set(sourceHash, count + 1)\n const id = count === 0 ? sourceHash : `${sourceHash}~${count}`\n blocks.push({\n id,\n type,\n sourceStart,\n sourceEnd,\n sourceHash,\n ...(lang ? { lang } : {}),\n })\n }\n\n let i = 0\n while (i < lines.length) {\n const line = lines[i]\n\n if (line.trim() === \"\") {\n i++\n continue\n }\n\n const fence = line.match(FENCE)\n if (fence) {\n let j = i + 1\n while (j < lines.length && !FENCE.test(lines[j])) j++\n const toLine = j < lines.length ? j + 1 : lines.length // include closing fence, or EOF\n const lang = fence[1] || \"\"\n push(lang === \"diff\" ? \"diff\" : \"code\", i, toLine, lang || undefined)\n i = toLine\n continue\n }\n\n if (HEADING.test(line)) {\n push(\"heading\", i, i + 1)\n i++\n continue\n }\n\n if (THEMATIC_BREAK.test(line)) {\n push(\"thematic-break\", i, i + 1)\n i++\n continue\n }\n\n if (QUOTE.test(line)) {\n let j = i\n while (j < lines.length && QUOTE.test(lines[j])) j++\n push(\"quote\", i, j)\n i = j\n continue\n }\n\n if (LIST.test(line)) {\n let j = i\n while (\n j < lines.length &&\n lines[j].trim() !== \"\" &&\n !FENCE.test(lines[j])\n )\n j++\n push(\"list\", i, j)\n i = j\n continue\n }\n\n // Paragraph — consume consecutive non-blank, non-structural lines.\n let j = i\n while (\n j < lines.length &&\n lines[j].trim() !== \"\" &&\n !FENCE.test(lines[j]) &&\n !HEADING.test(lines[j]) &&\n !THEMATIC_BREAK.test(lines[j]) &&\n !QUOTE.test(lines[j]) &&\n !LIST.test(lines[j])\n )\n j++\n push(\"paragraph\", i, j)\n i = j\n }\n\n return blocks\n}\n","import { segment } from \"./segment.js\"\n\nexport type MarkdownBlockType =\n | \"paragraph\"\n | \"heading\"\n | \"code\"\n | \"diff\"\n | \"list\"\n | \"quote\"\n | \"thematic-break\"\n\nexport type MarkdownBlock = {\n id: string\n type: MarkdownBlockType\n sourceStart: number\n sourceEnd: number\n sourceHash: string\n /** Fence info string for `code` / `diff` blocks (e.g. \"ts\", \"diff\"). */\n lang?: string\n}\n\nexport type MarkdownDocument = {\n version: number\n blocks: readonly MarkdownBlock[]\n sourceLength: number\n}\n\nexport const createMarkdownDocument = (\n source: string,\n version = 1,\n): MarkdownDocument => ({\n version,\n blocks: segment(source),\n sourceLength: source.length,\n})\n\n// Reparse after an edit. Block identity is content-based, so unchanged blocks keep their ids\n// automatically — cached layouts downstream stay valid — and this just restamps the version.\nexport const updateMarkdownDocument = (\n previous: MarkdownDocument,\n source: string,\n): MarkdownDocument => createMarkdownDocument(source, previous.version + 1)\n","import stringWidth from \"string-width\"\n\n// Semantic (renderer-independent) styling. No ANSI here — the Ink view maps these to\n// <Text> props, a future OpenTUI view maps them to its own styling, and a path-A composer\n// turns them into ANSI. Colours are resolved theme values (e.g. \"cyan\"), not tokens.\nexport type SpanStyle = {\n color?: string\n backgroundColor?: string\n bold?: boolean\n italic?: boolean\n underline?: boolean\n dim?: boolean\n inverse?: boolean\n}\n\nexport type TextSpan = {\n text: string\n style?: SpanStyle\n}\n\nexport type LayoutLine = {\n id: string\n spans: readonly TextSpan[]\n plainText: string\n displayWidth: number\n}\n\nexport type BlockLayout = {\n blockId: string\n width: number\n height: number\n lines: readonly LayoutLine[]\n}\n\nexport type MarkdownTheme = {\n foreground?: string\n muted: string\n heading: string\n link: string\n inlineCode: string\n quote: string\n added: string\n removed: string\n context: string\n}\n\nexport const defaultTheme: MarkdownTheme = {\n muted: \"gray\",\n heading: \"cyan\",\n link: \"blue\",\n inlineCode: \"yellow\",\n quote: \"gray\",\n added: \"green\",\n removed: \"red\",\n context: \"gray\",\n}\n\n/** Display width in terminal columns (Unicode-aware: wide chars count as 2). */\nexport const measureWidth = (text: string): number => stringWidth(text)\n\n/** Slice a string to at most `max` display columns without splitting a code point. */\nexport const sliceByWidth = (text: string, max: number): string => {\n let used = 0\n let out = \"\"\n for (const ch of text) {\n const cw = stringWidth(ch)\n if (used + cw > max) break\n out += ch\n used += cw\n }\n return out\n}\n\nconst sameStyle = (a?: SpanStyle, b?: SpanStyle): boolean =>\n JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n\n// Append text to a span list, merging into the previous span when the style matches so\n// runs of identical styling stay a single span (fewer nodes for the renderer).\nconst append = (line: TextSpan[], text: string, style?: SpanStyle): void => {\n const last = line[line.length - 1]\n if (last && sameStyle(last.style, style))\n line[line.length - 1] = { text: last.text + text, style }\n else line.push(style ? { text, style } : { text })\n}\n\nconst INLINE =\n /(\\*\\*([^*]+)\\*\\*|`([^`]+)`|\\*([^*]+)\\*|_([^_]+)_|\\[([^\\]]+)\\]\\(([^)]+)\\))/g\n\n// Minimal inline parser → styled spans: bold, italic, inline code, links. Markdown-it can\n// replace this later for fuller GFM inline; this covers the common PR-comment cases.\nexport const parseInline = (text: string, theme: MarkdownTheme): TextSpan[] => {\n const spans: TextSpan[] = []\n const re = new RegExp(INLINE)\n let last = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(text)) !== null) {\n if (m.index > last) spans.push({ text: text.slice(last, m.index) })\n if (m[2] !== undefined) spans.push({ text: m[2], style: { bold: true } })\n else if (m[3] !== undefined)\n spans.push({ text: m[3], style: { color: theme.inlineCode } })\n else if (m[4] !== undefined)\n spans.push({ text: m[4], style: { italic: true } })\n else if (m[5] !== undefined)\n spans.push({ text: m[5], style: { italic: true } })\n else if (m[6] !== undefined)\n spans.push({ text: m[6], style: { color: theme.link, underline: true } })\n last = m.index + m[0].length\n }\n if (last < text.length) spans.push({ text: text.slice(last) })\n return spans.length > 0 ? spans : [{ text }]\n}\n\n// Word-wrap a run of styled spans to `max` display columns, splitting spans at column\n// boundaries and hard-breaking any single token longer than the line.\nexport const wrapSpans = (\n spans: readonly TextSpan[],\n max: number,\n): TextSpan[][] => {\n const width = Math.max(1, max)\n const lines: TextSpan[][] = []\n let line: TextSpan[] = []\n let used = 0\n const flush = () => {\n lines.push(line)\n line = []\n used = 0\n }\n\n for (const span of spans) {\n const tokens = span.text.match(/\\s+|\\S+/g) ?? []\n for (const token of tokens) {\n const w = stringWidth(token)\n if (/^\\s+$/.test(token)) {\n // Keep inter-word whitespace, but drop it at a line start (no leading spaces on wraps).\n if (used > 0 && used + w <= width) {\n append(line, token, span.style)\n used += w\n }\n continue\n }\n if (used + w <= width) {\n append(line, token, span.style)\n used += w\n continue\n }\n if (used > 0) flush()\n if (w <= width) {\n append(line, token, span.style)\n used += w\n } else {\n let rest = token\n while (stringWidth(rest) > width) {\n const chunk = sliceByWidth(rest, width)\n append(line, chunk, span.style)\n flush()\n rest = rest.slice(chunk.length)\n }\n if (rest) {\n append(line, rest, span.style)\n used = stringWidth(rest)\n }\n }\n }\n }\n if (line.length > 0) lines.push(line)\n return lines.length > 0 ? lines : [[]]\n}\n\n/** Truncate a run of spans to `max` display columns (for code / diff — clip, don't wrap). */\nexport const clipSpans = (\n spans: readonly TextSpan[],\n max: number,\n): TextSpan[] => {\n const out: TextSpan[] = []\n let used = 0\n for (const span of spans) {\n if (used >= max) break\n const w = stringWidth(span.text)\n if (used + w <= max) {\n out.push(span)\n used += w\n } else {\n out.push({ text: sliceByWidth(span.text, max - used), style: span.style })\n break\n }\n }\n return out\n}\n","import { hash } from \"./hash.js\"\nimport type { MarkdownBlock, MarkdownDocument } from \"./document.js\"\nimport {\n type BlockLayout,\n type LayoutLine,\n type MarkdownTheme,\n type TextSpan,\n clipSpans,\n defaultTheme,\n measureWidth,\n parseInline,\n wrapSpans,\n} from \"./spans.js\"\n\nconst blockText = (block: MarkdownBlock, source: string): string =>\n source.slice(block.sourceStart, block.sourceEnd)\n\nconst toLine = (\n blockId: string,\n index: number,\n spans: TextSpan[],\n): LayoutLine => {\n const plainText = spans.map((span) => span.text).join(\"\")\n return {\n id: `${blockId}:${index}`,\n spans,\n plainText,\n displayWidth: measureWidth(plainText),\n }\n}\n\nconst stripFences = (text: string): string[] => {\n const lines = text.split(\"\\n\")\n if (lines[0]?.startsWith(\"```\")) lines.shift()\n if (lines.length > 0 && lines[lines.length - 1]?.startsWith(\"```\"))\n lines.pop()\n return lines\n}\n\nconst diffStyle = (line: string, theme: MarkdownTheme) => {\n if (line.startsWith(\"@@\")) return { color: theme.heading }\n if (/^(\\+\\+\\+|---|diff |index )/.test(line)) return { bold: true }\n if (line.startsWith(\"+\")) return { color: theme.added }\n if (line.startsWith(\"-\")) return { color: theme.removed }\n return { color: theme.context }\n}\n\n// One block → its visual terminal lines. Prose wraps; code/diff clip. Pure + deterministic\n// given (block text, width, theme) → safe to cache. Syntax highlighting arrives in M4;\n// code renders plain here.\nconst layoutBlock = (\n block: MarkdownBlock,\n source: string,\n width: number,\n theme: MarkdownTheme,\n): LayoutLine[] => {\n const text = blockText(block, source)\n const rows: TextSpan[][] = []\n\n switch (block.type) {\n case \"heading\": {\n const content = text.replace(/^#{1,6}\\s+/, \"\")\n rows.push(\n ...wrapSpans(\n [{ text: content, style: { color: theme.heading, bold: true } }],\n width,\n ),\n )\n break\n }\n case \"thematic-break\": {\n rows.push([\n { text: \"─\".repeat(Math.max(1, width)), style: { color: theme.muted } },\n ])\n break\n }\n case \"quote\": {\n const content = text.replace(/^>\\s?/gm, \"\").split(\"\\n\").join(\" \")\n const prefix: TextSpan = { text: \"│ \", style: { color: theme.quote } }\n for (const wrapped of wrapSpans(parseInline(content, theme), width - 2))\n rows.push([prefix, ...wrapped])\n break\n }\n case \"list\": {\n for (const raw of text.split(\"\\n\")) {\n const item = raw.match(/^(\\s*)([-*+]|\\d+\\.)\\s+(.*)$/)\n if (!item) continue\n wrapSpans(parseInline(item[3], theme), width - 2).forEach(\n (wrapped, i) => {\n const prefix: TextSpan =\n i === 0\n ? { text: \"• \", style: { color: theme.heading } }\n : { text: \" \" }\n rows.push([prefix, ...wrapped])\n },\n )\n }\n break\n }\n case \"code\": {\n for (const codeLine of stripFences(text))\n rows.push(clipSpans([{ text: codeLine }], width))\n break\n }\n case \"diff\": {\n const body = block.lang === \"diff\" ? stripFences(text) : text.split(\"\\n\")\n for (const diffLine of body)\n rows.push(\n clipSpans(\n [{ text: diffLine, style: diffStyle(diffLine, theme) }],\n width,\n ),\n )\n break\n }\n default: {\n const content = text.split(\"\\n\").join(\" \")\n rows.push(...wrapSpans(parseInline(content, theme), width))\n }\n }\n\n return rows.map((spans, index) => toLine(block.id, index, spans))\n}\n\nexport type LayoutOptions = {\n width: number\n theme?: MarkdownTheme\n themeId?: string\n /** Optional cross-call cache (survives scroll/resize) keyed by block id + width + theme. */\n cache?: Map<string, readonly LayoutLine[]>\n}\n\nexport type MarkdownLayout = {\n width: number\n themeId: string\n blocks: readonly BlockLayout[]\n /** All lines in document order — O(1) viewport slicing. */\n lines: readonly LayoutLine[]\n /** Cumulative first-line index of each block (parallel to blocks). */\n lineOffsets: readonly number[]\n totalLines: number\n reused: number\n computed: number\n}\n\n// Lay out every block for a given width, reusing cached block layouts where possible. Scroll\n// never recomputes; a width change invalidates only width-dependent entries via the key.\nexport const createMarkdownLayout = (\n document: MarkdownDocument,\n source: string,\n options: LayoutOptions,\n): MarkdownLayout => {\n const theme = options.theme ?? defaultTheme\n const themeId =\n options.themeId ?? (options.theme ? hash(JSON.stringify(theme)) : \"default\")\n const width = options.width\n const cache = options.cache\n const blocks: BlockLayout[] = []\n const lines: LayoutLine[] = []\n const lineOffsets: number[] = []\n let reused = 0\n let computed = 0\n\n for (const block of document.blocks) {\n const key = `${block.id}:${width}:${themeId}`\n let blockLines = cache?.get(key)\n if (blockLines) {\n reused++\n } else {\n blockLines = layoutBlock(block, source, width, theme)\n cache?.set(key, blockLines)\n computed++\n }\n lineOffsets.push(lines.length)\n blocks.push({\n blockId: block.id,\n width,\n height: blockLines.length,\n lines: blockLines,\n })\n for (const line of blockLines) lines.push(line)\n }\n\n return {\n width,\n themeId,\n blocks,\n lines,\n lineOffsets,\n totalLines: lines.length,\n reused,\n computed,\n }\n}\n\n/** Visible slice for a viewport — clamped so the last page never scrolls past the end. */\nexport const sliceLines = (\n layout: MarkdownLayout,\n offset: number,\n height: number,\n): readonly LayoutLine[] => {\n const start = Math.max(\n 0,\n Math.min(offset, Math.max(0, layout.totalLines - height)),\n )\n return layout.lines.slice(start, start + height)\n}\n\n/** Index of the first block whose cumulative offset ≤ line — the log-n first-visible lookup. */\nexport const firstBlockAt = (layout: MarkdownLayout, line: number): number => {\n const offsets = layout.lineOffsets\n let lo = 0\n let hi = offsets.length - 1\n let answer = 0\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (offsets[mid] <= line) {\n answer = mid\n lo = mid + 1\n } else {\n hi = mid - 1\n }\n }\n return answer\n}\n","import { useMemo } from \"react\"\nimport { Box, Text } from \"ink\"\nimport {\n createMarkdownDocument,\n createMarkdownLayout,\n sliceLines,\n type LayoutLine,\n type MarkdownDocument,\n type MarkdownLayout,\n type MarkdownTheme,\n} from \"../core/index.js\"\nimport { linesToAnsi } from \"./ansi.js\"\n\nexport type MarkdownViewportProps = {\n /** Raw Markdown source. Ignored when `layout` is provided. */\n source?: string\n /** Pre-parsed document (reused with `source` to skip re-parsing). */\n document?: MarkdownDocument\n /** Pre-computed layout — skips parse + layout entirely. */\n layout?: MarkdownLayout\n width: number\n height: number\n /** First visible document line. Clamped so the last page never scrolls past the end. */\n scrollOffset?: number\n theme?: MarkdownTheme\n /** Layout cache shared across renders (survives scroll/resize). */\n cache?: Map<string, readonly LayoutLine[]>\n}\n\n// The viewport mounts exactly ONE <Text>, whatever the document size: the visible slice is\n// composed into a single pre-rendered ANSI string (render-path A). React/Yoga work stays\n// O(viewport), not O(document) — the whole performance thesis, made concrete.\nexport const MarkdownViewport = ({\n source,\n document,\n layout: providedLayout,\n width,\n height,\n scrollOffset = 0,\n theme,\n cache,\n}: MarkdownViewportProps) => {\n const layout = useMemo(() => {\n if (providedLayout) return providedLayout\n const text = source ?? \"\"\n const doc = document ?? createMarkdownDocument(text)\n return createMarkdownLayout(doc, text, { width, theme, cache })\n }, [providedLayout, document, source, width, theme, cache])\n\n const composed = linesToAnsi(sliceLines(layout, scrollOffset, height))\n\n return (\n <Box width={width} height={height} flexDirection=\"column\">\n <Text wrap=\"truncate\">{composed}</Text>\n </Box>\n )\n}\n","import chalk, { type ChalkInstance } from \"chalk\"\nimport type { LayoutLine, SpanStyle } from \"../core/index.js\"\n\n// Path-A composition lives here: semantic spans → one ANSI string. Chalk auto-detects the\n// terminal's colour support, so this degrades to plain text where colour is unavailable.\nconst chain = (fn: ChalkInstance, key: string): ChalkInstance => {\n const next = (fn as unknown as Record<string, unknown>)[key]\n return typeof next === \"function\" ? (next as ChalkInstance) : fn\n}\n\nconst capitalize = (value: string): string =>\n value.charAt(0).toUpperCase() + value.slice(1)\n\nconst applyStyle = (text: string, style?: SpanStyle): string => {\n if (!style) return text\n let fn: ChalkInstance = chalk\n if (style.color) fn = chain(fn, style.color)\n if (style.backgroundColor)\n fn = chain(fn, `bg${capitalize(style.backgroundColor)}`)\n if (style.bold) fn = chain(fn, \"bold\")\n if (style.italic) fn = chain(fn, \"italic\")\n if (style.underline) fn = chain(fn, \"underline\")\n if (style.dim) fn = chain(fn, \"dim\")\n if (style.inverse) fn = chain(fn, \"inverse\")\n return fn(text)\n}\n\n/** Compose one layout line's styled spans into a single ANSI string. */\nexport const lineToAnsi = (line: LayoutLine): string =>\n line.spans.map((span) => applyStyle(span.text, span.style)).join(\"\")\n\n/** Path-A composition: the visible span-lines → one ANSI string for a single <Text>. */\nexport const linesToAnsi = (lines: readonly LayoutLine[]): string =>\n lines.map(lineToAnsi).join(\"\\n\")\n","import { useInput } from \"ink\"\n\nexport type MarkdownScrollOptions = {\n totalLines: number\n height: number\n offset: number\n onChange: (offset: number) => void\n /** Disable the key handler (e.g. when another pane has focus). */\n active?: boolean\n}\n\nconst clamp = (value: number, max: number): number =>\n Math.max(0, Math.min(value, max))\n\n// Optional keyboard controller — the core owns scrolling *primitives* (offset in/out), not\n// keybindings. Apps that want their own bindings ignore this and drive `scrollOffset` directly.\nexport const useMarkdownScroll = ({\n totalLines,\n height,\n offset,\n onChange,\n active = true,\n}: MarkdownScrollOptions): void => {\n const max = Math.max(0, totalLines - height)\n useInput(\n (input, key) => {\n if (key.downArrow || input === \"j\") onChange(clamp(offset + 1, max))\n else if (key.upArrow || input === \"k\") onChange(clamp(offset - 1, max))\n else if (key.pageDown || input === \" \")\n onChange(clamp(offset + height, max))\n else if (key.pageUp || input === \"b\")\n onChange(clamp(offset - height, max))\n else if (input === \"g\") onChange(0)\n else if (input === \"G\") onChange(max)\n },\n { isActive: active },\n )\n}\n"],"mappings":"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","names":["toLine","j"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/hash.ts","../src/core/segment.ts","../src/core/document.ts","../src/core/spans.ts","../src/core/layout.ts","../src/ink/markdown-viewport.tsx","../src/ink/ansi.ts","../src/ink/use-markdown-scroll.ts"],"sourcesContent":["// FNV-1a — cheap, stable content hash used for block identity and layout-cache keys.\nexport const hash = (input: string): string => {\n let h = 0x811c9dc5\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(36)\n}\n","import { hash } from \"./hash.js\"\nimport type { MarkdownBlock, MarkdownBlockType } from \"./document.js\"\n\nconst FENCE = /^```(\\S*)\\s*$/\nconst HEADING = /^#{1,6}\\s+/\nconst THEMATIC_BREAK = /^(-{3,}|\\*{3,}|_{3,})\\s*$/\nconst QUOTE = /^>\\s?/\nconst LIST = /^(\\s*)([-*+]|\\d+\\.)\\s+/\n\n// Line-scan segmenter → renderer-independent top-level blocks with stable, content-based\n// identity. Minimal GFM-ish block set; inline parsing happens later in the layout stage.\n// An unterminated fence stays a single open block, which is what the streaming tail needs.\nexport const segment = (source: string): MarkdownBlock[] => {\n const lines = source.split(\"\\n\")\n const lineStart: number[] = []\n let offset = 0\n for (const line of lines) {\n lineStart.push(offset)\n offset += line.length + 1 // + newline\n }\n\n const blocks: MarkdownBlock[] = []\n const seen = new Map<string, number>()\n\n const push = (\n type: MarkdownBlockType,\n fromLine: number,\n toLine: number,\n lang?: string,\n ) => {\n const lastLine = toLine - 1\n const sourceStart = lineStart[fromLine]\n const sourceEnd = lineStart[lastLine] + lines[lastLine].length\n const text = lines.slice(fromLine, toLine).join(\"\\n\")\n const sourceHash = hash(text)\n // Content hash is the identity; disambiguate genuinely-identical blocks by occurrence so\n // React keys stay unique without reintroducing position sensitivity for unique content.\n const count = seen.get(sourceHash) ?? 0\n seen.set(sourceHash, count + 1)\n const id = count === 0 ? sourceHash : `${sourceHash}~${count}`\n blocks.push({\n id,\n type,\n sourceStart,\n sourceEnd,\n sourceHash,\n ...(lang ? { lang } : {}),\n })\n }\n\n let i = 0\n while (i < lines.length) {\n const line = lines[i]\n\n if (line.trim() === \"\") {\n i++\n continue\n }\n\n const fence = line.match(FENCE)\n if (fence) {\n let j = i + 1\n while (j < lines.length && !FENCE.test(lines[j])) j++\n const toLine = j < lines.length ? j + 1 : lines.length // include closing fence, or EOF\n const lang = fence[1] || \"\"\n push(lang === \"diff\" ? \"diff\" : \"code\", i, toLine, lang || undefined)\n i = toLine\n continue\n }\n\n if (HEADING.test(line)) {\n push(\"heading\", i, i + 1)\n i++\n continue\n }\n\n if (THEMATIC_BREAK.test(line)) {\n push(\"thematic-break\", i, i + 1)\n i++\n continue\n }\n\n if (QUOTE.test(line)) {\n let j = i\n while (j < lines.length && QUOTE.test(lines[j])) j++\n push(\"quote\", i, j)\n i = j\n continue\n }\n\n if (LIST.test(line)) {\n let j = i\n while (\n j < lines.length &&\n lines[j].trim() !== \"\" &&\n !FENCE.test(lines[j])\n )\n j++\n push(\"list\", i, j)\n i = j\n continue\n }\n\n // Paragraph — consume consecutive non-blank, non-structural lines.\n let j = i\n while (\n j < lines.length &&\n lines[j].trim() !== \"\" &&\n !FENCE.test(lines[j]) &&\n !HEADING.test(lines[j]) &&\n !THEMATIC_BREAK.test(lines[j]) &&\n !QUOTE.test(lines[j]) &&\n !LIST.test(lines[j])\n )\n j++\n push(\"paragraph\", i, j)\n i = j\n }\n\n return blocks\n}\n","import { segment } from \"./segment.js\"\n\nexport type MarkdownBlockType =\n | \"paragraph\"\n | \"heading\"\n | \"code\"\n | \"diff\"\n | \"list\"\n | \"quote\"\n | \"thematic-break\"\n\nexport type MarkdownBlock = {\n id: string\n type: MarkdownBlockType\n sourceStart: number\n sourceEnd: number\n sourceHash: string\n /** Fence info string for `code` / `diff` blocks (e.g. \"ts\", \"diff\"). */\n lang?: string\n}\n\nexport type MarkdownDocument = {\n version: number\n blocks: readonly MarkdownBlock[]\n sourceLength: number\n}\n\nexport const createMarkdownDocument = (\n source: string,\n version = 1,\n): MarkdownDocument => ({\n version,\n blocks: segment(source),\n sourceLength: source.length,\n})\n\n// Reparse after an edit. Block identity is content-based, so unchanged blocks keep their ids\n// automatically — cached layouts downstream stay valid — and this just restamps the version.\nexport const updateMarkdownDocument = (\n previous: MarkdownDocument,\n source: string,\n): MarkdownDocument => createMarkdownDocument(source, previous.version + 1)\n","import stringWidth from \"string-width\"\n\n// Semantic (renderer-independent) styling. No ANSI here — the Ink view maps these to\n// <Text> props, a future OpenTUI view maps them to its own styling, and a path-A composer\n// turns them into ANSI. Colours are resolved theme values (e.g. \"cyan\"), not tokens.\nexport type SpanStyle = {\n color?: string\n backgroundColor?: string\n bold?: boolean\n italic?: boolean\n underline?: boolean\n dim?: boolean\n inverse?: boolean\n}\n\nexport type TextSpan = {\n text: string\n style?: SpanStyle\n}\n\nexport type LayoutLine = {\n id: string\n spans: readonly TextSpan[]\n plainText: string\n displayWidth: number\n}\n\nexport type BlockLayout = {\n blockId: string\n width: number\n height: number\n lines: readonly LayoutLine[]\n}\n\nexport type MarkdownTheme = {\n foreground?: string\n muted: string\n heading: string\n link: string\n inlineCode: string\n quote: string\n added: string\n removed: string\n context: string\n}\n\nexport const defaultTheme: MarkdownTheme = {\n muted: \"gray\",\n heading: \"cyan\",\n link: \"blue\",\n inlineCode: \"yellow\",\n quote: \"gray\",\n added: \"green\",\n removed: \"red\",\n context: \"gray\",\n}\n\n/** Display width in terminal columns (Unicode-aware: wide chars count as 2). */\nexport const measureWidth = (text: string): number => stringWidth(text)\n\n/** Slice a string to at most `max` display columns without splitting a code point. */\nexport const sliceByWidth = (text: string, max: number): string => {\n let used = 0\n let out = \"\"\n for (const ch of text) {\n const cw = stringWidth(ch)\n if (used + cw > max) break\n out += ch\n used += cw\n }\n return out\n}\n\nconst sameStyle = (a?: SpanStyle, b?: SpanStyle): boolean =>\n JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n\n// Append text to a span list, merging into the previous span when the style matches so\n// runs of identical styling stay a single span (fewer nodes for the renderer).\nconst append = (line: TextSpan[], text: string, style?: SpanStyle): void => {\n const last = line[line.length - 1]\n if (last && sameStyle(last.style, style))\n line[line.length - 1] = { text: last.text + text, style }\n else line.push(style ? { text, style } : { text })\n}\n\n// A bare URL is the last alternative, so a `[text](url)` link wins over the URL\n// inside it. Trailing punctuation is left out of the match: `see https://x.y.`\n// links `https://x.y`, and a URL wrapped in parentheses keeps its closer.\nconst INLINE =\n /(\\*\\*([^*]+)\\*\\*|`([^`]+)`|\\*([^*]+)\\*|_([^_]+)_|\\[([^\\]]+)\\]\\(([^)]+)\\)|(https?:\\/\\/[^\\s<>()]+?)(?=[.,;:!?]*(?:\\s|$|\\))))/g\n\n// Minimal inline parser → styled spans: bold, italic, inline code, links,\n// bare URLs. Markdown-it can replace this later for fuller GFM inline; this\n// covers the common PR-comment cases. Bare URLs are styled as links because a\n// pasted `PR: https://…` is usually the one actionable thing in a comment, and\n// it was the only thing rendered plain.\nexport const parseInline = (text: string, theme: MarkdownTheme): TextSpan[] => {\n const spans: TextSpan[] = []\n const re = new RegExp(INLINE)\n const link = { color: theme.link, underline: true }\n let last = 0\n let m: RegExpExecArray | null\n while ((m = re.exec(text)) !== null) {\n if (m.index > last) spans.push({ text: text.slice(last, m.index) })\n if (m[2] !== undefined) spans.push({ text: m[2], style: { bold: true } })\n else if (m[3] !== undefined)\n spans.push({ text: m[3], style: { color: theme.inlineCode } })\n else if (m[4] !== undefined)\n spans.push({ text: m[4], style: { italic: true } })\n else if (m[5] !== undefined)\n spans.push({ text: m[5], style: { italic: true } })\n else if (m[6] !== undefined) spans.push({ text: m[6], style: link })\n else if (m[8] !== undefined) spans.push({ text: m[8], style: link })\n last = m.index + m[0].length\n }\n if (last < text.length) spans.push({ text: text.slice(last) })\n return spans.length > 0 ? spans : [{ text }]\n}\n\n// Word-wrap a run of styled spans to `max` display columns, splitting spans at column\n// boundaries and hard-breaking any single token longer than the line.\nexport const wrapSpans = (\n spans: readonly TextSpan[],\n max: number,\n): TextSpan[][] => {\n const width = Math.max(1, max)\n const lines: TextSpan[][] = []\n let line: TextSpan[] = []\n let used = 0\n const flush = () => {\n lines.push(line)\n line = []\n used = 0\n }\n\n for (const span of spans) {\n const tokens = span.text.match(/\\s+|\\S+/g) ?? []\n for (const token of tokens) {\n const w = stringWidth(token)\n if (/^\\s+$/.test(token)) {\n // Keep inter-word whitespace, but drop it at a line start (no leading spaces on wraps).\n if (used > 0 && used + w <= width) {\n append(line, token, span.style)\n used += w\n }\n continue\n }\n if (used + w <= width) {\n append(line, token, span.style)\n used += w\n continue\n }\n if (used > 0) flush()\n if (w <= width) {\n append(line, token, span.style)\n used += w\n } else {\n let rest = token\n while (stringWidth(rest) > width) {\n const chunk = sliceByWidth(rest, width)\n append(line, chunk, span.style)\n flush()\n rest = rest.slice(chunk.length)\n }\n if (rest) {\n append(line, rest, span.style)\n used = stringWidth(rest)\n }\n }\n }\n }\n if (line.length > 0) lines.push(line)\n return lines.length > 0 ? lines : [[]]\n}\n\n/** Truncate a run of spans to `max` display columns (for code / diff — clip, don't wrap). */\nexport const clipSpans = (\n spans: readonly TextSpan[],\n max: number,\n): TextSpan[] => {\n const out: TextSpan[] = []\n let used = 0\n for (const span of spans) {\n if (used >= max) break\n const w = stringWidth(span.text)\n if (used + w <= max) {\n out.push(span)\n used += w\n } else {\n out.push({ text: sliceByWidth(span.text, max - used), style: span.style })\n break\n }\n }\n return out\n}\n","import { hash } from \"./hash.js\"\nimport type { MarkdownBlock, MarkdownDocument } from \"./document.js\"\nimport {\n type BlockLayout,\n type LayoutLine,\n type MarkdownTheme,\n type TextSpan,\n clipSpans,\n defaultTheme,\n measureWidth,\n parseInline,\n wrapSpans,\n} from \"./spans.js\"\n\nconst blockText = (block: MarkdownBlock, source: string): string =>\n source.slice(block.sourceStart, block.sourceEnd)\n\nconst toLine = (\n blockId: string,\n index: number,\n spans: TextSpan[],\n): LayoutLine => {\n const plainText = spans.map((span) => span.text).join(\"\")\n return {\n id: `${blockId}:${index}`,\n spans,\n plainText,\n displayWidth: measureWidth(plainText),\n }\n}\n\nconst stripFences = (text: string): string[] => {\n const lines = text.split(\"\\n\")\n if (lines[0]?.startsWith(\"```\")) lines.shift()\n if (lines.length > 0 && lines[lines.length - 1]?.startsWith(\"```\"))\n lines.pop()\n return lines\n}\n\nconst diffStyle = (line: string, theme: MarkdownTheme) => {\n if (line.startsWith(\"@@\")) return { color: theme.heading }\n if (/^(\\+\\+\\+|---|diff |index )/.test(line)) return { bold: true }\n if (line.startsWith(\"+\")) return { color: theme.added }\n if (line.startsWith(\"-\")) return { color: theme.removed }\n return { color: theme.context }\n}\n\n// One block → its visual terminal lines. Prose wraps; code/diff clip. Pure + deterministic\n// given (block text, width, theme) → safe to cache. Syntax highlighting arrives in M4;\n// code renders plain here.\nconst layoutBlock = (\n block: MarkdownBlock,\n source: string,\n width: number,\n theme: MarkdownTheme,\n): LayoutLine[] => {\n const text = blockText(block, source)\n const rows: TextSpan[][] = []\n\n switch (block.type) {\n case \"heading\": {\n const content = text.replace(/^#{1,6}\\s+/, \"\")\n rows.push(\n ...wrapSpans(\n [{ text: content, style: { color: theme.heading, bold: true } }],\n width,\n ),\n )\n break\n }\n case \"thematic-break\": {\n rows.push([\n { text: \"─\".repeat(Math.max(1, width)), style: { color: theme.muted } },\n ])\n break\n }\n case \"quote\": {\n const content = text.replace(/^>\\s?/gm, \"\").split(\"\\n\").join(\" \")\n const prefix: TextSpan = { text: \"│ \", style: { color: theme.quote } }\n for (const wrapped of wrapSpans(parseInline(content, theme), width - 2))\n rows.push([prefix, ...wrapped])\n break\n }\n case \"list\": {\n for (const raw of text.split(\"\\n\")) {\n const item = raw.match(/^(\\s*)([-*+]|\\d+\\.)\\s+(.*)$/)\n if (!item) continue\n wrapSpans(parseInline(item[3], theme), width - 2).forEach(\n (wrapped, i) => {\n const prefix: TextSpan =\n i === 0\n ? { text: \"• \", style: { color: theme.heading } }\n : { text: \" \" }\n rows.push([prefix, ...wrapped])\n },\n )\n }\n break\n }\n case \"code\": {\n for (const codeLine of stripFences(text))\n rows.push(clipSpans([{ text: codeLine }], width))\n break\n }\n case \"diff\": {\n const body = block.lang === \"diff\" ? stripFences(text) : text.split(\"\\n\")\n for (const diffLine of body)\n rows.push(\n clipSpans(\n [{ text: diffLine, style: diffStyle(diffLine, theme) }],\n width,\n ),\n )\n break\n }\n default: {\n const content = text.split(\"\\n\").join(\" \")\n rows.push(...wrapSpans(parseInline(content, theme), width))\n }\n }\n\n return rows.map((spans, index) => toLine(block.id, index, spans))\n}\n\nexport type LayoutOptions = {\n width: number\n theme?: MarkdownTheme\n themeId?: string\n /** Optional cross-call cache (survives scroll/resize) keyed by block id + width + theme. */\n cache?: Map<string, readonly LayoutLine[]>\n}\n\nexport type MarkdownLayout = {\n width: number\n themeId: string\n blocks: readonly BlockLayout[]\n /** All lines in document order — O(1) viewport slicing. */\n lines: readonly LayoutLine[]\n /** Cumulative first-line index of each block (parallel to blocks). */\n lineOffsets: readonly number[]\n totalLines: number\n reused: number\n computed: number\n}\n\n// Lay out every block for a given width, reusing cached block layouts where possible. Scroll\n// never recomputes; a width change invalidates only width-dependent entries via the key.\nexport const createMarkdownLayout = (\n document: MarkdownDocument,\n source: string,\n options: LayoutOptions,\n): MarkdownLayout => {\n const theme = options.theme ?? defaultTheme\n const themeId =\n options.themeId ?? (options.theme ? hash(JSON.stringify(theme)) : \"default\")\n const width = options.width\n const cache = options.cache\n const blocks: BlockLayout[] = []\n const lines: LayoutLine[] = []\n const lineOffsets: number[] = []\n let reused = 0\n let computed = 0\n\n // One blank line between any two adjacent blocks, none before the first or\n // after the last. Every Markdown renderer separates blocks with vertical\n // space; without it a bold-paragraph \"heading\" runs straight into the list\n // beneath it and the eye has to parse where one thought ends. A list is one\n // block, so its items stay tight; a fence is one block, so its lines do; a\n // rule gets air on both sides. Pushed HERE rather than inside layoutBlock so\n // the per-block cache stays a function of the block alone, and `blocks[i]`\n // still reports its own height — the gap belongs to the document, not to\n // either neighbour. `lineOffsets` count it, so viewport math stays honest.\n document.blocks.forEach((block, i) => {\n if (i > 0) lines.push(toLine(block.id, -1, []))\n const key = `${block.id}:${width}:${themeId}`\n let blockLines = cache?.get(key)\n if (blockLines) {\n reused++\n } else {\n blockLines = layoutBlock(block, source, width, theme)\n cache?.set(key, blockLines)\n computed++\n }\n lineOffsets.push(lines.length)\n blocks.push({\n blockId: block.id,\n width,\n height: blockLines.length,\n lines: blockLines,\n })\n for (const line of blockLines) lines.push(line)\n })\n\n return {\n width,\n themeId,\n blocks,\n lines,\n lineOffsets,\n totalLines: lines.length,\n reused,\n computed,\n }\n}\n\n/** Visible slice for a viewport — clamped so the last page never scrolls past the end. */\nexport const sliceLines = (\n layout: MarkdownLayout,\n offset: number,\n height: number,\n): readonly LayoutLine[] => {\n const start = Math.max(\n 0,\n Math.min(offset, Math.max(0, layout.totalLines - height)),\n )\n return layout.lines.slice(start, start + height)\n}\n\n/** Index of the first block whose cumulative offset ≤ line — the log-n first-visible lookup. */\nexport const firstBlockAt = (layout: MarkdownLayout, line: number): number => {\n const offsets = layout.lineOffsets\n let lo = 0\n let hi = offsets.length - 1\n let answer = 0\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (offsets[mid] <= line) {\n answer = mid\n lo = mid + 1\n } else {\n hi = mid - 1\n }\n }\n return answer\n}\n","import { useMemo } from \"react\"\nimport { Box, Text } from \"ink\"\nimport {\n createMarkdownDocument,\n createMarkdownLayout,\n sliceLines,\n type LayoutLine,\n type MarkdownDocument,\n type MarkdownLayout,\n type MarkdownTheme,\n} from \"../core/index.js\"\nimport { linesToAnsi } from \"./ansi.js\"\n\nexport type MarkdownViewportProps = {\n /** Raw Markdown source. Ignored when `layout` is provided. */\n source?: string\n /** Pre-parsed document (reused with `source` to skip re-parsing). */\n document?: MarkdownDocument\n /** Pre-computed layout — skips parse + layout entirely. */\n layout?: MarkdownLayout\n width: number\n height: number\n /** First visible document line. Clamped so the last page never scrolls past the end. */\n scrollOffset?: number\n theme?: MarkdownTheme\n /** Layout cache shared across renders (survives scroll/resize). */\n cache?: Map<string, readonly LayoutLine[]>\n}\n\n// The viewport mounts exactly ONE <Text>, whatever the document size: the visible slice is\n// composed into a single pre-rendered ANSI string (render-path A). React/Yoga work stays\n// O(viewport), not O(document) — the whole performance thesis, made concrete.\nexport const MarkdownViewport = ({\n source,\n document,\n layout: providedLayout,\n width,\n height,\n scrollOffset = 0,\n theme,\n cache,\n}: MarkdownViewportProps) => {\n const layout = useMemo(() => {\n if (providedLayout) return providedLayout\n const text = source ?? \"\"\n const doc = document ?? createMarkdownDocument(text)\n return createMarkdownLayout(doc, text, { width, theme, cache })\n }, [providedLayout, document, source, width, theme, cache])\n\n const composed = linesToAnsi(sliceLines(layout, scrollOffset, height))\n\n return (\n <Box width={width} height={height} flexDirection=\"column\">\n <Text wrap=\"truncate\">{composed}</Text>\n </Box>\n )\n}\n","import chalk, { type ChalkInstance } from \"chalk\"\nimport type { LayoutLine, SpanStyle } from \"../core/index.js\"\n\n// Path-A composition lives here: semantic spans → one ANSI string. Chalk auto-detects the\n// terminal's colour support, so this degrades to plain text where colour is unavailable.\nconst chain = (fn: ChalkInstance, key: string): ChalkInstance => {\n const next = (fn as unknown as Record<string, unknown>)[key]\n return typeof next === \"function\" ? (next as ChalkInstance) : fn\n}\n\nconst capitalize = (value: string): string =>\n value.charAt(0).toUpperCase() + value.slice(1)\n\nconst applyStyle = (text: string, style?: SpanStyle): string => {\n if (!style) return text\n let fn: ChalkInstance = chalk\n if (style.color) fn = chain(fn, style.color)\n if (style.backgroundColor)\n fn = chain(fn, `bg${capitalize(style.backgroundColor)}`)\n if (style.bold) fn = chain(fn, \"bold\")\n if (style.italic) fn = chain(fn, \"italic\")\n if (style.underline) fn = chain(fn, \"underline\")\n if (style.dim) fn = chain(fn, \"dim\")\n if (style.inverse) fn = chain(fn, \"inverse\")\n return fn(text)\n}\n\n/** Compose one layout line's styled spans into a single ANSI string. */\nexport const lineToAnsi = (line: LayoutLine): string =>\n line.spans.map((span) => applyStyle(span.text, span.style)).join(\"\")\n\n/** Path-A composition: the visible span-lines → one ANSI string for a single <Text>. */\nexport const linesToAnsi = (lines: readonly LayoutLine[]): string =>\n lines.map(lineToAnsi).join(\"\\n\")\n","import { useInput } from \"ink\"\n\nexport type MarkdownScrollOptions = {\n totalLines: number\n height: number\n offset: number\n onChange: (offset: number) => void\n /** Disable the key handler (e.g. when another pane has focus). */\n active?: boolean\n}\n\nconst clamp = (value: number, max: number): number =>\n Math.max(0, Math.min(value, max))\n\n// Optional keyboard controller — the core owns scrolling *primitives* (offset in/out), not\n// keybindings. Apps that want their own bindings ignore this and drive `scrollOffset` directly.\nexport const useMarkdownScroll = ({\n totalLines,\n height,\n offset,\n onChange,\n active = true,\n}: MarkdownScrollOptions): void => {\n const max = Math.max(0, totalLines - height)\n useInput(\n (input, key) => {\n if (key.downArrow || input === \"j\") onChange(clamp(offset + 1, max))\n else if (key.upArrow || input === \"k\") onChange(clamp(offset - 1, max))\n else if (key.pageDown || input === \" \")\n onChange(clamp(offset + height, max))\n else if (key.pageUp || input === \"b\")\n onChange(clamp(offset - height, max))\n else if (input === \"g\") onChange(0)\n else if (input === \"G\") onChange(max)\n },\n { isActive: active },\n )\n}\n"],"mappings":"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","names":["toLine","j"]}
|