@typstmate/typst-syntax
v0.14.2-6
Published
Typst syntax definitions and parser
Downloads
484
Maintainers
Readme
@typstmate/typst-syntax
This project is a modified TypeScript port of typst-syntax from typst/typst.
Installation
bun install @typstmate/typst-syntaxUsage
import { parse, type SyntaxNode } from '@typstmate/typst-syntax';
const code = `
#set page(width: auto, height: auto, margin: 1em)
#let vb(x) = math.bold(math.italic(x))
$
nabla dot vb(E) & = rho / epsilon_0 \
nabla times vb(E) & = - (partial vb(B)) / (partial t) \
nabla dot vb(B) & = 0 \
nabla times vb(B) & = mu_0 vb(J) + mu_0 epsilon_0 (partial vb(E)) / (partial t)
$`.trim();
const ast: SyntaxNode = parse(code);
console.log(ast)import { StateField } from '@codemirror/state';
import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
import { highlight, LinkedNode, parse, reparse, type SyntaxNode } from '@typstmate/typst-syntax';
export const typstTreeStateField = StateField.define<SyntaxNode>({
create(state) {
return parse(state.doc.toString());
},
update(value, transaction) {
if (!transaction.docChanged) return value;
const docString = transaction.state.doc.toString();
let changesCount = 0;
let changeFromA = 0;
let changeToA = 0;
let changeInsertedLen = 0;
transaction.changes.iterChanges((fromA, toA, _fromB, _toB, inserted) => {
changesCount++;
changeFromA = fromA;
changeToA = toA;
changeInsertedLen = inserted.length;
});
if (changesCount === 1) {
try {
return reparse(value, docString, { start: changeFromA, end: changeToA }, changeInsertedLen);
} catch (e) {
console.warn('Reparse failed, falling back to full parse.', e);
return parse(docString);
}
} else (changesCount !== 0) return parse(docString);
},
});
export const typstSyntaxHighlightPlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (!update.docChanged && !update.viewportChanged) return;
this.decorations = this.buildDecorations(update.view);
}
buildDecorations(view: EditorView): DecorationSet {
const tree = view.state.field(typstTreeStateField);
const { from: vpFrom, to: vpTo } = view.viewport;
const marks: { from: number; to: number; class: string }[] = [];
const traverse = (node: LinkedNode) => {
const start = node.offset;
const end = node.offset + node.len();
if (end < vpFrom || vpTo < start) return;
const cssClass = highlight(node);
if (cssClass && start < end) marks.push({ from: start, to: end, class: cssClass });
for (const child of node.children()) traverse(child);
};
traverse(LinkedNode.new(tree));
marks.sort((a, b) => a.from - b.from);
return Decoration.set(
marks.map((m) => Decoration.mark({ class: m.class }).range(m.from, m.to)),
true,
);
}
},
{
decorations: (v) => v.decorations,
},
);Extras
Getting the Syntax Context
You can retrieve the exact syntax mode and block context (e.g., SyntaxMode.Markup, SyntaxMode.Code, SyntaxMode.Math, or SyntaxMode.Plain) at any given cursor position using getSyntaxContextAt.
[!NOTE]
SyntaxMode.Plainis not part of Typst's native syntax modes. This package introduces it to represent plain text contexts such asSyntaxKind.Str,SyntaxKind.LineComment,SyntaxKind.BlockComment,SyntaxKind.Raw, andSyntaxKind.Shebang.
import { parse, getSyntaxContextAt } from '@typstmate/typst-syntax';
// Mixed markup, math, and comment
const code = `$x$$ x $ // comment`;
const tree = parse(code);
const ctxAt3 = getSyntaxContextAt(tree, 3);
const ctxAt4 = getSyntaxContextAt(tree, 4);
const ctxAtLast = getSyntaxContextAt(tree, code.length);
console.log(ctxAt3.mode); // SyntaxMode.Markup
console.log(ctxAt4.mode); // SyntaxMode.Math
console.log(ctxAtLast.mode); // SyntaxMode.Plain
console.log(ctxAt3.isBlock); // false
console.log(ctxAt4.isBlock); // true
console.log(ctxAtLast.isBlock); // false