Technical previewShare feedback

Search

Search documentation

HTML plugins

Updated Nov 1, 2026

HTML plugins parse HTML into hast, transform it and turn it into HTML, Markdown trees or natural-language trees. A Markdown pipeline reaches them through md.toHtml.

html.parse

Parses HTML into a hast tree, as a browser does: a document by default, a fragment by option.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.stringify())
  .processSync("<p>One<p>Two")
file.value
// <p>One</p><p>Two</p>

Follows rehype-parse.

OptionTypeDefaultDescription
fragmentbooleanfalseParse a fragment, as in a template, rather than a whole document.
emitParseErrorsbooleanfalseReport parse errors as messages on the file.
verbosebooleanfalseAdd the positions of opening and closing tags and attributes to elements.

html.raw

Reparses the raw HTML a Markdown document carries, so it becomes real hast nodes.

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.stringify())
  .processSync("<details><summary>More</summary>\n\n*Hidden*\n\n</details>")
file.value
// <details><summary>More</summary>
// <p><em>Hidden</em></p>
// </details>

Follows rehype-raw.

OptionTypeDefaultDescription
passThroughstring[][]Node types, unknown to hast, to copy through as they are; their children are reparsed on their own.
tagfilterbooleanfalseEscape the opening < of the tags GFM's tagfilter disallows in raw nodes.

html.format

Indents the HTML and collapses whitespace, for output a person reads.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.format())
  .use(html.stringify())
  .processSync("<ul><li>One</li><li>Two</li></ul>")
file.value
// <ul>
//   <li>One</li>
//   <li>Two</li>
// </ul>

minify: true only collapses whitespace, as rehype-minify-whitespace does, adding no line endings or indentation; minify: { newlines: true } keeps a line ending where a collapsed run held one.

Follows rehype-format.

OptionTypeDefaultDescription
blanksstring[][]Tag names of elements to put a blank line between, when both are one.
indentIndent | nullOne level of indentation: a number of spaces or the text itself; two spaces when left out.
indentInitialboolean | nullnullIndent the first level, usually <html>'s head and body; true when left out.
minifyMinify | nullOnly collapse whitespace, as rehype-minify-whitespace does, adding no line endings or indentation; { newlines: true } collapses a run holding a line ending to it.

Indent

One level of indentation.

Takes number | string.

Minify

html.format's minify: on, or on with rehype-minify-whitespace's options.

Takes boolean | MinifyOptions.

MinifyOptions

rehype-minify-whitespace's options.

OptionTypeDefaultDescription
newlinesbooleanfalseCollapse a run of whitespace holding a line ending to a line ending, not a space.

html.sanitize

Keeps only the elements and attributes a schema allows — GitHub's, by default — so untrusted HTML is safe to show.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.sanitize())
  .use(html.stringify())
  .processSync('<a href="javascript:alert(1)" onclick="x()">Hi</a><script>x()</script>')
file.value
// <a>Hi</a>

Follows rehype-sanitize.

OptionTypeDefaultDescription
allowCommentsboolean | nullnullKeep comments.
allowDoctypesboolean | nullnullKeep doctypes.
ancestorsRecord<string, string[]> | nullnullElements allowed only inside one of these ancestors, by tag name.
attributesRecord<string, AttributeDefinition[]> | nullProperties allowed per tag name, * for every element; a list names a property and the values it may have.
clobberstring[] | nullnullProperties whose values get clobber_prefix, so they cannot clobber globals.
clobberPrefixstring | nullnullThe prefix of clobbered values.
protocolsRecord<string, string[]> | nullnullURL properties and the protocols they may use.
requiredRecord<string, Record<string, Primitive>> | nullProperties set on elements whatever they had, by tag name.
stripstring[] | nullnullElements dropped with their content rather than unwrapped.
tagNamesstring[] | nullnullThe elements kept.

AttributeDefinition

