cookcli 0.36.0

A command-line interface for managing and working with Cooklang recipes
Documentation
import { StreamLanguage } from "@codemirror/language";

// Cooklang syntax highlighting mode for CodeMirror 6
// Ported from cooklang-obsidian/src/mode/cook/cook.ts
export const cooklang = StreamLanguage.define({
  name: "cooklang",

  startState() {
    return {
      formatting: false,
      nextMultiline: false,
      inMultiline: false,
      afterSection: false,
      position: null,
      inFrontmatter: false,
      inMetadata: false,
      inNote: false,
      inComment: false,
      afterAmount: false  // Track if we just closed a {} to look for (prep)
    };
  },

  token(stream, state) {
    const sol = stream.sol() || state.afterSection;
    const eol = stream.eol();

    state.afterSection = false;

    if (sol) {
      // Reset single-line states at start of new line
      state.inNote = false;
      state.inMetadata = false;

      if (state.nextMultiline) {
        state.inMultiline = true;
        state.nextMultiline = false;
      } else {
        state.position = null;
      }
    }

    if (eol && !state.nextMultiline) {
      state.inMultiline = false;
      state.position = null;
    }

    if (sol) {
      while (stream.eatSpace()) {}
    }

    // Frontmatter delimiters (---)
    if (sol && stream.match(/^---\s*$/)) {
      state.inFrontmatter = !state.inFrontmatter;
      return "meta";
    }

    // Inside frontmatter
    if (state.inFrontmatter) {
      stream.skipToEnd();
      return "meta";
    }

    // Sections (= Section Name, == Section ==, etc.)
    if (sol && stream.match(/^=+/)) {
      stream.skipToEnd();
      return "heading";
    }

    // Line comments (-- comment)
    if (sol && stream.match(/^--/)) {
      stream.skipToEnd();
      return "comment";
    }

    // Block comments ([- comment -])
    if (stream.match(/^\[-/)) {
      state.inComment = true;
      return "comment";
    }

    if (state.inComment) {
      if (stream.match(/-]/)) {
        state.inComment = false;
        return "comment";
      }
      stream.skipToEnd();
      return "comment";
    }

    // Metadata (>> key: value)
    if (sol && stream.match(/^>>/)) {
      state.inMetadata = true;
      state.position = "metadata-key";
      return "meta";
    }

    if (state.inMetadata) {
      if (state.position === "metadata-key") {
        if (stream.match(/^[^:]+:/)) {
          state.position = "metadata-value";
          return "meta";
        }
        stream.skipToEnd();
        return "meta";
      } else if (state.position === "metadata-value") {
        stream.skipToEnd();
        return "meta";
      }
    }

    // Notes (lines starting with >)
    if (sol && stream.match(/^>/)) {
      state.inNote = true;
      return "comment";
    }

    if (state.inNote) {
      stream.skipToEnd();
      return "comment";
    }

    // Shorthand preparations (prep) after ingredient amounts like @onion{1}(chopped)
    if (state.afterAmount && stream.match(/^\([^)]*\)/)) {
      state.afterAmount = false;
      return "string";  // Use string style for prep instructions
    }

    // Reset afterAmount if we see anything else
    if (state.afterAmount && !stream.match(/^\s*(?=\()/, false)) {
      state.afterAmount = false;
    }

    // Ingredients (@ingredient{amount})
    if (stream.match(/^@([^@#~]+?(?={))/)) {
      return "variableName";
    } else if (stream.match(/^@(.+?\b)/)) {
      return "variableName";
    }

    // Cookware (#cookware{amount})
    if (stream.match(/^#([^@#~]+?(?={))/)) {
      return "keyword";
    } else if (stream.match(/^#(.+?\b)/)) {
      return "keyword";
    }

    // Timers (~timer{amount})
    if (stream.match(/^~([^@#~]+?(?={))/)) {
      return "number";
    } else if (stream.match(/^~(.+?\b)/)) {
      return "number";
    }

    // Amounts in curly braces
    const ch = stream.next();
    if (!ch) return null;

    if (ch === '{') {
      if (state.position !== "timer") state.position = "measurement";
      return null;
    }

    if (ch === '}') {
      state.position = null;
      state.afterAmount = true;  // Look for (prep) after closing brace
      return null;
    }

    if (ch === '%' && (state.position === "measurement" || state.position === "timer")) {
      state.position = "unit";
      return null;
    }

    return state.position;
  }
});