Search
Search documentation
Recipes
Updated Nov 1, 2026
Markdown to HTML, as GitHub renders it
GFM syntax with its alerts, headings that link to themselves and colored code:
import { textlane } from "textlane"
import * as html from "textlane/html"
import * as md from "textlane/md"
const file = textlane()
.use(md.parse())
.use(md.gfm({ alerts: true }))
.use(md.toHtml())
.use(html.slug({ autolink: { behavior: "wrap" } }))
.use(html.highlight({ theme: "github-dark" }))
.use(html.stringify())
.processSync("## Install\n\n> [!TIP]\n> Use pnpm.\n\n```sh\nnpm install textlane\n```")
file.value
// <h2 id="install"><a href="#install">Install</a></h2>
// <div class="markdown-alert markdown-alert-tip" dir="auto">
// …
// <pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" …
// …Untrusted Markdown to safe HTML
Keep the raw HTML a user wrote, then let html.sanitize remove whatever GitHub's schema does
not allow:
import { textlane } from "textlane"
import * as html from "textlane/html"
import * as md from "textlane/md"
const file = textlane()
.use(md.parse())
.use(md.toHtml({ allowDangerousHtml: true }))
.use(html.raw())
.use(html.sanitize())
.use(html.stringify())
.processSync("Hi <img src=x onerror=alert(1)>")
file.value
// <p>Hi <img src="x"></p>HTML to Markdown
import { textlane } from "textlane"
import * as html from "textlane/html"
import * as md from "textlane/md"
const file = textlane()
.use(html.parse({ fragment: true }))
.use(html.toMarkdown())
.use(md.gfm())
.use(md.stringify())
.processSync("<h1>Notes</h1><ul><li><s>old</s></li><li>new</li></ul>")
file.value
// # Notes
//
// * ~~old~~
// * newFormatting Markdown
Parse and stringify with the markers you prefer:
import { textlane } from "textlane"
import * as md from "textlane/md"
const file = textlane()
.use(md.parse())
.use(md.stringify({ bullet: "-", emphasis: "_", strong: "*" }))
.processSync("* *a*\n* __b__")
file.value
// - _a_
// - **b**MDX to JavaScript
import { textlane } from "textlane"
import * as md from "textlane/md"
import * as mdx from "textlane/mdx"
const file = textlane()
.use(md.parse())
.use(md.mdx())
.use(md.toHtml())
.use(mdx.compile())
.processSync("# Hi {props.name}")
file.value
// import {jsx as _jsx} from "react/jsx-runtime";
// …Checking prose in Markdown
The checks report messages and leave the text alone; each message names its rule and where it is:
import { textlane } from "textlane"
import * as md from "textlane/md"
import * as nl from "textlane/nl"
const file = textlane()
.use(md.parse())
.use(md.toNaturalLanguage())
.use(nl.passive())
.use(nl.indefiniteArticle())
.use(nl.stringify())
.processSync("# Notes\n\nThe build was broken by a update.")
const report = file.messages
.map(message => `${message.place?.start.line}:${message.place?.start.column} ${message.reason}`)
.join("\n")
report
// 3:15 Unexpected use of the passive voice
// 3:25 Unexpected article `a` before `update`, expected `an`Created with ❤ and Livemark