A tiny, fast Markdown(-like) parser for when you need just the essentials. Not tied to HTML output.
Most Markdown parsers are bloated — full CommonMark implementations have edge cases you'll never use. LiteMarkup is different:
- ✅ Tiny & fast — less than 3 KB gzipped, zero dependencies, fast parsing
- ✅ AST-first — Parse once, render to anything (HTML, React, JSON, plain text...) with ease
- ✅ TypeScript — Full type safety for AST out of the box
- ✅ Simple API — No complex config, no plugins, no learning curve
Perfect for: Comment systems, chat apps, note-taking tools, or anywhere you want lightweight markup without the bloat.
💡 AST-first design: Unlike libraries that only output HTML, LiteMarkup gives you a clean typed AST. Integrate to custom output formats easily.
npm install litemarkupimport { parser, htmlRenderer } from 'litemarkup'
// Create a parser (optionally with transforms)
const parse = parser(/* options */)
// Parse markup to produce an AST
const ast = parse('# Hello *world*!')
// → [{ type: 'h', level: 1, body: [
// { type: '', txt: 'Hello ' },
// { type: 'b', body: [{ type: '', txt: 'world' }] },
// { type: '', txt: '!' }
// ]}]
// Produce output, for example with the built-in HTML rendering
const render = htmlRenderer(/* options */)
const html = render(ast)
// → <h1>Hello <b>world</b>!</h1>
// Or bring/build your own rendering: React, DOCX, PDF, ...
// See examples further belowThere's a convenience shorthand if you want to convert directly to HTML:
import { convertToHtml } from 'litemarkup'
// Note that raw html, links and images are converted to text...
convertToHtml('Click [here](/url)!')
// → '<p>Click [here]</url>!</p>'
// ... unless you allow unsafe HTML. Don’t do this on untrusted input!
convertToHtml('Click [here]<javascript:alert(`You’ve been warned!`);>!', { allowUnsafeHtml: true })
// → '<p>Click <a href="javascript:alert(`You’ve been warned!`);">here</a>!</p>'That's it. No complex config, no plugins, no 50KB bundle.
⚠️ Security note: By default, any HTML blocks, links, and images are textified.
This security measure can be disabled withhtmlRenderer({ allowUnsafeHtml: true })orconvertToHtml(src, { allowUnsafeHtml: true }).
Learn how to selectively sanitize untrusted input before doing so.
-
✅ Headings —
# H1through###### H6 -
✅ Bold, Italic & Strikethrough —
*bold*,_italic_, and~deleted~(or use markdown mode for**bold**/*italic*/~~deleted~~) -
✅ Lists — Ordered (
1.) and unordered (-or*) -
✅ Links —
[text]<url>or[text](url) -
✅ Code — Inline
`code`and fenced blocks -
✅ Tables — GFM-style pipe tables
-
✅ Blockquotes —
> quoted text -
✅ Thematic breaks —
--- -
✅ Escape characters —
\*not bold\* -
✅ Line breaks — Trailing
\
interface ParserOptions {
markdownMode?: boolean
transformBlock?: (node: Ast.Block) => Ast.Block[]
transformInline?: (node: Ast.Inline) => Ast.Inline[]
}
function parser(o: ParserOptions = {}): (src: string) => Ast.Block[]
export interface HtmlRendererOptions {
allowUnsafeHtml?: boolean
indentCharacters?: string
}
function htmlRenderer(o: HtmlRendererOptions = {}): (ast: Ast.Block[]) => stringPrefer minimal imports for only what you need?
import { parser } from 'litemarkup/parser'
import { htmlRenderer } from 'litemarkup/html'
import type { Block, Inline } from 'litemarkup/ast'By default, LiteMarkup uses *bold*, _italic_, and ~deleted~. Enable markdown mode for **bold**, *italic*, and ~~deleted~~:
import { parser, htmlRenderer } from 'litemarkup'
const parse = parser({ markdownMode: true })
const ast = parse('Hello **world** and *italic* and ~~deleted~~!')
const html = htmlRenderer()(ast)
// → '<p>Hello <b>world</b> and <i>italic</i> and <del>deleted</del>!</p>'Use transformBlock and transformInline hooks to modify the AST during parsing:
import { parser, htmlRenderer } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
// Example 1: Convert all headings to level 2
const parse = parser({
transformBlock: (node: Block): Block[] => {
if (node.type === 'h') {
return [{ ...node, level: 2 }]
}
return [node]
},
})
// Example 2: Auto-link URLs in text
const parseWithAutoLinks = parser({
transformInline: (node: Inline): Inline[] => {
if (node.type === '' && node.txt.includes('http')) {
const match = node.txt.match(/(https?:\/\/[^\s]+)/)
if (match) {
const url = match[1]
const idx = node.txt.indexOf(url)
return [
{ type: '', txt: node.txt.slice(0, idx) },
{ type: 'a', href: url, body: [{ type: '', txt: url }] },
{ type: '', txt: node.txt.slice(idx + url.length) },
]
}
}
return [node]
},
})
// Example 3: Remove a node by returning empty array
const parseNoImages = parser({
transformInline: (node: Inline): Inline[] => (node.type === 'img' ? [] : [node]),
})Strip or modify dangerous content using transforms depending on your needs. For example:
import { parser, htmlRenderer } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
const isSafeUrl = (url: string) => /^https?:\/\//.test(url)
const parse = parser({
transformBlock: (node: Block): Block[] => {
// Drop HTML blocks
if (node.type === 'htm') {
return []
}
return [node]
},
transformInline: (node: Inline): Inline[] => {
// Remove links with unsafe URLs, keep the text
if (node.type === 'a' && !isSafeUrl(node.href)) {
return node.body
}
// Remove images with unsafe URLs entirely
if (node.type === 'img' && !isSafeUrl(node.src)) {
return []
}
return [node]
},
})
// Now that input is sanitized to our liking, we can pass { allowUnsafeHtml: true }
// to let the renderer process the AST in full
htmlRenderer({ allowUnsafeHtml: true })(
parse('[safe](https://example.com) and [danger](javascript:void)'),
)
// → '<p><a href="https://example.com">safe</a> and danger</p>'The AST makes it easy to render to anything — not just HTML. For example, render directly into React components:
import { parser } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
const parse = parser()
const ast = parse('Hello *world*!')
// e.g. create a custom render that outputs React elements
function renderInline(node: Inline) {
switch (node.type) {
case '':
return node.txt
case 'a':
return <a href={node.href}>{node.body.map(renderInline)}</a>
// ... handle other inline types
}
}
function renderBlock(node: Block) {
switch (node.type) {
case 'p':
return <p>{node.body.map(renderInline)}</p>
case 'h':
return <h1>{node.body.map(renderInline)}</h1>
// ... handle other block types
}
}
// Render in your wrapper component
return <>{ast.map(renderBlock)}</>Or generate Word documents with docx:
import { Document, Paragraph, TextRun, HeadingLevel, ExternalHyperlink, Packer } from 'docx'
import { writeFileSync } from 'fs'
import { parser } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
const parse = parser()
const ast = parse('# Hello *world*!')
function renderInline(node: Inline): (TextRun | ExternalHyperlink)[] {
switch (node.type) {
case '':
return [new TextRun(node.txt)]
case 'b':
return [
new TextRun({
text: node.body.map(n => (n.type === '' ? n.txt : '')).join(''),
bold: true,
}),
]
case 'i':
return [
new TextRun({
text: node.body.map(n => (n.type === '' ? n.txt : '')).join(''),
italics: true,
}),
]
case 'a':
return [new ExternalHyperlink({ link: node.href, children: node.body.flatMap(renderInline) })]
default:
return []
}
}
function renderBlock(node: Block): Paragraph {
switch (node.type) {
case 'p':
return new Paragraph({ children: node.body.flatMap(renderInline) })
case 'h':
return new Paragraph({
heading: HeadingLevel[`HEADING_${node.level}`],
children: node.body.flatMap(renderInline),
})
default:
return new Paragraph({})
}
}
const doc = new Document({ sections: [{ children: ast.map(renderBlock) }] })
Packer.toBuffer(doc).then(buffer => {
writeFileSync('output.docx', buffer)
})LiteMarkup is designed to be forked and extended. The Extension Cookbook has full examples for some common features people might want to add.
# Heading 1
## Heading 2
*This is bold*
_This is italic_
~This is deleted~
In markdown mode: **bold**, _italic_, and ~~deleted~~
1. Ordered list
2. Second item
- Nested unordered
A [link](https://example.com) in text.
| Feature | Status |
| ---------------- | ------ |
| Basic formatting | ✅ |
| Tables | ✅ |
| Simple API | ✅ |
> A blockquote
`inline code` and:
```javascript
// fenced code block
const x = 42
```
---
Thematic break above. Force line break with backslash:\
New line here.
Use backslash to escape special characters to keep them verbatim:
\*this is not bolded — verbatim asterisks\*
* Indent anything four spaces to keep entire paragraph verbatim without using backslash escapes. *
Notable differences from CommonMark:
- Emphasis and strong emphasis use single
_and*characters, respectively. - Settext headings are not supported (use ATX headings (
# foo) instead) - Indented code blocks are not supported (use fenced code blocks (```) instead)
- Only U+000A (aka
\n/ LF) is considered line ending. - Only space and tab are considered whitespace characters.
- Tabs have no behaviour.
- Thematic breaks do not interrupt paragraph (blank line needed).
- ATX headings cannot have closing sequence.
- Entity and numeric character references are not supported
- Only type 7 HTML blocks are supported (and with some limitations)
- Link reference definitions are not supported
echo "# Hello" | npx litemarkup
# → <h1>Hello</h1>
# Pass --allow-unsafe-html to allow HTML blocks, links, and images
echo "[click](http://example.com)" | npx litemarkup --allow-unsafe-html
# → <p><a href="http://example.com">click</a></p><script src="https://unpkg.com/litemarkup/dist/litemarkup.min.iife.js"></script>
<script>
const html = litemarkup.convertToHtml('# Hello from the browser!')
document.body.innerHTML = html
</script>Bugfixes and small enhancements are welcome! This project intentionally stays minimal — if you need more features, use custom AST transformations and/or a custom renderer to extend functionality outside the core parser. See the Extension Cookbook for examples.
For development setup and guidelines, see DEV-README.md.
MIT © tuures