markdansi 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -3,6 +3,31 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { render } from "./index.js";
6
+ function readOptionValue(argv, index, option, allowLeadingDashes = false) {
7
+ const value = argv[index + 1];
8
+ if (value === undefined || (!allowLeadingDashes && value.startsWith("--"))) {
9
+ throw new Error(`${option} requires a value`);
10
+ }
11
+ return value;
12
+ }
13
+ function parseIntegerOption(option, value, minimum) {
14
+ const parsed = Number(value);
15
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(parsed) || parsed < minimum) {
16
+ const range = minimum === 0 ? "a non-negative integer" : "a positive integer";
17
+ throw new Error(`${option} must be ${range}`);
18
+ }
19
+ return parsed;
20
+ }
21
+ function parseTheme(value) {
22
+ if (value === "default" || value === "dim" || value === "bright")
23
+ return value;
24
+ throw new Error("--theme must be default, dim, or bright");
25
+ }
26
+ function parseTableBorder(value) {
27
+ if (value === "unicode" || value === "ascii" || value === "none")
28
+ return value;
29
+ throw new Error("--table-border must be unicode, ascii, or none");
30
+ }
6
31
  /**
7
32
  * Ignore EPIPE when downstream (e.g., `head`) closes early.
8
33
  */
@@ -32,80 +57,85 @@ export function parseArgs(argv) {
32
57
  args.color = false;
33
58
  else if (a === "--no-links")
34
59
  args.hyperlinks = false;
60
+ else if (a === "--code-wrap")
61
+ args.codeWrap = true;
35
62
  else if (a === "--code-wrap=false")
36
63
  args.codeWrap = false;
37
64
  else if (a === "--code-wrap=true")
38
65
  args.codeWrap = true;
66
+ else if (a === "--code-box")
67
+ args.codeBox = true;
39
68
  else if (a === "--code-box=false")
40
69
  args.codeBox = false;
41
70
  else if (a === "--code-box=true")
42
71
  args.codeBox = true;
72
+ else if (a === "--code-gutter")
73
+ args.codeGutter = true;
43
74
  else if (a === "--code-gutter=true")
44
75
  args.codeGutter = true;
45
76
  else if (a === "--code-gutter=false")
46
77
  args.codeGutter = false;
47
78
  else if (a.startsWith("--table-border=")) {
48
- const val = a.split("=")[1];
49
- if (val === "unicode" || val === "ascii" || val === "none")
50
- args.tableBorder = val;
79
+ args.tableBorder = parseTableBorder(a.slice("--table-border=".length));
80
+ }
81
+ else if (a === "--table-border") {
82
+ args.tableBorder = parseTableBorder(readOptionValue(argv, i, a));
83
+ i += 1;
51
84
  }
52
85
  else if (a === "--table-dense")
53
86
  args.tableDense = true;
87
+ else if (a === "--table-truncate")
88
+ args.tableTruncate = true;
54
89
  else if (a === "--table-truncate=false")
55
90
  args.tableTruncate = false;
56
91
  else if (a === "--table-truncate=true")
57
92
  args.tableTruncate = true;
58
93
  else if (a === "--table-padding") {
59
- const next = argv[i + 1];
60
- if (next)
61
- args.tablePadding = Number(next);
94
+ const next = readOptionValue(argv, i, a);
95
+ args.tablePadding = parseIntegerOption(a, next, 0);
62
96
  i += 1;
63
97
  }
64
98
  else if (a === "--table-ellipsis") {
65
- const next = argv[i + 1];
66
- if (next)
67
- args.tableEllipsis = next;
99
+ args.tableEllipsis = readOptionValue(argv, i, a, true);
68
100
  i += 1;
69
101
  }
70
102
  else if (a === "--in") {
71
- const next = argv[i + 1];
72
- if (next)
73
- args.in = next;
103
+ args.in = readOptionValue(argv, i, a, true);
74
104
  i += 1;
75
105
  }
76
106
  else if (a === "--out") {
77
- const next = argv[i + 1];
78
- if (next)
79
- args.out = next;
107
+ args.out = readOptionValue(argv, i, a, true);
80
108
  i += 1;
81
109
  }
82
110
  else if (a === "--width") {
83
- const next = argv[i + 1];
84
- if (next)
85
- args.width = Number(next);
111
+ const next = readOptionValue(argv, i, a);
112
+ args.width = parseIntegerOption(a, next, 1);
86
113
  i += 1;
87
114
  }
88
115
  else if (a.startsWith("--theme=")) {
89
- const themeVal = a.split("=")[1];
90
- if (themeVal)
91
- args.theme = themeVal;
116
+ args.theme = parseTheme(a.slice("--theme=".length));
117
+ }
118
+ else if (a === "--theme") {
119
+ args.theme = parseTheme(readOptionValue(argv, i, a));
120
+ i += 1;
92
121
  }
93
122
  else if (a === "--list-indent") {
94
- const next = argv[i + 1];
95
- if (next)
96
- args.listIndent = Number(next);
123
+ const next = readOptionValue(argv, i, a);
124
+ args.listIndent = parseIntegerOption(a, next, 0);
97
125
  i += 1;
98
126
  }
99
127
  else if (a === "--quote-prefix") {
100
- const next = argv[i + 1];
101
- if (next)
102
- args.quotePrefix = next;
128
+ args.quotePrefix = readOptionValue(argv, i, a, true);
103
129
  i += 1;
104
130
  }
105
131
  else if (a === "--help" || a === "-h")
106
132
  args.help = true;
107
- else if (!a.startsWith("-") && !args.in)
133
+ else if (a.startsWith("-"))
134
+ throw new Error(`unknown option: ${a}`);
135
+ else if (!args.in)
108
136
  args.in = a;
137
+ else
138
+ throw new Error(`unexpected positional argument: ${a}`);
109
139
  }