A property a sanitize schema allows: by name, or a name and its allowed values.

Takes string | Primitive[].

Primitive

A primitive property value.

Takes boolean | number | string.

html.slug

Gives each heading an id from its text, as GitHub does, so it can be linked to.

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())
  .use(html.slug())
  .use(html.stringify())
  .processSync("## Getting started")
file.value
// <h2 id="getting-started">Getting started</h2>

autolink: true also links each heading to itself, as rehype-autolink-headings does after rehype-slug: an <a href="#id"> prepended by default, or placed by behavior (append, wrap, before, after). Its options take JSON values only: content and group are hast nodes, not functions.

Follows rehype-slug.

OptionTypeDefaultDescription
prefixstring""Put before each id.
reserveExistingbooleanfalseCount the ids already in the document as taken, so a generated id never repeats one a link may point at.
ranksnumber[] | nullnullThe heading ranks, 1 to 6, that get ids; every heading when left out.
autolinkAutolink | nullLink each heading with an id to itself, as rehype-autolink-headings does: true, or its options.

Autolink

html.slug's autolink: on, or on with rehype-autolink-headings' options.

Takes boolean | AutolinkOptions.

AutolinkOptions

rehype-autolink-headings' options, JSON values only.

OptionTypeDefaultDescription
behaviorAutolinkBehaviorWhere the link goes: in the heading first or last, around its content, or before or after it.
propertiesRecord<string, unknown> | nullnullThe link's properties, as hast names them; ariaHidden and tabIndex: -1 for prepend and append when left out.
contentunknownnullhast content of the link, one node or a list; <span class="icon icon-link"> when left out, except for wrap, which appends it to the heading's content.
groupunknownnullA hast element wrapping the heading and its link, for before and after.
headingPropertiesRecord<string, unknown> | nullnullProperties set on each linked heading.
testTagNames | nullThe tag names of the headings to link; every heading when left out.

AutolinkBehavior

Where autolink puts each heading's link.

Takes "prepend" | "append" | "wrap" | "before" | "after".

TagNames

One tag name or several.

Takes string | string[].

Marks links to other sites as rehype-external-links does: rel (nofollow by default), target, more properties, and content appended in a <span>. A link counts as external when its URL is absolute in one of protocols (http and https by default) or protocol-relative. Its options take JSON values only, not functions.

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())
  .use(html.externalLinks({ target: "_blank", rel: ["noopener"] }))
  .use(html.stringify())
  .processSync("See [npm](https://www.npmjs.com) or the [guide](/guide).")
file.value
// <p>See <a href="https://www.npmjs.com" rel="noopener" target="_blank">npm</a> or the <a href="/guide">guide</a>.</p>

Follows rehype-external-links.

OptionTypeDefaultDescription
targetstring | nullnullThe target of each external link, such as _blank.
relTokens | nullThe rel of each external link: a list, or tokens separated by white space; nofollow when left out or empty, none for an empty list.
protocolsstring[] | nullnullThe protocols of absolute URLs that count as external; http and https when left out. A protocol-relative //… URL always counts.
propertiesRecord<string, unknown> | nullnullMore properties for each external link, as hast names them.
contentunknownnullhast content appended to each external link in a <span>: one node or a list.
contentPropertiesRecord<string, unknown> | nullnullThe properties of that <span>.

Tokens

Tokens: a list, or one string of them separated by white space.

Takes string | string[].

html.unwrapImages

Replaces each paragraph that holds only images, links around them and white space with its content, as rehype-unwrap-images does, so images are not wrapped in <p>.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.unwrapImages())
  .use(html.stringify())
  .processSync('<p><img src="a.png"></p>')
file.value
// <img src="a.png">

Follows rehype-unwrap-images.

No options.

html.math

