npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cooklang-parse

v1.2.0

Published

A simple, type-safe Cooklang parser built with Ohm.js

Downloads

495

Readme

cooklang-parse

A simple, type-safe Cooklang parser built with Ohm.js

npm version CI coverage License: MIT

Features

  • Full Cooklang spec support including ingredients, cookware, timers, metadata, sections, notes, and YAML frontmatter
  • Written in TypeScript with exported type definitions
  • Single function API with extension presets — parseCooklang(source, options?)
  • 235 tests with parity coverage against cooklang-rs canonical and default parser behaviors
  • Source position tracking and parse error reporting

Installation

npm install cooklang-parse
# or
bun add cooklang-parse

Quick Start

import { parseCooklang } from "cooklang-parse"

const recipe = parseCooklang(`
>> servings: 4

Preheat #oven to 180C.

Mix @flour{250%g} and @eggs{3} in a #bowl{}.

Bake for ~{20%minutes}.
`)

recipe.metadata    // { servings: 4 }
recipe.ingredients // [{ type: "ingredient", name: "flour", quantity: 250, units: "g", fixed: false, modifiers: {...}, relation: {...} }, ...]
recipe.cookware    // [{ type: "cookware", name: "oven", quantity: 1, units: "", modifiers: {...}, relation: {...} }, ...]
recipe.timers      // [{ type: "timer", name: "", quantity: 20, units: "minutes" }]
recipe.inlineQuantities // [] in canonical mode
recipe.errors      // [] (parse errors and warnings)

// Steps are organized into sections:
recipe.sections[0].name    // null (default unnamed section)
recipe.sections[0].content // array of { type: "step", items: [...] } and { type: "text", value: "..." }

// Each step contains ordered text + inline component tokens:
const step = recipe.sections[0].content[0] // { type: "step", items: [...] }
step.items
// [
//   { type: "text", value: "Preheat " },
//   { type: "cookware", name: "oven", quantity: 1, units: "", modifiers: {...}, relation: {...} },
//   { type: "text", value: " to 180C." }
// ]

Cooklang Syntax

| Syntax | Description | Example | |--------|-------------|---------| | @name{qty%unit} | Ingredient with quantity and unit | @flour{250%g} | | @name{qty} | Ingredient with quantity only | @eggs{3} | | @name | Ingredient (implicit "some") | @salt | | @name{} | Multi-word ingredient | @olive oil{} | | #name{} | Cookware | #cast iron skillet{} | | ~name{qty%unit} | Named timer | ~resting{30%minutes} | | ~{qty%unit} | Anonymous timer | ~{20%minutes} | | -- comment | Inline comment (space required after --) | -- note to self | | [- text -] | Block comment | [- Chef's tip -] | | > text | Note | > Serve immediately | | == Title == | Section header | == For the sauce == | | >> key: value | Metadata directive | >> servings: 4 | | --- | YAML frontmatter block | See below | | @name{=qty%unit} | Fixed quantity (won't scale) | @salt{=1%tsp} | | @name{qty}(note) | Ingredient with note | @flour{100%g}(sifted) | | #name(note) | Cookware with note | #pan(large) | | @name\|alias{} | Pipe alias syntax | @ground beef\|beef{} |

API

parseCooklang(source: string, options?: ParseCooklangOptions): CooklangRecipe

Parses a Cooklang source string into a structured recipe object.

interface ParseCooklangOptions {
  extensions?: "canonical" | "all" // default: "canonical"
}
  • "canonical": canonical/spec behavior (extensions off)
  • "all": cooklang-rs default behavior (modes + inline temperature quantities)
interface CooklangRecipe {
  metadata: Record<string, unknown>
  sections: RecipeSection[]        // Sections with interleaved steps and notes
  ingredients: RecipeIngredient[]  // Deduplicated across all steps
  cookware: RecipeCookware[]       // Deduplicated across all steps
  timers: RecipeTimer[]            // Deduplicated across all steps
  inlineQuantities: Array<{ quantity: number | string; units: string }>
  errors: ParseError[]
  warnings: ParseError[]
}

interface RecipeSection {
  name: string | null              // null for the default unnamed section
  content: SectionContent[]
}

type SectionContent =
  | { type: "step"; items: RecipeStepItem[]; number?: number }
  | { type: "text"; value: string }          // Notes (> lines)

sections contains all recipe content. Each section has a name (null for the default section) and content — an interleaved array of steps and text (notes). Steps contain ordered RecipeStepItem[] arrays with text and typed tokens in document order.

ingredients, cookware, and timers are deduplicated across all steps.

Types

type RecipeStepItem =
  | { type: "text"; value: string }
  | RecipeIngredient
  | RecipeCookware
  | RecipeTimer

interface RecipeIngredient {
  type: "ingredient"
  name: string
  alias?: string            // from @name|alias{} syntax
  quantity: number | string
  units: string             // only % separator: @name{qty%unit}
  fixed: boolean
  note?: string             // from @name{}(note) syntax
  modifiers: RecipeModifiers
  relation: IngredientRelation
}

interface RecipeCookware {
  type: "cookware"
  name: string
  alias?: string
  quantity: number | string
  units: string             // always ""
  note?: string             // from #name(note) syntax
  modifiers: RecipeModifiers
  relation: ComponentRelation
}

interface RecipeTimer {
  type: "timer"
  name: string
  quantity: number | string
  units: string
}

interface ParseError {
  message: string
  shortMessage?: string
  position: { line: number; column: number; offset: number }
  severity: "error" | "warning"
}

Grammar Access

The underlying Ohm.js grammar is exported for advanced use cases:

import { grammar } from "cooklang-parse"

const match = grammar.match(source)

Example: Recipe with Frontmatter and Sections

const recipe = parseCooklang(`
---
title: Sourdough Bread
source: My grandmother
servings: 2
---

== Starter ==
Mix @starter{100%g} with @water{100%g}
Let ferment for ~{8%hours}

== Dough ==
Combine @flour{500%g} and @water{325%g}
Add @starter{200%g} and @salt{10%g}
Knead in #mixing bowl{} for ~kneading{10%minutes}
`)

recipe.metadata
// { title: "Sourdough Bread", source: "My grandmother", servings: 2 }

recipe.sections.map(s => s.name)
// [null, "Starter", "Dough"]

recipe.ingredients.map(i => `${i.quantity} ${i.units} ${i.name}`.trim())
// ["100 g starter", "100 g water", "500 g flour", ...]

Note: With YAML frontmatter (---), non-special >> key: value lines are treated as regular step text (matching cooklang-rs). In { extensions: "all" }, [mode]/[define]/[duplicate] directives still apply as configuration.

Development

bun install          # Install dependencies
bun test             # Run all 235 tests
bun run build        # Bundle + emit declarations
bun run typecheck    # Type-check without emitting
bun run lint         # Lint with Biome

License

MIT