110
140
  return args;
111
141
  }
@@ -114,8 +144,8 @@ export function parseArgs(argv) {
114
144
  */
115
145
  function main() {
116
146
  handleStdoutEpipe();
117
- const args = parseArgs(process.argv);
118
- if (args.help) {
147
+ const { in: inputPath, out: outputPath, help, ...renderOptions } = parseArgs(process.argv);
148
+ if (help) {
119
149
  process.stdout.write(`markdansi [FILE] [options]
120
150
 
121
151
  markdansi file.md Render file
@@ -143,21 +173,12 @@ Options:
143
173
  `);
144
174
  process.exit(0);
145
175
  }
146
- const input = args.in && args.in !== "-"
147
- ? fs.readFileSync(path.resolve(args.in), "utf8")
176
+ const input = inputPath && inputPath !== "-"
177
+ ? fs.readFileSync(path.resolve(inputPath), "utf8")
148
178
  : fs.readFileSync(0, "utf8");
149
- const renderOptions = {
150
- ...(args.wrap !== undefined ? { wrap: args.wrap } : {}),
151
- ...(args.width !== undefined ? { width: args.width } : {}),
152
- ...(args.color !== undefined ? { color: args.color } : {}),
153
- ...(args.hyperlinks !== undefined ? { hyperlinks: args.hyperlinks } : {}),
154
- ...(args.theme !== undefined ? { theme: args.theme } : {}),
155
- ...(args.listIndent !== undefined ? { listIndent: args.listIndent } : {}),
156
- ...(args.quotePrefix !== undefined ? { quotePrefix: args.quotePrefix } : {}),
157
- };
158
179
  const output = render(input, renderOptions);
159
- if (args.out) {
160
- fs.writeFileSync(path.resolve(args.out), output, "utf8");
180
+ if (outputPath) {
181
+ fs.writeFileSync(path.resolve(outputPath), output, "utf8");
161
182
  }
162
183
  else {
163
184
  process.stdout.write(output);
@@ -177,5 +198,11 @@ export function isDirectCliInvocation(metaUrl, argv1) {
177
198
  }
178
199
  // Only run the CLI when executed directly, not when imported for tests.
179
200
  if (isDirectCliInvocation(import.meta.url, process.argv[1])) {
180
- main();
201
+ try {
202
+ main();
203
+ }
204
+ catch (error) {
205
+ console.error(`markdansi: ${error instanceof Error ? error.message : String(error)}`);
206
+ process.exitCode = 1;
207
+ }
181
208
  }
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
@@ -468,9 +468,9 @@ function renderInline(children, ctx) {
468
468
  case "break":
469
469
  out += HARD_BREAK;
470
470
  break;
471
- default:
472
- if ("value" in node && typeof node.value === "string")
473
- out += node.value;
471
+ case "html":
472
+ out += node.value;
473
+ break;
474
474
  }
475
475
  }
476
476
  return out;
package/docs/spec.md CHANGED
@@ -4,7 +4,8 @@ Goal: Tiny, dependency‑light Markdown → ANSI renderer & CLI for Node ≥22,
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
+ - `marked`: GFM parsing (tables, task lists, strikethrough, autolink literals).
8
+ - `decode-named-character-reference`: decode Markdown character references in text and URLs.
8
9
  - `chalk`: small, ESM‑only color/style helper.
9
10
  - `string-width`: correct visible width (emoji / wide chars).
10
11
  - `strip-ansi`: strip codes for width/wrapping.
@@ -65,7 +66,7 @@ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineC
65
66
 
66
67
  ## Rendering Pipeline
67
68
 
68
- 1. **Parse** via micromark with combined GFM extensions → AST events.
69
+ 1. **Parse** via Marked's GFM lexerlightweight internal AST.
69
70
  2. **Build light IR** (nodes: paragraph, heading, list, listItem, taskItem, table, tableRow, tableCell, code, inline text/emph/strong/del/code/link).
70
71
  3. **Render** to ANSI:
71
72
  - Style map from theme to SGR codes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markdansi",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Tiny dependency-light markdown to ANSI converter.",
5
5
  "keywords": [
6
6
  "ansi",
@@ -48,41 +48,31 @@
48
48
  "lint": "rm -rf dist coverage && pnpm format:check && oxlint --deny-warnings src test",
49
49
  "test": "vitest run",
50
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",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "types": "tsc -p tsconfig.json --emitDeclarationOnly",
53
+ "compile": "tsc -p tsconfig.json",
54
54
  "prepare": "pnpm compile",
55
55
  "markdansi": "tsx src/cli.ts"
56
56
  },
57
57
  "dependencies": {
58
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",
59
+ "decode-named-character-reference": "^1.3.0",
60
+ "marked": "^18.0.5",
64
61
  "slice-ansi": "^9.0.0",
65
62
  "string-width": "^8.2.1",
66
63
  "strip-ansi": "^7.2.0",
67
- "supports-hyperlinks": "^4.4.0"
64
+ "supports-hyperlinks": "^4.5.0"
68
65
  },
69
66
  "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",
67
+ "@types/node": "^26.0.1",
68
+ "@vitest/coverage-v8": "^4.1.9",
69
+ "oxfmt": "^0.57.0",
70
+ "oxlint": "^1.72.0",
71
+ "tsx": "^4.22.4",
77
72
  "typescript": "^6.0.3",
78
- "vitest": "^4.1.5"
73
+ "vitest": "^4.1.9"
79
74
  },
80
75
  "engines": {
81
76
  "node": ">=22"
82
- },
83
- "pnpm": {
84
- "onlyBuiltDependencies": [
85
- "esbuild"
86
- ]
87
77
  }
88
78
  }