Cooklang
A Ruby parser for the Cooklang recipe markup language.
Requirements
- Ruby 2.7 or higher
Installation
Add to your Gemfile:
gem 'cooklang'
Or install directly:
gem install cooklang
Usage
require 'cooklang'
recipe_text = <<~RECIPE
>> title: Pancakes
>> servings: 4
Crack @eggs{3} into a bowl, add @flour{125%g} and @milk{250%ml}.
Heat #frying pan over medium heat for ~{5%minutes}.
Pour batter and cook until golden.
RECIPE
recipe = Cooklang.parse(recipe_text)
# Access metadata
recipe.metadata['title'] # => "Pancakes"
recipe.metadata['servings'] # => 4
# Access ingredients
recipe.ingredients.each do |ingredient|
puts "#{ingredient.name}: #{ingredient.quantity} #{ingredient.unit}"
end
# => eggs: 3
# => flour: 125 g
# => milk: 250 ml
# Parse from file
recipe = Cooklang.parse_file('pancakes.cook')
Formatters
The gem provides three built-in formatters for different output needs. Here's an example using Uncle Roger's Fried Rice recipe:
>> title: Uncle Roger's Fried Rice
>> servings: 2
>> prep_time: 10 minutes
>> cook_time: 8 minutes
Heat @peanut oil{1%tbsp} in #wok over high heat until smoking.
Add @garlic{3%cloves}(minced) and @shallots{2}(diced), stir-fry for ~{30%seconds}.
Add @eggs{3}(beaten) and scramble until almost set.
Add @day-old rice{3%cups} and stir-fry, breaking up clumps for ~{2%minutes}.
Season with @soy sauce{2%tbsp}, @sesame oil{1%tbsp}, @msg{1%tsp}, and @white pepper{0.5%tsp}.
Add @spring onions{3%stalks}(chopped) and stir for ~{30%seconds} more.
Serve immediately while hot!
Text Formatter
Human-readable format with ingredients list and numbered steps:
Usage: Cooklang::Formatters::Text.new(recipe).generate
Ingredients:
peanut oil 1 tbsp
garlic 3 cloves
shallots 2
eggs 3
day-old rice 3 cups
soy sauce 2 tbsp
sesame oil 1 tbsp
msg 1 tsp
white pepper 0.5 tsp
spring onions 3 stalks
Steps:
1. Heat peanut oil in wok over high heat until smoking.
2. Add garlic and shallots, stirfry for for 30 seconds.
3. Add eggs and scramble until almost set.
4. Add day-old rice and stirfry, breaking up clumps for for 2 minutes.
5. Season with soy sauce, sesame oil, msg, and white pepper.
6. Add spring onions and stir for for 30 seconds more.
7. Serve immediately while hot!
Hash Formatter
Structured Ruby hash format compatible with Cook CLI:
Usage: Cooklang::Formatters::Hash.new(recipe).generate
{:metadata=>
{:map=>
{"title"=>"Uncle Roger's Fried Rice",
"servings"=>"2",
"prep_time"=>"10 minutes",
"cook_time"=>"8 minutes"}},
:sections=>
[{:name=>nil,
:content=>
[{:type=>"step",
:value=>
{:items=>
[{:type=>"text", :value=>"Heat "},
{:type=>"ingredient", :index=>0},
{:type=>"text", :value=>" in "},
{:type=>"cookware", :index=>0},
{:type=>"text", :value=>" over high heat until smoking."}],
:number=>1}},
{:type=>"step",
:value=>
{:items=>
[{:type=>"text", :value=>"Add "},
{:type=>"ingredient", :index=>1},
{:type=>"text", :value=>" and "},
{:type=>"ingredient", :index=>2},
{:type=>"text", :value=>" and stir for "},
{:type=>"timer", :index=>0}],
:number=>2}}]}],
:ingredients=>
[{:name=>"peanut oil",
:quantity=>
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>1.0}},
:unit=>"tbsp"}},
{:name=>"garlic",
:quantity=>
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>3.0}},
:unit=>"cloves"},
:note=>"minced"},
{:name=>"spring onions",
:quantity=>
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>3.0}},
:unit=>"stalks"},
:note=>"chopped"}],
:cookware=>[{:name=>"wok"}],
:timers=>
[{:quantity=>
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>30.0}},
:unit=>"seconds"}},
{:quantity=>
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>2.0}},
:unit=>"minutes"}}]}
JSON Formatter
JSON string format for APIs and data interchange:
Usage: Cooklang::Formatters::Json.new(recipe).generate
{
"metadata": {
"map": {
"title": "Uncle Roger's Fried Rice",
"servings": "2",
"prep_time": "10 minutes",
"cook_time": "8 minutes"
}
},
"sections": [
{
"name": null,
"content": [
{
"type": "step",
"value": {
"items": [
{"type": "text", "value": "Heat "},
{"type": "ingredient", "index": 0},
{"type": "text", "value": " in "},
{"type": "cookware", "index": 0},
{"type": "text", "value": " over high heat until smoking."}
],
"number": 1
}
},
{
"type": "step",
"value": {
"items": [
{"type": "text", "value": "Add "},
{"type": "ingredient", "index": 1},
{"type": "text", "value": " and stir for "},
{"type": "timer", "index": 0}
],
"number": 2
}
}
]
}
],
"ingredients": [
{
"name": "peanut oil",
"quantity": {
"value": {"type": "number", "value": {"type": "regular", "value": 1.0}},
"unit": "tbsp"
}
},
{
"name": "garlic",
"quantity": {
"value": {"type": "number", "value": {"type": "regular", "value": 3.0}},
"unit": "cloves"
},
"note": "minced"
}
],
"cookware": [{"name": "wok"}],
"timers": [
{
"quantity": {
"value": {"type": "number", "value": {"type": "regular", "value": 30.0}},
"unit": "seconds"
}
}
]
}
API
# Recipe object
recipe.metadata # Hash of metadata
recipe.ingredients # Array of Ingredient objects
recipe.cookware # Array of Cookware objects
recipe.timers # Array of Timer objects
recipe.steps # Array of Step objects
recipe.steps_text # Array of plain text steps
# Ingredient
ingredient.name # "flour"
ingredient.quantity # 125
ingredient.unit # "g"
ingredient.notes # "sifted"
# Cookware
cookware.name # "frying pan"
cookware.quantity # 1
# Timer
timer.name # "pasta"
timer.duration # 10
timer.unit # "minutes"
Cooklang Syntax
-
Ingredients:
@salt
,@flour{125%g}
,@onion{1}(diced)
-
Cookware:
#pan
,#mixing bowl{}
-
Timers:
~{5%minutes}
,~pasta{10%minutes}
-
Comments:
-- line comment
,[- block comment -]
-
Metadata:
>> key: value
or YAML front matter
Development
# Install dependencies
bundle install
# Run tests
bundle exec rspec
# Run linter
bundle exec rubocop
Resources
Contributing
Bug reports and pull requests welcome on GitHub.
License
MIT License