Turns $…$ and $$…$$ in text into the <code class="language-math math-inline"> and math-display elements md.math makes, for html.katex to render. It reads dollars by pandoc's rule, so prices stay text: an opening $ is followed by a non-space, a closing one follows a non-space and is not followed by a digit; \$ is a dollar sign. Links, code, scripts, form text, math and svg are left alone, or the elements ignore names.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.math())
  .use(html.stringify())
  .processSync(String.raw`<p>Area $\pi r^2$, price $5</p>`)
file.value
// <p>Area <code class="language-math math-inline">\pi r^2</code>, price $5</p>
OptionTypeDefaultDescription
singleDollarTextMathbooleantrueLet one dollar, $a$, form math, as well as two.
ignorestring[] | nullnullElements, by tag name, whose text is left alone; links, code, scripts, form text, math and svg when left out.
ignoreAttributesstring[][]Attributes, such as data-literal, whose elements' text is left alone.

html.linkify

Turns bare URLs and email addresses in text into links, as GitHub's literal autolinks do: an http, https or ftp URL as written, a www. one under https://, an email under mailto:. Any other scheme, such as javascript:, and a bare domain, such as index.html, stay text. Links, code, scripts, form text, math and svg are left alone, or the elements ignore names, and any element with an attribute ignoreAttributes names.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.linkify())
  .use(html.stringify())
  .processSync("<p>See www.example.com or a@example.com</p>")
file.value
// <p>See <a href="https://www.example.com">www.example.com</a> or <a href="mailto:a@example.com">a@example.com</a></p>
OptionTypeDefaultDescription
ignorestring[] | nullnullElements, by tag name, whose text is left alone; links, code, scripts, form text, math and svg when left out.
ignoreAttributesstring[][]Attributes, such as data-literal, whose elements' text is left alone.

html.gemoji

Turns gemoji shortcodes in text, :rocket:, into their emoji, as md.gemoji does in Markdown; with accessible, each sits in a <span role="img"> labelled with its name. Text that only looks like one, 10:30:00, stays as written.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.gemoji())
  .use(html.stringify())
  .processSync("<p>Ship it :rocket:</p>")
file.value
// <p>Ship it 🚀</p>
OptionTypeDefaultDescription
accessiblebooleanfalseWrap each emoji in <span role="img" aria-label="…">, labelled with its name.
ignorestring[] | nullnullElements, by tag name, whose text is left alone; links, code, scripts, form text, math and svg when left out.
ignoreAttributesstring[][]Attributes, such as data-literal, whose elements' text is left alone.

html.pathRewrite

Rewrites the URLs in href, src, srcset and poster, or the attributes attributes names, as a site serves them, as md.pathRewrite does for Markdown: a relative URL is resolved against the file's path and made root-relative, every root-relative one goes under base once, and an href naming a file in pages takes that page's URL.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.pathRewrite({ base: "/docs" }))
  .use(html.stringify())
  .processSync('<a href="/guide.html">Guide</a>')
file.value
// <a href="/docs/guide.html">Guide</a>
OptionTypeDefaultDescription
rootstring | nullnullThe directory URLs count from, as the file's path spells it; the path's own starting point when left out.
basestring | nullnullThe URL the site is served at, such as /docs, put before every root-relative URL once.
pagesRecord<string, string>{}Files, relative to root, to the URL of their page, such as {"guide.html": "/guide/"}; an href naming one takes it.
attributesstring[] | nullnullThe attributes whose URLs are rewritten; href, src, srcset and poster when left out.

html.extractToc

Stores the headings, nested by rank, as the file's data.toc, as @stefanprobst/rehype-extract-toc does: each { depth, value, id?, children? }. Run it after html.slug so headings carry their ids.

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())
  .use(html.slug())
  .use(html.extractToc())
  .use(html.stringify())
  .processSync("# Guide\n\n## Install")
file.data.toc
// [{ depth: 1, value: "Guide", id: "guide", children: [{ depth: 2, value: "Install", id: "install" }] }]

