markdansi 0.1.2 → 0.1.4
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/README.md +56 -7
- package/dist/cli.js +0 -0
- package/dist/render.js +100 -4
- package/package.json +77 -74
package/README.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
# 🎨 Markdansi: Wraps, colors, links—no baggage.
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./markdansi.png" alt="Markdansi README header" width="1100">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
2
7
|
   
|
|
3
8
|
|
|
4
9
|
Tiny, dependency-light Markdown → ANSI renderer and CLI for modern Node (>=22). Focuses on readable terminal output with sensible wrapping, GFM support (tables, task lists, strikethrough), optional OSC‑8 hyperlinks, and zero built‑in syntax highlighting (pluggable hook). Written in TypeScript, ships ESM.
|
|
@@ -6,14 +11,18 @@ Tiny, dependency-light Markdown → ANSI renderer and CLI for modern Node (>=22)
|
|
|
6
11
|
Published on npm as `markdansi`.
|
|
7
12
|
|
|
8
13
|
## Install
|
|
14
|
+
Grab it from npm; no native deps, so install is instant on Node 22+.
|
|
9
15
|
|
|
10
16
|
```bash
|
|
17
|
+
bun add markdansi
|
|
18
|
+
# or
|
|
11
19
|
pnpm add markdansi
|
|
12
20
|
# or
|
|
13
21
|
npm install markdansi
|
|
14
22
|
```
|
|
15
23
|
|
|
16
24
|
## CLI
|
|
25
|
+
Quick one-shot renderer: pipe Markdown in, ANSI comes out. Flags let you pick width, wrap, colors, links, and table/list styling.
|
|
17
26
|
|
|
18
27
|
```bash
|
|
19
28
|
markdansi [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]
|
|
@@ -27,6 +36,15 @@ markdansi [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-li
|
|
|
27
36
|
- Lists/quotes: `--list-indent` sets spaces per nesting level (default 2); `--quote-prefix` sets blockquote prefix (default `│ `).
|
|
28
37
|
|
|
29
38
|
## Library
|
|
39
|
+
Use the renderer directly in Node/TS for customizable theming, optional syntax highlighting hooks, and OSC‑8 link control.
|
|
40
|
+
|
|
41
|
+
### ESM / CommonJS
|
|
42
|
+
Markdansi ships ESM (`"type":"module"`). If you’re in CommonJS (or a tool like `tsx` running your script as CJS), prefer dynamic import:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const { render } = await import('markdansi');
|
|
46
|
+
console.log(render('# hello'));
|
|
47
|
+
```
|
|
30
48
|
|
|
31
49
|
```js
|
|
32
50
|
import { render, createRenderer, strip, themes } from 'markdansi';
|
|
@@ -50,6 +68,39 @@ const custom = createRenderer({
|
|
|
50
68
|
highlighter: (code, lang) => code.toUpperCase(),
|
|
51
69
|
});
|
|
52
70
|
console.log(custom('`inline`\n\n```\nblock code\n```'));
|
|
71
|
+
|
|
72
|
+
// Example: real syntax highlighting with Shiki (TS + Swift)
|
|
73
|
+
import { bundledLanguages, bundledThemes, createHighlighter } from 'shiki';
|
|
74
|
+
|
|
75
|
+
const shiki = await createHighlighter({
|
|
76
|
+
themes: [bundledThemes['github-dark']],
|
|
77
|
+
langs: [bundledLanguages.typescript, bundledLanguages.swift],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const highlighted = createRenderer({
|
|
81
|
+
highlighter: (code, lang) => {
|
|
82
|
+
if (!lang) return code;
|
|
83
|
+
const normalized = lang.toLowerCase();
|
|
84
|
+
if (!['ts', 'typescript', 'swift'].includes(normalized)) return code;
|
|
85
|
+
const { tokens } = shiki.codeToTokens(code, {
|
|
86
|
+
lang: normalized === 'swift' ? 'swift' : 'ts',
|
|
87
|
+
theme: 'github-dark',
|
|
88
|
+
});
|
|
89
|
+
return tokens
|
|
90
|
+
.map((line) =>
|
|
91
|
+
line
|
|
92
|
+
.map((token) =>
|
|
93
|
+
token.color ? `\u001b[38;2;${parseInt(token.color.slice(1, 3), 16)};${parseInt(
|
|
94
|
+
token.color.slice(3, 5),
|
|
95
|
+
16,
|
|
96
|
+
)};${parseInt(token.color.slice(5, 7), 16)}m${token.content}\u001b[39m` : token.content,
|
|
97
|
+
)
|
|
98
|
+
.join(''),
|
|
99
|
+
)
|
|
100
|
+
.join('\n');
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
console.log(highlighted('```ts\nconst x: number = 1\n```\n```swift\nlet x = 1\n```'));
|
|
53
104
|
```
|
|
54
105
|
|
|
55
106
|
### Options
|
|
@@ -67,12 +118,6 @@ console.log(custom('`inline`\n\n```\nblock code\n```'));
|
|
|
67
118
|
- `codeBox`: draw a box around fenced code (default true); `codeGutter` shows line numbers; `codeWrap` wraps code lines by default.
|
|
68
119
|
- `highlighter(code, lang)`: optional hook to recolor code blocks; must not add/remove newlines.
|
|
69
120
|
|
|
70
|
-
## Status
|
|
71
|
-
|
|
72
|
-
Version: `0.1.2` (released)
|
|
73
|
-
Tests: `pnpm test`
|
|
74
|
-
License: MIT
|
|
75
|
-
|
|
76
121
|
## Notes
|
|
77
122
|
|
|
78
123
|
- Code blocks wrap to the render width by default; disable with `codeWrap=false`. If `lang` is present, a faint `[lang]` label is shown and boxes use unicode borders.
|
|
@@ -80,4 +125,8 @@ License: MIT
|
|
|
80
125
|
- Tables use unicode borders by default, include padding, respect GFM alignment, and truncate long cells with `…` so layouts stay tidy. Turn off truncation with `tableTruncate=false`.
|
|
81
126
|
- Tight vs loose lists follow GFM; task items render `[ ]` / `[x]`.
|
|
82
127
|
|
|
83
|
-
See `docs/spec.md` for full behavior details
|
|
128
|
+
See [`docs/spec.md`](docs/spec.md) for full behavior details.
|
|
129
|
+
|
|
130
|
+
Looking for the Swift port? Check out [Swiftdansi](https://github.com/steipete/Swiftdansi).
|
|
131
|
+
|
|
132
|
+
MIT license.
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/render.js
CHANGED
|
@@ -132,7 +132,100 @@ function normalizeNodes(tree) {
|
|
|
132
132
|
if (node)
|
|
133
133
|
normalized.push(node);
|
|
134
134
|
}
|
|
135
|
-
|
|
135
|
+
const mergedCodes = mergeAdjacentCodeBlocks(normalized);
|
|
136
|
+
const taggedDiffs = mergedCodes.map((child) => tagDiffBlock(child));
|
|
137
|
+
return { ...tree, children: taggedDiffs };
|
|
138
|
+
}
|
|
139
|
+
function flattenCodeList(list) {
|
|
140
|
+
if (!list.children.length ||
|
|
141
|
+
!list.children.every((item) => item.children.length === 1 &&
|
|
142
|
+
item.children[0]?.type === "code" &&
|
|
143
|
+
item.children[0].value !== undefined))
|
|
144
|
+
return null;
|
|
145
|
+
const codes = list.children.map((item) => item.children[0]);
|
|
146
|
+
const sameLang = codes.every((c) => c.lang === codes[0]?.lang);
|
|
147
|
+
const lang = sameLang ? (codes[0]?.lang ?? undefined) : undefined;
|
|
148
|
+
return {
|
|
149
|
+
type: "code",
|
|
150
|
+
lang: lang ?? undefined,
|
|
151
|
+
value: codes.map((c) => c.value).join("\n"),
|
|
152
|
+
position: list.position,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function mergeAdjacentCodeBlocks(nodes) {
|
|
156
|
+
const out = [];
|
|
157
|
+
let pending = null;
|
|
158
|
+
const flush = () => {
|
|
159
|
+
if (pending) {
|
|
160
|
+
out.push(pending);
|
|
161
|
+
pending = null;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
for (const node of nodes) {
|
|
165
|
+
if (node?.type === "code") {
|
|
166
|
+
if (pending &&
|
|
167
|
+
(pending.lang === node.lang || (!pending.lang && !node.lang))) {
|
|
168
|
+
const nextValue = `${pending.value}\n${node.value}`;
|
|
169
|
+
pending = {
|
|
170
|
+
type: "code",
|
|
171
|
+
lang: pending.lang,
|
|
172
|
+
meta: pending.meta,
|
|
173
|
+
value: nextValue,
|
|
174
|
+
position: pending.position,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
flush();
|
|
179
|
+
pending = node;
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (node?.type === "list") {
|
|
184
|
+
const flattened = flattenCodeList(node);
|
|
185
|
+
if (flattened) {
|
|
186
|
+
if (pending &&
|
|
187
|
+
(pending.lang === flattened.lang ||
|
|
188
|
+
(!pending.lang && !flattened.lang))) {
|
|
189
|
+
const nextValue = `${pending.value}\n${flattened.value}`;
|
|
190
|
+
pending = {
|
|
191
|
+
type: "code",
|
|
192
|
+
lang: pending.lang,
|
|
193
|
+
meta: pending.meta,
|
|
194
|
+
value: nextValue,
|
|
195
|
+
position: pending.position,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
flush();
|
|
200
|
+
pending = flattened;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
flush();
|
|
206
|
+
out.push(node);
|
|
207
|
+
}
|
|
208
|
+
flush();
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
function looksLikeDiff(text) {
|
|
212
|
+
const lines = text.split("\n").map((l) => l.trim());
|
|
213
|
+
if (lines.some((l) => l.startsWith("diff --git") ||
|
|
214
|
+
l.startsWith("--- a/") ||
|
|
215
|
+
l.startsWith("+++ b/") ||
|
|
216
|
+
l.startsWith("@@ ")))
|
|
217
|
+
return true;
|
|
218
|
+
const nonEmpty = lines.filter((l) => l !== "");
|
|
219
|
+
if (nonEmpty.length < 3)
|
|
220
|
+
return false;
|
|
221
|
+
const markers = nonEmpty.filter((l) => /^[+\-@]/.test(l)).length;
|
|
222
|
+
return markers >= Math.max(3, Math.ceil(nonEmpty.length * 0.6));
|
|
223
|
+
}
|
|
224
|
+
function tagDiffBlock(node) {
|
|
225
|
+
if (node?.type === "code" && !node.lang && looksLikeDiff(node.value)) {
|
|
226
|
+
return { ...node, lang: "diff" };
|
|
227
|
+
}
|
|
228
|
+
return node;
|
|
136
229
|
}
|
|
137
230
|
const HR_WIDTH = 40;
|
|
138
231
|
const MAX_COL = 40;
|
|
@@ -320,11 +413,14 @@ function renderDefinition(node, _ctx) {
|
|
|
320
413
|
function renderCodeBlock(node, ctx) {
|
|
321
414
|
const theme = ctx.options.theme.blockCode || ctx.options.theme.inlineCode;
|
|
322
415
|
const lines = (node.value ?? "").split("\n");
|
|
416
|
+
const isDiff = node.lang === "diff";
|
|
323
417
|
const gutterWidth = ctx.options.codeGutter
|
|
324
418
|
? String(lines.length).length + 2
|
|
325
419
|
: 0;
|
|
326
|
-
const
|
|
327
|
-
const
|
|
420
|
+
const shouldWrap = isDiff ? false : ctx.options.codeWrap;
|
|
421
|
+
const useBox = ctx.options.codeBox && lines.length > 1;
|
|
422
|
+
const boxPadding = useBox ? 4 : 0;
|
|
423
|
+
const wrapLimit = shouldWrap && ctx.options.wrap && ctx.options.width
|
|
328
424
|
? Math.max(1, ctx.options.width - boxPadding - gutterWidth)
|
|
329
425
|
: undefined; // undefined => no hard wrap limit
|
|
330
426
|
const contentLines = lines.flatMap((line, idx) => {
|
|
@@ -340,7 +436,7 @@ function renderCodeBlock(node, ctx) {
|
|
|
340
436
|
return `${ctx.style(num, { dim: true })} ${highlighted}`;
|
|
341
437
|
});
|
|
342
438
|
});
|
|
343
|
-
if (!
|
|
439
|
+
if (!useBox) {
|
|
344
440
|
return [`${contentLines.join("\n")}\n\n`];
|
|
345
441
|
}
|
|
346
442
|
// Boxed block
|
package/package.json
CHANGED
|
@@ -1,75 +1,78 @@
|
|
|
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
|
-
|
|
2
|
+
"name": "markdansi",
|
|
3
|
+
"version": "0.1.4",
|
|
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.3.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@biomejs/biome": "^2.3.5",
|
|
71
|
+
"@types/mdast": "^4.0.4",
|
|
72
|
+
"@types/node": "^24.10.1",
|
|
73
|
+
"@vitest/coverage-v8": "^4.0.9",
|
|
74
|
+
"tsx": "^4.20.6",
|
|
75
|
+
"typescript": "^5.9.3",
|
|
76
|
+
"vitest": "^4.0.9"
|
|
77
|
+
}
|
|
78
|
+
}
|