Technical previewShare feedback

Search

Search documentation

Syntax trees

Updated Nov 1, 2026

A pipeline passes one of three trees between its stages, each a unist tree as the unified ecosystem defines it:

  • mdast — Markdown, from md.parse
  • hast — HTML, from html.parse and md.toHtml
  • nlcst — natural language, from nl.parse, md.toNaturalLanguage and html.toNaturalLanguage

parse returns the tree as plain JSON, node for node what the unified ecosystem produces, and run and stringify take it back. Every node has a type; parents have children; literals have a value. The parser types it: after md.parse(), html.parse() or nl.parse(), parse returns the Root of @types/mdast, @types/hast or @types/nlcst, which textlane depends on, and run and stringify take any unist node, so trees pass to and from unified utilities without a cast.

textlane exports those types as the unist, mdast, hast, nlcst and estree type namespaces, so import type { mdast } from "textlane" names mdast.Heading without adding @types/mdast yourself. They also hold the nodes textlane's syntax extensions make, added to the content unions as the extensions' unified utilities add them: mdast.MdxJsxFlowElement, mdast.ContainerDirective, mdast.InlineMath, mdast.DefListNode, mdast.Toml and their hast counterparts, hast.Raw and data.hName. The declarations are those utilities' own, so installing mdast-util-mdx or mdast-util-directive beside textlane does not conflict.

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

const tree = textlane().use(md.parse()).parse("# Hi *there*")
const heading = tree.children[0]
heading.type
// heading
heading.depth
// 1
heading.children[1].type
// emphasis

Positions

Each node parsed from text carries a position: its start and end points, each with a 1-indexed line and column and a 0-indexed offset.

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

const tree = textlane().use(md.parse()).parse("# Hi *there*")
tree.children[0].children[1].position.start
// {"line":1,"column":6,"offset":5}

hast and nlcst

hast elements carry a tagName and properties, named as the DOM names them; nlcst nests paragraphs, sentences and words:

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

const element = textlane()
  .use(html.parse({ fragment: true }))
  .parse('<p class="note">Hi</p>').children[0]
element.properties
// {"className":["note"]}
const sentence = textlane().use(nl.parse()).parse("Hi there.").children[0].children[0]
sentence.children.map(node => node.type).join(" ")
// WordNode WhiteSpaceNode WordNode PunctuationNode

A tree changed in JavaScript goes back through run or stringify, which read it as the kind of tree the next stage takes.

Created with ❤ and Livemark