slimdown-js is a lightweight, dependency-free, regex-based Markdown-to-HTML parser written in TypeScript.
It is designed for practical Markdown snippets: notes, blog posts, comments, previews, documentation fragments, and LLM-generated responses where a small package and predictable rules matter more than strict CommonMark or GitHub Flavored Markdown compliance.
import { render } from 'slimdown-js';
const html = render('# Hello World\n\nThis is **bold** text with $math$!');Use slimdown-js when you want:
- A tiny Markdown renderer with no runtime dependencies
- A parser that works in Node.js, browsers, and edge-style runtimes
- TypeScript types out of the box
- Common Markdown features such as headings, emphasis, links, lists, tables, code, math, task lists, footnotes, and definition lists
- Simple custom syntax via
addRule(regex, replacement) - Optional renderer extensions for KaTeX, Mermaid, and syntax-highlighted code blocks
- A practical renderer for trusted or already-sanitized Markdown snippets
For strict CommonMark/GFM compatibility, large documents, or full Markdown extension ecosystems, use a spec-oriented parser such as marked, markdown-it, or micromark.
npm install slimdown-jsimport { render } from 'slimdown-js';
const html = render('# Hello World\n\nThis is **bold** text with $math$!');
console.log(html);const { render } = require('slimdown-js');
const html = render('# Hello World\n\nThis is **bold** text with $math$!');
console.log(html);<script src="https://unpkg.com/slimdown-js/dist/slimdown.umd.js"></script>
<script>
const html = window.slimdownJs.render('# Browser Example');
</script><script type="module">
import { render } from 'https://esm.sh/slimdown-js';
document.body.innerHTML = render('# Hello from ESM!');
</script>For more examples, see examples.md.
slimdown-js supports a pragmatic Markdown subset plus a few useful extensions:
- Text: headings, paragraphs, blockquotes, hard line breaks
- Inline formatting: bold, emphasis, strikethrough, quotes, superscript, subscript
- Links and media: inline links, images, autolinks
- Code: inline code, fenced code blocks, optional language classes
- Lists: unordered lists, numeric ordered lists, task lists, nested lists
- Optional alpha ordered lists:
a.,A),(b)withrender(markdown, { alphaLists: true }) - Optional heading anchor IDs:
render(markdown, { headingIds: true })adds a slugifiedidto each heading - Optional semantic page breaks with
render(markdown, { pageBreaks: true }) - Tables: pipe tables, table captions, simple column spanning
- Academic and note-taking syntax: inline math, block math, footnotes, definition lists
- Escaped underscores
Because parsing is regex-based, the goal is useful, predictable coverage rather than full Markdown specification compatibility.
End a non-empty line with two or more spaces to insert a hard line break. The following line remains in the same paragraph:
render('first line \nsecond line');
// <p>first line<br>second line</p>A single newline is a soft break: consecutive prose lines remain in one paragraph without
inserting <br>. A blank line ends the paragraph.
Blockquotes use the same rules. Consecutive quoted lines stay in one blockquote: an ordinary newline is soft, while two or more trailing spaces insert exactly one hard break.
render('> first\n> second');
// <blockquote>first
// second</blockquote>
render('> first \n> second');
// <blockquote>first<br>second</blockquote>Indented and lazy prose lines after an ordered or unordered list marker remain in that list item. The same soft- and hard-break rules apply inside the item:
render('- first line \n second line');
// <ul><li>first line<br>second line</li></ul>A sibling or nested list marker, a blank line, or a code block ends the prose continuation. Trailing spaces and newlines inside fenced code blocks and inline code are preserved as code.
Page-break parsing is disabled by default because it is a slimdown-js extension rather than standard Markdown. Enable it and place the canonical marker on its own line:
render('Before\n\n<!-- markdown:page-break -->\n\nAfter', {
pageBreaks: true,
});The marker renders as a semantic <div class="md-page-break"> with EPUB and accessibility
attributes. Consumers provide print or EPUB CSS for .md-page-break, for example
break-after: page.
PAGE_BREAK_MARKER and PAGE_BREAK_HTML are exported for consumers that need the canonical
input or fallback output. Extensions can override enabled page breaks:
const extension: SlimdownExtension = {
renderPageBreak: () => '<hr class="print-page-break">',
};slimdown-js is not a sanitizer. If you render untrusted Markdown into a web page, sanitize the generated HTML with a tool such as DOMPurify.
The test suite covers the supported behavior in this package, including list continuation and optional alpha lists. It does not currently claim to pass the full CommonMark or GitHub Flavored Markdown test suites.
| Parameter | Type | Default | Description |
|---|---|---|---|
markdown |
string |
n/a | The Markdown text to render |
options.removeParagraphs |
boolean |
false |
Strip the <p> wrapper from top-level paragraphs |
options.externalLinks |
boolean |
false |
Add target="_blank" to links |
options.alphaLists |
boolean |
false |
Parse alpha ordered-list markers such as a., A), and (b) when at least two sequential markers appear in the same list run |
options.headingIds |
boolean |
false |
Add a slugified id attribute to each <h1>-<h6> heading, derived from its rendered text. Duplicate slugs within a render call get a -2, -3, ... suffix |
options.pageBreaks |
boolean |
false |
Render standalone <!-- markdown:page-break --> markers as semantic page-break elements |
options.extensions |
SlimdownExtension[] |
[] |
Optional render hooks for fenced code blocks, inline math, block math, and page breaks |
The legacy positional form render(markdown, removeParagraphs?, externalLinks?) is also supported for backwards compatibility.
The core package remains dependency-free. Heavier rendering features live in sibling packages that plug into the extensions option:
npm install slimdown-js slimdown-katex katex
npm install slimdown-js slimdown-mermaid mermaid
npm install slimdown-js slimdown-highlight highlight.jsimport { render } from 'slimdown-js';
import { katexExtension } from 'slimdown-katex';
import { mermaidExtension } from 'slimdown-mermaid';
import { highlightExtension } from 'slimdown-highlight';
const html = render(markdown, {
extensions: [
katexExtension(),
mermaidExtension(),
highlightExtension(),
],
});Extensions are tried in order. If an extension does not handle a code block, math expression, or enabled page break, slimdown-js falls back to its built-in HTML output.
import type { SlimdownExtension } from 'slimdown-js';
const customCodeBlocks: SlimdownExtension = {
renderCodeBlock: ({ lang, code, escapeHtml }) => {
if (lang !== 'demo') return undefined;
return `<figure data-lang="demo">${escapeHtml(code)}</figure>`;
},
};Adds a custom parsing rule. Rules are applied during the pre-paragraph phase.
import { addRule, render } from 'slimdown-js';
addRule(/(^|\W):\)(?=\W|$)/g, '$1<img src="smiley.png" alt=":)" />');
console.log(render("Know what I'm sayin? :)"));Function replacements are supported too:
import { addRule, render } from 'slimdown-js';
addRule(/\[\[(.*?)\]\]/g, (_match: string, title: string) => {
const slug = title.replace(/[^a-zA-Z0-9_-]+/g, '_');
return `<a href="${slug}">${title}</a>`;
});
console.log(render('Check [[This Page]] out!'));Alpha ordered lists are disabled by default to avoid accidentally parsing names or short labels such as A. Smith. Enable them per render call with alphaLists: true.
import { render } from 'slimdown-js';
const html = render(
`A. first item
1. first sub item
2. second sub item
B. second item`,
{ alphaLists: true },
);Alpha lists are only parsed when at least two sequential alpha markers are found in the same list run, such as A. followed by B., a) followed by b), or (b) followed by (c). Deeper-indented nested list items may appear between the alpha markers.
import { render } from 'slimdown-js';
console.log(render(`# A longer example
And *now* [a link](http://www.google.com) to **follow** and [another](http://yahoo.com/).
* One
* Two
* Three
## Subhead
One **two** three **four** five.
One __two__ three _four_ five __six__ seven _eight_.
1. One
2. Two
3. Three
More text with \`inline($code)\` sample.
> A block quote
> across two lines.
More text...`));import { render } from 'slimdown-js';
console.log(render('Einstein discovered $E = mc^2$ in 1905.'));
// <p>Einstein discovered <span class="math-inline">E = mc^2</span> in 1905.</p>
console.log(render(`The Gaussian integral:
$$
\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}
$$
is fundamental in mathematics.`));import { render } from 'slimdown-js';
console.log(render(`- [x] Learn markdown
- [ ] Practice LaTeX
- [X] Write documentation`));
// <ul>
// <li><input type="checkbox" checked disabled> Learn markdown</li>
// <li><input type="checkbox" disabled> Practice LaTeX</li>
// <li><input type="checkbox" checked disabled> Write documentation</li>
// </ul>import { render } from 'slimdown-js';
console.log(render(`[Sales Data 2024]
| Product | Q1 Sales | | | Q2 |
| ------- | -------- | ----- |
| Widget | $1000 | $1200 |
| Gadget | $800 | $900 |
`));import { render } from 'slimdown-js';
console.log(render(`Technology : Computer and software tools
Science : Study of the natural world`));
// <dl><dt>Technology</dt><dd>Computer and software tools</dd></dl>
// <dl><dt>Science</dt><dd>Study of the natural world</dd></dl>Try slimdown-js in the browser with the live Flems example.
npm test
npm run buildnpm test runs the AVA test suite. npm run build uses tsup and TypeScript to generate CommonJS, ESM, UMD, and declaration files.
slimdown-js started as a TypeScript port of the Johnny Broadway Slimdown gist and was also inspired by: