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.parseandmd.to_html - nlcst — natural language, from
nl.parse,md.to_natural_languageandhtml.to_natural_language
parse returns the tree as dicts and lists, 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. Keys keep their JavaScript names, such as tagName.
Types
The parser types the tree: after md.parse(), html.parse() or nl.parse(), parse
returns the Root of textlane.mdast, textlane.hast or textlane.nlcst, TypedDicts
named node for node as @types/mdast,
@types/hast and @types/nlcst name them, with Point and Position in textlane.unist.
They include the nodes textlane's syntax extensions add (ContainerDirective, Math,
DefListNode, MdxJsxFlowElement, MdxjsEsm, …), and MDX's data.estree is typed by
textlane.estree. Narrow a union by comparing type, and read an optional key, such as
position or lang, with get:
from textlane import md, mdast, textlane
tree: mdast.Root = textlane().use(md.parse()).parse("```py\nx = 1\n```")
for node in tree["children"]:
if node["type"] == "code":
position = node.get("position")
print(node.get("lang"), node["value"], position and position["start"]["line"])
# py x = 1 1Before a parser is used, and for a tree run returns, the tree is a plain dict. run and
stringify take any mapping, so a typed tree goes back without a cast.
from textlane import md, textlane
tree = textlane().use(md.parse()).parse("# Hi *there*")
heading = tree["children"][0]
heading["type"]
# heading
heading["depth"]
# 1
heading["children"][1]["type"]
# emphasisPositions
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.
from textlane import md, textlane
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:
from textlane import html, nl, textlane
fragment = textlane().use(html.parse({"fragment": True}))
element = fragment.parse('<p class="note">Hi</p>')["children"][0]
element["properties"]
# {'className': ['note']}
sentence = textlane().use(nl.parse()).parse("Hi there.")["children"][0]["children"][0]
" ".join(node["type"] for node in sentence["children"])
# WordNode WhiteSpaceNode WordNode PunctuationNodeA tree changed in Python goes back through run or stringify, which read it as the kind
of tree the next stage takes.
Created with ❤ and Livemark