markdansi 0.1.0 → 0.1.2

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/src/render.js DELETED
@@ -1,342 +0,0 @@
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
-
8
- function resolveOptions(userOptions = {}) {
9
- const wrap = userOptions.wrap !== undefined ? userOptions.wrap : true;
10
- const baseWidth =
11
- userOptions.width ?? (wrap ? process.stdout.columns || 80 : undefined);
12
- const color =
13
- userOptions.color !== undefined ? userOptions.color : process.stdout.isTTY;
14
- // OSC hyperlinks require color support; if color is off, force hyperlinks off too
15
- const hyperlinks =
16
- userOptions.hyperlinks !== undefined
17
- ? userOptions.hyperlinks
18
- : color && hyperlinkSupported();
19
- const effectiveHyperlinks = color ? hyperlinks : false;
20
- const theme =
21
- userOptions.theme && typeof userOptions.theme === "object"
22
- ? userOptions.theme
23
- : themes[userOptions.theme || "default"] || themes.default;
24
- const mergedTheme = {
25
- ...themes.default,
26
- ...(theme || {}),
27
- // optional fallback: if only `code` provided, reuse for inline/block
28
- inlineCode: theme?.inlineCode || theme?.code || themes.default.inlineCode,
29
- blockCode: theme?.blockCode || theme?.code || themes.default.blockCode,
30
- };
31
- const highlighter = userOptions.highlighter;
32
- const listIndent = userOptions.listIndent ?? 2;
33
- const quotePrefix = userOptions.quotePrefix ?? "│ ";
34
- return {
35
- wrap,
36
- width: baseWidth,
37
- color,
38
- hyperlinks: effectiveHyperlinks,
39
- theme: mergedTheme,
40
- highlighter,
41
- listIndent,
42
- quotePrefix,
43
- };
44
- }
45
-
46
- const HR_WIDTH = 40;
47
- const MAX_COL = 40;
48
-
49
- export function render(markdown, userOptions = {}) {
50
- const options = resolveOptions(userOptions);
51
- const style = createStyler({ color: options.color });
52
- const tree = parse(markdown);
53
- const ctx = { options, style };
54
- const body = renderChildren(tree.children, ctx, 0, true).join("");
55
- return options.color ? body : stripAnsi(body);
56
- }
57
-
58
- export function createRenderer(options) {
59
- return (md) => render(md, options);
60
- }
61
-
62
- function renderChildren(children, ctx, indentLevel = 0, isTightList = false) {
63
- const out = [];
64
- for (const node of children) {
65
- out.push(renderNode(node, ctx, indentLevel, isTightList));
66
- }
67
- return out.flat();
68
- }
69
-
70
- function renderNode(node, ctx, indentLevel, isTightList) {
71
- switch (node.type) {
72
- case "paragraph":
73
- return renderParagraph(node, ctx, indentLevel);
74
- case "heading":
75
- return renderHeading(node, ctx);
76
- case "thematicBreak":
77
- return renderHr(ctx);
78
- case "blockquote":
79
- return renderBlockquote(node, ctx, indentLevel);
80
- case "list":
81
- return renderList(node, ctx, indentLevel);
82
- case "listItem":
83
- return renderListItem(node, ctx, indentLevel, isTightList);
84
- case "code":
85
- return renderCodeBlock(node, ctx);
86
- case "table":
87
- return renderTable(node, ctx);
88
- default:
89
- return []; // inline handled elsewhere or intentionally skipped
90
- }
91
- }
92
-
93
- function renderParagraph(node, ctx, indentLevel) {
94
- const text = renderInline(node.children, ctx);
95
- const prefix = " ".repeat(ctx.options.listIndent * indentLevel);
96
- const lines = wrapWithPrefix(
97
- text,
98
- ctx.options.width ?? 80,
99
- ctx.options.wrap,
100
- prefix,
101
- );
102
- return lines.map((l) => `${l}\n`);
103
- }
104
-
105
- function renderHeading(node, ctx) {
106
- const text = renderInline(node.children, ctx);
107
- const styled = ctx.style(text, ctx.options.theme.heading);
108
- return [`\n${styled}\n`];
109
- }
110
-
111
- function renderHr(ctx) {
112
- const width = ctx.options.wrap
113
- ? Math.min(ctx.options.width ?? HR_WIDTH, HR_WIDTH)
114
- : HR_WIDTH;
115
- const line = "—".repeat(width);
116
- return [`${ctx.style(line, ctx.options.theme.hr)}\n`];
117
- }
118
-
119
- function renderBlockquote(node, ctx, indentLevel) {
120
- // Render blockquote children as text, then wrap with the quote prefix so
121
- // wrapping accounts for prefix width.
122
- const inner = renderChildren(node.children, ctx, indentLevel);
123
- const prefix = ctx.style(ctx.options.quotePrefix, ctx.options.theme.quote);
124
- const text = inner.join("").trimEnd();
125
- const wrapped = wrapWithPrefix(
126
- text,
127
- ctx.options.width ?? 80,
128
- ctx.options.wrap,
129
- prefix,
130
- );
131
- return wrapped.map((l) => `${l}\n`);
132
- }
133
-
134
- function renderList(node, ctx, indentLevel) {
135
- const tight = node.spread === false;
136
- const items = node.children.flatMap((item, idx) =>
137
- renderListItem(
138
- item,
139
- ctx,
140
- indentLevel,
141
- tight,
142
- node.ordered,
143
- node.start ?? 1,
144
- idx,
145
- ),
146
- );
147
- return items;
148
- }
149
-
150
- function renderListItem(
151
- node,
152
- ctx,
153
- indentLevel,
154
- tight,
155
- ordered = false,
156
- start = 1,
157
- idx = 0,
158
- ) {
159
- const marker = ordered ? `${start + idx}.` : "-";
160
- const markerStyled = ctx.style(marker, ctx.options.theme.listMarker);
161
- const content = renderChildren(node.children, ctx, indentLevel + 1, tight)
162
- .join("")
163
- .trimEnd()
164
- .split("\n");
165
-
166
- // Drop leading blank lines so bullets prefix real content (e.g., headings in lists)
167
- while (content.length && content[0].trim() === "") {
168
- content.shift();
169
- }
170
-
171
- const isTask = typeof node.checked === "boolean";
172
- const box = isTask ? (node.checked ? "[x]" : "[ ]") : null;
173
- const firstBullet =
174
- " ".repeat(ctx.options.listIndent * indentLevel) +
175
- (isTask
176
- ? `${ctx.style(box, ctx.options.theme.listMarker)} `
177
- : `${markerStyled} `);
178
-
179
- const lines = [];
180
- content.forEach((line, i) => {
181
- const clean = line.replace(/^\s+/, "");
182
- const prefix =
183
- i === 0
184
- ? firstBullet
185
- : `${" ".repeat(ctx.options.listIndent * indentLevel)}${" ".repeat(
186
- ctx.options.listIndent,
187
- )}`;
188
- lines.push(prefix + clean);
189
- });
190
- if (!tight) lines.push("");
191
- return lines.map((l) => `${l}\n`);
192
- }
193
-
194
- function renderCodeBlock(node, ctx) {
195
- const theme = ctx.options.theme.blockCode || ctx.options.theme.inlineCode;
196
- const label = node.lang ? ctx.style(`[${node.lang}] `, { dim: true }) : "";
197
- let body = node.value ?? "";
198
- if (ctx.options.highlighter) {
199
- const res = ctx.options.highlighter(body, node.lang);
200
- if (typeof res === "string") body = res;
201
- } else {
202
- body = body
203
- .split("\n")
204
- .map((l) => ctx.style(l, theme))
205
- .join("\n");
206
- }
207
- return [`${label}${body}\n\n`];
208
- }
209
-
210
- function renderInline(children, ctx) {
211
- let out = "";
212
- for (const node of children) {
213
- switch (node.type) {
214
- case "text":
215
- out += node.value;
216
- break;
217
- case "emphasis":
218
- out += ctx.style(
219
- renderInline(node.children, ctx),
220
- ctx.options.theme.emph,
221
- );
222
- break;
223
- case "strong":
224
- out += ctx.style(
225
- renderInline(node.children, ctx),
226
- ctx.options.theme.strong,
227
- );
228
- break;
229
- case "delete":
230
- out += ctx.style(renderInline(node.children, ctx), { strike: true });
231
- break;
232
- case "inlineCode": {
233
- const codeTheme =
234
- ctx.options.theme.inlineCode || ctx.options.theme.blockCode;
235
- const content = ctx.style(node.value, codeTheme);
236
- out += content;
237
- break;
238
- }
239
- case "link":
240
- out += renderLink(node, ctx);
241
- break;
242
- case "break":
243
- out += "\n";
244
- break;
245
- default:
246
- if (node.value) out += node.value;
247
- }
248
- }
249
- return out;
250
- }
251
-
252
- function renderLink(node, ctx) {
253
- const label = renderInline(node.children, ctx) || node.url;
254
- const url = node.url || "";
255
- if (ctx.options.hyperlinks && url) {
256
- return osc8(url, label);
257
- }
258
- if (url && label !== url) {
259
- return (
260
- ctx.style(label, ctx.options.theme.link) +
261
- ctx.style(` (${url})`, { dim: true })
262
- );
263
- }
264
- return ctx.style(label, ctx.options.theme.link);
265
- }
266
-
267
- function renderTable(node, ctx) {
268
- const header = node.children[0];
269
- const rows = node.children.slice(1);
270
- const cells = [header, ...rows].map((row) =>
271
- row.children.map((cell) => renderInline(cell.children, ctx)),
272
- );
273
- const colCount = Math.max(...cells.map((r) => r.length));
274
- const widths = new Array(colCount).fill(1);
275
- const aligns = node.align || [];
276
-
277
- cells.forEach((row) => {
278
- row.forEach((cell, idx) => {
279
- // Cap each column to MAX_COL but keep at least 1
280
- widths[idx] = Math.max(
281
- widths[idx],
282
- Math.min(MAX_COL, visibleWidth(cell)),
283
- );
284
- });
285
- });
286
-
287
- const totalWidth = widths.reduce((a, b) => a + b, 0) + 3 * colCount + 1;
288
- if (ctx.options.wrap && ctx.options.width && totalWidth > ctx.options.width) {
289
- // Shrink widest columns until the table fits; allow overflow if already at minima
290
- let over = totalWidth - ctx.options.width;
291
- while (over > 0) {
292
- const i = widths.indexOf(Math.max(...widths));
293
- if (widths[i] <= 1) break;
294
- widths[i] -= 1;
295
- over -= 1;
296
- }
297
- }
298
-
299
- const renderRow = (row, isHeader = false) => {
300
- const linesPerCol = row.map((cell, idx) =>
301
- wrapText(cell, widths[idx], ctx.options.wrap).map((l) =>
302
- padCell(l, widths[idx], aligns[idx]),
303
- ),
304
- );
305
- // Row height = max wrapped lines in any column; pad shorter ones
306
- const height = Math.max(...linesPerCol.map((c) => c.length));
307
- const out = [];
308
- for (let i = 0; i < height; i += 1) {
309
- const parts = linesPerCol.map((col, idx) => {
310
- const content = col[i] ?? padCell("", widths[idx], aligns[idx]);
311
- return isHeader
312
- ? ctx.style(content, ctx.options.theme.tableHeader)
313
- : ctx.style(content, ctx.options.theme.tableCell);
314
- });
315
- out.push(`| ${parts.join(" | ")} |\n`);
316
- }
317
- return out;
318
- };
319
-
320
- const headerRows = renderRow(
321
- header.children.map((c) => renderInline(c.children, ctx)),
322
- true,
323
- );
324
- const divider = `| ${widths.map((w) => "—".repeat(w)).join(" | ")} |\n`;
325
- const bodyRows = rows.flatMap((r) =>
326
- renderRow(r.children.map((c) => renderInline(c.children, ctx))),
327
- );
328
-
329
- return [...headerRows, divider, ...bodyRows, "\n"];
330
- }
331
-
332
- function padCell(text, width, align = "left") {
333
- const pad = width - stringWidth(stripAnsi(text));
334
- if (pad <= 0) return text;
335
- if (align === "right") return `${" ".repeat(pad)}${text}`;
336
- if (align === "center") {
337
- const left = Math.floor(pad / 2);
338
- const right = pad - left;
339
- return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
340
- }
341
- return `${text}${" ".repeat(pad)}`;
342
- }
package/src/theme.js DELETED
@@ -1,53 +0,0 @@
1
- import { Chalk } from "chalk";
2
-
3
- const base = {
4
- heading: { color: "yellow", bold: true },
5
- strong: { bold: true },
6
- emph: { italic: true },
7
- inlineCode: { color: "cyan", dim: true },
8
- blockCode: { color: "cyan", dim: true },
9
- link: { color: "blue", underline: true },
10
- quote: { dim: true },
11
- hr: { dim: true },
12
- listMarker: { color: "cyan" },
13
- tableHeader: { bold: true },
14
- tableCell: {},
15
- };
16
-
17
- const dim = {
18
- ...base,
19
- heading: { color: "white", bold: true, dim: true },
20
- link: { color: "blue", underline: true, dim: true },
21
- };
22
-
23
- const bright = {
24
- ...base,
25
- heading: { color: "magenta", bold: true },
26
- link: { color: "cyan", underline: true },
27
- inlineCode: { color: "green" },
28
- blockCode: { color: "green" },
29
- };
30
-
31
- export const themes = {
32
- default: Object.freeze(base),
33
- dim: Object.freeze(dim),
34
- bright: Object.freeze(bright),
35
- };
36
-
37
- export function createStyler({ color }) {
38
- const level = color ? 3 : 0;
39
- const chalk = new Chalk({ level });
40
- const apply = (text, style = {}) => {
41
- if (!color) return text;
42
- let fn = chalk;
43
- if (style.color && fn[style.color]) fn = fn[style.color];
44
- if (style.bgColor && fn[style.bgColor]) fn = fn[style.bgColor];
45
- if (style.bold) fn = fn.bold;
46
- if (style.italic) fn = fn.italic;
47
- if (style.underline) fn = fn.underline;
48
- if (style.dim) fn = fn.dim;
49
- if (style.strike) fn = fn.strikethrough;
50
- return fn(text);
51
- };
52
- return apply;
53
- }
package/src/wrap.js DELETED
@@ -1,45 +0,0 @@
1
- import stringWidth from "string-width";
2
- import stripAnsi from "strip-ansi";
3
-
4
- export function visibleWidth(text) {
5
- return stringWidth(stripAnsi(text));
6
- }
7
-
8
- /**
9
- * Wrap a single paragraph string into lines respecting visible width.
10
- * Breaks only on spaces. Words longer than width overflow.
11
- */
12
- export function wrapText(text, width, wrap) {
13
- if (!wrap || width <= 0) return [text];
14
- const words = text.split(/(\s+)/).filter((w) => w.length > 0);
15
- const lines = [];
16
- let current = "";
17
- let currentWidth = 0;
18
-
19
- for (const word of words) {
20
- const w = visibleWidth(word);
21
- if (current !== "" && currentWidth + w > width && !/^\s+$/.test(word)) {
22
- lines.push(current);
23
- current = word.replace(/^\s+/, "");
24
- currentWidth = visibleWidth(current);
25
- continue;
26
- }
27
- current += word;
28
- currentWidth = visibleWidth(current);
29
- }
30
-
31
- if (current !== "") lines.push(current);
32
- if (lines.length === 0) lines.push("");
33
- return lines;
34
- }
35
-
36
- export function wrapWithPrefix(text, width, wrap, prefix = "") {
37
- if (!wrap) return text.split("\n").map((line) => prefix + line);
38
- const out = [];
39
- const w = Math.max(1, width - visibleWidth(prefix));
40
- for (const line of text.split("\n")) {
41
- const parts = wrapText(line, w, wrap);
42
- for (const p of parts) out.push(prefix + p);
43
- }
44
- return out;
45
- }
package/types/index.ts DELETED
@@ -1,74 +0,0 @@
1
- // Public API typings for Markdansi.
2
- // Hand-authored source for `pnpm types` (tsc --emitDeclarationOnly)
3
- // to produce dist/index.d.ts. Keep in sync with src/ changes.
4
-
5
- export type ColorName =
6
- | "black"
7
- | "red"
8
- | "green"
9
- | "yellow"
10
- | "blue"
11
- | "magenta"
12
- | "cyan"
13
- | "white"
14
- | `#${string}`
15
- | `${number}`;
16
-
17
- export type StyleIntent = {
18
- color?: ColorName;
19
- bgColor?: ColorName;
20
- bold?: boolean;
21
- italic?: boolean;
22
- underline?: boolean;
23
- dim?: boolean;
24
- strike?: boolean;
25
- };
26
-
27
- export type Theme = {
28
- heading?: StyleIntent;
29
- strong?: StyleIntent;
30
- emph?: StyleIntent;
31
- inlineCode?: StyleIntent;
32
- blockCode?: StyleIntent;
33
- code?: StyleIntent;
34
- link?: StyleIntent;
35
- quote?: StyleIntent;
36
- hr?: StyleIntent;
37
- listMarker?: StyleIntent;
38
- tableHeader?: StyleIntent;
39
- tableCell?: StyleIntent;
40
- };
41
-
42
- export type ThemeName = "default" | "dim" | "bright";
43
-
44
- export type Highlighter = (code: string, lang?: string) => string;
45
-
46
- export interface RenderOptions {
47
- wrap?: boolean;
48
- width?: number;
49
- hyperlinks?: boolean;
50
- color?: boolean;
51
- theme?: ThemeName | Theme;
52
- /**
53
- * Spaces per nesting level for lists (default 2).
54
- */
55
- listIndent?: number;
56
- /**
57
- * Prefix used for blockquotes (default "│ ").
58
- */
59
- quotePrefix?: string;
60
- highlighter?: Highlighter;
61
- }
62
-
63
- export declare function render(
64
- markdown: string,
65
- options?: RenderOptions,
66
- ): string;
67
- export declare function createRenderer(
68
- options?: RenderOptions,
69
- ): (markdown: string) => string;
70
- export declare function strip(
71
- markdown: string,
72
- options?: RenderOptions,
73
- ): string;
74
- export declare const themes: Record<ThemeName | string, Theme>;