Skip to content

tuures/LiteMarkup

Repository files navigation

🪶 LiteMarkup

A tiny, fast Markdown(-like) parser for when you need just the essentials. Not tied to HTML output.

npm version Bundle Size License: MIT

Live Playground


Why LiteMarkup?

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.


Quick start

npm install litemarkup
import { 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 below

There'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]&lt;/url&gt;!</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 with htmlRenderer({ allowUnsafeHtml: true }) or convertToHtml(src, { allowUnsafeHtml: true }).
Learn how to selectively sanitize untrusted input before doing so.


Syntax overview

  • Headings# H1 through ###### 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 \

See language tour


API

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[]) => string

Prefer minimal imports for only what you need?

import { parser } from 'litemarkup/parser'

import { htmlRenderer } from 'litemarkup/html'

import type { Block, Inline } from 'litemarkup/ast'

Markdown compatibility mode

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

Transforming AST on-the-fly

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

Sanitizing untrusted input

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

Custom output formats

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

Extending LiteMarkup

LiteMarkup is designed to be forked and extended. The Extension Cookbook has full examples for some common features people might want to add.


Language tour

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

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


License

MIT © tuures

About

Unbloated Markdown-like parser/language with AST-first design. 3KB gzipped, zero dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Contributors