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 ¶
- Constants
- func CreateTypedUnit(unitStr string) *units.Unit
- func FormatAsFraction(value float64, tolerance float64) string
- func FormatAsFractionDefault(value float64) string
- func FormatBartenderValue(result SmartUnitResult) string
- func IsCocktailSpecificUnit(unitName string) bool
- func IsNiceFraction(value float64, tolerance float64) bool
- func ParseFraction(s string) (float64, error)
- func ParseYield(yieldStr string) (float64, string)
- func RoundToNiceFraction(value float64, tolerance float64) float64
- func ScaleByYield(recipe *Recipe, targetQuantity float64, targetUnit string) (float64, error)
- func ShouldSkipConversion(sourceUnit string, targetSystem UnitSystem) bool
- type CocktailUnit
- type Comment
- type ConversionMode
- type CooklangRenderable
- type Cookware
- type FileSystemResolver
- type FrontmatterEditor
- func (fe *FrontmatterEditor) AppendToArray(key, value string) error
- func (fe *FrontmatterEditor) DeleteMetadata(key string) error
- func (fe *FrontmatterEditor) GetAllMetadata() map[string]string
- func (fe *FrontmatterEditor) GetContent() string
- func (fe *FrontmatterEditor) GetMetadata(key string) (string, bool)
- func (fe *FrontmatterEditor) GetUpdatedContent() string
- func (fe *FrontmatterEditor) RemoveFromArray(key, value string) error
- func (fe *FrontmatterEditor) Save() error
- func (fe *FrontmatterEditor) SaveAs(filePath string) error
- func (fe *FrontmatterEditor) SetMetadata(key, value string) error
- type Ingredient
- func (i *Ingredient) CanConvertTo(targetUnitStr string) bool
- func (i *Ingredient) ConvertTo(targetUnitStr string) (*Ingredient, error)
- func (i *Ingredient) ConvertToSystem(system UnitSystem) *Ingredient
- func (i *Ingredient) ConvertToSystemBartender(system UnitSystem) *Ingredient
- func (i *Ingredient) FormatQuantityBartender() string
- func (i *Ingredient) GetNext() StepComponent
- func (i *Ingredient) GetUnitType() string
- func (i Ingredient) Render() string
- func (i Ingredient) RenderDisplay() string
- func (i *Ingredient) SetNext(next StepComponent)
- type IngredientList
- func (il *IngredientList) Add(ingredient *Ingredient)
- func (il *IngredientList) ConsolidateByName(targetUnit string) (*IngredientList, error)
- func (il *IngredientList) ConvertToSystem(system UnitSystem) *IngredientList
- func (il *IngredientList) ConvertToSystemBartender(system UnitSystem) *IngredientList
- func (il *IngredientList) ConvertToSystemWithConsolidation(system UnitSystem) (*IngredientList, error)
- func (il *IngredientList) GetIngredientsByName(name string) []*Ingredient
- func (il *IngredientList) ToMap() map[string]string
- type Instruction
- type Menu
- type MenuDay
- type MenuRecipe
- type Metadata
- type Note
- type Recipe
- func (r *Recipe) GetCollectedIngredients() (*IngredientList, error)
- func (r *Recipe) GetCollectedIngredientsMap() (map[string]string, error)
- func (r *Recipe) GetCollectedIngredientsWithUnit(targetUnit string) (*IngredientList, error)
- func (r *Recipe) GetCookware() []*Cookware
- func (r *Recipe) GetImperialShoppingList() (map[string]string, error)
- func (r *Recipe) GetIngredients() *IngredientList
- func (r *Recipe) GetMetricShoppingList() (map[string]string, error)
- func (r *Recipe) GetShoppingListInSystem(system UnitSystem) (map[string]string, error)
- func (r *Recipe) GetUSShoppingList() (map[string]string, error)
- func (r *Recipe) Render() string
- func (r *Recipe) RenderWith(renderer RecipeRenderer) string
- func (r *Recipe) Scale(factor float64) *Recipe
- func (r *Recipe) ScaleToServings(targetServings float64) *Recipe
- func (r *Recipe) SetRenderer(renderer RecipeRenderer)
- func (r *Recipe) SetRendererFunc(renderFunc func(*Recipe) string)
- type RecipeReference
- type RecipeRenderer
- type RecipeResolver
- type RendererFunc
- type Section
- type ShoppingList
- func CreateShoppingList(recipes ...*Recipe) (*ShoppingList, error)
- func CreateShoppingListForServings(targetServings float64, recipes ...*Recipe) (*ShoppingList, error)
- func CreateShoppingListForServingsWithUnit(targetServings float64, targetUnit string, recipes ...*Recipe) (*ShoppingList, error)
- func CreateShoppingListWithUnit(targetUnit string, recipes ...*Recipe) (*ShoppingList, error)
- type SmartUnitResult
- type Step
- type StepComponent
- type Timer
- type UnitSystem
Examples ¶
- Cookware
- CreateShoppingList
- CreateShoppingListForServings
- FrontmatterEditor.GetMetadata
- Ingredient.CanConvertTo
- Ingredient.ConvertTo
- Ingredient.GetUnitType
- Ingredient.Render
- IngredientList.ConsolidateByName
- IngredientList.ConvertToSystem
- IngredientList.ToMap
- NewFrontmatterEditor
- ParseFile
- ParseString
- Recipe.GetCollectedIngredients
- Recipe.GetCollectedIngredientsMap
- Recipe.GetCookware
- Recipe.GetIngredients
- Recipe.GetMetricShoppingList
- Recipe.GetUSShoppingList
- Recipe.Render
- Recipe.Scale
- Recipe.ScaleToServings
- ShoppingList.Scale
- Timer
Constants ¶
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
const DefaultFractionTolerance = 0.02
DefaultFractionTolerance is the default tolerance for matching fractions
Variables ¶
This section is empty.
Functions ¶
func CreateTypedUnit ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
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
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
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
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 ¶
Render returns the Cooklang syntax representation of this cookware. Examples: "#pot{}", "#bowl{2}", "#oven{}(preheated)"
func (Cookware) RenderDisplay ¶
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.
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.
Output:
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 ¶ added in v1.0.5
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
ParseMenuFile reads and parses a .menu file from disk.
func ParseMenuString ¶ added in v1.0.5
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 ¶ added in v1.0.5
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 ¶ added in v1.0.5
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 ¶
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
Render returns the Cooklang syntax representation of this note. Example: "> This is a note"
func (Note) RenderDisplay ¶ added in v0.4.0
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 ¶
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 ¶
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 ¶
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:
- No unit — scales by the given factor
- "servings" unit — scales to target servings
- Other units — uses yield-based scaling (experimental)
func ToCooklangRecipe ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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
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 ¶
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 ¶
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 ¶
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
Render returns the Cooklang syntax representation of this section. Examples: "== Section Name =="
func (Section) RenderDisplay ¶ added in v0.3.0
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
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 ¶
Render returns the Cooklang syntax representation of this timer. Examples: "~{10%minutes}", "~boil{15%min}"
func (Timer) RenderDisplay ¶
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
Source Files
¶
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. |