@4kk11/cooklang-sankey 0.1.2 → 0.1.4

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/dist/index.cjs CHANGED
@@ -141,20 +141,63 @@ var calculateNodeValues = (nodes, sortedNodeIds) => {
141
141
  }
142
142
  return valueMap;
143
143
  };
144
+ var formatNumber = (num) => {
145
+ if (num.type === "regular") {
146
+ return num.value.toString();
147
+ } else if (num.type === "fraction") {
148
+ const { whole, num: numerator, den } = num.value;
149
+ if (whole === 0 && numerator === 0) {
150
+ return "0";
151
+ }
152
+ if (numerator === 0) {
153
+ return whole.toString();
154
+ }
155
+ if (whole === 0) {
156
+ return `${numerator}/${den}`;
157
+ }
158
+ return `${whole} ${numerator}/${den}`;
159
+ }
160
+ return "";
161
+ };
162
+ var formatValue = (value) => {
163
+ if (!value) return "";
164
+ if (value.type === "number") {
165
+ const numValue = value.value;
166
+ return formatNumber(numValue);
167
+ } else if (value.type === "range") {
168
+ const rangeValue = value.value;
169
+ const startStr = formatNumber(rangeValue.start);
170
+ const endStr = formatNumber(rangeValue.end);
171
+ if (startStr && endStr) {
172
+ return `${startStr}-${endStr}`;
173
+ }
174
+ return "";
175
+ } else if (value.type === "text") {
176
+ return value.value;
177
+ }
178
+ return "";
179
+ };
180
+ var formatQuantityAmount = (quantity) => {
181
+ if (!quantity) {
182
+ return { quantity: "", unit: "" };
183
+ }
184
+ return {
185
+ quantity: formatValue(quantity.value),
186
+ unit: quantity.unit || ""
187
+ };
188
+ };
144
189
  var generateStepText = (step, recipe) => {
145
190
  return step.items.map((item) => {
146
191
  if (item.type === "text") {
147
192
  return item.value;
148
193
  } else if (item.type === "ingredient") {
149
- const ingredient = recipe.ingredients[item.index];
150
- if (!ingredient) return "";
151
- const name = cooklang.ingredient_display_name(ingredient);
152
- const quantityText = ingredient.quantity ? `(${cooklang.quantity_display(ingredient.quantity)})` : "";
153
- return `${name}${quantityText}`;
194
+ const originalIngredient = recipe.ingredients[item.index];
195
+ if (!originalIngredient) return "";
196
+ const formatted = formatQuantityAmount(originalIngredient.quantity);
197
+ const quantityText = formatted.quantity && formatted.unit ? `(${formatted.quantity}${formatted.unit})` : formatted.quantity ? `(${formatted.quantity})` : "";
198
+ return `${originalIngredient.name}${quantityText}`;
154
199
  } else if (item.type === "cookware") {
155
- const cookware = recipe.cookware[item.index];
156
- if (!cookware) return "";
157
- return cooklang.cookware_display_name(cookware);
200
+ return recipe.cookware[item.index]?.name || "";
158
201
  } else if (item.type === "timer") {
159
202
  const timer = recipe.timers[item.index];
160
203
  if (!timer) return "";
@@ -440,40 +483,6 @@ var optimizeSankeyData = (data) => {
440
483
  links: normalizedLinks
441
484
  };
442
485
  };
443
- function parseRecipeForDisplay(recipe) {
444
- const flatIngredients = cooklang.getFlatIngredients(recipe);
445
- const ingredients = flatIngredients.map((ing) => ({
446
- name: ing.name,
447
- quantity: ing.quantity !== null ? String(ing.quantity) : "",
448
- unit: ing.unit || ""
449
- }));
450
- const flatCookware = cooklang.getFlatCookware(recipe);
451
- const cookware = flatCookware.map((cw) => ({
452
- name: cw.name,
453
- note: cw.note || ""
454
- }));
455
- const flatTimers = cooklang.getFlatTimers(recipe);
456
- const timers = flatTimers.map((timer) => ({
457
- name: timer.name || "",
458
- quantity: timer.quantity !== null ? String(timer.quantity) : "",
459
- unit: timer.unit || "minutes"
460
- }));
461
- const steps = [];
462
- for (const section of recipe.sections) {
463
- for (const content of section.content) {
464
- if (content.type === "step") {
465
- const stepText = generateStepText(content.value, recipe);
466
- steps.push({ text: stepText });
467
- }
468
- }
469
- }
470
- return {
471
- ingredients,
472
- cookware,
473
- timers,
474
- steps
475
- };
476
- }
477
486
 
478
487
  Object.defineProperty(exports, "CooklangParser", {
479
488
  enumerable: true,
@@ -483,34 +492,19 @@ Object.defineProperty(exports, "CooklangRecipe", {
483
492
  enumerable: true,
484
493
  get: function () { return cooklang.CooklangRecipe; }
485
494
  });
486
- Object.defineProperty(exports, "cookware_display_name", {
487
- enumerable: true,
488
- get: function () { return cooklang.cookware_display_name; }
489
- });
490
- Object.defineProperty(exports, "getFlatCookware", {
491
- enumerable: true,
492
- get: function () { return cooklang.getFlatCookware; }
493
- });
494
- Object.defineProperty(exports, "getFlatIngredients", {
495
- enumerable: true,
496
- get: function () { return cooklang.getFlatIngredients; }
497
- });
498
- Object.defineProperty(exports, "getFlatTimers", {
499
- enumerable: true,
500
- get: function () { return cooklang.getFlatTimers; }
501
- });
502
- Object.defineProperty(exports, "ingredient_display_name", {
495
+ Object.defineProperty(exports, "getNumericValue", {
503
496
  enumerable: true,
504
- get: function () { return cooklang.ingredient_display_name; }
497
+ get: function () { return cooklang.getNumericValue; }
505
498
  });
506
499
  Object.defineProperty(exports, "quantity_display", {
507
500
  enumerable: true,
508
501
  get: function () { return cooklang.quantity_display; }
509
502
  });
510
503
  exports.extractMetadata = extractMetadata;
504
+ exports.formatQuantityAmount = formatQuantityAmount;
505
+ exports.formatValue = formatValue;
511
506
  exports.generateSankeyData = generateSankeyData;
512
507
  exports.generateStepText = generateStepText;
513
508
  exports.optimizeSankeyData = optimizeSankeyData;
514
- exports.parseRecipeForDisplay = parseRecipeForDisplay;
515
509
  //# sourceMappingURL=index.cjs.map
516
510
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/parser.ts","../src/constants.ts","../src/sankey/normalizer.ts","../src/sankey/dag.ts","../src/formatter.ts","../src/sankey/node-builders.ts","../src/sankey/edge-builders.ts","../src/sankey/generator.ts","../src/display.ts"],"names":["toposort","ingredient_display_name","quantity_display","cookware_display_name","grouped_quantity_is_empty","grouped_quantity_display","getNumericValue","getFlatIngredients","getFlatCookware","getFlatTimers"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Cooklang Parser wrapper using official @cooklang/cooklang package.\n *\n * @remarks\n * This module provides a thin wrapper around the official Cooklang parser,\n * re-exporting types and adding metadata extraction utilities.\n *\n * @packageDocumentation\n */\n\nimport {\n CooklangParser,\n CooklangRecipe,\n type Ingredient,\n type Quantity,\n type Section,\n type Step,\n type Content,\n type Item,\n type Value,\n} from \"@cooklang/cooklang\";\n\n// Re-export types for convenience\nexport type {\n CooklangParser,\n CooklangRecipe,\n Ingredient,\n Quantity,\n Section,\n Step,\n Content,\n Item,\n Value,\n};\n\n/**\n * Metadata extracted from a parsed Cooklang recipe.\n *\n * @remarks\n * Contains standard recipe metadata fields plus any custom metadata\n * defined in the recipe using the `>> key: value` syntax.\n */\nexport interface RecipeMetadata {\n /** Recipe title from `>> title:` */\n title?: string;\n /** Recipe description from `>> description:` */\n description?: string;\n /** Array of tags from `>> tags:` */\n tags?: string[];\n /** Formatted cooking time string (e.g., \"prep: 10min, cook: 30min\") */\n cookingTime?: string;\n /** Servings range or value (e.g., \"4\" or \"4-6\") */\n servings?: string;\n /** Additional custom metadata fields */\n [key: string]: unknown;\n}\n\n/**\n * Extracts metadata from a parsed Cooklang recipe.\n *\n * @param recipe - The parsed CooklangRecipe object\n * @returns An object containing extracted metadata fields\n *\n * @example\n * ```ts\n * import { CooklangParser } from \"@cooklang/cooklang\";\n * import { extractMetadata } from \"./parser\";\n *\n * const parser = new CooklangParser();\n * const [recipe] = parser.parse(`\n * >> title: Pasta Carbonara\n * >> servings: 4\n * @pasta{400g} を茹でる\n * `);\n *\n * const metadata = extractMetadata(recipe);\n * // { title: \"Pasta Carbonara\", servings: \"4\" }\n * ```\n */\nexport function extractMetadata(recipe: CooklangRecipe): RecipeMetadata {\n const result: RecipeMetadata = {};\n\n if (recipe.title) {\n result.title = recipe.title;\n }\n\n if (recipe.description) {\n result.description = recipe.description;\n }\n\n if (recipe.tags && recipe.tags.size > 0) {\n result.tags = Array.from(recipe.tags);\n }\n\n if (recipe.time) {\n // Format time object to string\n const timeValue = recipe.time;\n if (typeof timeValue === \"object\" && timeValue !== null) {\n const parts: string[] = [];\n if (\"prep_time\" in timeValue && timeValue.prep_time) {\n parts.push(`prep: ${timeValue.prep_time}`);\n }\n if (\"cook_time\" in timeValue && timeValue.cook_time) {\n parts.push(`cook: ${timeValue.cook_time}`);\n }\n if (\"total_time\" in timeValue && timeValue.total_time) {\n parts.push(`total: ${timeValue.total_time}`);\n }\n if (parts.length > 0) {\n result.cookingTime = parts.join(\", \");\n }\n }\n }\n\n if (recipe.servings) {\n const servings = recipe.servings;\n if (Array.isArray(servings)) {\n result.servings = servings.join(\"-\");\n } else if (typeof servings === \"object\" && servings !== null) {\n result.servings = String(servings);\n }\n }\n\n // Add custom metadata\n if (recipe.custom_metadata && recipe.custom_metadata.size > 0) {\n for (const [key, value] of recipe.custom_metadata) {\n if (!(key in result)) {\n result[key] = value;\n }\n }\n }\n\n return result;\n}\n","/**\n * Constants for Sankey diagram generation\n */\n\n/**\n * Normalization constants for value scaling\n */\nexport const NORMALIZATION = {\n /** Minimum normalized value for logarithmic scale */\n LOG_MIN: 0.1,\n /** Maximum normalized value for logarithmic scale */\n LOG_MAX: 0.3,\n /** Minimum normalized value for linear scale */\n LINEAR_MIN: 0.1,\n /** Range for linear normalization */\n LINEAR_RANGE: 0.2,\n /** Default value when normalization range is zero */\n DEFAULT_VALUE: 1,\n} as const;\n\n/**\n * Default options for Sankey generation\n */\nexport const DEFAULT_FINAL_NODE_NAME = \"完成品\";\nexport const DEFAULT_MIN_LINK_VALUE = 0.1;\nexport const DEFAULT_NORMALIZATION = \"logarithmic\" as const;\n\n/**\n * Node identifiers\n */\nexport const FINAL_NODE_ID = \"final_dish\";\nexport const DEFAULT_SECTION_NAME = \"main\";\n","/**\n * Value normalization utilities for Sankey diagram nodes.\n *\n * @remarks\n * Provides normalization functions to scale ingredient values for\n * balanced Sankey diagram visualization. Supports logarithmic, linear,\n * and no normalization modes.\n *\n * @packageDocumentation\n */\n\nimport { NORMALIZATION } from \"../constants\";\nimport type { SankeyNode } from \"../types/sankey\";\n\n/**\n * Normalizes ingredient node values using the specified method.\n *\n * @remarks\n * Returns a Map for O(1) lookup of normalized values by node ID.\n * - **logarithmic**: Compresses large value ranges using log10 scale\n * - **linear**: Scales values linearly within a fixed range\n * - **none**: Returns original values unchanged\n *\n * @param ingredientNodes - Array of ingredient nodes to normalize\n * @param normalization - The normalization method to apply\n * @returns A Map of node ID to normalized value\n *\n * @example\n * ```ts\n * const nodes: SankeyNode[] = [\n * { id: \"0\", name: \"flour\", value: 500, ... },\n * { id: \"1\", name: \"salt\", value: 5, ... }\n * ];\n *\n * const normalized = normalizeIngredientValues(nodes, \"logarithmic\");\n * // Map { \"0\" => 0.3, \"1\" => 0.1 }\n * ```\n */\nexport const normalizeIngredientValues = (\n ingredientNodes: SankeyNode[],\n normalization: \"logarithmic\" | \"linear\" | \"none\",\n): Map<string, number> => {\n const result = new Map<string, number>();\n\n if (normalization === \"none\") {\n ingredientNodes.forEach((node) => {\n result.set(node.id, node.value || NORMALIZATION.DEFAULT_VALUE);\n });\n return result;\n }\n\n const nodeValues = ingredientNodes.map((n) => n.value || NORMALIZATION.DEFAULT_VALUE);\n const minValue = Math.min(...nodeValues);\n const maxValue = Math.max(...nodeValues);\n const valueRange = maxValue - minValue;\n\n const normalizeValue = (value: number): number => {\n if (valueRange === 0) return NORMALIZATION.DEFAULT_VALUE;\n\n if (normalization === \"logarithmic\") {\n const normalizedLinear = (value - minValue) / valueRange;\n const logScale = Math.log10(1 + normalizedLinear * 9);\n return Math.min(Math.max(NORMALIZATION.LOG_MIN, logScale), NORMALIZATION.LOG_MAX);\n } else {\n return (\n NORMALIZATION.LINEAR_MIN + ((value - minValue) / valueRange) * NORMALIZATION.LINEAR_RANGE\n );\n }\n };\n\n ingredientNodes.forEach((node) => {\n result.set(node.id, node.value ? normalizeValue(node.value) : NORMALIZATION.DEFAULT_VALUE);\n });\n\n return result;\n};\n","/**\n * DAG (Directed Acyclic Graph) operations for Sankey diagram generation.\n *\n * @remarks\n * Provides topological sorting and value propagation algorithms\n * for computing flow values through the recipe graph.\n *\n * @packageDocumentation\n */\n\nimport toposort from \"toposort\";\nimport type { DAGNode, DAGEdge } from \"../types/sankey\";\n\n/**\n * Builds a DAG from nodes and edges, returning topologically sorted node IDs.\n *\n * @remarks\n * Uses Kahn's algorithm via the `toposort` library. If a cycle is detected,\n * falls back to returning node IDs in their original order.\n *\n * @param nodes - Array of DAG nodes\n * @param edges - Array of DAG edges defining dependencies\n * @returns Array of node IDs in topological order (dependencies before dependents)\n *\n * @example\n * ```ts\n * const nodes = [ingredientNode, stepNode, finalNode];\n * const edges = [\n * { from: ingredientNode.id, to: stepNode.id },\n * { from: stepNode.id, to: finalNode.id }\n * ];\n *\n * const sorted = buildDAGAndSort(nodes, edges);\n * // [ingredientNode.id, stepNode.id, finalNode.id]\n * ```\n */\nexport const buildDAGAndSort = (nodes: DAGNode[], edges: DAGEdge[]): string[] => {\n const edgePairs: Array<[string, string]> = edges.map((edge) => [edge.from, edge.to]);\n\n try {\n return toposort(edgePairs);\n } catch (error) {\n console.error(\"Cycle detected in DAG:\", error);\n return nodes.map((node) => node.id);\n }\n};\n\n/**\n * Calculates node values by propagating flow through the DAG.\n *\n * @remarks\n * Ingredient nodes retain their original values. Process and final nodes\n * accumulate values from their inputs. When a node has multiple outputs,\n * its value is split equally among them.\n *\n * @param nodes - Array of DAG nodes with input/output connections\n * @param sortedNodeIds - Node IDs in topological order\n * @returns A Map of node ID to calculated flow value\n *\n * @example\n * ```ts\n * // Given: flour(500) -> step1 -> final\n * // salt(5) -> step1\n * const values = calculateNodeValues(nodes, sortedIds);\n * // Map { \"flour\" => 500, \"salt\" => 5, \"step1\" => 505, \"final\" => 505 }\n * ```\n */\nexport const calculateNodeValues = (\n nodes: DAGNode[],\n sortedNodeIds: string[],\n): Map<string, number> => {\n const nodeMap = new Map<string, DAGNode>();\n const valueMap = new Map<string, number>();\n\n nodes.forEach((node) => {\n nodeMap.set(node.id, node);\n if (node.category === \"ingredient\") {\n valueMap.set(node.id, node.value);\n } else {\n valueMap.set(node.id, 0);\n }\n });\n\n for (const nodeId of sortedNodeIds) {\n const node = nodeMap.get(nodeId);\n if (!node) continue;\n\n if (node.category === \"ingredient\") {\n continue;\n }\n\n let totalInputValue = 0;\n for (const inputId of node.inputs) {\n let inputValue = valueMap.get(inputId) || 0;\n const inputNode = nodeMap.get(inputId);\n if (inputNode && inputNode.outputs.length > 1) {\n inputValue /= inputNode.outputs.length;\n }\n totalInputValue += inputValue;\n }\n\n valueMap.set(nodeId, totalInputValue);\n }\n\n return valueMap;\n};\n","/**\n * Step text generation utilities using official @cooklang/cooklang package.\n *\n * @remarks\n * Provides utilities for converting Cooklang step items into human-readable\n * string representations using official display functions.\n *\n * @packageDocumentation\n */\n\nimport {\n ingredient_display_name,\n cookware_display_name,\n quantity_display,\n type CooklangRecipe,\n type Step,\n} from \"@cooklang/cooklang\";\n\n/**\n * Generates complete text from step items by resolving references.\n *\n * @remarks\n * Converts a Step's items array into a readable string by:\n * - Keeping text items as-is\n * - Resolving ingredient references to \"name(quantity)\" format using official display functions\n * - Resolving cookware references to their display names\n * - Resolving timer references to their display values\n *\n * @param step - The Step object containing items to format\n * @param recipe - The parent CooklangRecipe for resolving references\n * @returns A concatenated string of all step items\n *\n * @example\n * ```ts\n * // For a step with text \"Cook \" + ingredient(pasta, 400g) + \" until done\"\n * generateStepText(step, recipe);\n * // \"Cook pasta(400 g) until done\"\n * ```\n */\nexport const generateStepText = (step: Step, recipe: CooklangRecipe): string => {\n return step.items\n .map((item) => {\n if (item.type === \"text\") {\n return item.value;\n } else if (item.type === \"ingredient\") {\n const ingredient = recipe.ingredients[item.index];\n if (!ingredient) return \"\";\n\n const name = ingredient_display_name(ingredient);\n const quantityText = ingredient.quantity\n ? `(${quantity_display(ingredient.quantity)})`\n : \"\";\n\n return `${name}${quantityText}`;\n } else if (item.type === \"cookware\") {\n const cookware = recipe.cookware[item.index];\n if (!cookware) return \"\";\n return cookware_display_name(cookware);\n } else if (item.type === \"timer\") {\n const timer = recipe.timers[item.index];\n if (!timer) return \"\";\n return timer.name || (timer.quantity ? quantity_display(timer.quantity) : \"\");\n }\n return \"\";\n })\n .join(\"\");\n};\n\n// Re-export useful functions from official package\nexport { ingredient_display_name, cookware_display_name, quantity_display };\n","/**\n * Node builders for Sankey diagram generation.\n *\n * @remarks\n * Factory functions for creating DAG nodes from Cooklang recipe elements.\n * Handles ingredients, process steps, and the final dish node.\n *\n * @packageDocumentation\n */\n\nimport {\n type CooklangRecipe,\n getNumericValue,\n grouped_quantity_display,\n grouped_quantity_is_empty,\n type Quantity,\n} from \"@cooklang/cooklang\";\nimport { generateStepText } from \"../formatter\";\nimport { DEFAULT_SECTION_NAME, FINAL_NODE_ID } from \"../constants\";\nimport type { DAGNode, NodeCategory, ExtractedValue } from \"../types/sankey\";\n\n/**\n * Extracts a numeric or text value from a Cooklang GroupedQuantity.\n *\n * @remarks\n * Attempts to extract a numeric value from fixed, scalable, or unknown\n * quantity types. Falls back to text representation if no numeric value\n * is available.\n *\n * @param groupedQuantity - The GroupedQuantity object from Cooklang parser\n * @returns An ExtractedValue with either numeric value or text label\n *\n * @example\n * ```ts\n * // For \"200g\" quantity\n * calculateIngredientQuantity(gq);\n * // { type: \"number\", value: 200, label: \"200g\" }\n *\n * // For \"some\" quantity\n * calculateIngredientQuantity(gq);\n * // { type: \"text\", label: \"some\" }\n * ```\n */\nexport const calculateIngredientQuantity = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n groupedQuantity: any,\n): ExtractedValue => {\n if (grouped_quantity_is_empty(groupedQuantity)) {\n return { type: \"text\", label: \"unknown\" };\n }\n\n const displayText = grouped_quantity_display(groupedQuantity);\n\n const gq = groupedQuantity as {\n fixed?: Quantity | null;\n scalable?: Quantity | null;\n fixed_unknown?: Quantity | null;\n scalable_unknown?: Quantity | null;\n };\n\n const quantityToUse = gq.fixed || gq.scalable || gq.fixed_unknown || gq.scalable_unknown;\n\n if (quantityToUse) {\n const numericValue = getNumericValue(quantityToUse.value);\n if (numericValue !== null) {\n return { type: \"number\", value: numericValue, label: displayText || `${numericValue}` };\n }\n }\n\n return { type: \"text\", label: displayText || \"unknown\" };\n};\n\n/**\n * Creates DAG nodes for all ingredients in a recipe.\n *\n * @remarks\n * Processes the recipe's grouped ingredients to create nodes with:\n * - Unique IDs based on ingredient index\n * - Extracted quantity values for flow calculation\n * - Display labels with quantity information\n *\n * @param recipe - The parsed CooklangRecipe\n * @returns An object containing:\n * - `nodes`: Array of ingredient DAGNodes\n * - `indexMap`: Map from original ingredient index to node ID\n *\n * @example\n * ```ts\n * const { nodes, indexMap } = createIngredientNodes(recipe);\n * // nodes: [{ id: \"0\", name: \"flour\", value: 500, ... }, ...]\n * // indexMap: Map { 0 => \"0\", 1 => \"1\", ... }\n * ```\n */\nexport const createIngredientNodes = (\n recipe: CooklangRecipe,\n): { nodes: DAGNode[]; indexMap: Map<number, string> } => {\n const nodes: DAGNode[] = [];\n const indexMap = new Map<number, string>();\n\n recipe.groupedIngredients.forEach(([ingredient, groupedQuantity], index) => {\n const extracted = calculateIngredientQuantity(groupedQuantity);\n const value = extracted.type === \"number\" ? extracted.value : 1;\n const label = extracted.label;\n const ingredientId = index.toString();\n\n indexMap.set(index, ingredientId);\n\n nodes.push({\n id: ingredientId,\n name: ingredient.name,\n category: \"ingredient\" as NodeCategory,\n value,\n originalValue: value,\n label,\n inputs: [],\n outputs: [],\n metadata: {\n originalIndex: index,\n },\n });\n });\n\n return { nodes, indexMap };\n};\n\n/**\n * Creates DAG nodes for all cooking steps in a recipe.\n *\n * @remarks\n * Iterates through all sections and steps, creating process nodes with:\n * - Unique IDs based on section name and step number\n * - Generated text from step items (ingredients, cookware, timers)\n * - Initial value of 0 (calculated later via flow propagation)\n *\n * @param recipe - The parsed CooklangRecipe\n * @returns Array of process DAGNodes representing cooking steps\n *\n * @example\n * ```ts\n * const stepNodes = createStepNodes(recipe);\n * // [{ id: \"step_main_1\", name: \"Boil pasta(400g)\", category: \"process\", ... }]\n * ```\n */\nexport const createStepNodes = (recipe: CooklangRecipe): DAGNode[] => {\n const nodes: DAGNode[] = [];\n\n for (const section of recipe.sections) {\n for (const content of section.content) {\n if (content.type === \"step\") {\n const step = content.value;\n const stepText = generateStepText(step, recipe);\n const stepId = `step_${section.name || DEFAULT_SECTION_NAME}_${step.number}`;\n\n nodes.push({\n id: stepId,\n name: stepText || `手順 ${step.number}`,\n category: \"process\" as NodeCategory,\n value: 0,\n label: `${step.number}`,\n inputs: [],\n outputs: [],\n metadata: {\n stepNumber: step.number,\n sectionName: section.name || undefined,\n },\n });\n }\n }\n }\n\n return nodes;\n};\n\n/**\n * Creates the final node representing the completed dish.\n *\n * @remarks\n * The final node is the sink of the Sankey diagram where all\n * process flows converge. Its value is calculated by summing\n * all incoming flows.\n *\n * @param finalNodeName - Display name for the final dish node\n * @returns A DAGNode with category \"final\"\n *\n * @example\n * ```ts\n * const finalNode = createFinalNode(\"Carbonara\");\n * // { id: \"final_dish\", name: \"Carbonara\", category: \"final\", ... }\n * ```\n */\nexport const createFinalNode = (finalNodeName: string): DAGNode => ({\n id: FINAL_NODE_ID,\n name: finalNodeName,\n category: \"final\" as NodeCategory,\n value: 0,\n label: \"\",\n inputs: [],\n outputs: [],\n});\n","/**\n * Edge builders for Sankey diagram generation.\n *\n * @remarks\n * Factory functions for creating DAG edges that connect nodes.\n * Handles ingredient-to-step and step-to-final connections,\n * including step reference resolution.\n *\n * @packageDocumentation\n */\n\nimport { type CooklangRecipe, type Step } from \"@cooklang/cooklang\";\nimport { DEFAULT_SECTION_NAME } from \"../constants\";\nimport type { DAGNode, DAGEdge, TransformationType } from \"../types/sankey\";\n\n/**\n * Generates a unique step ID from section name and step number.\n *\n * @param sectionName - The recipe section name (null uses default)\n * @param stepNumber - The 1-indexed step number\n * @returns A unique step ID string (e.g., \"step_main_1\")\n *\n * @example\n * ```ts\n * getStepId(\"main\", 1); // \"step_main_1\"\n * getStepId(null, 2); // \"step_main_2\"\n * getStepId(\"sauce\", 1); // \"step_sauce_1\"\n * ```\n */\nexport const getStepId = (sectionName: string | null | undefined, stepNumber: number): string => {\n return `step_${sectionName || DEFAULT_SECTION_NAME}_${stepNumber}`;\n};\n\n/**\n * Extracts ingredient usage information from a recipe step.\n *\n * @param step - The Step object to analyze\n * @returns Array of objects containing ingredient index and step number\n *\n * @example\n * ```ts\n * // For a step using @pasta and @salt\n * extractIngredientUsageFromStep(step);\n * // [{ ingredientIndex: 0, stepNumber: 1 }, { ingredientIndex: 1, stepNumber: 1 }]\n * ```\n */\nexport const extractIngredientUsageFromStep = (\n step: Step,\n): Array<{ ingredientIndex: number; stepNumber: number }> => {\n const usages: Array<{ ingredientIndex: number; stepNumber: number }> = [];\n\n for (const item of step.items) {\n if (item.type === \"ingredient\") {\n usages.push({\n ingredientIndex: item.index,\n stepNumber: step.number,\n });\n }\n }\n\n return usages;\n};\n\n/**\n * Builds edges connecting ingredients to their consuming steps.\n *\n * @remarks\n * Analyzes the recipe to create edges from:\n * - Ingredient nodes to step nodes (for direct ingredient usage)\n * - Step nodes to other step nodes (for step references like \"the pasta from step 1\")\n *\n * Also updates the input/output arrays on the nodes for DAG traversal.\n *\n * @param recipe - The parsed CooklangRecipe\n * @param ingredientIndexMap - Map from ingredient index to node ID\n * @param nodes - Array of all DAG nodes (modified in place)\n * @returns Array of DAGEdges connecting ingredients/steps to steps\n *\n * @example\n * ```ts\n * const edges = buildIngredientToStepEdges(recipe, indexMap, nodes);\n * // [{ from: \"0\", to: \"step_main_1\", metadata: { ... } }, ...]\n * ```\n */\nexport const buildIngredientToStepEdges = (\n recipe: CooklangRecipe,\n ingredientIndexMap: Map<number, string>,\n nodes: DAGNode[],\n): DAGEdge[] => {\n const edges: DAGEdge[] = [];\n const nodeMap = new Map<string, DAGNode>();\n\n nodes.forEach((node) => nodeMap.set(node.id, node));\n\n for (const section of recipe.sections) {\n for (const content of section.content) {\n if (content.type === \"step\") {\n const step = content.value;\n const currentStepId = getStepId(section.name, step.number);\n const usedIngredients = extractIngredientUsageFromStep(step);\n\n for (const usage of usedIngredients) {\n const ingredient = recipe.ingredients[usage.ingredientIndex];\n\n // Check for step reference\n const relation = ingredient.relation;\n if (\n relation &&\n relation.relation?.type === \"reference\" &&\n relation.reference_target === \"step\"\n ) {\n const referenceTo = relation.relation.references_to;\n const sourceStepId = getStepId(section.name, referenceTo + 1);\n edges.push({\n from: sourceStepId,\n to: currentStepId,\n metadata: {\n stepNumber: step.number,\n transformationType: \"cooking\" as TransformationType,\n },\n });\n\n const sourceNode = nodeMap.get(sourceStepId);\n const targetNode = nodeMap.get(currentStepId);\n if (sourceNode && targetNode) {\n sourceNode.outputs.push(currentStepId);\n targetNode.inputs.push(sourceStepId);\n }\n continue;\n }\n\n const ingredientId = ingredientIndexMap.get(usage.ingredientIndex);\n if (ingredientId) {\n edges.push({\n from: ingredientId,\n to: currentStepId,\n metadata: {\n stepNumber: step.number,\n transformationType: \"preparation\" as TransformationType,\n },\n });\n\n const ingredientNode = nodeMap.get(ingredientId);\n const stepNode = nodeMap.get(currentStepId);\n if (ingredientNode && stepNode) {\n ingredientNode.outputs.push(currentStepId);\n stepNode.inputs.push(ingredientId);\n }\n }\n }\n }\n }\n }\n\n return edges;\n};\n\n/**\n * Builds an edge from the last step to the final dish node.\n *\n * @remarks\n * Connects only the last step to the final node, representing\n * the completion of the recipe. Also updates the input/output\n * arrays on the affected nodes.\n *\n * @param stepNodes - Array of step nodes in order\n * @param finalNode - The final dish node (modified in place)\n * @returns Array containing a single edge to the final node, or empty if no steps\n *\n * @example\n * ```ts\n * const edges = buildStepToFinalEdges(stepNodes, finalNode);\n * // [{ from: \"step_main_3\", to: \"final_dish\", metadata: { transformationType: \"completion\" } }]\n * ```\n */\nexport const buildStepToFinalEdges = (stepNodes: DAGNode[], finalNode: DAGNode): DAGEdge[] => {\n if (stepNodes.length === 0) return [];\n\n const lastStepNode = stepNodes[stepNodes.length - 1];\n\n lastStepNode.outputs.push(finalNode.id);\n finalNode.inputs.push(lastStepNode.id);\n\n return [\n {\n from: lastStepNode.id,\n to: finalNode.id,\n metadata: {\n transformationType: \"completion\" as TransformationType,\n },\n },\n ];\n};\n","/**\n * Sankey diagram data generator from Cooklang recipes.\n *\n * @remarks\n * Main module for transforming parsed Cooklang recipes into\n * Sankey diagram data structures. Orchestrates the DAG building,\n * value calculation, and normalization pipeline.\n *\n * @packageDocumentation\n */\n\nimport type { CooklangRecipe } from \"@cooklang/cooklang\";\nimport {\n DEFAULT_FINAL_NODE_NAME,\n DEFAULT_MIN_LINK_VALUE,\n DEFAULT_NORMALIZATION,\n} from \"../constants\";\nimport type { SankeyNode, SankeyLink, SankeyData, DAGNode, DAGEdge } from \"../types/sankey\";\nimport { normalizeIngredientValues } from \"./normalizer\";\nimport { buildDAGAndSort, calculateNodeValues } from \"./dag\";\nimport { createIngredientNodes, createStepNodes, createFinalNode } from \"./node-builders\";\nimport { buildIngredientToStepEdges, buildStepToFinalEdges } from \"./edge-builders\";\n\n/**\n * Configuration options for Sankey diagram generation.\n *\n * @example\n * ```ts\n * const options: SankeyGeneratorOptions = {\n * finalNodeName: \"Carbonara\",\n * normalization: \"logarithmic\",\n * minLinkValue: 0.1\n * };\n * ```\n */\nexport interface SankeyGeneratorOptions {\n /**\n * Display name for the final dish node.\n * @defaultValue \"完成品\"\n */\n finalNodeName?: string;\n /**\n * Method for normalizing ingredient values.\n * - `logarithmic`: Compress large ranges (recommended)\n * - `linear`: Linear scaling\n * - `none`: Use raw values\n * @defaultValue \"logarithmic\"\n */\n normalization?: \"logarithmic\" | \"linear\" | \"none\";\n /**\n * Minimum value for links to ensure visibility.\n * @defaultValue 0.1\n */\n minLinkValue?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<SankeyGeneratorOptions> = {\n finalNodeName: DEFAULT_FINAL_NODE_NAME,\n normalization: DEFAULT_NORMALIZATION,\n minLinkValue: DEFAULT_MIN_LINK_VALUE,\n};\n\n/**\n * Transforms DAG nodes and edges into final Sankey diagram data.\n *\n * @remarks\n * Internal function that:\n * 1. Normalizes ingredient values using the specified method\n * 2. Calculates flow values for all nodes via DAG propagation\n * 3. Converts DAG structures to Sankey-compatible format\n * 4. Applies minimum value constraints to links\n *\n * @param dagNodes - Array of DAG nodes with dependency info\n * @param dagEdges - Array of DAG edges\n * @param sortedNodeIds - Node IDs in topological order\n * @param options - Generation options with all defaults applied\n * @returns Complete SankeyData structure ready for visualization\n *\n * @internal\n */\nconst buildSankeyData = (\n dagNodes: DAGNode[],\n dagEdges: DAGEdge[],\n sortedNodeIds: string[],\n options: Required<SankeyGeneratorOptions>,\n): SankeyData => {\n const ingredientNodes = dagNodes\n .filter((node) => node.category === \"ingredient\")\n .map((node) => ({\n id: node.id,\n name: node.name,\n category: node.category,\n value: node.value,\n label: node.label,\n originalValue: node.originalValue,\n metadata: node.metadata,\n }));\n\n const normalizedValueMap = normalizeIngredientValues(ingredientNodes, options.normalization);\n\n const normalizedDAGNodes = dagNodes.map((node) => {\n if (node.category === \"ingredient\") {\n return {\n ...node,\n value: normalizedValueMap.get(node.id) ?? node.value,\n };\n }\n return node;\n });\n\n const finalCalculatedValues = calculateNodeValues(normalizedDAGNodes, sortedNodeIds);\n\n const sankeyNodes: SankeyNode[] = dagNodes.map((dagNode) => {\n const finalValue = finalCalculatedValues.get(dagNode.id) || 0;\n return {\n id: dagNode.id,\n name: dagNode.name,\n category: dagNode.category,\n value:\n dagNode.category === \"ingredient\"\n ? (normalizedValueMap.get(dagNode.id) ?? dagNode.value)\n : finalValue,\n label: dagNode.label,\n originalValue: dagNode.originalValue,\n metadata: dagNode.metadata,\n };\n });\n\n const sankeyLinks: SankeyLink[] = dagEdges.map((edge) => {\n const sourceValue = finalCalculatedValues.get(edge.from) || 0;\n const sourceNode = dagNodes.find((n) => n.id === edge.from);\n\n let linkValue = sourceValue;\n if (sourceNode && sourceNode.outputs.length > 1) {\n linkValue = sourceValue / sourceNode.outputs.length;\n }\n\n return {\n source: edge.from,\n target: edge.to,\n value: Math.max(options.minLinkValue, linkValue),\n originalValue: sourceValue,\n metadata: edge.metadata,\n };\n });\n\n return {\n nodes: sankeyNodes,\n links: sankeyLinks,\n };\n};\n\n/**\n * Generates Sankey diagram data from a parsed Cooklang recipe.\n *\n * @remarks\n * This is the main entry point for the library. It transforms a parsed\n * Cooklang recipe into a Sankey diagram data structure by:\n * 1. Creating nodes for ingredients, steps, and the final dish\n * 2. Building edges representing ingredient flow\n * 3. Performing topological sort for correct value propagation\n * 4. Normalizing values for balanced visualization\n *\n * @param recipe - A parsed CooklangRecipe object\n * @param options - Optional configuration for generation\n * @returns SankeyData structure, or null if generation fails\n *\n * @example\n * ```ts\n * import { CooklangParser, generateSankeyData } from 'cooklang-sankey';\n *\n * const parser = new CooklangParser();\n * const [recipe] = parser.parse(`\n * @pasta{400g} を茹でる。\n * @卵{3個}と@チーズ{100g}を混ぜる。\n * `);\n *\n * const data = generateSankeyData(recipe, {\n * finalNodeName: \"Carbonara\",\n * normalization: \"logarithmic\"\n * });\n * ```\n */\nexport const generateSankeyData = (\n recipe: CooklangRecipe,\n options?: SankeyGeneratorOptions,\n): SankeyData | null => {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n try {\n const { nodes: ingredientNodes, indexMap: ingredientIndexMap } = createIngredientNodes(recipe);\n\n const stepNodes = createStepNodes(recipe);\n const finalNode = createFinalNode(opts.finalNodeName);\n\n const allNodes = [...ingredientNodes, ...stepNodes, finalNode];\n\n const ingredientToStepEdges = buildIngredientToStepEdges(recipe, ingredientIndexMap, allNodes);\n const stepToFinalEdges = buildStepToFinalEdges(stepNodes, finalNode);\n const allEdges = [...ingredientToStepEdges, ...stepToFinalEdges];\n\n const sortedNodeIds = buildDAGAndSort(allNodes, allEdges);\n\n return buildSankeyData(allNodes, allEdges, sortedNodeIds, opts);\n } catch (error) {\n console.error(\"Error generating sankey data from cooklang:\", error);\n return null;\n }\n};\n\n/**\n * Validates and optimizes Sankey data for visualization.\n *\n * @remarks\n * Performs three optimization steps:\n * 1. **Remove invalid links**: Filters out links referencing non-existent nodes\n * 2. **Remove orphaned nodes**: Keeps only nodes connected to valid links\n * (final nodes are always preserved)\n * 3. **Normalize link values**: Scales values relative to minimum for\n * stable visualization (ensures all values >= 1)\n *\n * @param data - The SankeyData to optimize\n * @returns A new SankeyData with optimizations applied\n *\n * @example\n * ```ts\n * const rawData = generateSankeyData(recipe);\n * if (rawData) {\n * const optimized = optimizeSankeyData(rawData);\n * // Use optimized data for visualization\n * }\n * ```\n */\nexport const optimizeSankeyData = (data: SankeyData): SankeyData => {\n // Create a Set of existing node IDs for O(1) lookup\n const existingNodeIds = new Set(data.nodes.map((node) => node.id));\n\n // Step 1: Remove invalid links\n // Filter out links where source or target node doesn't exist\n const validLinks = data.links.filter((link) => {\n const sourceExists = existingNodeIds.has(link.source);\n const targetExists = existingNodeIds.has(link.target);\n\n if (!sourceExists) {\n console.error(`Missing source node: ${link.source}`);\n }\n if (!targetExists) {\n console.error(`Missing target node: ${link.target}`);\n }\n\n return sourceExists && targetExists;\n });\n\n // Step 2: Remove orphaned nodes\n // Collect node IDs that are connected to valid links\n const connectedNodeIds = new Set<string>();\n validLinks.forEach((link) => {\n connectedNodeIds.add(link.source);\n connectedNodeIds.add(link.target);\n });\n\n // Keep only nodes connected to links, or final nodes (always preserved)\n const filteredNodes = data.nodes.filter(\n (node) => connectedNodeIds.has(node.id) || node.category === \"final\",\n );\n\n // Step 3: Normalize link values\n // Scale values relative to minimum for stable visualization\n let normalizedLinks = validLinks;\n if (validLinks.length > 0) {\n const minValue = Math.min(...validLinks.map((link) => link.value));\n if (minValue > 0) {\n // Divide by min value and clamp to ensure value >= 1\n normalizedLinks = validLinks.map((link) => ({\n ...link,\n value: Math.max(1, link.value / minValue),\n }));\n }\n }\n\n return {\n nodes: filteredNodes,\n links: normalizedLinks,\n };\n};\n","/**\n * Recipe display utilities for UI rendering.\n *\n * @remarks\n * Provides utilities for converting parsed CooklangRecipe objects into\n * display-ready data structures suitable for UI rendering.\n *\n * Uses the official @cooklang/cooklang package's convenience functions\n * for accurate quantity aggregation and formatting.\n *\n * @packageDocumentation\n */\n\nimport {\n type CooklangRecipe,\n getFlatIngredients,\n getFlatCookware,\n getFlatTimers,\n} from \"@cooklang/cooklang\";\nimport { generateStepText } from \"./formatter\";\nimport type {\n DisplayRecipe,\n DisplayIngredient,\n DisplayCookware,\n DisplayTimer,\n DisplayStep,\n} from \"./types/display\";\n\n/**\n * Parses a CooklangRecipe into a display-ready format.\n *\n * @remarks\n * Uses the official @cooklang/cooklang package's convenience functions\n * (`getFlatIngredients`, `getFlatCookware`, `getFlatTimers`) for proper\n * handling of grouped quantities.\n *\n * @param recipe - The parsed CooklangRecipe object\n * @returns DisplayRecipe with formatted data for UI rendering\n *\n * @example\n * ```ts\n * import { CooklangParser, parseRecipeForDisplay } from '@4kk11/cooklang-sankey';\n *\n * const parser = new CooklangParser();\n * const [recipe] = parser.parse(recipeText);\n * const displayData = parseRecipeForDisplay(recipe);\n *\n * // Render ingredients\n * displayData.ingredients.forEach(ing => {\n * console.log(`${ing.name}: ${ing.quantity}${ing.unit}`);\n * });\n *\n * // Render steps\n * displayData.steps.forEach((step, i) => {\n * console.log(`${i + 1}. ${step.text}`);\n * });\n * ```\n */\nexport function parseRecipeForDisplay(recipe: CooklangRecipe): DisplayRecipe {\n // Use getFlatIngredients which properly handles grouped quantities\n const flatIngredients = getFlatIngredients(recipe);\n const ingredients: DisplayIngredient[] = flatIngredients.map((ing) => ({\n name: ing.name,\n quantity: ing.quantity !== null ? String(ing.quantity) : \"\",\n unit: ing.unit || \"\",\n }));\n\n // Use getFlatCookware for cookware\n const flatCookware = getFlatCookware(recipe);\n const cookware: DisplayCookware[] = flatCookware.map((cw) => ({\n name: cw.name,\n note: cw.note || \"\",\n }));\n\n // Use getFlatTimers for timers\n const flatTimers = getFlatTimers(recipe);\n const timers: DisplayTimer[] = flatTimers.map((timer) => ({\n name: timer.name || \"\",\n quantity: timer.quantity !== null ? String(timer.quantity) : \"\",\n unit: timer.unit || \"minutes\",\n }));\n\n // Generate step texts\n const steps: DisplayStep[] = [];\n for (const section of recipe.sections) {\n for (const content of section.content) {\n if (content.type === \"step\") {\n const stepText = generateStepText(content.value, recipe);\n steps.push({ text: stepText });\n }\n }\n }\n\n return {\n ingredients,\n cookware,\n timers,\n steps,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/parser.ts","../src/constants.ts","../src/sankey/normalizer.ts","../src/sankey/dag.ts","../src/formatter.ts","../src/sankey/node-builders.ts","../src/sankey/edge-builders.ts","../src/sankey/generator.ts"],"names":["toposort","quantity_display","grouped_quantity_is_empty","grouped_quantity_display","getNumericValue"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Cooklang Parser wrapper using official @cooklang/cooklang package.\n *\n * @remarks\n * This module provides a thin wrapper around the official Cooklang parser,\n * re-exporting types and adding metadata extraction utilities.\n *\n * @packageDocumentation\n */\n\nimport {\n CooklangParser,\n CooklangRecipe,\n type Ingredient,\n type Quantity,\n type Section,\n type Step,\n type Content,\n type Item,\n type Value,\n} from \"@cooklang/cooklang\";\n\n// Re-export types for convenience\nexport type {\n CooklangParser,\n CooklangRecipe,\n Ingredient,\n Quantity,\n Section,\n Step,\n Content,\n Item,\n Value,\n};\n\n/**\n * Metadata extracted from a parsed Cooklang recipe.\n *\n * @remarks\n * Contains standard recipe metadata fields plus any custom metadata\n * defined in the recipe using the `>> key: value` syntax.\n */\nexport interface RecipeMetadata {\n /** Recipe title from `>> title:` */\n title?: string;\n /** Recipe description from `>> description:` */\n description?: string;\n /** Array of tags from `>> tags:` */\n tags?: string[];\n /** Formatted cooking time string (e.g., \"prep: 10min, cook: 30min\") */\n cookingTime?: string;\n /** Servings range or value (e.g., \"4\" or \"4-6\") */\n servings?: string;\n /** Additional custom metadata fields */\n [key: string]: unknown;\n}\n\n/**\n * Extracts metadata from a parsed Cooklang recipe.\n *\n * @param recipe - The parsed CooklangRecipe object\n * @returns An object containing extracted metadata fields\n *\n * @example\n * ```ts\n * import { CooklangParser } from \"@cooklang/cooklang\";\n * import { extractMetadata } from \"./parser\";\n *\n * const parser = new CooklangParser();\n * const [recipe] = parser.parse(`\n * >> title: Pasta Carbonara\n * >> servings: 4\n * @pasta{400g} を茹でる\n * `);\n *\n * const metadata = extractMetadata(recipe);\n * // { title: \"Pasta Carbonara\", servings: \"4\" }\n * ```\n */\nexport function extractMetadata(recipe: CooklangRecipe): RecipeMetadata {\n const result: RecipeMetadata = {};\n\n if (recipe.title) {\n result.title = recipe.title;\n }\n\n if (recipe.description) {\n result.description = recipe.description;\n }\n\n if (recipe.tags && recipe.tags.size > 0) {\n result.tags = Array.from(recipe.tags);\n }\n\n if (recipe.time) {\n // Format time object to string\n const timeValue = recipe.time;\n if (typeof timeValue === \"object\" && timeValue !== null) {\n const parts: string[] = [];\n if (\"prep_time\" in timeValue && timeValue.prep_time) {\n parts.push(`prep: ${timeValue.prep_time}`);\n }\n if (\"cook_time\" in timeValue && timeValue.cook_time) {\n parts.push(`cook: ${timeValue.cook_time}`);\n }\n if (\"total_time\" in timeValue && timeValue.total_time) {\n parts.push(`total: ${timeValue.total_time}`);\n }\n if (parts.length > 0) {\n result.cookingTime = parts.join(\", \");\n }\n }\n }\n\n if (recipe.servings) {\n const servings = recipe.servings;\n if (Array.isArray(servings)) {\n result.servings = servings.join(\"-\");\n } else if (typeof servings === \"object\" && servings !== null) {\n result.servings = String(servings);\n }\n }\n\n // Add custom metadata\n if (recipe.custom_metadata && recipe.custom_metadata.size > 0) {\n for (const [key, value] of recipe.custom_metadata) {\n if (!(key in result)) {\n result[key] = value;\n }\n }\n }\n\n return result;\n}\n","/**\n * Constants for Sankey diagram generation\n */\n\n/**\n * Normalization constants for value scaling\n */\nexport const NORMALIZATION = {\n /** Minimum normalized value for logarithmic scale */\n LOG_MIN: 0.1,\n /** Maximum normalized value for logarithmic scale */\n LOG_MAX: 0.3,\n /** Minimum normalized value for linear scale */\n LINEAR_MIN: 0.1,\n /** Range for linear normalization */\n LINEAR_RANGE: 0.2,\n /** Default value when normalization range is zero */\n DEFAULT_VALUE: 1,\n} as const;\n\n/**\n * Default options for Sankey generation\n */\nexport const DEFAULT_FINAL_NODE_NAME = \"完成品\";\nexport const DEFAULT_MIN_LINK_VALUE = 0.1;\nexport const DEFAULT_NORMALIZATION = \"logarithmic\" as const;\n\n/**\n * Node identifiers\n */\nexport const FINAL_NODE_ID = \"final_dish\";\nexport const DEFAULT_SECTION_NAME = \"main\";\n","/**\n * Value normalization utilities for Sankey diagram nodes.\n *\n * @remarks\n * Provides normalization functions to scale ingredient values for\n * balanced Sankey diagram visualization. Supports logarithmic, linear,\n * and no normalization modes.\n *\n * @packageDocumentation\n */\n\nimport { NORMALIZATION } from \"../constants\";\nimport type { SankeyNode } from \"../types/sankey\";\n\n/**\n * Normalizes ingredient node values using the specified method.\n *\n * @remarks\n * Returns a Map for O(1) lookup of normalized values by node ID.\n * - **logarithmic**: Compresses large value ranges using log10 scale\n * - **linear**: Scales values linearly within a fixed range\n * - **none**: Returns original values unchanged\n *\n * @param ingredientNodes - Array of ingredient nodes to normalize\n * @param normalization - The normalization method to apply\n * @returns A Map of node ID to normalized value\n *\n * @example\n * ```ts\n * const nodes: SankeyNode[] = [\n * { id: \"0\", name: \"flour\", value: 500, ... },\n * { id: \"1\", name: \"salt\", value: 5, ... }\n * ];\n *\n * const normalized = normalizeIngredientValues(nodes, \"logarithmic\");\n * // Map { \"0\" => 0.3, \"1\" => 0.1 }\n * ```\n */\nexport const normalizeIngredientValues = (\n ingredientNodes: SankeyNode[],\n normalization: \"logarithmic\" | \"linear\" | \"none\",\n): Map<string, number> => {\n const result = new Map<string, number>();\n\n if (normalization === \"none\") {\n ingredientNodes.forEach((node) => {\n result.set(node.id, node.value || NORMALIZATION.DEFAULT_VALUE);\n });\n return result;\n }\n\n const nodeValues = ingredientNodes.map((n) => n.value || NORMALIZATION.DEFAULT_VALUE);\n const minValue = Math.min(...nodeValues);\n const maxValue = Math.max(...nodeValues);\n const valueRange = maxValue - minValue;\n\n const normalizeValue = (value: number): number => {\n if (valueRange === 0) return NORMALIZATION.DEFAULT_VALUE;\n\n if (normalization === \"logarithmic\") {\n const normalizedLinear = (value - minValue) / valueRange;\n const logScale = Math.log10(1 + normalizedLinear * 9);\n return Math.min(Math.max(NORMALIZATION.LOG_MIN, logScale), NORMALIZATION.LOG_MAX);\n } else {\n return (\n NORMALIZATION.LINEAR_MIN + ((value - minValue) / valueRange) * NORMALIZATION.LINEAR_RANGE\n );\n }\n };\n\n ingredientNodes.forEach((node) => {\n result.set(node.id, node.value ? normalizeValue(node.value) : NORMALIZATION.DEFAULT_VALUE);\n });\n\n return result;\n};\n","/**\n * DAG (Directed Acyclic Graph) operations for Sankey diagram generation.\n *\n * @remarks\n * Provides topological sorting and value propagation algorithms\n * for computing flow values through the recipe graph.\n *\n * @packageDocumentation\n */\n\nimport toposort from \"toposort\";\nimport type { DAGNode, DAGEdge } from \"../types/sankey\";\n\n/**\n * Builds a DAG from nodes and edges, returning topologically sorted node IDs.\n *\n * @remarks\n * Uses Kahn's algorithm via the `toposort` library. If a cycle is detected,\n * falls back to returning node IDs in their original order.\n *\n * @param nodes - Array of DAG nodes\n * @param edges - Array of DAG edges defining dependencies\n * @returns Array of node IDs in topological order (dependencies before dependents)\n *\n * @example\n * ```ts\n * const nodes = [ingredientNode, stepNode, finalNode];\n * const edges = [\n * { from: ingredientNode.id, to: stepNode.id },\n * { from: stepNode.id, to: finalNode.id }\n * ];\n *\n * const sorted = buildDAGAndSort(nodes, edges);\n * // [ingredientNode.id, stepNode.id, finalNode.id]\n * ```\n */\nexport const buildDAGAndSort = (nodes: DAGNode[], edges: DAGEdge[]): string[] => {\n const edgePairs: Array<[string, string]> = edges.map((edge) => [edge.from, edge.to]);\n\n try {\n return toposort(edgePairs);\n } catch (error) {\n console.error(\"Cycle detected in DAG:\", error);\n return nodes.map((node) => node.id);\n }\n};\n\n/**\n * Calculates node values by propagating flow through the DAG.\n *\n * @remarks\n * Ingredient nodes retain their original values. Process and final nodes\n * accumulate values from their inputs. When a node has multiple outputs,\n * its value is split equally among them.\n *\n * @param nodes - Array of DAG nodes with input/output connections\n * @param sortedNodeIds - Node IDs in topological order\n * @returns A Map of node ID to calculated flow value\n *\n * @example\n * ```ts\n * // Given: flour(500) -> step1 -> final\n * // salt(5) -> step1\n * const values = calculateNodeValues(nodes, sortedIds);\n * // Map { \"flour\" => 500, \"salt\" => 5, \"step1\" => 505, \"final\" => 505 }\n * ```\n */\nexport const calculateNodeValues = (\n nodes: DAGNode[],\n sortedNodeIds: string[],\n): Map<string, number> => {\n const nodeMap = new Map<string, DAGNode>();\n const valueMap = new Map<string, number>();\n\n nodes.forEach((node) => {\n nodeMap.set(node.id, node);\n if (node.category === \"ingredient\") {\n valueMap.set(node.id, node.value);\n } else {\n valueMap.set(node.id, 0);\n }\n });\n\n for (const nodeId of sortedNodeIds) {\n const node = nodeMap.get(nodeId);\n if (!node) continue;\n\n if (node.category === \"ingredient\") {\n continue;\n }\n\n let totalInputValue = 0;\n for (const inputId of node.inputs) {\n let inputValue = valueMap.get(inputId) || 0;\n const inputNode = nodeMap.get(inputId);\n if (inputNode && inputNode.outputs.length > 1) {\n inputValue /= inputNode.outputs.length;\n }\n totalInputValue += inputValue;\n }\n\n valueMap.set(nodeId, totalInputValue);\n }\n\n return valueMap;\n};\n","/**\n * Value formatting utilities using official @cooklang/cooklang package.\n *\n * @remarks\n * Provides utilities for converting Cooklang quantity and value types\n * into human-readable string representations.\n *\n * @packageDocumentation\n */\n\nimport {\n getNumericValue,\n quantity_display,\n type CooklangRecipe,\n type Value,\n type Quantity,\n type Step,\n} from \"@cooklang/cooklang\";\n\n/**\n * Regular number representation from @cooklang/cooklang WASM.\n */\ninterface RegularNumber {\n type: \"regular\";\n value: number;\n}\n\n/**\n * Fractional number representation from @cooklang/cooklang WASM.\n *\n * @remarks\n * Represents a number in the form of `[whole] num/den`.\n * The total value is `whole + err + num / den`.\n */\ninterface FractionNumber {\n type: \"fraction\";\n value: {\n whole: number;\n num: number;\n den: number;\n err: number;\n };\n}\n\n/**\n * Union type for Number values from @cooklang/cooklang WASM.\n */\ntype NumberValue = RegularNumber | FractionNumber;\n\n/**\n * Formats a NumberValue to a string representation.\n *\n * @param num - The NumberValue object (regular or fraction)\n * @returns A string representation of the number\n *\n * @example\n * ```ts\n * formatNumber({ type: \"regular\", value: 3 }); // \"3\"\n * formatNumber({ type: \"fraction\", value: { whole: 2, num: 1, den: 2, err: 0 } }); // \"2 1/2\"\n * formatNumber({ type: \"fraction\", value: { whole: 0, num: 1, den: 4, err: 0 } }); // \"1/4\"\n * ```\n */\nconst formatNumber = (num: NumberValue): string => {\n if (num.type === \"regular\") {\n return num.value.toString();\n } else if (num.type === \"fraction\") {\n const { whole, num: numerator, den } = num.value;\n if (whole === 0 && numerator === 0) {\n return \"0\";\n }\n if (numerator === 0) {\n return whole.toString();\n }\n if (whole === 0) {\n return `${numerator}/${den}`;\n }\n return `${whole} ${numerator}/${den}`;\n }\n return \"\";\n};\n\n/**\n * Formats a Cooklang Value to a string representation.\n *\n * @param value - The Value object to format (number, range, or text)\n * @returns A string representation of the value, or empty string if null/undefined\n *\n * @remarks\n * Handles both Regular and Fraction number types from the WASM parser.\n * Fractions are displayed in their natural form (e.g., \"1/2\", \"2 1/2\").\n *\n * @example\n * ```ts\n * // Regular number\n * formatValue({ type: \"number\", value: { type: \"regular\", value: 200 } }); // \"200\"\n * // Fraction\n * formatValue({ type: \"number\", value: { type: \"fraction\", value: { whole: 0, num: 1, den: 2, err: 0 } } }); // \"1/2\"\n * // Range with fractions\n * formatValue({ type: \"range\", value: { start: { type: \"regular\", value: 2 }, end: { type: \"regular\", value: 3 } } }); // \"2-3\"\n * formatValue({ type: \"text\", value: \"some\" }); // \"some\"\n * formatValue(null); // \"\"\n * ```\n */\nexport const formatValue = (value: Value | null | undefined): string => {\n if (!value) return \"\";\n\n if (value.type === \"number\") {\n // WASM returns nested structure: { type: \"number\", value: { type: \"regular\"|\"fraction\", ... } }\n const numValue = value.value as unknown as NumberValue;\n return formatNumber(numValue);\n } else if (value.type === \"range\") {\n // Range structure: { start: NumberValue, end: NumberValue }\n const rangeValue = value.value as unknown as { start: NumberValue; end: NumberValue };\n const startStr = formatNumber(rangeValue.start);\n const endStr = formatNumber(rangeValue.end);\n if (startStr && endStr) {\n return `${startStr}-${endStr}`;\n }\n return \"\";\n } else if (value.type === \"text\") {\n return value.value as string;\n }\n return \"\";\n};\n\n/**\n * Formats a Cooklang Quantity into separate value and unit strings.\n *\n * @param quantity - The Quantity object to format\n * @returns An object with `quantity` (numeric string) and `unit` (unit string)\n *\n * @example\n * ```ts\n * formatQuantityAmount({ value: { type: \"number\", value: 200 }, unit: \"g\" });\n * // { quantity: \"200\", unit: \"g\" }\n *\n * formatQuantityAmount(null);\n * // { quantity: \"\", unit: \"\" }\n * ```\n */\nexport const formatQuantityAmount = (\n quantity: Quantity | null | undefined,\n): { quantity: string; unit: string } => {\n if (!quantity) {\n return { quantity: \"\", unit: \"\" };\n }\n return {\n quantity: formatValue(quantity.value),\n unit: quantity.unit || \"\",\n };\n};\n\n/**\n * Generates complete text from step items by resolving references.\n *\n * @remarks\n * Converts a Step's items array into a readable string by:\n * - Keeping text items as-is\n * - Resolving ingredient references to \"name(quantity)\" format\n * - Resolving cookware references to their names\n * - Resolving timer references to their display values\n *\n * @param step - The Step object containing items to format\n * @param recipe - The parent CooklangRecipe for resolving references\n * @returns A concatenated string of all step items\n *\n * @example\n * ```ts\n * // For a step with text \"Cook \" + ingredient(pasta, 400g) + \" until done\"\n * generateStepText(step, recipe);\n * // \"Cook pasta(400g) until done\"\n * ```\n */\nexport const generateStepText = (step: Step, recipe: CooklangRecipe): string => {\n return step.items\n .map((item) => {\n if (item.type === \"text\") {\n return item.value;\n } else if (item.type === \"ingredient\") {\n const originalIngredient = recipe.ingredients[item.index];\n if (!originalIngredient) return \"\";\n\n const formatted = formatQuantityAmount(originalIngredient.quantity);\n const quantityText =\n formatted.quantity && formatted.unit\n ? `(${formatted.quantity}${formatted.unit})`\n : formatted.quantity\n ? `(${formatted.quantity})`\n : \"\";\n\n return `${originalIngredient.name}${quantityText}`;\n } else if (item.type === \"cookware\") {\n return recipe.cookware[item.index]?.name || \"\";\n } else if (item.type === \"timer\") {\n const timer = recipe.timers[item.index];\n if (!timer) return \"\";\n return timer.name || (timer.quantity ? quantity_display(timer.quantity) : \"\");\n }\n return \"\";\n })\n .join(\"\");\n};\n\n// Re-export useful functions from official package\nexport { getNumericValue, quantity_display };\n","/**\n * Node builders for Sankey diagram generation.\n *\n * @remarks\n * Factory functions for creating DAG nodes from Cooklang recipe elements.\n * Handles ingredients, process steps, and the final dish node.\n *\n * @packageDocumentation\n */\n\nimport {\n type CooklangRecipe,\n getNumericValue,\n grouped_quantity_display,\n grouped_quantity_is_empty,\n type Quantity,\n} from \"@cooklang/cooklang\";\nimport { generateStepText } from \"../formatter\";\nimport { DEFAULT_SECTION_NAME, FINAL_NODE_ID } from \"../constants\";\nimport type { DAGNode, NodeCategory, ExtractedValue } from \"../types/sankey\";\n\n/**\n * Extracts a numeric or text value from a Cooklang GroupedQuantity.\n *\n * @remarks\n * Attempts to extract a numeric value from fixed, scalable, or unknown\n * quantity types. Falls back to text representation if no numeric value\n * is available.\n *\n * @param groupedQuantity - The GroupedQuantity object from Cooklang parser\n * @returns An ExtractedValue with either numeric value or text label\n *\n * @example\n * ```ts\n * // For \"200g\" quantity\n * calculateIngredientQuantity(gq);\n * // { type: \"number\", value: 200, label: \"200g\" }\n *\n * // For \"some\" quantity\n * calculateIngredientQuantity(gq);\n * // { type: \"text\", label: \"some\" }\n * ```\n */\nexport const calculateIngredientQuantity = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n groupedQuantity: any,\n): ExtractedValue => {\n if (grouped_quantity_is_empty(groupedQuantity)) {\n return { type: \"text\", label: \"unknown\" };\n }\n\n const displayText = grouped_quantity_display(groupedQuantity);\n\n const gq = groupedQuantity as {\n fixed?: Quantity | null;\n scalable?: Quantity | null;\n fixed_unknown?: Quantity | null;\n scalable_unknown?: Quantity | null;\n };\n\n const quantityToUse = gq.fixed || gq.scalable || gq.fixed_unknown || gq.scalable_unknown;\n\n if (quantityToUse) {\n const numericValue = getNumericValue(quantityToUse.value);\n if (numericValue !== null) {\n return { type: \"number\", value: numericValue, label: displayText || `${numericValue}` };\n }\n }\n\n return { type: \"text\", label: displayText || \"unknown\" };\n};\n\n/**\n * Creates DAG nodes for all ingredients in a recipe.\n *\n * @remarks\n * Processes the recipe's grouped ingredients to create nodes with:\n * - Unique IDs based on ingredient index\n * - Extracted quantity values for flow calculation\n * - Display labels with quantity information\n *\n * @param recipe - The parsed CooklangRecipe\n * @returns An object containing:\n * - `nodes`: Array of ingredient DAGNodes\n * - `indexMap`: Map from original ingredient index to node ID\n *\n * @example\n * ```ts\n * const { nodes, indexMap } = createIngredientNodes(recipe);\n * // nodes: [{ id: \"0\", name: \"flour\", value: 500, ... }, ...]\n * // indexMap: Map { 0 => \"0\", 1 => \"1\", ... }\n * ```\n */\nexport const createIngredientNodes = (\n recipe: CooklangRecipe,\n): { nodes: DAGNode[]; indexMap: Map<number, string> } => {\n const nodes: DAGNode[] = [];\n const indexMap = new Map<number, string>();\n\n recipe.groupedIngredients.forEach(([ingredient, groupedQuantity], index) => {\n const extracted = calculateIngredientQuantity(groupedQuantity);\n const value = extracted.type === \"number\" ? extracted.value : 1;\n const label = extracted.label;\n const ingredientId = index.toString();\n\n indexMap.set(index, ingredientId);\n\n nodes.push({\n id: ingredientId,\n name: ingredient.name,\n category: \"ingredient\" as NodeCategory,\n value,\n originalValue: value,\n label,\n inputs: [],\n outputs: [],\n metadata: {\n originalIndex: index,\n },\n });\n });\n\n return { nodes, indexMap };\n};\n\n/**\n * Creates DAG nodes for all cooking steps in a recipe.\n *\n * @remarks\n * Iterates through all sections and steps, creating process nodes with:\n * - Unique IDs based on section name and step number\n * - Generated text from step items (ingredients, cookware, timers)\n * - Initial value of 0 (calculated later via flow propagation)\n *\n * @param recipe - The parsed CooklangRecipe\n * @returns Array of process DAGNodes representing cooking steps\n *\n * @example\n * ```ts\n * const stepNodes = createStepNodes(recipe);\n * // [{ id: \"step_main_1\", name: \"Boil pasta(400g)\", category: \"process\", ... }]\n * ```\n */\nexport const createStepNodes = (recipe: CooklangRecipe): DAGNode[] => {\n const nodes: DAGNode[] = [];\n\n for (const section of recipe.sections) {\n for (const content of section.content) {\n if (content.type === \"step\") {\n const step = content.value;\n const stepText = generateStepText(step, recipe);\n const stepId = `step_${section.name || DEFAULT_SECTION_NAME}_${step.number}`;\n\n nodes.push({\n id: stepId,\n name: stepText || `手順 ${step.number}`,\n category: \"process\" as NodeCategory,\n value: 0,\n label: `${step.number}`,\n inputs: [],\n outputs: [],\n metadata: {\n stepNumber: step.number,\n sectionName: section.name || undefined,\n },\n });\n }\n }\n }\n\n return nodes;\n};\n\n/**\n * Creates the final node representing the completed dish.\n *\n * @remarks\n * The final node is the sink of the Sankey diagram where all\n * process flows converge. Its value is calculated by summing\n * all incoming flows.\n *\n * @param finalNodeName - Display name for the final dish node\n * @returns A DAGNode with category \"final\"\n *\n * @example\n * ```ts\n * const finalNode = createFinalNode(\"Carbonara\");\n * // { id: \"final_dish\", name: \"Carbonara\", category: \"final\", ... }\n * ```\n */\nexport const createFinalNode = (finalNodeName: string): DAGNode => ({\n id: FINAL_NODE_ID,\n name: finalNodeName,\n category: \"final\" as NodeCategory,\n value: 0,\n label: \"\",\n inputs: [],\n outputs: [],\n});\n","/**\n * Edge builders for Sankey diagram generation.\n *\n * @remarks\n * Factory functions for creating DAG edges that connect nodes.\n * Handles ingredient-to-step and step-to-final connections,\n * including step reference resolution.\n *\n * @packageDocumentation\n */\n\nimport { type CooklangRecipe, type Step } from \"@cooklang/cooklang\";\nimport { DEFAULT_SECTION_NAME } from \"../constants\";\nimport type { DAGNode, DAGEdge, TransformationType } from \"../types/sankey\";\n\n/**\n * Generates a unique step ID from section name and step number.\n *\n * @param sectionName - The recipe section name (null uses default)\n * @param stepNumber - The 1-indexed step number\n * @returns A unique step ID string (e.g., \"step_main_1\")\n *\n * @example\n * ```ts\n * getStepId(\"main\", 1); // \"step_main_1\"\n * getStepId(null, 2); // \"step_main_2\"\n * getStepId(\"sauce\", 1); // \"step_sauce_1\"\n * ```\n */\nexport const getStepId = (sectionName: string | null | undefined, stepNumber: number): string => {\n return `step_${sectionName || DEFAULT_SECTION_NAME}_${stepNumber}`;\n};\n\n/**\n * Extracts ingredient usage information from a recipe step.\n *\n * @param step - The Step object to analyze\n * @returns Array of objects containing ingredient index and step number\n *\n * @example\n * ```ts\n * // For a step using @pasta and @salt\n * extractIngredientUsageFromStep(step);\n * // [{ ingredientIndex: 0, stepNumber: 1 }, { ingredientIndex: 1, stepNumber: 1 }]\n * ```\n */\nexport const extractIngredientUsageFromStep = (\n step: Step,\n): Array<{ ingredientIndex: number; stepNumber: number }> => {\n const usages: Array<{ ingredientIndex: number; stepNumber: number }> = [];\n\n for (const item of step.items) {\n if (item.type === \"ingredient\") {\n usages.push({\n ingredientIndex: item.index,\n stepNumber: step.number,\n });\n }\n }\n\n return usages;\n};\n\n/**\n * Builds edges connecting ingredients to their consuming steps.\n *\n * @remarks\n * Analyzes the recipe to create edges from:\n * - Ingredient nodes to step nodes (for direct ingredient usage)\n * - Step nodes to other step nodes (for step references like \"the pasta from step 1\")\n *\n * Also updates the input/output arrays on the nodes for DAG traversal.\n *\n * @param recipe - The parsed CooklangRecipe\n * @param ingredientIndexMap - Map from ingredient index to node ID\n * @param nodes - Array of all DAG nodes (modified in place)\n * @returns Array of DAGEdges connecting ingredients/steps to steps\n *\n * @example\n * ```ts\n * const edges = buildIngredientToStepEdges(recipe, indexMap, nodes);\n * // [{ from: \"0\", to: \"step_main_1\", metadata: { ... } }, ...]\n * ```\n */\nexport const buildIngredientToStepEdges = (\n recipe: CooklangRecipe,\n ingredientIndexMap: Map<number, string>,\n nodes: DAGNode[],\n): DAGEdge[] => {\n const edges: DAGEdge[] = [];\n const nodeMap = new Map<string, DAGNode>();\n\n nodes.forEach((node) => nodeMap.set(node.id, node));\n\n for (const section of recipe.sections) {\n for (const content of section.content) {\n if (content.type === \"step\") {\n const step = content.value;\n const currentStepId = getStepId(section.name, step.number);\n const usedIngredients = extractIngredientUsageFromStep(step);\n\n for (const usage of usedIngredients) {\n const ingredient = recipe.ingredients[usage.ingredientIndex];\n\n // Check for step reference\n const relation = ingredient.relation;\n if (\n relation &&\n relation.relation?.type === \"reference\" &&\n relation.reference_target === \"step\"\n ) {\n const referenceTo = relation.relation.references_to;\n const sourceStepId = getStepId(section.name, referenceTo + 1);\n edges.push({\n from: sourceStepId,\n to: currentStepId,\n metadata: {\n stepNumber: step.number,\n transformationType: \"cooking\" as TransformationType,\n },\n });\n\n const sourceNode = nodeMap.get(sourceStepId);\n const targetNode = nodeMap.get(currentStepId);\n if (sourceNode && targetNode) {\n sourceNode.outputs.push(currentStepId);\n targetNode.inputs.push(sourceStepId);\n }\n continue;\n }\n\n const ingredientId = ingredientIndexMap.get(usage.ingredientIndex);\n if (ingredientId) {\n edges.push({\n from: ingredientId,\n to: currentStepId,\n metadata: {\n stepNumber: step.number,\n transformationType: \"preparation\" as TransformationType,\n },\n });\n\n const ingredientNode = nodeMap.get(ingredientId);\n const stepNode = nodeMap.get(currentStepId);\n if (ingredientNode && stepNode) {\n ingredientNode.outputs.push(currentStepId);\n stepNode.inputs.push(ingredientId);\n }\n }\n }\n }\n }\n }\n\n return edges;\n};\n\n/**\n * Builds an edge from the last step to the final dish node.\n *\n * @remarks\n * Connects only the last step to the final node, representing\n * the completion of the recipe. Also updates the input/output\n * arrays on the affected nodes.\n *\n * @param stepNodes - Array of step nodes in order\n * @param finalNode - The final dish node (modified in place)\n * @returns Array containing a single edge to the final node, or empty if no steps\n *\n * @example\n * ```ts\n * const edges = buildStepToFinalEdges(stepNodes, finalNode);\n * // [{ from: \"step_main_3\", to: \"final_dish\", metadata: { transformationType: \"completion\" } }]\n * ```\n */\nexport const buildStepToFinalEdges = (stepNodes: DAGNode[], finalNode: DAGNode): DAGEdge[] => {\n if (stepNodes.length === 0) return [];\n\n const lastStepNode = stepNodes[stepNodes.length - 1];\n\n lastStepNode.outputs.push(finalNode.id);\n finalNode.inputs.push(lastStepNode.id);\n\n return [\n {\n from: lastStepNode.id,\n to: finalNode.id,\n metadata: {\n transformationType: \"completion\" as TransformationType,\n },\n },\n ];\n};\n","/**\n * Sankey diagram data generator from Cooklang recipes.\n *\n * @remarks\n * Main module for transforming parsed Cooklang recipes into\n * Sankey diagram data structures. Orchestrates the DAG building,\n * value calculation, and normalization pipeline.\n *\n * @packageDocumentation\n */\n\nimport type { CooklangRecipe } from \"@cooklang/cooklang\";\nimport {\n DEFAULT_FINAL_NODE_NAME,\n DEFAULT_MIN_LINK_VALUE,\n DEFAULT_NORMALIZATION,\n} from \"../constants\";\nimport type { SankeyNode, SankeyLink, SankeyData, DAGNode, DAGEdge } from \"../types/sankey\";\nimport { normalizeIngredientValues } from \"./normalizer\";\nimport { buildDAGAndSort, calculateNodeValues } from \"./dag\";\nimport { createIngredientNodes, createStepNodes, createFinalNode } from \"./node-builders\";\nimport { buildIngredientToStepEdges, buildStepToFinalEdges } from \"./edge-builders\";\n\n/**\n * Configuration options for Sankey diagram generation.\n *\n * @example\n * ```ts\n * const options: SankeyGeneratorOptions = {\n * finalNodeName: \"Carbonara\",\n * normalization: \"logarithmic\",\n * minLinkValue: 0.1\n * };\n * ```\n */\nexport interface SankeyGeneratorOptions {\n /**\n * Display name for the final dish node.\n * @defaultValue \"完成品\"\n */\n finalNodeName?: string;\n /**\n * Method for normalizing ingredient values.\n * - `logarithmic`: Compress large ranges (recommended)\n * - `linear`: Linear scaling\n * - `none`: Use raw values\n * @defaultValue \"logarithmic\"\n */\n normalization?: \"logarithmic\" | \"linear\" | \"none\";\n /**\n * Minimum value for links to ensure visibility.\n * @defaultValue 0.1\n */\n minLinkValue?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<SankeyGeneratorOptions> = {\n finalNodeName: DEFAULT_FINAL_NODE_NAME,\n normalization: DEFAULT_NORMALIZATION,\n minLinkValue: DEFAULT_MIN_LINK_VALUE,\n};\n\n/**\n * Transforms DAG nodes and edges into final Sankey diagram data.\n *\n * @remarks\n * Internal function that:\n * 1. Normalizes ingredient values using the specified method\n * 2. Calculates flow values for all nodes via DAG propagation\n * 3. Converts DAG structures to Sankey-compatible format\n * 4. Applies minimum value constraints to links\n *\n * @param dagNodes - Array of DAG nodes with dependency info\n * @param dagEdges - Array of DAG edges\n * @param sortedNodeIds - Node IDs in topological order\n * @param options - Generation options with all defaults applied\n * @returns Complete SankeyData structure ready for visualization\n *\n * @internal\n */\nconst buildSankeyData = (\n dagNodes: DAGNode[],\n dagEdges: DAGEdge[],\n sortedNodeIds: string[],\n options: Required<SankeyGeneratorOptions>,\n): SankeyData => {\n const ingredientNodes = dagNodes\n .filter((node) => node.category === \"ingredient\")\n .map((node) => ({\n id: node.id,\n name: node.name,\n category: node.category,\n value: node.value,\n label: node.label,\n originalValue: node.originalValue,\n metadata: node.metadata,\n }));\n\n const normalizedValueMap = normalizeIngredientValues(ingredientNodes, options.normalization);\n\n const normalizedDAGNodes = dagNodes.map((node) => {\n if (node.category === \"ingredient\") {\n return {\n ...node,\n value: normalizedValueMap.get(node.id) ?? node.value,\n };\n }\n return node;\n });\n\n const finalCalculatedValues = calculateNodeValues(normalizedDAGNodes, sortedNodeIds);\n\n const sankeyNodes: SankeyNode[] = dagNodes.map((dagNode) => {\n const finalValue = finalCalculatedValues.get(dagNode.id) || 0;\n return {\n id: dagNode.id,\n name: dagNode.name,\n category: dagNode.category,\n value:\n dagNode.category === \"ingredient\"\n ? (normalizedValueMap.get(dagNode.id) ?? dagNode.value)\n : finalValue,\n label: dagNode.label,\n originalValue: dagNode.originalValue,\n metadata: dagNode.metadata,\n };\n });\n\n const sankeyLinks: SankeyLink[] = dagEdges.map((edge) => {\n const sourceValue = finalCalculatedValues.get(edge.from) || 0;\n const sourceNode = dagNodes.find((n) => n.id === edge.from);\n\n let linkValue = sourceValue;\n if (sourceNode && sourceNode.outputs.length > 1) {\n linkValue = sourceValue / sourceNode.outputs.length;\n }\n\n return {\n source: edge.from,\n target: edge.to,\n value: Math.max(options.minLinkValue, linkValue),\n originalValue: sourceValue,\n metadata: edge.metadata,\n };\n });\n\n return {\n nodes: sankeyNodes,\n links: sankeyLinks,\n };\n};\n\n/**\n * Generates Sankey diagram data from a parsed Cooklang recipe.\n *\n * @remarks\n * This is the main entry point for the library. It transforms a parsed\n * Cooklang recipe into a Sankey diagram data structure by:\n * 1. Creating nodes for ingredients, steps, and the final dish\n * 2. Building edges representing ingredient flow\n * 3. Performing topological sort for correct value propagation\n * 4. Normalizing values for balanced visualization\n *\n * @param recipe - A parsed CooklangRecipe object\n * @param options - Optional configuration for generation\n * @returns SankeyData structure, or null if generation fails\n *\n * @example\n * ```ts\n * import { CooklangParser, generateSankeyData } from 'cooklang-sankey';\n *\n * const parser = new CooklangParser();\n * const [recipe] = parser.parse(`\n * @pasta{400g} を茹でる。\n * @卵{3個}と@チーズ{100g}を混ぜる。\n * `);\n *\n * const data = generateSankeyData(recipe, {\n * finalNodeName: \"Carbonara\",\n * normalization: \"logarithmic\"\n * });\n * ```\n */\nexport const generateSankeyData = (\n recipe: CooklangRecipe,\n options?: SankeyGeneratorOptions,\n): SankeyData | null => {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n try {\n const { nodes: ingredientNodes, indexMap: ingredientIndexMap } = createIngredientNodes(recipe);\n\n const stepNodes = createStepNodes(recipe);\n const finalNode = createFinalNode(opts.finalNodeName);\n\n const allNodes = [...ingredientNodes, ...stepNodes, finalNode];\n\n const ingredientToStepEdges = buildIngredientToStepEdges(recipe, ingredientIndexMap, allNodes);\n const stepToFinalEdges = buildStepToFinalEdges(stepNodes, finalNode);\n const allEdges = [...ingredientToStepEdges, ...stepToFinalEdges];\n\n const sortedNodeIds = buildDAGAndSort(allNodes, allEdges);\n\n return buildSankeyData(allNodes, allEdges, sortedNodeIds, opts);\n } catch (error) {\n console.error(\"Error generating sankey data from cooklang:\", error);\n return null;\n }\n};\n\n/**\n * Validates and optimizes Sankey data for visualization.\n *\n * @remarks\n * Performs three optimization steps:\n * 1. **Remove invalid links**: Filters out links referencing non-existent nodes\n * 2. **Remove orphaned nodes**: Keeps only nodes connected to valid links\n * (final nodes are always preserved)\n * 3. **Normalize link values**: Scales values relative to minimum for\n * stable visualization (ensures all values >= 1)\n *\n * @param data - The SankeyData to optimize\n * @returns A new SankeyData with optimizations applied\n *\n * @example\n * ```ts\n * const rawData = generateSankeyData(recipe);\n * if (rawData) {\n * const optimized = optimizeSankeyData(rawData);\n * // Use optimized data for visualization\n * }\n * ```\n */\nexport const optimizeSankeyData = (data: SankeyData): SankeyData => {\n // Create a Set of existing node IDs for O(1) lookup\n const existingNodeIds = new Set(data.nodes.map((node) => node.id));\n\n // Step 1: Remove invalid links\n // Filter out links where source or target node doesn't exist\n const validLinks = data.links.filter((link) => {\n const sourceExists = existingNodeIds.has(link.source);\n const targetExists = existingNodeIds.has(link.target);\n\n if (!sourceExists) {\n console.error(`Missing source node: ${link.source}`);\n }\n if (!targetExists) {\n console.error(`Missing target node: ${link.target}`);\n }\n\n return sourceExists && targetExists;\n });\n\n // Step 2: Remove orphaned nodes\n // Collect node IDs that are connected to valid links\n const connectedNodeIds = new Set<string>();\n validLinks.forEach((link) => {\n connectedNodeIds.add(link.source);\n connectedNodeIds.add(link.target);\n });\n\n // Keep only nodes connected to links, or final nodes (always preserved)\n const filteredNodes = data.nodes.filter(\n (node) => connectedNodeIds.has(node.id) || node.category === \"final\",\n );\n\n // Step 3: Normalize link values\n // Scale values relative to minimum for stable visualization\n let normalizedLinks = validLinks;\n if (validLinks.length > 0) {\n const minValue = Math.min(...validLinks.map((link) => link.value));\n if (minValue > 0) {\n // Divide by min value and clamp to ensure value >= 1\n normalizedLinks = validLinks.map((link) => ({\n ...link,\n value: Math.max(1, link.value / minValue),\n }));\n }\n }\n\n return {\n nodes: filteredNodes,\n links: normalizedLinks,\n };\n};\n"]}