Aller au contenu

@gram-lang/parser

Transforme le code source .gram en un Arbre Syntaxique Abstrait (AST) constitué de simples objets JSON. C’est la seule étape du pipeline qui peut planter sur une entrée malformée : tous les packages en aval partent du principe que s’ils reçoivent un AST, c’est qu’il est structurellement valide.

function getAST(input: string): RecipeAST

Parse une chaîne source .gram et retourne le nœud racine RecipeAST. Throw une GramParseError si la syntaxe est invalide.

import { getAST } from '@gram-lang/parser';

const ast = getAST(`
---
title: 'Crêpes'
---

## Pâte

Mélanger @farine{200g} et @lait{200ml}.
`);
class GramParseError extends Error {
  readonly offset: number;
  readonly expected: string;
}

Levée (throw) par getAST en cas d’erreur de syntaxe.

ChampDescription
messageLe texte lisible d’ohm-js (extrait de la source inclus) — peut être affiché tel quel.
offsetDécalage en caractères dans input où l’échec est survenu.
expectedDescription de ce que le parser attendait à cet endroit.

offset et expected constituent la charge utile structurée de l’erreur — particulièrement utile pour les intégrations d’éditeur (soulignement, fix rapides) qui n’ont pas besoin de re-parser le message d’ohm-js.

Chaque nœud possède un discriminant type: ASTNodeType et un loc: { start, end } optionnel (décalages de caractères dans le code source, présents sur la majorité des nœuds — voir les interfaces ci-dessous).

Valeur
Recipe
Section
Step
Comment
Text
IntermediateDecl
RelativeQuantity
TextQuantity
Quantity
Ingredient
Composite
Cookware
Reference
Timer
Temperature
Alternative
ImportDecl
interface RecipeAST {
  type: ASTNodeType.Recipe;
  meta: Meta;              // Frontmatter parsé (title, date, tags, densities, ...)
  children: (SectionAST | StepAST | CommentAST)[]; // Sections, ou étapes/commentaires de premier niveau
}

interface SectionAST {
  type: ASTNodeType.Section;
  title: string | null;
  retroPlanning?: RetroPlanningAST | null;
  intermediateDecl?: IntermediateDecl | null;
  children: (StepAST | CommentAST)[];
  loc?: Location;
}

interface RetroPlanningAST {
  raw: string;             // ex : "-2h", tel qu'écrit
  sign: 1 | -1;
  value: number | null;    // null si aucun nombre n'a pu être extrait (ex : texte libre)
  unit: string | null;     // null si aucune unité trouvée ; sinon le token brut tel
                           // qu'écrit (kitchen le résout/valide ensuite contre d/h/min)
}

Pour ## Pâte Feuilletée ~{-2h}, retroPlanning vaut { raw: "-2h", sign: -1, value: 2, unit: "h" }. Du texte libre comme ~{la veille} passe le parsing avec succès (le parser ne jette jamais d’erreur pour ce cas — voir Temps & Planification pour comprendre pourquoi), mais produit { raw: "la veille", sign: 1, value: null, unit: null } ; c’est @gram-lang/kitchen qui flaggera ce cas en erreur (MISSING_UNIT) au moment de la compilation.

interface StepAST {
  type: ASTNodeType.Step;
  action?: string | null;              // ex : "Mélanger" issu de "[Mélanger] ..."
  children: (TextAST | IngredientAST | CookwareAST | TimerAST | TemperatureAST
    | ReferenceAST | AlternativeAST | IntermediateDecl | CommentAST)[];
  loc?: Location;
}

type Modifier = "?" | "-" | "*" | "&" | "="; // optionnel, masqué, % boulanger, référence, fixe

interface IngredientAST {
  type: ASTNodeType.Ingredient;
  name: string;
  modifiers: Modifier[];               // Modificateurs sous forme de sigles présents sur l'ingrédient
  quantity: QuantityAST | RelativeQuantityAST | TextQuantityAST | null;
  alias?: string | null;
  preparation?: string | null;
  composite?: CompositeAST | null;     // défini pour les ingrédients composites "<@parent"
  loc?: Location;
}

interface QuantityAST {
  type: ASTNodeType.Quantity;
  value?: QuantityValueAST;            // Union discriminée : SingleQuantityAST | FractionQuantityAST | RangeQuantityAST
  unit?: string | null;
  fixed: boolean;                      // true pour le modificateur "@=" (proportions jamais ajustées)
}

interface RelativeQuantityAST {
  type: ASTNodeType.RelativeQuantity;
  percent: number;
  target: string;
  referenceType: "variable" | "ingredient"; // "&nom" vs "@nom"
}

Les autres interfaces de nœuds (CookwareAST, ReferenceAST, TimerAST, TemperatureAST, CommentAST, AlternativeAST, IntermediateDecl, TextQuantityAST) suivent le même schéma — voir packages/parser/src/types.ts pour la liste exhaustive.

13 fonctions de garde (type guards) sont exportées pour narrow de manière sécurisée des entrées ASTNode | StepAST | ... | null | undefined, et vous éviter les vérifications manuelles du champ .type :

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('Ingrédient :', node.name);
      } else if (isTimer(node)) {
        console.log('Minuteur :', node.name, node.quantity);
      }
    }
  }
}

Coloration syntaxique : @gram-lang/parser/textmate

Section intitulée « Coloration syntaxique : @gram-lang/parser/textmate »

Un export de sous-chemin fournit la grammaire TextMate utilisée par l’extension VS Code et par les blocs de code Shiki de cette documentation :

import gramGrammar from '@gram-lang/parser/textmate' with { type: 'json' };

Il résout vers un fichier .tmLanguage.json — passez-le directement à n’importe quel colorateur syntaxique compatible avec les grammaires TextMate (Shiki, Monaco, VS Code).