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.
Published on npm as markdansi.
Grab it from npm; no native deps, so install is instant on Node 22+.
bun add markdansi
# or
pnpm add markdansi
# or
npm install markdansiQuick one-shot renderer: pipe Markdown in, ANSI comes out. Flags let you pick width, wrap, colors, links, and table/list styling.
markdansi [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]
[--list-indent N] [--quote-prefix STR]- Input: stdin if
--innot given (use--in -for stdin explicitly). - Output: stdout unless
--outprovided. - Wrapping: on by default;
--no-wrapdisables hard wrapping. - Links: OSC‑8 when supported;
--no-linksdisables. - Lists/quotes:
--list-indentsets spaces per nesting level (default 2);--quote-prefixsets blockquote prefix (default│).
Use the renderer directly in Node/TS for customizable theming, optional syntax highlighting hooks, and OSC‑8 link control.
Markdansi ships ESM ("type":"module"). If you’re in CommonJS (or a tool like tsx running your script as CJS), prefer dynamic import:
const { render } = await import('markdansi');
console.log(render('# hello'));For streaming output (LLM responses, logs, progress), use createLiveRenderer to re-render and redraw in-place. Uses terminal “synchronized output” when supported.
import { createLiveRenderer, render } from 'markdansi';
const live = createLiveRenderer({
renderFrame: (markdown) => render(markdown),
write: process.stdout.write.bind(process.stdout),
});
let buffer = '';
buffer += '# Hello\\n';
live.render(buffer);
buffer += '\\nMore…\\n';
live.render(buffer);
live.finish();import { render, createRenderer, strip, themes } from 'markdansi';
const ansi = render('# Hello **world**', { width: 60 });
const renderNoWrap = createRenderer({ wrap: false });
const out = renderNoWrap('A very long line...');
// Plain text (no ANSI/OSC)
const plain = strip('link to [x](https://example.com)');
// Custom theme and highlighter hook
const custom = createRenderer({
theme: {
...themes.default,
code: { color: 'cyan', dim: true }, // fallback used for inline/block
inlineCode: { color: 'red' },
blockCode: { color: 'green' },
},
highlighter: (code, lang) => code.toUpperCase(),
});
console.log(custom('`inline`\n\n```\nblock code\n```'));
// Example: real syntax highlighting with Shiki (TS + Swift)
import { bundledLanguages, bundledThemes, createHighlighter } from 'shiki';
const shiki = await createHighlighter({
themes: [bundledThemes['github-dark']],
langs: [bundledLanguages.typescript, bundledLanguages.swift],
});
const highlighted = createRenderer({
highlighter: (code, lang) => {
if (!lang) return code;
const normalized = lang.toLowerCase();
if (!['ts', 'typescript', 'swift'].includes(normalized)) return code;
const { tokens } = shiki.codeToTokens(code, {
lang: normalized === 'swift' ? 'swift' : 'ts',
theme: 'github-dark',
});
return tokens
.map((line) =>
line
.map((token) =>
token.color ? `\u001b[38;2;${parseInt(token.color.slice(1, 3), 16)};${parseInt(
token.color.slice(3, 5),
16,
)};${parseInt(token.color.slice(5, 7), 16)}m${token.content}\u001b[39m` : token.content,
)
.join(''),
)
.join('\n');
},
});
console.log(highlighted('```ts\nconst x: number = 1\n```\n```swift\nlet x = 1\n```'));wrap(defaulttrue): iffalse, no hard wrapping anywhere.width: used only whenwrap===true; default TTY columns or 80.color(default TTY):falseremoves all ANSI/OSC.hyperlinks(default auto): enable/disable OSC‑8 links.theme:default | dim | bright | solarized | monochrome | contrastor custom theme object.listIndent: spaces per nesting level (default 2).quotePrefix: blockquote line prefix (default│).tableBorder:unicode(default) |ascii|none.tablePadding: spaces inside cells (default 1);tableDensedrops extra separators.tableTruncate: cap cells to column width (defaulttrue, ellipsis…).codeBox: draw a box around fenced code (default true);codeGuttershows line numbers;codeWrapwraps code lines by default.highlighter(code, lang): optional hook to recolor code blocks; must not add/remove newlines.
- Code blocks wrap to the render width by default; disable with
codeWrap=false. Iflangis present, a faint[lang]label is shown and boxes use unicode borders. - Link/reference definitions that spill their titles onto indented lines are merged back into one line so copied notes don’t turn into boxed code.
- Tables use unicode borders by default, include padding, respect GFM alignment, and truncate long cells with
…so layouts stay tidy. Turn off truncation withtableTruncate=false. - Tight vs loose lists follow GFM; task items render
[ ]/[x].
See docs/spec.md for full behavior details.
Looking for the Swift port? Check out Swiftdansi.
MIT license.