markdansi 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- \g<1>2026 Peter Steinberger
3
+ Copyright (c) 2026 Peter Steinberger
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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: stdin if `--in` not given (use `--in -` for stdin explicitly).
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('markdansi');
46
- console.log(render('# hello'));
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 'markdansi';
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: 'single', // collapse consecutive blank lines
68
+ spacing: "single", // collapse consecutive blank lines
63
69
  });
64
70
 
65
- process.stdin.setEncoding('utf8');
66
- process.stdin.on('data', (delta) => {
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('end', () => {
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
- ```js
77
- import { render, createRenderer, strip, themes } from 'markdansi';
82
+ ````js
83
+ import { render, createRenderer, strip, themes } from "markdansi";
78
84
 
79
- const ansi = render('# Hello **world**', { width: 60 });
85
+ const ansi = render("# Hello **world**", { width: 60 });
80
86
 
81
87
  const renderNoWrap = createRenderer({ wrap: false });
82
- const out = renderNoWrap('A very long line...');
88
+ const out = renderNoWrap("A very long line...");
83
89
 
84
90
  // Plain text (no ANSI/OSC)
85
- const plain = strip('link to [x](https://example.com)');
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: 'cyan', dim: true }, // fallback used for inline/block
92
- inlineCode: { color: 'red' },
93
- blockCode: { color: 'green' },
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('`inline`\n\n```\nblock code\n```'));
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 'shiki';
106
+ import { bundledLanguages, bundledThemes, createHighlighter } from "shiki";
101
107
 
102
108
  const shiki = await createHighlighter({
103
- themes: [bundledThemes['github-dark']],
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 (!['ts', 'typescript', 'swift'].includes(normalized)) return code;
117
+ if (!["ts", "typescript", "swift"].includes(normalized)) return code;
112
118
  const { tokens } = shiki.codeToTokens(code, {
113
- lang: normalized === 'swift' ? 'swift' : 'ts',
114
- theme: 'github-dark',
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 ? `\u001b[38;2;${parseInt(token.color.slice(1, 3), 16)};${parseInt(
121
- token.color.slice(3, 5),
122
- 16,
123
- )};${parseInt(token.color.slice(5, 7), 16)}m${token.content}\u001b[39m` : token.content,
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('\n');
135
+ .join("\n");
128
136
  },
129
137
  });
130
- console.log(highlighted('```ts\nconst x: number = 1\n```\n```swift\nlet x = 1\n```'));
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/ast.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ export type Position = unknown;
2
+ type Node = {
3
+ type: string;
4
+ position?: Position;
5
+ };
6
+ export type Text = Node & {
7
+ type: "text";
8
+ value: string;
9
+ };
10
+ export type Emphasis = Node & {
11
+ type: "emphasis";
12
+ children: InlineNode[];
13
+ };
14
+ export type Strong = Node & {
15
+ type: "strong";
16
+ children: InlineNode[];
17
+ };
18
+ export type Delete = Node & {
19
+ type: "delete";
20
+ children: InlineNode[];
21
+ };
22
+ export type InlineCode = Node & {
23
+ type: "inlineCode";
24
+ value: string;
25
+ };
26
+ export type Link = Node & {
27
+ type: "link";
28
+ url: string;
29
+ title?: string | null | undefined;
30
+ children: InlineNode[];
31
+ };
32
+ export type Break = Node & {
33
+ type: "break";
34
+ };
35
+ export type Html = Node & {
36
+ type: "html";
37
+ value: string;
38
+ };
39
+ export type InlineNode = Text | Emphasis | Strong | Delete | InlineCode | Link | Break | Html;
40
+ export type Paragraph = Node & {
41
+ type: "paragraph";
42
+ children: InlineNode[];
43
+ };
44
+ export type Heading = Node & {
45
+ type: "heading";
46
+ depth: number;
47
+ children: InlineNode[];
48
+ };
49
+ export type ThematicBreak = Node & {
50
+ type: "thematicBreak";
51
+ };
52
+ export type Blockquote = Node & {
53
+ type: "blockquote";
54
+ children: BlockNode[];
55
+ };
56
+ export type List = Node & {
57
+ type: "list";
58
+ ordered?: boolean | undefined;
59
+ start?: number | null | undefined;
60
+ spread?: boolean | undefined;
61
+ children: ListItem[];
62
+ };
63
+ export type ListItem = Node & {
64
+ type: "listItem";
65
+ checked?: boolean | null | undefined;
66
+ spread?: boolean | undefined;
67
+ children: BlockNode[];
68
+ };
69
+ export type Code = Node & {
70
+ type: "code";
71
+ value: string;
72
+ lang?: string | null | undefined;
73
+ meta?: string | null | undefined;
74
+ };
75
+ export type TableCell = Node & {
76
+ type: "tableCell";
77
+ children: InlineNode[];
78
+ };
79
+ export type TableRow = Node & {
80
+ type: "tableRow";
81
+ children: TableCell[];
82
+ };
83
+ export type Table = Node & {
84
+ type: "table";
85
+ align?: Array<"left" | "right" | "center" | null> | undefined;
86
+ children: TableRow[];
87
+ };
88
+ export type Definition = Node & {
89
+ type: "definition";
90
+ identifier: string;
91
+ url?: string | undefined;
92
+ title?: string | null | undefined;
93
+ };
94
+ export type BlockNode = Paragraph | Heading | ThematicBreak | Blockquote | List | ListItem | Code | Table | Definition;
95
+ export type Root = Node & {
96
+ type: "root";
97
+ children: BlockNode[];
98
+ };
99
+ export {};
package/dist/ast.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js CHANGED
@@ -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) {
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/parser.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { Root } from "mdast";
1
+ import type { Root } from "./ast.js";
2
2
  export declare function parse(markdown: string): Root;
package/dist/parser.js CHANGED
@@ -1,9 +1,205 @@
1
- import { fromMarkdown } from "mdast-util-from-markdown";
2
- import { gfmFromMarkdown } from "mdast-util-gfm";
3
- import { gfm as gfmSyntax } from "micromark-extension-gfm";
4
- export function parse(markdown) {
5
- return fromMarkdown(markdown, {
6
- extensions: [gfmSyntax()],
7
- mdastExtensions: [gfmFromMarkdown()],
1
+ import { decodeNamedCharacterReference } from "decode-named-character-reference";
2
+ import { marked } from "marked";
3
+ const CHARACTER_REFERENCE = /&(#\d+|#x[\da-f]+|[a-z][\da-z]+);/giu;
4
+ function decodeNumericReference(body, radix) {
5
+ const offset = radix === 16 ? 2 : 1;
6
+ const codePoint = Number.parseInt(body.slice(offset), radix);
7
+ const isDisallowedControl = codePoint <= 0x08 ||
8
+ codePoint === 0x0b ||
9
+ (codePoint >= 0x0e && codePoint <= 0x1f) ||
10
+ (codePoint >= 0x7f && codePoint <= 0x9f);
11
+ const isNonCharacter = (codePoint >= 0xfdd0 && codePoint <= 0xfdef) ||
12
+ (codePoint & 0xffff) === 0xfffe ||
13
+ (codePoint & 0xffff) === 0xffff;
14
+ if (!Number.isSafeInteger(codePoint) ||
15
+ codePoint <= 0 ||
16
+ codePoint > 0x10ffff ||
17
+ (codePoint >= 0xd800 && codePoint <= 0xdfff) ||
18
+ isDisallowedControl ||
19
+ isNonCharacter) {
20
+ return "\uFFFD";
21
+ }
22
+ return String.fromCodePoint(codePoint);
23
+ }
24
+ function decodeEntities(value) {
25
+ return value.replace(CHARACTER_REFERENCE, (reference, body) => {
26
+ if (body.startsWith("#x") || body.startsWith("#X")) {
27
+ return decodeNumericReference(body, 16);
28
+ }
29
+ if (body.startsWith("#")) {
30
+ return decodeNumericReference(body, 10);
31
+ }
32
+ return decodeNamedCharacterReference(body) || reference;
33
+ });
34
+ }
35
+ function convertInlineTokens(tokens) {
36
+ if (!tokens)
37
+ return [];
38
+ const nodes = [];
39
+ for (const token of tokens) {
40
+ switch (token.type) {
41
+ case "text": {
42
+ const text = token;
43
+ if (text.tokens?.length) {
44
+ nodes.push(...convertInlineTokens(text.tokens));
45
+ }
46
+ else {
47
+ nodes.push({ type: "text", value: decodeEntities(text.text) });
48
+ }
49
+ break;
50
+ }
51
+ case "escape":
52
+ nodes.push({ type: "text", value: token.text });
53
+ break;
54
+ case "em":
55
+ nodes.push({
56
+ type: "emphasis",
57
+ children: convertInlineTokens(token.tokens),
58
+ });
59
+ break;
60
+ case "strong":
61
+ nodes.push({
62
+ type: "strong",
63
+ children: convertInlineTokens(token.tokens),
64
+ });
65
+ break;
66
+ case "del":
67
+ nodes.push({
68
+ type: "delete",
69
+ children: convertInlineTokens(token.tokens),
70
+ });
71
+ break;
72
+ case "codespan":
73
+ nodes.push({ type: "inlineCode", value: token.text });
74
+ break;
75
+ case "link": {
76
+ const link = token;
77
+ nodes.push({
78
+ type: "link",
79
+ url: decodeEntities(link.href),
80
+ title: link.title ? decodeEntities(link.title) : link.title,
81
+ children: convertInlineTokens(link.tokens),
82
+ });
83
+ break;
84
+ }
85
+ case "br":
86
+ nodes.push({ type: "break" });
87
+ break;
88
+ case "html":
89
+ nodes.push({ type: "html", value: token.text });
90
+ break;
91
+ default:
92
+ break;
93
+ }
94
+ }
95
+ return nodes;
96
+ }
97
+ function convertCode(token) {
98
+ const info = token.lang?.trim() ?? "";
99
+ const separator = info.search(/\s/u);
100
+ const lang = separator >= 0 ? info.slice(0, separator) : info;
101
+ const meta = separator >= 0 ? info.slice(separator).trim() : "";
102
+ return {
103
+ type: "code",
104
+ value: token.text,
105
+ ...(lang ? { lang } : {}),
106
+ ...(meta ? { meta } : {}),
107
+ };
108
+ }
109
+ function convertListItem(item) {
110
+ return {
111
+ type: "listItem",
112
+ checked: item.task ? Boolean(item.checked) : null,
113
+ spread: item.loose,
114
+ children: convertBlockTokens(item.tokens),
115
+ };
116
+ }
117
+ function convertTableCell(cell) {
118
+ return {
119
+ type: "tableCell",
120
+ children: convertInlineTokens(cell.tokens),
121
+ };
122
+ }
123
+ function convertTableRow(cells) {
124
+ return {
125
+ type: "tableRow",
126
+ children: cells.map(convertTableCell),
127
+ };
128
+ }
129
+ function convertBlockToken(token) {
130
+ switch (token.type) {
131
+ case "paragraph":
132
+ return {
133
+ type: "paragraph",
134
+ children: convertInlineTokens(token.tokens),
135
+ };
136
+ case "text": {
137
+ const text = token;
138
+ return {
139
+ type: "paragraph",
140
+ children: text.tokens?.length
141
+ ? convertInlineTokens(text.tokens)
142
+ : [{ type: "text", value: decodeEntities(text.text) }],
143
+ };
144
+ }
145
+ case "heading": {
146
+ const heading = token;
147
+ return {
148
+ type: "heading",
149
+ depth: heading.depth,
150
+ children: convertInlineTokens(heading.tokens),
151
+ };
152
+ }
153
+ case "hr":
154
+ return { type: "thematicBreak" };
155
+ case "blockquote":
156
+ return {
157
+ type: "blockquote",
158
+ children: convertBlockTokens(token.tokens),
159
+ };
160
+ case "list": {
161
+ const list = token;
162
+ return {
163
+ type: "list",
164
+ ordered: list.ordered,
165
+ start: list.ordered && typeof list.start === "number" ? list.start : null,
166
+ spread: list.loose,
167
+ children: list.items.map(convertListItem),
168
+ };
169
+ }
170
+ case "code":
171
+ return convertCode(token);
172
+ case "table": {
173
+ const table = token;
174
+ return {
175
+ type: "table",
176
+ align: table.align,
177
+ children: [convertTableRow(table.header), ...table.rows.map(convertTableRow)],
178
+ };
179
+ }
180
+ case "def": {
181
+ const definition = token;
182
+ const title = decodeEntities((definition.title ?? "").replace(/\s+/gu, " ").trim());
183
+ return {
184
+ type: "definition",
185
+ identifier: definition.tag,
186
+ url: decodeEntities(definition.href),
187
+ title: title || null,
188
+ };
189
+ }
190
+ default:
191
+ return null;
192
+ }
193
+ }
194
+ function convertBlockTokens(tokens) {
195
+ return tokens.flatMap((token) => {
196
+ const node = convertBlockToken(token);
197
+ return node ? [node] : [];
8
198
  });
9
199
  }
200
+ export function parse(markdown) {
201
+ return {
202
+ type: "root",
203
+ children: convertBlockTokens(marked.lexer(markdown, { gfm: true })),
204
+ };
205
+ }
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
- ...(userTheme || {}),
35
- inlineCode: userTheme?.inlineCode ||
36
- userTheme?.code ||
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
  });
@@ -491,9 +468,9 @@ function renderInline(children, ctx) {
491
468
  case "break":
492
469
  out += HARD_BREAK;
493
470
  break;
494
- default:
495
- if ("value" in node && typeof node.value === "string")
496
- out += node.value;
471
+ case "html":
472
+ out += node.value;
473
+ break;
497
474
  }
498
475
  }
499
476
  return out;
@@ -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 (ctx.style(label, ctx.options.theme.link) +
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,7 +535,7 @@ 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 = new Array(colCount).fill(1);
538
+ const widths = Array.from({ length: colCount }, () => 1);
566
539
  const aligns = node.align || [];
567
540
  const pad = ctx.options.tablePadding;
568
541
  const padStr = " ".repeat(Math.max(0, pad));
@@ -573,7 +546,7 @@ function renderTable(node, ctx) {
573
546
  row.forEach((cell, idx) => {
574
547
  const padded = `${padStr}${cell}${padStr}`;
575
548
  // Cap each column to MAX_COL but keep at least 1
576
- widths[idx] = Math.max(widths[idx], Math.min(MAX_COL, visibleWidth(padded)));
549
+ widths[idx] = Math.max(widths[idx] ?? 1, Math.min(MAX_COL, visibleWidth(padded)));
577
550
  });
578
551
  });
579
552
  const totalWidth = widths.reduce((a, b) => a + b, 0) + 3 * colCount + 1;
@@ -582,19 +555,20 @@ function renderTable(node, ctx) {
582
555
  let over = totalWidth - ctx.options.width;
583
556
  while (over > 0) {
584
557
  const i = widths.indexOf(Math.max(...widths));
585
- if (widths[i] <= minColWidth)
558
+ if ((widths[i] ?? minColWidth) <= minColWidth)
586
559
  break;
587
- widths[i] -= 1;
560
+ widths[i] = (widths[i] ?? minColWidth) - 1;
588
561
  over -= 1;
589
562
  }
590
563
  }
591
564
  for (let i = 0; i < widths.length; i += 1) {
592
- if (widths[i] < minColWidth)
565
+ if ((widths[i] ?? minColWidth) < minColWidth)
593
566
  widths[i] = minColWidth;
594
567
  }
595
568
  const renderRow = (row, isHeader = false) => {
596
569
  const linesPerCol = row.map((cell, idx) => {
597
- const target = Math.max(minContent, widths[idx] - pad * 2);
570
+ const width = widths[idx] ?? minColWidth;
571
+ const target = Math.max(minContent, width - pad * 2);
598
572
  const content = ctx.options.tableTruncate
599
573
  ? truncateCell(cell, target, ctx.options.tableEllipsis)
600
574
  : cell;
@@ -602,7 +576,7 @@ function renderTable(node, ctx) {
602
576
  return wrapped.map((l) => {
603
577
  const aligned = padCell(l, target, aligns[idx] ?? "left");
604
578
  const padded = `${padStr}${aligned}${padStr}`;
605
- return padCell(padded, widths[idx], "left");
579
+ return padCell(padded, width, "left");
606
580
  });
607
581
  });
608
582
  // Row height = max wrapped lines in any column; pad shorter ones
@@ -610,7 +584,7 @@ function renderTable(node, ctx) {
610
584
  const out = [];
611
585
  for (let i = 0; i < height; i += 1) {
612
586
  const parts = linesPerCol.map((col, idx) => {
613
- const content = col[i] ?? padCell("", widths[idx], aligns[idx] ?? "left");
587
+ const content = col[i] ?? padCell("", widths[idx] ?? minColWidth, aligns[idx] ?? "left");
614
588
  return isHeader
615
589
  ? ctx.style(content, ctx.options.theme.tableHeader)
616
590
  : ctx.style(content, ctx.options.theme.tableCell);
@@ -622,38 +596,36 @@ function renderTable(node, ctx) {
622
596
  const headerRows = renderRow(header.children.map((c) => renderInline(c.children, ctx)), true);
623
597
  const bodyRows = rows.flatMap((r) => renderRow(r.children.map((c) => renderInline(c.children, ctx))));
624
598
  if (ctx.options.tableBorder === "none") {
625
- const lines = [...headerRows, ...bodyRows]
626
- .map((row) => row.join(" | "))
627
- .join("\n");
599
+ const lines = [...headerRows, ...bodyRows].map((row) => row.join(" | ")).join("\n");
628
600
  return [`${lines}\n\n`];
629
601
  }
630
602
  const box = TABLE_BOX[ctx.options.tableBorder] || TABLE_BOX.unicode;
631
- const hLine = (sepMid, sepLeft, sepRight) => `${sepLeft}${widths
632
- .map((w) => box.hSep.repeat(w))
633
- .join(sepMid)}${sepRight}\n`;
603
+ const hLine = (sepMid, sepLeft, sepRight) => `${sepLeft}${widths.map((w) => box.hSep.repeat(w)).join(sepMid)}${sepRight}\n`;
634
604
  const top = hLine(box.tSep, box.topLeft, box.topRight);
635
605
  const mid = hLine(box.mSep, box.mLeft, box.mRight);
636
606
  const bottom = hLine(box.bSep, box.bottomLeft, box.bottomRight);
637
- const renderFlat = (rowsArr) => rowsArr
638
- .map((r) => `${box.vSep}${r.map((c) => c).join(box.vSep)}${box.vSep}\n`)
639
- .join("");
607
+ const renderFlat = (rowsArr) => rowsArr.map((r) => `${box.vSep}${r.map((c) => c).join(box.vSep)}${box.vSep}\n`).join("");
640
608
  const dense = ctx.options.tableDense;
641
- const out = [
642
- top,
643
- renderFlat(headerRows),
644
- dense ? "" : mid,
645
- renderFlat(bodyRows),
646
- bottom,
647
- "\n",
648
- ];
609
+ const out = [top, renderFlat(headerRows), dense ? "" : mid, renderFlat(bodyRows), bottom, "\n"];
649
610
  return out;
650
611
  }
651
612
  function truncateCell(text, width, ellipsis) {
652
613
  if (stringWidth(text) <= width)
653
614
  return text;
654
- if (width <= ellipsis.length)
655
- return ellipsis.slice(0, width);
656
- return text.slice(0, width - ellipsis.length) + ellipsis;
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;
657
629
  }
658
630
  function wrapCodeLine(text, width) {
659
631
  // Hard-wrap code even without spaces while keeping ANSI-safe width accounting.
@@ -661,7 +633,7 @@ function wrapCodeLine(text, width) {
661
633
  return [text];
662
634
  const parts = [];
663
635
  let current = "";
664
- for (const ch of [...text]) {
636
+ for (const ch of text) {
665
637
  const chWidth = stringWidth(ch);
666
638
  if (visibleWidth(current) + chWidth > width) {
667
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,7 +3,9 @@
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
- - `micromark`, `micromark-extension-gfm`, `micromark-util-combine-extensions`: GFM parsing (tables, task lists, strikethrough, autolink literals).
6
+
7
+ - `marked`: GFM parsing (tables, task lists, strikethrough, autolink literals).
8
+ - `decode-named-character-reference`: decode Markdown character references in text and URLs.
7
9
  - `chalk`: small, ESM‑only color/style helper.
8
10
  - `string-width`: correct visible width (emoji / wide chars).
9
11
  - `strip-ansi`: strip codes for width/wrapping.
@@ -12,7 +14,9 @@ Goal: Tiny, dependency‑light Markdown → ANSI renderer & CLI for Node ≥22,
12
14
  Dev: `vitest`, TypeScript (NodeNext).
13
15
 
14
16
  ## Surface Area
17
+
15
18
  ### Library (ESM default, CJS export provided)
19
+
16
20
  `render(markdown: string, options?: RenderOptions): string`
17
21
 
18
22
  `createRenderer(options?: RenderOptions): (md: string) => string`
@@ -42,13 +46,16 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
42
46
  `strip(markdown: string): string` — convenience: render with `color=false`, `hyperlinks=false`.
43
47
 
44
48
  ### CLI
45
- `markdansi [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]`
46
- - Input: stdin if no `--in`.
49
+
50
+ `markdansi [FILE] [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]`
51
+
52
+ - Input: positional `FILE`, `--in FILE`, or stdin when neither is given.
47
53
  - Output: stdout if no `--out`.
48
54
  - Wrap: on by default; `--no-wrap` disables; width auto from TTY when not provided.
49
55
  - Links: OSC‑8 hyperlinks enabled when terminal supports; `--no-links` disables.
50
56
 
51
57
  ## Feature Scope (v1)
58
+
52
59
  - Blocks: paragraphs, headings (1–6), blockquotes, fenced/indented code blocks, HR, tables, unordered/ordered lists, task lists.
53
60
  - Inline: strong, emphasis, code spans, autolinks/links, strikethrough (GFM `~~`), backslash escapes.
54
61
  - 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 +65,17 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
58
65
  - Error handling: never throw on malformed emphasis; leave literals untouched if unmatched.
59
66
 
60
67
  ## Rendering Pipeline
61
- 1) **Parse** via micromark with combined GFM extensions → AST events.
62
- 2) **Build light IR** (nodes: paragraph, heading, list, listItem, taskItem, table, tableRow, tableCell, code, inline text/emph/strong/del/code/link).
63
- 3) **Render** to ANSI:
68
+
69
+ 1. **Parse** via Marked's GFM lexer lightweight internal AST.
70
+ 2. **Build light IR** (nodes: paragraph, heading, list, listItem, taskItem, table, tableRow, tableCell, code, inline text/emph/strong/del/code/link).
71
+ 3. **Render** to ANSI:
64
72
  - Style map from theme to SGR codes.
65
73
  - Wrap paragraphs/table cells using `string-width` + `strip-ansi`; wrap only breaks on spaces.
66
74
  - OSC‑8 links when `hyperlinks` true; otherwise underline + optional URL suffix.
67
75
  - Track active SGR for wrapping splits to re-open styles on new lines.
68
76
 
69
77
  ## Themes (initial)
78
+
70
79
  - `default`: bold headings, blue links, cyan inline code, green block code, yellow table headers, subtle quotes/hr.
71
80
  - `dim`: muted colors for low-contrast terminals.
72
81
  - `bright`: higher contrast variant.
@@ -75,17 +84,21 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
75
84
  - `contrast`: magenta headings, cyan inline, green block code, yellow headers, bright markers.
76
85
 
77
86
  ## Testing (vitest)
87
+
78
88
  - Unit: inline formatting (emph/strong/code/strike), links/hyperlinks on/off, wrap/no-wrap behavior, table alignment and wrapping, task lists, strikethrough.
79
89
  - Snapshot-ish string comparisons for representative documents (with colors off to avoid brittle codes).
80
90
 
81
91
  ## Non-Goals (v1)
92
+
82
93
  - Images, footnotes, math, HTML passthrough, syntax highlighting bundle.
83
94
 
84
95
  ## Notes
96
+
85
97
  - Highlighting: built-in is “label-only”; extensibility via `highlighter` hook. No extra deps added for highlighting.
86
98
  - ESM-first; provide CJS export entry for compatibility.
87
99
 
88
100
  ## Behaviors & edge-case rules
101
+
89
102
  - 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
103
  - Color flag: `color=false` removes all ANSI/OSC output (no bold/italic/underline, no hyperlinks); output is plain text.
91
104
  - 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,78 @@
1
1
  {
2
- "name": "markdansi",
3
- "version": "0.2.1",
4
- "description": "Tiny dependency-light markdown to ANSI converter.",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "exports": {
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js",
11
- "default": "./dist/index.js"
12
- },
13
- "./cli": "./dist/cli.js"
14
- },
15
- "bin": {
16
- "markdansi": "dist/cli.js"
17
- },
18
- "scripts": {
19
- "build": "pnpm lint && pnpm test && pnpm compile",
20
- "clean": "rm -rf dist",
21
- "lint": "rm -rf dist coverage && biome check .",
22
- "test": "vitest run",
23
- "test:coverage": "vitest run --coverage",
24
- "types": "tsc -p tsconfig.json --emitDeclarationOnly",
25
- "compile": "tsc -p tsconfig.json",
26
- "prepare": "pnpm compile",
27
- "markdansi": "tsx src/cli.ts"
28
- },
29
- "keywords": [
30
- "markdown",
31
- "ansi",
32
- "terminal",
33
- "cli"
34
- ],
35
- "engines": {
36
- "node": ">=22"
37
- },
38
- "repository": {
39
- "type": "git",
40
- "url": "git+https://github.com/steipete/Markdansi.git"
41
- },
42
- "bugs": {
43
- "url": "https://github.com/steipete/Markdansi/issues"
44
- },
45
- "homepage": "https://github.com/steipete/Markdansi#readme",
46
- "author": "Peter Steinberger",
47
- "license": "MIT",
48
- "sideEffects": false,
49
- "files": [
50
- "dist",
51
- "README.md",
52
- "docs/spec.md",
53
- "package.json",
54
- "tsconfig.json",
55
- ".biome.json"
56
- ],
57
- "types": "dist/index.d.ts",
58
- "dependencies": {
59
- "chalk": "^5.6.2",
60
- "mdast-util-from-markdown": "^2.0.2",
61
- "mdast-util-gfm": "^3.1.0",
62
- "micromark": "^4.0.2",
63
- "micromark-extension-gfm": "^3.0.0",
64
- "micromark-util-combine-extensions": "^2.0.1",
65
- "string-width": "^8.1.0",
66
- "strip-ansi": "^7.1.2",
67
- "supports-hyperlinks": "^4.4.0"
68
- },
69
- "devDependencies": {
70
- "@biomejs/biome": "^2.3.10",
71
- "@types/mdast": "^4.0.4",
72
- "@types/node": "^25.0.3",
73
- "@vitest/coverage-v8": "^4.0.16",
74
- "tsx": "^4.21.0",
75
- "typescript": "^5.9.3",
76
- "vitest": "^4.0.16"
77
- },
78
- "pnpm": {
79
- "onlyBuiltDependencies": [
80
- "esbuild"
81
- ]
82
- }
2
+ "name": "markdansi",
3
+ "version": "0.3.1",
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": "tsc -p tsconfig.json --noEmit",
52
+ "types": "tsc -p tsconfig.json --emitDeclarationOnly",
53
+ "compile": "tsc -p tsconfig.json",
54
+ "prepare": "pnpm compile",
55
+ "markdansi": "tsx src/cli.ts"
56
+ },
57
+ "dependencies": {
58
+ "chalk": "^5.6.2",
59
+ "decode-named-character-reference": "^1.3.0",
60
+ "marked": "^18.0.5",
61
+ "slice-ansi": "^9.0.0",
62
+ "string-width": "^8.2.1",
63
+ "strip-ansi": "^7.2.0",
64
+ "supports-hyperlinks": "^4.4.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^25.6.0",
68
+ "@vitest/coverage-v8": "^4.1.5",
69
+ "oxfmt": "^0.47.0",
70
+ "oxlint": "^1.62.0",
71
+ "tsx": "^4.21.0",
72
+ "typescript": "^6.0.3",
73
+ "vitest": "^4.1.5"
74
+ },
75
+ "engines": {
76
+ "node": ">=22"
77
+ }
83
78
  }
package/tsconfig.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
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
- "resolveJsonModule": true
14
- },
15
- "include": ["src/**/*.ts"],
16
- "exclude": ["node_modules", "dist", "coverage"]
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
- }