Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c385726

Browse files
claudexcapaldi
authored andcommitted
docs: write README as a valid RecipeMD recipe
1 parent cdf9f87 commit c385726

1 file changed

Lines changed: 323 additions & 1 deletion

File tree

README.md

Lines changed: 323 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,325 @@
11
# recipemd-go
22

3-
Go implementation of RecipeMD parser.
3+
<p>
4+
<a href="https://github.com/xcapaldi/recipemd-go/releases"><img src="https://img.shields.io/github/release/xcapaldi/recipemd-go.svg" alt="Latest Release"></a>
5+
<a href="https://pkg.go.dev/xcapaldi/recipemd-go?tab=doc"><img src="https://godoc.org/xcapaldi/recipemd-go?status.svg" alt="GoDoc"></a>
6+
</p>
7+
8+
A Go library for parsing, scaling, and rendering recipes in the [RecipeMD](https://recipemd.org) format.
9+
This format builds on top of structured Markdown such that both humans and programs can digest it.
10+
11+
*Go, parser, RecipeMD, Markdown*
12+
13+
**1 Go module**
14+
15+
---
16+
17+
- *1 pkg* `github.com/xcapaldi/recipemd-go`
18+
- *1 pkg* `github.com/yuin/goldmark` Markdown parser
19+
20+
## Examples
21+
22+
- *1* `parse` — parse a recipe and output JSON
23+
- *1* `scale` — scale a recipe by factor or target yield
24+
- *1* `flatten` — inline all linked sub-recipe ingredients
25+
- *1* `renderhtml` — render a recipe as an HTML `<article>`
26+
27+
---
28+
29+
## Installation
30+
31+
```bash
32+
go get github.com/xcapaldi/recipemd-go
33+
```
34+
35+
## What is RecipeMD?
36+
37+
[RecipeMD](https://recipemd.org/specification.html) is a Markdown-based format for writing recipes.
38+
A recipe file is plain Markdown with a defined structure:
39+
40+
```markdown
41+
# Carbonara
42+
43+
A classic Roman pasta.
44+
45+
*Italian, pasta*
46+
47+
**2 servings**
48+
49+
---
50+
51+
- *200 g* spaghetti
52+
- *100 g* guanciale
53+
- *2* eggs
54+
- *50 g* Pecorino Romano
55+
56+
## Sauce
57+
58+
- *1 tbsp* black pepper, coarsely ground
59+
60+
---
61+
62+
Boil pasta. Render guanciale. Whisk eggs with cheese and pepper.
63+
Toss together off the heat.
64+
```
65+
66+
The document has three sections divided by `---` thematic breaks:
67+
68+
| Section | Content |
69+
|---|---|
70+
| Preamble | H1 title, optional description, optional *tags* (italic), optional **yields** (bold) |
71+
| Ingredients | Unordered lists; H2+ headings introduce named ingredient groups |
72+
| Instructions | Free-form Markdown text |
73+
74+
Amounts are wrapped in emphasis: `*2 tbsp*`. Supported number formats:
75+
integers (`3`), decimals (`1.5`), fractions (`1/2`), improper fractions
76+
(`1 1/2`), and Unicode vulgar fractions (`½ ¼ ¾`).
77+
78+
## Why RecipeMD?
79+
80+
Shockingly there are multiple competing specifications in the world of plaintext cooking:
81+
82+
- [Open Recipe Format](https://open-recipe-format.readthedocs.io/en/latest/) -- YAML
83+
- [hrecipe](https://microformats.org/wiki/hrecipe) -- XML
84+
- [schema.org Recipe](https://schema.org/Recipe)
85+
- [Cooklang](https://cooklang.org) -- DSL
86+
87+
Of the above, the only one that goes beyond a strictly formatted schema is Cooklang.
88+
It is an extensive project with a CLI, web server and thoughtful features like inline ingredent declarations (these are extracted at render time).
89+
Despite this, it lacks one fundamental advantage working in plain text -- it's not very readable in it's raw format.
90+
This may seem like a minor inconvenience but it makes reading and writing recipes less accessible to humans and (increasingly important) AI agents.
91+
The beauty of RecipeMD is that it's a ruleset on standard Markdown syntax with all of that format's inherent flexibility (images/tables/etc).
92+
The nature of Cooklang's strict DSL enables some advanced features but I believe they can be acheived in RecipeMD through contextual analysis.
93+
94+
As an example working in the raw formats, here is a moderately complex recipe in both:
95+
96+
### RecipeMD
97+
98+
```markdown
99+
# Chicken Tikka Masala
100+
101+
Tender charred chicken in a rich, spiced tomato-cream sauce.
102+
103+
*Indian, chicken, curry*
104+
105+
**4 servings**
106+
107+
---
108+
109+
## Marinade
110+
111+
- *700 g* boneless chicken thighs, cut into chunks
112+
- *150 g* plain yogurt
113+
- *2 tbsp* lemon juice
114+
- *3 cloves* garlic, minced
115+
- *1 tsp* fresh ginger, grated
116+
- *1 tsp* garam masala
117+
- *1 tsp* cumin
118+
- *1/2 tsp* turmeric
119+
- *1/2 tsp* cayenne pepper
120+
- *1 tsp* salt
121+
122+
## Sauce
123+
124+
- *2 tbsp* ghee
125+
- *1* large onion, finely diced
126+
- *4 cloves* garlic, minced
127+
- *1 tbsp* fresh ginger, grated
128+
- *1 tbsp* garam masala
129+
- *1 tsp* cumin
130+
- *1 tsp* coriander
131+
- *1/2 tsp* turmeric
132+
- *1/2 tsp* cayenne pepper
133+
- *400 g* canned crushed tomatoes
134+
- *200 ml* heavy cream
135+
- *1 tsp* salt
136+
137+
## Serving
138+
139+
- *300 g* basmati rice
140+
- *1* handful fresh cilantro, roughly chopped
141+
142+
---
143+
144+
Combine chicken with all marinade ingredients in a bowl. Cover and refrigerate for at least 1 hour, ideally overnight.
145+
146+
Preheat a broiler or grill to high. Thread chicken onto skewers and cook for 10–12 minutes, turning once, until charred in spots and just cooked through. Set aside.
147+
148+
Cook rice according to package directions.
149+
150+
Heat ghee in a large saucepan over medium heat. Add onion and cook for 8–10 minutes until deep golden. Add garlic and ginger; cook 2 minutes until fragrant.
151+
152+
Stir in garam masala, cumin, coriander, turmeric, and cayenne. Toast 1 minute. Add crushed tomatoes and simmer uncovered for 15 minutes, stirring occasionally, until sauce thickens.
153+
154+
Pour in cream and stir to combine. Add the grilled chicken and simmer 5 minutes to meld flavors. Season with salt.
155+
156+
Serve over rice, garnished with fresh cilantro.
157+
```
158+
159+
### Cooklang
160+
161+
```
162+
---
163+
title: Chicken Tikka Masala
164+
description: Tender charred chicken in a rich, spiced tomato-cream sauce.
165+
tags: Indian, chicken, curry
166+
servings: 4
167+
---
168+
169+
== Marinate ==
170+
171+
Combine @chicken thighs{700%g}(boneless, cut into chunks) with @plain yogurt{150%g},
172+
@lemon juice{2%tbsp}, @garlic{3%cloves}(minced), @fresh ginger{1%tsp}(grated),
173+
@garam masala{1%tsp}, @cumin{1%tsp}, @turmeric{1/2%tsp}, @cayenne pepper{1/2%tsp},
174+
and @salt{1%tsp} in a #large bowl{}.
175+
176+
Cover and refrigerate for ~marinating{1%hour} (or overnight for best results).
177+
178+
== Grill Chicken ==
179+
180+
Preheat a #grill or broiler to high. Thread chicken onto #skewers and cook for
181+
~grilling{12%minutes}, turning once, until charred in spots and cooked through. Set aside.
182+
183+
== Cook Rice ==
184+
185+
Cook @basmati rice{300%g} in a #saucepan{} according to package directions (~{18%minutes}).
186+
187+
== Make Sauce ==
188+
189+
Heat @ghee{2%tbsp} in a #large saucepan{} over medium heat.
190+
Add @onion{1%large}(finely diced) and cook for ~onion{10%minutes} until deep golden.
191+
192+
Add @garlic{4%cloves}(minced) and @fresh ginger{1%tbsp}(grated); cook ~{2%minutes} until fragrant.
193+
194+
Stir in @garam masala{1%tbsp}, @cumin{1%tsp}, @coriander{1%tsp}, @turmeric{1/2%tsp}, and @cayenne pepper{1/2%tsp}.
195+
Toast ~{1%minute}, then add @crushed tomatoes{400%g} and simmer uncovered for ~{15%minutes},
196+
stirring occasionally, until thickened.
197+
198+
Pour in @heavy cream{200%ml} and stir to combine.
199+
Add grilled chicken and simmer ~finishing{5%minutes} to meld flavors.
200+
Season with @salt{1%tsp}.
201+
202+
== Serve ==
203+
204+
Plate over rice and garnish with @fresh cilantro{1%handful}(roughly chopped).
205+
```
206+
207+
## Library Usage
208+
209+
### Parsing
210+
211+
`Parse` accepts any `io.Reader`:
212+
213+
```go
214+
import recipemd "github.com/xcapaldi/recipemd-go"
215+
216+
p := recipemd.NewParser()
217+
218+
f, _ := os.Open("carbonara.md")
219+
defer f.Close()
220+
221+
recipe, err := p.Parse(f)
222+
if err != nil {
223+
log.Fatal(err)
224+
}
225+
226+
fmt.Println(recipe.Title) // "Carbonara"
227+
fmt.Println(recipe.Tags) // ["Italian", "pasta"]
228+
fmt.Println(recipe.Yields) // [{Factor:2 Unit:"servings"}]
229+
```
230+
231+
Parse collects all structural and value-level problems via `errors.Join`,
232+
so a single call surfaces every issue at once. Individual errors carry line
233+
and column information:
234+
235+
```go
236+
if parseError, ok := errors.AsType[*recipemd.ParseError](err); ok {
237+
fmt.Printf("line %d, col %d: %s\n", parseError.Line, parseError.Column, parseError.Message)
238+
}
239+
```
240+
241+
#### Parser options
242+
243+
While not part of the official spec, this implementation supports some parser options to make the format more ergonomic.
244+
In particular `WithFrontmatter` will strip YAML/TOML frontmatter before parsing the remaining content as spec-complient RecipeMD.
245+
This is particularly useful if you combine this format with another note management system (like [Denote](https://protesilaos.com/emacs/denote)) that relies on frontmatter.
246+
In addition `WithGithubFormattedMarkdown` enables support for GFM features like tables, autolinks, task lists (in ingredients as well) and strikethrough.
247+
248+
```go
249+
parser := recipemd.NewParser(
250+
recipemd.WithFrontmatter(), // strip YAML/TOML front matter
251+
recipemd.WithGithubFormattedMarkdown(), // enable GFM (tables, task lists, …)
252+
)
253+
```
254+
255+
### Scaling
256+
257+
```go
258+
// Multiply all amounts by a factor.
259+
recipe.Scale(2)
260+
261+
// Scale to a specific yield.
262+
desired, _ := recipemd.ParseAmountString("6 servings")
263+
if err := recipe.ScaleForYield(desired); err != nil {
264+
log.Fatal(err)
265+
}
266+
```
267+
268+
### Rendering
269+
270+
```go
271+
// RecipeMD Markdown (amounts rounded to 2 decimal places)
272+
fmt.Print(p.RenderMarkdown(recipe, 2))
273+
274+
// Compact JSON
275+
data, err := p.RenderJSON(recipe)
276+
277+
// HTML <article> element (amounts rounded to 3 decimal places) (still WIP)
278+
fmt.Println(p.RenderHTML(recipe, 3))
279+
```
280+
281+
## Examples
282+
283+
The `examples/` directory contains small, self-contained programs that
284+
demonstrate common use cases:
285+
286+
| Example | Description |
287+
|---|---|
288+
| [`examples/parse`](examples/parse) | Parse a recipe and write compact JSON to stdout |
289+
| [`examples/scale`](examples/scale) | Scale a recipe by factor or target yield, write RecipeMD to stdout |
290+
| [`examples/flatten`](examples/flatten) | Inline all linked sub-recipes, write RecipeMD to stdout |
291+
| [`examples/renderhtml`](examples/renderhtml) | Flatten and render as an HTML `<article>` |
292+
293+
Run any example directly:
294+
295+
```bash
296+
go run ./examples/parse carbonara.md
297+
go run ./examples/scale carbonara.md "4 servings"
298+
go run ./examples/flatten main_dish.md
299+
go run ./examples/renderhtml carbonara.md
300+
```
301+
302+
## Running tests
303+
304+
```bash
305+
go test ./...
306+
```
307+
308+
The suite includes the [RecipeMD canonical test suite](https://github.com/tstehr/RecipeMD/tree/master/test),
309+
extensive golden tests and unit tests.
310+
311+
## Performance
312+
313+
This library is quite performant compared to the reference implementation but not due to any clever optimizations.
314+
Go is simply much faster and this translates directly.
315+
In practice, the difference should be almost unnoticeable for any level of personal use.
316+
Nevertheless, here is a very crude benchmark parsing and scaling a short recipe:
317+
318+
| Implementation | Command | Runs | Total | Avg/run | Speedup |
319+
|----------------|---------|-----:|------:|--------:|--------:|
320+
| Python recipemd 5.0.0 | `recipemd -y "10 ml" testdata/canonical/recipe.md` | 100 | 13,956 ms | 139.5 ms | 1x |
321+
| Go (examples/scale) | `scale testdata/canonical/recipe.md "10 ml"` | 100 | 434 ms | 4.3 ms | **32x** |
322+
323+
## License
324+
325+
MIT — see [LICENSE](LICENSE).

0 commit comments

Comments
 (0)