Thanks to visit codestin.com
Credit goes to pkg.go.dev

cooklang

package module
v1.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 22, 2026 License: MIT Imports: 10 Imported by: 0

README

CI - Build and Test

cooklang

Go implementation of a cooklang parser.

Features

  • ✅ Full Cooklang specification compliance
  • 🖼️ Automatic image detection - Auto-discovers recipe images matching filename patterns
  • 📝 Frontmatter CRUD operations - Programmatically edit recipe metadata
  • 🧮 Unit conversion system with metric/imperial/US systems
  • 📋 Shopping list generation from multiple recipes
  • 🎨 Multiple output formats (Cooklang, Markdown, HTML, JSON)
  • 🔧 Extended mode with ingredient/cookware annotations
  • ⚖️ Recipe scaling and ingredient consolidation
  • 🛠️ Comprehensive CLI tool

Usage Examples

The library includes comprehensive, runnable examples demonstrating all major features. See docs/EXAMPLES.md for a complete list.

Quick example:

package main

import (
    "fmt"
    "log"
    "github.com/hilli/cooklang"
)

func main() {
    recipeText := `---
title: Pasta Aglio e Olio
servings: 2
---
Cook @pasta{400%g} in salted water for ~{10%minutes}.
Meanwhile, heat @olive oil{4%tbsp} and sauté @garlic{3%cloves}.
Toss everything together and serve.`

    recipe, err := cooklang.ParseString(recipeText)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Recipe: %s (serves %.0f)\n", recipe.Title, recipe.Servings)
    
    // Get shopping list
    shoppingList, _ := recipe.GetCollectedIngredientsMap()
    for ingredient, quantity := range shoppingList {
        fmt.Printf("- %s: %s\n", ingredient, quantity)
    }
}

Known Usages

Projects using this library:

Using this library? Open a PR to add your project!

Cooklang specification

See the Cooklang specification for details.

Extended Syntax

This parser supports additional syntax extensions beyond the base Cooklang specification:

Ingredient Annotations

Ingredients can have annotations in parentheses to specify preparation notes or state:

@milk{1%l}(cold)

The annotation (cold) is stored as the ingredient's value field.

Cookware Annotations

Similarly, cookware items can have annotations for usage hints:

#pan{}(for frying)
Named Timers

Timers can have descriptive multi-word names:

~roast time{4%hours}
Comment Preservation

Comments are preserved as a distinct type in the parsed output rather than being discarded:

-- This is a comment

Comments are accessible with type: comment and their text in the value field.

Developing

Prerequisites (Well, not really)

This project uses a Taskfile for convenience. Install by running:

go install tool
Running Tests

Run all tests with:

task test

Test the specification specifically with:

task test-spec

Lint the stuff:

task lint

Documentation

Overview

Package cooklang provides a parser and tools for working with Cooklang recipe files.

Cooklang is a markup language for cooking recipes that makes it easy to manage recipes as plain text files while providing rich semantic information about ingredients, cookware, timers, and instructions.

Basic Usage

Parse a recipe file:

recipe, err := cooklang.ParseFile("lasagna.cook")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Recipe: %s (serves %.0f)\n", recipe.Title, recipe.Servings)

Working with Ingredients

Extract and consolidate ingredients for shopping lists:

ingredients := recipe.GetIngredients()
consolidated, err := ingredients.ConsolidateByName("")
if err != nil {
    log.Fatal(err)
}

for _, ing := range consolidated.Ingredients {
    fmt.Printf("- %s: %.1f %s\n", ing.Name, ing.Quantity, ing.Unit)
}

Unit Conversion

Convert ingredients between measurement systems:

// Convert to metric
shoppingList, err := recipe.GetMetricShoppingList()

// Convert to US customary
shoppingList, err := recipe.GetUSShoppingList()

// Convert individual ingredients
ingredient := &cooklang.Ingredient{Name: "flour", Quantity: 2, Unit: "cup"}
converted, err := ingredient.ConvertTo("g")

Shopping Lists

Create shopping lists from multiple recipes:

recipe1, _ := cooklang.ParseFile("pasta.cook")
recipe2, _ := cooklang.ParseFile("salad.cook")
recipe3, _ := cooklang.ParseFile("dessert.cook")

shoppingList, err := cooklang.CreateShoppingList(recipe1, recipe2, recipe3)
if err != nil {
    log.Fatal(err)
}

// Scale for meal prep
doubled := shoppingList.Scale(2.0)

// Print the list
for ingredient, amount := range doubled.ToMap() {
    fmt.Printf("☐ %s: %s\n", ingredient, amount)
}

Metadata Management

Edit recipe frontmatter metadata:

editor, err := cooklang.NewFrontmatterEditor("recipe.cook")
if err != nil {
    log.Fatal(err)
}

editor.SetMetadata("title", "Improved Lasagna")
editor.SetMetadata("servings", "8")
editor.SetMetadata("tags", "italian, pasta, main course")
editor.SetMetadata("difficulty", "medium")

if err := editor.Save(); err != nil {
    log.Fatal(err)
}

Rendering Recipes

Render recipes in different formats:

import "github.com/hilli/cooklang/renderers"

recipe, _ := cooklang.ParseFile("recipe.cook")

// Render as Markdown
markdown := recipe.RenderWith(renderers.MarkdownRenderer{})
fmt.Println(markdown)

// Render as HTML
html := recipe.RenderWith(renderers.HTMLRenderer{})

// Set a custom renderer
recipe.SetRenderer(renderers.CooklangRenderer{})
cooklangText := recipe.Render()

Recipe Structure

Recipes are organized as linked lists of steps, where each step contains a linked list of components (ingredients, instructions, timers, cookware). This structure allows for efficient traversal and manipulation:

// Walk through all steps
currentStep := recipe.FirstStep
stepNum := 1
for currentStep != nil {
    fmt.Printf("Step %d:\n", stepNum)

    // Walk through components in this step
    component := currentStep.FirstComponent
    for component != nil {
        switch c := component.(type) {
        case *cooklang.Ingredient:
            fmt.Printf("  Add %s (%.1f %s)\n", c.Name, c.Quantity, c.Unit)
        case *cooklang.Instruction:
            fmt.Printf("  %s\n", c.Text)
        case *cooklang.Timer:
            fmt.Printf("  Wait for %s %s\n", c.Duration, c.Unit)
        case *cooklang.Cookware:
            fmt.Printf("  Using: %s\n", c.Name)
        }
        component = component.GetNext()
    }

    currentStep = currentStep.NextStep
    stepNum++
}

Cooklang Syntax

The parser supports standard Cooklang syntax:

  • Ingredients: @flour{500%g}, @salt{}, @milk{2%cups}(room temperature)
  • Cookware: #pot{}, #bowl{2}, #oven{}(preheated)
  • Timers: ~{10%minutes}, ~boil{15%min}
  • Comments: -- This is a comment
  • Metadata: YAML frontmatter between --- delimiters

See https://cooklang.org for full specification details.

Index

Examples

Constants

View Source
const (
	// Volume conversions (bartender-friendly)
	MlPerOz       = 30.0  // Bartender standard (scientific: 29.5735)
	MlPerTbsp     = 15.0  // 1 tablespoon
	MlPerTsp      = 5.0   // 1 teaspoon
	MlPerCup      = 240.0 // Standard US cup
	MlPerDash     = 0.92  // ~1ml, standard bar dash
	MlPerSplash   = 7.5   // Roughly 1/4 oz
	MlPerBarspoon = 5.0   // Same as teaspoon
	MlPerJigger   = 45.0  // Standard jigger (1.5 oz)
	MlPerPony     = 30.0  // Pony shot (1 oz)

	// Scientific conversions for PreciseMode
	MlPerOzPrecise = 29.5735
)

Bartender-friendly conversion constants These are the practical conversions used by bartenders, not scientific ones

View Source
const DefaultFractionTolerance = 0.02

DefaultFractionTolerance is the default tolerance for matching fractions

Variables

This section is empty.

Functions

func CreateTypedUnit

func CreateTypedUnit(unitStr string) *units.Unit

CreateTypedUnit attempts to find a unit in go-units or creates a new one if not found. This function is used internally when parsing Cooklang content and can be used externally when programmatically creating Ingredient structs that need unit conversion support.

If the unit string is empty, nil is returned. If the unit is found in the go-units library, a pointer to that unit is returned. Otherwise, a new unit is created with the given string as both name and symbol.

func FormatAsFraction

func FormatAsFraction(value float64, tolerance float64) string

FormatAsFraction converts a float to a human-readable fraction string. It handles whole numbers, simple fractions, and mixed numbers.

Examples:

  • 0.5 → "1/2"
  • 2.5 → "2 1/2"
  • 0.0833 → "1/12"
  • 3.0 → "3"
  • 1.234 → "1.23" (fallback for non-standard values)

Parameters:

  • value: The numeric value to format
  • tolerance: How close a value must be to a fraction to match (e.g., 0.02 = 2%)

Returns:

  • A human-readable string representation

func FormatAsFractionDefault

func FormatAsFractionDefault(value float64) string

FormatAsFractionDefault uses the default tolerance for fraction matching. This is a convenience wrapper around FormatAsFraction with DefaultFractionTolerance.

Parameters:

  • value: The numeric value to format

Returns:

  • A human-readable string representation

Example:

cooklang.FormatAsFractionDefault(0.5)  // "1/2"
cooklang.FormatAsFractionDefault(2.25) // "2 1/4"
cooklang.FormatAsFractionDefault(3.0)  // "3"

func FormatBartenderValue

func FormatBartenderValue(result SmartUnitResult) string

FormatBartenderValue formats a SmartUnitResult for display with fractions and pluralization. Uses fraction formatting (e.g., "1 1/2" instead of "1.5") and handles unit pluralization.

Parameters:

  • result: The SmartUnitResult to format

Returns:

  • string: Formatted string (e.g., "1 1/2 oz", "3 dashes")

Example:

result := cooklang.SmartUnitResult{Value: 1.5, Unit: "oz"}
fmt.Println(cooklang.FormatBartenderValue(result)) // "1 1/2 oz"

func IsCocktailSpecificUnit

func IsCocktailSpecificUnit(unitName string) bool

IsCocktailSpecificUnit returns true if the unit is cocktail-specific (dash, splash, etc.). Cocktail-specific units are universal and should not be converted between systems.

Parameters:

  • unitName: The unit to check

Returns:

  • bool: true if the unit is cocktail-specific

Example:

cooklang.IsCocktailSpecificUnit("dash")   // true
cooklang.IsCocktailSpecificUnit("oz")     // false

func IsNiceFraction

func IsNiceFraction(value float64, tolerance float64) bool

