@4kk11/cooklang-sankey 0.1.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.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # cooklang-sankey
2
+
3
+ Generate Sankey diagram data from Cooklang recipes.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install cooklang-sankey
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Browser Environment
14
+
15
+ ```typescript
16
+ import { CooklangParser, generateSankeyData, optimizeSankeyData } from 'cooklang-sankey';
17
+
18
+ // Create a parser instance
19
+ const parser = new CooklangParser();
20
+
21
+ // Your Cooklang recipe text
22
+ const recipeText = `
23
+ ---
24
+ title: カルボナーラ
25
+ servings: 2
26
+ ---
27
+
28
+ @パスタ{200g}を茹でる。
29
+ @卵{2個}と@パルメザンチーズ{50g}を混ぜる。
30
+ @ベーコン{100g}を炒める。
31
+ パスタとソースを合わせて完成。
32
+ `;
33
+
34
+ // Parse the recipe text
35
+ const [recipe] = parser.parse(recipeText);
36
+
37
+ // Generate Sankey data from parsed recipe
38
+ const data = generateSankeyData(recipe, {
39
+ finalNodeName: "完成品",
40
+ normalization: "logarithmic",
41
+ });
42
+
43
+ // Optionally optimize the data
44
+ const optimized = data ? optimizeSankeyData(data) : null;
45
+
46
+ console.log(optimized);
47
+ ```
48
+
49
+ ### Node.js Environment
50
+
51
+ ```typescript
52
+ import { CooklangParser, generateSankeyData, optimizeSankeyData } from 'cooklang-sankey';
53
+
54
+ // Create parser instance
55
+ const parser = new CooklangParser();
56
+
57
+ const recipeText = `
58
+ @パスタ{200g}を茹でる。
59
+ @卵{2個}と@パルメザンチーズ{50g}を混ぜる。
60
+ `;
61
+
62
+ // Parse the recipe text
63
+ const [recipe] = parser.parse(recipeText);
64
+
65
+ // Generate Sankey data from parsed recipe
66
+ const data = generateSankeyData(recipe);
67
+ const optimized = data ? optimizeSankeyData(data) : null;
68
+
69
+ console.log(JSON.stringify(optimized, null, 2));
70
+ ```
71
+
72
+ ### API
73
+
74
+ #### `generateSankeyData(recipe, options?)`
75
+
76
+ Generates Sankey diagram data from a parsed Cooklang recipe.
77
+
78
+ **Parameters:**
79
+ - `recipe: CooklangRecipe` - Parsed Cooklang recipe object
80
+ - `options?: SankeyGeneratorOptions`
81
+ - `finalNodeName?: string` - Name for the final node (default: "完成品")
82
+ - `normalization?: "logarithmic" | "linear" | "none"` - Value normalization method
83
+ - `minLinkValue?: number` - Minimum link value (default: 0.1)
84
+
85
+ **Returns:** `SankeyData | null`
86
+
87
+ #### `optimizeSankeyData(data)`
88
+
89
+ Validates and optimizes Sankey data by removing invalid links and orphaned nodes.
90
+
91
+ **Returns:** `SankeyData`
92
+
93
+ ### Types
94
+
95
+ ```typescript
96
+ interface SankeyData {
97
+ nodes: SankeyNode[];
98
+ links: SankeyLink[];
99
+ }
100
+
101
+ interface SankeyNode {
102
+ id: string;
103
+ name: string;
104
+ category: "ingredient" | "process" | "final";
105
+ value: number;
106
+ label: string;
107
+ originalValue?: number;
108
+ metadata?: BaseNodeMetadata;
109
+ }
110
+
111
+ interface SankeyLink {
112
+ source: string;
113
+ target: string;
114
+ value: number;
115
+ originalValue?: number;
116
+ metadata?: BaseLinkMetadata;
117
+ }
118
+ ```
119
+
120
+ ## React Integration
121
+
122
+ For React applications, we recommend creating custom hooks:
123
+
124
+ ```typescript
125
+ import { useMemo } from 'react';
126
+ import {
127
+ CooklangParser,
128
+ generateSankeyData,
129
+ optimizeSankeyData,
130
+ type CooklangRecipe
131
+ } from 'cooklang-sankey';
132
+
133
+ // Create a singleton parser instance
134
+ const parser = new CooklangParser();
135
+
136
+ export function useSankeyData(recipeText: string) {
137
+ return useMemo(() => {
138
+ if (!recipeText) return null;
139
+
140
+ // Parse the recipe text
141
+ const [recipe] = parser.parse(recipeText);
142
+
143
+ // Generate Sankey data from parsed recipe
144
+ const data = generateSankeyData(recipe);
145
+ return data ? optimizeSankeyData(data) : null;
146
+ }, [recipeText]);
147
+ }
148
+
149
+ // Or if you already have a parsed recipe:
150
+ export function useSankeyDataFromRecipe(recipe: CooklangRecipe | null) {
151
+ return useMemo(() => {
152
+ if (!recipe) return null;
153
+ const data = generateSankeyData(recipe);
154
+ return data ? optimizeSankeyData(data) : null;
155
+ }, [recipe]);
156
+ }
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,492 @@
1
+ 'use strict';
2
+
3
+ var cooklang = require('@cooklang/cooklang');
4
+ var toposort = require('toposort');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var toposort__default = /*#__PURE__*/_interopDefault(toposort);
9
+
10
+ // src/parser.ts
11
+ function extractMetadata(recipe) {
12
+ const result = {};
13
+ if (recipe.title) {
14
+ result.title = recipe.title;
15
+ }
16
+ if (recipe.description) {
17
+ result.description = recipe.description;
18
+ }
19
+ if (recipe.tags && recipe.tags.size > 0) {
20
+ result.tags = Array.from(recipe.tags);
21
+ }
22
+ if (recipe.time) {
23
+ const timeValue = recipe.time;
24
+ if (typeof timeValue === "object" && timeValue !== null) {
25
+ const parts = [];
26
+ if ("prep_time" in timeValue && timeValue.prep_time) {
27
+ parts.push(`prep: ${timeValue.prep_time}`);
28
+ }
29
+ if ("cook_time" in timeValue && timeValue.cook_time) {
30
+ parts.push(`cook: ${timeValue.cook_time}`);
31
+ }
32
+ if ("total_time" in timeValue && timeValue.total_time) {
33
+ parts.push(`total: ${timeValue.total_time}`);
34
+ }
35
+ if (parts.length > 0) {
36
+ result.cookingTime = parts.join(", ");
37
+ }
38
+ }
39
+ }
40
+ if (recipe.servings) {
41
+ const servings = recipe.servings;
42
+ if (Array.isArray(servings)) {
43
+ result.servings = servings.join("-");
44
+ } else if (typeof servings === "object" && servings !== null) {
45
+ result.servings = String(servings);
46
+ }
47
+ }
48
+ if (recipe.custom_metadata && recipe.custom_metadata.size > 0) {
49
+ for (const [key, value] of recipe.custom_metadata) {
50
+ if (!(key in result)) {
51
+ result[key] = value;
52
+ }
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
58
+ // src/constants.ts
59
+ var NORMALIZATION = {
60
+ /** Minimum normalized value for logarithmic scale */
61
+ LOG_MIN: 0.1,
62
+ /** Maximum normalized value for logarithmic scale */
63
+ LOG_MAX: 0.3,
64
+ /** Minimum normalized value for linear scale */
65
+ LINEAR_MIN: 0.1,
66
+ /** Range for linear normalization */
67
+ LINEAR_RANGE: 0.2,
68
+ /** Default value when normalization range is zero */
69
+ DEFAULT_VALUE: 1
70
+ };
71
+ var DEFAULT_FINAL_NODE_NAME = "\u5B8C\u6210\u54C1";
72
+ var DEFAULT_MIN_LINK_VALUE = 0.1;
73
+ var DEFAULT_NORMALIZATION = "logarithmic";
74
+ var FINAL_NODE_ID = "final_dish";
75
+ var DEFAULT_SECTION_NAME = "main";
76
+
77
+ // src/sankey/normalizer.ts
78
+ var normalizeIngredientValues = (ingredientNodes, normalization) => {
79
+ const result = /* @__PURE__ */ new Map();
80
+ if (normalization === "none") {
81
+ ingredientNodes.forEach((node) => {
82
+ result.set(node.id, node.value || NORMALIZATION.DEFAULT_VALUE);
83
+ });
84
+ return result;
85
+ }
86
+ const nodeValues = ingredientNodes.map((n) => n.value || NORMALIZATION.DEFAULT_VALUE);
87
+ const minValue = Math.min(...nodeValues);
88
+ const maxValue = Math.max(...nodeValues);
89
+ const valueRange = maxValue - minValue;
90
+ const normalizeValue = (value) => {
91
+ if (valueRange === 0) return NORMALIZATION.DEFAULT_VALUE;
92
+ if (normalization === "logarithmic") {
93
+ const normalizedLinear = (value - minValue) / valueRange;
94
+ const logScale = Math.log10(1 + normalizedLinear * 9);
95
+ return Math.min(Math.max(NORMALIZATION.LOG_MIN, logScale), NORMALIZATION.LOG_MAX);
96
+ } else {
97
+ return NORMALIZATION.LINEAR_MIN + (value - minValue) / valueRange * NORMALIZATION.LINEAR_RANGE;
98
+ }
99
+ };
100
+ ingredientNodes.forEach((node) => {
101
+ result.set(node.id, node.value ? normalizeValue(node.value) : NORMALIZATION.DEFAULT_VALUE);
102
+ });
103
+ return result;
104
+ };
105
+ var buildDAGAndSort = (nodes, edges) => {
106
+ const edgePairs = edges.map((edge) => [edge.from, edge.to]);
107
+ try {
108
+ return toposort__default.default(edgePairs);
109
+ } catch (error) {
110
+ console.error("Cycle detected in DAG:", error);
111
+ return nodes.map((node) => node.id);
112
+ }
113
+ };
114
+ var calculateNodeValues = (nodes, sortedNodeIds) => {
115
+ const nodeMap = /* @__PURE__ */ new Map();
116
+ const valueMap = /* @__PURE__ */ new Map();
117
+ nodes.forEach((node) => {
118
+ nodeMap.set(node.id, node);
119
+ if (node.category === "ingredient") {
120
+ valueMap.set(node.id, node.value);
121
+ } else {
122
+ valueMap.set(node.id, 0);
123
+ }
124
+ });
125
+ for (const nodeId of sortedNodeIds) {
126
+ const node = nodeMap.get(nodeId);
127
+ if (!node) continue;
128
+ if (node.category === "ingredient") {
129
+ continue;
130
+ }
131
+ let totalInputValue = 0;
132
+ for (const inputId of node.inputs) {
133
+ let inputValue = valueMap.get(inputId) || 0;
134
+ const inputNode = nodeMap.get(inputId);
135
+ if (inputNode && inputNode.outputs.length > 1) {
136
+ inputValue /= inputNode.outputs.length;
137
+ }
138
+ totalInputValue += inputValue;
139
+ }
140
+ valueMap.set(nodeId, totalInputValue);
141
+ }
142
+ return valueMap;
143
+ };
144
+ var formatValue = (value) => {
145
+ if (!value) return "";
146
+ if (value.type === "number") {
147
+ const num = cooklang.getNumericValue(value);
148
+ return num !== null ? num.toString() : "";
149
+ } else if (value.type === "range") {
150
+ const rangeValue = value.value;
151
+ const startNum = cooklang.getNumericValue({ type: "number", value: rangeValue.start });
152
+ const endNum = cooklang.getNumericValue({ type: "number", value: rangeValue.end });
153
+ if (startNum !== null && endNum !== null) {
154
+ return `${startNum}-${endNum}`;
155
+ }
156
+ return "";
157
+ } else if (value.type === "text") {
158
+ return value.value;
159
+ }
160
+ return "";
161
+ };
162
+ var formatQuantityAmount = (quantity) => {
163
+ if (!quantity) {
164
+ return { quantity: "", unit: "" };
165
+ }
166
+ return {
167
+ quantity: formatValue(quantity.value),
168
+ unit: quantity.unit || ""
169
+ };
170
+ };
171
+ var generateStepText = (step, recipe) => {
172
+ return step.items.map((item) => {
173
+ if (item.type === "text") {
174
+ return item.value;
175
+ } else if (item.type === "ingredient") {
176
+ const originalIngredient = recipe.ingredients[item.index];
177
+ if (!originalIngredient) return "";
178
+ const formatted = formatQuantityAmount(originalIngredient.quantity);
179
+ const quantityText = formatted.quantity && formatted.unit ? `(${formatted.quantity}${formatted.unit})` : formatted.quantity ? `(${formatted.quantity})` : "";
180
+ return `${originalIngredient.name}${quantityText}`;
181
+ } else if (item.type === "cookware") {
182
+ return recipe.cookware[item.index]?.name || "";
183
+ } else if (item.type === "timer") {
184
+ const timer = recipe.timers[item.index];
185
+ if (!timer) return "";
186
+ return timer.name || (timer.quantity ? cooklang.quantity_display(timer.quantity) : "");
187
+ }
188
+ return "";
189
+ }).join("");
190
+ };
191
+
192
+ // src/sankey/node-builders.ts
193
+ var calculateIngredientQuantity = (groupedQuantity) => {
194
+ if (cooklang.grouped_quantity_is_empty(groupedQuantity)) {
195
+ return { type: "text", label: "unknown" };
196
+ }
197
+ const displayText = cooklang.grouped_quantity_display(groupedQuantity);
198
+ const gq = groupedQuantity;
199
+ const quantityToUse = gq.fixed || gq.scalable || gq.fixed_unknown || gq.scalable_unknown;
200
+ if (quantityToUse) {
201
+ const numericValue = cooklang.getNumericValue(quantityToUse.value);
202
+ if (numericValue !== null) {
203
+ return { type: "number", value: numericValue, label: displayText || `${numericValue}` };
204
+ }
205
+ }
206
+ return { type: "text", label: displayText || "unknown" };
207
+ };
208
+ var createIngredientNodes = (recipe) => {
209
+ const nodes = [];
210
+ const indexMap = /* @__PURE__ */ new Map();
211
+ recipe.groupedIngredients.forEach(([ingredient, groupedQuantity], index) => {
212
+ const extracted = calculateIngredientQuantity(groupedQuantity);
213
+ const value = extracted.type === "number" ? extracted.value : 1;
214
+ const label = extracted.label;
215
+ const ingredientId = index.toString();
216
+ indexMap.set(index, ingredientId);
217
+ nodes.push({
218
+ id: ingredientId,
219
+ name: ingredient.name,
220
+ category: "ingredient",
221
+ value,
222
+ originalValue: value,
223
+ label,
224
+ inputs: [],
225
+ outputs: [],
226
+ metadata: {
227
+ originalIndex: index
228
+ }
229
+ });
230
+ });
231
+ return { nodes, indexMap };
232
+ };
233
+ var createStepNodes = (recipe) => {
234
+ const nodes = [];
235
+ for (const section of recipe.sections) {
236
+ for (const content of section.content) {
237
+ if (content.type === "step") {
238
+ const step = content.value;
239
+ const stepText = generateStepText(step, recipe);
240
+ const stepId = `step_${section.name || DEFAULT_SECTION_NAME}_${step.number}`;
241
+ nodes.push({
242
+ id: stepId,
243
+ name: stepText || `\u624B\u9806 ${step.number}`,
244
+ category: "process",
245
+ value: 0,
246
+ label: `${step.number}`,
247
+ inputs: [],
248
+ outputs: [],
249
+ metadata: {
250
+ stepNumber: step.number,
251
+ sectionName: section.name || void 0
252
+ }
253
+ });
254
+ }
255
+ }
256
+ }
257
+ return nodes;
258
+ };
259
+ var createFinalNode = (finalNodeName) => ({
260
+ id: FINAL_NODE_ID,
261
+ name: finalNodeName,
262
+ category: "final",
263
+ value: 0,
264
+ label: "",
265
+ inputs: [],
266
+ outputs: []
267
+ });
268
+
269
+ // src/sankey/edge-builders.ts
270
+ var getStepId = (sectionName, stepNumber) => {
271
+ return `step_${sectionName || DEFAULT_SECTION_NAME}_${stepNumber}`;
272
+ };
273
+ var extractIngredientUsageFromStep = (step) => {
274
+ const usages = [];
275
+ for (const item of step.items) {
276
+ if (item.type === "ingredient") {
277
+ usages.push({
278
+ ingredientIndex: item.index,
279
+ stepNumber: step.number
280
+ });
281
+ }
282
+ }
283
+ return usages;
284
+ };
285
+ var buildIngredientToStepEdges = (recipe, ingredientIndexMap, nodes) => {
286
+ const edges = [];
287
+ const nodeMap = /* @__PURE__ */ new Map();
288
+ nodes.forEach((node) => nodeMap.set(node.id, node));
289
+ for (const section of recipe.sections) {
290
+ for (const content of section.content) {
291
+ if (content.type === "step") {
292
+ const step = content.value;
293
+ const currentStepId = getStepId(section.name, step.number);
294
+ const usedIngredients = extractIngredientUsageFromStep(step);
295
+ for (const usage of usedIngredients) {
296
+ const ingredient = recipe.ingredients[usage.ingredientIndex];
297
+ const relation = ingredient.relation;
298
+ if (relation && relation.relation?.type === "reference" && relation.reference_target === "step") {
299
+ const referenceTo = relation.relation.references_to;
300
+ const sourceStepId = getStepId(section.name, referenceTo + 1);
301
+ edges.push({
302
+ from: sourceStepId,
303
+ to: currentStepId,
304
+ metadata: {
305
+ stepNumber: step.number,
306
+ transformationType: "cooking"
307
+ }
308
+ });
309
+ const sourceNode = nodeMap.get(sourceStepId);
310
+ const targetNode = nodeMap.get(currentStepId);
311
+ if (sourceNode && targetNode) {
312
+ sourceNode.outputs.push(currentStepId);
313
+ targetNode.inputs.push(sourceStepId);
314
+ }
315
+ continue;
316
+ }
317
+ const ingredientId = ingredientIndexMap.get(usage.ingredientIndex);
318
+ if (ingredientId) {
319
+ edges.push({
320
+ from: ingredientId,
321
+ to: currentStepId,
322
+ metadata: {
323
+ stepNumber: step.number,
324
+ transformationType: "preparation"
325
+ }
326
+ });
327
+ const ingredientNode = nodeMap.get(ingredientId);
328
+ const stepNode = nodeMap.get(currentStepId);
329
+ if (ingredientNode && stepNode) {
330
+ ingredientNode.outputs.push(currentStepId);
331
+ stepNode.inputs.push(ingredientId);
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ return edges;
339
+ };
340
+ var buildStepToFinalEdges = (stepNodes, finalNode) => {
341
+ if (stepNodes.length === 0) return [];
342
+ const lastStepNode = stepNodes[stepNodes.length - 1];
343
+ lastStepNode.outputs.push(finalNode.id);
344
+ finalNode.inputs.push(lastStepNode.id);
345
+ return [
346
+ {
347
+ from: lastStepNode.id,
348
+ to: finalNode.id,
349
+ metadata: {
350
+ transformationType: "completion"
351
+ }
352
+ }
353
+ ];
354
+ };
355
+
356
+ // src/sankey/generator.ts
357
+ var DEFAULT_OPTIONS = {
358
+ finalNodeName: DEFAULT_FINAL_NODE_NAME,
359
+ normalization: DEFAULT_NORMALIZATION,
360
+ minLinkValue: DEFAULT_MIN_LINK_VALUE
361
+ };
362
+ var buildSankeyData = (dagNodes, dagEdges, sortedNodeIds, options) => {
363
+ const ingredientNodes = dagNodes.filter((node) => node.category === "ingredient").map((node) => ({
364
+ id: node.id,
365
+ name: node.name,
366
+ category: node.category,
367
+ value: node.value,
368
+ label: node.label,
369
+ originalValue: node.originalValue,
370
+ metadata: node.metadata
371
+ }));
372
+ const normalizedValueMap = normalizeIngredientValues(ingredientNodes, options.normalization);
373
+ const normalizedDAGNodes = dagNodes.map((node) => {
374
+ if (node.category === "ingredient") {
375
+ return {
376
+ ...node,
377
+ value: normalizedValueMap.get(node.id) ?? node.value
378
+ };
379
+ }
380
+ return node;
381
+ });
382
+ const finalCalculatedValues = calculateNodeValues(normalizedDAGNodes, sortedNodeIds);
383
+ const sankeyNodes = dagNodes.map((dagNode) => {
384
+ const finalValue = finalCalculatedValues.get(dagNode.id) || 0;
385
+ return {
386
+ id: dagNode.id,
387
+ name: dagNode.name,
388
+ category: dagNode.category,
389
+ value: dagNode.category === "ingredient" ? normalizedValueMap.get(dagNode.id) ?? dagNode.value : finalValue,
390
+ label: dagNode.label,
391
+ originalValue: dagNode.originalValue,
392
+ metadata: dagNode.metadata
393
+ };
394
+ });
395
+ const sankeyLinks = dagEdges.map((edge) => {
396
+ const sourceValue = finalCalculatedValues.get(edge.from) || 0;
397
+ const sourceNode = dagNodes.find((n) => n.id === edge.from);
398
+ let linkValue = sourceValue;
399
+ if (sourceNode && sourceNode.outputs.length > 1) {
400
+ linkValue = sourceValue / sourceNode.outputs.length;
401
+ }
402
+ return {
403
+ source: edge.from,
404
+ target: edge.to,
405
+ value: Math.max(options.minLinkValue, linkValue),
406
+ originalValue: sourceValue,
407
+ metadata: edge.metadata
408
+ };
409
+ });
410
+ return {
411
+ nodes: sankeyNodes,
412
+ links: sankeyLinks
413
+ };
414
+ };
415
+ var generateSankeyData = (recipe, options) => {
416
+ const opts = { ...DEFAULT_OPTIONS, ...options };
417
+ try {
418
+ const { nodes: ingredientNodes, indexMap: ingredientIndexMap } = createIngredientNodes(recipe);
419
+ const stepNodes = createStepNodes(recipe);
420
+ const finalNode = createFinalNode(opts.finalNodeName);
421
+ const allNodes = [...ingredientNodes, ...stepNodes, finalNode];
422
+ const ingredientToStepEdges = buildIngredientToStepEdges(recipe, ingredientIndexMap, allNodes);
423
+ const stepToFinalEdges = buildStepToFinalEdges(stepNodes, finalNode);
424
+ const allEdges = [...ingredientToStepEdges, ...stepToFinalEdges];
425
+ const sortedNodeIds = buildDAGAndSort(allNodes, allEdges);
426
+ return buildSankeyData(allNodes, allEdges, sortedNodeIds, opts);
427
+ } catch (error) {
428
+ console.error("Error generating sankey data from cooklang:", error);
429
+ return null;
430
+ }
431
+ };
432
+ var optimizeSankeyData = (data) => {
433
+ const existingNodeIds = new Set(data.nodes.map((node) => node.id));
434
+ const validLinks = data.links.filter((link) => {
435
+ const sourceExists = existingNodeIds.has(link.source);
436
+ const targetExists = existingNodeIds.has(link.target);
437
+ if (!sourceExists) {
438
+ console.error(`Missing source node: ${link.source}`);
439
+ }
440
+ if (!targetExists) {
441
+ console.error(`Missing target node: ${link.target}`);
442
+ }
443
+ return sourceExists && targetExists;
444
+ });
445
+ const connectedNodeIds = /* @__PURE__ */ new Set();
446
+ validLinks.forEach((link) => {
447
+ connectedNodeIds.add(link.source);
448
+ connectedNodeIds.add(link.target);
449
+ });
450
+ const filteredNodes = data.nodes.filter(
451
+ (node) => connectedNodeIds.has(node.id) || node.category === "final"
452
+ );
453
+ let normalizedLinks = validLinks;
454
+ if (validLinks.length > 0) {
455
+ const minValue = Math.min(...validLinks.map((link) => link.value));
456
+ if (minValue > 0) {
457
+ normalizedLinks = validLinks.map((link) => ({
458
+ ...link,
459
+ value: Math.max(1, link.value / minValue)
460
+ }));
461
+ }
462
+ }
463
+ return {
464
+ nodes: filteredNodes,
465
+ links: normalizedLinks
466
+ };
467
+ };
468
+
469
+ Object.defineProperty(exports, "CooklangParser", {
470
+ enumerable: true,
471
+ get: function () { return cooklang.CooklangParser; }
472
+ });
473
+ Object.defineProperty(exports, "CooklangRecipe", {
474
+ enumerable: true,
475
+ get: function () { return cooklang.CooklangRecipe; }
476
+ });
477
+ Object.defineProperty(exports, "getNumericValue", {
478
+ enumerable: true,
479
+ get: function () { return cooklang.getNumericValue; }
480
+ });
481
+ Object.defineProperty(exports, "quantity_display", {
482
+ enumerable: true,
483
+ get: function () { return cooklang.quantity_display; }
484
+ });
485
+ exports.extractMetadata = extractMetadata;
486
+ exports.formatQuantityAmount = formatQuantityAmount;
487
+ exports.formatValue = formatValue;
488
+ exports.generateSankeyData = generateSankeyData;
489
+ exports.generateStepText = generateStepText;
490
+ exports.optimizeSankeyData = optimizeSankeyData;
491
+ //# sourceMappingURL=index.cjs.map
492
+ //# sourceMappingURL=index.cjs.map