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

Skip to content

Commit 5f751d5

Browse files
claudexcapaldi
authored andcommitted
feat: Add RenderHTML method for HTML output
Implements Parser.RenderHTML that converts a Recipe to an HTML <article> element. Markdown fields (Description, Instructions) are rendered to HTML via the parser's goldmark instance. Ingredient amounts are wrapped in <em>, yields in <strong>, and tags in <em> to restore the emphasis conveyed by the original RecipeMD markdown formatting. All elements carry class attributes matching RecipeMD types (recipemd-recipe, recipemd-title, recipemd-preamble, recipemd-description, recipemd-tags, recipemd-yields, recipemd-separator, recipemd-ingredients, recipemd-ingredient-list, recipemd-ingredient, recipemd-amount, recipemd-ingredient-name, recipemd-ingredient-link, recipemd-ingredient-group, recipemd-group-title, recipemd-instructions) for CSS styling. Ingredient group headings use h2-h6 matching nesting depth. Nested groups are supported recursively.
1 parent af19e84 commit 5f751d5

7 files changed

Lines changed: 569 additions & 124 deletions

File tree

examples/flatten/main.go

Lines changed: 2 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
package main
88

99
import (
10-
"bytes"
1110
"fmt"
1211
"os"
13-
"path/filepath"
14-
"strings"
1512

1613
recipemd "github.com/xcapaldi/recipemd-go"
14+
"github.com/xcapaldi/recipemd-go/examples/helper"
1715
)
1816

1917
func main() {
@@ -36,124 +34,10 @@ func main() {
3634
os.Exit(1)
3735
}
3836

39-
if err := flatten(p, r, os.Args[1]); err != nil {
37+
if err := helper.Flatten(p, r, os.Args[1]); err != nil {
4038
fmt.Fprintln(os.Stderr, err)
4139
os.Exit(1)
4240
}
4341

4442
fmt.Print(p.RenderMarkdown(r, 2))
4543
}
46-
47-
func flatten(p *recipemd.Parser, r *recipemd.Recipe, recipeFile string) error {
48-
baseDir := filepath.Dir(recipeFile)
49-
ingredients, err := flattenIngredients(p, r.Ingredients, baseDir)
50-
if err != nil {
51-
return fmt.Errorf("flattenIngredients: %w", err)
52-
}
53-
r.Ingredients = ingredients
54-
groups, err := flattenIngredientGroups(p, r.IngredientGroups, baseDir)
55-
if err != nil {
56-
return fmt.Errorf("flattenIngredientGroups: %w", err)
57-
}
58-
r.IngredientGroups = groups
59-
return nil
60-
}
61-
62-
func flattenIngredients(p *recipemd.Parser, ingredients []recipemd.Ingredient, baseDir string) ([]recipemd.Ingredient, error) {
63-
result := make([]recipemd.Ingredient, 0, len(ingredients))
64-
for _, ing := range ingredients {
65-
if ing.Link != nil {
66-
resolved, err := resolveLinkedRecipe(p, *ing.Link, baseDir, &ing)
67-
if err != nil {
68-
return nil, fmt.Errorf("resolveLinkedRecipe: %w", err)
69-
}
70-
result = append(result, resolved...)
71-
} else {
72-
result = append(result, ing)
73-
}
74-
}
75-
return result, nil
76-
}
77-
78-
func flattenIngredientGroups(p *recipemd.Parser, groups []recipemd.IngredientGroup, baseDir string) ([]recipemd.IngredientGroup, error) {
79-
result := make([]recipemd.IngredientGroup, 0, len(groups))
80-
for _, g := range groups {
81-
ingredients, err := flattenIngredients(p, g.Ingredients, baseDir)
82-
if err != nil {
83-
return nil, fmt.Errorf("flattenIngredients: %w", err)
84-
}
85-
subGroups, err := flattenIngredientGroups(p, g.IngredientGroups, baseDir)
86-
if err != nil {
87-
return nil, fmt.Errorf("flattenIngredientGroups: %w", err)
88-
}
89-
result = append(result, recipemd.IngredientGroup{
90-
Title: g.Title,
91-
Ingredients: ingredients,
92-
IngredientGroups: subGroups,
93-
})
94-
}
95-
return result, nil
96-
}
97-
98-
func resolveLinkedRecipe(p *recipemd.Parser, link string, baseDir string, parent *recipemd.Ingredient) ([]recipemd.Ingredient, error) {
99-
if strings.Contains(link, "://") {
100-
return []recipemd.Ingredient{*parent}, nil
101-
}
102-
103-
path := filepath.Join(baseDir, link)
104-
data, err := os.ReadFile(path)
105-
if err != nil {
106-
return nil, fmt.Errorf("os.ReadFile: %w", err)
107-
}
108-
109-
linked, err := p.Parse(bytes.NewReader(data))
110-
if err != nil {
111-
return nil, fmt.Errorf("Parse: %w", err)
112-
}
113-
114-
if parent.Amount != nil && len(linked.Yields) > 0 {
115-
if err := linked.ScaleForYield(*parent.Amount); err != nil {
116-
return nil, fmt.Errorf("linked.ScaleForYield: %w", err)
117-
}
118-
}
119-
120-
linkedDir := filepath.Dir(path)
121-
flatIngredients, err := flattenIngredients(p, linked.Ingredients, linkedDir)
122-
if err != nil {
123-
return nil, fmt.Errorf("flattenIngredients: %w", err)
124-
}
125-
for _, g := range linked.IngredientGroups {
126-
ingredients, err := flattenIngredients(p, g.Ingredients, linkedDir)
127-
if err != nil {
128-
return nil, fmt.Errorf("flattenIngredients: %w", err)
129-
}
130-
flatIngredients = append(flatIngredients, ingredients...)
131-
groupIngredients, err := flattenGroupIngredients(p, g.IngredientGroups, linkedDir)
132-
if err != nil {
133-
return nil, fmt.Errorf("flattenGroupIngredients: %w", err)
134-
}
135-
flatIngredients = append(flatIngredients, groupIngredients...)
136-
}
137-
138-
if len(flatIngredients) == 0 {
139-
return []recipemd.Ingredient{*parent}, nil
140-
}
141-
return flatIngredients, nil
142-
}
143-
144-
func flattenGroupIngredients(p *recipemd.Parser, groups []recipemd.IngredientGroup, baseDir string) ([]recipemd.Ingredient, error) {
145-
result := make([]recipemd.Ingredient, 0, len(groups))
146-
for _, g := range groups {
147-
ingredients, err := flattenIngredients(p, g.Ingredients, baseDir)
148-
if err != nil {
149-
return nil, fmt.Errorf("flattenIngredients: %w", err)
150-
}
151-
result = append(result, ingredients...)
152-
groupIngredients, err := flattenGroupIngredients(p, g.IngredientGroups, baseDir)
153-
if err != nil {
154-
return nil, fmt.Errorf("flattenGroupIngredients: %w", err)
155-
}
156-
result = append(result, groupIngredients...)
157-
}
158-
return result, nil
159-
}

examples/flatten/main_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
recipemd "github.com/xcapaldi/recipemd-go"
10+
"github.com/xcapaldi/recipemd-go/examples/helper"
1011
)
1112

1213
// TestFlattenInlinesLinksRecursively verifies that linked ingredients are
@@ -28,7 +29,7 @@ func TestFlattenInlinesLinksRecursively(t *testing.T) {
2829
t.Fatal(err)
2930
}
3031

31-
if err := flatten(p, r, recipeFile); err != nil {
32+
if err := helper.Flatten(p, r, recipeFile); err != nil {
3233
t.Fatalf("flatten: %v", err)
3334
}
3435

@@ -57,7 +58,7 @@ func TestFlatten(t *testing.T) {
5758
Ingredients: []recipemd.Ingredient{{Name: "salt"}},
5859
IngredientGroups: []recipemd.IngredientGroup{},
5960
}
60-
if err := flatten(p, r, "/fake/recipe.md"); err != nil {
61+
if err := helper.Flatten(p, r, "/fake/recipe.md"); err != nil {
6162
t.Fatal(err)
6263
}
6364
if len(r.Ingredients) != 1 || r.Ingredients[0].Name != "salt" {
@@ -72,7 +73,7 @@ func TestFlatten(t *testing.T) {
7273
Ingredients: []recipemd.Ingredient{{Name: "sauce", Link: new("https://example.com/sauce.md")}},
7374
IngredientGroups: []recipemd.IngredientGroup{},
7475
}
75-
if err := flatten(p, r, "/fake/recipe.md"); err != nil {
76+
if err := helper.Flatten(p, r, "/fake/recipe.md"); err != nil {
7677
t.Fatal(err)
7778
}
7879
if r.Ingredients[0].Link == nil {
@@ -94,7 +95,7 @@ func TestFlatten(t *testing.T) {
9495
Ingredients: []recipemd.Ingredient{{Name: "sauce", Link: new("sauce.md"), Amount: &recipemd.Amount{Factor: 2, Unit: new("cups")}}},
9596
IngredientGroups: []recipemd.IngredientGroup{},
9697
}
97-
if err := flatten(p, r, main); err != nil {
98+
if err := helper.Flatten(p, r, main); err != nil {
9899
t.Fatal(err)
99100
}
100101
if len(r.Ingredients) < 1 {
@@ -109,7 +110,7 @@ func TestFlatten(t *testing.T) {
109110
Ingredients: []recipemd.Ingredient{{Name: "x", Link: new("nonexistent.md")}},
110111
IngredientGroups: []recipemd.IngredientGroup{},
111112
}
112-
if err := flatten(p, r, "/fake/recipe.md"); err == nil {
113+
if err := helper.Flatten(p, r, "/fake/recipe.md"); err == nil {
113114
t.Fatal("expected error for missing linked file")
114115
}
115116
})
@@ -131,7 +132,7 @@ func TestFlattenHTTPLinksPreserved(t *testing.T) {
131132
t.Fatal(err)
132133
}
133134

134-
if err := flatten(p, r, recipeFile); err != nil {
135+
if err := helper.Flatten(p, r, recipeFile); err != nil {
135136
t.Fatalf("flatten: %v", err)
136137
}
137138

examples/helper/flatten.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package helper
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
recipemd "github.com/xcapaldi/recipemd-go"
11+
)
12+
13+
// Flatten resolves all locally-linked ingredients in r, inlining them in place.
14+
// recipeFile is the path to the recipe file, used to resolve relative links.
15+
// HTTP(S) links are left as-is.
16+
func Flatten(p *recipemd.Parser, r *recipemd.Recipe, recipeFile string) error {
17+
baseDir := filepath.Dir(recipeFile)
18+
ingredients, err := flattenIngredients(p, r.Ingredients, baseDir)
19+
if err != nil {
20+
return fmt.Errorf("flattenIngredients: %w", err)
21+
}
22+
r.Ingredients = ingredients
23+
groups, err := flattenIngredientGroups(p, r.IngredientGroups, baseDir)
24+
if err != nil {
25+
return fmt.Errorf("flattenIngredientGroups: %w", err)
26+
}
27+
r.IngredientGroups = groups
28+
return nil
29+
}
30+
31+
func flattenIngredients(p *recipemd.Parser, ingredients []recipemd.Ingredient, baseDir string) ([]recipemd.Ingredient, error) {
32+
result := make([]recipemd.Ingredient, 0, len(ingredients))
33+
for _, ing := range ingredients {
34+
if ing.Link != nil {
35+
resolved, err := resolveLinkedRecipe(p, *ing.Link, baseDir, &ing)
36+
if err != nil {
37+
return nil, fmt.Errorf("resolveLinkedRecipe: %w", err)
38+
}
39+
result = append(result, resolved...)
40+
} else {
41+
result = append(result, ing)
42+
}
43+
}
44+
return result, nil
45+
}
46+
47+
func flattenIngredientGroups(p *recipemd.Parser, groups []recipemd.IngredientGroup, baseDir string) ([]recipemd.IngredientGroup, error) {
48+
result := make([]recipemd.IngredientGroup, 0, len(groups))
49+
for _, g := range groups {
50+
ingredients, err := flattenIngredients(p, g.Ingredients, baseDir)
51+
if err != nil {
52+
return nil, fmt.Errorf("flattenIngredients: %w", err)
53+
}
54+
subGroups, err := flattenIngredientGroups(p, g.IngredientGroups, baseDir)
55+
if err != nil {
56+
return nil, fmt.Errorf("flattenIngredientGroups: %w", err)
57+
}
58+
result = append(result, recipemd.IngredientGroup{
59+
Title: g.Title,
60+
Ingredients: ingredients,
61+
IngredientGroups: subGroups,
62+
})
63+
}
64+
return result, nil
65+
}
66+
67+
func resolveLinkedRecipe(p *recipemd.Parser, link string, baseDir string, parent *recipemd.Ingredient) ([]recipemd.Ingredient, error) {
68+
if strings.Contains(link, "://") {
69+
return []recipemd.Ingredient{*parent}, nil
70+
}
71+
72+
path := filepath.Join(baseDir, link)
73+
data, err := os.ReadFile(path)
74+
if err != nil {
75+
return nil, fmt.Errorf("os.ReadFile: %w", err)
76+
}
77+
78+
linked, err := p.Parse(bytes.NewReader(data))
79+
if err != nil {
80+
return nil, fmt.Errorf("Parse: %w", err)
81+
}
82+
83+
if parent.Amount != nil && len(linked.Yields) > 0 {
84+
if err := linked.ScaleForYield(*parent.Amount); err != nil {
85+
return nil, fmt.Errorf("linked.ScaleForYield: %w", err)
86+
}
87+
}
88+
89+
linkedDir := filepath.Dir(path)
90+
flatIngredients, err := flattenIngredients(p, linked.Ingredients, linkedDir)
91+
if err != nil {
92+
return nil, fmt.Errorf("flattenIngredients: %w", err)
93+
}
94+
for _, g := range linked.IngredientGroups {
95+
ingredients, err := flattenIngredients(p, g.Ingredients, linkedDir)
96+
if err != nil {
97+
return nil, fmt.Errorf("flattenIngredients: %w", err)
98+
}
99+
flatIngredients = append(flatIngredients, ingredients...)
100+
groupIngredients, err := flattenGroupIngredients(p, g.IngredientGroups, linkedDir)
101+
if err != nil {
102+
return nil, fmt.Errorf("flattenGroupIngredients: %w", err)
103+
}
104+
flatIngredients = append(flatIngredients, groupIngredients...)
105+
}
106+
107+
if len(flatIngredients) == 0 {
108+
return []recipemd.Ingredient{*parent}, nil
109+
}
110+
return flatIngredients, nil
111+
}
112+
113+
func flattenGroupIngredients(p *recipemd.Parser, groups []recipemd.IngredientGroup, baseDir string) ([]recipemd.Ingredient, error) {
114+
result := make([]recipemd.Ingredient, 0, len(groups))
115+
for _, g := range groups {
116+
ingredients, err := flattenIngredients(p, g.Ingredients, baseDir)
117+
if err != nil {
118+
return nil, fmt.Errorf("flattenIngredients: %w", err)
119+
}
120+
result = append(result, ingredients...)
121+
groupIngredients, err := flattenGroupIngredients(p, g.IngredientGroups, baseDir)
122+
if err != nil {
123+
return nil, fmt.Errorf("flattenGroupIngredients: %w", err)
124+
}
125+
result = append(result, groupIngredients...)
126+
}
127+
return result, nil
128+
}

examples/renderhtml/main.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Renderhtml reads a RecipeMD file, flattens linked ingredients, and writes
2+
// an HTML <article> element to stdout.
3+
//
4+
// Usage: renderhtml <recipe.md>
5+
//
6+
// Linked ingredients are resolved and inlined before rendering. Only local
7+
// file links are resolved; HTTP(S) links are left as-is.
8+
//
9+
// renderhtml recipe.md > recipe.html
10+
package main
11+
12+
import (
13+
"fmt"
14+
"os"
15+
16+
recipemd "github.com/xcapaldi/recipemd-go"
17+
"github.com/xcapaldi/recipemd-go/examples/helper"
18+
)
19+
20+
func main() {
21+
if len(os.Args) < 2 {
22+
fmt.Fprintln(os.Stderr, "usage: renderhtml <recipe.md>")
23+
os.Exit(1)
24+
}
25+
26+
f, err := os.Open(os.Args[1])
27+
if err != nil {
28+
fmt.Fprintln(os.Stderr, err)
29+
os.Exit(1)
30+
}
31+
defer f.Close()
32+
33+
p := recipemd.NewParser()
34+
r, err := p.Parse(f)
35+
if err != nil {
36+
fmt.Fprintln(os.Stderr, err)
37+
os.Exit(1)
38+
}
39+
40+
if err := helper.Flatten(p, r, os.Args[1]); err != nil {
41+
fmt.Fprintln(os.Stderr, err)
42+
os.Exit(1)
43+
}
44+
45+
fmt.Println(p.RenderHTML(r, 3))
46+
}

0 commit comments

Comments
 (0)