jekyll-cooklang-converter 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +56 -0
- data/clash/test-site/_config.yml +2 -0
- data/clash/test-site/_expected/recipes/baked-ziti.html +40 -52
- data/clash/test-site/_site/recipes/baked-ziti.html +40 -52
- data/lib/jekyll/converters/cooklang/configuration.rb +66 -0
- data/lib/jekyll/converters/cooklang/formatters.rb +83 -0
- data/lib/jekyll/converters/cooklang/legacy_compatibility.rb +97 -0
- data/lib/jekyll/converters/cooklang/recipe_parser.rb +41 -0
- data/lib/jekyll/converters/cooklang/renderers.rb +103 -0
- data/lib/jekyll/converters/cooklang.rb +52 -122
- data/lib/jekyll-cooklang-converter/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ce3eb10976d607accb5a82ecf44070b6c6ea4f35cabe5300d2775ef240482fb
|
4
|
+
data.tar.gz: 42bd15562aac13ca34daec10c8f71daae5b73cdd48bf170e7f0ee8fdcccb8920
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 888391b3d344450951ef24ee701a6f008bc26bf177ab56fb6f07bbfced06a16c07f936d4e84469e3f2c87510b6c56a0b69f252b0bed333ac43da659aca0da6c9
|
7
|
+
data.tar.gz: 5ad888c8033a71e1c320dc7f72b2aa51319c8d42678dd87e7a6b4f3669de7dc769e969856e01adfa209f70962b2b94d503d78d2c8403ab2fb4dee7ab701c9478
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Development Commands
|
6
|
+
|
7
|
+
- **Setup**: `bin/setup` - Install dependencies after checkout
|
8
|
+
- **Console**: `bin/console` - Interactive prompt for experimentation
|
9
|
+
- **Install locally**: `bundle exec rake install` - Install gem to local machine
|
10
|
+
- **Tests**: `bundle exec rake spec` - Run RSpec tests
|
11
|
+
- **Linting**: `bundle exec rake standard` - Run StandardRB linter
|
12
|
+
- **Integration tests**: `bundle exec rake clash` - Run clash integration tests
|
13
|
+
- **All checks**: `bundle exec rake` - Runs standard, spec, and clash (default task)
|
14
|
+
- **Release**: `bundle exec rake release` - Create git tag and push to RubyGems
|
15
|
+
|
16
|
+
## Architecture
|
17
|
+
|
18
|
+
This is a Jekyll plugin that converts Cooklang recipe files (.cook) to HTML. The plugin structure follows Jekyll's converter pattern:
|
19
|
+
|
20
|
+
### Core Components
|
21
|
+
|
22
|
+
**Main Converter** (`lib/jekyll/converters/cooklang.rb:106-149`):
|
23
|
+
- `CooklangConverter` class inherits from `Jekyll::Converter`
|
24
|
+
- Matches `.cook` file extensions and outputs `.html`
|
25
|
+
- Uses `cooklang_rb` gem to parse recipe files
|
26
|
+
- Converts parsed data to structured HTML with ingredients, cookware, and steps sections
|
27
|
+
|
28
|
+
**HTML Generation Classes** (`lib/jekyll/converters/cooklang.rb:3-105`):
|
29
|
+
- `Ingredient` - Handles quantity/unit/name formatting with rational number support
|
30
|
+
- `Timer` - Formats timing instructions
|
31
|
+
- `CookWare` - Formats cookware requirements
|
32
|
+
- `Step` - Converts recipe steps to paragraph HTML
|
33
|
+
- `OrderedList`/`UnorderedList` - Generate HTML lists for steps and ingredients
|
34
|
+
- All classes inherit from `ToHTML` base class with `to_html` and `to_list_item` methods
|
35
|
+
|
36
|
+
### Dependencies
|
37
|
+
|
38
|
+
- **Jekyll** (~> 4.3.4) - Static site generator framework
|
39
|
+
- **cooklang_rb** (~> 0.2.0) - Cooklang recipe parsing
|
40
|
+
- **htmlbeautifier** (~> 1.4.3) - HTML formatting
|
41
|
+
- **rspec** - Testing framework
|
42
|
+
- **clash** - Integration testing for Jekyll sites
|
43
|
+
- **standard** - Ruby linting
|
44
|
+
|
45
|
+
### Testing Strategy
|
46
|
+
|
47
|
+
- **Unit tests**: RSpec tests in `spec/` directory test converter functionality
|
48
|
+
- **Integration tests**: Clash framework tests actual Jekyll site generation in `clash/test-site/`
|
49
|
+
- Test site includes `.cook` recipe files and expected HTML output for comparison
|
50
|
+
|
51
|
+
### File Structure
|
52
|
+
|
53
|
+
- Main gem entry point: `lib/jekyll-cooklang-converter.rb`
|
54
|
+
- Converter implementation: `lib/jekyll/converters/cooklang.rb`
|
55
|
+
- Version: `lib/jekyll-cooklang-converter/version.rb`
|
56
|
+
- Integration test site: `clash/test-site/` with sample recipes and expected output
|
data/clash/test-site/_config.yml
CHANGED
@@ -1,52 +1,40 @@
|
|
1
|
-
<
|
2
|
-
<
|
3
|
-
<
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
</
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
<
|
22
|
-
|
23
|
-
<
|
24
|
-
<
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
<
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
<
|
33
|
-
|
34
|
-
|
35
|
-
<
|
36
|
-
|
37
|
-
|
38
|
-
<
|
39
|
-
</
|
40
|
-
|
41
|
-
<p>add back in the mushrooms and sausage, and pour pasta sauce over everything. Mix well, and allow it to simmer until it bubbles a bit at the bottom</p>
|
42
|
-
</li>
|
43
|
-
<li>
|
44
|
-
<p>mix together ricotta and shredded mozarella</p>
|
45
|
-
</li>
|
46
|
-
<li>
|
47
|
-
<p>layer half of the cooked pasta into the bottom of a greased casserole dish. Ladle half of the meat + veggie mixture on top. Then, spread the cheese mixture from the previous step on top of everything. Layer sliced provolone on that, followed by the rest of the sauce, the rest of the pasta, and a final layer of mozarella cheese. Sprinkle the top with italian seasoning</p>
|
48
|
-
</li>
|
49
|
-
<li>
|
50
|
-
<p>put the dish in the preheated oven for 25 minutes. Then it's all ready!</p>
|
51
|
-
</li>
|
52
|
-
</ol>
|
1
|
+
<section class="recipe-ingredients">
|
2
|
+
<h2>Ingredients</h2>
|
3
|
+
<ul class="ingredient-list">
|
4
|
+
<li><em class="quantity">1/2 box</em> <span class="name">mushrooms</span></li>
|
5
|
+
<li><em class="quantity">1 pound</em> <span class="name">ground sausage</span></li>
|
6
|
+
<li><em class="quantity">some</em> <span class="name">garlic powder</span></li>
|
7
|
+
<li><em class="quantity">some</em> <span class="name">italian seasoning</span></li>
|
8
|
+
<li><em class="quantity">some</em> <span class="name">chili powder</span></li>
|
9
|
+
<li><em class="quantity">1</em> <span class="name">large yellow onion</span></li>
|
10
|
+
<li><em class="quantity">some</em> <span class="name">olive oil</span></li>
|
11
|
+
<li><em class="quantity">some</em> <span class="name">salt</span></li>
|
12
|
+
<li><em class="quantity">1</em> <span class="name">bell pepper</span></li>
|
13
|
+
<li><em class="quantity">1 jar</em> <span class="name">pasta sauce</span></li>
|
14
|
+
<li><em class="quantity">1 15oz package</em> <span class="name">ricotta</span></li>
|
15
|
+
<li><em class="quantity">1 cup</em> <span class="name">shredded mozarella</span></li>
|
16
|
+
<li><em class="quantity">1 package</em> <span class="name">sliced provolone</span></li>
|
17
|
+
<li><em class="quantity">2 cups</em> <span class="name">mozarella cheese</span></li>
|
18
|
+
</ul>
|
19
|
+
</section>
|
20
|
+
<section class="recipe-cookware">
|
21
|
+
<h2>Cookware</h2>
|
22
|
+
<ul class="cookware-list">
|
23
|
+
<li><em class="quantity">1</em> <span class="name">pot</span></li>
|
24
|
+
<li><em class="quantity">1</em> <span class="name">casserole dish</span></li>
|
25
|
+
</ul>
|
26
|
+
</section>
|
27
|
+
<section class="recipe-steps">
|
28
|
+
<h2>Steps</h2>
|
29
|
+
<ol class="steps-list">
|
30
|
+
<li>set a pot of water on the stove, to boil (you'll need for pasta later), and preheat the oven to 350</li>
|
31
|
+
<li>cut mushrooms into slices, and sautee (no oil) in a pan over medium high heat until browned. Set aside</li>
|
32
|
+
<li>brown ground sausage in the same pan, seasoning with garlic powder, italian seasoning, and chili powder set aside</li>
|
33
|
+
<li>the water should hopefully be boiling by now</li>
|
34
|
+
<li>chop and sautee large yellow onion in olive oil. Seasoning with salt. Add the bell pepper, chopped</li>
|
35
|
+
<li>add back in the mushrooms and sausage, and pour pasta sauce over everything. Mix well, and allow it to simmer until it bubbles a bit at the bottom</li>
|
36
|
+
<li>mix together ricotta and shredded mozarella</li>
|
37
|
+
<li>layer half of the cooked pasta into the bottom of a greased casserole dish. Ladle half of the meat + veggie mixture on top. Then, spread the cheese mixture from the previous step on top of everything. Layer sliced provolone on that, followed by the rest of the sauce, the rest of the pasta, and a final layer of mozarella cheese. Sprinkle the top with italian seasoning</li>
|
38
|
+
<li>put the dish in the preheated oven for 25 minutes. Then it's all ready!</li>
|
39
|
+
</ol>
|
40
|
+
</section>
|
@@ -1,52 +1,40 @@
|
|
1
|
-
<
|
2
|
-
<
|
3
|
-
<
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
</
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
<
|
22
|
-
|
23
|
-
<
|
24
|
-
<
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
<
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
<
|
33
|
-
|
34
|
-
|
35
|
-
<
|
36
|
-
|
37
|
-
|
38
|
-
<
|
39
|
-
</
|
40
|
-
|
41
|
-
<p>add back in the mushrooms and sausage, and pour pasta sauce over everything. Mix well, and allow it to simmer until it bubbles a bit at the bottom</p>
|
42
|
-
</li>
|
43
|
-
<li>
|
44
|
-
<p>mix together ricotta and shredded mozarella</p>
|
45
|
-
</li>
|
46
|
-
<li>
|
47
|
-
<p>layer half of the cooked pasta into the bottom of a greased casserole dish. Ladle half of the meat + veggie mixture on top. Then, spread the cheese mixture from the previous step on top of everything. Layer sliced provolone on that, followed by the rest of the sauce, the rest of the pasta, and a final layer of mozarella cheese. Sprinkle the top with italian seasoning</p>
|
48
|
-
</li>
|
49
|
-
<li>
|
50
|
-
<p>put the dish in the preheated oven for 25 minutes. Then it's all ready!</p>
|
51
|
-
</li>
|
52
|
-
</ol>
|
1
|
+
<section class="recipe-ingredients">
|
2
|
+
<h2>Ingredients</h2>
|
3
|
+
<ul class="ingredient-list">
|
4
|
+
<li><em class="quantity">1/2 box</em> <span class="name">mushrooms</span></li>
|
5
|
+
<li><em class="quantity">1 pound</em> <span class="name">ground sausage</span></li>
|
6
|
+
<li><em class="quantity">some</em> <span class="name">garlic powder</span></li>
|
7
|
+
<li><em class="quantity">some</em> <span class="name">italian seasoning</span></li>
|
8
|
+
<li><em class="quantity">some</em> <span class="name">chili powder</span></li>
|
9
|
+
<li><em class="quantity">1</em> <span class="name">large yellow onion</span></li>
|
10
|
+
<li><em class="quantity">some</em> <span class="name">olive oil</span></li>
|
11
|
+
<li><em class="quantity">some</em> <span class="name">salt</span></li>
|
12
|
+
<li><em class="quantity">1</em> <span class="name">bell pepper</span></li>
|
13
|
+
<li><em class="quantity">1 jar</em> <span class="name">pasta sauce</span></li>
|
14
|
+
<li><em class="quantity">1 15oz package</em> <span class="name">ricotta</span></li>
|
15
|
+
<li><em class="quantity">1 cup</em> <span class="name">shredded mozarella</span></li>
|
16
|
+
<li><em class="quantity">1 package</em> <span class="name">sliced provolone</span></li>
|
17
|
+
<li><em class="quantity">2 cups</em> <span class="name">mozarella cheese</span></li>
|
18
|
+
</ul>
|
19
|
+
</section>
|
20
|
+
<section class="recipe-cookware">
|
21
|
+
<h2>Cookware</h2>
|
22
|
+
<ul class="cookware-list">
|
23
|
+
<li><em class="quantity">1</em> <span class="name">pot</span></li>
|
24
|
+
<li><em class="quantity">1</em> <span class="name">casserole dish</span></li>
|
25
|
+
</ul>
|
26
|
+
</section>
|
27
|
+
<section class="recipe-steps">
|
28
|
+
<h2>Steps</h2>
|
29
|
+
<ol class="steps-list">
|
30
|
+
<li>set a pot of water on the stove, to boil (you'll need for pasta later), and preheat the oven to 350</li>
|
31
|
+
<li>cut mushrooms into slices, and sautee (no oil) in a pan over medium high heat until browned. Set aside</li>
|
32
|
+
<li>brown ground sausage in the same pan, seasoning with garlic powder, italian seasoning, and chili powder set aside</li>
|
33
|
+
<li>the water should hopefully be boiling by now</li>
|
34
|
+
<li>chop and sautee large yellow onion in olive oil. Seasoning with salt. Add the bell pepper, chopped</li>
|
35
|
+
<li>add back in the mushrooms and sausage, and pour pasta sauce over everything. Mix well, and allow it to simmer until it bubbles a bit at the bottom</li>
|
36
|
+
<li>mix together ricotta and shredded mozarella</li>
|
37
|
+
<li>layer half of the cooked pasta into the bottom of a greased casserole dish. Ladle half of the meat + veggie mixture on top. Then, spread the cheese mixture from the previous step on top of everything. Layer sliced provolone on that, followed by the rest of the sauce, the rest of the pasta, and a final layer of mozarella cheese. Sprinkle the top with italian seasoning</li>
|
38
|
+
<li>put the dish in the preheated oven for 25 minutes. Then it's all ready!</li>
|
39
|
+
</ol>
|
40
|
+
</section>
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Jekyll
|
2
|
+
module Converters
|
3
|
+
module Cooklang
|
4
|
+
class Configuration
|
5
|
+
DEFAULT_CONFIG = {
|
6
|
+
"legacy_mode" => false,
|
7
|
+
"css_classes" => {
|
8
|
+
"ingredients_section" => "recipe-ingredients",
|
9
|
+
"cookware_section" => "recipe-cookware",
|
10
|
+
"steps_section" => "recipe-steps",
|
11
|
+
"ingredient_list" => "ingredient-list",
|
12
|
+
"cookware_list" => "cookware-list",
|
13
|
+
"steps_list" => "steps-list",
|
14
|
+
"quantity" => "quantity",
|
15
|
+
"unit" => "unit",
|
16
|
+
"name" => "name"
|
17
|
+
},
|
18
|
+
"headings" => {
|
19
|
+
"ingredients" => "Ingredients",
|
20
|
+
"cookware" => "Cookware",
|
21
|
+
"steps" => "Steps"
|
22
|
+
},
|
23
|
+
"templates" => {
|
24
|
+
"ingredient" => '<em class="<%= css_classes["quantity"] %>"><%= quantity %><%= unit.empty? ? "" : " " + unit %></em> <span class="<%= css_classes["name"] %>"><%= name %></span>',
|
25
|
+
"cookware" => '<%= quantity.empty? ? "" : "<em class=\"" + css_classes["quantity"] + "\">" + quantity + "</em> " %><span class="<%= css_classes["name"] %>"><%= name %></span>',
|
26
|
+
"timer" => '<em class="<%= css_classes["quantity"] %>"><%= quantity %> <%= unit %></em><%= name.empty? ? "" : " (" + name + ")" %>',
|
27
|
+
"step" => "<%= content %>",
|
28
|
+
"legacy_ingredient" => '<em><%= quantity %><%= unit.empty? ? "" : " " + unit %></em> <%= name %>',
|
29
|
+
"legacy_cookware" => '<%= quantity.empty? ? "" : "<em>" + quantity + "</em> " %><%= name %>',
|
30
|
+
"legacy_step" => "<p><%= content %></p>"
|
31
|
+
}
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
attr_reader :config
|
35
|
+
|
36
|
+
def initialize(site_config = {})
|
37
|
+
@config = deep_merge(DEFAULT_CONFIG, site_config["cooklang"] || {})
|
38
|
+
end
|
39
|
+
|
40
|
+
def css_classes
|
41
|
+
@config["css_classes"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def headings
|
45
|
+
@config["headings"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def templates
|
49
|
+
@config["templates"]
|
50
|
+
end
|
51
|
+
|
52
|
+
def legacy_mode?
|
53
|
+
@config["legacy_mode"]
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def deep_merge(hash1, hash2)
|
59
|
+
hash1.merge(hash2) do |key, oldval, newval|
|
60
|
+
(oldval.is_a?(Hash) && newval.is_a?(Hash)) ? deep_merge(oldval, newval) : newval
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Jekyll
|
2
|
+
module Converters
|
3
|
+
module Cooklang
|
4
|
+
module Formatters
|
5
|
+
class QuantityFormatter
|
6
|
+
def self.format(quantity)
|
7
|
+
return quantity.to_s unless quantity.respond_to?(:rationalize)
|
8
|
+
|
9
|
+
rationalized = quantity.rationalize(0.05)
|
10
|
+
(rationalized.denominator == 1) ? quantity.to_s : rationalized.to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class BaseFormatter
|
15
|
+
attr_reader :data, :config
|
16
|
+
|
17
|
+
def initialize(data, config)
|
18
|
+
@data = data
|
19
|
+
@config = config
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
raise NotImplementedError, "Subclasses must implement to_hash"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class IngredientFormatter < BaseFormatter
|
28
|
+
def to_hash
|
29
|
+
{
|
30
|
+
quantity: QuantityFormatter.format(data["quantity"] || ""),
|
31
|
+
unit: data["units"] || "",
|
32
|
+
name: data["name"] || "",
|
33
|
+
type: "ingredient"
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class CookwareFormatter < BaseFormatter
|
39
|
+
def to_hash
|
40
|
+
{
|
41
|
+
quantity: data["quantity"]&.to_s || "",
|
42
|
+
name: data["name"] || "",
|
43
|
+
type: "cookware"
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class TimerFormatter < BaseFormatter
|
49
|
+
def to_hash
|
50
|
+
{
|
51
|
+
quantity: data["quantity"]&.to_s || "",
|
52
|
+
unit: data["units"] || "",
|
53
|
+
name: data["name"] || "",
|
54
|
+
type: "timer"
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class StepFormatter < BaseFormatter
|
60
|
+
def to_hash
|
61
|
+
content = data.map do |substep|
|
62
|
+
case substep["type"]
|
63
|
+
when "cookware"
|
64
|
+
substep["name"]
|
65
|
+
when "ingredient"
|
66
|
+
substep["name"]
|
67
|
+
when "timer"
|
68
|
+
"#{substep["quantity"]} #{substep["units"]}"
|
69
|
+
when "text"
|
70
|
+
substep["value"]
|
71
|
+
end
|
72
|
+
end.join
|
73
|
+
|
74
|
+
{
|
75
|
+
content: content,
|
76
|
+
type: "step"
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Jekyll
|
2
|
+
module Converters
|
3
|
+
class ToHTML
|
4
|
+
def to_html
|
5
|
+
"<em>hello world</em>"
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_list_item
|
9
|
+
"<li>#{to_html}</li>"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Ingredient < ToHTML
|
14
|
+
def initialize(quantity, unit, name)
|
15
|
+
@formatter = Cooklang::Formatters::IngredientFormatter.new({
|
16
|
+
"quantity" => quantity,
|
17
|
+
"units" => unit,
|
18
|
+
"name" => name
|
19
|
+
}, nil)
|
20
|
+
@data = @formatter.to_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_html
|
24
|
+
return "<em>#{@data[:quantity]}</em> #{@data[:name]}" if @data[:unit].empty?
|
25
|
+
"<em>#{@data[:quantity]} #{@data[:unit]}</em> #{@data[:name]}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Timer < ToHTML
|
30
|
+
def initialize(quantity, unit, name)
|
31
|
+
@formatter = Cooklang::Formatters::TimerFormatter.new({
|
32
|
+
"quantity" => quantity,
|
33
|
+
"units" => unit,
|
34
|
+
"name" => name
|
35
|
+
}, nil)
|
36
|
+
@data = @formatter.to_hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_html
|
40
|
+
return "<em>#{@data[:quantity]} #{@data[:unit]}</em>" if @data[:name].empty?
|
41
|
+
"<em>#{@data[:quantity]} #{@data[:unit]}</em> (#{@data[:name]})"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class CookWare < ToHTML
|
46
|
+
def initialize(quantity, name)
|
47
|
+
@formatter = Cooklang::Formatters::CookwareFormatter.new({
|
48
|
+
"quantity" => quantity,
|
49
|
+
"name" => name
|
50
|
+
}, nil)
|
51
|
+
@data = @formatter.to_hash
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_html
|
55
|
+
return @data[:name].to_s if @data[:quantity].empty?
|
56
|
+
"<em>#{@data[:quantity]}</em> #{@data[:name]}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class OrderedList < ToHTML
|
61
|
+
def initialize(items)
|
62
|
+
@items = items
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_html
|
66
|
+
list_items = @items.map do |item|
|
67
|
+
item.to_list_item
|
68
|
+
end
|
69
|
+
"<ol>" + list_items.join + "</ol>"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class UnorderedList < ToHTML
|
74
|
+
def initialize(items)
|
75
|
+
@items = items
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_html
|
79
|
+
list_items = @items.map do |item|
|
80
|
+
item.to_list_item
|
81
|
+
end
|
82
|
+
"<ul>" + list_items.join + "</ul>"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Step < ToHTML
|
87
|
+
def initialize(step)
|
88
|
+
@formatter = Cooklang::Formatters::StepFormatter.new(step, nil)
|
89
|
+
@data = @formatter.to_hash
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_html
|
93
|
+
"<p>#{@data[:content]}</p>"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Jekyll
|
2
|
+
module Converters
|
3
|
+
module Cooklang
|
4
|
+
class RecipeParser
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse(content)
|
10
|
+
recipe = CooklangRb::Recipe.from(content)
|
11
|
+
|
12
|
+
{
|
13
|
+
ingredients: extract_ingredients(recipe),
|
14
|
+
cookware: extract_cookware(recipe),
|
15
|
+
steps: extract_steps(recipe)
|
16
|
+
}
|
17
|
+
rescue => e
|
18
|
+
raise "Error parsing recipe: #{e.message}"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def extract_ingredients(recipe)
|
24
|
+
recipe["steps"].flatten
|
25
|
+
.select { |item| item["type"] == "ingredient" }
|
26
|
+
.map { |item| Formatters::IngredientFormatter.new(item, @config).to_hash }
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_cookware(recipe)
|
30
|
+
recipe["steps"].flatten
|
31
|
+
.select { |item| item["type"] == "cookware" }
|
32
|
+
.map { |item| Formatters::CookwareFormatter.new(item, @config).to_hash }
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_steps(recipe)
|
36
|
+
recipe["steps"].map { |step| Formatters::StepFormatter.new(step, @config).to_hash }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
module Jekyll
|
4
|
+
module Converters
|
5
|
+
module Cooklang
|
6
|
+
module Renderers
|
7
|
+
class TemplateRenderer
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(template_name, data)
|
13
|
+
template = @config.templates[template_name.to_s]
|
14
|
+
return "" unless template
|
15
|
+
|
16
|
+
erb = ERB.new(template)
|
17
|
+
context = RenderContext.new(data, @config)
|
18
|
+
erb.result(context.get_binding)
|
19
|
+
rescue => e
|
20
|
+
raise "Error rendering template '#{template_name}': #{e.message}"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
class RenderContext
|
26
|
+
def initialize(data, config)
|
27
|
+
@data = data
|
28
|
+
@config = config
|
29
|
+
data.each { |key, value| instance_variable_set(:"@#{key}", value) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def css_classes
|
33
|
+
@config.css_classes
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(method_name, *args, &block)
|
37
|
+
if @data.key?(method_name.to_s)
|
38
|
+
@data[method_name.to_s]
|
39
|
+
elsif @data.key?(method_name.to_sym)
|
40
|
+
@data[method_name.to_sym]
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to_missing?(method_name, include_private = false)
|
47
|
+
@data.key?(method_name.to_s) || @data.key?(method_name.to_sym) || super
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_binding
|
51
|
+
binding
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class HtmlRenderer
|
57
|
+
def initialize(config)
|
58
|
+
@config = config
|
59
|
+
@template_renderer = TemplateRenderer.new(config)
|
60
|
+
end
|
61
|
+
|
62
|
+
def render_ingredient(ingredient_data)
|
63
|
+
template_name = @config.legacy_mode? ? "legacy_ingredient" : "ingredient"
|
64
|
+
@template_renderer.render(template_name, ingredient_data)
|
65
|
+
end
|
66
|
+
|
67
|
+
def render_cookware(cookware_data)
|
68
|
+
template_name = @config.legacy_mode? ? "legacy_cookware" : "cookware"
|
69
|
+
@template_renderer.render(template_name, cookware_data)
|
70
|
+
end
|
71
|
+
|
72
|
+
def render_timer(timer_data)
|
73
|
+
@template_renderer.render("timer", timer_data)
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_step(step_data)
|
77
|
+
template_name = @config.legacy_mode? ? "legacy_step" : "step"
|
78
|
+
@template_renderer.render(template_name, step_data)
|
79
|
+
end
|
80
|
+
|
81
|
+
def render_list(items, type = "ul", css_class = "")
|
82
|
+
list_items = items.map { |item| "<li>#{item}</li>" }.join
|
83
|
+
if @config.legacy_mode?
|
84
|
+
"<#{type}>#{list_items}</#{type}>"
|
85
|
+
else
|
86
|
+
class_attr = css_class.empty? ? "" : " class=\"#{css_class}\""
|
87
|
+
"<#{type}#{class_attr}>#{list_items}</#{type}>"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def render_section(heading, content, css_class = "")
|
92
|
+
if @config.legacy_mode?
|
93
|
+
"<h2>#{heading}</h2>#{content}"
|
94
|
+
else
|
95
|
+
class_attr = css_class.empty? ? "" : " class=\"#{css_class}\""
|
96
|
+
"<section#{class_attr}><h2>#{heading}</h2>#{content}</section>"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -1,150 +1,80 @@
|
|
1
|
+
require_relative "cooklang/configuration"
|
2
|
+
require_relative "cooklang/formatters"
|
3
|
+
require_relative "cooklang/renderers"
|
4
|
+
require_relative "cooklang/recipe_parser"
|
5
|
+
require_relative "cooklang/legacy_compatibility"
|
6
|
+
|
1
7
|
module Jekyll
|
2
8
|
module Converters
|
3
|
-
class
|
4
|
-
|
5
|
-
"<em>hello world</em>"
|
6
|
-
end
|
7
|
-
|
8
|
-
def to_list_item
|
9
|
-
"<li>#{to_html}</li>"
|
10
|
-
end
|
11
|
-
end
|
9
|
+
class CooklangConverter < Converter
|
10
|
+
safe true
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
@
|
16
|
-
|
17
|
-
@
|
18
|
-
|
19
|
-
|
20
|
-
else
|
21
|
-
quantity.rationalize(0.1).to_s
|
22
|
-
end
|
23
|
-
else
|
24
|
-
quantity.to_s
|
25
|
-
end
|
12
|
+
def initialize(config = {})
|
13
|
+
super
|
14
|
+
@site_config = config.is_a?(Hash) ? config : {}
|
15
|
+
# Initialize with empty config, will be overridden by Jekyll
|
16
|
+
@cooklang_config = nil
|
17
|
+
@parser = nil
|
18
|
+
@renderer = nil
|
26
19
|
end
|
27
20
|
|
28
|
-
def
|
29
|
-
return
|
30
|
-
|
21
|
+
def setup
|
22
|
+
return if @cooklang_config
|
23
|
+
site_config = @config || {}
|
24
|
+
@cooklang_config = Cooklang::Configuration.new(site_config)
|
25
|
+
@parser = Cooklang::RecipeParser.new(@cooklang_config)
|
26
|
+
@renderer = Cooklang::Renderers::HtmlRenderer.new(@cooklang_config)
|
31
27
|
end
|
32
|
-
end
|
33
28
|
|
34
|
-
|
35
|
-
|
36
|
-
@name = name.to_s
|
37
|
-
@unit = unit.to_s
|
38
|
-
@quantity = quantity.to_s
|
29
|
+
def matches(ext)
|
30
|
+
ext =~ /^.cook/i
|
39
31
|
end
|
40
32
|
|
41
|
-
def
|
42
|
-
|
43
|
-
"<em>#{@quantity} #{@unit}</em> (#{@name})"
|
33
|
+
def output_ext(ext)
|
34
|
+
".html"
|
44
35
|
end
|
45
|
-
end
|
46
36
|
|
47
|
-
|
48
|
-
|
49
|
-
@name = name.to_s
|
50
|
-
@quantity = quantity.to_s
|
51
|
-
end
|
37
|
+
def convert(content)
|
38
|
+
setup
|
52
39
|
|
53
|
-
|
54
|
-
return @name.to_s if @quantity.empty?
|
55
|
-
"<em>#{@quantity}</em> #{@name}"
|
56
|
-
end
|
57
|
-
end
|
40
|
+
recipe_data = @parser.parse(content)
|
58
41
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
42
|
+
ingredients_html = render_ingredients_section(recipe_data[:ingredients])
|
43
|
+
cookware_html = render_cookware_section(recipe_data[:cookware])
|
44
|
+
steps_html = render_steps_section(recipe_data[:steps])
|
63
45
|
|
64
|
-
|
65
|
-
list_items = @items.map do |item|
|
66
|
-
item.to_list_item
|
67
|
-
end
|
68
|
-
"<ol>" + list_items.join + "</ol>"
|
69
|
-
end
|
70
|
-
end
|
46
|
+
full_html = ingredients_html + cookware_html + steps_html
|
71
47
|
|
72
|
-
|
73
|
-
|
74
|
-
|
48
|
+
HtmlBeautifier.beautify(full_html) + "\n"
|
49
|
+
rescue => e
|
50
|
+
Jekyll.logger.error "Cooklang Converter:", "Failed to convert content: #{e.message}"
|
51
|
+
raise
|
75
52
|
end
|
76
53
|
|
77
|
-
|
78
|
-
list_items = @items.map do |item|
|
79
|
-
item.to_list_item
|
80
|
-
end
|
81
|
-
"<ul>" + list_items.join + "</ul>"
|
82
|
-
end
|
83
|
-
end
|
54
|
+
private
|
84
55
|
|
85
|
-
|
86
|
-
|
87
|
-
@text = step.map do |substep|
|
88
|
-
case substep["type"]
|
89
|
-
when "cookware"
|
90
|
-
substep["name"]
|
91
|
-
when "ingredient"
|
92
|
-
substep["name"]
|
93
|
-
when "timer"
|
94
|
-
"#{substep["quantity"]} #{substep["units"]}"
|
95
|
-
when "text"
|
96
|
-
substep["value"]
|
97
|
-
end
|
98
|
-
end.join
|
99
|
-
end
|
56
|
+
def render_ingredients_section(ingredients)
|
57
|
+
return "" if ingredients.empty?
|
100
58
|
|
101
|
-
|
102
|
-
"
|
59
|
+
ingredient_items = ingredients.map { |ingredient| @renderer.render_ingredient(ingredient) }
|
60
|
+
ingredients_list = @renderer.render_list(ingredient_items, "ul", @cooklang_config.css_classes["ingredient_list"])
|
61
|
+
@renderer.render_section(@cooklang_config.headings["ingredients"], ingredients_list, @cooklang_config.css_classes["ingredients_section"])
|
103
62
|
end
|
104
|
-
end
|
105
63
|
|
106
|
-
|
107
|
-
|
64
|
+
def render_cookware_section(cookware)
|
65
|
+
return "" if cookware.empty?
|
108
66
|
|
109
|
-
|
110
|
-
|
67
|
+
cookware_items = cookware.map { |item| @renderer.render_cookware(item) }
|
68
|
+
cookware_list = @renderer.render_list(cookware_items, "ul", @cooklang_config.css_classes["cookware_list"])
|
69
|
+
@renderer.render_section(@cooklang_config.headings["cookware"], cookware_list, @cooklang_config.css_classes["cookware_section"])
|
111
70
|
end
|
112
71
|
|
113
|
-
def
|
114
|
-
".
|
115
|
-
end
|
72
|
+
def render_steps_section(steps)
|
73
|
+
return "" if steps.empty?
|
116
74
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
ingredients = recipe["steps"].flatten.select { |item|
|
121
|
-
item["type"] == "ingredient"
|
122
|
-
}.map { |item|
|
123
|
-
Ingredient.new(item["quantity"], item["units"], item["name"])
|
124
|
-
}
|
125
|
-
|
126
|
-
cookware = recipe["steps"].flatten.select { |item|
|
127
|
-
item["type"] == "cookware"
|
128
|
-
}.map { |item|
|
129
|
-
CookWare.new(item["quantity"], item["name"])
|
130
|
-
}
|
131
|
-
|
132
|
-
steps = recipe["steps"].map do |step|
|
133
|
-
Step.new(step)
|
134
|
-
end
|
135
|
-
|
136
|
-
ingredients_list = UnorderedList.new(ingredients)
|
137
|
-
cookware_list = UnorderedList.new(cookware)
|
138
|
-
steps_list = OrderedList.new(steps)
|
139
|
-
|
140
|
-
HtmlBeautifier.beautify(
|
141
|
-
"<h2>Ingredients</h2>" +
|
142
|
-
ingredients_list.to_html +
|
143
|
-
"<h2>Cookware</h2>" +
|
144
|
-
cookware_list.to_html +
|
145
|
-
"<h2>Steps</h2>" +
|
146
|
-
steps_list.to_html
|
147
|
-
) + "\n"
|
75
|
+
step_items = steps.map { |step| @renderer.render_step(step) }
|
76
|
+
steps_list = @renderer.render_list(step_items, "ol", @cooklang_config.css_classes["steps_list"])
|
77
|
+
@renderer.render_section(@cooklang_config.headings["steps"], steps_list, @cooklang_config.css_classes["steps_section"])
|
148
78
|
end
|
149
79
|
end
|
150
80
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jekyll-cooklang-converter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- BraeTroutman
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jekyll
|
@@ -106,6 +106,7 @@ extensions: []
|
|
106
106
|
extra_rdoc_files: []
|
107
107
|
files:
|
108
108
|
- ".standard.yml"
|
109
|
+
- CLAUDE.md
|
109
110
|
- README.md
|
110
111
|
- Rakefile
|
111
112
|
- clash/_clash.yml
|
@@ -120,6 +121,11 @@ files:
|
|
120
121
|
- lib/jekyll-cooklang-converter.rb
|
121
122
|
- lib/jekyll-cooklang-converter/version.rb
|
122
123
|
- lib/jekyll/converters/cooklang.rb
|
124
|
+
- lib/jekyll/converters/cooklang/configuration.rb
|
125
|
+
- lib/jekyll/converters/cooklang/formatters.rb
|
126
|
+
- lib/jekyll/converters/cooklang/legacy_compatibility.rb
|
127
|
+
- lib/jekyll/converters/cooklang/recipe_parser.rb
|
128
|
+
- lib/jekyll/converters/cooklang/renderers.rb
|
123
129
|
- sig/jekyll/cooklang/converter.rbs
|
124
130
|
homepage: https://github.com/BraeTroutman/jekyll-cooklang-converter
|
125
131
|
licenses:
|