LiteMarkup

# πŸͺΆ LiteMarkup **A tiny, fast Markdown(-like) parser for when you need just the essentials. Not tied to HTML output.** [![npm version](https://img.shields.io/npm/v/litemarkup.svg)](https://www.npmjs.com/package/litemarkup) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/litemarkup)](https://bundlephobia.com/package/litemarkup) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [Live Playground](https://tuures.github.io/LiteMarkup/demopage/demopage.html) Β· [API Docs](#api) Β· [Why LiteMarkup?](#why-litemarkup)

Why LiteMarkup?

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.


Quick start

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: astToHtml assumes 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 shorthand convertToHtml is 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>&lt;script&gt;\nalert(1)\n&lt;/script&gt;</p>'

Syntax overview

See language tour


API

Basic usage

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)

Markdown compatibility mode

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>'

Transforming AST on-the-fly

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]
})

Sanitizing untrusted input

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>'

Custom renderers

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)
})


Language tour

# 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.

Try it live β†’

Notable differences from CommonMark:

Full syntax description β†’


CLI usage

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>

Quick browser usage the old-school way

<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>

Contributing

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

License

MIT Β© tuures