@gram-lang/parser
Turns .gram source text into a plain-object Abstract Syntax Tree (AST). This is the only stage of the pipeline that can fail on malformed input — every other package trusts that if it received an AST, it's structurally valid.
getAST
function getAST(input: string): RecipeASTParses a .gram source string and returns the root RecipeAST node. Throws a GramParseError on invalid syntax.
import { getAST } from '@gram-lang/parser';
const ast = getAST(`
---
title: 'Crepes'
---
## Batter
Mix @flour{200g} and @milk{200ml}.
`);GramParseError
class GramParseError extends Error {
readonly offset: number;
readonly expected: string;
}Thrown by getAST on a syntax error.
| Field | Description |
|---|---|
message | ohm-js's human-readable prose (source excerpt included) — safe to display directly. |
offset | Plain character offset into input where the failure occurred. |
expected | Description of what the parser expected at that offset. |
offset and expected are the portable, structured parts of the failure — useful for editor integrations (squiggly underlines, quick-fixes) that don't want to parse ohm's prose.
AST node types
Every node has a type: ASTNodeType discriminant and an optional loc: { start, end } (character offsets into the source, present on most but not all node types — see the interfaces below).
| Value |
|---|
Recipe |
Section |
Step |
Comment |
Text |
IntermediateDecl |
RelativeQuantity |
TextQuantity |
Quantity |
Ingredient |
Composite |
Cookware |
Reference |
Timer |
Temperature |
Alternative |
Key interfaces
interface RecipeAST {
type: ASTNodeType.Recipe;
meta: Meta; // Parsed frontmatter (title, date, tags, densities, ...)
children: (SectionAST | StepAST | CommentAST)[]; // Sections, or top-level steps/comments
}
interface SectionAST {
type: ASTNodeType.Section;
title: string | null;
retroPlanning?: RetroPlanningAST | null;
intermediateDecl?: IntermediateDecl | null;
children: (StepAST | CommentAST)[];
loc?: Location;
}
interface RetroPlanningAST {
raw: string; // e.g. "-2h", exactly as written
sign: 1 | -1;
value: number | null; // null if no number could be extracted (e.g. free text)
unit: string | null; // null if no unit token was found; otherwise the raw token
// as written (kitchen resolves/validates it against d/h/min)
}For ## Puff Pastry ~{-2h}, retroPlanning is { raw: "-2h", sign: -1, value: 2, unit: "h" }. Free text like ~{the day before} still parses (the parser never throws on this — see Times & Scheduling for why), but produces { raw: "the day before", sign: 1, value: null, unit: null }; it's @gram-lang/kitchen that flags this as invalid (MISSING_UNIT) when compiling.
interface StepAST {
type: ASTNodeType.Step;
action?: string | null; // e.g. "Mix" from "[Mix] ..."
children: (TextAST | IngredientAST | CookwareAST | TimerAST | TemperatureAST
| ReferenceAST | AlternativeAST | IntermediateDecl | CommentAST)[];
loc?: Location;
}
type Modifier = "?" | "-" | "*" | "&" | "="; // optional, hidden, baker's %, reference, fixed
interface IngredientAST {
type: ASTNodeType.Ingredient;
name: string;
modifiers: Modifier[]; // Sigil modifiers present on the ingredient
quantity: QuantityAST | RelativeQuantityAST | TextQuantityAST | null;
alias?: string | null;
preparation?: string | null;
composite?: CompositeAST | null; // set for "<@parent" composite ingredients
loc?: Location;
}
interface QuantityAST {
type: ASTNodeType.Quantity;
value?: QuantityValueAST; // Discriminated union: SingleQuantityAST | FractionQuantityAST | RangeQuantityAST
unit?: string | null;
fixed: boolean; // true for the "@=" (never-scale) modifier
}
interface RelativeQuantityAST {
type: ASTNodeType.RelativeQuantity;
percent: number;
target: string;
referenceType: "variable" | "ingredient"; // "&name" vs "@name"
}The remaining node interfaces (CookwareAST, ReferenceAST, TimerAST, TemperatureAST, CommentAST, AlternativeAST, IntermediateDecl, TextQuantityAST) follow the same pattern — see packages/parser/src/types.ts for the exhaustive list.
Type guards
13 type guards are exported for safely narrowing ASTNode | StepAST | ... | null | undefined inputs without manual .type checks:
isIngredient, isCookware, isTimer, isTemperature, isReference, isIntermediateDecl, isAlternative, isComment, isStep, isSection, isQuantity, isTextQuantity, isRelativeQuantity.
import { getAST, isIngredient, isTimer } from '@gram-lang/parser';
const ast = getAST(source);
for (const section of ast.children) {
for (const step of section.children) {
if (step.type !== 'Step') continue;
for (const node of step.children) {
if (isIngredient(node)) {
console.log('Ingredient:', node.name);
} else if (isTimer(node)) {
console.log('Timer:', node.name, node.quantity);
}
}
}
}Syntax highlighting: @gram-lang/parser/textmate
A subpath export ships the TextMate grammar used by the VS Code extension and the docs' own Shiki-powered code blocks:
import gramGrammar from '@gram-lang/parser/textmate' with { type: 'json' };It resolves to a .tmLanguage.json file — pass it straight into any TextMate-grammar-compatible highlighter (Shiki, Monaco, VS Code).