@kud/ink-markdown 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -21,6 +21,7 @@ Ink apps that display large, code-heavy, streaming documents (AI responses, code
21
21
  - **Block-based incremental parsing** ✅ — Markdown is segmented into top-level blocks with stable IDs and source hashes, so an edit in one block leaves the others untouched (`updateMarkdownDocument`).
22
22
  - **Cached, width-aware layout** ✅ — wrapping, Unicode width and styling are computed once per `(block, width, theme)` and reused until one of those changes, so a terminal resize doesn't force a full relayout.
23
23
  - **Unified-diff rendering** ✅ — hunk headers, additions and deletions are styled from the theme rather than treated as flat text.
24
+ - **Pipe tables** ✅ — columns padded to their widest cell and joined by two spaces, a `─` rule segment per column under a bold header, `---:` right-aligns. Always one line per row: when the table is wider than the viewport the widest column shrinks first and cells end in `…`, floored at the header width, so no column ever silently disappears.
24
25
  - **Syntax highlighting for fenced code** ⏳ — planned for M4; code blocks currently render unhighlighted, clipped rather than wrapped.
25
26
  - **Streaming with a mutable tail** ⏳ — the immutable-completed-blocks design is in place, but the `useMarkdownStream` hook that drives it is not written yet.
26
27
  - **Overridable block renderers and perf instrumentation** ⏳ — designed in [`prd.md`](./prd.md), not yet built.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
 
3
- type MarkdownBlockType = "paragraph" | "heading" | "code" | "diff" | "list" | "quote" | "thematic-break";
3
+ type MarkdownBlockType = "paragraph" | "heading" | "code" | "diff" | "list" | "quote" | "thematic-break" | "table";
4
4
  type MarkdownBlock = {
5
5
  id: string;
6
6
  type: MarkdownBlockType;
@@ -93,6 +93,8 @@ declare const sliceLines: (layout: MarkdownLayout, offset: number, height: numbe
93
93
  /** Index of the first block whose cumulative offset ≤ line — the log-n first-visible lookup. */
94
94
  declare const firstBlockAt: (layout: MarkdownLayout, line: number) => number;
95
95
 
96
+ declare const layoutTable: (text: string, width: number, theme: MarkdownTheme) => TextSpan[][];
97
+
96
98
  type MarkdownViewportProps = {
97
99
  /** Raw Markdown source. Ignored when `layout` is provided. */
98
100
  source?: string;
@@ -125,4 +127,4 @@ declare const lineToAnsi: (line: LayoutLine) => string;
125
127
  /** Path-A composition: the visible span-lines → one ANSI string for a single <Text>. */
126
128
  declare const linesToAnsi: (lines: readonly LayoutLine[]) => string;
127
129
 
128
- export { type BlockLayout, type LayoutLine, type LayoutOptions, type MarkdownBlock, type MarkdownBlockType, type MarkdownDocument, type MarkdownLayout, type MarkdownScrollOptions, type MarkdownTheme, MarkdownViewport, type MarkdownViewportProps, type SpanStyle, type TextSpan, clipSpans, createMarkdownDocument, createMarkdownLayout, defaultTheme, firstBlockAt, hash, lineToAnsi, linesToAnsi, measureWidth, parseInline, segment, sliceByWidth, sliceLines, updateMarkdownDocument, useMarkdownScroll, wrapSpans };
130
+ export { type BlockLayout, type LayoutLine, type LayoutOptions, type MarkdownBlock, type MarkdownBlockType, type MarkdownDocument, type MarkdownLayout, type MarkdownScrollOptions, type MarkdownTheme, MarkdownViewport, type MarkdownViewportProps, type SpanStyle, type TextSpan, clipSpans, createMarkdownDocument, createMarkdownLayout, defaultTheme, firstBlockAt, hash, layoutTable, lineToAnsi, linesToAnsi, measureWidth, parseInline, segment, sliceByWidth, sliceLines, updateMarkdownDocument, useMarkdownScroll, wrapSpans };
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var HEADING = /^#{1,6}\s+/;
14
14
  var THEMATIC_BREAK = /^(-{3,}|\*{3,}|_{3,})\s*$/;
15
15
  var QUOTE = /^>\s?/;
16
16
  var LIST = /^(\s*)([-*+]|\d+\.)\s+/;
17
+ var TABLE_ROW = /^\s*\|.*\|\s*$/;
17
18
  var segment = (source) => {
18
19
  const lines = source.split("\n");
19
20
  const lineStart = [];
@@ -84,8 +85,15 @@ var segment = (source) => {
84
85
  i = j2;
85
86
  continue;
86
87
  }
88
+ if (TABLE_ROW.test(line) && i + 1 < lines.length && TABLE_ROW.test(lines[i + 1])) {
89
+ let j2 = i;
90
+ while (j2 < lines.length && TABLE_ROW.test(lines[j2])) j2++;
91
+ push("table", i, j2);
92
+ i = j2;
93
+ continue;
94
+ }
87
95
  let j = i;
88
- while (j < lines.length && lines[j].trim() !== "" && !FENCE.test(lines[j]) && !HEADING.test(lines[j]) && !THEMATIC_BREAK.test(lines[j]) && !QUOTE.test(lines[j]) && !LIST.test(lines[j]))
96
+ while (j < lines.length && lines[j].trim() !== "" && !FENCE.test(lines[j]) && !HEADING.test(lines[j]) && !THEMATIC_BREAK.test(lines[j]) && !QUOTE.test(lines[j]) && !LIST.test(lines[j]) && !(TABLE_ROW.test(lines[j]) && j + 1 < lines.length && TABLE_ROW.test(lines[j + 1])))
89
97
  j++;
90
98
  push("paragraph", i, j);
91
99
  i = j;
@@ -132,7 +140,7 @@ var append = (line, text, style) => {
132
140
  line[line.length - 1] = { text: last.text + text, style };
133
141
  else line.push(style ? { text, style } : { text });
134
142
  };
135
- var INLINE = /(\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*|_([^_]+)_|\[([^\]]+)\]\(([^)]+)\)|(https?:\/\/[^\s<>()]+?)(?=[.,;:!?]*(?:\s|$|\))))/g;
143
+ var INLINE = /(\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*|_([^_]+)_|\[((?:[^\[\]]|\[[^\[\]]*\])+)\]\(([^)]+)\)|(https?:\/\/[^\s<>()]+?)(?=[.,;:!?]*(?:\s|$|\))))/g;
136
144
  var parseInline = (text, theme) => {
137
145
  const spans = [];
138
146
  const re = new RegExp(INLINE);
@@ -220,6 +228,91 @@ var clipSpans = (spans, max) => {
220
228
  return out;
221
229
  };
222
230
 
231
+ // src/core/table.ts
232
+ var GUTTER = " ";
233
+ var splitRow = (line) => {
234
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
235
+ return trimmed.split("|").map((c) => c.trim());
236
+ };
237
+ var SEPARATOR = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
238
+ var alignOf = (cell) => /^-+:$/.test(cell.trim()) ? "right" : "left";
239
+ var spanWidth = (spans) => spans.reduce((n, s) => n + measureWidth(s.text), 0);
240
+ var truncateSpans = (spans, max) => {
241
+ if (spanWidth(spans) <= max) return [...spans];
242
+ if (max <= 1) return [{ text: max === 1 ? "\u2026" : "" }];
243
+ const kept = clipSpans(spans, max - 1);
244
+ const last = kept[kept.length - 1];
245
+ if (last) kept[kept.length - 1] = { ...last, text: last.text + "\u2026" };
246
+ else kept.push({ text: "\u2026" });
247
+ return kept;
248
+ };
249
+ var pad = (spans, width, align) => {
250
+ const gap = width - spanWidth(spans);
251
+ if (gap <= 0) return spans;
252
+ const fill = { text: " ".repeat(gap) };
253
+ return align === "right" ? [fill, ...spans] : [...spans, fill];
254
+ };
255
+ var layoutTable = (text, width, theme) => {
256
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
257
+ const separatorAt = lines.findIndex((l, i) => i > 0 && SEPARATOR.test(l));
258
+ const header = separatorAt > 0 ? splitRow(lines[0]) : null;
259
+ const aligns = separatorAt > 0 ? splitRow(lines[separatorAt]).map(alignOf) : [];
260
+ const bodyLines = lines.filter(
261
+ (_, i) => i !== separatorAt && !(header && i === 0)
262
+ );
263
+ const body = bodyLines.map(splitRow);
264
+ const columns = Math.max(header?.length ?? 0, ...body.map((r) => r.length));
265
+ if (columns === 0) return [];
266
+ const cellSpans = (cells, style) => Array.from({ length: columns }, (_, c) => {
267
+ const raw = cells[c] ?? "";
268
+ const spans = parseInline(raw, theme);
269
+ return style ? spans.map((s) => ({ ...s, style: { ...s.style, ...style } })) : spans;
270
+ });
271
+ const headerSpans = header ? cellSpans(header, { color: theme.heading, bold: true }) : null;
272
+ const rowSpans = body.map((cells) => cellSpans(cells));
273
+ const allRows = headerSpans ? [headerSpans, ...rowSpans] : rowSpans;
274
+ const natural = Array.from(
275
+ { length: columns },
276
+ (_, c) => Math.max(1, ...allRows.map((r) => spanWidth(r[c])))
277
+ );
278
+ const floors = Array.from(
279
+ { length: columns },
280
+ (_, c) => Math.max(1, headerSpans ? spanWidth(headerSpans[c]) : 1)
281
+ );
282
+ const widths = [...natural];
283
+ const total = () => widths.reduce((n, w) => n + w, 0) + GUTTER.length * (columns - 1);
284
+ while (total() > width) {
285
+ let widest = -1;
286
+ for (let c = 0; c < columns; c++)
287
+ if (widths[c] > floors[c] && (widest < 0 || widths[c] > widths[widest]))
288
+ widest = c;
289
+ if (widest < 0) break;
290
+ widths[widest] -= 1;
291
+ }
292
+ const rowLine = (cells) => {
293
+ const out = [];
294
+ cells.forEach((spans, c) => {
295
+ if (c > 0) out.push({ text: GUTTER });
296
+ out.push(
297
+ ...pad(truncateSpans(spans, widths[c]), widths[c], aligns[c] ?? "left")
298
+ );
299
+ });
300
+ return clipSpans(out, width);
301
+ };
302
+ const rows = [];
303
+ if (headerSpans) {
304
+ rows.push(rowLine(headerSpans));
305
+ const rule = [];
306
+ widths.forEach((w, c) => {
307
+ if (c > 0) rule.push({ text: GUTTER });
308
+ rule.push({ text: "\u2500".repeat(w), style: { color: theme.muted } });
309
+ });
310
+ rows.push(clipSpans(rule, width));
311
+ }
312
+ for (const r of rowSpans) rows.push(rowLine(r));
313
+ return rows;
314
+ };
315
+
223
316
  // src/core/layout.ts
224
317
  var blockText = (block, source) => source.slice(block.sourceStart, block.sourceEnd);
225
318
  var toLine = (blockId, index, spans) => {
@@ -301,6 +394,10 @@ var layoutBlock = (block, source, width, theme) => {
301
394
  );
302
395
  break;
303
396
  }
397
+ case "table": {
398
+ rows.push(...layoutTable(text, width, theme));
399
+ break;
400
+ }
304
401
  default: {
305
402
  const content = text.split("\n").join(" ");
306
403
  rows.push(...wrapSpans(parseInline(content, theme), width));
@@ -384,12 +481,12 @@ var chain = (fn, key) => {
384
481
  return typeof next === "function" ? next : fn;
385
482
  };
386
483
  var capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
484
+ var colour = (fn, value, bg) => value.startsWith("#") ? bg ? fn.bgHex(value) : fn.hex(value) : chain(fn, bg ? `bg${capitalize(value)}` : value);
387
485
  var applyStyle = (text, style) => {
388
486
  if (!style) return text;
389
487
  let fn = chalk;
390
- if (style.color) fn = chain(fn, style.color);
391
- if (style.backgroundColor)
392
- fn = chain(fn, `bg${capitalize(style.backgroundColor)}`);
488
+ if (style.color) fn = colour(fn, style.color, false);
489
+ if (style.backgroundColor) fn = colour(fn, style.backgroundColor, true);
393
490
  if (style.bold) fn = chain(fn, "bold");
394
491
  if (style.italic) fn = chain(fn, "italic");
395
492
  if (style.underline) fn = chain(fn, "underline");
@@ -455,6 +552,7 @@ export {
455
552
  defaultTheme,
456
553
  firstBlockAt,
457
554
  hash,
555
+ layoutTable,
458
556
  lineToAnsi,
459
557
  linesToAnsi,
460
558
  measureWidth,
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\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"]}
1
+ {"version":3,"sources":["../src/core/hash.ts","../src/core/segment.ts","../src/core/document.ts","../src/core/spans.ts","../src/core/table.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// A pipe-table row: starts and ends with a pipe. Two such lines in a row are\n// a table; one on its own is prose that happens to start with a bar.\nconst TABLE_ROW = /^\\s*\\|.*\\|\\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 if (TABLE_ROW.test(line) && i + 1 < lines.length && TABLE_ROW.test(lines[i + 1])) {\n let j = i\n while (j < lines.length && TABLE_ROW.test(lines[j])) j++\n push(\"table\", 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 !(TABLE_ROW.test(lines[j]) && j + 1 < lines.length && TABLE_ROW.test(lines[j + 1]))\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 | \"table\"\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 {\n type MarkdownTheme,\n type TextSpan,\n clipSpans,\n measureWidth,\n parseInline,\n} from \"./spans.js\"\n\n// A pipe table, laid out as a grid the eye can travel DOWN: one line per row,\n// always. Columns are padded to their widest cell and joined by two spaces\n// with no vertical rule — width is the scarce resource, and a four-column\n// table spends nine cells on ` │ ` against six on gaps for the same\n// information. The column structure is drawn once instead, as per-column `─`\n// segments under the header in `theme.muted`, the thematic break's glyph and\n// token; the padding carries the boundaries down from there. The header takes\n// `theme.heading` bold, exactly as a heading block does, so it reads as the\n// same class of thing. Not underlined: underline is what a link wears, and a\n// header that looks clickable is a lie.\n//\n// Overflow shrinks and truncates rather than wrapping or clipping. A row two\n// lines tall is a list wearing a grid; clipping at the right edge loses the\n// rightmost columns, which in a ticket table are the short informative ones\n// (points, status) while sparing the long prose. So while the row overflows,\n// one column at a time comes off whichever is currently widest, floored at\n// its header's width, and cells past their column end in `…`. If every\n// column is at its floor and it still does not fit, the row clips like a code\n// line — never a hidden column. A silently missing column is a lie; a visibly\n// truncated one is honest.\n//\n// Empty cells stay blank. The ADF converter already emits `—` for \"none\", so\n// inventing a dash here would collide with a real value. A leading empty\n// header cell is a row-label column: blank header, but still a `─` segment,\n// because the segment marks the column and is the one thing that says \"this\n// is a column\" when the header cannot.\n\ntype Align = \"left\" | \"right\"\n\nconst GUTTER = \" \"\n\nconst splitRow = (line: string): string[] => {\n const trimmed = line.trim().replace(/^\\|/, \"\").replace(/\\|$/, \"\")\n return trimmed.split(\"|\").map((c) => c.trim())\n}\n\nconst SEPARATOR = /^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)*\\|?\\s*$/\n\nconst alignOf = (cell: string): Align =>\n /^-+:$/.test(cell.trim()) ? \"right\" : \"left\"\n\nconst spanWidth = (spans: readonly TextSpan[]): number =>\n spans.reduce((n, s) => n + measureWidth(s.text), 0)\n\n// Truncate a cell's spans to `max` columns, ending in `…` in the last span's\n// style when anything was lost. The ellipsis inherits rather than taking a\n// colour of its own — a clipped link stays a link to the end.\nconst truncateSpans = (spans: readonly TextSpan[], max: number): TextSpan[] => {\n if (spanWidth(spans) <= max) return [...spans]\n if (max <= 1) return [{ text: max === 1 ? \"…\" : \"\" }]\n const kept = clipSpans(spans, max - 1)\n const last = kept[kept.length - 1]\n if (last) kept[kept.length - 1] = { ...last, text: last.text + \"…\" }\n else kept.push({ text: \"…\" })\n return kept\n}\n\nconst pad = (spans: TextSpan[], width: number, align: Align): TextSpan[] => {\n const gap = width - spanWidth(spans)\n if (gap <= 0) return spans\n const fill = { text: \" \".repeat(gap) }\n return align === \"right\" ? [fill, ...spans] : [...spans, fill]\n}\n\nexport const layoutTable = (\n text: string,\n width: number,\n theme: MarkdownTheme,\n): TextSpan[][] => {\n const lines = text.split(\"\\n\").filter((l) => l.trim() !== \"\")\n const separatorAt = lines.findIndex((l, i) => i > 0 && SEPARATOR.test(l))\n const header = separatorAt > 0 ? splitRow(lines[0]) : null\n const aligns =\n separatorAt > 0 ? splitRow(lines[separatorAt]).map(alignOf) : []\n const bodyLines = lines.filter(\n (_, i) => i !== separatorAt && !(header && i === 0),\n )\n const body = bodyLines.map(splitRow)\n const columns = Math.max(header?.length ?? 0, ...body.map((r) => r.length))\n if (columns === 0) return []\n\n const cellSpans = (\n cells: string[],\n style?: TextSpan[\"style\"],\n ): TextSpan[][] =>\n Array.from({ length: columns }, (_, c) => {\n const raw = cells[c] ?? \"\"\n const spans = parseInline(raw, theme)\n return style\n ? spans.map((s) => ({ ...s, style: { ...s.style, ...style } }))\n : spans\n })\n\n const headerSpans = header\n ? cellSpans(header, { color: theme.heading, bold: true })\n : null\n const rowSpans = body.map((cells) => cellSpans(cells))\n const allRows = headerSpans ? [headerSpans, ...rowSpans] : rowSpans\n\n const natural = Array.from({ length: columns }, (_, c) =>\n Math.max(1, ...allRows.map((r) => spanWidth(r[c]))),\n )\n const floors = Array.from({ length: columns }, (_, c) =>\n Math.max(1, headerSpans ? spanWidth(headerSpans[c]) : 1),\n )\n const widths = [...natural]\n const total = () =>\n widths.reduce((n, w) => n + w, 0) + GUTTER.length * (columns - 1)\n while (total() > width) {\n let widest = -1\n for (let c = 0; c < columns; c++)\n if (widths[c] > floors[c] && (widest < 0 || widths[c] > widths[widest]))\n widest = c\n if (widest < 0) break\n widths[widest] -= 1\n }\n\n const rowLine = (cells: TextSpan[][]): TextSpan[] => {\n const out: TextSpan[] = []\n cells.forEach((spans, c) => {\n if (c > 0) out.push({ text: GUTTER })\n out.push(\n ...pad(truncateSpans(spans, widths[c]), widths[c], aligns[c] ?? \"left\"),\n )\n })\n return clipSpans(out, width)\n }\n\n const rows: TextSpan[][] = []\n if (headerSpans) {\n rows.push(rowLine(headerSpans))\n const rule: TextSpan[] = []\n widths.forEach((w, c) => {\n if (c > 0) rule.push({ text: GUTTER })\n rule.push({ text: \"─\".repeat(w), style: { color: theme.muted } })\n })\n rows.push(clipSpans(rule, width))\n }\n for (const r of rowSpans) rows.push(rowLine(r))\n return rows\n}\n","import { hash } from \"./hash.js\"\nimport { layoutTable } from \"./table.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 case \"table\": {\n rows.push(...layoutTable(text, width, theme))\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\n// A theme colour is either a chalk name (`cyan`) or a hex (`#FF8C00`) — Ink's\n// `<Text color>` takes both, and a host handing its design tokens through the\n// theme reasonably expects the same here. Chalk exposes the two differently:\n// a name is a property, a hex goes through `.hex()` / `.bgHex()`. Routed by\n// shape, because a hex looked up as a property is `undefined`, and `chain`\n// then hands back the unstyled chain — which is how a cockpit's orange links\n// rendered plain for a week without an error anywhere.\nconst colour = (\n fn: ChalkInstance,\n value: string,\n bg: boolean,\n): ChalkInstance =>\n value.startsWith(\"#\")\n ? bg\n ? fn.bgHex(value)\n : fn.hex(value)\n : chain(fn, bg ? `bg${capitalize(value)}` : value)\n\nconst applyStyle = (text: string, style?: SpanStyle): string => {\n if (!style) return text\n let fn: ChalkInstance = chalk\n if (style.color) fn = colour(fn, style.color, false)\n if (style.backgroundColor) fn = colour(fn, style.backgroundColor, true)\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/ink-markdown",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "A high-performance Markdown / code / diff rendering engine for Ink 7.",
6
6
  "exports": {