markdansi 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +39 -31
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +24 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render.js +52 -75
- package/dist/stream.js +1 -2
- package/docs/spec.md +17 -5
- package/package.json +86 -81
- package/tsconfig.json +16 -15
- package/.biome.json +0 -24
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Tiny, dependency-light Markdown → ANSI renderer and CLI for modern Node (>=22)
|
|
|
11
11
|
Published on npm as `markdansi`.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
|
+
|
|
14
15
|
Grab it from npm; no native deps, so install is instant on Node 22+.
|
|
15
16
|
|
|
16
17
|
```bash
|
|
@@ -22,85 +23,90 @@ npm install markdansi
|
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
## CLI
|
|
26
|
+
|
|
25
27
|
Quick one-shot renderer: pipe Markdown in, ANSI comes out. Flags let you pick width, wrap, colors, links, and table/list styling.
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
|
-
markdansi [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]
|
|
30
|
+
markdansi [FILE] [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]
|
|
29
31
|
[--list-indent N] [--quote-prefix STR]
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
- Input:
|
|
34
|
+
- Input: positional `FILE`, `--in FILE`, or stdin when neither is given (use `--in -` for stdin explicitly).
|
|
33
35
|
- Output: stdout unless `--out` provided.
|
|
34
36
|
- Wrapping: on by default; `--no-wrap` disables hard wrapping.
|
|
35
37
|
- Links: OSC‑8 when supported; `--no-links` disables.
|
|
36
38
|
- Lists/quotes: `--list-indent` sets spaces per nesting level (default 2); `--quote-prefix` sets blockquote prefix (default `│ `).
|
|
37
39
|
|
|
38
40
|
## Library
|
|
41
|
+
|
|
39
42
|
Use the renderer directly in Node/TS for customizable theming, optional syntax highlighting hooks, and OSC‑8 link control.
|
|
40
43
|
|
|
41
44
|
### ESM / CommonJS
|
|
45
|
+
|
|
42
46
|
Markdansi ships ESM (`"type":"module"`). If you’re in CommonJS (or a tool like `tsx` running your script as CJS), prefer dynamic import:
|
|
43
47
|
|
|
44
48
|
```js
|
|
45
|
-
const { render } = await import(
|
|
46
|
-
console.log(render(
|
|
49
|
+
const { render } = await import("markdansi");
|
|
50
|
+
console.log(render("# hello"));
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
### Streaming (recommended: hybrid blocks)
|
|
54
|
+
|
|
50
55
|
If you’re streaming Markdown (LLM output), keep scrollback safe by emitting **completed fragments only**
|
|
51
56
|
and writing them once (append-only; no in-place redraw).
|
|
52
57
|
|
|
53
58
|
Hybrid mode streams regular lines as they complete, but buffers multi-line constructs that need context:
|
|
59
|
+
|
|
54
60
|
- Fenced code blocks (``` / ~~~) — flushed only after the closing fence
|
|
55
61
|
- Tables — flushed only after the header separator row + until the table ends
|
|
56
62
|
|
|
57
63
|
```js
|
|
58
|
-
import { createMarkdownStreamer, render } from
|
|
64
|
+
import { createMarkdownStreamer, render } from "markdansi";
|
|
59
65
|
|
|
60
66
|
const streamer = createMarkdownStreamer({
|
|
61
67
|
render: (md) => render(md, { width: process.stdout.columns ?? 80 }),
|
|
62
|
-
spacing:
|
|
68
|
+
spacing: "single", // collapse consecutive blank lines
|
|
63
69
|
});
|
|
64
70
|
|
|
65
|
-
process.stdin.setEncoding(
|
|
66
|
-
process.stdin.on(
|
|
71
|
+
process.stdin.setEncoding("utf8");
|
|
72
|
+
process.stdin.on("data", (delta) => {
|
|
67
73
|
const chunk = streamer.push(delta);
|
|
68
74
|
if (chunk) process.stdout.write(chunk);
|
|
69
75
|
});
|
|
70
|
-
process.stdin.on(
|
|
76
|
+
process.stdin.on("end", () => {
|
|
71
77
|
const tail = streamer.finish();
|
|
72
78
|
if (tail) process.stdout.write(tail);
|
|
73
79
|
});
|
|
74
80
|
```
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
import { render, createRenderer, strip, themes } from
|
|
82
|
+
````js
|
|
83
|
+
import { render, createRenderer, strip, themes } from "markdansi";
|
|
78
84
|
|
|
79
|
-
const ansi = render(
|
|
85
|
+
const ansi = render("# Hello **world**", { width: 60 });
|
|
80
86
|
|
|
81
87
|
const renderNoWrap = createRenderer({ wrap: false });
|
|
82
|
-
const out = renderNoWrap(
|
|
88
|
+
const out = renderNoWrap("A very long line...");
|
|
83
89
|
|
|
84
90
|
// Plain text (no ANSI/OSC)
|
|
85
|
-
const plain = strip(
|
|
91
|
+
const plain = strip("link to [x](https://example.com)");
|
|
86
92
|
|
|
87
93
|
// Custom theme and highlighter hook
|
|
88
94
|
const custom = createRenderer({
|
|
89
95
|
theme: {
|
|
90
96
|
...themes.default,
|
|
91
|
-
code: { color:
|
|
92
|
-
inlineCode: { color:
|
|
93
|
-
blockCode: { color:
|
|
97
|
+
code: { color: "cyan", dim: true }, // fallback used for inline/block
|
|
98
|
+
inlineCode: { color: "red" },
|
|
99
|
+
blockCode: { color: "green" },
|
|
94
100
|
},
|
|
95
101
|
highlighter: (code, lang) => code.toUpperCase(),
|
|
96
102
|
});
|
|
97
|
-
console.log(custom(
|
|
103
|
+
console.log(custom("`inline`\n\n```\nblock code\n```"));
|
|
98
104
|
|
|
99
105
|
// Example: real syntax highlighting with Shiki (TS + Swift)
|
|
100
|
-
import { bundledLanguages, bundledThemes, createHighlighter } from
|
|
106
|
+
import { bundledLanguages, bundledThemes, createHighlighter } from "shiki";
|
|
101
107
|
|
|
102
108
|
const shiki = await createHighlighter({
|
|
103
|
-
themes: [bundledThemes[
|
|
109
|
+
themes: [bundledThemes["github-dark"]],
|
|
104
110
|
langs: [bundledLanguages.typescript, bundledLanguages.swift],
|
|
105
111
|
});
|
|
106
112
|
|
|
@@ -108,27 +114,29 @@ const highlighted = createRenderer({
|
|
|
108
114
|
highlighter: (code, lang) => {
|
|
109
115
|
if (!lang) return code;
|
|
110
116
|
const normalized = lang.toLowerCase();
|
|
111
|
-
if (![
|
|
117
|
+
if (!["ts", "typescript", "swift"].includes(normalized)) return code;
|
|
112
118
|
const { tokens } = shiki.codeToTokens(code, {
|
|
113
|
-
lang: normalized ===
|
|
114
|
-
theme:
|
|
119
|
+
lang: normalized === "swift" ? "swift" : "ts",
|
|
120
|
+
theme: "github-dark",
|
|
115
121
|
});
|
|
116
122
|
return tokens
|
|
117
123
|
.map((line) =>
|
|
118
124
|
line
|
|
119
125
|
.map((token) =>
|
|
120
|
-
token.color
|
|
121
|
-
token.color.slice(3,
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
token.color
|
|
127
|
+
? `\u001b[38;2;${parseInt(token.color.slice(1, 3), 16)};${parseInt(
|
|
128
|
+
token.color.slice(3, 5),
|
|
129
|
+
16,
|
|
130
|
+
)};${parseInt(token.color.slice(5, 7), 16)}m${token.content}\u001b[39m`
|
|
131
|
+
: token.content,
|
|
124
132
|
)
|
|
125
|
-
.join(
|
|
133
|
+
.join(""),
|
|
126
134
|
)
|
|
127
|
-
.join(
|
|
135
|
+
.join("\n");
|
|
128
136
|
},
|
|
129
137
|
});
|
|
130
|
-
console.log(highlighted(
|
|
131
|
-
|
|
138
|
+
console.log(highlighted("```ts\nconst x: number = 1\n```\n```swift\nlet x = 1\n```"));
|
|
139
|
+
````
|
|
132
140
|
|
|
133
141
|
### Options
|
|
134
142
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export declare function handleStdoutEpipe(): void;
|
|
|
13
13
|
* Parse CLI arguments into RenderOptions-ish object (plus in/out paths).
|
|
14
14
|
*/
|
|
15
15
|
export declare function parseArgs(argv: string[]): CliArgs;
|
|
16
|
+
export declare function isDirectCliInvocation(metaUrl: string, argv1?: string): boolean;
|
|
16
17
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { render } from "./index.js";
|
|
6
6
|
/**
|
|
7
7
|
* Ignore EPIPE when downstream (e.g., `head`) closes early.
|
|
@@ -104,6 +104,8 @@ export function parseArgs(argv) {
|
|
|
104
104
|
}
|
|
105
105
|
else if (a === "--help" || a === "-h")
|
|
106
106
|
args.help = true;
|
|
107
|
+
else if (!a.startsWith("-") && !args.in)
|
|
108
|
+
args.in = a;
|
|
107
109
|
}
|
|
108
110
|
return args;
|
|
109
111
|
}
|
|
@@ -114,7 +116,13 @@ function main() {
|
|
|
114
116
|
handleStdoutEpipe();
|
|
115
117
|
const args = parseArgs(process.argv);
|
|
116
118
|
if (args.help) {
|
|
117
|
-
process.stdout.write(`markdansi options
|
|
119
|
+
process.stdout.write(`markdansi [FILE] [options]
|
|
120
|
+
|
|
121
|
+
markdansi file.md Render file
|
|
122
|
+
markdansi --in file.md Same (explicit)
|
|
123
|
+
cat file.md | markdansi Read from stdin
|
|
124
|
+
|
|
125
|
+
Options:
|
|
118
126
|
--in FILE Input file (default: stdin)
|
|
119
127
|
--out FILE Output file (default: stdout)
|
|
120
128
|
--width N Wrap width (default: TTY cols or 80)
|
|
@@ -145,9 +153,7 @@ function main() {
|
|
|
145
153
|
...(args.hyperlinks !== undefined ? { hyperlinks: args.hyperlinks } : {}),
|
|
146
154
|
...(args.theme !== undefined ? { theme: args.theme } : {}),
|
|
147
155
|
...(args.listIndent !== undefined ? { listIndent: args.listIndent } : {}),
|
|
148
|
-
...(args.quotePrefix !== undefined
|
|
149
|
-
? { quotePrefix: args.quotePrefix }
|
|
150
|
-
: {}),
|
|
156
|
+
...(args.quotePrefix !== undefined ? { quotePrefix: args.quotePrefix } : {}),
|
|
151
157
|
};
|
|
152
158
|
const output = render(input, renderOptions);
|
|
153
159
|
if (args.out) {
|
|
@@ -157,10 +163,19 @@ function main() {
|
|
|
157
163
|
process.stdout.write(output);
|
|
158
164
|
}
|
|
159
165
|
}
|
|
166
|
+
export function isDirectCliInvocation(metaUrl, argv1) {
|
|
167
|
+
if (!argv1)
|
|
168
|
+
return false;
|
|
169
|
+
try {
|
|
170
|
+
const entry = fs.realpathSync(argv1);
|
|
171
|
+
const self = fs.realpathSync(fileURLToPath(metaUrl));
|
|
172
|
+
return entry === self;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
160
178
|
// Only run the CLI when executed directly, not when imported for tests.
|
|
161
|
-
|
|
162
|
-
? pathToFileURL(process.argv[1]).href
|
|
163
|
-
: undefined;
|
|
164
|
-
if (import.meta.url === entryHref) {
|
|
179
|
+
if (isDirectCliInvocation(import.meta.url, process.argv[1])) {
|
|
165
180
|
main();
|
|
166
181
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createRenderer, render as renderMarkdown } from "./render.js";
|
|
|
2
2
|
import { createMarkdownStreamer } from "./stream.js";
|
|
3
3
|
import { themes } from "./theme.js";
|
|
4
4
|
import type { RenderOptions, Theme, ThemeName } from "./types.js";
|
|
5
|
-
export { createMarkdownStreamer, createRenderer, renderMarkdown as render, themes
|
|
5
|
+
export { createMarkdownStreamer, createRenderer, renderMarkdown as render, themes };
|
|
6
6
|
export type { RenderOptions, Theme, ThemeName };
|
|
7
7
|
/**
|
|
8
8
|
* Render Markdown to plain text (no ANSI, no hyperlinks) while preserving layout/wrapping.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRenderer, render as renderMarkdown } from "./render.js";
|
|
2
2
|
import { createMarkdownStreamer } from "./stream.js";
|
|
3
3
|
import { themes } from "./theme.js";
|
|
4
|
-
export { createMarkdownStreamer, createRenderer, renderMarkdown as render, themes
|
|
4
|
+
export { createMarkdownStreamer, createRenderer, renderMarkdown as render, themes };
|
|
5
5
|
/**
|
|
6
6
|
* Render Markdown to plain text (no ANSI, no hyperlinks) while preserving layout/wrapping.
|
|
7
7
|
*/
|
package/dist/render.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sliceAnsi from "slice-ansi";
|
|
1
2
|
import stringWidth from "string-width";
|
|
2
3
|
import stripAnsi from "strip-ansi";
|
|
3
4
|
import { hyperlinkSupported, osc8 } from "./hyperlink.js";
|
|
@@ -21,9 +22,7 @@ function resolveOptions(userOptions = {}) {
|
|
|
21
22
|
const baseWidth = userOptions.width ?? (wrap ? process.stdout.columns || 80 : undefined);
|
|
22
23
|
const color = userOptions.color !== undefined ? userOptions.color : process.stdout.isTTY;
|
|
23
24
|
// OSC hyperlinks require color support; if color is off, force hyperlinks off too
|
|
24
|
-
const hyperlinks = userOptions.hyperlinks !== undefined
|
|
25
|
-
? userOptions.hyperlinks
|
|
26
|
-
: color && hyperlinkSupported();
|
|
25
|
+
const hyperlinks = userOptions.hyperlinks !== undefined ? userOptions.hyperlinks : color && hyperlinkSupported();
|
|
27
26
|
const effectiveHyperlinks = color ? hyperlinks : false;
|
|
28
27
|
const baseTheme = themes.default ?? {};
|
|
29
28
|
const userTheme = userOptions.theme && typeof userOptions.theme === "object"
|
|
@@ -31,17 +30,9 @@ function resolveOptions(userOptions = {}) {
|
|
|
31
30
|
: themes[userOptions.theme || "default"] || baseTheme;
|
|
32
31
|
const mergedTheme = {
|
|
33
32
|
...baseTheme,
|
|
34
|
-
...
|
|
35
|
-
inlineCode: userTheme?.inlineCode ||
|
|
36
|
-
|
|
37
|
-
baseTheme.inlineCode ||
|
|
38
|
-
baseTheme.code ||
|
|
39
|
-
{},
|
|
40
|
-
blockCode: userTheme?.blockCode ||
|
|
41
|
-
userTheme?.code ||
|
|
42
|
-
baseTheme.blockCode ||
|
|
43
|
-
baseTheme.code ||
|
|
44
|
-
{},
|
|
33
|
+
...userTheme,
|
|
34
|
+
inlineCode: userTheme?.inlineCode || userTheme?.code || baseTheme.inlineCode || baseTheme.code || {},
|
|
35
|
+
blockCode: userTheme?.blockCode || userTheme?.code || baseTheme.blockCode || baseTheme.code || {},
|
|
45
36
|
};
|
|
46
37
|
const highlighter = userOptions.highlighter;
|
|
47
38
|
const listIndent = userOptions.listIndent ?? 2;
|
|
@@ -163,8 +154,7 @@ function mergeAdjacentCodeBlocks(nodes) {
|
|
|
163
154
|
};
|
|
164
155
|
for (const node of nodes) {
|
|
165
156
|
if (node?.type === "code") {
|
|
166
|
-
if (pending &&
|
|
167
|
-
(pending.lang === node.lang || (!pending.lang && !node.lang))) {
|
|
157
|
+
if (pending && (pending.lang === node.lang || (!pending.lang && !node.lang))) {
|
|
168
158
|
const nextValue = `${pending.value}\n${node.value}`;
|
|
169
159
|
pending = {
|
|
170
160
|
type: "code",
|
|
@@ -183,9 +173,7 @@ function mergeAdjacentCodeBlocks(nodes) {
|
|
|
183
173
|
if (node?.type === "list") {
|
|
184
174
|
const flattened = flattenCodeList(node);
|
|
185
175
|
if (flattened) {
|
|
186
|
-
if (pending &&
|
|
187
|
-
(pending.lang === flattened.lang ||
|
|
188
|
-
(!pending.lang && !flattened.lang))) {
|
|
176
|
+
if (pending && (pending.lang === flattened.lang || (!pending.lang && !flattened.lang))) {
|
|
189
177
|
const nextValue = `${pending.value}\n${flattened.value}`;
|
|
190
178
|
pending = {
|
|
191
179
|
type: "code",
|
|
@@ -329,9 +317,7 @@ function renderParagraph(node, ctx, indentLevel) {
|
|
|
329
317
|
const defPattern = /^\[[^\]]+]:\s+\S/;
|
|
330
318
|
let inDefinitions = false;
|
|
331
319
|
for (const line of rawLines) {
|
|
332
|
-
if (defPattern.test(line) &&
|
|
333
|
-
normalized.length > 0 &&
|
|
334
|
-
normalized.at(-1) !== "") {
|
|
320
|
+
if (defPattern.test(line) && normalized.length > 0 && normalized.at(-1) !== "") {
|
|
335
321
|
normalized.push(""); // insert blank line before footer-style definitions
|
|
336
322
|
}
|
|
337
323
|
if (defPattern.test(line)) {
|
|
@@ -355,9 +341,7 @@ function renderHeading(node, ctx) {
|
|
|
355
341
|
return [`\n${styled}\n`];
|
|
356
342
|
}
|
|
357
343
|
function renderHr(ctx) {
|
|
358
|
-
const width = ctx.options.wrap
|
|
359
|
-
? Math.min(ctx.options.width ?? HR_WIDTH, HR_WIDTH)
|
|
360
|
-
: HR_WIDTH;
|
|
344
|
+
const width = ctx.options.wrap ? Math.min(ctx.options.width ?? HR_WIDTH, HR_WIDTH) : HR_WIDTH;
|
|
361
345
|
const line = "—".repeat(width);
|
|
362
346
|
return [`${ctx.style(line, ctx.options.theme.hr)}\n`];
|
|
363
347
|
}
|
|
@@ -389,9 +373,7 @@ function renderListItem(node, ctx, indentLevel, tight, ordered = false, start =
|
|
|
389
373
|
const isTask = typeof node.checked === "boolean";
|
|
390
374
|
const box = isTask && node.checked ? "[x]" : "[ ]";
|
|
391
375
|
const firstBullet = " ".repeat(ctx.options.listIndent * indentLevel) +
|
|
392
|
-
(isTask
|
|
393
|
-
? `${ctx.style(box, ctx.options.theme.listMarker)} `
|
|
394
|
-
: `${markerStyled} `);
|
|
376
|
+
(isTask ? `${ctx.style(box, ctx.options.theme.listMarker)} ` : `${markerStyled} `);
|
|
395
377
|
const lines = [];
|
|
396
378
|
content.forEach((line, i) => {
|
|
397
379
|
const clean = line.replace(/^\s+/, "");
|
|
@@ -414,9 +396,7 @@ function renderCodeBlock(node, ctx) {
|
|
|
414
396
|
const theme = ctx.options.theme.blockCode || ctx.options.theme.inlineCode;
|
|
415
397
|
const lines = (node.value ?? "").split("\n");
|
|
416
398
|
const isDiff = node.lang === "diff";
|
|
417
|
-
const gutterWidth = ctx.options.codeGutter
|
|
418
|
-
? String(lines.length).length + 2
|
|
419
|
-
: 0;
|
|
399
|
+
const gutterWidth = ctx.options.codeGutter ? String(lines.length).length + 2 : 0;
|
|
420
400
|
const shouldWrap = isDiff ? false : ctx.options.codeWrap;
|
|
421
401
|
const useBox = ctx.options.codeBox && lines.length > 1;
|
|
422
402
|
const boxPadding = useBox ? 4 : 0;
|
|
@@ -426,13 +406,10 @@ function renderCodeBlock(node, ctx) {
|
|
|
426
406
|
const contentLines = lines.flatMap((line, idx) => {
|
|
427
407
|
const segments = wrapLimit !== undefined ? wrapCodeLine(line, wrapLimit) : [line];
|
|
428
408
|
return segments.map((segment, segIdx) => {
|
|
429
|
-
const highlighted = ctx.options.highlighter?.(segment, node.lang ?? undefined) ??
|
|
430
|
-
ctx.style(segment, theme);
|
|
409
|
+
const highlighted = ctx.options.highlighter?.(segment, node.lang ?? undefined) ?? ctx.style(segment, theme);
|
|
431
410
|
if (!ctx.options.codeGutter)
|
|
432
411
|
return highlighted;
|
|
433
|
-
const num = segIdx === 0
|
|
434
|
-
? String(idx + 1).padStart(gutterWidth - 2, " ")
|
|
435
|
-
: " ".repeat(gutterWidth - 1);
|
|
412
|
+
const num = segIdx === 0 ? String(idx + 1).padStart(gutterWidth - 2, " ") : " ".repeat(gutterWidth - 1);
|
|
436
413
|
return `${ctx.style(num, { dim: true })} ${highlighted}`;
|
|
437
414
|
});
|
|
438
415
|
});
|
|
@@ -530,10 +507,7 @@ function normalizeParagraphInlineText(text) {
|
|
|
530
507
|
}
|
|
531
508
|
const leftTrim = left.trimStart();
|
|
532
509
|
const rightTrim = right.trimStart();
|
|
533
|
-
const keepNewline = left === "" ||
|
|
534
|
-
right === "" ||
|
|
535
|
-
defPattern.test(leftTrim) ||
|
|
536
|
-
defPattern.test(rightTrim);
|
|
510
|
+
const keepNewline = left === "" || right === "" || defPattern.test(leftTrim) || defPattern.test(rightTrim);
|
|
537
511
|
out += keepNewline ? "\n" : " ";
|
|
538
512
|
out += rightTrim;
|
|
539
513
|
}
|
|
@@ -550,8 +524,7 @@ function renderLink(node, ctx) {
|
|
|
550
524
|
return osc8(url, label);
|
|
551
525
|
}
|
|
552
526
|
if (url && label !== url) {
|
|
553
|
-
return
|
|
554
|
-
ctx.style(` (${url})`, { dim: true }));
|
|
527
|
+
return ctx.style(label, ctx.options.theme.link) + ctx.style(` (${url})`, { dim: true });
|
|
555
528
|
}
|
|
556
529
|
return ctx.style(label, ctx.options.theme.link);
|
|
557
530
|
}
|
|
@@ -562,16 +535,18 @@ function renderTable(node, ctx) {
|
|
|
562
535
|
const rows = node.children.slice(1);
|
|
563
536
|
const cells = [header, ...rows].map((row) => row.children.map((cell) => renderInline(cell.children, ctx)));
|
|
564
537
|
const colCount = Math.max(...cells.map((r) => r.length));
|
|
565
|
-
const widths =
|
|
538
|
+
const widths = Array.from({ length: colCount }, () => 1);
|
|
566
539
|
const aligns = node.align || [];
|
|
567
540
|
const pad = ctx.options.tablePadding;
|
|
541
|
+
const padStr = " ".repeat(Math.max(0, pad));
|
|
568
542
|
const minContent = Math.max(1, ctx.options.tableEllipsis.length + 1);
|
|
569
543
|
// ensure we always have room for at least one visible char + ellipsis + padding
|
|
570
544
|
const minColWidth = Math.max(1, pad * 2 + minContent);
|
|
571
545
|
cells.forEach((row) => {
|
|
572
546
|
row.forEach((cell, idx) => {
|
|
547
|
+
const padded = `${padStr}${cell}${padStr}`;
|
|
573
548
|
// Cap each column to MAX_COL but keep at least 1
|
|
574
|
-
widths[idx] = Math.max(widths[idx], Math.min(MAX_COL, visibleWidth(
|
|
549
|
+
widths[idx] = Math.max(widths[idx] ?? 1, Math.min(MAX_COL, visibleWidth(padded)));
|
|
575
550
|
});
|
|
576
551
|
});
|
|
577
552
|
const totalWidth = widths.reduce((a, b) => a + b, 0) + 3 * colCount + 1;
|
|
@@ -580,32 +555,36 @@ function renderTable(node, ctx) {
|
|
|
580
555
|
let over = totalWidth - ctx.options.width;
|
|
581
556
|
while (over > 0) {
|
|
582
557
|
const i = widths.indexOf(Math.max(...widths));
|
|
583
|
-
if (widths[i] <= minColWidth)
|
|
558
|
+
if ((widths[i] ?? minColWidth) <= minColWidth)
|
|
584
559
|
break;
|
|
585
|
-
widths[i]
|
|
560
|
+
widths[i] = (widths[i] ?? minColWidth) - 1;
|
|
586
561
|
over -= 1;
|
|
587
562
|
}
|
|
588
563
|
}
|
|
589
564
|
for (let i = 0; i < widths.length; i += 1) {
|
|
590
|
-
if (widths[i] < minColWidth)
|
|
565
|
+
if ((widths[i] ?? minColWidth) < minColWidth)
|
|
591
566
|
widths[i] = minColWidth;
|
|
592
567
|
}
|
|
593
568
|
const renderRow = (row, isHeader = false) => {
|
|
594
569
|
const linesPerCol = row.map((cell, idx) => {
|
|
595
|
-
const
|
|
596
|
-
const target = Math.max(minContent,
|
|
597
|
-
const
|
|
570
|
+
const width = widths[idx] ?? minColWidth;
|
|
571
|
+
const target = Math.max(minContent, width - pad * 2);
|
|
572
|
+
const content = ctx.options.tableTruncate
|
|
598
573
|
? truncateCell(cell, target, ctx.options.tableEllipsis)
|
|
599
|
-
:
|
|
600
|
-
const wrapped = wrapText(
|
|
601
|
-
return wrapped.map((l) =>
|
|
574
|
+
: cell;
|
|
575
|
+
const wrapped = wrapText(content, ctx.options.wrap ? target : Number.MAX_SAFE_INTEGER, ctx.options.wrap);
|
|
576
|
+
return wrapped.map((l) => {
|
|
577
|
+
const aligned = padCell(l, target, aligns[idx] ?? "left");
|
|
578
|
+
const padded = `${padStr}${aligned}${padStr}`;
|
|
579
|
+
return padCell(padded, width, "left");
|
|
580
|
+
});
|
|
602
581
|
});
|
|
603
582
|
// Row height = max wrapped lines in any column; pad shorter ones
|
|
604
583
|
const height = Math.max(...linesPerCol.map((c) => c.length));
|
|
605
584
|
const out = [];
|
|
606
585
|
for (let i = 0; i < height; i += 1) {
|
|
607
586
|
const parts = linesPerCol.map((col, idx) => {
|
|
608
|
-
const content = col[i] ?? padCell("", widths[idx], aligns[idx] ?? "left");
|
|
587
|
+
const content = col[i] ?? padCell("", widths[idx] ?? minColWidth, aligns[idx] ?? "left");
|
|
609
588
|
return isHeader
|
|
610
589
|
? ctx.style(content, ctx.options.theme.tableHeader)
|
|
611
590
|
: ctx.style(content, ctx.options.theme.tableCell);
|
|
@@ -617,38 +596,36 @@ function renderTable(node, ctx) {
|
|
|
617
596
|
const headerRows = renderRow(header.children.map((c) => renderInline(c.children, ctx)), true);
|
|
618
597
|
const bodyRows = rows.flatMap((r) => renderRow(r.children.map((c) => renderInline(c.children, ctx))));
|
|
619
598
|
if (ctx.options.tableBorder === "none") {
|
|
620
|
-
const lines = [...headerRows, ...bodyRows]
|
|
621
|
-
.map((row) => row.join(" | "))
|
|
622
|
-
.join("\n");
|
|
599
|
+
const lines = [...headerRows, ...bodyRows].map((row) => row.join(" | ")).join("\n");
|
|
623
600
|
return [`${lines}\n\n`];
|
|
624
601
|
}
|
|
625
602
|
const box = TABLE_BOX[ctx.options.tableBorder] || TABLE_BOX.unicode;
|
|
626
|
-
const hLine = (sepMid, sepLeft, sepRight) => `${sepLeft}${widths
|
|
627
|
-
.map((w) => box.hSep.repeat(w))
|
|
628
|
-
.join(sepMid)}${sepRight}\n`;
|
|
603
|
+
const hLine = (sepMid, sepLeft, sepRight) => `${sepLeft}${widths.map((w) => box.hSep.repeat(w)).join(sepMid)}${sepRight}\n`;
|
|
629
604
|
const top = hLine(box.tSep, box.topLeft, box.topRight);
|
|
630
605
|
const mid = hLine(box.mSep, box.mLeft, box.mRight);
|
|
631
606
|
const bottom = hLine(box.bSep, box.bottomLeft, box.bottomRight);
|
|
632
|
-
const renderFlat = (rowsArr) => rowsArr
|
|
633
|
-
.map((r) => `${box.vSep}${r.map((c) => c).join(box.vSep)}${box.vSep}\n`)
|
|
634
|
-
.join("");
|
|
607
|
+
const renderFlat = (rowsArr) => rowsArr.map((r) => `${box.vSep}${r.map((c) => c).join(box.vSep)}${box.vSep}\n`).join("");
|
|
635
608
|
const dense = ctx.options.tableDense;
|
|
636
|
-
const out = [
|
|
637
|
-
top,
|
|
638
|
-
renderFlat(headerRows),
|
|
639
|
-
dense ? "" : mid,
|
|
640
|
-
renderFlat(bodyRows),
|
|
641
|
-
bottom,
|
|
642
|
-
"\n",
|
|
643
|
-
];
|
|
609
|
+
const out = [top, renderFlat(headerRows), dense ? "" : mid, renderFlat(bodyRows), bottom, "\n"];
|
|
644
610
|
return out;
|
|
645
611
|
}
|
|
646
612
|
function truncateCell(text, width, ellipsis) {
|
|
647
613
|
if (stringWidth(text) <= width)
|
|
648
614
|
return text;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
615
|
+
const ellipsisWidth = stringWidth(ellipsis);
|
|
616
|
+
if (width <= ellipsisWidth)
|
|
617
|
+
return sliceCellContent(ellipsis, width);
|
|
618
|
+
return `${sliceCellContent(text, width - ellipsisWidth)}${ellipsis}`;
|
|
619
|
+
}
|
|
620
|
+
function sliceCellContent(text, width) {
|
|
621
|
+
let end = Math.max(0, width);
|
|
622
|
+
let sliced = sliceAnsi(text, 0, end);
|
|
623
|
+
// slice-ansi owns ANSI/OSC/grapheme boundaries; clamp with Markdansi's width metric.
|
|
624
|
+
while (end > 0 && stringWidth(sliced) > width) {
|
|
625
|
+
end -= 1;
|
|
626
|
+
sliced = sliceAnsi(text, 0, end);
|
|
627
|
+
}
|
|
628
|
+
return sliced;
|
|
652
629
|
}
|
|
653
630
|
function wrapCodeLine(text, width) {
|
|
654
631
|
// Hard-wrap code even without spaces while keeping ANSI-safe width accounting.
|
|
@@ -656,7 +633,7 @@ function wrapCodeLine(text, width) {
|
|
|
656
633
|
return [text];
|
|
657
634
|
const parts = [];
|
|
658
635
|
let current = "";
|
|
659
|
-
for (const ch of
|
|
636
|
+
for (const ch of text) {
|
|
660
637
|
const chWidth = stringWidth(ch);
|
|
661
638
|
if (visibleWidth(current) + chWidth > width) {
|
|
662
639
|
parts.push(current);
|
package/dist/stream.js
CHANGED
|
@@ -126,8 +126,7 @@ export function createMarkdownStreamer(options) {
|
|
|
126
126
|
fence = fenceStart;
|
|
127
127
|
fenceBuffer = `${line}\n`;
|
|
128
128
|
// Some fences are single-line in streams (rare). Handle close immediately.
|
|
129
|
-
if (isFenceEnd(line, fenceStart) &&
|
|
130
|
-
line.trimStart().match(/^(```+|~~~+)\s*$/)) {
|
|
129
|
+
if (isFenceEnd(line, fenceStart) && line.trimStart().match(/^(```+|~~~+)\s*$/)) {
|
|
131
130
|
return out + flushFence();
|
|
132
131
|
}
|
|
133
132
|
return out;
|
package/docs/spec.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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
|
|
|
5
5
|
## Core Dependencies (runtime)
|
|
6
|
+
|
|
6
7
|
- `micromark`, `micromark-extension-gfm`, `micromark-util-combine-extensions`: GFM parsing (tables, task lists, strikethrough, autolink literals).
|
|
7
8
|
- `chalk`: small, ESM‑only color/style helper.
|
|
8
9
|
- `string-width`: correct visible width (emoji / wide chars).
|
|
@@ -12,7 +13,9 @@ Goal: Tiny, dependency‑light Markdown → ANSI renderer & CLI for Node ≥22,
|
|
|
12
13
|
Dev: `vitest`, TypeScript (NodeNext).
|
|
13
14
|
|
|
14
15
|
## Surface Area
|
|
16
|
+
|
|
15
17
|
### Library (ESM default, CJS export provided)
|
|
18
|
+
|
|
16
19
|
`render(markdown: string, options?: RenderOptions): string`
|
|
17
20
|
|
|
18
21
|
`createRenderer(options?: RenderOptions): (md: string) => string`
|
|
@@ -42,13 +45,16 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
|
|
|
42
45
|
`strip(markdown: string): string` — convenience: render with `color=false`, `hyperlinks=false`.
|
|
43
46
|
|
|
44
47
|
### CLI
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
|
|
49
|
+
`markdansi [FILE] [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]`
|
|
50
|
+
|
|
51
|
+
- Input: positional `FILE`, `--in FILE`, or stdin when neither is given.
|
|
47
52
|
- Output: stdout if no `--out`.
|
|
48
53
|
- Wrap: on by default; `--no-wrap` disables; width auto from TTY when not provided.
|
|
49
54
|
- Links: OSC‑8 hyperlinks enabled when terminal supports; `--no-links` disables.
|
|
50
55
|
|
|
51
56
|
## Feature Scope (v1)
|
|
57
|
+
|
|
52
58
|
- Blocks: paragraphs, headings (1–6), blockquotes, fenced/indented code blocks, HR, tables, unordered/ordered lists, task lists.
|
|
53
59
|
- Inline: strong, emphasis, code spans, autolinks/links, strikethrough (GFM `~~`), backslash escapes.
|
|
54
60
|
- 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.
|
|
@@ -58,15 +64,17 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
|
|
|
58
64
|
- Error handling: never throw on malformed emphasis; leave literals untouched if unmatched.
|
|
59
65
|
|
|
60
66
|
## Rendering Pipeline
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
|
|
68
|
+
1. **Parse** via micromark with combined GFM extensions → AST events.
|
|
69
|
+
2. **Build light IR** (nodes: paragraph, heading, list, listItem, taskItem, table, tableRow, tableCell, code, inline text/emph/strong/del/code/link).
|
|
70
|
+
3. **Render** to ANSI:
|
|
64
71
|
- Style map from theme to SGR codes.
|
|
65
72
|
- Wrap paragraphs/table cells using `string-width` + `strip-ansi`; wrap only breaks on spaces.
|
|
66
73
|
- OSC‑8 links when `hyperlinks` true; otherwise underline + optional URL suffix.
|
|
67
74
|
- Track active SGR for wrapping splits to re-open styles on new lines.
|
|
68
75
|
|
|
69
76
|
## Themes (initial)
|
|
77
|
+
|
|
70
78
|
- `default`: bold headings, blue links, cyan inline code, green block code, yellow table headers, subtle quotes/hr.
|
|
71
79
|
- `dim`: muted colors for low-contrast terminals.
|
|
72
80
|
- `bright`: higher contrast variant.
|
|
@@ -75,17 +83,21 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
|
|
|
75
83
|
- `contrast`: magenta headings, cyan inline, green block code, yellow headers, bright markers.
|
|
76
84
|
|
|
77
85
|
## Testing (vitest)
|
|
86
|
+
|
|
78
87
|
- Unit: inline formatting (emph/strong/code/strike), links/hyperlinks on/off, wrap/no-wrap behavior, table alignment and wrapping, task lists, strikethrough.
|
|
79
88
|
- Snapshot-ish string comparisons for representative documents (with colors off to avoid brittle codes).
|
|
80
89
|
|
|
81
90
|
## Non-Goals (v1)
|
|
91
|
+
|
|
82
92
|
- Images, footnotes, math, HTML passthrough, syntax highlighting bundle.
|
|
83
93
|
|
|
84
94
|
## Notes
|
|
95
|
+
|
|
85
96
|
- Highlighting: built-in is “label-only”; extensibility via `highlighter` hook. No extra deps added for highlighting.
|
|
86
97
|
- ESM-first; provide CJS export entry for compatibility.
|
|
87
98
|
|
|
88
99
|
## Behaviors & edge-case rules
|
|
100
|
+
|
|
89
101
|
- Wrap/width precedence: `wrap=false` disables all hard wrapping; `width` is ignored in that mode. When `wrap=true`, width is `options.width ?? ttyColumns ?? 80`.
|
|
90
102
|
- Color flag: `color=false` removes all ANSI/OSC output (no bold/italic/underline, no hyperlinks); output is plain text.
|
|
91
103
|
- Hyperlinks fallback: inline links render as `label (url)` when OSC‑8 disabled; autolinks render as the URL only. URLs count toward width.
|
package/package.json
CHANGED
|
@@ -1,83 +1,88 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
2
|
+
"name": "markdansi",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Tiny dependency-light markdown to ANSI converter.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ansi",
|
|
7
|
+
"cli",
|
|
8
|
+
"markdown",
|
|
9
|
+
"terminal"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/steipete/Markdansi#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/steipete/Markdansi/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Peter Steinberger",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/steipete/Markdansi.git"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"markdansi": "dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"docs/spec.md",
|
|
28
|
+
"package.json",
|
|
29
|
+
"tsconfig.json"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "dist/index.js",
|
|
34
|
+
"types": "dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./cli": "./dist/cli.js"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "pnpm lint && pnpm typecheck && pnpm test && pnpm compile",
|
|
45
|
+
"clean": "rm -rf dist",
|
|
46
|
+
"format": "oxfmt --write .",
|
|
47
|
+
"format:check": "oxfmt --check .",
|
|
48
|
+
"lint": "rm -rf dist coverage && pnpm format:check && oxlint --deny-warnings src test",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:coverage": "vitest run --coverage",
|
|
51
|
+
"typecheck": "tsgo -p tsconfig.json --noEmit",
|
|
52
|
+
"types": "tsgo -p tsconfig.json --emitDeclarationOnly",
|
|
53
|
+
"compile": "tsgo -p tsconfig.json",
|
|
54
|
+
"prepare": "pnpm compile",
|
|
55
|
+
"markdansi": "tsx src/cli.ts"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"chalk": "^5.6.2",
|
|
59
|
+
"mdast-util-from-markdown": "^2.0.3",
|
|
60
|
+
"mdast-util-gfm": "^3.1.0",
|
|
61
|
+
"micromark": "^4.0.2",
|
|
62
|
+
"micromark-extension-gfm": "^3.0.0",
|
|
63
|
+
"micromark-util-combine-extensions": "^2.0.1",
|
|
64
|
+
"slice-ansi": "^9.0.0",
|
|
65
|
+
"string-width": "^8.2.1",
|
|
66
|
+
"strip-ansi": "^7.2.0",
|
|
67
|
+
"supports-hyperlinks": "^4.4.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/mdast": "^4.0.4",
|
|
71
|
+
"@types/node": "^25.6.0",
|
|
72
|
+
"@typescript/native-preview": "7.0.0-dev.20260503.1",
|
|
73
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
74
|
+
"oxfmt": "^0.47.0",
|
|
75
|
+
"oxlint": "^1.62.0",
|
|
76
|
+
"tsx": "^4.21.0",
|
|
77
|
+
"typescript": "^6.0.3",
|
|
78
|
+
"vitest": "^4.1.5"
|
|
79
|
+
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">=22"
|
|
82
|
+
},
|
|
83
|
+
"pnpm": {
|
|
84
|
+
"onlyBuiltDependencies": [
|
|
85
|
+
"esbuild"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
83
88
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"noUncheckedIndexedAccess": true,
|
|
5
|
+
"exactOptionalPropertyTypes": true,
|
|
6
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"module": "NodeNext",
|
|
11
|
+
"moduleResolution": "NodeNext",
|
|
12
|
+
"target": "ES2022",
|
|
13
|
+
"types": ["node"],
|
|
14
|
+
"resolveJsonModule": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "coverage"]
|
|
17
18
|
}
|
package/.biome.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
|
|
3
|
-
"extends": ["biome:recommended"],
|
|
4
|
-
"formatter": {
|
|
5
|
-
"enabled": true
|
|
6
|
-
},
|
|
7
|
-
"organizeImports": {
|
|
8
|
-
"enabled": true
|
|
9
|
-
},
|
|
10
|
-
"files": {
|
|
11
|
-
"ignore": [
|
|
12
|
-
"coverage/**",
|
|
13
|
-
"**/coverage/**",
|
|
14
|
-
"node_modules/**",
|
|
15
|
-
"dist/**",
|
|
16
|
-
"**/dist/**"
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
"linter": {
|
|
20
|
-
"rules": {
|
|
21
|
-
"recommended": true
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|