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/docs/spec.md CHANGED
@@ -1,4 +1,4 @@
1
- # Markdansi v1 – Design Spec
1
+ # Markdansi v0.1.1 – Design Spec
2
2
 
3
3
  Goal: Tiny, dependency‑light Markdown → ANSI renderer & CLI for Node ≥22, using pnpm. Output is terminal ANSI only (no HTML). Focus on readable defaults, sensible wrapping, and minimal runtime deps.
4
4
 
@@ -9,7 +9,7 @@ Goal: Tiny, dependency‑light Markdown → ANSI renderer & CLI for Node ≥22,
9
9
  - `strip-ansi`: strip codes for width/wrapping.
10
10
  - `supports-hyperlinks`: detect OSC‑8 hyperlink support.
11
11
 
12
- Dev: `vitest`.
12
+ Dev: `vitest`, TypeScript (NodeNext).
13
13
 
14
14
  ## Surface Area
15
15
  ### Library (ESM default, CJS export provided)
@@ -22,12 +22,22 @@ Dev: `vitest`.
22
22
  ` width?: number; // used only when wrap===true; default: TTY cols or 80`
23
23
  ` hyperlinks?: boolean; // default: auto via supports-hyperlinks`
24
24
  ` color?: boolean; // default: true if TTY; if false => no ANSI/OSC at all`
25
- ` theme?: ThemeName | Theme; // built-ins: default, dim, bright`
25
+ ` theme?: ThemeName | Theme; // built-ins: default, dim, bright, solarized, monochrome, contrast`
26
+ ` listIndent?: number; // spaces per nesting level; default 2`
27
+ ` quotePrefix?: string; // blockquote line prefix; default "│ "`
28
+ ` tableBorder?: "unicode" | "ascii" | "none"; // default unicode box drawing`
29
+ ` tablePadding?: number; // spaces inside cells (L/R); default 1`
30
+ ` tableDense?: boolean; // reduce separator rows; default false`
31
+ ` tableTruncate?: boolean; // truncate cells to fit col widths; default true`
32
+ ` tableEllipsis?: string; // truncation marker; default "…"`
33
+ ` codeBox?: boolean; // draw a box around fenced code; default true`
34
+ ` codeGutter?: boolean; // left gutter with line numbers; default false`
35
+ ` codeWrap?: boolean; // wrap code to width; default true`
26
36
  ` highlighter?: (code: string, lang?: string) => string; // hook, must not add newlines`
