Technical previewShare feedback

Search

Search documentation

Overview

Updated Nov 1, 2026

Textlane is a Rust-native replacement for unified: a pipeline that parses text into a syntax tree, runs plugins over it and serializes the result. The pipeline and its most expensive plugins run in Rust; existing JavaScript plugins can still take part, called only at the stage where they run.

Installation

npm install textlane

It needs Node.js 20 or later; a prebuilt addon for Linux, macOS and Windows, x64 and arm64, comes along as an optional dependency, so no Rust toolchain is needed.

The TypeScript package

npm install textlane installs a small loader and one prebuilt napi addon for your platform, pulled in as an optional dependency, so no Rust toolchain is needed. Never install with --omit=optional. The addon is the textlane Rust crate itself: the npm package only maps its types, so Node.js gets the same pipeline as Rust and Python, at the same version.

Conformance

Compatibility is defined by a language-neutral conformance suite: JSON cases, each naming a pipeline, an input and the expected output, ported from the upstream unified, remark, rehype, retext and MDX test suites. Every implementation, TypeScript included, runs the same cases.

Status

Textlane is pre-release. textlane() builds a processor over the native pipeline, and textlane/md, textlane/html, textlane/mdx and textlane/nl export one function per plugin, typed from the crate's option structs.

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

const file = await textlane().use(md.parse()).use(md.toHtml()).use(html.stringify()).process("# hi")
file.value // "<h1>hi</h1>"

JavaScript plugins

A function passed to use is a unified attacher: called with the processor as this and its options when the processor freezes, it returns a transformer. A run pauses before each run of consecutive JavaScript plugins and hands them the tree as JSON, mdast or hast as the stage before left it; they change it in place or return a new one, directly, through a promise or through next, and the run goes on natively. processSync fails with async-in-sync on a transformer that finishes later, and one returning an Error fails the run. The file is vfile's shape: message, info and fail make vfile-messages that land in the result's messages, file.data becomes its data, and process({ value, path }) gives plugins path, dirname, stem and extname and puts path on the result. file.messages holds what native stages reported before the plugin, then what plugins reported.

const strong = () => tree => {
  tree.children[0].children[0].type = "strong"
}

await textlane().use(md.parse()).use(strong).use(md.toHtml()).use(html.stringify()).process("*a*")

Each pause serializes the tree, so a plugin costs one round trip per batch, not per node. A JavaScript plugin only transforms: syntax extensions such as remark-gfm need the native plugin.

Next steps

Created with ❤ and Livemark