markdansi 0.1.5 → 0.1.7
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 +20 -1
- package/dist/render.js +43 -2
- package/dist/wrap.js +28 -3
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
   
|
|
8
8
|
|
|
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.
|
|
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). Includes live in-place terminal rendering for streaming updates (`createLiveRenderer`). Written in TypeScript, ships ESM.
|
|
10
10
|
|
|
11
11
|
Published on npm as `markdansi`.
|
|
12
12
|
|
|
@@ -46,6 +46,25 @@ const { render } = await import('markdansi');
|
|
|
46
46
|
console.log(render('# hello'));
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
### Live streaming / in-place rendering
|
|
50
|
+
For streaming output (LLM responses, logs, progress), use `createLiveRenderer` to re-render and redraw in-place. Uses terminal “synchronized output” when supported.
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import { createLiveRenderer, render } from 'markdansi';
|
|
54
|
+
|
|
55
|
+
const live = createLiveRenderer({
|
|
56
|
+
renderFrame: (markdown) => render(markdown),
|
|
57
|
+
write: process.stdout.write.bind(process.stdout),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let buffer = '';
|
|
61
|
+
buffer += '# Hello\\n';
|
|
62
|
+
live.render(buffer);
|
|
63
|
+
buffer += '\\nMore…\\n';
|
|
64
|
+
live.render(buffer);
|
|
65
|
+
live.finish();
|
|
66
|
+
```
|
|
67
|
+
|
|
49
68
|
```js
|
|
50
69
|
import { render, createRenderer, strip, themes } from 'markdansi';
|
|
51
70
|
|
package/dist/render.js
CHANGED
|
@@ -322,7 +322,7 @@ function renderNode(node, ctx, indentLevel, isTightList) {
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
function renderParagraph(node, ctx, indentLevel) {
|
|
325
|
-
const text = renderInline(node.children, ctx);
|
|
325
|
+
const text = normalizeParagraphInlineText(renderInline(node.children, ctx));
|
|
326
326
|
const prefix = " ".repeat(ctx.options.listIndent * indentLevel);
|
|
327
327
|
const rawLines = text.split("\n");
|
|
328
328
|
const normalized = [];
|
|
@@ -489,7 +489,7 @@ function renderInline(children, ctx) {
|
|
|
489
489
|
out += renderLink(node, ctx);
|
|
490
490
|
break;
|
|
491
491
|
case "break":
|
|
492
|
-
out +=
|
|
492
|
+
out += HARD_BREAK;
|
|
493
493
|
break;
|
|
494
494
|
default:
|
|
495
495
|
if ("value" in node && typeof node.value === "string")
|
|
@@ -498,6 +498,47 @@ function renderInline(children, ctx) {
|
|
|
498
498
|
}
|
|
499
499
|
return out;
|
|
500
500
|
}
|
|
501
|
+
const HARD_BREAK = "\u000B";
|
|
502
|
+
function normalizeParagraphInlineText(text) {
|
|
503
|
+
if (!text.includes("\n") && !text.includes(HARD_BREAK))
|
|
504
|
+
return text;
|
|
505
|
+
const segments = [];
|
|
506
|
+
let current = "";
|
|
507
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
508
|
+
const ch = text[i];
|
|
509
|
+
if (ch === "\n" || ch === HARD_BREAK) {
|
|
510
|
+
segments.push({
|
|
511
|
+
text: current,
|
|
512
|
+
breakAfter: ch === HARD_BREAK ? "hard" : "soft",
|
|
513
|
+
});
|
|
514
|
+
current = "";
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
current += ch;
|
|
518
|
+
}
|
|
519
|
+
segments.push({ text: current });
|
|
520
|
+
const defPattern = /^\[[^\]]+]:\s+\S/;
|
|
521
|
+
let out = segments[0]?.text ?? "";
|
|
522
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
523
|
+
const kind = segments[i]?.breakAfter ?? "soft";
|
|
524
|
+
const left = segments[i]?.text ?? "";
|
|
525
|
+
const right = segments[i + 1]?.text ?? "";
|
|
526
|
+
if (kind === "hard") {
|
|
527
|
+
out += "\n";
|
|
528
|
+
out += right;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const leftTrim = left.trimStart();
|
|
532
|
+
const rightTrim = right.trimStart();
|
|
533
|
+
const keepNewline = left === "" ||
|
|
534
|
+
right === "" ||
|
|
535
|
+
defPattern.test(leftTrim) ||
|
|
536
|
+
defPattern.test(rightTrim);
|
|
537
|
+
out += keepNewline ? "\n" : " ";
|
|
538
|
+
out += rightTrim;
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
501
542
|
function renderLink(node, ctx) {
|
|
502
543
|
const label = renderInline(node.children, ctx) || node.url;
|
|
503
544
|
const url = node.url || "";
|
package/dist/wrap.js
CHANGED
|
@@ -17,11 +17,36 @@ export function wrapText(text, width, wrap) {
|
|
|
17
17
|
const lines = [];
|
|
18
18
|
let current = "";
|
|
19
19
|
let currentWidth = 0;
|
|
20
|
+
const trimEndSpaces = (s) => s.replace(/\s+$/, "");
|
|
21
|
+
const orphanPhraseTail = (s) => {
|
|
22
|
+
const trimmed = trimEndSpaces(s);
|
|
23
|
+
const phrase = trimmed.match(/\b(with|in|on|of|to|for)\s+(a|an|the)$/i);
|
|
24
|
+
if (phrase) {
|
|
25
|
+
const preposition = phrase[1];
|
|
26
|
+
const article = phrase[2];
|
|
27
|
+
if (preposition && article)
|
|
28
|
+
return `${preposition} ${article}`;
|
|
29
|
+
}
|
|
30
|
+
const single = trimmed.match(/\b(a|an|the|to|of|with|and|or|in|on|for)$/i);
|
|
31
|
+
return single?.[1] ?? null;
|
|
32
|
+
};
|
|
20
33
|
for (const word of words) {
|
|
21
34
|
const w = visibleWidth(word);
|
|
22
35
|
if (current !== "" && currentWidth + w > width && !/^\s+$/.test(word)) {
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
const nextWord = word.replace(/^\s+/, "");
|
|
37
|
+
const currentNoTrail = trimEndSpaces(current);
|
|
38
|
+
const tail = orphanPhraseTail(currentNoTrail);
|
|
39
|
+
if (tail && currentNoTrail.length > tail.length) {
|
|
40
|
+
const base = trimEndSpaces(currentNoTrail.slice(0, currentNoTrail.length - tail.length));
|
|
41
|
+
if (base !== "") {
|
|
42
|
+
lines.push(base);
|
|
43
|
+
current = `${tail} ${nextWord}`;
|
|
44
|
+
currentWidth = visibleWidth(current);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
lines.push(currentNoTrail);
|
|
49
|
+
current = nextWord;
|
|
25
50
|
currentWidth = visibleWidth(current);
|
|
26
51
|
continue;
|
|
27
52
|
}
|
|
@@ -29,7 +54,7 @@ export function wrapText(text, width, wrap) {
|
|
|
29
54
|
currentWidth = visibleWidth(current);
|
|
30
55
|
}
|
|
31
56
|
if (current !== "")
|
|
32
|
-
lines.push(current);
|
|
57
|
+
lines.push(trimEndSpaces(current));
|
|
33
58
|
if (lines.length === 0)
|
|
34
59
|
lines.push("");
|
|
35
60
|
return lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markdansi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Tiny dependency-light markdown to ANSI converter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -67,12 +67,17 @@
|
|
|
67
67
|
"supports-hyperlinks": "^4.3.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@biomejs/biome": "^2.3.
|
|
70
|
+
"@biomejs/biome": "^2.3.10",
|
|
71
71
|
"@types/mdast": "^4.0.4",
|
|
72
|
-
"@types/node": "^
|
|
73
|
-
"@vitest/coverage-v8": "^4.0.
|
|
74
|
-
"tsx": "^4.
|
|
72
|
+
"@types/node": "^25.0.3",
|
|
73
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
74
|
+
"tsx": "^4.21.0",
|
|
75
75
|
"typescript": "^5.9.3",
|
|
76
|
-
"vitest": "^4.0.
|
|
76
|
+
"vitest": "^4.0.16"
|
|
77
|
+
},
|
|
78
|
+
"pnpm": {
|
|
79
|
+
"onlyBuiltDependencies": [
|
|
80
|
+
"esbuild"
|
|
81
|
+
]
|
|
77
82
|
}
|
|
78
83
|
}
|