27
37
  `}``
28
38
 
29
- `type Theme = { heading, strong, emph, inlineCode, blockCode, code?, link, quote, hr, listMarker, tableHeader, tableCell }`
30
- Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineCode` / `blockCode` are used if present; otherwise `code` acts as a fallback for both.
39
+ `type Theme = { heading, strong, emph, inlineCode, blockCode, code?, link, quote, hr, listMarker, tableHeader, tableCell, tableBorder, tableSeparator }`
40
+ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineCode` / `blockCode` are used if present; otherwise `code` acts as a fallback for both. Theme exposes defaults for table borders/separators; caller can override per render via options above.
31
41
 
32
42
  `strip(markdown: string): string` — convenience: render with `color=false`, `hyperlinks=false`.
33
43
 
@@ -41,9 +51,9 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
41
51
  ## Feature Scope (v1)
42
52
  - Blocks: paragraphs, headings (1–6), blockquotes, fenced/indented code blocks, HR, tables, unordered/ordered lists, task lists.
43
53
  - Inline: strong, emphasis, code spans, autolinks/links, strikethrough (GFM `~~`), backslash escapes.
44
- - Code blocks: monospace dim box; if `lang` present, show a faint `[lang]` tag. Highlighter hook may recolor text but must not add/remove newlines. Code blocks never hard-wrap; long lines may overflow.
45
- - Tables: render with simple ASCII borders; align based on GFM alignment; wrap cell text respecting width.
46
- - Wrapping: word-wrap on spaces; uses `string-width` on stripped text. Preserve hard breaks; words longer than width are allowed to overflow. Code blocks ignore wrap.
54
+ - Code blocks: monospace box (unicode or ascii; `codeBox=false` disables). Optional gutter with 1‑based line numbers when `codeGutter=true`. If `lang` present, show faint header label. Highlighter hook may recolor text but must not add/remove newlines. Code blocks wrap to the available width by default (hard-wrap long tokens); set `codeWrap=false` to allow overflow.
55
+ - Tables: box-drawing (unicode default, ascii or none). Respect GFM alignment per column, pad cells by `tablePadding`, optional dense borders. Can truncate cell text (`tableTruncate=true`, `tableEllipsis` marker) to keep width. Width balancing shrinks columns while possible; if still too wide, cells overflow.
56
+ - Wrapping: word-wrap on spaces; uses `string-width` on stripped text. Preserve hard breaks; words longer than width may overflow. Code blocks wrap by default; turn off with `codeWrap=false`.
47
57
  - Hyperlinks: OSC‑8 when supported and allowed; fallback to underlined text plus URL in parentheses.
48
58
  - Error handling: never throw on malformed emphasis; leave literals untouched if unmatched.
49
59
 
@@ -57,9 +67,12 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
57
67
  - Track active SGR for wrapping splits to re-open styles on new lines.
58
68
 
59
69
  ## Themes (initial)
60
- - `default`: bold headings, blue links, cyan code, subtle quotes/hr.
70
+ - `default`: bold headings, blue links, cyan inline code, green block code, yellow table headers, subtle quotes/hr.
61
71
  - `dim`: muted colors for low-contrast terminals.
62
72
  - `bright`: higher contrast variant.
73
+ - `solarized`: yellow headings, cyan inline, teal block code, blue links, yellow headers.
74
+ - `monochrome`: bold/italic cues only, dim code, underlined links.
75
+ - `contrast`: magenta headings, cyan inline, green block code, yellow headers, bright markers.
63
76
 
64
77
  ## Testing (vitest)
65
78
  - Unit: inline formatting (emph/strong/code/strike), links/hyperlinks on/off, wrap/no-wrap behavior, table alignment and wrapping, task lists, strikethrough.
@@ -77,7 +90,7 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
77
90
  - Color flag: `color=false` removes all ANSI/OSC output (no bold/italic/underline, no hyperlinks); output is plain text.
78
91
  - Hyperlinks fallback: inline links render as `label (url)` when OSC‑8 disabled; autolinks render as the URL only. URLs count toward width.
79
92
  - Highlighter hook: receives raw code and optional lang; may return ANSI-colored text but must not add or remove newlines. Markdansi owns indentation/padding; code blocks never hard-wrap.
80
- - Tables width algorithm: compute desired column widths from content (cap at e.g. 40). While total exceeds width, decrement widest columns until it fits; if even minimums won’t fit, allow overflow. Respect GFM alignment per column. Cells with newlines keep those breaks.
93
+ - Tables width algorithm: compute desired column widths from content (cap at e.g. 40). While total exceeds width, decrement widest columns until it fits; if even minimums won’t fit, allow overflow. Respect GFM alignment per column. Cells with newlines keep those breaks. Optional truncation shortens cells before layout with `tableEllipsis`.
81
94
  - Lists: honor GFM tight vs loose lists (tight => no blank line between items; loose => blank line). Nesting indent = 2 spaces per level; bullets use `-`; ordered lists use input numbering.
82
95
  - Blockquotes: prefix each wrapped line with `│ ` (configurable via `quotePrefix`); quote content wraps accounting for the prefix width.
83
96
  - List indent is configurable via `listIndent` (default 2 spaces per level).
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "markdansi",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Tiny dependency-light markdown to ANSI converter.",
5
5
  "type": "module",
6
- "main": "src/index.js",
6
+ "main": "dist/index.js",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./src/index.js"
9
+ "import": "./dist/index.js"
10
10
  },
11
- "./cli": "./src/cli.js"
11
+ "./cli": "./dist/cli.js"
12
12
  },
13
13
  "bin": {
14
- "markdansi": "./src/cli.js"
14
+ "markdansi": "./dist/cli.js"
15
15
  },
16
16
  "keywords": [
17
17
  "markdown",
@@ -34,14 +34,12 @@
34
34
  "license": "MIT",
35
35
  "sideEffects": false,
36
36
  "files": [
37
- "src",
38
37
  "dist",
39
38
  "README.md",
40
39
  "docs/spec.md",
41
40
  "package.json",
42
41
  "tsconfig.json",
43
- ".biome.json",
44
- "types"
42
+ ".biome.json"
45
43
  ],
46
44
  "types": "dist/index.d.ts",
47
45
  "dependencies": {
@@ -57,17 +55,19 @@
57
55
  },
58
56
  "devDependencies": {
59
57
  "@biomejs/biome": "^2.3.5",
58
+ "@types/mdast": "^4.0.4",
60
59
  "@types/node": "^24.10.1",
61
60
  "@vitest/coverage-v8": "^4.0.9",
62
61
  "typescript": "^5.9.3",
63
62
  "vitest": "^4.0.9"
64
63
  },
65
64
  "scripts": {
66
- "build": "pnpm lint && pnpm test",
65
+ "build": "pnpm lint && pnpm test && pnpm compile",
67
66
  "clean": "rm -rf dist",
68
67
  "lint": "rm -rf dist coverage && biome check .",
69
68
  "test": "vitest run",
70
69
  "test:coverage": "vitest run --coverage",
71
- "types": "tsc -p tsconfig.json"
70
+ "types": "tsc -p tsconfig.json --emitDeclarationOnly",
71
+ "compile": "tsc -p tsconfig.json"
72
72
  }
73
73
  }
package/tsconfig.json CHANGED
@@ -4,16 +4,14 @@
4
4
  "noUncheckedIndexedAccess": true,
5
5
  "exactOptionalPropertyTypes": true,
6
6
  "noPropertyAccessFromIndexSignature": true,
7
- "allowJs": true,
8
- "checkJs": true,
9
7
  "declaration": true,
10
- "emitDeclarationOnly": true,
11
8
  "outDir": "dist",
9
+ "rootDir": "src",
12
10
  "module": "NodeNext",
13
11
  "moduleResolution": "NodeNext",
14
12
  "target": "ES2022",
15
13
  "resolveJsonModule": true
16
14
  },
17
- "include": ["types/**/*.ts"],
15
+ "include": ["src/**/*.ts"],
18
16
  "exclude": ["node_modules", "dist", "coverage"]
19
17
  }
package/src/cli.js DELETED
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { render } from "./index.js";
5
-
6
- function parseArgs(argv) {
7
- const args = {};
8
- for (let i = 2; i < argv.length; i += 1) {
9
- const a = argv[i];
10
- if (a === "--no-wrap") args.wrap = false;
11
- else if (a === "--no-color") args.color = false;
12
- else if (a === "--no-links") args.hyperlinks = false;
13
- else if (a === "--in") args.in = argv[++i];
14
- else if (a === "--out") args.out = argv[++i];
15
- else if (a === "--width") args.width = Number(argv[++i]);
16
- else if (a.startsWith("--theme=")) args.theme = a.split("=")[1];
17
- else if (a === "--list-indent") args.listIndent = Number(argv[++i]);
18
- else if (a === "--quote-prefix") args.quotePrefix = argv[++i];
19
- else if (a === "--help" || a === "-h") args.help = true;
20
- }
21
- return args;
22
- }
23
-
24
- function main() {
25
- const args = parseArgs(process.argv);
26
- if (args.help) {
27
- process.stdout.write(`markdansi options:
28
- --in FILE Input file (default: stdin)
29
- --out FILE Output file (default: stdout)
30
- --width N Wrap width (default: TTY cols or 80)
31
- --no-wrap Disable hard wrapping
32
- --no-color Disable ANSI/OSC output
33
- --no-links Disable OSC-8 hyperlinks
34
- --theme NAME Theme (default|dim|bright)
35
- --list-indent N Spaces per list nesting level (default: 2)
36
- --quote-prefix STR Prefix for blockquotes (default: "│ ")
37
- `);
38
- process.exit(0);
39
- }
40
- const input =
41
- args.in && args.in !== "-"
42
- ? fs.readFileSync(path.resolve(args.in), "utf8")
43
- : fs.readFileSync(0, "utf8");
44
-
45
- const output = render(input, {
46
- wrap: args.wrap,
47
- width: args.width,
48
- color: args.color,
49
- hyperlinks: args.hyperlinks,
50
- theme: args.theme,
51
- listIndent: args.listIndent,
52
- quotePrefix: args.quotePrefix,
53
- });
54
-
55
- if (args.out) {
56
- fs.writeFileSync(path.resolve(args.out), output, "utf8");
57
- } else {
58
- process.stdout.write(output);
59
- }
60
- }
61
-
62
- main();
package/src/index.js DELETED
@@ -1,12 +0,0 @@
1
- import { createRenderer, render as renderMarkdown } from "./render.js";
2
- import { themes } from "./theme.js";
3
-
4
- export { renderMarkdown as render, createRenderer, themes };
5
-
6
- export function strip(markdown, options = {}) {
7
- return renderMarkdown(markdown, {
8
- ...options,
9
- color: false,
10
- hyperlinks: false,
11
- });
12
- }
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
- }