IsNiceFraction checks if a value is close to a common fraction. This is useful for determining if a value will format nicely as a fraction.

Parameters:

  • value: The numeric value to check
  • tolerance: How close to a fraction the value must be (e.g., 0.02 = 2%)

Returns:

  • bool: true if the value matches a common fraction within tolerance

Example:

cooklang.IsNiceFraction(0.5, 0.02)   // true (1/2)
cooklang.IsNiceFraction(0.333, 0.02) // true (1/3)
cooklang.IsNiceFraction(0.37, 0.02)  // false

func ParseFraction

func ParseFraction(s string) (float64, error)

ParseFraction parses a fraction string into a float64. Handles multiple formats: simple fractions ("1/2"), mixed numbers ("2 1/2"), decimals ("0.5"), and integers ("2").

Parameters:

  • s: The string to parse

Returns:

  • float64: The parsed numeric value
  • error: An error if the string cannot be parsed

Example:

cooklang.ParseFraction("1/2")     // 0.5, nil
cooklang.ParseFraction("2 1/2")   // 2.5, nil
cooklang.ParseFraction("0.75")    // 0.75, nil
cooklang.ParseFraction("invalid") // 0, error

func ParseYield added in v1.0.5

func ParseYield(yieldStr string) (float64, string)

ParseYield parses a yield metadata value in Cooklang format (e.g., "500%ml", "2%loaves"). Returns quantity and unit, or 0 and empty string if parsing fails.

func RoundToNiceFraction

func RoundToNiceFraction(value float64, tolerance float64) float64

RoundToNiceFraction rounds a value to the nearest common fraction. This is useful for bartender mode where clean measurements are preferred. If no common fraction is within tolerance, the original value is returned.

Parameters:

  • value: The numeric value to round
  • tolerance: Maximum difference to accept for rounding (e.g., 0.02 = 2%)

Returns:

  • float64: The rounded value, or original if no good match

Example:

cooklang.RoundToNiceFraction(0.48, 0.05)  // 0.5 (rounds to 1/2)
cooklang.RoundToNiceFraction(0.26, 0.02)  // 0.25 (rounds to 1/4)
cooklang.RoundToNiceFraction(0.37, 0.02)  // 0.37 (no good match)

func ScaleByYield added in v1.0.5

func ScaleByYield(recipe *Recipe, targetQuantity float64, targetUnit string) (float64, error)

ScaleByYield calculates a scaling factor for a recipe reference based on yield metadata. If the recipe has yield metadata matching the requested unit, it calculates targetQuantity / yieldQuantity as the scaling factor.

This is an experimental feature per the Cooklang spec.

func ShouldSkipConversion

func ShouldSkipConversion(sourceUnit string, targetSystem UnitSystem) bool

ShouldSkipConversion returns true if unit conversion should be skipped. Conversion is skipped when the source unit is already in the target system, or when the unit is cocktail-specific (universal units like dash, splash).

Parameters:

  • sourceUnit: The current unit
  • targetSystem: The desired unit system

Returns:

  • bool: true if conversion should be skipped

Example:

cooklang.ShouldSkipConversion("ml", cooklang.UnitSystemMetric) // true (already metric)
cooklang.ShouldSkipConversion("dash", cooklang.UnitSystemUS)   // true (cocktail-specific)
cooklang.ShouldSkipConversion("ml", cooklang.UnitSystemUS)     // false (needs conversion)

Types

type CocktailUnit

type CocktailUnit struct {
	Name       string     // Primary name (e.g., "dash")
	Aliases    []string   // Alternative names
	MlValue    float64    // Value in milliliters
	USValue    float64    // Value in fluid ounces (for bartender mode)
	System     UnitSystem // Which system this unit belongs to
	IsCocktail bool       // True if this is a cocktail-specific unit (dash, splash, etc.)
}

CocktailUnit represents a unit commonly used in cocktail recipes

func GetCocktailUnit

func GetCocktailUnit(name string) *CocktailUnit

GetCocktailUnit looks up a unit by name (case-insensitive). It searches both primary names and aliases for cocktail-related units.

Parameters:

  • name: The unit name to look up (e.g., "oz", "dash", "ml")

Returns:

  • *CocktailUnit: The matching unit info, or nil if not found

Example:

unit := cooklang.GetCocktailUnit("fl oz")
if unit != nil {
    fmt.Printf("1 %s = %.1f ml\n", unit.Name, unit.MlValue)
}

type Comment added in v0.3.0

type Comment struct {
	Text          string        `json:"text,omitempty"`           // Comment text
	IsBlock       bool          `json:"is_block,omitempty"`       // True if this is a block comment [- -]
	NextComponent StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

Comment represents a comment in a recipe. Comments are notes that don't affect the cooking instructions.

Example Cooklang syntax: - Line comment: -- This is a comment - Block comment: [- This is a block comment -]

func (*Comment) GetNext added in v0.3.0

func (cm *Comment) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (Comment) Render added in v0.3.0

func (cm Comment) Render() string

Render returns the Cooklang syntax representation of this comment. Examples: "-- comment text" for line comments, "[- comment text -]" for block comments

func (Comment) RenderDisplay added in v0.3.0

func (cm Comment) RenderDisplay() string

RenderDisplay returns comment text suitable for display.

func (*Comment) SetNext added in v0.3.0

func (cm *Comment) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type ConversionMode

type ConversionMode int

ConversionMode defines how precise unit conversions should be

const (
	// PreciseMode uses exact scientific conversions (29.5735 ml/oz)
	PreciseMode ConversionMode = iota

	// BartenderMode uses practical bartender conversions (30 ml/oz)
	// with rounding to friendly values and smart unit selection
	BartenderMode
)

type CooklangRenderable

type CooklangRenderable struct {
	RenderFunc func() string `json:"-"` // Custom rendering function
}

CooklangRenderable provides rendering capabilities for recipe components. It allows custom rendering functions to be attached to recipes and their components.

type Cookware

type Cookware struct {
	Name          string        `json:"name,omitempty"`           // Cookware name (e.g., "pot", "bowl", "oven")
	Quantity      int           `json:"quantity,omitempty"`       // Number of items needed (default 1)
	Annotation    string        `json:"annotation,omitempty"`     // Optional annotation (e.g., "large", "non-stick")
	NextComponent StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

Cookware represents a cooking utensil or equipment needed for a recipe.

Example Cooklang syntax: #pot{}, #bowl{2}, #oven{}

Example

ExampleCookware demonstrates working with cookware items

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Use a #large pot{} and #wooden spoons{2}.`

	recipe, _ := cooklang.ParseString(recipeText)
	cookware := recipe.GetCookware()

	for _, cw := range cookware {
		if cw.Quantity > 1 {
			fmt.Printf("%s (×%d)\n", cw.Name, cw.Quantity)
		} else {
			fmt.Printf("%s\n", cw.Name)
		}
	}
}
Output:
large pot
wooden spoons (×2)

func (*Cookware) GetNext

func (c *Cookware) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (Cookware) Render

func (c Cookware) Render() string

Render returns the Cooklang syntax representation of this cookware. Examples: "#pot{}", "#bowl{2}", "#oven{}(preheated)"

func (Cookware) RenderDisplay

func (c Cookware) RenderDisplay() string

RenderDisplay returns cookware in plain text format suitable for display. Returns just the cookware name.

func (*Cookware) SetNext

func (c *Cookware) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type FileSystemResolver added in v1.0.5

type FileSystemResolver struct {
	BasePath string
	// contains filtered or unexported fields
}

FileSystemResolver resolves recipe references by reading .cook files from disk.

func NewFileSystemResolver added in v1.0.5

func NewFileSystemResolver(basePath string) *FileSystemResolver

NewFileSystemResolver creates a new resolver rooted at the given base directory.

func (*FileSystemResolver) Resolve added in v1.0.5

func (r *FileSystemResolver) Resolve(path string) (*Recipe, error)

Resolve reads and parses a .cook file relative to the base path. It caches results and detects cycles.

type FrontmatterEditor

type FrontmatterEditor struct {
	// contains filtered or unexported fields
}

FrontmatterEditor provides CRUD operations for recipe frontmatter metadata. It allows reading, updating, and managing recipe metadata without manually parsing YAML.

The editor works with the structured Recipe fields (title, cuisine, servings, etc.) as well as custom metadata fields, providing a unified interface for metadata management.

Example:

editor, err := cooklang.NewFrontmatterEditor("recipe.cook")
if err != nil {
    log.Fatal(err)
}
editor.SetMetadata("title", "Improved Pasta")
editor.SetMetadata("servings", "4")
editor.Save()

func NewFrontmatterEditor

func NewFrontmatterEditor(filePath string) (*FrontmatterEditor, error)

NewFrontmatterEditor creates a new FrontmatterEditor for the given recipe file. It reads and parses the file, making the metadata available for manipulation.

Parameters:

  • filePath: Path to the .cook file to edit

Returns:

  • *FrontmatterEditor: An editor instance ready for metadata operations
  • error: Any error encountered during file reading or parsing

Example:

editor, err := cooklang.NewFrontmatterEditor("lasagna.cook")
if err != nil {
    log.Fatal(err)
}
title, _ := editor.GetMetadata("title")
fmt.Println(title)
Example

ExampleNewFrontmatterEditor demonstrates editing recipe metadata

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	// In real usage, you would use an actual file path
	// This example shows the API usage
	recipeText := `---
title: Old Title
servings: 2
---
Cook @pasta{400%g}.`

	recipe, _ := cooklang.ParseString(recipeText)
	fmt.Printf("Original title: %s\n", recipe.Title)
	fmt.Printf("Original servings: %.0f\n", recipe.Servings)

	// Note: In actual use, you'd create editor with NewFrontmatterEditor(filepath)
	// and then call SetMetadata, Save, etc.
	

func (*FrontmatterEditor) AppendToArray

func (fe *FrontmatterEditor) AppendToArray(key, value string) error

AppendToArray appends a value to an array field (tags or images). Unlike FrontmatterEditor.SetMetadata, this adds a single item without replacing existing values. The underlying storage is []string, not a comma-separated string.

Parameters:

  • key: The array field name ("tags" or "images")
  • value: The single value to append

Returns:

  • error: An error if the field is not an array field

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
// Assuming tags are currently ["italian"]
editor.AppendToArray("tags", "vegetarian") // tags: ["italian", "vegetarian"]
editor.AppendToArray("tags", "healthy")    // tags: ["italian", "vegetarian", "healthy"]
editor.Save()

func (*FrontmatterEditor) DeleteMetadata

func (fe *FrontmatterEditor) DeleteMetadata(key string) error

DeleteMetadata removes a metadata key from the recipe. For structured fields, this clears the value. For custom fields, it removes the entry.

Parameters:

  • key: The metadata key to delete

Returns:

  • error: Currently always returns nil (reserved for future validation)

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
editor.DeleteMetadata("author")
editor.DeleteMetadata("custom_field")
editor.Save()

func (*FrontmatterEditor) GetAllMetadata

func (fe *FrontmatterEditor) GetAllMetadata() map[string]string

GetAllMetadata returns all metadata as a map. This includes both structured fields (title, cuisine, etc.) and custom metadata.

Returns:

  • map[string]string: All metadata key-value pairs

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
allMeta := editor.GetAllMetadata()
for key, value := range allMeta {
    fmt.Printf("%s: %s\n", key, value)
}

func (*FrontmatterEditor) GetContent

func (fe *FrontmatterEditor) GetContent() string

GetContent returns the original file content as read from disk.

Returns:

  • string: The original file content

func (*FrontmatterEditor) GetMetadata

func (fe *FrontmatterEditor) GetMetadata(key string) (string, bool)

GetMetadata retrieves a metadata value by key. It checks structured fields first (title, cuisine, etc.) then falls back to the generic metadata map.

For array fields (tags, images), the value is returned as a comma-separated string. Use FrontmatterEditor.AppendToArray and FrontmatterEditor.RemoveFromArray for individual item operations.

Parameters:

  • key: The metadata key to retrieve

Returns:

  • string: The metadata value (comma-separated for array fields)
  • bool: true if the key exists, false otherwise

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
if title, ok := editor.GetMetadata("title"); ok {
    fmt.Printf("Recipe title: %s\n", title)
}
if tags, ok := editor.GetMetadata("tags"); ok {
    fmt.Printf("Tags: %s\n", tags) // e.g., "italian, pasta, quick"
}
Example

ExampleFrontmatterEditor_GetMetadata shows retrieving metadata values

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	// Demonstrates the pattern for getting metadata
	recipeText := `---
title: Chocolate Cake
difficulty: Medium
prep_time: 30 minutes
---
Mix ingredients.`

	recipe, _ := cooklang.ParseString(recipeText)

	// Access metadata directly from recipe
	fmt.Printf("Title: %s\n", recipe.Title)
	fmt.Printf("Difficulty: %s\n", recipe.Difficulty)
	fmt.Printf("Prep time: %s\n", recipe.PrepTime)
}
Output:
Title: Chocolate Cake
Difficulty: Medium
Prep time: 30 minutes

