Most Markdown parsers are bloated β full CommonMark implementations have edge cases youβll never use. LiteMarkup is different:
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. Build custom renderers easily.
npm install litemarkup
import { parseToAst, astToHtml } from 'litemarkup'
// Create a parser (optionally with transforms)
const parse = parseToAst()
const ast = parse('# Hello *world*!')
// β [{ name: 'h', level: 1, body: [
// { name: '', txt: 'Hello ' },
// { name: 'b', body: [{ name: '', txt: 'world' }] },
// { name: '', txt: '!' }
// ]}]
// Built-in HTML renderer
const html = astToHtml(ast)
// β <h1>Hello <b>world</b>!</h1>
// Or build your own renderer: React, DOCX, PDF, ...
// See examples below
Thatβs it. No complex config, no plugins, no 50KB bundle.
β οΈ Security note:
astToHtmlassumes input can be trusted β it renders the AST as-is. If your input is untrusted you need to sanitize the input first. For convenience, a built-in shorthandconvertToHtmlis provided that includes AST transforms to textify HTML blocks, links, and images.
import { convertToHtml } from 'litemarkup'
// Links and images are converted to text
convertToHtml('Click [here](/url)!')
// β '<p>Click here (/url)!</p>'
// HTML blocks are converted to paragraphs (which escape content automatically)
convertToHtml('<script>\nalert(1)\n</script>\n\n')
// β '<p><script>\nalert(1)\n</script></p>'
# H1 through ###### H6*bold* and _italic_ (or use markdown mode for **bold** / *italic*)1.) and unordered (*)[text]<url> or [text](url)`code` and fenced blocks> quoted text---\*not bold\*\import { parseToAst, astToHtml } from 'litemarkup'
// Create a parser (optionally with transforms)
const parse = parseToAst()
// Get the AST for custom processing
const ast = parse('Hello *world*!')
// β [{ name: 'p', body: [
// { name: '', txt: 'Hello ' },
// { name: 'b', body: [{ name: '', txt: 'world' }] },
// { name: '', txt: '!' }
// ]}]
// Then render to HTML
const html = astToHtml(ast)
By default, LiteMarkup uses *bold* and _italic_. Enable markdown mode for CommonMark-style emphasis:
import { parseToAst, astToHtml } from 'litemarkup'
const parse = parseToAst({ markdownMode: true })
const ast = parse('Hello **world** and *italic*!')
const html = astToHtml(ast)
// β '<p>Hello <b>world</b> and <i>italic</i>!</p>'
Use transformBlock and transformInline hooks to modify the AST during parsing:
import { parseToAst, astToHtml } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
// Example 1: Convert all headings to level 2
const parse = parseToAst({
transformBlock: (node: Block): Block[] => {
if (node.name === 'h') {
return [{ ...node, level: 2 }]
}
return [node]
}
})
// Example 2: Auto-link URLs in text
const parseWithAutoLinks = parseToAst({
transformInline: (node: Inline): Inline[] => {
if (node.name === '' && node.txt.includes('http')) {
const match = node.txt.match(/(https?:\/\/[^\s]+)/)
if (match) {
const url = match[1]
const idx = node.txt.indexOf(url)
return [
{ name: '', txt: node.txt.slice(0, idx) },
{ name: 'a', href: url, body: [{ name: '', txt: url }] },
{ name: '', txt: node.txt.slice(idx + url.length) }
]
}
}
return [node]
}
})
// Example 3: Remove a node by returning empty array
const parseNoImages = parseToAst({
transformInline: (node: Inline): Inline[] =>
node.name === 'img' ? [] : [node]
})
Strip or modify dangerous content using transforms. For example:
import { parseToAst, astToHtml } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
const isSafeUrl = (url: string) => /^https?:\/\//.test(url)
const parse = parseToAst({
transformBlock: (node: Block): Block[] => {
// Drop HTML blocks
if (node.name === 'htm') {
return []
}
return [node]
},
transformInline: (node: Inline): Inline[] => {
// Remove links with unsafe URLs, keep the text
if (node.name === 'a' && !isSafeUrl(node.href)) {
return node.body
}
// Remove images with unsafe URLs entirely
if (node.name === 'img' && !isSafeUrl(node.src)) {
return []
}
return [node]
}
})
astToHtml(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 { parseToAst } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
const parse = parseToAst()
const ast = parse('Hello *world*!')
// e.g. create a custom render that outputs React elements
function renderInline(node: Inline) {
switch (node.name) {
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.name) {
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 { parseToAst } from 'litemarkup'
import type { Block, Inline } from 'litemarkup'
const parse = parseToAst()
const ast = parse('# Hello *world*!')
function renderInline(node: Inline): (TextRun | ExternalHyperlink)[] {
switch (node.name) {
case '': return [new TextRun(node.txt)]
case 'b': return [new TextRun({ text: node.body.map(n => n.name === '' ? n.txt : '').join(''), bold: true })]
case 'i': return [new TextRun({ text: node.body.map(n => n.name === '' ? 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.name) {
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)
})
# Heading 1
## Heading 2
*This is bold*
_This is italic_
In markdown mode: **bold** and *italic*
1. Ordered list
2. Second item
* Nested unordered
A [link](https://example.com) in text.
> A blockquote
`inline code` and:
```javascript
// fenced code block
const x = 42
```
---
Thematic break above. Force line break with backslash:\
New line here.
Notable differences from CommonMark:
_ and * characters, respectively.# foo) instead)\n / LF) is considered line ending.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, consider forking or using custom AST transformations to extend functionality outside the core parser.
git clone https://github.com/tuures/LiteMarkup.git
cd LiteMarkup
npm install
npm test
npm run build
MIT Β© tuures