cooklang 1.0.1 → 1.0.3
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/.github/workflows/test.yml +2 -2
- data/.gitignore +1 -0
- data/README.md +191 -12
- data/cooklang.gemspec +2 -2
- data/lib/cooklang/builders/recipe_builder.rb +0 -4
- data/lib/cooklang/builders/step_builder.rb +0 -2
- data/lib/cooklang/formatter.rb +6 -2
- data/lib/cooklang/formatters/hash.rb +257 -0
- data/lib/cooklang/formatters/json.rb +13 -0
- data/lib/cooklang/formatters/text.rb +1 -3
- data/lib/cooklang/metadata.rb +2 -2
- data/lib/cooklang/parser.rb +0 -6
- data/lib/cooklang/parsers/cookware_parser.rb +0 -3
- data/lib/cooklang/parsers/ingredient_parser.rb +0 -3
- data/lib/cooklang/parsers/timer_parser.rb +0 -3
- data/lib/cooklang/processors/element_parser.rb +0 -5
- data/lib/cooklang/processors/metadata_processor.rb +0 -2
- data/lib/cooklang/processors/step_processor.rb +0 -4
- data/lib/cooklang/processors/token_processor.rb +0 -3
- data/lib/cooklang/recipe.rb +6 -9
- data/lib/cooklang/version.rb +1 -1
- data/lib/cooklang.rb +31 -3
- data/spec/models/recipe_spec.rb +209 -6
- metadata +10 -12
- data/Gemfile.lock +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4a3402d1f6f0e5864c0e59c8f237f0d06ca9a469ef3783f51b49e48d2ab1adb
|
4
|
+
data.tar.gz: bba07497106184771d712868f8abaaca2538bff3b85278357ba8d62438e0d452
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d89c0f9d7828567958e37b3b98f2318d5d69e6bb64b31ef2a1536a14e142c031143c798236e36da450dd5f1ac15370306fa2694ef8358cbe790358c621e642d
|
7
|
+
data.tar.gz: f1d1881eccc351da77bb473447e4b1eb8508d951ed71ad044e082bc8ed7ab92cbbbd8cb9904a996595db74b04ee91ef56896a9b3d75b1c12e51a39503d3615fc
|
data/.github/workflows/test.yml
CHANGED
@@ -11,7 +11,7 @@ jobs:
|
|
11
11
|
runs-on: ubuntu-latest
|
12
12
|
strategy:
|
13
13
|
matrix:
|
14
|
-
ruby-version: ['3.2', '3.3', '3.4', '3.5']
|
14
|
+
ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5']
|
15
15
|
|
16
16
|
steps:
|
17
17
|
- uses: actions/checkout@v4
|
@@ -29,7 +29,7 @@ jobs:
|
|
29
29
|
run: bundle exec rubocop
|
30
30
|
|
31
31
|
- uses: qltysh/qlty-action/coverage@v2
|
32
|
-
if: matrix.ruby-version == '3.
|
32
|
+
if: matrix.ruby-version == '3.4'
|
33
33
|
with:
|
34
34
|
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
35
35
|
files: coverage/.resultset.json
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -7,6 +7,10 @@
|
|
7
7
|
|
8
8
|
A Ruby parser for the [Cooklang](https://cooklang.org) recipe markup language.
|
9
9
|
|
10
|
+
## Requirements
|
11
|
+
|
12
|
+
- Ruby 2.7 or higher
|
13
|
+
|
10
14
|
## Installation
|
11
15
|
|
12
16
|
Add to your Gemfile:
|
@@ -52,19 +56,194 @@ end
|
|
52
56
|
|
53
57
|
# Parse from file
|
54
58
|
recipe = Cooklang.parse_file('pancakes.cook')
|
59
|
+
```
|
60
|
+
|
61
|
+
## Formatters
|
62
|
+
|
63
|
+
The gem provides three built-in formatters for different output needs. Here's an example using Uncle Roger's Fried Rice recipe:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
>> title: Uncle Roger's Fried Rice
|
67
|
+
>> servings: 2
|
68
|
+
>> prep_time: 10 minutes
|
69
|
+
>> cook_time: 8 minutes
|
70
|
+
|
71
|
+
Heat @peanut oil{1%tbsp} in #wok over high heat until smoking.
|
72
|
+
|
73
|
+
Add @garlic{3%cloves}(minced) and @shallots{2}(diced), stir-fry for ~{30%seconds}.
|
74
|
+
|
75
|
+
Add @eggs{3}(beaten) and scramble until almost set.
|
76
|
+
|
77
|
+
Add @day-old rice{3%cups} and stir-fry, breaking up clumps for ~{2%minutes}.
|
78
|
+
|
79
|
+
Season with @soy sauce{2%tbsp}, @sesame oil{1%tbsp}, @msg{1%tsp}, and @white pepper{0.5%tsp}.
|
80
|
+
|
81
|
+
Add @spring onions{3%stalks}(chopped) and stir for ~{30%seconds} more.
|
82
|
+
|
83
|
+
Serve immediately while hot!
|
84
|
+
```
|
85
|
+
|
86
|
+
### Text Formatter
|
87
|
+
Human-readable format with ingredients list and numbered steps:
|
88
|
+
|
89
|
+
Usage: `Cooklang::Formatters::Text.new(recipe).generate`
|
90
|
+
|
91
|
+
```
|
92
|
+
Ingredients:
|
93
|
+
peanut oil 1 tbsp
|
94
|
+
garlic 3 cloves
|
95
|
+
shallots 2
|
96
|
+
eggs 3
|
97
|
+
day-old rice 3 cups
|
98
|
+
soy sauce 2 tbsp
|
99
|
+
sesame oil 1 tbsp
|
100
|
+
msg 1 tsp
|
101
|
+
white pepper 0.5 tsp
|
102
|
+
spring onions 3 stalks
|
103
|
+
|
104
|
+
Steps:
|
105
|
+
1. Heat peanut oil in wok over high heat until smoking.
|
106
|
+
2. Add garlic and shallots, stirfry for for 30 seconds.
|
107
|
+
3. Add eggs and scramble until almost set.
|
108
|
+
4. Add day-old rice and stirfry, breaking up clumps for for 2 minutes.
|
109
|
+
5. Season with soy sauce, sesame oil, msg, and white pepper.
|
110
|
+
6. Add spring onions and stir for for 30 seconds more.
|
111
|
+
7. Serve immediately while hot!
|
112
|
+
```
|
113
|
+
|
114
|
+
### Hash Formatter
|
115
|
+
Structured Ruby hash format compatible with Cook CLI:
|
116
|
+
|
117
|
+
Usage: `Cooklang::Formatters::Hash.new(recipe).generate`
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
{:metadata=>
|
121
|
+
{:map=>
|
122
|
+
{"title"=>"Uncle Roger's Fried Rice",
|
123
|
+
"servings"=>"2",
|
124
|
+
"prep_time"=>"10 minutes",
|
125
|
+
"cook_time"=>"8 minutes"}},
|
126
|
+
:sections=>
|
127
|
+
[{:name=>nil,
|
128
|
+
:content=>
|
129
|
+
[{:type=>"step",
|
130
|
+
:value=>
|
131
|
+
{:items=>
|
132
|
+
[{:type=>"text", :value=>"Heat "},
|
133
|
+
{:type=>"ingredient", :index=>0},
|
134
|
+
{:type=>"text", :value=>" in "},
|
135
|
+
{:type=>"cookware", :index=>0},
|
136
|
+
{:type=>"text", :value=>" over high heat until smoking."}],
|
137
|
+
:number=>1}},
|
138
|
+
{:type=>"step",
|
139
|
+
:value=>
|
140
|
+
{:items=>
|
141
|
+
[{:type=>"text", :value=>"Add "},
|
142
|
+
{:type=>"ingredient", :index=>1},
|
143
|
+
{:type=>"text", :value=>" and "},
|
144
|
+
{:type=>"ingredient", :index=>2},
|
145
|
+
{:type=>"text", :value=>" and stir for "},
|
146
|
+
{:type=>"timer", :index=>0}],
|
147
|
+
:number=>2}}]}],
|
148
|
+
:ingredients=>
|
149
|
+
[{:name=>"peanut oil",
|
150
|
+
:quantity=>
|
151
|
+
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>1.0}},
|
152
|
+
:unit=>"tbsp"}},
|
153
|
+
{:name=>"garlic",
|
154
|
+
:quantity=>
|
155
|
+
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>3.0}},
|
156
|
+
:unit=>"cloves"},
|
157
|
+
:note=>"minced"},
|
158
|
+
{:name=>"spring onions",
|
159
|
+
:quantity=>
|
160
|
+
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>3.0}},
|
161
|
+
:unit=>"stalks"},
|
162
|
+
:note=>"chopped"}],
|
163
|
+
:cookware=>[{:name=>"wok"}],
|
164
|
+
:timers=>
|
165
|
+
[{:quantity=>
|
166
|
+
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>30.0}},
|
167
|
+
:unit=>"seconds"}},
|
168
|
+
{:quantity=>
|
169
|
+
{:value=>{:type=>"number", :value=>{:type=>"regular", :value=>2.0}},
|
170
|
+
:unit=>"minutes"}}]}
|
171
|
+
```
|
55
172
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
173
|
+
### JSON Formatter
|
174
|
+
JSON string format for APIs and data interchange:
|
175
|
+
|
176
|
+
Usage: `Cooklang::Formatters::Json.new(recipe).generate`
|
177
|
+
|
178
|
+
```json
|
179
|
+
{
|
180
|
+
"metadata": {
|
181
|
+
"map": {
|
182
|
+
"title": "Uncle Roger's Fried Rice",
|
183
|
+
"servings": "2",
|
184
|
+
"prep_time": "10 minutes",
|
185
|
+
"cook_time": "8 minutes"
|
186
|
+
}
|
187
|
+
},
|
188
|
+
"sections": [
|
189
|
+
{
|
190
|
+
"name": null,
|
191
|
+
"content": [
|
192
|
+
{
|
193
|
+
"type": "step",
|
194
|
+
"value": {
|
195
|
+
"items": [
|
196
|
+
{"type": "text", "value": "Heat "},
|
197
|
+
{"type": "ingredient", "index": 0},
|
198
|
+
{"type": "text", "value": " in "},
|
199
|
+
{"type": "cookware", "index": 0},
|
200
|
+
{"type": "text", "value": " over high heat until smoking."}
|
201
|
+
],
|
202
|
+
"number": 1
|
203
|
+
}
|
204
|
+
},
|
205
|
+
{
|
206
|
+
"type": "step",
|
207
|
+
"value": {
|
208
|
+
"items": [
|
209
|
+
{"type": "text", "value": "Add "},
|
210
|
+
{"type": "ingredient", "index": 1},
|
211
|
+
{"type": "text", "value": " and stir for "},
|
212
|
+
{"type": "timer", "index": 0}
|
213
|
+
],
|
214
|
+
"number": 2
|
215
|
+
}
|
216
|
+
}
|
217
|
+
]
|
218
|
+
}
|
219
|
+
],
|
220
|
+
"ingredients": [
|
221
|
+
{
|
222
|
+
"name": "peanut oil",
|
223
|
+
"quantity": {
|
224
|
+
"value": {"type": "number", "value": {"type": "regular", "value": 1.0}},
|
225
|
+
"unit": "tbsp"
|
226
|
+
}
|
227
|
+
},
|
228
|
+
{
|
229
|
+
"name": "garlic",
|
230
|
+
"quantity": {
|
231
|
+
"value": {"type": "number", "value": {"type": "regular", "value": 3.0}},
|
232
|
+
"unit": "cloves"
|
233
|
+
},
|
234
|
+
"note": "minced"
|
235
|
+
}
|
236
|
+
],
|
237
|
+
"cookware": [{"name": "wok"}],
|
238
|
+
"timers": [
|
239
|
+
{
|
240
|
+
"quantity": {
|
241
|
+
"value": {"type": "number", "value": {"type": "regular", "value": 30.0}},
|
242
|
+
"unit": "seconds"
|
243
|
+
}
|
244
|
+
}
|
245
|
+
]
|
246
|
+
}
|
68
247
|
```
|
69
248
|
|
70
249
|
## API
|
data/cooklang.gemspec
CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.description = "Cooklang is a markup language for recipes that allows you to define ingredients, cookware, timers, and metadata in a structured way. This gem provides a Ruby parser for Cooklang files."
|
15
15
|
spec.homepage = "https://github.com/jamesbrooks/cooklang"
|
16
16
|
spec.license = "MIT"
|
17
|
-
spec.required_ruby_version = ">=
|
17
|
+
spec.required_ruby_version = ">= 2.7.0"
|
18
18
|
|
19
19
|
spec.metadata["homepage_uri"] = spec.homepage
|
20
20
|
spec.metadata["source_code_uri"] = "https://github.com/jamesbrooks/cooklang"
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
26
26
|
spec.require_paths = ["lib"]
|
27
27
|
|
28
|
-
spec.add_development_dependency "bundler", "
|
28
|
+
spec.add_development_dependency "bundler", ">= 2.1"
|
29
29
|
spec.add_development_dependency "rake", "~> 13.0"
|
30
30
|
spec.add_development_dependency "rspec", "~> 3.13"
|
31
31
|
spec.add_development_dependency "rubocop", "~> 1.80"
|
data/lib/cooklang/formatter.rb
CHANGED
@@ -8,8 +8,12 @@ module Cooklang
|
|
8
8
|
@recipe = recipe
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
raise NotImplementedError, "Subclasses must implement #
|
11
|
+
def generate(*args)
|
12
|
+
raise NotImplementedError, "Subclasses must implement #generate"
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s(*args)
|
16
|
+
generate(*args).to_s
|
13
17
|
end
|
14
18
|
|
15
19
|
private
|
@@ -0,0 +1,257 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cooklang
|
4
|
+
module Formatters
|
5
|
+
class Hash < Formatter
|
6
|
+
def generate(*args)
|
7
|
+
{
|
8
|
+
metadata: format_metadata,
|
9
|
+
sections: format_sections,
|
10
|
+
ingredients: format_ingredients,
|
11
|
+
cookware: format_cookware,
|
12
|
+
timers: format_timers,
|
13
|
+
inline_quantities: [],
|
14
|
+
data: format_data
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def format_metadata
|
20
|
+
{
|
21
|
+
map: recipe.metadata.to_h.transform_values(&:to_s)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def format_sections
|
26
|
+
[
|
27
|
+
{
|
28
|
+
name: nil,
|
29
|
+
content: format_steps_as_content
|
30
|
+
}
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
def format_steps_as_content
|
35
|
+
recipe.steps.each_with_index.map do |step, index|
|
36
|
+
{
|
37
|
+
type: "step",
|
38
|
+
value: {
|
39
|
+
items: format_step_items(step),
|
40
|
+
number: index + 1
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_step_items(step)
|
47
|
+
items = []
|
48
|
+
|
49
|
+
step.segments.each do |segment|
|
50
|
+
items << format_segment(segment)
|
51
|
+
end
|
52
|
+
|
53
|
+
items
|
54
|
+
end
|
55
|
+
|
56
|
+
def format_segment(segment)
|
57
|
+
case segment
|
58
|
+
when Hash
|
59
|
+
case segment[:type]
|
60
|
+
when :ingredient
|
61
|
+
{
|
62
|
+
type: "ingredient",
|
63
|
+
index: find_ingredient_index(segment[:name])
|
64
|
+
}
|
65
|
+
when :cookware
|
66
|
+
{
|
67
|
+
type: "cookware",
|
68
|
+
index: find_cookware_index(segment[:name])
|
69
|
+
}
|
70
|
+
when :timer
|
71
|
+
{
|
72
|
+
type: "timer",
|
73
|
+
index: find_timer_index(segment)
|
74
|
+
}
|
75
|
+
else
|
76
|
+
{
|
77
|
+
type: "text",
|
78
|
+
value: segment[:value] || ""
|
79
|
+
}
|
80
|
+
end
|
81
|
+
when String
|
82
|
+
{
|
83
|
+
type: "text",
|
84
|
+
value: segment
|
85
|
+
}
|
86
|
+
when Cooklang::Ingredient
|
87
|
+
{
|
88
|
+
type: "ingredient",
|
89
|
+
index: find_ingredient_index(segment.name)
|
90
|
+
}
|
91
|
+
when Cooklang::Cookware
|
92
|
+
{
|
93
|
+
type: "cookware",
|
94
|
+
index: find_cookware_index(segment.name)
|
95
|
+
}
|
96
|
+
when Cooklang::Timer
|
97
|
+
{
|
98
|
+
type: "timer",
|
99
|
+
index: find_timer_index(segment)
|
100
|
+
}
|
101
|
+
else
|
102
|
+
{
|
103
|
+
type: "text",
|
104
|
+
value: segment.to_s
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def format_ingredients
|
110
|
+
recipe.ingredients.map do |ingredient|
|
111
|
+
{
|
112
|
+
name: ingredient.name,
|
113
|
+
alias: nil,
|
114
|
+
quantity: format_ingredient_quantity(ingredient),
|
115
|
+
note: ingredient.notes,
|
116
|
+
reference: nil,
|
117
|
+
relation: {
|
118
|
+
type: "definition",
|
119
|
+
referenced_from: [],
|
120
|
+
defined_in_step: true,
|
121
|
+
reference_target: nil
|
122
|
+
},
|
123
|
+
modifiers: ""
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_ingredient_quantity(ingredient)
|
129
|
+
return nil unless ingredient.quantity && ingredient.quantity != "some"
|
130
|
+
|
131
|
+
{
|
132
|
+
value: {
|
133
|
+
type: "number",
|
134
|
+
value: {
|
135
|
+
type: "regular",
|
136
|
+
value: ingredient.quantity.to_f
|
137
|
+
}
|
138
|
+
},
|
139
|
+
unit: ingredient.unit
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def format_cookware
|
144
|
+
recipe.cookware.map do |cookware_item|
|
145
|
+
{
|
146
|
+
name: cookware_item.name,
|
147
|
+
alias: nil,
|
148
|
+
quantity: format_cookware_quantity(cookware_item),
|
149
|
+
note: nil,
|
150
|
+
relation: {
|
151
|
+
type: "definition",
|
152
|
+
referenced_from: [],
|
153
|
+
defined_in_step: true
|
154
|
+
},
|
155
|
+
modifiers: ""
|
156
|
+
}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def format_cookware_quantity(cookware_item)
|
161
|
+
return nil unless cookware_item.quantity && cookware_item.quantity != 1
|
162
|
+
|
163
|
+
{
|
164
|
+
value: {
|
165
|
+
type: "number",
|
166
|
+
value: {
|
167
|
+
type: "regular",
|
168
|
+
value: cookware_item.quantity.to_f
|
169
|
+
}
|
170
|
+
},
|
171
|
+
unit: nil
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
def format_timers
|
176
|
+
recipe.timers.map do |timer|
|
177
|
+
{
|
178
|
+
name: (timer.name.nil? || timer.name.empty?) ? nil : timer.name,
|
179
|
+
quantity: format_timer_quantity(timer)
|
180
|
+
}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def format_timer_quantity(timer)
|
185
|
+
return nil unless timer.duration
|
186
|
+
|
187
|
+
{
|
188
|
+
value: {
|
189
|
+
type: "number",
|
190
|
+
value: {
|
191
|
+
type: "regular",
|
192
|
+
value: timer.duration.to_f
|
193
|
+
}
|
194
|
+
},
|
195
|
+
unit: timer.unit
|
196
|
+
}
|
197
|
+
end
|
198
|
+
|
199
|
+
def find_ingredient_index(name)
|
200
|
+
recipe.ingredients.find_index { |ingredient| ingredient.name == name } || 0
|
201
|
+
end
|
202
|
+
|
203
|
+
def find_cookware_index(name)
|
204
|
+
recipe.cookware.find_index { |cookware_item| cookware_item.name == name } || 0
|
205
|
+
end
|
206
|
+
|
207
|
+
def find_timer_index(timer_data)
|
208
|
+
# For hash segments, we need to match by duration and unit
|
209
|
+
if timer_data.is_a?(Hash)
|
210
|
+
recipe.timers.find_index do |timer|
|
211
|
+
timer.duration == timer_data[:duration] && timer.unit == timer_data[:unit]
|
212
|
+
end || 0
|
213
|
+
elsif timer_data.is_a?(Cooklang::Timer)
|
214
|
+
recipe.timers.find_index { |timer| timer == timer_data } || 0
|
215
|
+
else
|
216
|
+
0
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def format_data
|
221
|
+
{
|
222
|
+
type: "Scaled",
|
223
|
+
target: {
|
224
|
+
factor: 1.0
|
225
|
+
},
|
226
|
+
ingredients: format_ingredient_data,
|
227
|
+
cookware: format_cookware_data,
|
228
|
+
timers: format_timer_data
|
229
|
+
}
|
230
|
+
end
|
231
|
+
|
232
|
+
def format_ingredient_data
|
233
|
+
recipe.ingredients.map do |ingredient|
|
234
|
+
if ingredient.quantity && ingredient.quantity != "some"
|
235
|
+
"scaled"
|
236
|
+
else
|
237
|
+
"noQuantity"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def format_cookware_data
|
243
|
+
recipe.cookware.map do |cookware_item|
|
244
|
+
if cookware_item.quantity && cookware_item.quantity != 1
|
245
|
+
"scaled"
|
246
|
+
else
|
247
|
+
"noQuantity"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def format_timer_data
|
253
|
+
recipe.timers.map { "fixed" }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
data/lib/cooklang/metadata.rb
CHANGED
data/lib/cooklang/parser.rb
CHANGED
@@ -1,11 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "lexer"
|
4
|
-
require_relative "processors/metadata_processor"
|
5
|
-
require_relative "processors/token_processor"
|
6
|
-
require_relative "processors/step_processor"
|
7
|
-
require_relative "builders/recipe_builder"
|
8
|
-
|
9
3
|
module Cooklang
|
10
4
|
class Parser
|
11
5
|
def parse(input)
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "../token_stream"
|
4
|
-
require_relative "../parsers/ingredient_parser"
|
5
|
-
require_relative "../parsers/cookware_parser"
|
6
|
-
require_relative "../parsers/timer_parser"
|
7
|
-
|
8
3
|
module Cooklang
|
9
4
|
module Processors
|
10
5
|
class ElementParser
|
data/lib/cooklang/recipe.rb
CHANGED
@@ -19,6 +19,7 @@ module Cooklang
|
|
19
19
|
(value || []).freeze
|
20
20
|
end
|
21
21
|
|
22
|
+
|
22
23
|
public
|
23
24
|
|
24
25
|
def ingredients_hash
|
@@ -35,15 +36,11 @@ module Cooklang
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def to_h
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
metadata: @metadata.to_h,
|
44
|
-
sections: @sections.map(&:to_h),
|
45
|
-
notes: @notes.map(&:to_h)
|
46
|
-
}
|
39
|
+
Formatters::Hash.new(self).generate
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_json(*args)
|
43
|
+
Formatters::Json.new(self).generate(*args)
|
47
44
|
end
|
48
45
|
|
49
46
|
def ==(other)
|
data/lib/cooklang/version.rb
CHANGED
data/lib/cooklang.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "cooklang/version"
|
4
|
-
|
5
|
-
|
6
|
-
require_relative "cooklang/recipe"
|
4
|
+
|
5
|
+
# Core classes
|
7
6
|
require_relative "cooklang/ingredient"
|
8
7
|
require_relative "cooklang/cookware"
|
9
8
|
require_relative "cooklang/timer"
|
@@ -11,8 +10,37 @@ require_relative "cooklang/step"
|
|
11
10
|
require_relative "cooklang/metadata"
|
12
11
|
require_relative "cooklang/section"
|
13
12
|
require_relative "cooklang/note"
|
13
|
+
|
14
|
+
# Parsing infrastructure
|
15
|
+
require_relative "cooklang/token_stream"
|
16
|
+
require_relative "cooklang/lexer"
|
17
|
+
|
18
|
+
# Parsers (depend on token_stream and core classes)
|
19
|
+
require_relative "cooklang/parsers/ingredient_parser"
|
20
|
+
require_relative "cooklang/parsers/cookware_parser"
|
21
|
+
require_relative "cooklang/parsers/timer_parser"
|
22
|
+
|
23
|
+
# Processors (depend on parsers and token_stream)
|
24
|
+
require_relative "cooklang/processors/element_parser"
|
25
|
+
require_relative "cooklang/processors/metadata_processor"
|
26
|
+
require_relative "cooklang/processors/token_processor"
|
27
|
+
|
28
|
+
# Builders (depend on core classes)
|
29
|
+
require_relative "cooklang/builders/step_builder"
|
30
|
+
require_relative "cooklang/builders/recipe_builder"
|
31
|
+
|
32
|
+
# Processors that depend on builders
|
33
|
+
require_relative "cooklang/processors/step_processor"
|
34
|
+
|
35
|
+
# Main parser (depends on processors and builders)
|
36
|
+
require_relative "cooklang/parser"
|
37
|
+
|
38
|
+
# Recipe (depends on formatters)
|
14
39
|
require_relative "cooklang/formatter"
|
40
|
+
require_relative "cooklang/formatters/hash"
|
41
|
+
require_relative "cooklang/formatters/json"
|
15
42
|
require_relative "cooklang/formatters/text"
|
43
|
+
require_relative "cooklang/recipe"
|
16
44
|
|
17
45
|
module Cooklang
|
18
46
|
class Error < StandardError; end
|
data/spec/models/recipe_spec.rb
CHANGED
@@ -96,7 +96,7 @@ RSpec.describe Cooklang::Recipe do
|
|
96
96
|
end
|
97
97
|
|
98
98
|
describe "#to_h" do
|
99
|
-
it "returns complete hash representation" do
|
99
|
+
it "returns complete hash representation in Cook CLI format" do
|
100
100
|
recipe = described_class.new(
|
101
101
|
ingredients: [ingredient],
|
102
102
|
cookware: [cookware],
|
@@ -107,11 +107,214 @@ RSpec.describe Cooklang::Recipe do
|
|
107
107
|
|
108
108
|
hash = recipe.to_h
|
109
109
|
|
110
|
-
expect(hash
|
111
|
-
expect(hash
|
112
|
-
expect(hash
|
113
|
-
expect(hash
|
114
|
-
expect(hash
|
110
|
+
expect(hash).to have_key(:metadata)
|
111
|
+
expect(hash).to have_key(:sections)
|
112
|
+
expect(hash).to have_key(:ingredients)
|
113
|
+
expect(hash).to have_key(:cookware)
|
114
|
+
expect(hash).to have_key(:timers)
|
115
|
+
expect(hash).to have_key(:inline_quantities)
|
116
|
+
|
117
|
+
expect(hash[:metadata]).to have_key(:map)
|
118
|
+
expect(hash[:ingredients]).to be_an(Array)
|
119
|
+
expect(hash[:cookware]).to be_an(Array)
|
120
|
+
expect(hash[:timers]).to be_an(Array)
|
121
|
+
expect(hash[:sections]).to be_an(Array)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "comprehensive output tests" do
|
126
|
+
let(:recipe_text) do
|
127
|
+
<<~RECIPE
|
128
|
+
>> title: Test Recipe
|
129
|
+
>> servings: 4
|
130
|
+
>> prep_time: 15 minutes
|
131
|
+
|
132
|
+
Heat #large pot{} over medium heat.
|
133
|
+
|
134
|
+
Add @olive oil{2%tbsp} and @onion{1%medium}(diced).
|
135
|
+
Cook for ~{5%minutes} until soft.
|
136
|
+
|
137
|
+
Add @salt{} and @pepper{} to taste.
|
138
|
+
Season with @garlic{2%cloves}(minced).
|
139
|
+
RECIPE
|
140
|
+
end
|
141
|
+
|
142
|
+
let(:parsed_recipe) { Cooklang.parse(recipe_text) }
|
143
|
+
|
144
|
+
let(:expected_hash) do
|
145
|
+
{
|
146
|
+
metadata: { map: { "title" => "Test Recipe", "servings" => "4", "prep_time" => "15 minutes" } },
|
147
|
+
sections: [
|
148
|
+
{
|
149
|
+
name: nil,
|
150
|
+
content: [
|
151
|
+
{ type: "step", value: { items: [{ type: "text", value: "Heat " }, { type: "cookware", index: 0 }, { type: "text", value: " over medium heat." }], number: 1 } },
|
152
|
+
{ type: "step", value: { items: [{ type: "text", value: "Add " }, { type: "ingredient", index: 0 }, { type: "text", value: " and " }, { type: "ingredient", index: 1 }, { type: "text", value: "." }, { type: "text", value: "\n" }, { type: "text", value: "Cook for " }, { type: "timer", index: 0 }, { type: "text", value: " until soft." }], number: 2 } },
|
153
|
+
{ type: "step", value: { items: [{ type: "text", value: "Add " }, { type: "ingredient", index: 2 }, { type: "text", value: " and " }, { type: "ingredient", index: 3 }, { type: "text", value: " to taste." }, { type: "text", value: "\n" }, { type: "text", value: "Season with " }, { type: "ingredient", index: 4 }, { type: "text", value: "." }], number: 3 } }
|
154
|
+
]
|
155
|
+
}
|
156
|
+
],
|
157
|
+
ingredients: [
|
158
|
+
{ name: "olive oil", alias: nil, quantity: { value: { type: "number", value: { type: "regular", value: 2.0 } }, unit: "tbsp" }, note: nil, reference: nil, relation: { type: "definition", referenced_from: [], defined_in_step: true, reference_target: nil }, modifiers: "" },
|
159
|
+
{ name: "onion", alias: nil, quantity: { value: { type: "number", value: { type: "regular", value: 1.0 } }, unit: "medium" }, note: "diced", reference: nil, relation: { type: "definition", referenced_from: [], defined_in_step: true, reference_target: nil }, modifiers: "" },
|
160
|
+
{ name: "salt", alias: nil, quantity: nil, note: nil, reference: nil, relation: { type: "definition", referenced_from: [], defined_in_step: true, reference_target: nil }, modifiers: "" },
|
161
|
+
{ name: "pepper", alias: nil, quantity: nil, note: nil, reference: nil, relation: { type: "definition", referenced_from: [], defined_in_step: true, reference_target: nil }, modifiers: "" },
|
162
|
+
{ name: "garlic", alias: nil, quantity: { value: { type: "number", value: { type: "regular", value: 2.0 } }, unit: "cloves" }, note: "minced", reference: nil, relation: { type: "definition", referenced_from: [], defined_in_step: true, reference_target: nil }, modifiers: "" }
|
163
|
+
],
|
164
|
+
cookware: [
|
165
|
+
{ name: "large pot", alias: nil, quantity: nil, note: nil, relation: { type: "definition", referenced_from: [], defined_in_step: true }, modifiers: "" }
|
166
|
+
],
|
167
|
+
timers: [
|
168
|
+
{ name: nil, quantity: { value: { type: "number", value: { type: "regular", value: 5.0 } }, unit: "minutes" } }
|
169
|
+
],
|
170
|
+
inline_quantities: [],
|
171
|
+
data: { type: "Scaled", target: { factor: 1.0 }, ingredients: ["scaled", "scaled", "noQuantity", "noQuantity", "scaled"], cookware: ["noQuantity"], timers: ["fixed"] }
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
let(:expected_json) do
|
176
|
+
{
|
177
|
+
"metadata" => { "map" => { "title" => "Test Recipe", "servings" => "4", "prep_time" => "15 minutes" } },
|
178
|
+
"sections" => [
|
179
|
+
{
|
180
|
+
"name" => nil,
|
181
|
+
"content" => [
|
182
|
+
{ "type" => "step", "value" => { "items" => [{ "type" => "text", "value" => "Heat " }, { "type" => "cookware", "index" => 0 }, { "type" => "text", "value" => " over medium heat." }], "number" => 1 } },
|
183
|
+
{ "type" => "step", "value" => { "items" => [{ "type" => "text", "value" => "Add " }, { "type" => "ingredient", "index" => 0 }, { "type" => "text", "value" => " and " }, { "type" => "ingredient", "index" => 1 }, { "type" => "text", "value" => "." }, { "type" => "text", "value" => "\n" }, { "type" => "text", "value" => "Cook for " }, { "type" => "timer", "index" => 0 }, { "type" => "text", "value" => " until soft." }], "number" => 2 } },
|
184
|
+
{ "type" => "step", "value" => { "items" => [{ "type" => "text", "value" => "Add " }, { "type" => "ingredient", "index" => 2 }, { "type" => "text", "value" => " and " }, { "type" => "ingredient", "index" => 3 }, { "type" => "text", "value" => " to taste." }, { "type" => "text", "value" => "\n" }, { "type" => "text", "value" => "Season with " }, { "type" => "ingredient", "index" => 4 }, { "type" => "text", "value" => "." }], "number" => 3 } }
|
185
|
+
]
|
186
|
+
}
|
187
|
+
],
|
188
|
+
"ingredients" => [
|
189
|
+
{ "name" => "olive oil", "alias" => nil, "quantity" => { "value" => { "type" => "number", "value" => { "type" => "regular", "value" => 2.0 } }, "unit" => "tbsp" }, "note" => nil, "reference" => nil, "relation" => { "type" => "definition", "referenced_from" => [], "defined_in_step" => true, "reference_target" => nil }, "modifiers" => "" },
|
190
|
+
{ "name" => "onion", "alias" => nil, "quantity" => { "value" => { "type" => "number", "value" => { "type" => "regular", "value" => 1.0 } }, "unit" => "medium" }, "note" => "diced", "reference" => nil, "relation" => { "type" => "definition", "referenced_from" => [], "defined_in_step" => true, "reference_target" => nil }, "modifiers" => "" },
|
191
|
+
{ "name" => "salt", "alias" => nil, "quantity" => nil, "note" => nil, "reference" => nil, "relation" => { "type" => "definition", "referenced_from" => [], "defined_in_step" => true, "reference_target" => nil }, "modifiers" => "" },
|
192
|
+
{ "name" => "pepper", "alias" => nil, "quantity" => nil, "note" => nil, "reference" => nil, "relation" => { "type" => "definition", "referenced_from" => [], "defined_in_step" => true, "reference_target" => nil }, "modifiers" => "" },
|
193
|
+
{ "name" => "garlic", "alias" => nil, "quantity" => { "value" => { "type" => "number", "value" => { "type" => "regular", "value" => 2.0 } }, "unit" => "cloves" }, "note" => "minced", "reference" => nil, "relation" => { "type" => "definition", "referenced_from" => [], "defined_in_step" => true, "reference_target" => nil }, "modifiers" => "" }
|
194
|
+
],
|
195
|
+
"cookware" => [
|
196
|
+
{ "name" => "large pot", "alias" => nil, "quantity" => nil, "note" => nil, "relation" => { "type" => "definition", "referenced_from" => [], "defined_in_step" => true }, "modifiers" => "" }
|
197
|
+
],
|
198
|
+
"timers" => [
|
199
|
+
{ "name" => nil, "quantity" => { "value" => { "type" => "number", "value" => { "type" => "regular", "value" => 5.0 } }, "unit" => "minutes" } }
|
200
|
+
],
|
201
|
+
"inline_quantities" => [],
|
202
|
+
"data" => { "type" => "Scaled", "target" => { "factor" => 1.0 }, "ingredients" => ["scaled", "scaled", "noQuantity", "noQuantity", "scaled"], "cookware" => ["noQuantity"], "timers" => ["fixed"] }
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
describe "#to_h" do
|
207
|
+
it "returns complete canonical structure" do
|
208
|
+
expect(parsed_recipe.to_h).to eq(expected_hash)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "#to_json" do
|
213
|
+
it "returns JSON with null for canonical defaults" do
|
214
|
+
expect(JSON.parse(parsed_recipe.to_json)).to eq(expected_json)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe "canonical vs JSON consistency" do
|
219
|
+
it "maintains different representations for same recipe" do
|
220
|
+
# Internal canonical representation keeps defaults
|
221
|
+
expect(parsed_recipe.ingredients.find { |i| i.name == "salt" }.quantity).to eq("some")
|
222
|
+
expect(parsed_recipe.cookware.first.quantity).to eq(1)
|
223
|
+
|
224
|
+
# Both hash and JSON output convert defaults to nil for external use
|
225
|
+
hash = parsed_recipe.to_h
|
226
|
+
json_parsed = JSON.parse(parsed_recipe.to_json)
|
227
|
+
|
228
|
+
salt_hash = hash[:ingredients].find { |i| i[:name] == "salt" }
|
229
|
+
salt_json = json_parsed["ingredients"].find { |i| i["name"] == "salt" }
|
230
|
+
expect(salt_hash[:quantity]).to be_nil
|
231
|
+
expect(salt_json["quantity"]).to be_nil
|
232
|
+
|
233
|
+
expect(hash[:cookware].first[:quantity]).to be_nil
|
234
|
+
expect(json_parsed["cookware"].first["quantity"]).to be_nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe "edge cases" do
|
240
|
+
it "handles empty recipe" do
|
241
|
+
recipe = described_class.new(
|
242
|
+
ingredients: [],
|
243
|
+
cookware: [],
|
244
|
+
timers: [],
|
245
|
+
steps: [],
|
246
|
+
metadata: Cooklang::Metadata.new
|
247
|
+
)
|
248
|
+
|
249
|
+
hash = recipe.to_h
|
250
|
+
expect(hash[:ingredients]).to eq([])
|
251
|
+
expect(hash[:cookware]).to eq([])
|
252
|
+
expect(hash[:timers]).to eq([])
|
253
|
+
expect(hash[:sections].first[:content]).to eq([])
|
254
|
+
|
255
|
+
json = JSON.parse(recipe.to_json)
|
256
|
+
expect(json["ingredients"]).to eq([])
|
257
|
+
expect(json["cookware"]).to eq([])
|
258
|
+
expect(json["timers"]).to eq([])
|
259
|
+
end
|
260
|
+
|
261
|
+
it "handles ingredients with no quantity specified" do
|
262
|
+
ingredient = Cooklang::Ingredient.new(name: "salt")
|
263
|
+
recipe = described_class.new(
|
264
|
+
ingredients: [ingredient],
|
265
|
+
cookware: [],
|
266
|
+
timers: [],
|
267
|
+
steps: [],
|
268
|
+
metadata: Cooklang::Metadata.new
|
269
|
+
)
|
270
|
+
|
271
|
+
hash = recipe.to_h
|
272
|
+
salt = hash[:ingredients].first
|
273
|
+
expect(salt[:quantity]).to be_nil
|
274
|
+
|
275
|
+
json = JSON.parse(recipe.to_json)
|
276
|
+
salt_json = json["ingredients"].first
|
277
|
+
expect(salt_json["quantity"]).to be_nil
|
278
|
+
end
|
279
|
+
|
280
|
+
it "handles cookware with no quantity specified" do
|
281
|
+
cookware = Cooklang::Cookware.new(name: "pan")
|
282
|
+
recipe = described_class.new(
|
283
|
+
ingredients: [],
|
284
|
+
cookware: [cookware],
|
285
|
+
timers: [],
|
286
|
+
steps: [],
|
287
|
+
metadata: Cooklang::Metadata.new
|
288
|
+
)
|
289
|
+
|
290
|
+
hash = recipe.to_h
|
291
|
+
pan = hash[:cookware].first
|
292
|
+
expect(pan[:quantity]).to be_nil
|
293
|
+
|
294
|
+
json = JSON.parse(recipe.to_json)
|
295
|
+
pan_json = json["cookware"].first
|
296
|
+
expect(pan_json["quantity"]).to be_nil
|
297
|
+
end
|
298
|
+
|
299
|
+
it "handles fractional quantities" do
|
300
|
+
ingredient = Cooklang::Ingredient.new(name: "flour", quantity: 1.5, unit: "cups")
|
301
|
+
recipe = described_class.new(
|
302
|
+
ingredients: [ingredient],
|
303
|
+
cookware: [],
|
304
|
+
timers: [],
|
305
|
+
steps: [],
|
306
|
+
metadata: Cooklang::Metadata.new
|
307
|
+
)
|
308
|
+
|
309
|
+
hash = recipe.to_h
|
310
|
+
flour = hash[:ingredients].first
|
311
|
+
expect(flour[:quantity][:value][:value][:value]).to eq(1.5)
|
312
|
+
expect(flour[:quantity][:unit]).to eq("cups")
|
313
|
+
|
314
|
+
json = JSON.parse(recipe.to_json)
|
315
|
+
flour_json = json["ingredients"].first
|
316
|
+
expect(flour_json["quantity"]["value"]["value"]["value"]).to eq(1.5)
|
317
|
+
expect(flour_json["quantity"]["unit"]).to eq("cups")
|
115
318
|
end
|
116
319
|
end
|
117
320
|
|
metadata
CHANGED
@@ -1,29 +1,28 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cooklang
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Brooks
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: bundler
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
|
-
- - "
|
16
|
+
- - ">="
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '2.
|
18
|
+
version: '2.1'
|
20
19
|
type: :development
|
21
20
|
prerelease: false
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
|
-
- - "
|
23
|
+
- - ">="
|
25
24
|
- !ruby/object:Gem::Version
|
26
|
-
version: '2.
|
25
|
+
version: '2.1'
|
27
26
|
- !ruby/object:Gem::Dependency
|
28
27
|
name: rake
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -125,7 +124,6 @@ files:
|
|
125
124
|
- ".rspec"
|
126
125
|
- ".rubocop.yml"
|
127
126
|
- Gemfile
|
128
|
-
- Gemfile.lock
|
129
127
|
- LICENSE.txt
|
130
128
|
- README.md
|
131
129
|
- Rakefile
|
@@ -135,6 +133,8 @@ files:
|
|
135
133
|
- lib/cooklang/builders/step_builder.rb
|
136
134
|
- lib/cooklang/cookware.rb
|
137
135
|
- lib/cooklang/formatter.rb
|
136
|
+
- lib/cooklang/formatters/hash.rb
|
137
|
+
- lib/cooklang/formatters/json.rb
|
138
138
|
- lib/cooklang/formatters/text.rb
|
139
139
|
- lib/cooklang/ingredient.rb
|
140
140
|
- lib/cooklang/lexer.rb
|
@@ -178,7 +178,6 @@ metadata:
|
|
178
178
|
homepage_uri: https://github.com/jamesbrooks/cooklang
|
179
179
|
source_code_uri: https://github.com/jamesbrooks/cooklang
|
180
180
|
rubygems_mfa_required: 'true'
|
181
|
-
post_install_message:
|
182
181
|
rdoc_options: []
|
183
182
|
require_paths:
|
184
183
|
- lib
|
@@ -186,15 +185,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
186
185
|
requirements:
|
187
186
|
- - ">="
|
188
187
|
- !ruby/object:Gem::Version
|
189
|
-
version:
|
188
|
+
version: 2.7.0
|
190
189
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
191
190
|
requirements:
|
192
191
|
- - ">="
|
193
192
|
- !ruby/object:Gem::Version
|
194
193
|
version: '0'
|
195
194
|
requirements: []
|
196
|
-
rubygems_version: 3.
|
197
|
-
signing_key:
|
195
|
+
rubygems_version: 3.7.1
|
198
196
|
specification_version: 4
|
199
197
|
summary: A Ruby parser for the Cooklang recipe markup language.
|
200
198
|
test_files:
|
data/Gemfile.lock
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
cooklang (1.0.0)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: https://rubygems.org/
|
8
|
-
specs:
|
9
|
-
ast (2.4.3)
|
10
|
-
diff-lcs (1.6.2)
|
11
|
-
docile (1.4.1)
|
12
|
-
json (2.13.2)
|
13
|
-
language_server-protocol (3.17.0.5)
|
14
|
-
lint_roller (1.1.0)
|
15
|
-
parallel (1.27.0)
|
16
|
-
parser (3.3.9.0)
|
17
|
-
ast (~> 2.4.1)
|
18
|
-
racc
|
19
|
-
prism (1.4.0)
|
20
|
-
racc (1.8.1)
|
21
|
-
rainbow (3.1.1)
|
22
|
-
rake (13.3.0)
|
23
|
-
regexp_parser (2.11.2)
|
24
|
-
rspec (3.13.1)
|
25
|
-
rspec-core (~> 3.13.0)
|
26
|
-
rspec-expectations (~> 3.13.0)
|
27
|
-
rspec-mocks (~> 3.13.0)
|
28
|
-
rspec-core (3.13.5)
|
29
|
-
rspec-support (~> 3.13.0)
|
30
|
-
rspec-expectations (3.13.5)
|
31
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
32
|
-
rspec-support (~> 3.13.0)
|
33
|
-
rspec-mocks (3.13.5)
|
34
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
35
|
-
rspec-support (~> 3.13.0)
|
36
|
-
rspec-support (3.13.5)
|
37
|
-
rubocop (1.80.0)
|
38
|
-
json (~> 2.3)
|
39
|
-
language_server-protocol (~> 3.17.0.2)
|
40
|
-
lint_roller (~> 1.1.0)
|
41
|
-
parallel (~> 1.10)
|
42
|
-
parser (>= 3.3.0.2)
|
43
|
-
rainbow (>= 2.2.2, < 4.0)
|
44
|
-
regexp_parser (>= 2.9.3, < 3.0)
|
45
|
-
rubocop-ast (>= 1.46.0, < 2.0)
|
46
|
-
ruby-progressbar (~> 1.7)
|
47
|
-
unicode-display_width (>= 2.4.0, < 4.0)
|
48
|
-
rubocop-ast (1.46.0)
|
49
|
-
parser (>= 3.3.7.2)
|
50
|
-
prism (~> 1.4)
|
51
|
-
rubocop-performance (1.25.0)
|
52
|
-
lint_roller (~> 1.1)
|
53
|
-
rubocop (>= 1.75.0, < 2.0)
|
54
|
-
rubocop-ast (>= 1.38.0, < 2.0)
|
55
|
-
rubocop-rspec (3.6.0)
|
56
|
-
lint_roller (~> 1.1)
|
57
|
-
rubocop (~> 1.72, >= 1.72.1)
|
58
|
-
ruby-progressbar (1.13.0)
|
59
|
-
simplecov (0.22.0)
|
60
|
-
docile (~> 1.1)
|
61
|
-
simplecov-html (~> 0.11)
|
62
|
-
simplecov_json_formatter (~> 0.1)
|
63
|
-
simplecov-html (0.13.2)
|
64
|
-
simplecov_json_formatter (0.1.4)
|
65
|
-
unicode-display_width (3.1.5)
|
66
|
-
unicode-emoji (~> 4.0, >= 4.0.4)
|
67
|
-
unicode-emoji (4.0.4)
|
68
|
-
|
69
|
-
PLATFORMS
|
70
|
-
arm64-darwin-24
|
71
|
-
ruby
|
72
|
-
|
73
|
-
DEPENDENCIES
|
74
|
-
bundler (~> 2.7)
|
75
|
-
cooklang!
|
76
|
-
rake (~> 13.0)
|
77
|
-
rspec (~> 3.13)
|
78
|
-
rubocop (~> 1.80)
|
79
|
-
rubocop-performance (~> 1.25)
|
80
|
-
rubocop-rspec (~> 3.6)
|
81
|
-
simplecov (~> 0.22.0)
|
82
|
-
|
83
|
-
BUNDLED WITH
|
84
|
-
2.7.1
|