func (*FrontmatterEditor) GetUpdatedContent

func (fe *FrontmatterEditor) GetUpdatedContent() string

GetUpdatedContent returns the updated content without saving to disk. This is useful for previewing changes before committing them.

Returns:

  • string: The complete file content with updated frontmatter

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
editor.SetMetadata("title", "Preview")
preview := editor.GetUpdatedContent()
fmt.Println(preview) // See changes without saving

func (*FrontmatterEditor) RemoveFromArray

func (fe *FrontmatterEditor) RemoveFromArray(key, value string) error

RemoveFromArray removes a value from an array field (tags or images). All occurrences of the value are removed. The underlying storage is []string.

Parameters:

  • key: The array field name ("tags" or "images")
  • value: The single value to remove

Returns:

  • error: An error if the field is not an array field

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
// Assuming tags are currently ["italian", "unhealthy", "quick"]
editor.RemoveFromArray("tags", "unhealthy") // tags: ["italian", "quick"]
editor.Save()

func (*FrontmatterEditor) Save

func (fe *FrontmatterEditor) Save() error

Save writes the updated recipe back to the original file. The recipe body (instructions) is preserved; only the frontmatter is updated.

Returns:

  • error: Any error encountered during file writing

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
editor.SetMetadata("title", "Updated Title")
if err := editor.Save(); err != nil {
    log.Fatal(err)
}

func (*FrontmatterEditor) SaveAs

func (fe *FrontmatterEditor) SaveAs(filePath string) error

SaveAs writes the updated recipe to a specified file path. The recipe body (instructions) is preserved; only the frontmatter is updated.

Parameters:

  • filePath: The destination file path

Returns:

  • error: Any error encountered during file writing

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
editor.SetMetadata("title", "Updated Recipe")
editor.SaveAs("recipe_v2.cook")

func (*FrontmatterEditor) SetMetadata

func (fe *FrontmatterEditor) SetMetadata(key, value string) error

SetMetadata sets or updates a metadata value. For array fields (tags, images), the value should be comma-separated; it will be split and stored as a []string internally. To add/remove individual items without replacing the entire array, use FrontmatterEditor.AppendToArray and FrontmatterEditor.RemoveFromArray instead.

For structured fields (servings, date), the value is validated and parsed.

Parameters:

  • key: The metadata key to set
  • value: The value to set (comma-separated for array fields)

Returns:

  • error: Validation error for structured fields (e.g., invalid date format)

Example:

editor, _ := cooklang.NewFrontmatterEditor("recipe.cook")
editor.SetMetadata("title", "Amazing Lasagna")
editor.SetMetadata("servings", "6")
editor.SetMetadata("tags", "italian, pasta, main course") // replaces all tags
editor.SetMetadata("date", "2024-01-15")
editor.Save()

type Ingredient

type Ingredient struct {
	Name           string        `json:"name,omitempty"`           // Ingredient name (e.g., "flour", "sugar")
	Quantity       float32       `json:"quantity,omitempty"`       // Amount (-1 means "some", 0 means none specified)
	Unit           string        `json:"unit,omitempty"`           // Unit of measurement (e.g., "g", "cup", "tbsp")
	Fixed          bool          `json:"fixed,omitempty"`          // Fixed quantity doesn't scale with servings
	Optional       bool          `json:"optional,omitempty"`       // Optional ingredient (can be omitted)
	TypedUnit      *units.Unit   `json:"typed_unit,omitempty"`     // Typed unit for conversion operations
	Subinstruction string        `json:"value,omitempty"`          // Additional preparation instructions
	Annotation     string        `json:"annotation,omitempty"`     // Optional annotation (e.g., "finely chopped")
	NextComponent  StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

Ingredient represents a recipe ingredient with quantity, unit, and optional annotations. Ingredients support unit conversion and consolidation for shopping lists.

Example Cooklang syntax: @flour{500%g}, @salt{}, @milk{2%cups}

The Quantity field uses -1 to represent "some" (unspecified amount). The Fixed field indicates a quantity that should not scale with servings (e.g., @salt{=1%tsp}). The Optional field indicates an optional ingredient (e.g., @?thyme{2%sprigs}).

func NewIngredient

func NewIngredient(name string, quantity float32, unit string) *Ingredient

NewIngredient creates a new Ingredient with proper unit typing for conversion operations. This constructor ensures that the TypedUnit field is properly initialized, which is required for unit conversion methods like ConvertTo and ConvertToSystem to work correctly.

Parameters:

  • name: The ingredient name (e.g., "vodka", "sugar")
  • quantity: The amount (-1 means "some" unspecified amount)
  • unit: The unit of measurement (e.g., "ml", "oz", "g", "cups")

Example:

ing := cooklang.NewIngredient("vodka", 50, "ml")
converted := ing.ConvertToSystem(cooklang.UnitSystemUS)
fmt.Printf("%v %s\n", converted.Quantity, converted.Unit) // "1.69 oz"

func (*Ingredient) CanConvertTo

func (i *Ingredient) CanConvertTo(targetUnitStr string) bool

CanConvertTo checks if the ingredient can be converted to the target unit. This allows validating conversions before attempting them.

Parameters:

  • targetUnitStr: The unit to check conversion compatibility with

Returns:

  • bool: true if conversion is possible, false otherwise

Example:

ingredient := &Ingredient{Name: "water", Quantity: 250, Unit: "ml"}
if ingredient.CanConvertTo("cup") {
    converted, _ := ingredient.ConvertTo("cup")
    fmt.Printf("Can convert: %.2f %s\n", converted.Quantity, converted.Unit)
}
Example

ExampleIngredient_CanConvertTo shows how to check if unit conversion is possible

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @flour{200%g}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	flour := recipe.GetIngredients().Ingredients[0]

	// Check various conversions
	fmt.Printf("Can convert to kg: %v\n", flour.CanConvertTo("kg"))
	fmt.Printf("Can convert to oz: %v\n", flour.CanConvertTo("oz"))
	fmt.Printf("Can convert to ml: %v\n", flour.CanConvertTo("ml"))
}
Output:
Can convert to kg: true
Can convert to oz: true
Can convert to ml: false

func (*Ingredient) ConvertTo

func (i *Ingredient) ConvertTo(targetUnitStr string) (*Ingredient, error)

ConvertTo converts the ingredient to a different unit if possible. The conversion uses either custom cooking unit conversions (for common units like cups, tbsp, oz) or the go-units library for scientific units.

Parameters:

  • targetUnitStr: The target unit to convert to (e.g., "g", "cup", "ml")

Returns:

  • *Ingredient: A new ingredient with the converted quantity and unit
  • error: Error if conversion is not possible (incompatible units, "some" quantity, etc.)

Example:

ingredient := &Ingredient{Name: "flour", Quantity: 2, Unit: "cup"}
converted, err := ingredient.ConvertTo("g")
if err == nil {
    fmt.Printf("%.0f %s\n", converted.Quantity, converted.Unit) // "473 g"
}
Example

ExampleIngredient_ConvertTo demonstrates unit conversion for ingredients

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @water{500%ml}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	ingredients := recipe.GetIngredients()
	water := ingredients.Ingredients[0]

	// Convert from milliliters to cups
	converted, err := water.ConvertTo("cup")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Original: %.0f %s\n", water.Quantity, water.Unit)
	fmt.Printf("Converted: %.2f %s\n", converted.Quantity, converted.Unit)
}
Output:
Original: 500 ml
Converted: 2.11 cup

func (*Ingredient) ConvertToSystem

func (i *Ingredient) ConvertToSystem(system UnitSystem) *Ingredient

ConvertToSystem converts an ingredient to the target unit system. The conversion selects an appropriate unit based on the ingredient's unit type (mass or volume) and converts the quantity accordingly.

If the ingredient has no TypedUnit or has "some" quantity (-1), a copy is returned unchanged.

Parameters:

  • system: The target unit system (UnitSystemMetric, UnitSystemUS, UnitSystemImperial)

Returns:

  • *Ingredient: A new ingredient with converted quantity and unit

Example:

flour := cooklang.NewIngredient("flour", 500, "g")
usFlour := flour.ConvertToSystem(cooklang.UnitSystemUS)
fmt.Printf("%v %s\n", usFlour.Quantity, usFlour.Unit) // "17.6 oz"

func (*Ingredient) ConvertToSystemBartender

func (i *Ingredient) ConvertToSystemBartender(system UnitSystem) *Ingredient

ConvertToSystemBartender converts an ingredient using bartender-friendly conversions. It uses practical measurements like dashes for very small amounts (<3ml), and skips conversion for cocktail-specific units (dash, splash, etc.).

Features:

  • Practical oz/ml conversion (30ml = 1oz)
  • Smart unit selection based on quantity
  • Dashes for tiny amounts (≤3ml)
  • Preserves cocktail-specific units

Parameters:

  • system: The target unit system (UnitSystemMetric, UnitSystemUS)

Returns:

  • *Ingredient: A new ingredient with bartender-friendly quantity and unit

Example:

vodka := cooklang.NewIngredient("vodka", 45, "ml")
usVodka := vodka.ConvertToSystemBartender(cooklang.UnitSystemUS)
fmt.Printf("%v %s\n", usVodka.Quantity, usVodka.Unit) // "1.5 oz"

func (*Ingredient) FormatQuantityBartender

func (i *Ingredient) FormatQuantityBartender() string

FormatQuantityBartender formats an ingredient's quantity using bartender-friendly formatting. This uses fractions instead of decimals (e.g., "1/2" instead of "0.5") and handles pluralization appropriately.

Returns:

  • string: Formatted quantity string (e.g., "1 1/2 oz", "some", "")

Example:

vodka := cooklang.NewIngredient("vodka", 1.5, "oz")
fmt.Println(vodka.FormatQuantityBartender()) // "1 1/2 oz"

func (*Ingredient) GetNext

func (i *Ingredient) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (*Ingredient) GetUnitType

func (i *Ingredient) GetUnitType() string

GetUnitType returns the ingredient's unit quantity type (e.g., "mass", "volume", "length"). This helps categorize ingredients and determine valid conversions.

Returns:

  • string: The quantity type ("mass", "volume", "length", "temperature", "time", "energy", or "")

Example:

ingredient := &Ingredient{Name: "flour", Quantity: 500, Unit: "g"}
fmt.Println(ingredient.GetUnitType()) // "mass"

ingredient2 := &Ingredient{Name: "milk", Quantity: 2, Unit: "cup"}
fmt.Println(ingredient2.GetUnitType()) // "volume"
Example

ExampleIngredient_GetUnitType shows how to get the type of a unit

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @water{500%ml}, @flour{200%g}, and @vanilla{1%tsp}.`

	recipe, _ := cooklang.ParseString(recipeText)
	ingredients := recipe.GetIngredients()

	for _, ing := range ingredients.Ingredients {
		unitType := ing.GetUnitType()
		fmt.Printf("%s: %s\n", ing.Name, unitType)
	}
}
Output:
water: volume
flour: mass
vanilla: volume

func (Ingredient) Render

func (i Ingredient) Render() string

Render returns the Cooklang syntax representation of this ingredient. Examples: "@flour{500%g}", "@salt{}", "@milk{2%cups}(cold)", "@yeast{=1%packet}", "@?thyme{2%sprigs}"

Example

ExampleIngredient_Render shows rendering an ingredient back to Cooklang format

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @garlic{3%cloves}(minced) and @salt{}.`

	recipe, _ := cooklang.ParseString(recipeText)
	ingredients := recipe.GetIngredients()

	for _, ing := range ingredients.Ingredients {
		fmt.Println(ing.Render())
	}
}
Output:
@garlic{3%cloves}(minced)
@salt{}

func (Ingredient) RenderDisplay

func (i Ingredient) RenderDisplay() string

RenderDisplay returns ingredient in plain text format suitable for display. Examples: "2 cups flour", "500 g flour", "salt", "2 sprigs thyme (optional)" Uses bartender-friendly fraction formatting (e.g., "1/2 oz" instead of "0.5 oz") When quantity is unspecified (e.g., @salt{}), returns just the ingredient name. Optional ingredients have "(optional)" appended.

func (*Ingredient) SetNext

func (i *Ingredient) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type IngredientList

type IngredientList struct {
	Ingredients []*Ingredient // The list of ingredients
}

IngredientList represents a collection of ingredients with unit consolidation capabilities. It provides methods for grouping, converting, and consolidating ingredients for shopping lists and recipe scaling operations.

func NewIngredientList

func NewIngredientList() *IngredientList

NewIngredientList creates a new empty ingredient list.

Returns:

  • *IngredientList: A new ingredient list ready for use

Example:

list := cooklang.NewIngredientList()
list.Add(&cooklang.Ingredient{Name: "flour", Quantity: 500, Unit: "g"})

func (*IngredientList) Add

func (il *IngredientList) Add(ingredient *Ingredient)

Add adds an ingredient to the list.

Parameters:

  • ingredient: The ingredient to add

Example:

list := cooklang.NewIngredientList()
list.Add(&cooklang.Ingredient{Name: "sugar", Quantity: 100, Unit: "g"})
list.Add(&cooklang.Ingredient{Name: "flour", Quantity: 2, Unit: "cup"})

func (*IngredientList) ConsolidateByName

func (il *IngredientList) ConsolidateByName(targetUnit string) (*IngredientList, error)

ConsolidateByName consolidates ingredients with the same name, converting to a common unit when possible. This is useful for creating shopping lists where multiple mentions of the same ingredient should be combined into a single entry.

If targetUnit is empty, the method attempts to find a common unit from the ingredients. If targetUnit is specified, all compatible ingredients are converted to that unit before consolidation.

Ingredients with "some" quantity (-1) or incompatible units are kept separate.

Parameters:

  • targetUnit: The unit to convert all ingredients to (empty string to auto-detect)

Returns:

  • *IngredientList: A new list with consolidated ingredients
  • error: Any error encountered during consolidation

Example:

list := cooklang.NewIngredientList()
list.Add(&cooklang.Ingredient{Name: "flour", Quantity: 100, Unit: "g"})
list.Add(&cooklang.Ingredient{Name: "flour", Quantity: 150, Unit: "g"})
consolidated, _ := list.ConsolidateByName("")
// consolidated will have one "flour" entry with 250g
Example

ExampleIngredientList_ConsolidateByName shows how to consolidate duplicate ingredients

package main

import (
	"fmt"
	"log"
	"sort"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @flour{200%g} to bowl.
Mix with @flour{300%g} and @sugar{100%g}.
Add @sugar{50%g} for sweetness.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	ingredients := recipe.GetIngredients()
	consolidated, err := ingredients.ConsolidateByName("")
	if err != nil {
		log.Fatal(err)
	}

	// Sort ingredients by name for consistent output
	sort.Slice(consolidated.Ingredients, func(i, j int) bool {
		return consolidated.Ingredients[i].Name < consolidated.Ingredients[j].Name
	})

	fmt.Println("Consolidated ingredients:")
	for _, ing := range consolidated.Ingredients {
		fmt.Printf("- %s: %.0f %s\n", ing.Name, ing.Quantity, ing.Unit)
	}
}
Output:
Consolidated ingredients:
- flour: 500 g
- sugar: 150 g

func (*IngredientList) ConvertToSystem

func (il *IngredientList) ConvertToSystem(system UnitSystem) *IngredientList

ConvertToSystem converts all ingredients in the list to the target unit system. Each ingredient is converted individually using Ingredient.ConvertToSystem. Ingredients that cannot be converted (no TypedUnit or "some" quantity) are copied as-is.

Parameters:

  • system: The target unit system (UnitSystemMetric, UnitSystemUS, UnitSystemImperial)

Returns:

  • *IngredientList: A new list with converted ingredients

Example:

metricList := recipe.GetIngredients()
usList := metricList.ConvertToSystem(cooklang.UnitSystemUS)
Example

ExampleIngredientList_ConvertToSystem demonstrates converting all ingredients to a unit system

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @water{2%cup}, @flour{500%g}, and @sugar{1%lb}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	ingredients := recipe.GetIngredients()

	// Convert to metric system
	metric := ingredients.ConvertToSystem(cooklang.UnitSystemMetric)

	fmt.Println("Metric ingredients:")
	for _, ing := range metric.Ingredients {
		fmt.Printf("- %s: %.0f %s\n", ing.Name, ing.Quantity, ing.Unit)
	}
}
Output:
Metric ingredients:
- water: 473 ml
- flour: 500 g
- sugar: 454 g

func (*IngredientList) ConvertToSystemBartender

func (il *IngredientList) ConvertToSystemBartender(system UnitSystem) *IngredientList

ConvertToSystemBartender converts all ingredients using bartender-friendly conversions. This uses practical bartender measurements (30ml = 1oz instead of 29.5735ml) and smart unit selection (dashes for tiny amounts, friendly fractions).

Parameters:

  • system: The target unit system (UnitSystemMetric, UnitSystemUS)

Returns:

  • *IngredientList: A new list with bartender-friendly converted ingredients

Example:

ingredients := cocktail.GetIngredients()
usBar := ingredients.ConvertToSystemBartender(cooklang.UnitSystemUS)

func (*IngredientList) ConvertToSystemWithConsolidation

func (il *IngredientList) ConvertToSystemWithConsolidation(system UnitSystem) (*IngredientList, error)

ConvertToSystemWithConsolidation converts ingredients to a target system and consolidates by name. This combines ConvertToSystem and ConsolidateByName in a single operation.

Parameters:

  • system: The target unit system (UnitSystemMetric, UnitSystemUS, UnitSystemImperial)

Returns:

  • *IngredientList: A new list with converted and consolidated ingredients
  • error: Any error encountered during consolidation

Example:

ingredients := recipe.GetIngredients()
usList, _ := ingredients.ConvertToSystemWithConsolidation(cooklang.UnitSystemUS)

func (*IngredientList) GetIngredientsByName

func (il *IngredientList) GetIngredientsByName(name string) []*Ingredient

GetIngredientsByName returns all ingredients with the given name. This is useful for finding duplicate ingredients before consolidation or for extracting specific ingredients from a list.

Parameters:

  • name: The ingredient name to search for (case-sensitive)

Returns:

  • []*Ingredient: Slice of matching ingredients (empty if none found)

Example:

list := recipe.GetIngredients()
flourEntries := list.GetIngredientsByName("flour")
for _, f := range flourEntries {
    fmt.Printf("Found %v %s flour\n", f.Quantity, f.Unit)
}

func (*IngredientList) ToMap

func (il *IngredientList) ToMap() map[string]string

ToMap returns a map of ingredient names to their formatted quantities. This is useful for displaying shopping lists in a simple key-value format.

The quantity formatting follows these rules:

  • Whole numbers are shown without decimals (e.g., "100 g")
  • Fractional quantities show one decimal place (e.g., "1.5 cup")
  • "Some" quantities (-1) are displayed as "some" or "some [unit]"
  • Unitless ingredients show just the quantity or "some"

Returns:

  • map[string]string: Map of ingredient names to formatted quantity strings

Example:

list := recipe.GetIngredients()
for name, qty := range list.ToMap() {
    fmt.Printf("- %s: %s\n", name, qty)
}
// Output:
// - flour: 500 g
// - eggs: 3
// - salt: some
Example

ExampleIngredientList_ToMap shows converting ingredient list to a map

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Use @flour{500%g}, @sugar{200%g}, and @eggs{3}.`

	recipe, _ := cooklang.ParseString(recipeText)
	ingredients := recipe.GetIngredients()

	ingredientMap := ingredients.ToMap()

	// Print in deterministic order for test
	fmt.Printf("eggs: %s\n", ingredientMap["eggs"])
	fmt.Printf("flour: %s\n", ingredientMap["flour"])
	fmt.Printf("sugar: %s\n", ingredientMap["sugar"])
}
Output:
eggs: 3
flour: 500 g
sugar: 200 g

type Instruction

type Instruction struct {
	Text          string        `json:"text,omitempty"`           // Instruction text
	NextComponent StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

Instruction represents a text instruction within a recipe step. This is plain text that provides cooking directions.

func (*Instruction) GetNext

func (inst *Instruction) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (Instruction) Render

func (inst Instruction) Render() string

Render returns the plain text instruction.

func (Instruction) RenderDisplay

func (inst Instruction) RenderDisplay() string

RenderDisplay returns instruction text suitable for display (same as Render for Instruction).

func (*Instruction) SetNext

func (inst *Instruction) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type Menu struct {
	Days []MenuDay `json:"days"`
}

Menu represents a parsed .menu file containing a meal plan organized by days.

func ParseMenuFile added in v1.0.5

func ParseMenuFile(filename string) (*Menu, error)

ParseMenuFile reads and parses a .menu file from disk.

func ParseMenuString added in v1.0.5

func ParseMenuString(s string) (*Menu, error)

ParseMenuString parses a menu from a string. A .menu file is a valid Cooklang file using sections for days and recipe references for dishes.

type MenuDay struct {
	Name    string       `json:"name"`
	Date    *time.Time   `json:"date,omitempty"`
	Recipes []MenuRecipe `json:"recipes"`
}

MenuDay represents a single day or section in a menu, containing recipe references.

type MenuRecipe struct {
	Path     string  `json:"path"`
	Quantity float32 `json:"quantity,omitempty"`
	Unit     string  `json:"unit,omitempty"`
}

MenuRecipe represents a recipe reference within a menu day.

type Metadata

type Metadata map[string]string

Metadata stores arbitrary key-value pairs for recipe metadata not covered by structured fields. This allows recipes to include custom fields beyond the standard ones.

Example:

metadata := Metadata{
    "source": "Grandma's cookbook",
    "category": "dessert",
}

type Note added in v0.4.0

type Note struct {
	Text          string        `json:"text,omitempty"`           // Note text
	NextComponent StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

Note represents a note block in a recipe. Notes are supplementary information that appears in recipe details but not during cooking mode. They are used for background stories, tips, or personal anecdotes related to the recipe.

Example Cooklang syntax: > This dish is even better the next day, after the flavors have melded overnight. > This is a multi-line note > that continues here.

func (*Note) GetNext added in v0.4.0

func (n *Note) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (Note) Render added in v0.4.0

func (n Note) Render() string

Render returns the Cooklang syntax representation of this note. Example: "> This is a note"

func (Note) RenderDisplay added in v0.4.0

func (n Note) RenderDisplay() string

RenderDisplay returns note text suitable for display.

func (*Note) SetNext added in v0.4.0

func (n *Note) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type Recipe

type Recipe struct {
	Title       string    `json:"title,omitempty"`       // Recipe title from frontmatter
	Cuisine     string    `json:"cuisine,omitempty"`     // Cuisine type (e.g., "Italian", "Mexican")
	Date        time.Time `json:"date,omitempty"`        // Recipe date in YYYY-MM-DD format
	Description string    `json:"description,omitempty"` // Brief recipe description
	Difficulty  string    `json:"difficulty,omitempty"`  // Difficulty level (e.g., "easy", "medium", "hard")
	PrepTime    string    `json:"prep_time,omitempty"`   // Preparation time (e.g., "15 minutes")
	TotalTime   string    `json:"total_time,omitempty"`  // Total cooking time
	Metadata    Metadata  `json:"metadata,omitempty"`    // Additional custom metadata fields
	Author      string    `json:"author,omitempty"`      // Recipe author name
	Images      []string  `json:"images,omitempty"`      // Image filenames associated with the recipe
	Servings    float32   `json:"servings,omitempty"`    // Number of servings this recipe makes
	Tags        []string  `json:"tags,omitempty"`        // Recipe tags for categorization
	FirstStep   *Step     `json:"first_step,omitempty"`  // First step in the linked list of recipe steps
	CooklangRenderable
}

Recipe represents a parsed Cooklang recipe with its metadata and step-by-step instructions. The Recipe struct provides access to all recipe information including ingredients, cookware, timers, and cooking instructions organized as a linked list of steps.

Recipes can be created by parsing Cooklang files using ParseFile, ParseString, or ParseBytes.

Example:

recipe, err := cooklang.ParseFile("lasagna.cook")
if err != nil {
    log.Fatal(err)
}
fmt.Println(recipe.Title)
ingredients := recipe.GetIngredients()

func ParseBytes

func ParseBytes(content []byte) (*Recipe, error)

ParseBytes parses Cooklang recipe content from a byte slice. This is useful for parsing recipes from memory, HTTP responses, or other byte sources.

Unlike ParseFile, this function does not perform image detection since no filename is available.

Parameters:

  • content: The raw Cooklang recipe content as bytes

Returns:

  • *Recipe: The parsed recipe with all metadata and steps
  • error: Any error encountered during parsing

Example:

content := []byte("---\ntitle: Quick Pasta\n---\n\nBoil @water{2%L} and add @pasta{100%g}.")
recipe, err := cooklang.ParseBytes(content)

func ParseFile

func ParseFile(filename string) (*Recipe, error)

ParseFile reads and parses a Cooklang recipe file, returning a Recipe object. It automatically detects and includes associated image files matching the recipe filename.

Image detection looks for files with the same base name:

  • Recipe.cook → Recipe.jpg, Recipe.png, Recipe.jpeg
  • Recipe.cook → Recipe-1.jpg, Recipe-2.png, etc. (numbered variants)

Parameters:

  • filename: Path to the .cook file to parse

Returns:

  • *Recipe: The parsed recipe with all metadata, steps, and detected images
  • error: Any error encountered during file reading or parsing

Example:

recipe, err := cooklang.ParseFile("recipes/lasagna.cook")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Recipe: %s\n", recipe.Title)
fmt.Printf("Servings: %.0f\n", recipe.Servings)
Example

ExampleParseFile demonstrates parsing a recipe from a file

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	// Create a temporary recipe file for demonstration
	// In real usage, you would use an actual .cook file path
	recipe, err := cooklang.ParseString(`---
title: Quick Omelette
servings: 1
---
Beat @eggs{2} with @milk{2%tbsp}.
Cook in a #pan{} over medium heat for ~{3%minutes}.`)

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(recipe.Title)
}
Output:
Quick Omelette

func ParseString

func ParseString(content string) (*Recipe, error)

ParseString parses Cooklang recipe content from a string. This is a convenience wrapper around ParseBytes for string input.

Parameters:

  • content: The Cooklang recipe content as a string

Returns:

  • *Recipe: The parsed recipe with all metadata and steps
  • error: Any error encountered during parsing

Example:

content := "---\ntitle: Quick Pasta\n---\n\nBoil @water{2%L}."
recipe, err := cooklang.ParseString(content)
Example

ExampleParseString demonstrates basic recipe parsing from a string

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `---
title: Pasta Aglio e Olio
servings: 2
---
Cook @pasta{400%g} in salted water for ~{10%minutes}.
Meanwhile, heat @olive oil{4%tbsp} and sauté @garlic{3%cloves}.
Toss everything together and serve.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Recipe: %s\n", recipe.Title)
	fmt.Printf("Servings: %.0f\n", recipe.Servings)
}
Output:
Recipe: Pasta Aglio e Olio
Servings: 2

func ResolveAndScale added in v1.0.5

func ResolveAndScale(resolver RecipeResolver, ref *RecipeReference) (*Recipe, error)

ResolveAndScale resolves a recipe reference and scales it according to the reference's quantity and unit. It supports three scaling modes:

  1. No unit — scales by the given factor
  2. "servings" unit — scales to target servings
  3. Other units — uses yield-based scaling (experimental)

func ToCooklangRecipe

func ToCooklangRecipe(pRecipe *parser.Recipe) *Recipe

ToCooklangRecipe converts a parser.Recipe to a cooklang.Recipe. This is the internal function that transforms the parser's output into the high-level Recipe structure with all metadata fields populated and step components organized as linked lists.

Most users should use ParseFile, ParseString, or ParseBytes instead of calling this directly.

func (*Recipe) GetCollectedIngredients

func (r *Recipe) GetCollectedIngredients() (*IngredientList, error)

GetCollectedIngredients returns a consolidated list of all ingredients from the recipe. This combines GetIngredients() and ConsolidateByName() into a single convenient function. Duplicate ingredients with compatible units are combined into single entries.

Returns:

  • *IngredientList: A consolidated list of ingredients
  • error: Any error encountered during consolidation

Example:

recipe, _ := cooklang.ParseFile("cake.cook")
ingredients, _ := recipe.GetCollectedIngredients()
for _, ing := range ingredients.Ingredients {
    fmt.Printf("%s: %v %s\n", ing.Name, ing.Quantity, ing.Unit)
}
Example

ExampleRecipe_GetCollectedIngredients demonstrates getting consolidated ingredients in one step

package main

import (
	"fmt"
	"log"
	"sort"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `First layer: @cheese{100%g} and @tomato{2}.
Second layer: @cheese{150%g} and @tomato{3}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	collected, err := recipe.GetCollectedIngredients()
	if err != nil {
		log.Fatal(err)
	}

	// Sort ingredients by name for consistent output
	sort.Slice(collected.Ingredients, func(i, j int) bool {
		return collected.Ingredients[i].Name < collected.Ingredients[j].Name
	})

	fmt.Println("Shopping list:")
	for _, ing := range collected.Ingredients {
		if ing.Unit != "" {
			fmt.Printf("- %s: %.0f %s\n", ing.Name, ing.Quantity, ing.Unit)
		} else {
			fmt.Printf("- %s: %.0f\n", ing.Name, ing.Quantity)
		}
	}
}
Output:
Shopping list:
- cheese: 250 g
- tomato: 5

func (*Recipe) GetCollectedIngredientsMap

func (r *Recipe) GetCollectedIngredientsMap() (map[string]string, error)

GetCollectedIngredientsMap returns a map of ingredient names to their consolidated quantities. This is useful for creating simple shopping lists or ingredient summaries.

Returns:

  • map[string]string: A map of ingredient names to formatted quantity strings
  • error: Any error encountered during consolidation

Example:

recipe, _ := cooklang.ParseFile("pasta.cook")
ingredientMap, _ := recipe.GetCollectedIngredientsMap()
for name, qty := range ingredientMap {
    fmt.Printf("- %s: %s\n", name, qty)
}
Example

ExampleRecipe_GetCollectedIngredientsMap shows getting ingredients as a map for display

package main

import (
	"fmt"
	"log"
	"sort"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @water{1%l}, @salt{1%tsp}, and @salt{0.5%tsp}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	shoppingMap, err := recipe.GetCollectedIngredientsMap()
	if err != nil {
		log.Fatal(err)
	}

	// Sort keys for consistent output
	keys := make([]string, 0, len(shoppingMap))
	for name := range shoppingMap {
		keys = append(keys, name)
	}
	sort.Strings(keys)

	for _, name := range keys {
		fmt.Printf("%s: %s\n", name, shoppingMap[name])
	}
}
Output:
salt: 1.5 tsp
water: 1 l

func (*Recipe) GetCollectedIngredientsWithUnit

func (r *Recipe) GetCollectedIngredientsWithUnit(targetUnit string) (*IngredientList, error)

GetCollectedIngredientsWithUnit returns a consolidated list of all ingredients from the recipe, converting them to the specified target unit when possible.

Parameters:

  • targetUnit: The unit to convert compatible ingredients to (e.g., "g", "ml", "oz")

Returns:

  • *IngredientList: A consolidated list with converted ingredients
  • error: Any error encountered during consolidation

Example:

recipe, _ := cooklang.ParseFile("cake.cook")
// Get all ingredients in grams
ingredients, _ := recipe.GetCollectedIngredientsWithUnit("g")

func (*Recipe) GetCookware

func (r *Recipe) GetCookware() []*Cookware

GetCookware returns all cookware items from a recipe, extracted from all steps.

Returns:

  • []*Cookware: A slice containing all cookware items in order of appearance

Example:

recipe, _ := cooklang.ParseFile("pasta.cook")
cookware := recipe.GetCookware()
for _, cw := range cookware {
    fmt.Printf("%s (qty: %d)\n", cw.Name, cw.Quantity)
}
Example

ExampleRecipe_GetCookware shows how to extract cookware items from a recipe

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Mix ingredients in a #mixing bowl{}.
Transfer to a #baking dish{} and bake in an #oven{}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	cookware := recipe.GetCookware()
	fmt.Printf("Cookware needed (%d items):\n", len(cookware))
	for _, cw := range cookware {
		fmt.Printf("- %s\n", cw.Name)
	}
}
Output:
Cookware needed (3 items):
- mixing bowl
- baking dish
- oven

func (*Recipe) GetImperialShoppingList

func (r *Recipe) GetImperialShoppingList() (map[string]string, error)

GetImperialShoppingList returns a shopping list with all ingredients converted to Imperial units. Common conversions include: pints, fluid ounces, pounds, ounces.

Returns:

  • map[string]string: A map of ingredient names to quantities
  • error: Any error encountered during conversion

func (*Recipe) GetIngredients

func (r *Recipe) GetIngredients() *IngredientList

GetIngredients returns all ingredients from a recipe, extracted from all steps. This traverses the recipe's linked list structure to collect every ingredient mention.

Returns:

  • *IngredientList: A list containing all ingredients in order of appearance

Example:

recipe, _ := cooklang.ParseFile("lasagna.cook")
ingredients := recipe.GetIngredients()
for _, ing := range ingredients.Ingredients {
    fmt.Printf("%s: %.1f %s\n", ing.Name, ing.Quantity, ing.Unit)
}
Example

ExampleRecipe_GetIngredients shows how to extract all ingredients from a recipe

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Mix @flour{200%g}, @sugar{150%g}, and @butter{100%g}.
Add @eggs{2} and @vanilla{1%tsp}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	ingredients := recipe.GetIngredients()
	fmt.Printf("Found %d ingredients:\n", len(ingredients.Ingredients))
	for _, ing := range ingredients.Ingredients {
		if ing.Unit != "" {
			fmt.Printf("- %s: %.0f %s\n", ing.Name, ing.Quantity, ing.Unit)
		} else {
			fmt.Printf("- %s: %.0f\n", ing.Name, ing.Quantity)
		}
	}
}
Output:
Found 5 ingredients:
- flour: 200 g
- sugar: 150 g
- butter: 100 g
- eggs: 2
- vanilla: 1 tsp

func (*Recipe) GetMetricShoppingList

func (r *Recipe) GetMetricShoppingList() (map[string]string, error)

GetMetricShoppingList returns a shopping list with all ingredients converted to metric units. This is a convenience method for GetShoppingListInSystem(UnitSystemMetric).

Returns:

  • map[string]string: A map of ingredient names to quantities (e.g., "flour": "500 g")
  • error: Any error encountered during conversion

Example:

recipe, _ := cooklang.ParseFile("cookies.cook")
shoppingList, err := recipe.GetMetricShoppingList()
if err == nil {
    for ingredient, amount := range shoppingList {
        fmt.Printf("%s: %s\n", ingredient, amount)
    }
}
Example

ExampleRecipe_GetMetricShoppingList shows getting a shopping list in metric units

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @flour{2%cup} and @butter{4%oz}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	metricList, err := recipe.GetMetricShoppingList()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Metric shopping list:")
	// Print in deterministic order
	fmt.Printf("- butter: %s\n", metricList["butter"])
	fmt.Printf("- flour: %s\n", metricList["flour"])
}
Output:
Metric shopping list:
- butter: 113.4 g
- flour: 473.2 ml

func (*Recipe) GetShoppingListInSystem

func (r *Recipe) GetShoppingListInSystem(system UnitSystem) (map[string]string, error)

GetShoppingListInSystem returns a shopping list with ingredients converted to the target unit system. The ingredients are first converted, then consolidated by name to combine duplicate entries.

Parameters:

  • system: The target unit system (UnitSystemMetric, UnitSystemUS, UnitSystemImperial)

Returns:

  • map[string]string: A map of ingredient names to formatted quantities
  • error: Any error encountered during conversion

Example:

recipe, _ := cooklang.ParseFile("pasta.cook")
usList, _ := recipe.GetShoppingListInSystem(cooklang.UnitSystemUS)
for name, qty := range usList {
    fmt.Printf("%s: %s\n", name, qty)
}

func (*Recipe) GetUSShoppingList

func (r *Recipe) GetUSShoppingList() (map[string]string, error)

GetUSShoppingList returns a shopping list with all ingredients converted to US customary units. Common conversions include: cups, tablespoons, teaspoons, ounces, pounds.

Returns:

  • map[string]string: A map of ingredient names to quantities (e.g., "flour": "2 cup")
  • error: Any error encountered during conversion

Example:

recipe, _ := cooklang.ParseFile("cookies.cook")
shoppingList, err := recipe.GetUSShoppingList()
Example

ExampleRecipe_GetUSShoppingList shows getting a shopping list in US units

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Add @water{500%ml} and @flour{250%g}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	usList, err := recipe.GetUSShoppingList()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("US shopping list:")
	// Print in deterministic order
	fmt.Printf("- flour: %s\n", usList["flour"])
	fmt.Printf("- water: %s\n", usList["water"])
}
Output:
US shopping list:
- flour: 8.8 oz
- water: 2.1 cup

func (*Recipe) Render

func (r *Recipe) Render() string

Render returns a human-readable representation of the recipe. If a custom renderer has been set via SetRenderer or SetRendererFunc, it will be used. Otherwise, a default text format is used showing metadata, ingredients, and steps.

Example:

recipe, _ := cooklang.ParseFile("lasagna.cook")
fmt.Println(recipe.Render())
Example

ExampleRecipe_Render demonstrates basic recipe rendering

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `---
title: Quick Snack
servings: 1
---
Toast @bread{2%slices} and spread @butter{1%tbsp}.`

	recipe, err := cooklang.ParseString(recipeText)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Recipe: %s (serves %.0f)\n", recipe.Title, recipe.Servings)
}
Output:
Recipe: Quick Snack (serves 1)

func (*Recipe) RenderWith

func (r *Recipe) RenderWith(renderer RecipeRenderer) string

RenderWith renders the recipe using the provided renderer. This allows one-time rendering without setting a permanent renderer on the recipe.

Parameters:

  • renderer: A RecipeRenderer implementation to use for rendering

Returns:

  • string: The rendered recipe in the format defined by the renderer

Example:

recipe, _ := cooklang.ParseFile("recipe.cook")
markdown := recipe.RenderWith(renderers.MarkdownRenderer{})
html := recipe.RenderWith(renderers.HTMLRenderer{})

func (*Recipe) Scale added in v0.3.0

func (r *Recipe) Scale(factor float64) *Recipe

Scale creates a new recipe with all ingredient quantities scaled by the given factor. This is useful for adjusting recipe servings or batch cooking. Timers, cookware, and instructions are copied unchanged. Ingredients with "some" quantity (-1) are not scaled.

The servings metadata is also updated if present.

Parameters:

  • factor: The scaling factor (e.g., 2.0 for double, 0.5 for half)

Returns:

  • *Recipe: A new recipe with scaled quantities

Example:

recipe, _ := cooklang.ParseFile("cookies.cook")
doubled := recipe.Scale(2.0)  // Double all quantities
halved := recipe.Scale(0.5)   // Half all quantities
Example

ExampleRecipe_Scale demonstrates scaling a recipe by a factor

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `---
title: Pancakes
servings: 2
---
Mix @flour{200%g} with @milk{300%ml} and @eggs{2}.`

	recipe, _ := cooklang.ParseString(recipeText)

	// Double the recipe
	doubled := recipe.Scale(2.0)

	fmt.Printf("Original servings: %.0f\n", recipe.Servings)
	fmt.Printf("Doubled servings: %.0f\n", doubled.Servings)

	// Show scaled ingredients
	ingredients := doubled.GetIngredients()
	for _, ing := range ingredients.Ingredients {
		if ing.Unit != "" {
			fmt.Printf("- %s: %.0f %s\n", ing.Name, ing.Quantity, ing.Unit)
		} else {
			fmt.Printf("- %s: %.0f\n", ing.Name, ing.Quantity)
		}
	}
}
Output:
Original servings: 2
Doubled servings: 4
- flour: 400 g
- milk: 600 ml
- eggs: 4

func (*Recipe) ScaleToServings added in v0.3.0

func (r *Recipe) ScaleToServings(targetServings float64) *Recipe

ScaleToServings creates a new recipe scaled to the target number of servings. If the recipe doesn't have servings specified, it assumes 1 serving.

Parameters:

  • targetServings: The desired number of servings

Returns:

  • *Recipe: A new recipe scaled to the target servings

Example:

recipe, _ := cooklang.ParseFile("cookies.cook") // 12 servings
scaled := recipe.ScaleToServings(24)            // Double the recipe
Example

ExampleRecipe_ScaleToServings demonstrates scaling a recipe to target servings

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `---
title: Cookies
servings: 12
---
Mix @flour{300%g}, @sugar{150%g}, and @butter{100%g}.`

	recipe, _ := cooklang.ParseString(recipeText)

	// Scale from 12 to 36 servings (triple)
	scaled := recipe.ScaleToServings(36)

	fmt.Printf("Original: %.0f servings\n", recipe.Servings)
	fmt.Printf("Scaled: %.0f servings\n", scaled.Servings)

	ingredients := scaled.GetIngredients()
	for _, ing := range ingredients.Ingredients {
		fmt.Printf("- %s: %.0f %s\n", ing.Name, ing.Quantity, ing.Unit)
	}
}
Output:
Original: 12 servings
Scaled: 36 servings
- flour: 900 g
- sugar: 450 g
- butter: 300 g

func (*Recipe) SetRenderer

func (r *Recipe) SetRenderer(renderer RecipeRenderer)

SetRenderer allows setting a custom renderer for a recipe. Once set, calling Render() will use this custom renderer instead of the default.

Parameters:

  • renderer: A RecipeRenderer implementation

Example:

recipe, _ := cooklang.ParseFile("recipe.cook")
recipe.SetRenderer(renderers.MarkdownRenderer{})
markdown := recipe.Render()

func (*Recipe) SetRendererFunc

func (r *Recipe) SetRendererFunc(renderFunc func(*Recipe) string)

SetRendererFunc allows setting a custom renderer function for a recipe

type RecipeReference added in v1.0.5

type RecipeReference struct {
	Path          string        `json:"path"`                     // Relative path to the referenced recipe
	Quantity      float32       `json:"quantity,omitempty"`       // Quantity (scaling factor, servings, or unit amount)
	Unit          string        `json:"unit,omitempty"`           // Unit (e.g., "servings", "ml", or empty for factor)
	NextComponent StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

RecipeReference represents a reference to another recipe file (e.g., @./sauces/Hollandaise{150%g}). Path is relative to the recipe root directory, without the .cook extension.

func (*RecipeReference) GetNext added in v1.0.5

func (r *RecipeReference) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (*RecipeReference) Render added in v1.0.5

func (r *RecipeReference) Render() string

Render returns a human-readable string representation of the recipe reference.

func (*RecipeReference) SetNext added in v1.0.5

func (r *RecipeReference) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type RecipeRenderer

type RecipeRenderer interface {
	RenderRecipe(recipe *Recipe) string
}

RecipeRenderer interface defines how recipes can be rendered to different output formats. Implementations can render recipes as Markdown, HTML, plain text, or any custom format.

Example implementation:

type JSONRenderer struct{}
func (jr JSONRenderer) RenderRecipe(recipe *cooklang.Recipe) string {
    data, _ := json.MarshalIndent(recipe, "", "  ")
    return string(data)
}

type RecipeResolver added in v1.0.5

type RecipeResolver interface {
	// Resolve takes a relative recipe path (e.g., "./sauces/Hollandaise") and returns
	// the parsed Recipe. The path is relative to the recipe root, without the .cook extension.
	Resolve(path string) (*Recipe, error)
}

RecipeResolver resolves recipe references to parsed Recipe objects.

type RendererFunc

type RendererFunc func(*Recipe) string

RendererFunc is a function type that implements RecipeRenderer. This allows using plain functions as renderers without creating a new type.

Example:

simpleRenderer := cooklang.RendererFunc(func(r *cooklang.Recipe) string {
    return fmt.Sprintf("# %s\n\nServings: %.0f", r.Title, r.Servings)
})
output := recipe.RenderWith(simpleRenderer)

func (RendererFunc) RenderRecipe

func (f RendererFunc) RenderRecipe(recipe *Recipe) string

RenderRecipe implements the RecipeRenderer interface for RendererFunc.

type Section added in v0.3.0

type Section struct {
	Name          string        `json:"name,omitempty"`           // Section name (e.g., "Dough", "Filling")
	NextComponent StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

Section represents a section header in a recipe. Sections divide complex recipes into logical parts (e.g., "Dough", "Filling").

Example Cooklang syntax: = Dough, == Filling ==

func (*Section) GetNext added in v0.3.0

func (s *Section) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (Section) Render added in v0.3.0

func (s Section) Render() string

Render returns the Cooklang syntax representation of this section. Examples: "== Section Name =="

func (Section) RenderDisplay added in v0.3.0

func (s Section) RenderDisplay() string

RenderDisplay returns section name suitable for display.

func (*Section) SetNext added in v0.3.0

func (s *Section) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type ShoppingList

type ShoppingList struct {
	Ingredients *IngredientList `json:"ingredients"`       // Consolidated ingredient list
	Recipes     []string        `json:"recipes,omitempty"` // List of recipe titles included
}

ShoppingList represents a consolidated list of ingredients from multiple recipes. It combines ingredients across recipes and provides a unified shopping list with recipe attribution.

func CreateShoppingList

func CreateShoppingList(recipes ...*Recipe) (*ShoppingList, error)

CreateShoppingList creates a consolidated shopping list from multiple recipes. All ingredients from all recipes are combined and consolidated by name, automatically converting compatible units and summing quantities.

Parameters:

  • recipes: Variable number of Recipe pointers to include in the shopping list

Returns:

  • *ShoppingList: A shopping list with consolidated ingredients
  • error: Any error encountered during consolidation

Example:

recipe1, _ := cooklang.ParseFile("pasta.cook")
recipe2, _ := cooklang.ParseFile("salad.cook")
shoppingList, err := cooklang.CreateShoppingList(recipe1, recipe2)
if err == nil {
    for name, amount := range shoppingList.ToMap() {
        fmt.Printf("%s: %s\n", name, amount)
    }
}
Example

ExampleCreateShoppingList demonstrates creating a shopping list from multiple recipes

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	recipe1Text := `---
title: Pasta
---
Cook @pasta{400%g} with @olive oil{2%tbsp}.`

	recipe2Text := `---
title: Salad
---
Mix @olive oil{3%tbsp} with @lettuce{100%g}.`

	recipe1, _ := cooklang.ParseString(recipe1Text)
	recipe2, _ := cooklang.ParseString(recipe2Text)

	shoppingList, err := cooklang.CreateShoppingList(recipe1, recipe2)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Shopping list for %d recipes:\n", len(shoppingList.Recipes))
	ingredientMap := shoppingList.ToMap()

	// Print in deterministic order
	fmt.Printf("- lettuce: %s\n", ingredientMap["lettuce"])
	fmt.Printf("- olive oil: %s\n", ingredientMap["olive oil"])
	fmt.Printf("- pasta: %s\n", ingredientMap["pasta"])
}
Output:
Shopping list for 2 recipes:
- lettuce: 100 g
- olive oil: 5 tbsp
- pasta: 400 g

func CreateShoppingListForServings added in v1.0.0

func CreateShoppingListForServings(targetServings float64, recipes ...*Recipe) (*ShoppingList, error)

CreateShoppingListForServings creates a shopping list by scaling each recipe to the target number of servings before combining ingredients.

This is ideal for meal planning where recipes have different serving sizes and you want to normalize them all to your household size.

Each recipe is scaled from its original servings to the target servings, then all ingredients are combined and consolidated.

Parameters:

  • targetServings: The desired number of servings for each recipe
  • recipes: Variable number of recipe pointers to combine

Returns:

  • *ShoppingList: Consolidated shopping list with all ingredients scaled
  • error: Error if consolidation fails

Example:

monday, _ := cooklang.ParseFile("monday.cook")     // servings: 2
tuesday, _ := cooklang.ParseFile("tuesday.cook")  // servings: 8
wednesday, _ := cooklang.ParseFile("wednesday.cook") // servings: 1 (default)

// Create shopping list for household of 5
list, _ := cooklang.CreateShoppingListForServings(5, monday, tuesday, wednesday)
// monday scaled 2.5x, tuesday scaled 0.625x, wednesday scaled 5x
Example

ExampleCreateShoppingListForServings demonstrates creating a shopping list for a specific number of servings across multiple recipes

package main

import (
	"fmt"
	"log"

	"github.com/hilli/cooklang"
)

func main() {
	mondayDinner := `---
title: Pasta
servings: 2
---
Cook @pasta{200%g} with @olive oil{2%tbsp}.`

	tuesdayDinner := `---
title: Rice Bowl
servings: 1
---
Serve @rice{150%g} with @olive oil{1%tbsp}.`

	recipe1, _ := cooklang.ParseString(mondayDinner)
	recipe2, _ := cooklang.ParseString(tuesdayDinner)

	// Create shopping list for 4 servings of each recipe
	list, err := cooklang.CreateShoppingListForServings(4, recipe1, recipe2)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Shopping list (4 servings each):")
	ingredientMap := list.ToMap()

	// Print in deterministic order
	fmt.Printf("- olive oil: %s\n", ingredientMap["olive oil"])
	fmt.Printf("- pasta: %s\n", ingredientMap["pasta"])
	fmt.Printf("- rice: %s\n", ingredientMap["rice"])
}
Output:
Shopping list (4 servings each):
- olive oil: 8 tbsp
- pasta: 400 g
- rice: 600 g

func CreateShoppingListForServingsWithUnit added in v1.0.0

func CreateShoppingListForServingsWithUnit(targetServings float64, targetUnit string, recipes ...*Recipe) (*ShoppingList, error)

CreateShoppingListForServingsWithUnit creates a shopping list by scaling each recipe to the target servings and converting ingredients to the target unit.

This combines the functionality of CreateShoppingListForServings and CreateShoppingListWithUnit for meal planning with unit standardization.

Parameters:

  • targetServings: The desired number of servings for each recipe
  • targetUnit: The unit to convert compatible ingredients to (e.g., "g", "ml", "kg")
  • recipes: Variable number of recipe pointers to combine

Returns:

  • *ShoppingList: Consolidated shopping list with scaled and converted ingredients
  • error: Error if conversion or consolidation fails

Example:

// Create shopping list for 4 servings with metric units
list, _ := cooklang.CreateShoppingListForServingsWithUnit(4, "g", recipes...)

func CreateShoppingListWithUnit

func CreateShoppingListWithUnit(targetUnit string, recipes ...*Recipe) (*ShoppingList, error)

CreateShoppingListWithUnit creates a shopping list with ingredients converted to the target unit. All compatible ingredients are converted to the specified unit before consolidation.

Parameters:

  • targetUnit: The unit to convert ingredients to (e.g., "g", "ml", "oz", "kg")
  • recipes: Variable number of Recipe pointers to include

Returns:

  • *ShoppingList: A shopping list with converted and consolidated ingredients
  • error: Any error encountered during consolidation

Example:

recipe1, _ := cooklang.ParseFile("pasta.cook")
recipe2, _ := cooklang.ParseFile("salad.cook")
// Get shopping list with all weights in grams
list, _ := cooklang.CreateShoppingListWithUnit("g", recipe1, recipe2)

func (*ShoppingList) Count

func (sl *ShoppingList) Count() int

Count returns the number of unique ingredients in the shopping list.

Returns:

  • int: The number of ingredients (0 if the list is empty or nil)

Example:

list, _ := cooklang.CreateShoppingList(recipe1, recipe2)
fmt.Printf("You need %d ingredients\n", list.Count())

func (*ShoppingList) Scale

func (sl *ShoppingList) Scale(multiplier float64) *ShoppingList

Scale scales all ingredients in the shopping list by the given multiplier. This is useful when adjusting recipe servings or batch cooking. Ingredients with "some" quantity (-1) are not scaled.

Parameters:

  • multiplier: The scaling factor (e.g., 2.0 for double, 0.5 for half)

Returns:

  • *ShoppingList: A new shopping list with scaled quantities

Example:

shoppingList, _ := cooklang.CreateShoppingList(recipe)
doubled := shoppingList.Scale(2.0)  // Double all quantities
halved := shoppingList.Scale(0.5)   // Half all quantities
Example

ExampleShoppingList_Scale demonstrates scaling a shopping list

package main

import (
	"fmt"
	"sort"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Use @flour{200%g} and @sugar{100%g}.`

	recipe, _ := cooklang.ParseString(recipeText)
	shoppingList, _ := cooklang.CreateShoppingList(recipe)

	// Double the recipe
	scaled := shoppingList.Scale(2.0)

	fmt.Println("Scaled (×2):")
	scaledMap := scaled.ToMap()
	// Sort keys for consistent output
	keys := make([]string, 0, len(scaledMap))
	for name := range scaledMap {
		keys = append(keys, name)
	}
	sort.Strings(keys)

	for _, name := range keys {
		fmt.Printf("- %s: %s\n", name, scaledMap[name])
	}
}
Output:
Scaled (×2):
- flour: 400 g
- sugar: 200 g

func (*ShoppingList) ToMap

func (sl *ShoppingList) ToMap() map[string]string

ToMap returns the shopping list as a map of ingredient names to formatted quantities. This is a convenience method that delegates to IngredientList.ToMap().

Returns:

  • map[string]string: A map of ingredient names to quantity strings

Example:

list, _ := cooklang.CreateShoppingList(recipe1, recipe2)
for name, qty := range list.ToMap() {
    fmt.Printf("- %s: %s\n", name, qty)
}

type SmartUnitResult

type SmartUnitResult struct {
	Value    float64 // The numeric value
	Unit     string  // The selected unit name
	Original string  // Original formatted value for reference
}

SmartUnitResult contains the result of intelligent unit selection

func ConvertVolumeBartender

func ConvertVolumeBartender(value float64, fromUnit string, toSystem UnitSystem) SmartUnitResult

ConvertVolumeBartender converts a volume from one unit to another using bartender-friendly rounding. First converts to ml, then selects the best unit in the target system.

Parameters:

  • value: The numeric quantity to convert
  • fromUnit: The source unit (e.g., "oz", "ml")
  • toSystem: The target unit system

Returns:

  • SmartUnitResult: Contains the converted value and selected unit

Example:

result := cooklang.ConvertVolumeBartender(1.5, "oz", cooklang.UnitSystemMetric)
fmt.Printf("%v %s\n", result.Value, result.Unit) // "45 ml"

func SelectBestUnit

func SelectBestUnit(mlValue float64, targetSystem UnitSystem) SmartUnitResult

SelectBestUnit chooses the most appropriate unit for a given volume in ml. This is used in bartender mode to pick human-friendly units.

Features:

  • Very small amounts (≤3ml) → dashes
  • Small amounts → barspoons or fractional oz
  • Standard amounts → oz with nice fractions
  • Large amounts → cups
  • Metric: rounds to nearest 5ml for amounts ≥30ml, 2.5ml for smaller amounts

Parameters:

  • mlValue: The volume in milliliters
  • targetSystem: The desired unit system (UnitSystemMetric or UnitSystemUS)

Returns:

  • SmartUnitResult: Contains the converted value and selected unit

Example:

result := cooklang.SelectBestUnit(45, cooklang.UnitSystemUS)
fmt.Printf("%v %s\n", result.Value, result.Unit) // "1.5 oz"

type Step

type Step struct {
	FirstComponent StepComponent `json:"first_component,omitempty"` // First component in this step
	NextStep       *Step         `json:"next_step,omitempty"`       // Next step in the recipe
	CooklangRenderable
}

Step represents a single step in a recipe's instructions. Each step contains a linked list of components (ingredients, cookware, timers, text instructions) and a link to the next step.

Steps are traversed by following the NextStep pointer to iterate through the recipe's instructions.

func (*Step) HasDisplayableContent added in v1.0.0

func (s *Step) HasDisplayableContent() bool

HasDisplayableContent returns true if the step contains any content that should be displayed to users (ingredients, cookware, timers, non-whitespace text, sections, or notes). Steps containing only comments or whitespace-only text return false.

type StepComponent

type StepComponent interface {
	Render() string         // Renders the component as Cooklang syntax
	SetNext(StepComponent)  // Sets the next component in the linked list
	GetNext() StepComponent // Gets the next component in the linked list
	// contains filtered or unexported methods
}

StepComponent represents a component within a recipe step (ingredient, instruction, timer, or cookware). Components are organized as a linked list within each step, allowing iteration through the sequence of actions.

type Timer

type Timer struct {
	Duration      string        `json:"duration,omitempty"`       // Duration value (e.g., "10")
	Name          string        `json:"name,omitempty"`           // Timer name/description (e.g., "boil", "rest")
	Text          string        `json:"text,omitempty"`           // Full timer text
	Unit          string        `json:"unit,omitempty"`           // Time unit (e.g., "minutes", "hours")
	Annotation    string        `json:"annotation,omitempty"`     // Optional annotation
	NextComponent StepComponent `json:"next_component,omitempty"` // Next component in the step
	CooklangRenderable
}

Timer represents a duration timer in a recipe step. Timers specify how long to perform an action.

Example Cooklang syntax: ~{10%minutes}, ~boil{15%min}

Example

ExampleTimer demonstrates working with timers in recipes

package main

import (
	"fmt"

	"github.com/hilli/cooklang"
)

func main() {
	recipeText := `Boil for ~{10%minutes}.
Rest for ~cooling{5%minutes}.`

	recipe, _ := cooklang.ParseString(recipeText)

	// Walk through steps to find timers
	step := recipe.FirstStep
	timersFound := 0
	for step != nil {
		component := step.FirstComponent
		for component != nil {
			if timer, ok := component.(*cooklang.Timer); ok {
				timersFound++
				if timer.Name != "" {
					fmt.Printf("Timer: %s (%s)\n", timer.Name, timer.Duration)
				} else {
					fmt.Printf("Timer: %s\n", timer.Duration)
				}
			}
			component = component.GetNext()
		}
		step = step.NextStep
	}
	fmt.Printf("Total timers: %d\n", timersFound)
}
Output:
Timer: 10
Timer: cooling (5)
Total timers: 2

func (*Timer) GetNext

func (t *Timer) GetNext() StepComponent

GetNext returns the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

func (Timer) Render

func (t Timer) Render() string

Render returns the Cooklang syntax representation of this timer. Examples: "~{10%minutes}", "~boil{15%min}"

func (Timer) RenderDisplay

func (t Timer) RenderDisplay() string

RenderDisplay returns timer in plain text format suitable for display. Returns the duration with unit if available, or just the duration. If the timer has a name but no duration, returns the name.

func (*Timer) SetNext

func (t *Timer) SetNext(next StepComponent)

SetNext sets the next component in the step's linked list. This implements the StepComponent interface for recipe step traversal.

type UnitSystem

type UnitSystem string

UnitSystem defines supported unit systems for easy conversion

const (
	UnitSystemMetric   UnitSystem = "metric"
	UnitSystemImperial UnitSystem = "imperial"
	UnitSystemUS       UnitSystem = "us"
)
const UnitSystemUnknown UnitSystem = "unknown"

UnitSystemUnknown represents an unknown or undetectable unit system

func DetectIngredientListUnitSystem

func DetectIngredientListUnitSystem(il *IngredientList) UnitSystem

DetectIngredientListUnitSystem detects the dominant unit system in an ingredient list. It counts US vs metric units and returns the more common one. Returns UnitSystemUS as default when tied (most cocktail recipes are US-based).

Parameters:

  • il: The ingredient list to analyze

Returns:

  • UnitSystem: The dominant system, or UnitSystemUnknown if no units found

Example:

ingredients := cocktail.GetIngredients()
system := cooklang.DetectIngredientListUnitSystem(ingredients)
if system == cooklang.UnitSystemMetric {
    fmt.Println("Recipe uses metric measurements")
}

func DetectUnitSystemFromUnit

func DetectUnitSystemFromUnit(unitName string) UnitSystem

DetectUnitSystemFromUnit determines the unit system from a single unit name. Returns UnitSystemUnknown if the unit is not recognized or is cocktail-specific.

Parameters:

  • unitName: The unit to check (e.g., "ml", "oz", "cup")

Returns:

  • UnitSystem: The detected system (UnitSystemMetric, UnitSystemUS, or UnitSystemUnknown)

Example:

system := cooklang.DetectUnitSystemFromUnit("cup")
// system == UnitSystemUS

Directories

Path Synopsis
cmd
cook command
Package pantry implements parsing for the Cooklang pantry configuration format.
Package pantry implements parsing for the Cooklang pantry configuration format.
Package renderers provides different renderers for Cooklang recipes.
Package renderers provides different renderers for Cooklang recipes.
Package shoppinglist implements parsing for the Cooklang shopping list file format.
Package shoppinglist implements parsing for the Cooklang shopping list file format.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL