syntree

Incremental syntax tree and highlighting toolkit for MoonBit

syntax
tree
highlight
moonbit
moon add mizchi/syntree@0.2.4
Download zip
Author
Version
0.2.4
License
MIT
Last updated
18 hours ago
Downloads
72K
README

#syntree.mbt

Incremental syntax tree and highlighting toolkit for MoonBit.

#Packages

  • mizchi/syntree - core tree, cursor, highlight APIs, and extension mapping
  • mizchi/syntree/<lang> - language tokenizers + highlighters
  • mizchi/syntree/highlight - HTML rendering with inline styles
  • mizchi/syntree/syntree_api - JS exports for the bundled highlighters

#Supported Languages (31)

bash, c, cpp, csharp, css, dart, dockerfile, go, graphql, haskell, html, java, json, kotlin, lua, makefile, mdx, moonbit, ocaml, php, python, ruby, rust, scala, sql, swift, toml, typescript, xml, yaml, zig

#MoonBit Usage

#DCE-friendly selective imports

Import only the languages you need for tree-shaking (Dead Code Elimination):

import {
"mizchi/syntree" // core + extension mapping
"mizchi/syntree/typescript" // TypeScript/JavaScript only
"mizchi/syntree/rust" // Rust only
}

fn main {
// Get language from filename
let lang = @syntree.get_language_for_filename("app.tsx") // Some("typescript")

// Highlight based on detected language
let source = "const x: number = 1"
let html = match lang {
Some("typescript") => @typescript.highlight_typescript_to_html(source)
Some("rust") => @rust.highlight_rust_to_html(source)
_ => source
}
println(html)
}

#Extension Mapping API

// Get language name from file extension
@syntree.get_language_for_ext(".ts") // Some("typescript")
@syntree.get_language_for_ext(".rs") // Some("rust")
@syntree.get_language_for_ext(".unknown") // None

// Get language name from filename (handles special files)
@syntree.get_language_for_filename("Dockerfile") // Some("dockerfile")
@syntree.get_language_for_filename("Makefile") // Some("makefile")
@syntree.get_language_for_filename("main.go") // Some("go")

// List all supported languages
@syntree.supported_languages() // ["bash", "c", "cpp", ...]

// Get extensions for a language
@syntree.get_extensions_for_language("typescript") // [".js", ".jsx", ".ts", ".tsx", ...]

#Incremental Highlighting with LineCache

LineCache provides line-based caching for efficient incremental syntax highlighting. It works with any language by accepting a highlight function:

import { "mizchi/syntree", "mizchi/syntree/typescript" }

fn main {
let source = "const x = 1\nlet y = 2"

// Create LineCache with a highlight function
let cache = @syntree.LineCache::new(source, @typescript.highlight_typescript)

// Get tokens for a specific line
let line0_tokens = cache.get_line_tokens(0)

// Get all tokens flattened
let all_tokens = cache.all_tokens()

// Update on edit (re-tokenizes from affected line)
let new_source = "const x = 1\nlet y = 'hello'"
let (start_line, end_line) = cache.update(new_source, 1, @typescript.highlight_typescript)

// Full rebuild
cache.rebuild("new source", @typescript.highlight_typescript)
}

LineCache API:

MethodDescription
LineCache::new(source, highlight_fn)Create cache from source
line_count()Number of lines
get_line_tokens(line)Tokens for a specific line
all_tokens()All tokens flattened
get_source()Current source text
update(new_source, edit_line, highlight_fn)Incremental update, returns affected range
rebuild(new_source, highlight_fn)Full rebuild

#JS usage

js/syntree_api.js wraps the MoonBit JS build output and exposes convenience helpers.

import { highlight, highlightTypeScript } from "./js/syntree_api.js"; const html = highlight("const x = 1", "ts"); const html2 = highlightTypeScript("const x = 1");

#Reference implementation

  • Lezer: https://lezer.codemirror.net/

#Development

just check just test just bench just info

#
CursorFrame

type CursorFrame

Stack frame for tree traversal

#
HighlightTag

pub(all) enum HighlightTag {
Keyword
Operator
Punctuation
String
Number
Regexp
Bool
Null
PropertyName
VariableName
FunctionName
TypeName
ClassName
PrivateName
Meta
Bracket
Brace
Paren
Comment
DocComment
TagName
TagBracket
Invalid
None
} derive(Eq,
Debug
)

Standard highlight tags for syntax highlighting

#
HighlightTag::to_class

fn HighlightTag::to_class(self : HighlightTag) -> String

Convert HighlightTag to CSS class name

#
HighlightToken

pub(all) struct HighlightToken {
from : Int
to : Int
tag : HighlightTag
} derive(Eq,
Debug
)

A highlighted span of text

#
HighlightToken::new

fn HighlightToken::new(from : Int, to : Int, tag : HighlightTag) -> HighlightToken

Create a highlight token

#
Highlighter

pub(all) struct Highlighter {
rules : Map[String, HighlightTag]
}

Highlighter configuration - maps node types to highlight tags

#
Highlighter::add_rule

fn Highlighter::add_rule(self : Highlighter, node_name : String, tag : HighlightTag) -> Unit

Add a highlighting rule

#
Highlighter::get_tag

fn Highlighter::get_tag(self : Highlighter, node_name : String) -> HighlightTag

Get highlight tag for a node type

#
Highlighter::highlight

fn Highlighter::highlight(self : Highlighter, tree : Tree) -> Array[HighlightToken]

Generate highlight tokens from a tree

#
Highlighter::new

Create a new highlighter

#
LineCache

pub(all) struct LineCache {
lines : Array[Array[HighlightToken]]
line_starts : Array[Int]
source : String
}

Line-based cache for incremental syntax highlighting Works with any language by accepting a highlight function

#
LineCache::all_tokens

fn LineCache::all_tokens(self : LineCache) -> Array[HighlightToken]

Get all tokens flattened

#
LineCache::get_line_tokens

fn LineCache::get_line_tokens(self : LineCache, line : Int) -> Array[HighlightToken]

Get tokens for a specific line

#
LineCache::get_source

fn LineCache::get_source(self : LineCache) -> String

Get source text

#
LineCache::line_count

fn LineCache::line_count(self : LineCache) -> Int

Get number of lines

#
LineCache::new

fn LineCache::new(source : String, highlight_fn : (String) -> Array[HighlightToken]) -> LineCache

Create a new line cache from source

#
LineCache::rebuild

fn LineCache::rebuild(self : LineCache, new_source : String, highlight_fn : (String) -> Array[HighlightToken]) -> Unit

Rebuild the entire cache with new source

#
LineCache::update

fn LineCache::update(self : LineCache, new_source : String, edit_line : Int, highlight_fn : (String) -> Array[HighlightToken]) -> (Int, Int)

Update cache when source changes Re-tokenizes from the edited line onwards Returns the range of lines that were affected (start_line, end_line)

#
NodeType

pub(all) struct NodeType {
id : Int
name : String
is_error : Bool
} derive(Eq,
Debug
)

Node type identifier with metadata
impl Show for NodeType

#
NodeType::error

fn NodeType::error(id : Int) -> NodeType

Create an error node type

#
NodeType::new

fn NodeType::new(id : Int, name : String) -> NodeType

Create a new node type

#
Tree

pub(all) enum Tree {
Node(node_type~ : NodeType, from~ : Int, to~ : Int, children~ : Array[Tree])
Leaf(node_type~ : NodeType, from~ : Int, to~ : Int)
Buffered(buffer~ : TreeBuffer, from~ : Int, to~ : Int)
}

  • Buffer-backed subtrees for compact storage of many small nodes

#
Tree::children

fn Tree::children(self : Tree) -> Array[Tree]

Get children (empty for Leaf and Buffered)

#
Tree::from

fn Tree::from(self : Tree) -> Int

Get the start position of a tree

#
Tree::iter

fn Tree::iter(self : Tree) -> Iter[Tree]

Iterate over all nodes in depth-first order

#
Tree::leaf

fn Tree::leaf(node_type : NodeType, from : Int, to : Int) -> Tree

Create a leaf node

#
Tree::length

fn Tree::length(self : Tree) -> Int

Get the length of the tree

#
Tree::node

fn Tree::node(node_type : NodeType, from : Int, to : Int, children : Array[Tree]) -> Tree

Create a node with children

#
Tree::node_type

fn Tree::node_type(self : Tree) -> NodeType?

Get the node type (returns None for Buffered trees)

#
Tree::resolve

fn Tree::resolve(self : Tree, pos : Int) -> Tree?

Find the deepest node containing a position

#
Tree::to

fn Tree::to(self : Tree) -> Int

Get the end position of a tree

#
TreeBuffer

pub(all) struct TreeBuffer {
data : Array[Int]
node_types : Array[NodeType]
}

This is inspired by compact tree buffer representations (Uint16Array style).

#
TreeBuffer::get_node

fn TreeBuffer::get_node(self : TreeBuffer, index : Int) -> (Int, Int, Int, Int)?

Get node info at index

#
TreeBuffer::new

fn TreeBuffer::new(node_types : Array[NodeType]) -> TreeBuffer

Create an empty TreeBuffer

#
TreeBuffer::node_count

fn TreeBuffer::node_count(self : TreeBuffer) -> Int

Number of nodes in the buffer

#
TreeBuffer::push_leaf

fn TreeBuffer::push_leaf(self : TreeBuffer, type_id : Int, from : Int, to : Int) -> Unit

Append a leaf node (no children)

#
TreeCursor

pub(all) struct TreeCursor {
root : Tree
// private fields
}

Allows walking the tree without allocating new node objects.

#
TreeCursor::at_root

fn TreeCursor::at_root(self : TreeCursor) -> Bool

Check if cursor is at root

#
TreeCursor::depth

fn TreeCursor::depth(self : TreeCursor) -> Int

Get depth in tree (0 = root)

#
TreeCursor::first_child

fn TreeCursor::first_child(self : TreeCursor) -> Bool

Move to first child, returns false if no children

#
TreeCursor::from

fn TreeCursor::from(self : TreeCursor) -> Int

Get current node's start position

#
TreeCursor::name

fn TreeCursor::name(self : TreeCursor) -> String

Get current node's name

#
TreeCursor::new

fn TreeCursor::new(tree : Tree) -> TreeCursor

Create a cursor at the root of a tree

#
TreeCursor::next_sibling

fn TreeCursor::next_sibling(self : TreeCursor) -> Bool

Move to next sibling, returns false if no more siblings

#
TreeCursor::node_type

fn TreeCursor::node_type(self : TreeCursor) -> NodeType?

Get current node's type

#
TreeCursor::parent

fn TreeCursor::parent(self : TreeCursor) -> Bool

Move to parent, returns false if at root

#
TreeCursor::reset

fn TreeCursor::reset(self : TreeCursor) -> Unit

Reset cursor to root

#
TreeCursor::to

fn TreeCursor::to(self : TreeCursor) -> Int

Get current node's end position

#
escape_html

fn escape_html(s : String) -> String

Escape HTML special characters

#
escape_html_slice_to

fn escape_html_slice_to(chars : Array[Char], from : Int, to : Int, buf : StringBuilder) -> Unit

Escape HTML from char slice directly to StringBuilder (zero-alloc)

#
get_extensions_for_language

fn get_extensions_for_language(lang : String) -> Array[String]

Get all extensions for a language name

#
get_language_for_ext

fn get_language_for_ext(ext : String) -> String?

Get language name for a file extension (with leading dot) Returns None if the extension is not recognized

#
get_language_for_filename

fn get_language_for_filename(filename : String) -> String?

Get language name for a filename (handles special files without extensions)

#
supported_languages

fn supported_languages() -> Array[String]

List of all supported language names

#
tokens_to_html

fn tokens_to_html(source : String, tokens : Array[HighlightToken]) -> String

Generate highlighted HTML from source and tokens Note: tokens are assumed to be already sorted by position (as produced by tokenizers)

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io