skip lists attributes that keep a heading out, such as data-toc-hidden; what would nest under it nests under the heading before. A hidden heading stays listed, which suits a heading meant for the table of contents alone.

Follows @stefanprobst/rehype-extract-toc.

OptionTypeDefaultDescription
skipstring[][]Attributes, such as data-toc-hidden, that keep a heading out of the table of contents, or name=value, such as data-toc=false, for one value of it; what would nest under it nests under the heading before.
skipInsidestring[][]Elements, by tag name, whose headings stay out, such as a card's title.
minDepthnumber | nullnullThe shallowest heading rank listed, 1 to 6.
maxDepthnumber | nullnullThe deepest heading rank listed, 1 to 6.

html.highlight

Colors code blocks with TextMate grammars and themes, as Shiki does, natively.

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())
  .use(html.highlight({ theme: "github-light" }))
  .use(html.stringify())
  .processSync("```js\nlet a = 1\n```")
file.value
// <pre class="shiki github-light" style="background-color:#fff;color:#24292e"  …

transformers runs @shikijs/transformers natively, by name or with their options: notationDiff, notationFocus, notationHighlight and notationErrorLevel read // [!code ++]-style comments, metaHighlight reads {1,3-5} after the fence's language, metaWordHighlight marks every /word/ it names and renderLineNumber numbers lines, each marking lines with Shiki's classes. inline: "tailing-curly-colon" colors inline code written `a(){:js}`, colorReplacements and addLanguageClass are Shiki's own, and metaProperty puts a fence's meta on its <pre>.

html.highlight({
  themes: { light: "github-light", dark: "github-dark" },
  transformers: ["notationDiff", { name: "renderLineNumber", start: 10 }],
})

A grammar's time grows with the square of a line's length, so a line of 2,000 characters or more, typically minified code, is left one plain token, as Shiki's tokenizeMaxLineLength leaves it; Shiki has no such limit by default, and 0 lifts it.

Follows @shikijs/rehype.

OptionTypeDefaultDescription
themesRecord<string, unknown> | nullnullThe theme of each color mode, light first, as {"light": …, "dark": …}.
themestring | nullnullThe one theme, when themes is not given.
defaultLanguagestring | nullnullThe language of a block that names none; such a block is left alone without it.
fallbackLanguagestring | nullnullThe language of a block naming one Shiki does not know; left alone without it.
colorReplacementsRecord<string, ColorReplacement>Colors to replace, by their lowercase spelling: a color for every theme, or a theme's name to a map of its own.
addLanguageClassbooleanfalseAdd language-<name> to each highlighted <code>.
inlineInlineCode | nullHighlight inline code too, by how it names its language: tailing-curly-colon reads `a(){:js}`.
metaPropertystring | nullnullThe <pre> property that gets a block's meta, the text after its language; the JSON stand-in for parseMetaString: meta => ({ [name]: meta }).
transformersHighlightTransformer[]The transformers of @shikijs/transformers to run, in order, by name or with options.
tokenizeMaxLineLengthnumber | nullnullLines at least this long, in UTF-16 units, are left one plain token as Shiki's own option leaves them; 2,000 when left out, where Shiki has no limit, since a grammar's time grows with the square of a line's length. 0 lifts it.
lineAttributesRecord<string, LineMark>Attributes of a block's <code> or <pre> listing lines, 2,4-5, each to the classes those lines get, or to {line, pre}, the second for the <pre>: {"data-focus": {"line": "focused", "pre": "has-focused"}}.
wordAttributestring | nullnullThe attribute of a block's <code> or <pre> listing words, separated by commas, marked at every place as metaWordHighlight marks a meta's /word/.
lineNumberAttributestring | nullnullThe attribute of a block's <code> or <pre> that numbers its lines as renderLineNumber does, from its value, or 1 when it is empty.
skipElementsbooleanfalseLeave a block alone when its code holds elements, such as <mark>, which highlighting would flatten into text.
keepPropertiesbooleanfalseKeep the properties of a block's <pre> and <code>, such as data-title, on the ones Shiki makes, merging classes; Shiki's own win where both set one.

