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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12645bcaffa4192f683b66dba99bab13816c443e95bfc230aefe1300b3a34cb6
4
- data.tar.gz: e41ce8b50f24b7ca9f78eb74016dc2a5802b90e7a87f05c95577ccfa2a2b9bf8
3
+ metadata.gz: 0ce3eb10976d607accb5a82ecf44070b6c6ea4f35cabe5300d2775ef240482fb
4
+ data.tar.gz: 42bd15562aac13ca34daec10c8f71daae5b73cdd48bf170e7f0ee8fdcccb8920
5
5
  SHA512:
6
- metadata.gz: ce4a582b365abf71907ca9b219835738be2a03339390e840a0cadb3d35c66b4686e6a75e86476c8749c55e6ed48cf9e20a8b4fec0209e76a6bbf07c09fe94645
7
- data.tar.gz: 21e44551283bd5a9e2f4a01670a885bd19a654b7279a20e4910d2e7d8897f2392a42603903621983873319a706beabbbcf4c23a6846976dd420face48fda39fe
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
@@ -10,3 +10,5 @@ whitelist:
10
10
  collections:
11
11
  recipes:
12
12
  output: true
13
+ cooklang:
14
+ legacy_mode: false
@@ -1,52 +1,40 @@
1
- <h2>Ingredients</h2>
2
- <ul>
3
- <li><em>1/2 box</em> mushrooms</li>
4
- <li><em>1 pound</em> ground sausage</li>
5
- <li><em>some</em> garlic powder</li>
6
- <li><em>some</em> italian seasoning</li>
7
- <li><em>some</em> chili powder</li>
8
- <li><em>1</em> large yellow onion</li>
9
- <li><em>some</em> olive oil</li>
10
- <li><em>some</em> salt</li>
11
- <li><em>1</em> bell pepper</li>
12
- <li><em>1 jar</em> pasta sauce</li>
13
- <li><em>1 15oz package</em> ricotta</li>
14
- <li><em>1 cup</em> shredded mozarella</li>
15
- <li><em>1 package</em> sliced provolone</li>
16
- <li><em>2 cups</em> mozarella cheese</li>
17
- </ul>
18
- <h2>Cookware</h2>
19
- <ul>
20
- <li><em>1</em> pot</li>
21
- <li><em>1</em> casserole dish</li>
22
- </ul>
23
- <h2>Steps</h2>
24
- <ol>
25
- <li>
26
- <p>set a pot of water on the stove, to boil (you'll need for pasta later), and preheat the oven to 350</p>
27
- </li>
28
- <li>
29
- <p>cut mushrooms into slices, and sautee (no oil) in a pan over medium high heat until browned. Set aside</p>
30
- </li>
31
- <li>
32
- <p>brown ground sausage in the same pan, seasoning with garlic powder, italian seasoning, and chili powder set aside</p>
33
- </li>
34
- <li>
35
- <p>the water should hopefully be boiling by now</p>
36
- </li>
37
- <li>
38
- <p>chop and sautee large yellow onion in olive oil. Seasoning with salt. Add the bell pepper, chopped</p>
39
- </li>
40
- <li>
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
- <h2>Ingredients</h2>
2
- <ul>
3
- <li><em>1/2 box</em> mushrooms</li>
4
- <li><em>1 pound</em> ground sausage</li>
5
- <li><em>some</em> garlic powder</li>
6
- <li><em>some</em> italian seasoning</li>
7
- <li><em>some</em> chili powder</li>
8
- <li><em>1</em> large yellow onion</li>
9
- <li><em>some</em> olive oil</li>
10
- <li><em>some</em> salt</li>
11
- <li><em>1</em> bell pepper</li>
12
- <li><em>1 jar</em> pasta sauce</li>
13
- <li><em>1 15oz package</em> ricotta</li>
14
- <li><em>1 cup</em> shredded mozarella</li>
15
- <li><em>1 package</em> sliced provolone</li>
16
- <li><em>2 cups</em> mozarella cheese</li>
17
- </ul>
18
- <h2>Cookware</h2>
19
- <ul>
20
- <li><em>1</em> pot</li>
21
- <li><em>1</em> casserole dish</li>
22
- </ul>
23
- <h2>Steps</h2>
24
- <ol>
25
- <li>
26
- <p>set a pot of water on the stove, to boil (you'll need for pasta later), and preheat the oven to 350</p>
27
- </li>
28
- <li>
29
- <p>cut mushrooms into slices, and sautee (no oil) in a pan over medium high heat until browned. Set aside</p>
30
- </li>
31
- <li>
32
- <p>brown ground sausage in the same pan, seasoning with garlic powder, italian seasoning, and chili powder set aside</p>
33
- </li>
34
- <li>
35
- <p>the water should hopefully be boiling by now</p>
36
- </li>
37
- <li>
38
- <p>chop and sautee large yellow onion in olive oil. Seasoning with salt. Add the bell pepper, chopped</p>
39
- </li>
40
- <li>
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 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
9
+ class CooklangConverter < Converter
10
+ safe true
12
11
 
13
- class Ingredient < ToHTML
14
- def initialize(quantity, unit, name)
15
- @name = name.to_s
16
- @unit = unit.to_s
17
- @quantity = if quantity.respond_to? :rationalize
18
- if quantity.rationalize(0.1).denominator == 1
19
- quantity.to_s
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 to_html
29
- return "<em>#{@quantity}</em> #{@name}" if @unit.empty?
30
- "<em>#{@quantity} #{@unit}</em> #{@name}"
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
- class Timer < ToHTML
35
- def initialize(quantity, unit, name)
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 to_html
42
- return "<em>#{@quantity} #{@unit}</em>" if @unit.empty?
43
- "<em>#{@quantity} #{@unit}</em> (#{@name})"
33
+ def output_ext(ext)
34
+ ".html"
44
35
  end
45
- end
46
36
 
47
- class CookWare < ToHTML
48
- def initialize(quantity, name)
49
- @name = name.to_s
50
- @quantity = quantity.to_s
51
- end
37
+ def convert(content)
38
+ setup
52
39
 
53
- def to_html
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
- class OrderedList < ToHTML
60
- def initialize(items)
61
- @items = items
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
- def to_html
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
- class UnorderedList < ToHTML
73
- def initialize(items)
74
- @items = items
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
- def to_html
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
- class Step < ToHTML
86
- def initialize(step)
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
- def to_html
102
- "<p>#{@text}</p>"
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
- class CooklangConverter < Converter
107
- safe true
64
+ def render_cookware_section(cookware)
65
+ return "" if cookware.empty?
108
66
 
109
- def matches(ext)
110
- ext =~ /^.cook/i
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 output_ext(ext)
114
- ".html"
115
- end
72
+ def render_steps_section(steps)
73
+ return "" if steps.empty?
116
74
 
117
- def convert(content)
118
- recipe = CooklangRb::Recipe.from(content)
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
@@ -1,5 +1,5 @@
1
1
  module Jekyll
2
2
  module CooklangConverter
3
- VERSION = "0.4.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  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.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: 2024-10-04 00:00:00.000000000 Z
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: