markdansi 0.1.0 → 0.1.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/dist/render.js ADDED
@@ -0,0 +1,424 @@
1
+ import stringWidth from "string-width";
2
+ import stripAnsi from "strip-ansi";
3
+ import { hyperlinkSupported, osc8 } from "./hyperlink.js";
4
+ import { parse } from "./parser.js";
5
+ import { createStyler, themes } from "./theme.js";
6
+ import { visibleWidth, wrapText, wrapWithPrefix } from "./wrap.js";
7
+ function resolveOptions(userOptions = {}) {
8
+ const wrap = userOptions.wrap !== undefined ? userOptions.wrap : true;
9
+ const baseWidth = userOptions.width ?? (wrap ? process.stdout.columns || 80 : undefined);
10
+ const color = userOptions.color !== undefined ? userOptions.color : process.stdout.isTTY;
11
+ // OSC hyperlinks require color support; if color is off, force hyperlinks off too
12
+ const hyperlinks = userOptions.hyperlinks !== undefined
13
+ ? userOptions.hyperlinks
14
+ : color && hyperlinkSupported();
15
+ const effectiveHyperlinks = color ? hyperlinks : false;
16
+ const baseTheme = themes.default ?? {};
17
+ const userTheme = userOptions.theme && typeof userOptions.theme === "object"
18
+ ? userOptions.theme
19
+ : themes[userOptions.theme || "default"] || baseTheme;
20
+ const mergedTheme = {
21
+ ...baseTheme,
22
+ ...(userTheme || {}),
23
+ inlineCode: userTheme?.inlineCode ||
24
+ userTheme?.code ||
25
+ baseTheme.inlineCode ||
26
+ baseTheme.code ||
27
+ {},
28
+ blockCode: userTheme?.blockCode ||
29
+ userTheme?.code ||
30
+ baseTheme.blockCode ||
31
+ baseTheme.code ||
32
+ {},
33
+ };
34
+ const highlighter = userOptions.highlighter;
35
+ const listIndent = userOptions.listIndent ?? 2;
36
+ const quotePrefix = userOptions.quotePrefix ?? "│ ";
37
+ const tableBorder = userOptions.tableBorder || "unicode";
38
+ const tablePadding = userOptions.tablePadding ?? 1;
39
+ const tableDense = userOptions.tableDense ?? false;
40
+ const tableTruncate = userOptions.tableTruncate ?? true;
41
+ const tableEllipsis = userOptions.tableEllipsis ?? "…";
42
+ const codeBox = userOptions.codeBox ?? true;
43
+ const codeGutter = userOptions.codeGutter ?? false;
44
+ const codeWrap = userOptions.codeWrap ?? true;
45
+ const resolved = {
46
+ wrap,
47
+ color,
48
+ hyperlinks: effectiveHyperlinks,
49
+ theme: mergedTheme,
50
+ highlighter,
51
+ listIndent,
52
+ quotePrefix,
53
+ tableBorder,
54
+ tablePadding,
55
+ tableDense,
56
+ tableTruncate,
57
+ tableEllipsis,
58
+ codeBox,
59
+ codeGutter,
60
+ codeWrap,
61
+ };
62
+ if (baseWidth !== undefined)
63
+ resolved.width = baseWidth;
64
+ return resolved;
65
+ }
66
+ const HR_WIDTH = 40;
67
+ const MAX_COL = 40;
68
+ const TABLE_BOX = {
69
+ unicode: {
70
+ topLeft: "┌",
71
+ topRight: "┐",
72
+ bottomLeft: "└",
73
+ bottomRight: "┘",
74
+ hSep: "─",
75
+ vSep: "│",
76
+ tSep: "┬",
77
+ mSep: "┼",
78
+ bSep: "┴",
79
+ mLeft: "├",
80
+ mRight: "┤",
81
+ },
82
+ ascii: {
83
+ topLeft: "+",
84
+ topRight: "+",
85
+ bottomLeft: "+",
86
+ bottomRight: "+",
87
+ hSep: "-",
88
+ vSep: "|",
89
+ tSep: "+",
90
+ mSep: "+",
91
+ bSep: "+",
92
+ mLeft: "+",
93
+ mRight: "+",
94
+ },
95
+ };
96
+ /**
97
+ * Render Markdown input to an ANSI string.
98
+ */
99
+ export function render(markdown, userOptions = {}) {
100
+ const options = resolveOptions(userOptions);
101
+ const style = createStyler({ color: options.color });
102
+ const tree = parse(markdown);
103
+ const ctx = { options, style };
104
+ const body = renderChildren(tree.children, ctx, 0, true).join("");
105
+ return options.color ? body : stripAnsi(body);
106
+ }
107
+ /**
108
+ * Create a reusable renderer with fixed options.
109
+ */
110
+ export function createRenderer(options) {
111
+ return (md) => render(md, options);
112
+ }
113
+ function renderChildren(children, ctx, indentLevel = 0, isTightList = false) {
114
+ const out = [];
115
+ for (const node of children) {
116
+ out.push(renderNode(node, ctx, indentLevel, isTightList));
117
+ }
118
+ return out.flat();
119
+ }
120
+ function renderNode(node, ctx, indentLevel, isTightList) {
121
+ switch (node.type) {
122
+ case "paragraph":
123
+ return renderParagraph(node, ctx, indentLevel);
124
+ case "heading":
125
+ return renderHeading(node, ctx);
126
+ case "thematicBreak":
127
+ return renderHr(ctx);
128
+ case "blockquote":
129
+ return renderBlockquote(node, ctx, indentLevel);
130
+ case "list":
131
+ return renderList(node, ctx, indentLevel);
132
+ case "listItem":
133
+ return renderListItem(node, ctx, indentLevel, isTightList);
134
+ case "code":
135
+ return renderCodeBlock(node, ctx);
136
+ case "table":
137
+ return renderTable(node, ctx);
138
+ default:
139
+ return []; // inline handled elsewhere or intentionally skipped
140
+ }
141
+ }
142
+ function renderParagraph(node, ctx, indentLevel) {
143
+ const text = renderInline(node.children, ctx);
144
+ const prefix = " ".repeat(ctx.options.listIndent * indentLevel);
145
+ const lines = wrapWithPrefix(text, ctx.options.width ?? 80, ctx.options.wrap, prefix);
146
+ return lines.map((l) => `${l}\n`);
147
+ }
148
+ function renderHeading(node, ctx) {
149
+ const text = renderInline(node.children, ctx);
150
+ const styled = ctx.style(text, ctx.options.theme.heading);
151
+ return [`\n${styled}\n`];
152
+ }
153
+ function renderHr(ctx) {
154
+ const width = ctx.options.wrap
155
+ ? Math.min(ctx.options.width ?? HR_WIDTH, HR_WIDTH)
156
+ : HR_WIDTH;
157
+ const line = "—".repeat(width);
158
+ return [`${ctx.style(line, ctx.options.theme.hr)}\n`];
159
+ }
160
+ function renderBlockquote(node, ctx, indentLevel) {
161
+ // Render blockquote children as text, then wrap with the quote prefix so
162
+ // wrapping accounts for prefix width.
163
+ const inner = renderChildren(node.children, ctx, indentLevel);
164
+ const prefix = ctx.style(ctx.options.quotePrefix, ctx.options.theme.quote);
165
+ const text = inner.join("").trimEnd();
166
+ const wrapped = wrapWithPrefix(text, ctx.options.width ?? 80, ctx.options.wrap, prefix);
167
+ return wrapped.map((l) => `${l}\n`);
168
+ }
169
+ function renderList(node, ctx, indentLevel) {
170
+ const tight = node.spread === false;
171
+ const items = node.children.flatMap((item, idx) => renderListItem(item, ctx, indentLevel, tight, Boolean(node.ordered), node.start ?? 1, idx));
172
+ return items;
173
+ }
174
+ function renderListItem(node, ctx, indentLevel, tight, ordered = false, start = 1, idx = 0) {
175
+ const marker = ordered ? `${start + idx}.` : "-";
176
+ const markerStyled = ctx.style(marker, ctx.options.theme.listMarker);
177
+ const content = renderChildren(node.children, ctx, indentLevel + 1, tight)
178
+ .join("")
179
+ .trimEnd()
180
+ .split("\n");
181
+ // Drop leading blank lines so bullets prefix real content (e.g., headings in lists)
182
+ while (content.length && (content[0]?.trim() ?? "") === "") {
183
+ content.shift();
184
+ }
185
+ const isTask = typeof node.checked === "boolean";
186
+ const box = isTask && node.checked ? "[x]" : "[ ]";
187
+ const firstBullet = " ".repeat(ctx.options.listIndent * indentLevel) +
188
+ (isTask
189
+ ? `${ctx.style(box, ctx.options.theme.listMarker)} `
190
+ : `${markerStyled} `);
191
+ const lines = [];
192
+ content.forEach((line, i) => {
193
+ const clean = line.replace(/^\s+/, "");
194
+ const prefix = i === 0
195
+ ? firstBullet
196
+ : `${" ".repeat(ctx.options.listIndent * indentLevel)}${" ".repeat(ctx.options.listIndent)}`;
197
+ lines.push(prefix + clean);
198
+ });
199
+ if (!tight)
200
+ lines.push("");
201
+ return lines.map((l) => `${l}\n`);
202
+ }
203
+ function renderCodeBlock(node, ctx) {
204
+ const theme = ctx.options.theme.blockCode || ctx.options.theme.inlineCode;
205
+ const lines = (node.value ?? "").split("\n");
206
+ const gutterWidth = ctx.options.codeGutter
207
+ ? String(lines.length).length + 2
208
+ : 0;
209
+ const boxPadding = ctx.options.codeBox ? 4 : 0;
210
+ const wrapLimit = ctx.options.codeWrap && ctx.options.wrap && ctx.options.width
211
+ ? Math.max(1, ctx.options.width - boxPadding - gutterWidth)
212
+ : undefined; // undefined => no hard wrap limit
213
+ const contentLines = lines.flatMap((line, idx) => {
214
+ const segments = wrapLimit !== undefined ? wrapCodeLine(line, wrapLimit) : [line];
215
+ return segments.map((segment, segIdx) => {
216
+ const highlighted = ctx.options.highlighter?.(segment, node.lang ?? undefined) ??
217
+ ctx.style(segment, theme);
218
+ if (!ctx.options.codeGutter)
219
+ return highlighted;
220
+ const num = segIdx === 0
221
+ ? String(idx + 1).padStart(gutterWidth - 2, " ")
222
+ : " ".repeat(gutterWidth - 1);
223
+ return `${ctx.style(num, { dim: true })} ${highlighted}`;
224
+ });
225
+ });
226
+ if (!ctx.options.codeBox) {
227
+ return [`${contentLines.join("\n")}\n\n`];
228
+ }
229
+ // Boxed block
230
+ const maxLine = Math.max(...contentLines.map((l) => visibleWidth(l)), 0);
231
+ const minInner = node.lang ? node.lang.length + 2 : 0;
232
+ const wrapTarget = ctx.options.codeWrap && ctx.options.width
233
+ ? Math.min(maxLine, Math.max(1, ctx.options.width - 4))
234
+ : maxLine;
235
+ const innerWidth = Math.max(ctx.options.codeWrap ? wrapTarget : maxLine, minInner);
236
+ const topLang = node.lang
237
+ ? `${ctx.style(`[${node.lang}]`, { dim: true })} `
238
+ : "";
239
+ const h = "─".repeat(Math.max(innerWidth, topLang.length));
240
+ const top = `┌ ${topLang}${h.slice(topLang.length)}┐`;
241
+ const bottom = `└${"─".repeat(h.length + 1)}┘`;
242
+ const boxLines = contentLines.map((ln) => {
243
+ const pad = Math.max(0, h.length - visibleWidth(ln));
244
+ const left = ctx.style("│ ", { dim: true });
245
+ const right = ctx.style(" │", { dim: true });
246
+ return `${left}${ln}${" ".repeat(pad)}${right}`;
247
+ });
248
+ return [`${top}\n${boxLines.join("\n")}\n${bottom}\n\n`];
249
+ }
250
+ function renderInline(children, ctx) {
251
+ let out = "";
252
+ for (const node of children) {
253
+ switch (node.type) {
254
+ case "text":
255
+ out += node.value;
256
+ break;
257
+ case "emphasis":
258
+ out += ctx.style(renderInline(node.children, ctx), ctx.options.theme.emph);
259
+ break;
260
+ case "strong":
261
+ out += ctx.style(renderInline(node.children, ctx), ctx.options.theme.strong);
262
+ break;
263
+ case "delete":
264
+ out += ctx.style(renderInline(node.children, ctx), { strike: true });
265
+ break;
266
+ case "inlineCode": {
267
+ const codeTheme = ctx.options.theme.inlineCode || ctx.options.theme.blockCode;
268
+ const content = ctx.style(node.value, codeTheme);
269
+ out += content;
270
+ break;
271
+ }
272
+ case "link":
273
+ out += renderLink(node, ctx);
274
+ break;
275
+ case "break":
276
+ out += "\n";
277
+ break;
278
+ default:
279
+ if ("value" in node && typeof node.value === "string")
280
+ out += node.value;
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+ function renderLink(node, ctx) {
286
+ const label = renderInline(node.children, ctx) || node.url;
287
+ const url = node.url || "";
288
+ if (ctx.options.hyperlinks && url) {
289
+ return osc8(url, label);
290
+ }
291
+ if (url && label !== url) {
292
+ return (ctx.style(label, ctx.options.theme.link) +
293
+ ctx.style(` (${url})`, { dim: true }));
294
+ }
295
+ return ctx.style(label, ctx.options.theme.link);
296
+ }
297
+ function renderTable(node, ctx) {
298
+ const header = node.children[0];
299
+ if (!header)
300
+ return [];
301
+ const rows = node.children.slice(1);
302
+ const cells = [header, ...rows].map((row) => row.children.map((cell) => renderInline(cell.children, ctx)));
303
+ const colCount = Math.max(...cells.map((r) => r.length));
304
+ const widths = new Array(colCount).fill(1);
305
+ const aligns = node.align || [];
306
+ const pad = ctx.options.tablePadding;
307
+ const minContent = Math.max(1, ctx.options.tableEllipsis.length + 1);
308
+ // ensure we always have room for at least one visible char + ellipsis + padding
309
+ const minColWidth = Math.max(1, pad * 2 + minContent);
310
+ cells.forEach((row) => {
311
+ row.forEach((cell, idx) => {
312
+ // Cap each column to MAX_COL but keep at least 1
313
+ widths[idx] = Math.max(widths[idx], Math.min(MAX_COL, visibleWidth(cell)));
314
+ });
315
+ });
316
+ const totalWidth = widths.reduce((a, b) => a + b, 0) + 3 * colCount + 1;
317
+ if (ctx.options.wrap && ctx.options.width && totalWidth > ctx.options.width) {
318
+ // Shrink widest columns until the table fits; allow overflow if already at minima
319
+ let over = totalWidth - ctx.options.width;
320
+ while (over > 0) {
321
+ const i = widths.indexOf(Math.max(...widths));
322
+ if (widths[i] <= minColWidth)
323
+ break;
324
+ widths[i] -= 1;
325
+ over -= 1;
326
+ }
327
+ }
328
+ for (let i = 0; i < widths.length; i += 1) {
329
+ if (widths[i] < minColWidth)
330
+ widths[i] = minColWidth;
331
+ }
332
+ const renderRow = (row, isHeader = false) => {
333
+ const linesPerCol = row.map((cell, idx) => {
334
+ const padded = ` ${cell} `;
335
+ const target = Math.max(minContent, widths[idx] - pad * 2);
336
+ const cellText = ctx.options.tableTruncate
337
+ ? truncateCell(cell, target, ctx.options.tableEllipsis)
338
+ : padded;
339
+ const wrapped = wrapText(cellText, ctx.options.wrap ? target : Number.MAX_SAFE_INTEGER, ctx.options.wrap);
340
+ return wrapped.map((l) => padCell(` ${l} `, widths[idx], aligns[idx] ?? "left", ctx.options.tablePadding));
341
+ });
342
+ // Row height = max wrapped lines in any column; pad shorter ones
343
+ const height = Math.max(...linesPerCol.map((c) => c.length));
344
+ const out = [];
345
+ for (let i = 0; i < height; i += 1) {
346
+ const parts = linesPerCol.map((col, idx) => {
347
+ const content = col[i] ?? padCell("", widths[idx], aligns[idx] ?? "left");
348
+ return isHeader
349
+ ? ctx.style(content, ctx.options.theme.tableHeader)
350
+ : ctx.style(content, ctx.options.theme.tableCell);
351
+ });
352
+ out.push(parts);
353
+ }
354
+ return out;
355
+ };
356
+ const headerRows = renderRow(header.children.map((c) => renderInline(c.children, ctx)), true);
357
+ const bodyRows = rows.flatMap((r) => renderRow(r.children.map((c) => renderInline(c.children, ctx))));
358
+ if (ctx.options.tableBorder === "none") {
359
+ const lines = [...headerRows, ...bodyRows]
360
+ .map((row) => row.join(" | "))
361
+ .join("\n");
362
+ return [`${lines}\n\n`];
363
+ }
364
+ const box = TABLE_BOX[ctx.options.tableBorder] || TABLE_BOX.unicode;
365
+ const hLine = (sepMid, sepLeft, sepRight) => `${sepLeft}${widths
366
+ .map((w) => box.hSep.repeat(w))
367
+ .join(sepMid)}${sepRight}\n`;
368
+ const top = hLine(box.tSep, box.topLeft, box.topRight);
369
+ const mid = hLine(box.mSep, box.mLeft, box.mRight);
370
+ const bottom = hLine(box.bSep, box.bottomLeft, box.bottomRight);
371
+ const renderFlat = (rowsArr) => rowsArr
372
+ .map((r) => `${box.vSep}${r.map((c) => c).join(box.vSep)}${box.vSep}\n`)
373
+ .join("");
374
+ const dense = ctx.options.tableDense;
375
+ const out = [
376
+ top,
377
+ renderFlat(headerRows),
378
+ dense ? "" : mid,
379
+ renderFlat(bodyRows),
380
+ bottom,
381
+ "\n",
382
+ ];
383
+ return out;
384
+ }
385
+ function truncateCell(text, width, ellipsis) {
386
+ if (stringWidth(text) <= width)
387
+ return text;
388
+ if (width <= ellipsis.length)
389
+ return ellipsis.slice(0, width);
390
+ return text.slice(0, width - ellipsis.length) + ellipsis;
391
+ }
392
+ function wrapCodeLine(text, width) {
393
+ // Hard-wrap code even without spaces while keeping ANSI-safe width accounting.
394
+ if (width <= 0)
395
+ return [text];
396
+ const parts = [];
397
+ let current = "";
398
+ for (const ch of [...text]) {
399
+ const chWidth = stringWidth(ch);
400
+ if (visibleWidth(current) + chWidth > width) {
401
+ parts.push(current);
402
+ current = ch;
403
+ continue;
404
+ }
405
+ current += ch;
406
+ }
407
+ if (current !== "")
408
+ parts.push(current);
409
+ return parts.length ? parts : [""];
410
+ }
411
+ function padCell(text, width, align = "left", _padSpaces = 0) {
412
+ const core = text;
413
+ const pad = width - stringWidth(stripAnsi(core));
414
+ if (pad <= 0)
415
+ return core;
416
+ if (align === "right")
417
+ return `${" ".repeat(pad)}${core}`;
418
+ if (align === "center") {
419
+ const left = Math.floor(pad / 2);
420
+ const right = pad - left;
421
+ return `${" ".repeat(left)}${core}${" ".repeat(right)}`;
422
+ }
423
+ return `${core}${" ".repeat(pad)}`;
424
+ }
@@ -0,0 +1,18 @@
1
+ import type { StyleIntent, Theme } from "./types.js";
2
+ export interface Themes {
3
+ default: Theme;
4
+ dim: Theme;
5
+ bright: Theme;
6
+ solarized: Theme;
7
+ monochrome: Theme;
8
+ contrast: Theme;
9
+ [key: string]: Theme;
10
+ }
11
+ export declare const themes: Themes;
12
+ export type Styler = (text: string, style?: StyleIntent) => string;
13
+ /**
14
+ * Create a Chalk-based styling helper that applies StyleIntent safely.
15
+ */
16
+ export declare function createStyler({ color }: {
17
+ color: boolean;
18
+ }): Styler;
package/dist/theme.js ADDED
@@ -0,0 +1,105 @@
1
+ import { Chalk } from "chalk";
2
+ const base = {
3
+ heading: { color: "yellow", bold: true },
4
+ strong: { bold: true },
5
+ emph: { italic: true },
6
+ inlineCode: { color: "cyan" },
7
+ blockCode: { color: "green" },
8
+ link: { color: "blue", underline: true },
9
+ quote: { dim: true },
10
+ hr: { dim: true },
11
+ listMarker: { color: "cyan" },
12
+ tableHeader: { bold: true, color: "yellow" },
13
+ tableCell: {},
14
+ };
15
+ const dim = {
16
+ ...base,
17
+ heading: { color: "white", bold: true, dim: true },
18
+ link: { color: "blue", underline: true, dim: true },
19
+ };
20
+ const bright = {
21
+ ...base,
22
+ heading: { color: "magenta", bold: true },
23
+ link: { color: "cyan", underline: true },
24
+ inlineCode: { color: "green" },
25
+ blockCode: { color: "green" },
26
+ };
27
+ const solarized = {
28
+ heading: { color: "yellow", bold: true },
29
+ strong: { bold: true },
30
+ emph: { italic: true },
31
+ inlineCode: { color: "cyan" },
32
+ blockCode: { color: "#2aa198" },
33
+ link: { color: "blue", underline: true },
34
+ quote: { color: "white", dim: true },
35
+ hr: { color: "white", dim: true },
36
+ listMarker: { color: "cyan" },
37
+ tableHeader: { color: "yellow", bold: true },
38
+ };
39
+ const monochrome = {
40
+ heading: { bold: true },
41
+ strong: { bold: true },
42
+ emph: { italic: true },
43
+ inlineCode: { dim: true },
44
+ blockCode: { dim: true },
45
+ link: { underline: true },
46
+ quote: { dim: true },
47
+ hr: { dim: true },
48
+ listMarker: { dim: true },
49
+ tableHeader: { bold: true },
50
+ };
51
+ const contrast = {
52
+ heading: { color: "magenta", bold: true },
53
+ strong: { color: "white", bold: true },
54
+ emph: { color: "white", italic: true },
55
+ inlineCode: { color: "cyan", bold: true },
56
+ blockCode: { color: "green", bold: true },
57
+ link: { color: "blue", underline: true },
58
+ quote: { color: "white", dim: true },
59
+ hr: { color: "white", dim: true },
60
+ listMarker: { color: "yellow", bold: true },
61
+ tableHeader: { color: "yellow", bold: true },
62
+ tableCell: { color: "white" },
63
+ };
64
+ export const themes = {
65
+ default: Object.freeze(base),
66
+ dim: Object.freeze(dim),
67
+ bright: Object.freeze(bright),
68
+ solarized: Object.freeze(solarized),
69
+ monochrome: Object.freeze(monochrome),
70
+ contrast: Object.freeze(contrast),
71
+ };
72
+ /**
73
+ * Create a Chalk-based styling helper that applies StyleIntent safely.
74
+ */
75
+ export function createStyler({ color }) {
76
+ const level = color ? 3 : 0;
77
+ const chalk = new Chalk({ level });
78
+ const apply = (text, style = {}) => {
79
+ if (!color)
80
+ return text;
81
+ let fn = chalk;
82
+ if (style.color) {
83
+ const indexed = fn;
84
+ if (indexed[style.color])
85
+ fn = indexed[style.color];
86
+ }
87
+ if (style.bgColor) {
88
+ const indexed = fn;
89
+ if (indexed[style.bgColor])
90
+ fn = indexed[style.bgColor];
91
+ }
92
+ if (style.bold)
93
+ fn = fn.bold;
94
+ if (style.italic)
95
+ fn = fn.italic;
96
+ if (style.underline)
97
+ fn = fn.underline;
98
+ if (style.dim)
99
+ fn = fn.dim;
100
+ if (style.strike)
101
+ fn = fn.strikethrough;
102
+ return fn(text);
103
+ };
104
+ return apply;
105
+ }
@@ -0,0 +1,58 @@
1
+ export type ColorName = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | `#${string}` | `${number}`;
2
+ export type StyleIntent = {
3
+ color?: ColorName;
4
+ bgColor?: ColorName;
5
+ bold?: boolean;
6
+ italic?: boolean;
7
+ underline?: boolean;
8
+ dim?: boolean;
9
+ strike?: boolean;
10
+ };
11
+ export type Theme = {
12
+ heading?: StyleIntent;
13
+ strong?: StyleIntent;
14
+ emph?: StyleIntent;
15
+ inlineCode?: StyleIntent;
16
+ blockCode?: StyleIntent;
17
+ code?: StyleIntent;
18
+ link?: StyleIntent;
19
+ quote?: StyleIntent;
20
+ hr?: StyleIntent;
21
+ listMarker?: StyleIntent;
22
+ tableHeader?: StyleIntent;
23
+ tableCell?: StyleIntent;
24
+ };
25
+ export type ThemeName = "default" | "dim" | "bright";
26
+ export type Highlighter = (code: string, lang?: string) => string;
27
+ export interface RenderOptions {
28
+ wrap?: boolean;
29
+ width?: number;
30
+ hyperlinks?: boolean;
31
+ color?: boolean;
32
+ theme?: ThemeName | Theme;
33
+ /**
34
+ * Spaces per nesting level for lists (default 2).
35
+ */
36
+ listIndent?: number;
37
+ /**
38
+ * Prefix used for blockquotes (default "│ ").
39
+ */
40
+ quotePrefix?: string;
41
+ /** Table border style: unicode (default), ascii, or none. */
42
+ tableBorder?: "unicode" | "ascii" | "none";
43
+ /** Spaces around cell content (default 1). */
44
+ tablePadding?: number;
45
+ /** If true, reduces separator rows (default false). */
46
+ tableDense?: boolean;
47
+ /** If true, truncates cell content to fit column width (default true). */
48
+ tableTruncate?: boolean;
49
+ /** Ellipsis text for truncation (default "…"). */
50
+ tableEllipsis?: string;
51
+ /** Draw a box around fenced code blocks (default true). */
52
+ codeBox?: boolean;
53
+ /** Show line-number gutter for code blocks (default false). */
54
+ codeGutter?: boolean;
55
+ /** Wrap code lines to width; otherwise overflow (default true). */
56
+ codeWrap?: boolean;
57
+ highlighter?: Highlighter;
58
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/wrap.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Visible width of a string, ignoring ANSI escape codes.
3
+ */
4
+ export declare function visibleWidth(text: string): number;
5
+ /**
6
+ * Wrap a single paragraph string into lines respecting visible width.
7
+ * Breaks only on spaces. Words longer than width overflow.
8
+ */
9
+ export declare function wrapText(text: string, width: number, wrap: boolean): string[];
10
+ export declare function wrapWithPrefix(text: string, width: number, wrap: boolean, prefix?: string): string[];
package/dist/wrap.js ADDED
@@ -0,0 +1,48 @@
1
+ import stringWidth from "string-width";
2
+ import stripAnsi from "strip-ansi";
3
+ /**
4
+ * Visible width of a string, ignoring ANSI escape codes.
5
+ */
6
+ export function visibleWidth(text) {
7
+ return stringWidth(stripAnsi(text));
8
+ }
9
+ /**
10
+ * Wrap a single paragraph string into lines respecting visible width.
11
+ * Breaks only on spaces. Words longer than width overflow.
12
+ */
13
+ export function wrapText(text, width, wrap) {
14
+ if (!wrap || width <= 0)
15
+ return [text];
16
+ const words = text.split(/(\s+)/).filter((w) => w.length > 0);
17
+ const lines = [];
18
+ let current = "";
19
+ let currentWidth = 0;
20
+ for (const word of words) {
21
+ const w = visibleWidth(word);
22
+ if (current !== "" && currentWidth + w > width && !/^\s+$/.test(word)) {
23
+ lines.push(current);
24
+ current = word.replace(/^\s+/, "");
25
+ currentWidth = visibleWidth(current);
26
+ continue;
27
+ }
28
+ current += word;
29
+ currentWidth = visibleWidth(current);
30
+ }
31
+ if (current !== "")
32
+ lines.push(current);
33
+ if (lines.length === 0)
34
+ lines.push("");
35
+ return lines;
36
+ }
37
+ export function wrapWithPrefix(text, width, wrap, prefix = "") {
38
+ if (!wrap)
39
+ return text.split("\n").map((line) => prefix + line);
40
+ const out = [];
41
+ const w = Math.max(1, width - visibleWidth(prefix));
42
+ for (const line of text.split("\n")) {
43
+ const parts = wrapText(line, w, wrap);
44
+ for (const p of parts)
45
+ out.push(prefix + p);
46
+ }
47
+ return out;
48
+ }