ColorReplacement

A replacement color, or a theme's own replacements.

Takes string | Record<string, string>.

InlineCode

How inline code names its language.

Takes "tailing-curly-colon" | "class".

HighlightTransformer

A transformer of @shikijs/transformers: its name alone, or its name and options.

Takes TransformerName | ConfiguredTransformer.

LineMark

The classes the lines a lineAttributes attribute lists get, and maybe its <pre>.

Takes Classes | { line: Classes; pre?: Classes | null }.

TransformerName

The transformers html.highlight runs natively.

Takes "notationDiff" | "notationFocus" | "notationHighlight" | "notationErrorLevel" | "metaHighlight" | "metaWordHighlight" | "renderLineNumber".

ConfiguredTransformer

A transformer with its options, as @shikijs/transformers names them.

Takes { name: "notationDiff"; classLineAdd?: Classes | null; classLineRemove?: Classes | null; classActivePre?: Classes | null; classActiveCode?: Classes | null } | { name: "notationFocus"; classActiveLine?: Classes | null; classActivePre?: Classes | null; classActiveCode?: Classes | null } | { name: "notationHighlight"; classActiveLine?: Classes | null; classActivePre?: Classes | null; classActiveCode?: Classes | null } | { name: "notationErrorLevel"; classMap?: Record<string, Classes> | null; classActivePre?: Classes | null; classActiveCode?: Classes | null } | { name: "metaHighlight"; className?: Classes | null; zeroIndexed?: boolean } | { name: "metaWordHighlight"; className?: Classes | null } | { name: "renderLineNumber"; classLineNumber?: Classes | null; start?: number | null }.

Classes

Class names: a list, or one string of them separated by white space.

Takes string | string[].

html.katex

Renders math with KaTeX, as rehype-katex does, natively: each $…$, $$…$$ and ```math that md.math and md.toHtml leave becomes KaTeX's HTML and MathML. Pages need KaTeX's stylesheet and fonts. A formula that fails to render is reported in the file's messages and shown in errorColor.

import { textlane } from "textlane"
import * as html from "textlane/html"
import * as md from "textlane/md"

const file = textlane()
  .use(md.parse())
  .use(md.math())
  .use(md.toHtml())
  .use(html.katex())
  .use(html.stringify())
  .processSync("Euler: $e^{i\\pi} = -1$")
file.value
// <p>Euler: <span class="katex"><span class="katex-mathml"><math …

Follows rehype-katex.

OptionTypeDefaultDescription
outputKatexOutput | nullThe markup to write, both HTML and MathML by default.
leqnobooleanfalsePut equation numbers on the left.
fleqnbooleanfalseAlign display math to the left instead of centering it.
errorColorstring | nullnullThe color of math that fails to render, #cc0000 by default.
macrosRecord<string, string>{}Macros by name, each to the TeX it expands to, as {"\\RR": "\\mathbb{R}"}.
minRuleThicknessnumber | nullnullThe thinnest a rule, such as a fraction line, is drawn, in ems.
colorIsTextColorbooleanfalseLet \color color the text after it, as LaTeX does.
strictKatexStrict | nullWhat to do with input LaTeX would reject: true or "error" fails it, "warn" prints a warning, false or "ignore" (the default) accepts it.
trustbooleanfalseAllow commands that can run untrusted input, such as \href and \includegraphics.
maxSizenumber | nullnullThe largest a user-given size may be, in ems.
maxExpandnumber | nullnullHow many macro expansions one formula may take, 1000 by default.
globalGroupbooleanfalseLet definitions such as \def last past the formula they are in.
tagNamesstring[][]Elements, by tag name, that hold math besides the classed ones, such as my-math; one is display math when it has a display attribute other than inline.

KatexOutput

The markup KaTeX writes.

Takes "html" | "mathml" | "htmlAndMathml".

KatexStrict

How strictly KaTeX holds input to LaTeX.

Takes boolean | KatexStrictMode.

KatexStrictMode

One level of KaTeX's strictness.

Takes "ignore" | "warn" | "error".

html.toMarkdown

Turns hast into mdast, so HTML can be written out as 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.stringify())
  .processSync("<h2>Title</h2><p>Some <strong>bold</strong> text.</p>")
file.value
// ## Title
//
// Some **bold** text.

Follows rehype-remark.

OptionTypeDefaultDescription
documentbooleanfalseWrap all phrasing content of the root in paragraphs, as for a whole document.
newlinesbooleanfalseCollapse whitespace holding a line ending to that line ending instead of a space.
checkedstring | nullnullThe text of a checked checkbox or radio, [x] by default.
uncheckedstring | nullnullThe text of an unchecked checkbox or radio, [ ] by default.
quotesstring[] | nullnullThe quotes <q> uses, an opening and closing character each, alternating by how deep it nests; " by default.
mathbooleanfalseTurn MathML <math> holding a TeX annotation, as html.katex writes it, into math nodes, $…$ once md.math writes them, display math for display="block", and drop KaTeX's visual katex-html copy.
codeMetaRecord<string, string>{}Attributes of a code block's <pre> or <code> written into its meta, each to the key it takes there: {"data-title": "title"} makes title="a.js".

html.toNaturalLanguage

Turns hast into nlcst, so nl.* plugins can check the prose of an HTML document.

import { textlane } from "textlane"
import * as html from "textlane/html"
import * as nl from "textlane/nl"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.toNaturalLanguage())
  .use(nl.repeatedWords())
  .use(nl.stringify())
  .processSync("<p>It is the the best.</p>")
file.messages[0].reason
// Unexpected repeated `the`, remove one occurrence

Follows rehype-retext.

No options.

html.stringify

Serializes hast into HTML; its options pick quoting, closing and escaping.

import { textlane } from "textlane"
import * as html from "textlane/html"

const file = textlane()
  .use(html.parse({ fragment: true }))
  .use(html.stringify({ quote: "'" }))
  .processSync('<img alt="Logo" src="logo.svg">')
file.value
// <img alt='Logo' src='logo.svg'>

Follows rehype-stringify.

OptionTypeDefaultDescription
allowDangerousHtmlbooleanfalseWrite raw nodes as they are instead of escaping them.
allowDangerousCharactersbooleanfalseLeave characters that are only dangerous in some contexts, such as `, as they are.
allowParseErrorsbooleanfalseLeave characters as they are where browsers accept them though the spec does not.
bogusCommentsbooleanfalseWrite comments as bogus comments, <?a>.
closeEmptyElementsbooleanfalseClose empty foreign elements, <a />.
closeSelfClosingbooleanfalseClose void elements, <br />.
collapseEmptyAttributesbooleanfalseWrite empty attributes as bare names, a rather than a="".
omitOptionalTagsbooleanfalseLeave out tags the HTML spec lets a parser imply.
preferUnquotedbooleanfalseLeave attribute values unquoted where that is safe.
quotestring | nullnullThe quote attribute values are wrapped in: " or '.
quoteSmartbooleanfalseUse the other quote when the value holds more of the preferred one.
spacestring | nullnullThe space the tree is in: html or svg.
tightAttributesbooleanfalseLeave out the space between quoted attributes.
tightCommaSeparatedListsbooleanfalseJoin comma-separated values with , rather than , .
tightDoctypebooleanfalseWrite <!doctypehtml>.
tightSelfClosingbooleanfalseLeave out the space before /> when the attributes allow.
upperDoctypebooleanfalseWrite <!DOCTYPE html>.
voidsstring[] | nullnullThe tag names of void elements, which have no closing tag.

Created with ❤ and Livemark