ln-tabular-recipes-dsl 0.1.1 → 0.1.2
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/CHANGELOG.md +12 -0
- package/README.md +20 -4
- package/dist/index.cjs +12 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/style.css +54 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
[Semantic Versioning](https://semver.org/). The text format is versioned in
|
|
6
6
|
[SPEC.md](./SPEC.md).
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-11-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `renderHtml` option `stepOrientation` (`'horizontal'` | `'vertical'` |
|
|
13
|
+
`'auto'`): step texts can be turned sideways (read bottom to top) so step
|
|
14
|
+
columns stay narrow on mobile; `'auto'` switches with a container query when
|
|
15
|
+
the wrapper is narrower than 40rem. New classes `trd-vsteps`, `trd-auto`,
|
|
16
|
+
`trd-step-text` and custom property `--trd-step-max-height` in `style.css`.
|
|
17
|
+
Default output is unchanged.
|
|
18
|
+
- The demo page renders with `stepOrientation: 'auto'`.
|
|
19
|
+
|
|
8
20
|
## [0.1.1] - 2026-11-12
|
|
9
21
|
|
|
10
22
|
First release published from GitHub Actions with npm provenance (trusted
|
package/README.md
CHANGED
|
@@ -172,12 +172,28 @@ interface Cell {
|
|
|
172
172
|
|
|
173
173
|
Returns an escaped HTML string (`<div class="trd-wrap"><table class="trd-table">…`).
|
|
174
174
|
Options: layout options + `classPrefix` (default `trd`; the bundled CSS uses
|
|
175
|
-
`trd`), `stickyIngredients`, `caption`. Classes: `trd-wrap`,
|
|
176
|
-
`trd-
|
|
177
|
-
`trd-
|
|
175
|
+
`trd`), `stickyIngredients`, `caption`, `stepOrientation`. Classes: `trd-wrap`,
|
|
176
|
+
`trd-auto`, `trd-table`, `trd-sticky`, `trd-invalid`, `trd-vsteps`,
|
|
177
|
+
`trd-banner-row`, `trd-banner`, `trd-qty`, `trd-ingredient`, `trd-step`,
|
|
178
|
+
`trd-step-text`, `trd-final`; cells carry `data-key`.
|
|
179
|
+
|
|
180
|
+
`stepOrientation` makes tables fit small screens by turning step texts
|
|
181
|
+
sideways (read bottom to top), so each step column is about one line wide:
|
|
182
|
+
|
|
183
|
+
| Value | Effect |
|
|
184
|
+
| ------------------------ | ----------------------------------------------------------------------- |
|
|
185
|
+
| `'horizontal'` (default) | Unchanged output |
|
|
186
|
+
| `'vertical'` | Step texts always vertical (`trd-vsteps` on the table) |
|
|
187
|
+
| `'auto'` | Vertical only when the wrapper is narrower than 40rem (container query) |
|
|
188
|
+
|
|
189
|
+
With `'auto'` the wrapper (`trd-wrap trd-auto`) is a size container: give it a
|
|
190
|
+
width from its parent (a block, or a flex/grid item with a set width), not from
|
|
191
|
+
its content. `--trd-step-max-height` (default `13em`) caps the height of a
|
|
192
|
+
vertical step text before it wraps onto a second column.
|
|
178
193
|
|
|
179
194
|
`style.css` exposes custom properties (`--trd-fg`, `--trd-bg`, `--trd-border`,
|
|
180
|
-
`--trd-step-bg`, `--trd-final-bg`, `--trd-banner-bg`, `--trd-qty-width`,
|
|
195
|
+
`--trd-step-bg`, `--trd-final-bg`, `--trd-banner-bg`, `--trd-qty-width`,
|
|
196
|
+
`--trd-step-max-height`, …)
|
|
181
197
|
and switches to dark with `prefers-color-scheme`, `.theme-dark` (Obsidian) or
|
|
182
198
|
`[data-theme="dark"]`; `.theme-light` / `[data-theme="light"]` force light.
|
|
183
199
|
|
package/dist/index.cjs
CHANGED
|
@@ -1028,15 +1028,25 @@ function renderHtml(recipe, options = {}) {
|
|
|
1028
1028
|
const tableClasses = [`${p}-table`];
|
|
1029
1029
|
if (options.stickyIngredients) tableClasses.push(`${p}-sticky`);
|
|
1030
1030
|
if (!grid.valid) tableClasses.push(`${p}-invalid`);
|
|
1031
|
+
const orientation = options.stepOrientation ?? "horizontal";
|
|
1032
|
+
if (orientation === "vertical") tableClasses.push(`${p}-vsteps`);
|
|
1033
|
+
const wrapClasses = [`${p}-wrap`];
|
|
1034
|
+
if (orientation === "auto") wrapClasses.push(`${p}-auto`);
|
|
1035
|
+
const wrapStepText = orientation !== "horizontal";
|
|
1031
1036
|
const cellHtml = (cell) => {
|
|
1032
1037
|
const classes = [`${p}-${cell.kind === "quantity" ? "qty" : cell.kind}`];
|
|
1033
1038
|
if (cell.final) classes.push(`${p}-final`);
|
|
1034
1039
|
const attrs = [`class="${classes.join(" ")}"`, `data-key="${escapeHtml(cell.key)}"`];
|
|
1035
1040
|
if (cell.rowSpan > 1) attrs.push(`rowspan="${cell.rowSpan}"`);
|
|
1036
1041
|
if (cell.colSpan > 1) attrs.push(`colspan="${cell.colSpan}"`);
|
|
1037
|
-
|
|
1042
|
+
const text = escapeHtml(cell.text);
|
|
1043
|
+
const content = wrapStepText && cell.kind === "step" ? `<span class="${p}-step-text">${text}</span>` : text;
|
|
1044
|
+
return `<td ${attrs.join(" ")}>${content}</td>`;
|
|
1038
1045
|
};
|
|
1039
|
-
const parts = [
|
|
1046
|
+
const parts = [
|
|
1047
|
+
`<div class="${wrapClasses.join(" ")}">`,
|
|
1048
|
+
`<table class="${tableClasses.join(" ")}">`
|
|
1049
|
+
];
|
|
1040
1050
|
if (options.caption !== void 0)
|
|
1041
1051
|
parts.push(`<caption>${escapeHtml(options.caption)}</caption>`);
|
|
1042
1052
|
parts.push("<tbody>");
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/model.ts","../src/diagnostics.ts","../src/validate.ts","../src/normalize.ts","../src/quantity.ts","../src/dsl/label.ts","../src/dsl/lexer.ts","../src/dsl/parser.ts","../src/dsl/serializer.ts","../src/layout.ts","../src/html.ts"],"sourcesContent":["export type { Recipe, Ingredient, Step, RecipeOptions, DecimalSeparator } from './model';\nexport { DEFAULT_UNITS, KEY_PATTERN, MAX_KEY_LENGTH } from './model';\n\nexport type { Diagnostic, DiagnosticCode, DiagnosticParams, Severity } from './diagnostics';\nexport {\n DIAGNOSTIC_CODES,\n DIAGNOSTIC_MESSAGES,\n formatDiagnostic,\n hasErrors,\n interpolate,\n} from './diagnostics';\n\nexport { validate } from './validate';\nexport { normalize } from './normalize';\n\nexport type { ParseResult, ParseOptions } from './dsl/parser';\nexport { parse } from './dsl/parser';\nexport type { SerializeOptions } from './dsl/serializer';\nexport { serialize } from './dsl/serializer';\n\nexport type { Cell, CellKind, Layout, LayoutOptions } from './layout';\nexport { layout } from './layout';\nexport type { RenderHtmlOptions } from './html';\nexport { renderHtml, escapeHtml } from './html';\n\nexport type { FormatQuantityOptions } from './quantity';\nexport { formatQuantity, scale } from './quantity';\n","/**\n * Core data model of a tabular recipe.\n *\n * A recipe is a tree: ingredients are leaves, steps are nodes and the last\n * step is the root (\"final step\"). `prerequisites` are steps that use no\n * ingredient (e.g. \"Preheat the oven\"); they are rendered as full-width\n * banners above the table.\n */\nexport interface Recipe {\n prerequisites: string[];\n ingredients: Ingredient[];\n steps: Step[];\n}\n\nexport interface Ingredient {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n name: string;\n /** Finite, strictly positive. */\n quantity?: number;\n /** Only meaningful together with `quantity`. */\n unit?: string;\n}\n\nexport interface Step {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n text: string;\n /** Keys of the ingredients and/or earlier steps this step combines, in row order. */\n inputs: string[];\n}\n\n/** Allowed node keys: 1 to 40 ASCII letters, digits, `_` or `-`. */\nexport const KEY_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;\n\nexport const MAX_KEY_LENGTH = 40;\n\n/** Default unit vocabulary. Matching is case-insensitive; the spelling below is canonical. */\nexport const DEFAULT_UNITS: readonly string[] = [\n 'pcs',\n 'g',\n 'kg',\n 'ml',\n 'cl',\n 'l',\n 'cs',\n 'cc',\n 'pincee',\n];\n\nexport type DecimalSeparator = '.' | ',';\n\n/** Options shared by most functions of the library. */\nexport interface RecipeOptions {\n /** Unit vocabulary (case-insensitive, canonical spelling taken from this list). */\n units?: readonly string[];\n /** Decimal separator used when *writing* quantities. Parsing always accepts both. Default `.`. */\n decimalSeparator?: DecimalSeparator;\n}\n\n/** Builds a lower-case → canonical spelling lookup for a unit list. */\nexport function unitLookup(units: readonly string[] = DEFAULT_UNITS): Map<string, string> {\n const map = new Map<string, string>();\n for (const unit of units) {\n const lower = unit.toLowerCase();\n if (!map.has(lower)) map.set(lower, unit);\n }\n return map;\n}\n","/**\n * Diagnostics are language-neutral: `code` + `params` identify the problem,\n * `message` is an English default. Consumers translate by providing their own\n * templates to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_CODES = [\n // --- DSL syntax ---\n 'SYNTAX_ERROR',\n 'UNTERMINATED_STRING',\n 'UNTERMINATED_LABEL',\n 'FAN_OUT',\n // --- DSL references ---\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n 'CYCLE',\n // --- JSON model ---\n 'INVALID_SHAPE',\n 'UNKNOWN_REFERENCE',\n 'FORWARD_REFERENCE',\n // --- shared ---\n 'INVALID_KEY',\n 'DUPLICATE_KEY',\n 'EMPTY_TEXT',\n 'INVALID_QUANTITY',\n 'UNIT_WITHOUT_QUANTITY',\n 'UNKNOWN_UNIT',\n // --- tree rules ---\n 'NO_INPUT',\n 'MULTIPLE_CONSUMERS',\n 'UNCONSUMED',\n 'MULTIPLE_ROOTS',\n] as const;\n\nexport type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];\n\nexport type Severity = 'error' | 'warning';\n\nexport type DiagnosticParams = Record<string, string | number>;\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n /** English default message, built from {@link DIAGNOSTIC_MESSAGES}. */\n message: string;\n severity: Severity;\n /** 1-based. DSL diagnostics only. */\n line?: number;\n /** 1-based, in UTF-16 code units (same as JS string indices / CodeMirror). */\n column?: number;\n endLine?: number;\n /** 1-based, exclusive. */\n endColumn?: number;\n /** JSON path of the offending value. Model (JSON) diagnostics only. */\n path?: (string | number)[];\n params?: DiagnosticParams;\n}\n\n/**\n * English message templates. `{name}` placeholders are replaced by `params.name`.\n * Copy this object, translate the values and pass it to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_MESSAGES: Record<DiagnosticCode, string> = {\n SYNTAX_ERROR: 'Syntax error: expected {expected}, found {found}.',\n UNTERMINATED_STRING: 'Unterminated quoted string.',\n UNTERMINATED_LABEL: 'Missing closing \"{close}\".',\n FAN_OUT: 'A node can only feed one step: \"&\" is not allowed after \"-->\".',\n UNDEFINED_NODE: 'Node \"{key}\" is used but never defined.',\n REDEFINED_NODE: 'Node \"{key}\" is already defined on line {firstLine}.',\n INGREDIENT_AS_TARGET: 'Ingredient \"{key}\" cannot be the target of \"-->\"; only steps can.',\n CYCLE: 'Steps form a cycle: {keys}.',\n INVALID_SHAPE: 'Invalid value: expected {expected}.',\n UNKNOWN_REFERENCE: 'Input \"{key}\" does not match any ingredient or step.',\n FORWARD_REFERENCE: 'Input \"{key}\" refers to a step that does not come before \"{step}\".',\n INVALID_KEY: 'Invalid key \"{key}\": use 1 to 40 letters, digits, \"_\" or \"-\".',\n DUPLICATE_KEY: 'Key \"{key}\" is used more than once.',\n EMPTY_TEXT: 'Text must not be empty.',\n INVALID_QUANTITY: 'Invalid quantity \"{quantity}\": it must be a number greater than 0.',\n UNIT_WITHOUT_QUANTITY: 'Unit \"{unit}\" is set without a quantity.',\n UNKNOWN_UNIT: 'Unknown unit \"{unit}\".',\n NO_INPUT: 'Step \"{key}\" has no input.',\n MULTIPLE_CONSUMERS:\n '\"{key}\" is already used by step \"{consumer}\"; each node feeds exactly one step.',\n UNCONSUMED: 'Ingredient \"{key}\" is not used by any step.',\n MULTIPLE_ROOTS:\n 'Step \"{key}\" is not used by any later step; only the final step \"{root}\" may be unused.',\n};\n\n/** Replaces `{name}` placeholders of `template` with `params`. Unknown placeholders are kept. */\nexport function interpolate(template: string, params: DiagnosticParams = {}): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match,\n );\n}\n\n/** Formats a diagnostic with custom (e.g. translated) templates, falling back to English. */\nexport function formatDiagnostic(\n diagnostic: Pick<Diagnostic, 'code' | 'params'>,\n templates: Partial<Record<DiagnosticCode, string>> = DIAGNOSTIC_MESSAGES,\n): string {\n const template = templates[diagnostic.code] ?? DIAGNOSTIC_MESSAGES[diagnostic.code];\n return interpolate(template, diagnostic.params);\n}\n\n/** Internal helper to build a diagnostic with its English message. */\nexport function diagnostic(\n code: DiagnosticCode,\n params: DiagnosticParams,\n location: Omit<Diagnostic, 'code' | 'message' | 'severity' | 'params'> = {},\n severity: Severity = 'error',\n): Diagnostic {\n return {\n code,\n message: interpolate(DIAGNOSTIC_MESSAGES[code], params),\n severity,\n ...location,\n params,\n };\n}\n\nexport function hasErrors(diagnostics: readonly Diagnostic[]): boolean {\n return diagnostics.some((d) => d.severity === 'error');\n}\n","import { diagnostic, type Diagnostic } from './diagnostics';\nimport { KEY_PATTERN, unitLookup, type Recipe, type RecipeOptions } from './model';\n\ntype Path = (string | number)[];\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Validates a recipe given as JSON (e.g. received by an API). Returns every\n * problem found, each with a `path` into the input. An empty array (or only\n * warnings) means the recipe is valid.\n *\n * Accepts `unknown` so it can be used on untrusted input: shape problems are\n * reported as INVALID_SHAPE instead of throwing.\n */\nexport function validate(recipe: Recipe | unknown, options: RecipeOptions = {}): Diagnostic[] {\n const out: Diagnostic[] = [];\n const shape = (path: Path, expected: string) =>\n out.push(diagnostic('INVALID_SHAPE', { expected }, { path }));\n\n if (!isObject(recipe)) {\n shape([], 'an object with prerequisites, ingredients and steps');\n return out;\n }\n const { prerequisites, ingredients, steps } = recipe;\n if (!Array.isArray(prerequisites)) shape(['prerequisites'], 'an array of strings');\n if (!Array.isArray(ingredients)) shape(['ingredients'], 'an array');\n if (!Array.isArray(steps)) shape(['steps'], 'an array');\n if (!Array.isArray(prerequisites) || !Array.isArray(ingredients) || !Array.isArray(steps)) {\n return out;\n }\n\n prerequisites.forEach((text, i) => {\n if (typeof text !== 'string') shape(['prerequisites', i], 'a string');\n else if (text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: ['prerequisites', i] }));\n });\n\n // --- keys -------------------------------------------------------------------------------\n const kinds = new Map<string, { kind: 'ingredient' | 'step'; index: number }>();\n const checkKey = (key: unknown, path: Path, kind: 'ingredient' | 'step', index: number) => {\n if (typeof key !== 'string') {\n shape([...path, 'key'], 'a string');\n return;\n }\n if (!KEY_PATTERN.test(key)) {\n out.push(diagnostic('INVALID_KEY', { key }, { path: [...path, 'key'] }));\n }\n if (kinds.has(key)) {\n out.push(diagnostic('DUPLICATE_KEY', { key }, { path: [...path, 'key'] }));\n } else {\n kinds.set(key, { kind, index });\n }\n };\n\n // --- ingredients ------------------------------------------------------------------------\n const units = unitLookup(options.units);\n let shapeOk = true;\n ingredients.forEach((ingredient, i) => {\n const path: Path = ['ingredients', i];\n if (!isObject(ingredient)) {\n shape(path, 'an ingredient object');\n shapeOk = false;\n return;\n }\n checkKey(ingredient.key, path, 'ingredient', i);\n if (typeof ingredient.name !== 'string') shape([...path, 'name'], 'a string');\n else if (ingredient.name.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'name'] }));\n\n const { quantity, unit } = ingredient;\n if (quantity !== undefined) {\n if (typeof quantity !== 'number') shape([...path, 'quantity'], 'a number');\n else if (!Number.isFinite(quantity) || quantity <= 0) {\n out.push(\n diagnostic(\n 'INVALID_QUANTITY',\n { quantity: String(quantity) },\n { path: [...path, 'quantity'] },\n ),\n );\n }\n }\n if (unit !== undefined) {\n if (typeof unit !== 'string') shape([...path, 'unit'], 'a string');\n else if (unit.trim() !== '') {\n if (quantity === undefined) {\n out.push(diagnostic('UNIT_WITHOUT_QUANTITY', { unit }, { path: [...path, 'unit'] }));\n }\n if (!units.has(unit.trim().toLowerCase())) {\n out.push(diagnostic('UNKNOWN_UNIT', { unit }, { path: [...path, 'unit'] }, 'warning'));\n }\n }\n }\n });\n\n // --- steps ------------------------------------------------------------------------------\n steps.forEach((step, i) => {\n const path: Path = ['steps', i];\n if (!isObject(step)) {\n shape(path, 'a step object');\n shapeOk = false;\n return;\n }\n checkKey(step.key, path, 'step', i);\n if (typeof step.text !== 'string') shape([...path, 'text'], 'a string');\n else if (step.text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'text'] }));\n if (!Array.isArray(step.inputs) || step.inputs.some((input) => typeof input !== 'string')) {\n shape([...path, 'inputs'], 'an array of keys');\n shapeOk = false;\n }\n });\n\n if (!shapeOk || steps.length === 0) return out; // drafts: no tree rules\n\n // --- tree rules -------------------------------------------------------------------------\n const consumers = new Map<string, string>();\n steps.forEach((rawStep, i) => {\n const step = rawStep as { key: string; inputs: string[] };\n const path: Path = ['steps', i];\n if (step.inputs.length === 0) {\n out.push(diagnostic('NO_INPUT', { key: String(step.key) }, { path: [...path, 'inputs'] }));\n }\n step.inputs.forEach((input, j) => {\n const inputPath: Path = [...path, 'inputs', j];\n const target = kinds.get(input);\n if (!target) {\n out.push(diagnostic('UNKNOWN_REFERENCE', { key: input }, { path: inputPath }));\n return;\n }\n if (target.kind === 'step' && target.index >= i) {\n out.push(\n diagnostic(\n 'FORWARD_REFERENCE',\n { key: input, step: String(step.key) },\n { path: inputPath },\n ),\n );\n return;\n }\n const consumer = consumers.get(input);\n if (consumer !== undefined) {\n out.push(diagnostic('MULTIPLE_CONSUMERS', { key: input, consumer }, { path: inputPath }));\n return;\n }\n consumers.set(input, String(step.key));\n });\n });\n\n ingredients.forEach((ingredient, i) => {\n const key = (ingredient as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('UNCONSUMED', { key }, { path: ['ingredients', i] }));\n }\n });\n\n const root = String((steps[steps.length - 1] as { key: unknown }).key);\n steps.slice(0, -1).forEach((step, i) => {\n const key = (step as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('MULTIPLE_ROOTS', { key, root }, { path: ['steps', i] }));\n }\n });\n\n return out;\n}\n","import { hasErrors } from './diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from './model';\nimport { validate } from './validate';\n\n/**\n * Returns a cleaned copy of a recipe. Never throws (on well-shaped input):\n *\n * - trims prerequisites, keys, names, step texts and units;\n * - drops empty units and canonicalises known ones (`KG` → `kg`);\n * - omits `quantity`/`unit` properties that are undefined (stable deep equality);\n * - when the recipe is a valid tree, reorders ingredients by depth-first\n * traversal from the final step (inputs in their given order), which makes\n * the ingredients of every step contiguous rows.\n *\n * Steps keep their given order. Drafts (no steps) and invalid recipes keep the\n * given ingredient order — call {@link validate} to know why a recipe is invalid.\n */\nexport function normalize(recipe: Recipe, options: RecipeOptions = {}): Recipe {\n const units = unitLookup(options.units);\n const prerequisites = recipe.prerequisites.map((text) => text.trim());\n const ingredients: Ingredient[] = recipe.ingredients.map((ingredient) => {\n const clean: Ingredient = { key: ingredient.key.trim(), name: ingredient.name.trim() };\n if (ingredient.quantity !== undefined) clean.quantity = ingredient.quantity;\n const unit = ingredient.unit?.trim();\n if (unit) clean.unit = units.get(unit.toLowerCase()) ?? unit;\n return clean;\n });\n const steps: Step[] = recipe.steps.map((step) => ({\n key: step.key.trim(),\n text: step.text.trim(),\n inputs: step.inputs.map((input) => input.trim()),\n }));\n const cleaned: Recipe = { prerequisites, ingredients, steps };\n\n if (steps.length === 0 || hasErrors(validate(cleaned, options))) return cleaned;\n return { ...cleaned, ingredients: treeOrder(cleaned) };\n}\n\n/** Ingredients in depth-first order from the final step. Assumes a valid tree. */\nexport function treeOrder(recipe: Recipe): Ingredient[] {\n const ingredientByKey = new Map(recipe.ingredients.map((i) => [i.key, i]));\n const stepByKey = new Map(recipe.steps.map((s) => [s.key, s]));\n const ordered: Ingredient[] = [];\n const visit = (step: Step) => {\n for (const input of step.inputs) {\n const ingredient = ingredientByKey.get(input);\n if (ingredient) ordered.push(ingredient);\n else {\n const child = stepByKey.get(input);\n if (child) visit(child);\n }\n }\n };\n const root = recipe.steps[recipe.steps.length - 1];\n if (root) visit(root);\n return ordered;\n}\n","import type { DecimalSeparator, Recipe } from './model';\n\nexport interface FormatQuantityOptions {\n decimalSeparator?: DecimalSeparator;\n /** Maximum number of decimals (default 2). Trailing zeros are removed. */\n maxDecimals?: number;\n}\n\n/**\n * Formats a quantity for display: rounded to `maxDecimals` (default 2),\n * trailing zeros removed. Positive values that would round to 0 keep two\n * significant digits instead (e.g. 0.004 → \"0.004\"), so a non-zero amount is\n * never shown as \"0\".\n */\nexport function formatQuantity(value: number, options: FormatQuantityOptions = {}): string {\n const { decimalSeparator = '.', maxDecimals = 2 } = options;\n if (!Number.isFinite(value)) return String(value);\n let text: string;\n const rounded = Number(value.toFixed(maxDecimals));\n if (rounded === 0 && value !== 0) {\n text = trimZeros(Math.abs(value) < 1e-6 ? value.toExponential(1) : value.toPrecision(2));\n } else {\n text = trimZeros(rounded.toFixed(maxDecimals));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\n/**\n * Formats a quantity losslessly (used by the serializer): no rounding, never\n * exponent notation for usual cooking magnitudes.\n */\nexport function formatExactQuantity(\n value: number,\n decimalSeparator: DecimalSeparator = '.',\n): string {\n let text = String(value);\n if (/e/i.test(text)) {\n // 1e-7 / 1e21 style: expand. Precision beyond 20 decimals is not representable in the DSL.\n text = trimZeros(value.toFixed(20));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\nfunction trimZeros(text: string): string {\n if (!text.includes('.') || /e/i.test(text)) return text;\n return text.replace(/0+$/, '').replace(/\\.$/, '');\n}\n\n/**\n * Parses a DSL quantity: integer, decimal with `.` or `,`, or simple fraction `a/b`.\n * Returns `undefined` when the text is not a quantity. May return 0 or Infinity\n * (e.g. \"0\", \"1/0\"); callers report those as INVALID_QUANTITY.\n */\nexport function parseQuantity(text: string): number | undefined {\n const fraction = /^(\\d+)\\/(\\d+)$/.exec(text);\n if (fraction) return Number(fraction[1]) / Number(fraction[2]);\n if (/^\\d+(?:[.,]\\d+)?$/.test(text)) return Number(text.replace(',', '.'));\n return undefined;\n}\n\n/** Regex source of a DSL quantity, shared by the lexer and the label parser. */\nexport const QUANTITY_SOURCE = String.raw`\\d+\\/\\d+|\\d+(?:[.,]\\d+)?`;\n\n/**\n * Multiplies every ingredient quantity by `factor` (e.g. 6/4 to go from 4 to\n * 6 servings). Quantities are not rounded; use {@link formatQuantity} to display.\n * Throws a RangeError if `factor` is not a finite number > 0.\n */\nexport function scale(recipe: Recipe, factor: number): Recipe {\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new RangeError(`scale factor must be a finite number > 0, got ${factor}`);\n }\n return {\n prerequisites: [...recipe.prerequisites],\n ingredients: recipe.ingredients.map((ingredient) =>\n ingredient.quantity === undefined\n ? { ...ingredient }\n : { ...ingredient, quantity: ingredient.quantity * factor },\n ),\n steps: recipe.steps.map((step) => ({ ...step, inputs: [...step.inputs] })),\n };\n}\n","import { parseQuantity, QUANTITY_SOURCE } from '../quantity';\n\nexport interface ParsedIngredientLabel {\n name: string;\n quantity?: number;\n unit?: string;\n /** Original quantity text, kept for diagnostics. */\n quantityText?: string;\n}\n\nconst LEADING_QUANTITY = new RegExp(String.raw`^(?:${QUANTITY_SOURCE})`);\nconst QUOTED_PREFIX = new RegExp(String.raw`^(${QUANTITY_SOURCE})(?:\\s*(\\S+))?$`);\n\n/**\n * Splits an ingredient label into quantity, unit and name.\n *\n * Unquoted `[1,5 kg Pommes de terre]`:\n * - a leading quantity (`3`, `1.5`, `1,5`, `1/2`) is taken when followed by\n * a blank, a letter or the end of the label (so `4-épices` stays a name);\n * - the next word is the unit if it is in `units` (case-insensitive, may be\n * glued: `500g`) **and** a name follows it (`[500 g]` = 500 × \"g\");\n * - the rest, trimmed, is the name.\n *\n * Quoted `[1,5 kg \"Pommes (vapeur\"]` / `[\"4 épices\"]`: the quoted string is\n * the name, verbatim; the prefix is the quantity and optional unit.\n *\n * Quantities may come out as 0 or Infinity; the caller reports them.\n *\n * @param units lower-case → canonical spelling.\n */\nexport function parseIngredientLabel(\n label: { value: string; quoted: boolean; prefix: string },\n units: ReadonlyMap<string, string>,\n): ParsedIngredientLabel {\n if (label.quoted) {\n const name = label.value.trim();\n const match = QUOTED_PREFIX.exec(label.prefix.trim());\n if (!match) return { name };\n const result: ParsedIngredientLabel = {\n name,\n quantityText: match[1]!,\n quantity: parseQuantity(match[1]!),\n };\n if (match[2] !== undefined) result.unit = units.get(match[2].toLowerCase()) ?? match[2];\n return result;\n }\n\n const text = label.value.trim();\n const quantityMatch = LEADING_QUANTITY.exec(text);\n if (!quantityMatch) return { name: text };\n const quantityText = quantityMatch[0];\n let rest = text.slice(quantityText.length);\n if (rest !== '' && !/^[\\s\\p{L}]/u.test(rest)) return { name: text };\n\n const result: ParsedIngredientLabel = {\n name: '',\n quantityText,\n quantity: parseQuantity(quantityText),\n };\n rest = rest.trimStart();\n const unitMatch = /^(\\S+)(\\s+|$)/.exec(rest);\n if (unitMatch) {\n const unit = units.get(unitMatch[1]!.toLowerCase());\n const after = rest.slice(unitMatch[0].length).trim();\n if (unit !== undefined && after !== '') {\n result.unit = unit;\n rest = after;\n }\n }\n result.name = rest.trim();\n return result;\n}\n","import { diagnostic, type Diagnostic } from '../diagnostics';\nimport { QUANTITY_SOURCE } from '../quantity';\n\n/** Position of a token on its line. Columns are 1-based UTF-16 offsets, `endColumn` exclusive. */\nexport interface Span {\n line: number;\n column: number;\n endColumn: number;\n}\n\nexport interface IdentToken extends Span {\n type: 'ident';\n value: string;\n}\n\nexport interface LabelToken extends Span {\n type: 'label';\n /** `square` = `[ingredient]`, `round` = `(step)`. */\n shape: 'square' | 'round';\n /** Label text: raw content between the delimiters, or the unescaped quoted string. */\n value: string;\n quoted: boolean;\n /** Square labels only: the `quantity [unit]` written before a quoted name (may be ''). */\n prefix: string;\n}\n\nexport interface PunctToken extends Span {\n type: 'amp' | 'arrow';\n}\n\nexport interface BeforeToken extends Span {\n type: 'before';\n text: string;\n quoted: boolean;\n}\n\nexport type Token = IdentToken | LabelToken | PunctToken | BeforeToken;\n\nexport interface LexedLine {\n line: number;\n tokens: Token[];\n /** True when the line has a lexical error; its tokens are then incomplete. */\n failed: boolean;\n}\n\nexport interface LexResult {\n lines: LexedLine[];\n diagnostics: Diagnostic[];\n}\n\nconst IDENT_CHAR = /[A-Za-z0-9_-]/;\nconst CLOSE = { square: ']', round: ')' } as const;\nconst OPEN = { square: '[', round: '(' } as const;\n\n/** Splits a document into lines (`\\n`, `\\r\\n` or `\\r`). */\nexport function splitLines(source: string): string[] {\n return source.split(/\\r\\n|\\r|\\n/);\n}\n\n/**\n * Tokenizes a whole document, line by line. Comments (`%%` outside labels and\n * quoted strings) and blank lines produce no tokens. A lexical error stops\n * the current line (marked `failed`) but not the following ones.\n *\n * @param units lower-cased unit vocabulary, used to recognise `[2 kg \"quoted name\"]`.\n */\nexport function lex(source: string, units: ReadonlySet<string>): LexResult {\n const diagnostics: Diagnostic[] = [];\n const lines = splitLines(source).map((text, i) => lexLine(text, i + 1, units, diagnostics));\n return { lines, diagnostics };\n}\n\nexport function lexLine(\n text: string,\n line: number,\n units: ReadonlySet<string>,\n diagnostics: Diagnostic[],\n): LexedLine {\n const tokens: Token[] = [];\n let pos = 0;\n const at = (offset = 0) => text[pos + offset] ?? '';\n const fail = (\n code: Parameters<typeof diagnostic>[0],\n params: Record<string, string>,\n start: number,\n end: number,\n ) => {\n diagnostics.push(\n diagnostic(code, params, { line, column: start + 1, endLine: line, endColumn: end + 1 }),\n );\n return { line, tokens, failed: true };\n };\n const skipSpaces = () => {\n while (pos < text.length && /\\s/.test(at())) pos++;\n };\n\n skipSpaces();\n\n // `before: text` statement (only at the start of a line).\n const beforeMatch = /^before\\s*:/.exec(text.slice(pos));\n if (beforeMatch) {\n const start = pos;\n pos += beforeMatch[0].length;\n skipSpaces();\n if (at() === '\"') {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) return fail('UNTERMINATED_STRING', {}, pos, text.length);\n pos = quoted.end;\n skipSpaces();\n if (pos < text.length && !text.startsWith('%%', pos)) {\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'end of line', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n tokens.push({\n type: 'before',\n text: quoted.value,\n quoted: true,\n line,\n column: start + 1,\n endColumn: quoted.end + 1,\n });\n } else {\n const comment = text.indexOf('%%', pos);\n const end = comment === -1 ? text.length : comment;\n tokens.push({\n type: 'before',\n text: text.slice(pos, end).trim(),\n quoted: false,\n line,\n column: start + 1,\n endColumn: end + 1,\n });\n }\n return { line, tokens, failed: false };\n }\n\n while (pos < text.length) {\n skipSpaces();\n if (pos >= text.length) break;\n const start = pos;\n const ch = at();\n\n if (text.startsWith('%%', pos)) break; // comment until end of line\n if (text.startsWith('-->', pos)) {\n pos += 3;\n tokens.push({ type: 'arrow', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (ch === '&') {\n pos++;\n tokens.push({ type: 'amp', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (IDENT_CHAR.test(ch)) {\n while (pos < text.length && IDENT_CHAR.test(at()) && !text.startsWith('-->', pos)) pos++;\n tokens.push({\n type: 'ident',\n value: text.slice(start, pos),\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n if (ch === '[' || ch === '(') {\n const shape = ch === '[' ? 'square' : 'round';\n const label = readLabel(text, pos, shape, units);\n if (!label.ok) return fail(label.code, label.params, label.start, label.end);\n pos = label.end;\n tokens.push({\n type: 'label',\n shape,\n value: label.value,\n quoted: label.quoted,\n prefix: label.prefix,\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'a node, \"&\" or \"-->\"', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n return { line, tokens, failed: false };\n}\n\n/** Human description of the character at `pos`, for SYNTAX_ERROR's `found` param. */\nexport function describe(text: string, pos: number): string {\n if (pos >= text.length) return 'end of line';\n return `\"${String.fromCodePoint(text.codePointAt(pos) ?? 0)}\"`;\n}\n\ntype QuotedResult = { ok: true; value: string; end: number } | { ok: false };\n\n/** Reads a `\"…\"` string starting at `start` (which must be `\"`). Escapes: `\\\"` and `\\\\`. */\nexport function readQuoted(text: string, start: number): QuotedResult {\n let value = '';\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\\\\' && (text[pos + 1] === '\"' || text[pos + 1] === '\\\\')) {\n value += text[pos + 1];\n pos += 2;\n continue;\n }\n if (ch === '\"') return { ok: true, value, end: pos + 1 };\n value += ch;\n pos++;\n }\n return { ok: false };\n}\n\ntype LabelResult =\n | { ok: true; value: string; quoted: boolean; prefix: string; end: number }\n | {\n ok: false;\n code: 'UNTERMINATED_LABEL' | 'UNTERMINATED_STRING' | 'SYNTAX_ERROR';\n params: Record<string, string>;\n start: number;\n end: number;\n };\n\nconst QUOTE_PREFIX_SOURCE = String.raw`^\\s*(?:(?:${QUANTITY_SOURCE})(?:\\s*([^\\s\"\\[\\]()]+))?\\s*)?$`;\nconst QUOTE_PREFIX = new RegExp(QUOTE_PREFIX_SOURCE);\n\n/**\n * Reads a label starting at the opening delimiter.\n *\n * - Unquoted: everything up to the matching closing delimiter; only the label's\n * own delimiter kind is counted for nesting (`[Ail (gousse)]`, `(Mix (well))`).\n * - Quoted: `(\"text\")`, `[\"name\"]`, or for ingredients `[qty unit \"name\"]` —\n * a `\"` opens a quoted string when it is the first non-blank character, or\n * (square labels) when everything before it is a quantity optionally\n * followed by a known unit. Any other `\"` is a literal character.\n */\nfunction readLabel(\n text: string,\n start: number,\n shape: 'square' | 'round',\n units: ReadonlySet<string>,\n): LabelResult {\n const open = OPEN[shape];\n const close = CLOSE[shape];\n let depth = 0;\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\"') {\n const before = text.slice(start + 1, pos);\n const opensQuote =\n before.trim() === '' ||\n (shape === 'square' && depth === 0 && isQuantityPrefix(before, units));\n if (opensQuote) {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) {\n return {\n ok: false,\n code: 'UNTERMINATED_STRING',\n params: {},\n start: pos,\n end: text.length,\n };\n }\n let end = quoted.end;\n while (end < text.length && /\\s/.test(text[end]!)) end++;\n if (text[end] !== close) {\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected: `\"${close}\"`, found: describe(text, end) },\n start: end,\n end: end + 1,\n };\n }\n return { ok: true, value: quoted.value, quoted: true, prefix: before.trim(), end: end + 1 };\n }\n }\n if (ch === open) depth++;\n if (ch === close) {\n if (depth === 0) {\n return {\n ok: true,\n value: text.slice(start + 1, pos),\n quoted: false,\n prefix: '',\n end: pos + 1,\n };\n }\n depth--;\n }\n pos++;\n }\n return { ok: false, code: 'UNTERMINATED_LABEL', params: { close }, start, end: text.length };\n}\n\nfunction isQuantityPrefix(before: string, units: ReadonlySet<string>): boolean {\n const match = QUOTE_PREFIX.exec(before);\n if (!match || before.trim() === '') return false;\n return match[1] === undefined || units.has(match[1].toLowerCase());\n}\n","import { diagnostic, hasErrors, type Diagnostic, type DiagnosticCode } from '../diagnostics';\nimport {\n MAX_KEY_LENGTH,\n unitLookup,\n type Ingredient,\n type Recipe,\n type RecipeOptions,\n type Step,\n} from '../model';\nimport { normalize } from '../normalize';\nimport { validate } from '../validate';\nimport { parseIngredientLabel } from './label';\nimport { lex, type IdentToken, type LabelToken, type Span, type Token } from './lexer';\n\nexport type ParseResult =\n | { ok: true; recipe: Recipe; diagnostics: Diagnostic[] }\n | { ok: false; diagnostics: Diagnostic[]; partial?: Recipe };\n\nexport type ParseOptions = Pick<RecipeOptions, 'units'>;\n\n/** One occurrence of a node in the text: `id` or `id[label]` / `id(label)`. */\ninterface NodeRef {\n key: string;\n ident: IdentToken;\n label?: LabelToken;\n span: Span;\n}\n\ninterface NodeInfo {\n key: string;\n /** Order of first mention in the document. */\n seq: number;\n first: NodeRef;\n def?: NodeRef;\n kind?: 'ingredient' | 'step';\n ingredient?: Ingredient;\n text?: string;\n inputs: string[];\n consumer?: string;\n}\n\ninterface Edge {\n from: NodeRef;\n to: NodeRef;\n}\n\n/**\n * Codes that make tree-structure checks (NO_INPUT, CYCLE, UNCONSUMED,\n * MULTIPLE_ROOTS) meaningless: they would only cascade. Tree rules are checked\n * once the document is syntactically valid and every reference resolves.\n */\nconst BLOCKING: ReadonlySet<DiagnosticCode> = new Set([\n 'SYNTAX_ERROR',\n 'UNTERMINATED_LABEL',\n 'UNTERMINATED_STRING',\n 'FAN_OUT',\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n]);\n\n/**\n * Parses a tabular-recipe document.\n *\n * On success, `recipe` is normalized (see {@link normalize}). On failure,\n * every diagnostic found is returned (sorted by position) together with a\n * best-effort `partial` recipe, handy for live previews.\n */\nexport function parse(source: string, options: ParseOptions = {}): ParseResult {\n const unitMap = unitLookup(options.units);\n const { lines, diagnostics } = lex(source, new Set(unitMap.keys()));\n const nodes = new Map<string, NodeInfo>();\n const prerequisites: string[] = [];\n const edges: Edge[] = [];\n const suppressed = new Set<string>(); // idents seen on failed lines: no UNDEFINED_NODE for them\n let seq = 0;\n\n const at = (span: Span) => ({\n line: span.line,\n column: span.column,\n endLine: span.line,\n endColumn: span.endColumn,\n });\n const report = (code: DiagnosticCode, params: Record<string, string | number>, span: Span) =>\n diagnostics.push(diagnostic(code, params, at(span)));\n\n const register = (ref: NodeRef): NodeInfo => {\n let info = nodes.get(ref.key);\n if (!info) {\n info = { key: ref.key, seq: seq++, first: ref, inputs: [] };\n nodes.set(ref.key, info);\n if (ref.key.length > MAX_KEY_LENGTH) report('INVALID_KEY', { key: ref.key }, ref.ident);\n }\n if (!ref.label) return info;\n if (info.def) {\n report('REDEFINED_NODE', { key: ref.key, firstLine: info.def.ident.line }, ref.ident);\n return info;\n }\n info.def = ref;\n if (ref.label.shape === 'square') {\n info.kind = 'ingredient';\n const parsed = parseIngredientLabel(ref.label, unitMap);\n const ingredient: Ingredient = { key: ref.key, name: parsed.name };\n if (parsed.quantity !== undefined) {\n if (!Number.isFinite(parsed.quantity) || parsed.quantity <= 0) {\n report('INVALID_QUANTITY', { quantity: parsed.quantityText ?? '' }, ref.label);\n } else {\n ingredient.quantity = parsed.quantity;\n if (parsed.unit !== undefined) ingredient.unit = parsed.unit;\n }\n }\n if (parsed.name === '') report('EMPTY_TEXT', {}, ref.label);\n info.ingredient = ingredient;\n } else {\n info.kind = 'step';\n info.text = ref.label.value.trim();\n if (info.text === '') report('EMPTY_TEXT', {}, ref.label);\n }\n return info;\n };\n\n for (const lexed of lines) {\n if (lexed.failed) {\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n const statement = parseStatement(lexed.tokens);\n if (!statement.ok) {\n report(statement.code, statement.params, statement.span);\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n if (statement.kind === 'empty') continue;\n if (statement.kind === 'before') {\n if (statement.token.text.trim() === '') report('EMPTY_TEXT', {}, statement.token);\n prerequisites.push(statement.token.text.trim());\n continue;\n }\n if (statement.kind === 'definition') {\n register(statement.node);\n continue;\n }\n // Edge statement: sources --> t1 --> t2 ...\n statement.sources.forEach(register);\n let previous = statement.sources;\n for (const target of statement.targets) {\n register(target);\n if (target.label?.shape === 'square') {\n report('INGREDIENT_AS_TARGET', { key: target.key }, target.span);\n } else {\n for (const from of previous) edges.push({ from, to: target });\n }\n previous = [target];\n }\n if (statement.fanOut) report('FAN_OUT', {}, statement.fanOut);\n }\n\n // --- resolution -------------------------------------------------------------------------\n for (const info of nodes.values()) {\n if (!info.def && !suppressed.has(info.key)) {\n report('UNDEFINED_NODE', { key: info.key }, info.first.ident);\n }\n }\n for (const edge of edges) {\n const from = nodes.get(edge.from.key)!;\n const to = nodes.get(edge.to.key)!;\n if (!from.def || !to.def) continue;\n if (to.kind === 'ingredient') {\n report('INGREDIENT_AS_TARGET', { key: to.key }, edge.to.span);\n continue;\n }\n if (from.consumer !== undefined) {\n report('MULTIPLE_CONSUMERS', { key: from.key, consumer: from.consumer }, edge.from.span);\n continue;\n }\n from.consumer = to.key;\n to.inputs.push(from.key);\n }\n\n const all = [...nodes.values()].filter((n) => n.def).sort((a, b) => a.seq - b.seq);\n const ingredients = all.filter((n) => n.kind === 'ingredient');\n const stepNodes = all.filter((n) => n.kind === 'step');\n const order = topologicalOrder(stepNodes, nodes);\n\n // --- tree rules ---------------------------------------------------------------------------\n const blocked = diagnostics.some((d) => BLOCKING.has(d.code));\n if (!blocked && stepNodes.length > 0) {\n for (const step of stepNodes) {\n if (step.inputs.length === 0) report('NO_INPUT', { key: step.key }, step.def!.span);\n }\n if (order.cyclic.length > 0) {\n const first = order.cyclic[0]!;\n report('CYCLE', { keys: order.cyclic.map((s) => s.key).join(', ') }, first.def!.span);\n }\n for (const ingredient of ingredients) {\n if (ingredient.consumer === undefined) {\n report('UNCONSUMED', { key: ingredient.key }, ingredient.def!.span);\n }\n }\n const roots = order.sorted.filter((s) => s.consumer === undefined);\n const root = roots[roots.length - 1];\n for (const extra of roots.slice(0, -1)) {\n report('MULTIPLE_ROOTS', { key: extra.key, root: root!.key }, extra.def!.span);\n }\n }\n\n const recipe: Recipe = {\n prerequisites,\n ingredients: ingredients.map((n) => n.ingredient!),\n steps: [...order.sorted, ...order.cyclic].map((n): Step => ({\n key: n.key,\n text: n.text!,\n inputs: n.inputs,\n })),\n };\n\n diagnostics.sort((a, b) => (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0));\n if (hasErrors(diagnostics)) {\n return { ok: false, diagnostics, partial: normalize(recipe, options) };\n }\n const normalized = normalize(recipe, options);\n // Safety net: the DSL checks above mirror validate(); anything left is a bug, but report it.\n const leftovers = validate(normalized, options).filter((d) => d.severity === 'error');\n if (leftovers.length > 0) return { ok: false, diagnostics: leftovers, partial: normalized };\n return { ok: true, recipe: normalized, diagnostics: [] };\n}\n\n/**\n * Stable topological order of steps: a step comes after the steps it consumes;\n * ties are broken by order of first mention. Steps on a cycle are returned apart.\n */\nfunction topologicalOrder(steps: NodeInfo[], nodes: Map<string, NodeInfo>) {\n const pending = new Map<string, number>();\n for (const step of steps) {\n pending.set(step.key, step.inputs.filter((input) => nodes.get(input)?.kind === 'step').length);\n }\n const sorted: NodeInfo[] = [];\n const done = new Set<string>();\n // Steps are few (< 100): a simple repeated scan keeps the tie-breaking obvious.\n for (;;) {\n const next = steps.find((s) => !done.has(s.key) && pending.get(s.key) === 0);\n if (!next) break;\n done.add(next.key);\n sorted.push(next);\n if (next.consumer !== undefined && pending.has(next.consumer)) {\n pending.set(next.consumer, pending.get(next.consumer)! - 1);\n }\n }\n return { sorted, cyclic: steps.filter((s) => !done.has(s.key)) };\n}\n\n// --- statements -----------------------------------------------------------------------------\n\ntype Statement =\n | { ok: true; kind: 'empty' }\n | { ok: true; kind: 'before'; token: Extract<Token, { type: 'before' }> }\n | { ok: true; kind: 'definition'; node: NodeRef }\n | { ok: true; kind: 'edges'; sources: NodeRef[]; targets: NodeRef[]; fanOut?: Span }\n | { ok: false; code: DiagnosticCode; params: Record<string, string>; span: Span };\n\nfunction found(token: Token | undefined): string {\n if (!token) return 'end of line';\n switch (token.type) {\n case 'ident':\n return `\"${token.value}\"`;\n case 'label':\n return token.shape === 'square' ? '\"[\"' : '\"(\"';\n case 'amp':\n return '\"&\"';\n case 'arrow':\n return '\"-->\"';\n case 'before':\n return '\"before:\"';\n }\n}\n\n/**\n * Grammar of one line (tokens already lexed):\n *\n * statement = before | definition | edges\n * definition = node-def\n * edges = node ( \"&\" node )* ( \"-->\" node )+\n * node = IDENT [ LABEL ]\n */\nfunction parseStatement(tokens: Token[]): Statement {\n if (tokens.length === 0) return { ok: true, kind: 'empty' };\n const first = tokens[0]!;\n if (first.type === 'before') return { ok: true, kind: 'before', token: first };\n\n let pos = 0;\n const lastSpan = tokens[tokens.length - 1]!;\n const eol: Span = {\n line: lastSpan.line,\n column: lastSpan.endColumn,\n endColumn: lastSpan.endColumn + 1,\n };\n const error = (expected: string): Statement => {\n const token = tokens[pos];\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected, found: found(token) },\n span: token ?? eol,\n };\n };\n const readNode = (): NodeRef | undefined => {\n const ident = tokens[pos];\n if (ident?.type !== 'ident') return undefined;\n pos++;\n const label = tokens[pos];\n if (label?.type === 'label') {\n pos++;\n return {\n key: ident.value,\n ident,\n label,\n span: { line: ident.line, column: ident.column, endColumn: label.endColumn },\n };\n }\n return { key: ident.value, ident, span: ident };\n };\n\n const sources: NodeRef[] = [];\n const firstNode = readNode();\n if (!firstNode) return error('a node id');\n sources.push(firstNode);\n while (tokens[pos]?.type === 'amp') {\n pos++;\n const node = readNode();\n if (!node) return error('a node id after \"&\"');\n sources.push(node);\n }\n\n if (pos === tokens.length) {\n if (sources.length === 1 && firstNode.label)\n return { ok: true, kind: 'definition', node: firstNode };\n return error(sources.length === 1 ? '\"[\", \"(\" or \"-->\"' : '\"-->\"');\n }\n\n const targets: NodeRef[] = [];\n while (tokens[pos]?.type === 'arrow') {\n pos++;\n const node = readNode();\n if (!node) return error('a step after \"-->\"');\n targets.push(node);\n }\n if (targets.length === 0) return error('\"&\" or \"-->\"');\n if (pos === tokens.length) return { ok: true, kind: 'edges', sources, targets };\n const extra = tokens[pos]!;\n if (extra.type === 'amp') return { ok: true, kind: 'edges', sources, targets, fanOut: extra };\n return error('\"-->\" or end of line');\n}\n","import type { Diagnostic } from '../diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from '../model';\nimport { normalize } from '../normalize';\nimport { formatExactQuantity } from '../quantity';\nimport { parseIngredientLabel } from './label';\nimport { lexLine } from './lexer';\n\nexport type SerializeOptions = RecipeOptions;\n\n/**\n * Writes a recipe in canonical DSL form:\n *\n * 1. `before: …` lines, then a blank line if anything follows;\n * 2. drafts (no steps): one ingredient definition per line;\n * 3. otherwise one line per step, in step order: `inputs --> key(text)`,\n * each ingredient defined inline at its use, earlier steps as bare ids.\n * A step whose single input is the step just before it is appended to\n * that line as a chain: `… --> a(x) --> b(y)`.\n *\n * Labels are quoted only when needed (the serializer checks that each label\n * reads back identically). Quantities are written losslessly with\n * `decimalSeparator` (default `.`). The recipe is normalized first, so\n * `parse(serialize(r)).recipe` deep-equals `normalize(r)` for every valid r.\n *\n * Invalid recipes are still written, best effort (unused ingredients become\n * standalone definitions at the end), so an editor can switch to text mode\n * and let the parser point at the problems.\n */\nexport function serialize(recipe: Recipe, options: SerializeOptions = {}): string {\n const r = normalize(recipe, options);\n const separator = options.decimalSeparator ?? '.';\n const unitSet = new Set(unitLookup(options.units).keys());\n const unitMap = unitLookup(options.units);\n const lines: string[] = r.prerequisites.map((text) => `before: ${prerequisiteText(text)}`);\n const body: string[] = [];\n\n const ingredientByKey = new Map(r.ingredients.map((i) => [i.key, i]));\n const defined = new Set<string>();\n const ingredientRef = (ingredient: Ingredient) => {\n if (defined.has(ingredient.key)) return ingredient.key;\n defined.add(ingredient.key);\n return `${ingredient.key}[${ingredientLabel(ingredient, separator, unitSet, unitMap)}]`;\n };\n const stepRef = (step: Step) => `${step.key}(${stepLabel(step.text, unitSet)})`;\n const inputRef = (key: string) => {\n const ingredient = ingredientByKey.get(key);\n return ingredient ? ingredientRef(ingredient) : key;\n };\n\n for (let i = 0; i < r.steps.length; i++) {\n let step = r.steps[i]!;\n let line =\n step.inputs.length > 0\n ? `${step.inputs.map(inputRef).join(' & ')} --> ${stepRef(step)}`\n : stepRef(step);\n let next = r.steps[i + 1];\n while (next && next.inputs.length === 1 && next.inputs[0] === step.key) {\n line += ` --> ${stepRef(next)}`;\n step = next;\n i++;\n next = r.steps[i + 1];\n }\n body.push(line);\n }\n for (const ingredient of r.ingredients) {\n if (!defined.has(ingredient.key)) body.push(ingredientRef(ingredient));\n }\n\n if (lines.length > 0 && body.length > 0) lines.push('');\n lines.push(...body);\n return lines.length > 0 ? `${lines.join('\\n')}\\n` : '';\n}\n\nfunction quote(text: string): string {\n return `\"${text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\nfunction prerequisiteText(text: string): string {\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(`before: ${text}`, 1, new Set(), diagnostics);\n const token = lexed.tokens[0];\n const readsBack = diagnostics.length === 0 && token?.type === 'before' && token.text === text;\n return readsBack && text !== '' ? text : quote(text);\n}\n\n/** Lexes `x<open>content<close>` and returns the label token if it reads back cleanly. */\nfunction relex(content: string, shape: 'square' | 'round', units: ReadonlySet<string>) {\n const [open, close] = shape === 'square' ? ['[', ']'] : ['(', ')'];\n const source = `x${open}${content}${close}`;\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(source, 1, units, diagnostics);\n const label = lexed.tokens[1];\n if (diagnostics.length > 0 || lexed.tokens.length !== 2 || label?.type !== 'label')\n return undefined;\n return label.endColumn === source.length + 1 ? label : undefined;\n}\n\nfunction stepLabel(text: string, units: ReadonlySet<string>): string {\n const label = relex(text, 'round', units);\n return label && !label.quoted && label.value.trim() === text && text !== '' ? text : quote(text);\n}\n\nfunction ingredientLabel(\n ingredient: Ingredient,\n separator: '.' | ',',\n units: ReadonlySet<string>,\n unitMap: ReadonlyMap<string, string>,\n): string {\n const prefix =\n ingredient.quantity === undefined\n ? ''\n : [formatExactQuantity(ingredient.quantity, separator), ingredient.unit]\n .filter((part) => part !== undefined)\n .join(' ');\n const raw = [prefix, ingredient.name].filter((part) => part !== '').join(' ');\n const label = relex(raw, 'square', units);\n if (label && !label.quoted) {\n const parsed = parseIngredientLabel(label, unitMap);\n if (\n parsed.name === ingredient.name &&\n parsed.quantity === ingredient.quantity &&\n parsed.unit === ingredient.unit &&\n ingredient.name !== ''\n ) {\n return raw;\n }\n }\n return prefix === '' ? quote(ingredient.name) : `${prefix} ${quote(ingredient.name)}`;\n}\n","import { hasErrors } from './diagnostics';\nimport type { Recipe, RecipeOptions, Step } from './model';\nimport { normalize } from './normalize';\nimport { formatQuantity } from './quantity';\nimport { validate } from './validate';\n\nexport type CellKind = 'quantity' | 'ingredient' | 'step';\n\nexport interface Cell {\n kind: CellKind;\n /** Ingredient key (quantity / ingredient cells) or step key. */\n key: string;\n text: string;\n /** 0-based grid position of the top-left corner. */\n row: number;\n col: number;\n rowSpan: number;\n colSpan: number;\n /** True for the final step (rightmost column). */\n final?: boolean;\n}\n\nexport interface Layout {\n /** False when the recipe is not a valid tree: only ingredients are laid out (draft layout). */\n valid: boolean;\n /** 2 (quantity + ingredient) + depth of the final step. */\n columnCount: number;\n /** Prerequisites, rendered as full-width rows above the table. */\n banners: string[];\n /** `rows[r]` = cells that *start* on row r, left to right (HTML table semantics). */\n rows: Cell[][];\n}\n\nexport interface LayoutOptions extends RecipeOptions {\n /** Max decimals of displayed quantities (default 2). */\n maxDecimals?: number;\n /** Display names for units, e.g. `{ pincee: 'pincée', cs: 'c. à s.' }`. Unlisted units show as-is. */\n unitLabels?: Readonly<Record<string, string>>;\n}\n\n/**\n * Computes the tabular grid of a recipe.\n *\n * - Column 0 = quantity, column 1 = ingredient, columns 2… = steps by depth:\n * depth(ingredient) = 0, depth(step) = 1 + max depth of its inputs,\n * step column = 1 + depth. The final step is alone in the last column.\n * - One row per ingredient (normalized depth-first order). A step spans the\n * rows of all the ingredients under it.\n * - No empty cells: a step or ingredient whose consumer sits more than one\n * column to the right stretches (`colSpan`) up to that consumer.\n *\n * Drafts (and invalid recipes, with `valid: false`) get a 2-column layout.\n */\nexport function layout(recipe: Recipe, options: LayoutOptions = {}): Layout {\n const r = normalize(recipe, options);\n const valid = !hasErrors(validate(r, options));\n const quantityText = (quantity?: number, unit?: string) =>\n quantity === undefined\n ? ''\n : [\n formatQuantity(quantity, {\n decimalSeparator: options.decimalSeparator,\n maxDecimals: options.maxDecimals,\n }),\n unit === undefined ? undefined : (options.unitLabels?.[unit] ?? unit),\n ]\n .filter(Boolean)\n .join(' ');\n\n const useTree = valid && r.steps.length > 0;\n const stepByKey = new Map(r.steps.map((s) => [s.key, s]));\n const depth = new Map<string, number>();\n const consumer = new Map<string, Step>();\n const leaves = new Map<string, string[]>(); // step key → ingredient keys under it, in row order\n if (useTree) {\n for (const step of r.steps) {\n // Steps are ordered so that inputs come first: one pass is enough.\n let d = 0;\n const under: string[] = [];\n for (const input of step.inputs) {\n consumer.set(input, step);\n const child = stepByKey.get(input);\n d = Math.max(d, child ? depth.get(input)! : 0);\n under.push(...(child ? leaves.get(input)! : [input]));\n }\n depth.set(step.key, d + 1);\n leaves.set(step.key, under);\n }\n }\n\n const root = r.steps[r.steps.length - 1];\n const columnCount = useTree && root ? 2 + depth.get(root.key)! : 2;\n const columnOf = (stepKey: string) => 1 + depth.get(stepKey)!;\n const rowOf = new Map(r.ingredients.map((ingredient, i) => [ingredient.key, i]));\n\n const rows: Cell[][] = r.ingredients.map((ingredient, row) => {\n const parent = consumer.get(ingredient.key);\n return [\n {\n kind: 'quantity',\n key: ingredient.key,\n text: quantityText(ingredient.quantity, ingredient.unit),\n row,\n col: 0,\n rowSpan: 1,\n colSpan: 1,\n },\n {\n kind: 'ingredient',\n key: ingredient.key,\n text: ingredient.name,\n row,\n col: 1,\n rowSpan: 1,\n colSpan: parent ? columnOf(parent.key) - 1 : 1,\n },\n ];\n });\n\n if (useTree) {\n for (const step of r.steps) {\n const under = leaves.get(step.key)!;\n const row = rowOf.get(under[0]!)!;\n const col = columnOf(step.key);\n const parent = consumer.get(step.key);\n rows[row]!.push({\n kind: 'step',\n key: step.key,\n text: step.text,\n row,\n col,\n rowSpan: under.length,\n colSpan: parent ? columnOf(parent.key) - col : 1,\n ...(parent ? {} : { final: true }),\n });\n }\n for (const cells of rows) cells.sort((a, b) => a.col - b.col);\n }\n\n return { valid, columnCount, banners: [...r.prerequisites], rows };\n}\n","import type { Recipe } from './model';\nimport { layout, type Cell, type LayoutOptions } from './layout';\n\nexport interface RenderHtmlOptions extends LayoutOptions {\n /** CSS class prefix (default `trd`). Must match the stylesheet you use. */\n classPrefix?: string;\n /** Keep the quantity and ingredient columns visible while scrolling horizontally. */\n stickyIngredients?: boolean;\n /** Optional `<caption>` text (e.g. the recipe name). */\n caption?: string;\n}\n\nconst ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\n\n/** Escapes text for HTML element content and attribute values. */\nexport function escapeHtml(text: string): string {\n return text.replace(/[&<>\"']/g, (ch) => ESCAPES[ch]!);\n}\n\n/**\n * Renders a recipe as a standalone HTML table (string), framework-free.\n * All text is escaped. Pair it with `ln-tabular-recipes-dsl/style.css`, or\n * style the classes yourself:\n *\n * `{p}-wrap` (scroll container), `{p}-table` (+ `{p}-sticky`, `{p}-invalid`),\n * `{p}-banner`, `{p}-qty`, `{p}-ingredient`, `{p}-step` (+ `{p}-final`).\n * Cells carry `data-key` with the ingredient/step key.\n */\nexport function renderHtml(recipe: Recipe, options: RenderHtmlOptions = {}): string {\n const p = options.classPrefix ?? 'trd';\n const grid = layout(recipe, options);\n const tableClasses = [`${p}-table`];\n if (options.stickyIngredients) tableClasses.push(`${p}-sticky`);\n if (!grid.valid) tableClasses.push(`${p}-invalid`);\n\n const cellHtml = (cell: Cell) => {\n const classes = [`${p}-${cell.kind === 'quantity' ? 'qty' : cell.kind}`];\n if (cell.final) classes.push(`${p}-final`);\n const attrs = [`class=\"${classes.join(' ')}\"`, `data-key=\"${escapeHtml(cell.key)}\"`];\n if (cell.rowSpan > 1) attrs.push(`rowspan=\"${cell.rowSpan}\"`);\n if (cell.colSpan > 1) attrs.push(`colspan=\"${cell.colSpan}\"`);\n return `<td ${attrs.join(' ')}>${escapeHtml(cell.text)}</td>`;\n };\n\n const parts: string[] = [`<div class=\"${p}-wrap\">`, `<table class=\"${tableClasses.join(' ')}\">`];\n if (options.caption !== undefined)\n parts.push(`<caption>${escapeHtml(options.caption)}</caption>`);\n parts.push('<tbody>');\n for (const banner of grid.banners) {\n parts.push(\n `<tr class=\"${p}-banner-row\"><td class=\"${p}-banner\" colspan=\"${grid.columnCount}\">${escapeHtml(banner)}</td></tr>`,\n );\n }\n for (const cells of grid.rows) parts.push(`<tr>${cells.map(cellHtml).join('')}</tr>`);\n parts.push('</tbody>', '</table>', '</div>');\n return parts.join('\\n');\n}\n"],"mappings":"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","names":["diagnostic","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/model.ts","../src/diagnostics.ts","../src/validate.ts","../src/normalize.ts","../src/quantity.ts","../src/dsl/label.ts","../src/dsl/lexer.ts","../src/dsl/parser.ts","../src/dsl/serializer.ts","../src/layout.ts","../src/html.ts"],"sourcesContent":["export type { Recipe, Ingredient, Step, RecipeOptions, DecimalSeparator } from './model';\nexport { DEFAULT_UNITS, KEY_PATTERN, MAX_KEY_LENGTH } from './model';\n\nexport type { Diagnostic, DiagnosticCode, DiagnosticParams, Severity } from './diagnostics';\nexport {\n DIAGNOSTIC_CODES,\n DIAGNOSTIC_MESSAGES,\n formatDiagnostic,\n hasErrors,\n interpolate,\n} from './diagnostics';\n\nexport { validate } from './validate';\nexport { normalize } from './normalize';\n\nexport type { ParseResult, ParseOptions } from './dsl/parser';\nexport { parse } from './dsl/parser';\nexport type { SerializeOptions } from './dsl/serializer';\nexport { serialize } from './dsl/serializer';\n\nexport type { Cell, CellKind, Layout, LayoutOptions } from './layout';\nexport { layout } from './layout';\nexport type { RenderHtmlOptions } from './html';\nexport { renderHtml, escapeHtml } from './html';\n\nexport type { FormatQuantityOptions } from './quantity';\nexport { formatQuantity, scale } from './quantity';\n","/**\n * Core data model of a tabular recipe.\n *\n * A recipe is a tree: ingredients are leaves, steps are nodes and the last\n * step is the root (\"final step\"). `prerequisites` are steps that use no\n * ingredient (e.g. \"Preheat the oven\"); they are rendered as full-width\n * banners above the table.\n */\nexport interface Recipe {\n prerequisites: string[];\n ingredients: Ingredient[];\n steps: Step[];\n}\n\nexport interface Ingredient {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n name: string;\n /** Finite, strictly positive. */\n quantity?: number;\n /** Only meaningful together with `quantity`. */\n unit?: string;\n}\n\nexport interface Step {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n text: string;\n /** Keys of the ingredients and/or earlier steps this step combines, in row order. */\n inputs: string[];\n}\n\n/** Allowed node keys: 1 to 40 ASCII letters, digits, `_` or `-`. */\nexport const KEY_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;\n\nexport const MAX_KEY_LENGTH = 40;\n\n/** Default unit vocabulary. Matching is case-insensitive; the spelling below is canonical. */\nexport const DEFAULT_UNITS: readonly string[] = [\n 'pcs',\n 'g',\n 'kg',\n 'ml',\n 'cl',\n 'l',\n 'cs',\n 'cc',\n 'pincee',\n];\n\nexport type DecimalSeparator = '.' | ',';\n\n/** Options shared by most functions of the library. */\nexport interface RecipeOptions {\n /** Unit vocabulary (case-insensitive, canonical spelling taken from this list). */\n units?: readonly string[];\n /** Decimal separator used when *writing* quantities. Parsing always accepts both. Default `.`. */\n decimalSeparator?: DecimalSeparator;\n}\n\n/** Builds a lower-case → canonical spelling lookup for a unit list. */\nexport function unitLookup(units: readonly string[] = DEFAULT_UNITS): Map<string, string> {\n const map = new Map<string, string>();\n for (const unit of units) {\n const lower = unit.toLowerCase();\n if (!map.has(lower)) map.set(lower, unit);\n }\n return map;\n}\n","/**\n * Diagnostics are language-neutral: `code` + `params` identify the problem,\n * `message` is an English default. Consumers translate by providing their own\n * templates to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_CODES = [\n // --- DSL syntax ---\n 'SYNTAX_ERROR',\n 'UNTERMINATED_STRING',\n 'UNTERMINATED_LABEL',\n 'FAN_OUT',\n // --- DSL references ---\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n 'CYCLE',\n // --- JSON model ---\n 'INVALID_SHAPE',\n 'UNKNOWN_REFERENCE',\n 'FORWARD_REFERENCE',\n // --- shared ---\n 'INVALID_KEY',\n 'DUPLICATE_KEY',\n 'EMPTY_TEXT',\n 'INVALID_QUANTITY',\n 'UNIT_WITHOUT_QUANTITY',\n 'UNKNOWN_UNIT',\n // --- tree rules ---\n 'NO_INPUT',\n 'MULTIPLE_CONSUMERS',\n 'UNCONSUMED',\n 'MULTIPLE_ROOTS',\n] as const;\n\nexport type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];\n\nexport type Severity = 'error' | 'warning';\n\nexport type DiagnosticParams = Record<string, string | number>;\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n /** English default message, built from {@link DIAGNOSTIC_MESSAGES}. */\n message: string;\n severity: Severity;\n /** 1-based. DSL diagnostics only. */\n line?: number;\n /** 1-based, in UTF-16 code units (same as JS string indices / CodeMirror). */\n column?: number;\n endLine?: number;\n /** 1-based, exclusive. */\n endColumn?: number;\n /** JSON path of the offending value. Model (JSON) diagnostics only. */\n path?: (string | number)[];\n params?: DiagnosticParams;\n}\n\n/**\n * English message templates. `{name}` placeholders are replaced by `params.name`.\n * Copy this object, translate the values and pass it to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_MESSAGES: Record<DiagnosticCode, string> = {\n SYNTAX_ERROR: 'Syntax error: expected {expected}, found {found}.',\n UNTERMINATED_STRING: 'Unterminated quoted string.',\n UNTERMINATED_LABEL: 'Missing closing \"{close}\".',\n FAN_OUT: 'A node can only feed one step: \"&\" is not allowed after \"-->\".',\n UNDEFINED_NODE: 'Node \"{key}\" is used but never defined.',\n REDEFINED_NODE: 'Node \"{key}\" is already defined on line {firstLine}.',\n INGREDIENT_AS_TARGET: 'Ingredient \"{key}\" cannot be the target of \"-->\"; only steps can.',\n CYCLE: 'Steps form a cycle: {keys}.',\n INVALID_SHAPE: 'Invalid value: expected {expected}.',\n UNKNOWN_REFERENCE: 'Input \"{key}\" does not match any ingredient or step.',\n FORWARD_REFERENCE: 'Input \"{key}\" refers to a step that does not come before \"{step}\".',\n INVALID_KEY: 'Invalid key \"{key}\": use 1 to 40 letters, digits, \"_\" or \"-\".',\n DUPLICATE_KEY: 'Key \"{key}\" is used more than once.',\n EMPTY_TEXT: 'Text must not be empty.',\n INVALID_QUANTITY: 'Invalid quantity \"{quantity}\": it must be a number greater than 0.',\n UNIT_WITHOUT_QUANTITY: 'Unit \"{unit}\" is set without a quantity.',\n UNKNOWN_UNIT: 'Unknown unit \"{unit}\".',\n NO_INPUT: 'Step \"{key}\" has no input.',\n MULTIPLE_CONSUMERS:\n '\"{key}\" is already used by step \"{consumer}\"; each node feeds exactly one step.',\n UNCONSUMED: 'Ingredient \"{key}\" is not used by any step.',\n MULTIPLE_ROOTS:\n 'Step \"{key}\" is not used by any later step; only the final step \"{root}\" may be unused.',\n};\n\n/** Replaces `{name}` placeholders of `template` with `params`. Unknown placeholders are kept. */\nexport function interpolate(template: string, params: DiagnosticParams = {}): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match,\n );\n}\n\n/** Formats a diagnostic with custom (e.g. translated) templates, falling back to English. */\nexport function formatDiagnostic(\n diagnostic: Pick<Diagnostic, 'code' | 'params'>,\n templates: Partial<Record<DiagnosticCode, string>> = DIAGNOSTIC_MESSAGES,\n): string {\n const template = templates[diagnostic.code] ?? DIAGNOSTIC_MESSAGES[diagnostic.code];\n return interpolate(template, diagnostic.params);\n}\n\n/** Internal helper to build a diagnostic with its English message. */\nexport function diagnostic(\n code: DiagnosticCode,\n params: DiagnosticParams,\n location: Omit<Diagnostic, 'code' | 'message' | 'severity' | 'params'> = {},\n severity: Severity = 'error',\n): Diagnostic {\n return {\n code,\n message: interpolate(DIAGNOSTIC_MESSAGES[code], params),\n severity,\n ...location,\n params,\n };\n}\n\nexport function hasErrors(diagnostics: readonly Diagnostic[]): boolean {\n return diagnostics.some((d) => d.severity === 'error');\n}\n","import { diagnostic, type Diagnostic } from './diagnostics';\nimport { KEY_PATTERN, unitLookup, type Recipe, type RecipeOptions } from './model';\n\ntype Path = (string | number)[];\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Validates a recipe given as JSON (e.g. received by an API). Returns every\n * problem found, each with a `path` into the input. An empty array (or only\n * warnings) means the recipe is valid.\n *\n * Accepts `unknown` so it can be used on untrusted input: shape problems are\n * reported as INVALID_SHAPE instead of throwing.\n */\nexport function validate(recipe: Recipe | unknown, options: RecipeOptions = {}): Diagnostic[] {\n const out: Diagnostic[] = [];\n const shape = (path: Path, expected: string) =>\n out.push(diagnostic('INVALID_SHAPE', { expected }, { path }));\n\n if (!isObject(recipe)) {\n shape([], 'an object with prerequisites, ingredients and steps');\n return out;\n }\n const { prerequisites, ingredients, steps } = recipe;\n if (!Array.isArray(prerequisites)) shape(['prerequisites'], 'an array of strings');\n if (!Array.isArray(ingredients)) shape(['ingredients'], 'an array');\n if (!Array.isArray(steps)) shape(['steps'], 'an array');\n if (!Array.isArray(prerequisites) || !Array.isArray(ingredients) || !Array.isArray(steps)) {\n return out;\n }\n\n prerequisites.forEach((text, i) => {\n if (typeof text !== 'string') shape(['prerequisites', i], 'a string');\n else if (text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: ['prerequisites', i] }));\n });\n\n // --- keys -------------------------------------------------------------------------------\n const kinds = new Map<string, { kind: 'ingredient' | 'step'; index: number }>();\n const checkKey = (key: unknown, path: Path, kind: 'ingredient' | 'step', index: number) => {\n if (typeof key !== 'string') {\n shape([...path, 'key'], 'a string');\n return;\n }\n if (!KEY_PATTERN.test(key)) {\n out.push(diagnostic('INVALID_KEY', { key }, { path: [...path, 'key'] }));\n }\n if (kinds.has(key)) {\n out.push(diagnostic('DUPLICATE_KEY', { key }, { path: [...path, 'key'] }));\n } else {\n kinds.set(key, { kind, index });\n }\n };\n\n // --- ingredients ------------------------------------------------------------------------\n const units = unitLookup(options.units);\n let shapeOk = true;\n ingredients.forEach((ingredient, i) => {\n const path: Path = ['ingredients', i];\n if (!isObject(ingredient)) {\n shape(path, 'an ingredient object');\n shapeOk = false;\n return;\n }\n checkKey(ingredient.key, path, 'ingredient', i);\n if (typeof ingredient.name !== 'string') shape([...path, 'name'], 'a string');\n else if (ingredient.name.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'name'] }));\n\n const { quantity, unit } = ingredient;\n if (quantity !== undefined) {\n if (typeof quantity !== 'number') shape([...path, 'quantity'], 'a number');\n else if (!Number.isFinite(quantity) || quantity <= 0) {\n out.push(\n diagnostic(\n 'INVALID_QUANTITY',\n { quantity: String(quantity) },\n { path: [...path, 'quantity'] },\n ),\n );\n }\n }\n if (unit !== undefined) {\n if (typeof unit !== 'string') shape([...path, 'unit'], 'a string');\n else if (unit.trim() !== '') {\n if (quantity === undefined) {\n out.push(diagnostic('UNIT_WITHOUT_QUANTITY', { unit }, { path: [...path, 'unit'] }));\n }\n if (!units.has(unit.trim().toLowerCase())) {\n out.push(diagnostic('UNKNOWN_UNIT', { unit }, { path: [...path, 'unit'] }, 'warning'));\n }\n }\n }\n });\n\n // --- steps ------------------------------------------------------------------------------\n steps.forEach((step, i) => {\n const path: Path = ['steps', i];\n if (!isObject(step)) {\n shape(path, 'a step object');\n shapeOk = false;\n return;\n }\n checkKey(step.key, path, 'step', i);\n if (typeof step.text !== 'string') shape([...path, 'text'], 'a string');\n else if (step.text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'text'] }));\n if (!Array.isArray(step.inputs) || step.inputs.some((input) => typeof input !== 'string')) {\n shape([...path, 'inputs'], 'an array of keys');\n shapeOk = false;\n }\n });\n\n if (!shapeOk || steps.length === 0) return out; // drafts: no tree rules\n\n // --- tree rules -------------------------------------------------------------------------\n const consumers = new Map<string, string>();\n steps.forEach((rawStep, i) => {\n const step = rawStep as { key: string; inputs: string[] };\n const path: Path = ['steps', i];\n if (step.inputs.length === 0) {\n out.push(diagnostic('NO_INPUT', { key: String(step.key) }, { path: [...path, 'inputs'] }));\n }\n step.inputs.forEach((input, j) => {\n const inputPath: Path = [...path, 'inputs', j];\n const target = kinds.get(input);\n if (!target) {\n out.push(diagnostic('UNKNOWN_REFERENCE', { key: input }, { path: inputPath }));\n return;\n }\n if (target.kind === 'step' && target.index >= i) {\n out.push(\n diagnostic(\n 'FORWARD_REFERENCE',\n { key: input, step: String(step.key) },\n { path: inputPath },\n ),\n );\n return;\n }\n const consumer = consumers.get(input);\n if (consumer !== undefined) {\n out.push(diagnostic('MULTIPLE_CONSUMERS', { key: input, consumer }, { path: inputPath }));\n return;\n }\n consumers.set(input, String(step.key));\n });\n });\n\n ingredients.forEach((ingredient, i) => {\n const key = (ingredient as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('UNCONSUMED', { key }, { path: ['ingredients', i] }));\n }\n });\n\n const root = String((steps[steps.length - 1] as { key: unknown }).key);\n steps.slice(0, -1).forEach((step, i) => {\n const key = (step as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('MULTIPLE_ROOTS', { key, root }, { path: ['steps', i] }));\n }\n });\n\n return out;\n}\n","import { hasErrors } from './diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from './model';\nimport { validate } from './validate';\n\n/**\n * Returns a cleaned copy of a recipe. Never throws (on well-shaped input):\n *\n * - trims prerequisites, keys, names, step texts and units;\n * - drops empty units and canonicalises known ones (`KG` → `kg`);\n * - omits `quantity`/`unit` properties that are undefined (stable deep equality);\n * - when the recipe is a valid tree, reorders ingredients by depth-first\n * traversal from the final step (inputs in their given order), which makes\n * the ingredients of every step contiguous rows.\n *\n * Steps keep their given order. Drafts (no steps) and invalid recipes keep the\n * given ingredient order — call {@link validate} to know why a recipe is invalid.\n */\nexport function normalize(recipe: Recipe, options: RecipeOptions = {}): Recipe {\n const units = unitLookup(options.units);\n const prerequisites = recipe.prerequisites.map((text) => text.trim());\n const ingredients: Ingredient[] = recipe.ingredients.map((ingredient) => {\n const clean: Ingredient = { key: ingredient.key.trim(), name: ingredient.name.trim() };\n if (ingredient.quantity !== undefined) clean.quantity = ingredient.quantity;\n const unit = ingredient.unit?.trim();\n if (unit) clean.unit = units.get(unit.toLowerCase()) ?? unit;\n return clean;\n });\n const steps: Step[] = recipe.steps.map((step) => ({\n key: step.key.trim(),\n text: step.text.trim(),\n inputs: step.inputs.map((input) => input.trim()),\n }));\n const cleaned: Recipe = { prerequisites, ingredients, steps };\n\n if (steps.length === 0 || hasErrors(validate(cleaned, options))) return cleaned;\n return { ...cleaned, ingredients: treeOrder(cleaned) };\n}\n\n/** Ingredients in depth-first order from the final step. Assumes a valid tree. */\nexport function treeOrder(recipe: Recipe): Ingredient[] {\n const ingredientByKey = new Map(recipe.ingredients.map((i) => [i.key, i]));\n const stepByKey = new Map(recipe.steps.map((s) => [s.key, s]));\n const ordered: Ingredient[] = [];\n const visit = (step: Step) => {\n for (const input of step.inputs) {\n const ingredient = ingredientByKey.get(input);\n if (ingredient) ordered.push(ingredient);\n else {\n const child = stepByKey.get(input);\n if (child) visit(child);\n }\n }\n };\n const root = recipe.steps[recipe.steps.length - 1];\n if (root) visit(root);\n return ordered;\n}\n","import type { DecimalSeparator, Recipe } from './model';\n\nexport interface FormatQuantityOptions {\n decimalSeparator?: DecimalSeparator;\n /** Maximum number of decimals (default 2). Trailing zeros are removed. */\n maxDecimals?: number;\n}\n\n/**\n * Formats a quantity for display: rounded to `maxDecimals` (default 2),\n * trailing zeros removed. Positive values that would round to 0 keep two\n * significant digits instead (e.g. 0.004 → \"0.004\"), so a non-zero amount is\n * never shown as \"0\".\n */\nexport function formatQuantity(value: number, options: FormatQuantityOptions = {}): string {\n const { decimalSeparator = '.', maxDecimals = 2 } = options;\n if (!Number.isFinite(value)) return String(value);\n let text: string;\n const rounded = Number(value.toFixed(maxDecimals));\n if (rounded === 0 && value !== 0) {\n text = trimZeros(Math.abs(value) < 1e-6 ? value.toExponential(1) : value.toPrecision(2));\n } else {\n text = trimZeros(rounded.toFixed(maxDecimals));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\n/**\n * Formats a quantity losslessly (used by the serializer): no rounding, never\n * exponent notation for usual cooking magnitudes.\n */\nexport function formatExactQuantity(\n value: number,\n decimalSeparator: DecimalSeparator = '.',\n): string {\n let text = String(value);\n if (/e/i.test(text)) {\n // 1e-7 / 1e21 style: expand. Precision beyond 20 decimals is not representable in the DSL.\n text = trimZeros(value.toFixed(20));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\nfunction trimZeros(text: string): string {\n if (!text.includes('.') || /e/i.test(text)) return text;\n return text.replace(/0+$/, '').replace(/\\.$/, '');\n}\n\n/**\n * Parses a DSL quantity: integer, decimal with `.` or `,`, or simple fraction `a/b`.\n * Returns `undefined` when the text is not a quantity. May return 0 or Infinity\n * (e.g. \"0\", \"1/0\"); callers report those as INVALID_QUANTITY.\n */\nexport function parseQuantity(text: string): number | undefined {\n const fraction = /^(\\d+)\\/(\\d+)$/.exec(text);\n if (fraction) return Number(fraction[1]) / Number(fraction[2]);\n if (/^\\d+(?:[.,]\\d+)?$/.test(text)) return Number(text.replace(',', '.'));\n return undefined;\n}\n\n/** Regex source of a DSL quantity, shared by the lexer and the label parser. */\nexport const QUANTITY_SOURCE = String.raw`\\d+\\/\\d+|\\d+(?:[.,]\\d+)?`;\n\n/**\n * Multiplies every ingredient quantity by `factor` (e.g. 6/4 to go from 4 to\n * 6 servings). Quantities are not rounded; use {@link formatQuantity} to display.\n * Throws a RangeError if `factor` is not a finite number > 0.\n */\nexport function scale(recipe: Recipe, factor: number): Recipe {\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new RangeError(`scale factor must be a finite number > 0, got ${factor}`);\n }\n return {\n prerequisites: [...recipe.prerequisites],\n ingredients: recipe.ingredients.map((ingredient) =>\n ingredient.quantity === undefined\n ? { ...ingredient }\n : { ...ingredient, quantity: ingredient.quantity * factor },\n ),\n steps: recipe.steps.map((step) => ({ ...step, inputs: [...step.inputs] })),\n };\n}\n","import { parseQuantity, QUANTITY_SOURCE } from '../quantity';\n\nexport interface ParsedIngredientLabel {\n name: string;\n quantity?: number;\n unit?: string;\n /** Original quantity text, kept for diagnostics. */\n quantityText?: string;\n}\n\nconst LEADING_QUANTITY = new RegExp(String.raw`^(?:${QUANTITY_SOURCE})`);\nconst QUOTED_PREFIX = new RegExp(String.raw`^(${QUANTITY_SOURCE})(?:\\s*(\\S+))?$`);\n\n/**\n * Splits an ingredient label into quantity, unit and name.\n *\n * Unquoted `[1,5 kg Pommes de terre]`:\n * - a leading quantity (`3`, `1.5`, `1,5`, `1/2`) is taken when followed by\n * a blank, a letter or the end of the label (so `4-épices` stays a name);\n * - the next word is the unit if it is in `units` (case-insensitive, may be\n * glued: `500g`) **and** a name follows it (`[500 g]` = 500 × \"g\");\n * - the rest, trimmed, is the name.\n *\n * Quoted `[1,5 kg \"Pommes (vapeur\"]` / `[\"4 épices\"]`: the quoted string is\n * the name, verbatim; the prefix is the quantity and optional unit.\n *\n * Quantities may come out as 0 or Infinity; the caller reports them.\n *\n * @param units lower-case → canonical spelling.\n */\nexport function parseIngredientLabel(\n label: { value: string; quoted: boolean; prefix: string },\n units: ReadonlyMap<string, string>,\n): ParsedIngredientLabel {\n if (label.quoted) {\n const name = label.value.trim();\n const match = QUOTED_PREFIX.exec(label.prefix.trim());\n if (!match) return { name };\n const result: ParsedIngredientLabel = {\n name,\n quantityText: match[1]!,\n quantity: parseQuantity(match[1]!),\n };\n if (match[2] !== undefined) result.unit = units.get(match[2].toLowerCase()) ?? match[2];\n return result;\n }\n\n const text = label.value.trim();\n const quantityMatch = LEADING_QUANTITY.exec(text);\n if (!quantityMatch) return { name: text };\n const quantityText = quantityMatch[0];\n let rest = text.slice(quantityText.length);\n if (rest !== '' && !/^[\\s\\p{L}]/u.test(rest)) return { name: text };\n\n const result: ParsedIngredientLabel = {\n name: '',\n quantityText,\n quantity: parseQuantity(quantityText),\n };\n rest = rest.trimStart();\n const unitMatch = /^(\\S+)(\\s+|$)/.exec(rest);\n if (unitMatch) {\n const unit = units.get(unitMatch[1]!.toLowerCase());\n const after = rest.slice(unitMatch[0].length).trim();\n if (unit !== undefined && after !== '') {\n result.unit = unit;\n rest = after;\n }\n }\n result.name = rest.trim();\n return result;\n}\n","import { diagnostic, type Diagnostic } from '../diagnostics';\nimport { QUANTITY_SOURCE } from '../quantity';\n\n/** Position of a token on its line. Columns are 1-based UTF-16 offsets, `endColumn` exclusive. */\nexport interface Span {\n line: number;\n column: number;\n endColumn: number;\n}\n\nexport interface IdentToken extends Span {\n type: 'ident';\n value: string;\n}\n\nexport interface LabelToken extends Span {\n type: 'label';\n /** `square` = `[ingredient]`, `round` = `(step)`. */\n shape: 'square' | 'round';\n /** Label text: raw content between the delimiters, or the unescaped quoted string. */\n value: string;\n quoted: boolean;\n /** Square labels only: the `quantity [unit]` written before a quoted name (may be ''). */\n prefix: string;\n}\n\nexport interface PunctToken extends Span {\n type: 'amp' | 'arrow';\n}\n\nexport interface BeforeToken extends Span {\n type: 'before';\n text: string;\n quoted: boolean;\n}\n\nexport type Token = IdentToken | LabelToken | PunctToken | BeforeToken;\n\nexport interface LexedLine {\n line: number;\n tokens: Token[];\n /** True when the line has a lexical error; its tokens are then incomplete. */\n failed: boolean;\n}\n\nexport interface LexResult {\n lines: LexedLine[];\n diagnostics: Diagnostic[];\n}\n\nconst IDENT_CHAR = /[A-Za-z0-9_-]/;\nconst CLOSE = { square: ']', round: ')' } as const;\nconst OPEN = { square: '[', round: '(' } as const;\n\n/** Splits a document into lines (`\\n`, `\\r\\n` or `\\r`). */\nexport function splitLines(source: string): string[] {\n return source.split(/\\r\\n|\\r|\\n/);\n}\n\n/**\n * Tokenizes a whole document, line by line. Comments (`%%` outside labels and\n * quoted strings) and blank lines produce no tokens. A lexical error stops\n * the current line (marked `failed`) but not the following ones.\n *\n * @param units lower-cased unit vocabulary, used to recognise `[2 kg \"quoted name\"]`.\n */\nexport function lex(source: string, units: ReadonlySet<string>): LexResult {\n const diagnostics: Diagnostic[] = [];\n const lines = splitLines(source).map((text, i) => lexLine(text, i + 1, units, diagnostics));\n return { lines, diagnostics };\n}\n\nexport function lexLine(\n text: string,\n line: number,\n units: ReadonlySet<string>,\n diagnostics: Diagnostic[],\n): LexedLine {\n const tokens: Token[] = [];\n let pos = 0;\n const at = (offset = 0) => text[pos + offset] ?? '';\n const fail = (\n code: Parameters<typeof diagnostic>[0],\n params: Record<string, string>,\n start: number,\n end: number,\n ) => {\n diagnostics.push(\n diagnostic(code, params, { line, column: start + 1, endLine: line, endColumn: end + 1 }),\n );\n return { line, tokens, failed: true };\n };\n const skipSpaces = () => {\n while (pos < text.length && /\\s/.test(at())) pos++;\n };\n\n skipSpaces();\n\n // `before: text` statement (only at the start of a line).\n const beforeMatch = /^before\\s*:/.exec(text.slice(pos));\n if (beforeMatch) {\n const start = pos;\n pos += beforeMatch[0].length;\n skipSpaces();\n if (at() === '\"') {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) return fail('UNTERMINATED_STRING', {}, pos, text.length);\n pos = quoted.end;\n skipSpaces();\n if (pos < text.length && !text.startsWith('%%', pos)) {\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'end of line', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n tokens.push({\n type: 'before',\n text: quoted.value,\n quoted: true,\n line,\n column: start + 1,\n endColumn: quoted.end + 1,\n });\n } else {\n const comment = text.indexOf('%%', pos);\n const end = comment === -1 ? text.length : comment;\n tokens.push({\n type: 'before',\n text: text.slice(pos, end).trim(),\n quoted: false,\n line,\n column: start + 1,\n endColumn: end + 1,\n });\n }\n return { line, tokens, failed: false };\n }\n\n while (pos < text.length) {\n skipSpaces();\n if (pos >= text.length) break;\n const start = pos;\n const ch = at();\n\n if (text.startsWith('%%', pos)) break; // comment until end of line\n if (text.startsWith('-->', pos)) {\n pos += 3;\n tokens.push({ type: 'arrow', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (ch === '&') {\n pos++;\n tokens.push({ type: 'amp', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (IDENT_CHAR.test(ch)) {\n while (pos < text.length && IDENT_CHAR.test(at()) && !text.startsWith('-->', pos)) pos++;\n tokens.push({\n type: 'ident',\n value: text.slice(start, pos),\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n if (ch === '[' || ch === '(') {\n const shape = ch === '[' ? 'square' : 'round';\n const label = readLabel(text, pos, shape, units);\n if (!label.ok) return fail(label.code, label.params, label.start, label.end);\n pos = label.end;\n tokens.push({\n type: 'label',\n shape,\n value: label.value,\n quoted: label.quoted,\n prefix: label.prefix,\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'a node, \"&\" or \"-->\"', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n return { line, tokens, failed: false };\n}\n\n/** Human description of the character at `pos`, for SYNTAX_ERROR's `found` param. */\nexport function describe(text: string, pos: number): string {\n if (pos >= text.length) return 'end of line';\n return `\"${String.fromCodePoint(text.codePointAt(pos) ?? 0)}\"`;\n}\n\ntype QuotedResult = { ok: true; value: string; end: number } | { ok: false };\n\n/** Reads a `\"…\"` string starting at `start` (which must be `\"`). Escapes: `\\\"` and `\\\\`. */\nexport function readQuoted(text: string, start: number): QuotedResult {\n let value = '';\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\\\\' && (text[pos + 1] === '\"' || text[pos + 1] === '\\\\')) {\n value += text[pos + 1];\n pos += 2;\n continue;\n }\n if (ch === '\"') return { ok: true, value, end: pos + 1 };\n value += ch;\n pos++;\n }\n return { ok: false };\n}\n\ntype LabelResult =\n | { ok: true; value: string; quoted: boolean; prefix: string; end: number }\n | {\n ok: false;\n code: 'UNTERMINATED_LABEL' | 'UNTERMINATED_STRING' | 'SYNTAX_ERROR';\n params: Record<string, string>;\n start: number;\n end: number;\n };\n\nconst QUOTE_PREFIX_SOURCE = String.raw`^\\s*(?:(?:${QUANTITY_SOURCE})(?:\\s*([^\\s\"\\[\\]()]+))?\\s*)?$`;\nconst QUOTE_PREFIX = new RegExp(QUOTE_PREFIX_SOURCE);\n\n/**\n * Reads a label starting at the opening delimiter.\n *\n * - Unquoted: everything up to the matching closing delimiter; only the label's\n * own delimiter kind is counted for nesting (`[Ail (gousse)]`, `(Mix (well))`).\n * - Quoted: `(\"text\")`, `[\"name\"]`, or for ingredients `[qty unit \"name\"]` —\n * a `\"` opens a quoted string when it is the first non-blank character, or\n * (square labels) when everything before it is a quantity optionally\n * followed by a known unit. Any other `\"` is a literal character.\n */\nfunction readLabel(\n text: string,\n start: number,\n shape: 'square' | 'round',\n units: ReadonlySet<string>,\n): LabelResult {\n const open = OPEN[shape];\n const close = CLOSE[shape];\n let depth = 0;\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\"') {\n const before = text.slice(start + 1, pos);\n const opensQuote =\n before.trim() === '' ||\n (shape === 'square' && depth === 0 && isQuantityPrefix(before, units));\n if (opensQuote) {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) {\n return {\n ok: false,\n code: 'UNTERMINATED_STRING',\n params: {},\n start: pos,\n end: text.length,\n };\n }\n let end = quoted.end;\n while (end < text.length && /\\s/.test(text[end]!)) end++;\n if (text[end] !== close) {\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected: `\"${close}\"`, found: describe(text, end) },\n start: end,\n end: end + 1,\n };\n }\n return { ok: true, value: quoted.value, quoted: true, prefix: before.trim(), end: end + 1 };\n }\n }\n if (ch === open) depth++;\n if (ch === close) {\n if (depth === 0) {\n return {\n ok: true,\n value: text.slice(start + 1, pos),\n quoted: false,\n prefix: '',\n end: pos + 1,\n };\n }\n depth--;\n }\n pos++;\n }\n return { ok: false, code: 'UNTERMINATED_LABEL', params: { close }, start, end: text.length };\n}\n\nfunction isQuantityPrefix(before: string, units: ReadonlySet<string>): boolean {\n const match = QUOTE_PREFIX.exec(before);\n if (!match || before.trim() === '') return false;\n return match[1] === undefined || units.has(match[1].toLowerCase());\n}\n","import { diagnostic, hasErrors, type Diagnostic, type DiagnosticCode } from '../diagnostics';\nimport {\n MAX_KEY_LENGTH,\n unitLookup,\n type Ingredient,\n type Recipe,\n type RecipeOptions,\n type Step,\n} from '../model';\nimport { normalize } from '../normalize';\nimport { validate } from '../validate';\nimport { parseIngredientLabel } from './label';\nimport { lex, type IdentToken, type LabelToken, type Span, type Token } from './lexer';\n\nexport type ParseResult =\n | { ok: true; recipe: Recipe; diagnostics: Diagnostic[] }\n | { ok: false; diagnostics: Diagnostic[]; partial?: Recipe };\n\nexport type ParseOptions = Pick<RecipeOptions, 'units'>;\n\n/** One occurrence of a node in the text: `id` or `id[label]` / `id(label)`. */\ninterface NodeRef {\n key: string;\n ident: IdentToken;\n label?: LabelToken;\n span: Span;\n}\n\ninterface NodeInfo {\n key: string;\n /** Order of first mention in the document. */\n seq: number;\n first: NodeRef;\n def?: NodeRef;\n kind?: 'ingredient' | 'step';\n ingredient?: Ingredient;\n text?: string;\n inputs: string[];\n consumer?: string;\n}\n\ninterface Edge {\n from: NodeRef;\n to: NodeRef;\n}\n\n/**\n * Codes that make tree-structure checks (NO_INPUT, CYCLE, UNCONSUMED,\n * MULTIPLE_ROOTS) meaningless: they would only cascade. Tree rules are checked\n * once the document is syntactically valid and every reference resolves.\n */\nconst BLOCKING: ReadonlySet<DiagnosticCode> = new Set([\n 'SYNTAX_ERROR',\n 'UNTERMINATED_LABEL',\n 'UNTERMINATED_STRING',\n 'FAN_OUT',\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n]);\n\n/**\n * Parses a tabular-recipe document.\n *\n * On success, `recipe` is normalized (see {@link normalize}). On failure,\n * every diagnostic found is returned (sorted by position) together with a\n * best-effort `partial` recipe, handy for live previews.\n */\nexport function parse(source: string, options: ParseOptions = {}): ParseResult {\n const unitMap = unitLookup(options.units);\n const { lines, diagnostics } = lex(source, new Set(unitMap.keys()));\n const nodes = new Map<string, NodeInfo>();\n const prerequisites: string[] = [];\n const edges: Edge[] = [];\n const suppressed = new Set<string>(); // idents seen on failed lines: no UNDEFINED_NODE for them\n let seq = 0;\n\n const at = (span: Span) => ({\n line: span.line,\n column: span.column,\n endLine: span.line,\n endColumn: span.endColumn,\n });\n const report = (code: DiagnosticCode, params: Record<string, string | number>, span: Span) =>\n diagnostics.push(diagnostic(code, params, at(span)));\n\n const register = (ref: NodeRef): NodeInfo => {\n let info = nodes.get(ref.key);\n if (!info) {\n info = { key: ref.key, seq: seq++, first: ref, inputs: [] };\n nodes.set(ref.key, info);\n if (ref.key.length > MAX_KEY_LENGTH) report('INVALID_KEY', { key: ref.key }, ref.ident);\n }\n if (!ref.label) return info;\n if (info.def) {\n report('REDEFINED_NODE', { key: ref.key, firstLine: info.def.ident.line }, ref.ident);\n return info;\n }\n info.def = ref;\n if (ref.label.shape === 'square') {\n info.kind = 'ingredient';\n const parsed = parseIngredientLabel(ref.label, unitMap);\n const ingredient: Ingredient = { key: ref.key, name: parsed.name };\n if (parsed.quantity !== undefined) {\n if (!Number.isFinite(parsed.quantity) || parsed.quantity <= 0) {\n report('INVALID_QUANTITY', { quantity: parsed.quantityText ?? '' }, ref.label);\n } else {\n ingredient.quantity = parsed.quantity;\n if (parsed.unit !== undefined) ingredient.unit = parsed.unit;\n }\n }\n if (parsed.name === '') report('EMPTY_TEXT', {}, ref.label);\n info.ingredient = ingredient;\n } else {\n info.kind = 'step';\n info.text = ref.label.value.trim();\n if (info.text === '') report('EMPTY_TEXT', {}, ref.label);\n }\n return info;\n };\n\n for (const lexed of lines) {\n if (lexed.failed) {\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n const statement = parseStatement(lexed.tokens);\n if (!statement.ok) {\n report(statement.code, statement.params, statement.span);\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n if (statement.kind === 'empty') continue;\n if (statement.kind === 'before') {\n if (statement.token.text.trim() === '') report('EMPTY_TEXT', {}, statement.token);\n prerequisites.push(statement.token.text.trim());\n continue;\n }\n if (statement.kind === 'definition') {\n register(statement.node);\n continue;\n }\n // Edge statement: sources --> t1 --> t2 ...\n statement.sources.forEach(register);\n let previous = statement.sources;\n for (const target of statement.targets) {\n register(target);\n if (target.label?.shape === 'square') {\n report('INGREDIENT_AS_TARGET', { key: target.key }, target.span);\n } else {\n for (const from of previous) edges.push({ from, to: target });\n }\n previous = [target];\n }\n if (statement.fanOut) report('FAN_OUT', {}, statement.fanOut);\n }\n\n // --- resolution -------------------------------------------------------------------------\n for (const info of nodes.values()) {\n if (!info.def && !suppressed.has(info.key)) {\n report('UNDEFINED_NODE', { key: info.key }, info.first.ident);\n }\n }\n for (const edge of edges) {\n const from = nodes.get(edge.from.key)!;\n const to = nodes.get(edge.to.key)!;\n if (!from.def || !to.def) continue;\n if (to.kind === 'ingredient') {\n report('INGREDIENT_AS_TARGET', { key: to.key }, edge.to.span);\n continue;\n }\n if (from.consumer !== undefined) {\n report('MULTIPLE_CONSUMERS', { key: from.key, consumer: from.consumer }, edge.from.span);\n continue;\n }\n from.consumer = to.key;\n to.inputs.push(from.key);\n }\n\n const all = [...nodes.values()].filter((n) => n.def).sort((a, b) => a.seq - b.seq);\n const ingredients = all.filter((n) => n.kind === 'ingredient');\n const stepNodes = all.filter((n) => n.kind === 'step');\n const order = topologicalOrder(stepNodes, nodes);\n\n // --- tree rules ---------------------------------------------------------------------------\n const blocked = diagnostics.some((d) => BLOCKING.has(d.code));\n if (!blocked && stepNodes.length > 0) {\n for (const step of stepNodes) {\n if (step.inputs.length === 0) report('NO_INPUT', { key: step.key }, step.def!.span);\n }\n if (order.cyclic.length > 0) {\n const first = order.cyclic[0]!;\n report('CYCLE', { keys: order.cyclic.map((s) => s.key).join(', ') }, first.def!.span);\n }\n for (const ingredient of ingredients) {\n if (ingredient.consumer === undefined) {\n report('UNCONSUMED', { key: ingredient.key }, ingredient.def!.span);\n }\n }\n const roots = order.sorted.filter((s) => s.consumer === undefined);\n const root = roots[roots.length - 1];\n for (const extra of roots.slice(0, -1)) {\n report('MULTIPLE_ROOTS', { key: extra.key, root: root!.key }, extra.def!.span);\n }\n }\n\n const recipe: Recipe = {\n prerequisites,\n ingredients: ingredients.map((n) => n.ingredient!),\n steps: [...order.sorted, ...order.cyclic].map((n): Step => ({\n key: n.key,\n text: n.text!,\n inputs: n.inputs,\n })),\n };\n\n diagnostics.sort((a, b) => (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0));\n if (hasErrors(diagnostics)) {\n return { ok: false, diagnostics, partial: normalize(recipe, options) };\n }\n const normalized = normalize(recipe, options);\n // Safety net: the DSL checks above mirror validate(); anything left is a bug, but report it.\n const leftovers = validate(normalized, options).filter((d) => d.severity === 'error');\n if (leftovers.length > 0) return { ok: false, diagnostics: leftovers, partial: normalized };\n return { ok: true, recipe: normalized, diagnostics: [] };\n}\n\n/**\n * Stable topological order of steps: a step comes after the steps it consumes;\n * ties are broken by order of first mention. Steps on a cycle are returned apart.\n */\nfunction topologicalOrder(steps: NodeInfo[], nodes: Map<string, NodeInfo>) {\n const pending = new Map<string, number>();\n for (const step of steps) {\n pending.set(step.key, step.inputs.filter((input) => nodes.get(input)?.kind === 'step').length);\n }\n const sorted: NodeInfo[] = [];\n const done = new Set<string>();\n // Steps are few (< 100): a simple repeated scan keeps the tie-breaking obvious.\n for (;;) {\n const next = steps.find((s) => !done.has(s.key) && pending.get(s.key) === 0);\n if (!next) break;\n done.add(next.key);\n sorted.push(next);\n if (next.consumer !== undefined && pending.has(next.consumer)) {\n pending.set(next.consumer, pending.get(next.consumer)! - 1);\n }\n }\n return { sorted, cyclic: steps.filter((s) => !done.has(s.key)) };\n}\n\n// --- statements -----------------------------------------------------------------------------\n\ntype Statement =\n | { ok: true; kind: 'empty' }\n | { ok: true; kind: 'before'; token: Extract<Token, { type: 'before' }> }\n | { ok: true; kind: 'definition'; node: NodeRef }\n | { ok: true; kind: 'edges'; sources: NodeRef[]; targets: NodeRef[]; fanOut?: Span }\n | { ok: false; code: DiagnosticCode; params: Record<string, string>; span: Span };\n\nfunction found(token: Token | undefined): string {\n if (!token) return 'end of line';\n switch (token.type) {\n case 'ident':\n return `\"${token.value}\"`;\n case 'label':\n return token.shape === 'square' ? '\"[\"' : '\"(\"';\n case 'amp':\n return '\"&\"';\n case 'arrow':\n return '\"-->\"';\n case 'before':\n return '\"before:\"';\n }\n}\n\n/**\n * Grammar of one line (tokens already lexed):\n *\n * statement = before | definition | edges\n * definition = node-def\n * edges = node ( \"&\" node )* ( \"-->\" node )+\n * node = IDENT [ LABEL ]\n */\nfunction parseStatement(tokens: Token[]): Statement {\n if (tokens.length === 0) return { ok: true, kind: 'empty' };\n const first = tokens[0]!;\n if (first.type === 'before') return { ok: true, kind: 'before', token: first };\n\n let pos = 0;\n const lastSpan = tokens[tokens.length - 1]!;\n const eol: Span = {\n line: lastSpan.line,\n column: lastSpan.endColumn,\n endColumn: lastSpan.endColumn + 1,\n };\n const error = (expected: string): Statement => {\n const token = tokens[pos];\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected, found: found(token) },\n span: token ?? eol,\n };\n };\n const readNode = (): NodeRef | undefined => {\n const ident = tokens[pos];\n if (ident?.type !== 'ident') return undefined;\n pos++;\n const label = tokens[pos];\n if (label?.type === 'label') {\n pos++;\n return {\n key: ident.value,\n ident,\n label,\n span: { line: ident.line, column: ident.column, endColumn: label.endColumn },\n };\n }\n return { key: ident.value, ident, span: ident };\n };\n\n const sources: NodeRef[] = [];\n const firstNode = readNode();\n if (!firstNode) return error('a node id');\n sources.push(firstNode);\n while (tokens[pos]?.type === 'amp') {\n pos++;\n const node = readNode();\n if (!node) return error('a node id after \"&\"');\n sources.push(node);\n }\n\n if (pos === tokens.length) {\n if (sources.length === 1 && firstNode.label)\n return { ok: true, kind: 'definition', node: firstNode };\n return error(sources.length === 1 ? '\"[\", \"(\" or \"-->\"' : '\"-->\"');\n }\n\n const targets: NodeRef[] = [];\n while (tokens[pos]?.type === 'arrow') {\n pos++;\n const node = readNode();\n if (!node) return error('a step after \"-->\"');\n targets.push(node);\n }\n if (targets.length === 0) return error('\"&\" or \"-->\"');\n if (pos === tokens.length) return { ok: true, kind: 'edges', sources, targets };\n const extra = tokens[pos]!;\n if (extra.type === 'amp') return { ok: true, kind: 'edges', sources, targets, fanOut: extra };\n return error('\"-->\" or end of line');\n}\n","import type { Diagnostic } from '../diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from '../model';\nimport { normalize } from '../normalize';\nimport { formatExactQuantity } from '../quantity';\nimport { parseIngredientLabel } from './label';\nimport { lexLine } from './lexer';\n\nexport type SerializeOptions = RecipeOptions;\n\n/**\n * Writes a recipe in canonical DSL form:\n *\n * 1. `before: …` lines, then a blank line if anything follows;\n * 2. drafts (no steps): one ingredient definition per line;\n * 3. otherwise one line per step, in step order: `inputs --> key(text)`,\n * each ingredient defined inline at its use, earlier steps as bare ids.\n * A step whose single input is the step just before it is appended to\n * that line as a chain: `… --> a(x) --> b(y)`.\n *\n * Labels are quoted only when needed (the serializer checks that each label\n * reads back identically). Quantities are written losslessly with\n * `decimalSeparator` (default `.`). The recipe is normalized first, so\n * `parse(serialize(r)).recipe` deep-equals `normalize(r)` for every valid r.\n *\n * Invalid recipes are still written, best effort (unused ingredients become\n * standalone definitions at the end), so an editor can switch to text mode\n * and let the parser point at the problems.\n */\nexport function serialize(recipe: Recipe, options: SerializeOptions = {}): string {\n const r = normalize(recipe, options);\n const separator = options.decimalSeparator ?? '.';\n const unitSet = new Set(unitLookup(options.units).keys());\n const unitMap = unitLookup(options.units);\n const lines: string[] = r.prerequisites.map((text) => `before: ${prerequisiteText(text)}`);\n const body: string[] = [];\n\n const ingredientByKey = new Map(r.ingredients.map((i) => [i.key, i]));\n const defined = new Set<string>();\n const ingredientRef = (ingredient: Ingredient) => {\n if (defined.has(ingredient.key)) return ingredient.key;\n defined.add(ingredient.key);\n return `${ingredient.key}[${ingredientLabel(ingredient, separator, unitSet, unitMap)}]`;\n };\n const stepRef = (step: Step) => `${step.key}(${stepLabel(step.text, unitSet)})`;\n const inputRef = (key: string) => {\n const ingredient = ingredientByKey.get(key);\n return ingredient ? ingredientRef(ingredient) : key;\n };\n\n for (let i = 0; i < r.steps.length; i++) {\n let step = r.steps[i]!;\n let line =\n step.inputs.length > 0\n ? `${step.inputs.map(inputRef).join(' & ')} --> ${stepRef(step)}`\n : stepRef(step);\n let next = r.steps[i + 1];\n while (next && next.inputs.length === 1 && next.inputs[0] === step.key) {\n line += ` --> ${stepRef(next)}`;\n step = next;\n i++;\n next = r.steps[i + 1];\n }\n body.push(line);\n }\n for (const ingredient of r.ingredients) {\n if (!defined.has(ingredient.key)) body.push(ingredientRef(ingredient));\n }\n\n if (lines.length > 0 && body.length > 0) lines.push('');\n lines.push(...body);\n return lines.length > 0 ? `${lines.join('\\n')}\\n` : '';\n}\n\nfunction quote(text: string): string {\n return `\"${text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\nfunction prerequisiteText(text: string): string {\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(`before: ${text}`, 1, new Set(), diagnostics);\n const token = lexed.tokens[0];\n const readsBack = diagnostics.length === 0 && token?.type === 'before' && token.text === text;\n return readsBack && text !== '' ? text : quote(text);\n}\n\n/** Lexes `x<open>content<close>` and returns the label token if it reads back cleanly. */\nfunction relex(content: string, shape: 'square' | 'round', units: ReadonlySet<string>) {\n const [open, close] = shape === 'square' ? ['[', ']'] : ['(', ')'];\n const source = `x${open}${content}${close}`;\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(source, 1, units, diagnostics);\n const label = lexed.tokens[1];\n if (diagnostics.length > 0 || lexed.tokens.length !== 2 || label?.type !== 'label')\n return undefined;\n return label.endColumn === source.length + 1 ? label : undefined;\n}\n\nfunction stepLabel(text: string, units: ReadonlySet<string>): string {\n const label = relex(text, 'round', units);\n return label && !label.quoted && label.value.trim() === text && text !== '' ? text : quote(text);\n}\n\nfunction ingredientLabel(\n ingredient: Ingredient,\n separator: '.' | ',',\n units: ReadonlySet<string>,\n unitMap: ReadonlyMap<string, string>,\n): string {\n const prefix =\n ingredient.quantity === undefined\n ? ''\n : [formatExactQuantity(ingredient.quantity, separator), ingredient.unit]\n .filter((part) => part !== undefined)\n .join(' ');\n const raw = [prefix, ingredient.name].filter((part) => part !== '').join(' ');\n const label = relex(raw, 'square', units);\n if (label && !label.quoted) {\n const parsed = parseIngredientLabel(label, unitMap);\n if (\n parsed.name === ingredient.name &&\n parsed.quantity === ingredient.quantity &&\n parsed.unit === ingredient.unit &&\n ingredient.name !== ''\n ) {\n return raw;\n }\n }\n return prefix === '' ? quote(ingredient.name) : `${prefix} ${quote(ingredient.name)}`;\n}\n","import { hasErrors } from './diagnostics';\nimport type { Recipe, RecipeOptions, Step } from './model';\nimport { normalize } from './normalize';\nimport { formatQuantity } from './quantity';\nimport { validate } from './validate';\n\nexport type CellKind = 'quantity' | 'ingredient' | 'step';\n\nexport interface Cell {\n kind: CellKind;\n /** Ingredient key (quantity / ingredient cells) or step key. */\n key: string;\n text: string;\n /** 0-based grid position of the top-left corner. */\n row: number;\n col: number;\n rowSpan: number;\n colSpan: number;\n /** True for the final step (rightmost column). */\n final?: boolean;\n}\n\nexport interface Layout {\n /** False when the recipe is not a valid tree: only ingredients are laid out (draft layout). */\n valid: boolean;\n /** 2 (quantity + ingredient) + depth of the final step. */\n columnCount: number;\n /** Prerequisites, rendered as full-width rows above the table. */\n banners: string[];\n /** `rows[r]` = cells that *start* on row r, left to right (HTML table semantics). */\n rows: Cell[][];\n}\n\nexport interface LayoutOptions extends RecipeOptions {\n /** Max decimals of displayed quantities (default 2). */\n maxDecimals?: number;\n /** Display names for units, e.g. `{ pincee: 'pincée', cs: 'c. à s.' }`. Unlisted units show as-is. */\n unitLabels?: Readonly<Record<string, string>>;\n}\n\n/**\n * Computes the tabular grid of a recipe.\n *\n * - Column 0 = quantity, column 1 = ingredient, columns 2… = steps by depth:\n * depth(ingredient) = 0, depth(step) = 1 + max depth of its inputs,\n * step column = 1 + depth. The final step is alone in the last column.\n * - One row per ingredient (normalized depth-first order). A step spans the\n * rows of all the ingredients under it.\n * - No empty cells: a step or ingredient whose consumer sits more than one\n * column to the right stretches (`colSpan`) up to that consumer.\n *\n * Drafts (and invalid recipes, with `valid: false`) get a 2-column layout.\n */\nexport function layout(recipe: Recipe, options: LayoutOptions = {}): Layout {\n const r = normalize(recipe, options);\n const valid = !hasErrors(validate(r, options));\n const quantityText = (quantity?: number, unit?: string) =>\n quantity === undefined\n ? ''\n : [\n formatQuantity(quantity, {\n decimalSeparator: options.decimalSeparator,\n maxDecimals: options.maxDecimals,\n }),\n unit === undefined ? undefined : (options.unitLabels?.[unit] ?? unit),\n ]\n .filter(Boolean)\n .join(' ');\n\n const useTree = valid && r.steps.length > 0;\n const stepByKey = new Map(r.steps.map((s) => [s.key, s]));\n const depth = new Map<string, number>();\n const consumer = new Map<string, Step>();\n const leaves = new Map<string, string[]>(); // step key → ingredient keys under it, in row order\n if (useTree) {\n for (const step of r.steps) {\n // Steps are ordered so that inputs come first: one pass is enough.\n let d = 0;\n const under: string[] = [];\n for (const input of step.inputs) {\n consumer.set(input, step);\n const child = stepByKey.get(input);\n d = Math.max(d, child ? depth.get(input)! : 0);\n under.push(...(child ? leaves.get(input)! : [input]));\n }\n depth.set(step.key, d + 1);\n leaves.set(step.key, under);\n }\n }\n\n const root = r.steps[r.steps.length - 1];\n const columnCount = useTree && root ? 2 + depth.get(root.key)! : 2;\n const columnOf = (stepKey: string) => 1 + depth.get(stepKey)!;\n const rowOf = new Map(r.ingredients.map((ingredient, i) => [ingredient.key, i]));\n\n const rows: Cell[][] = r.ingredients.map((ingredient, row) => {\n const parent = consumer.get(ingredient.key);\n return [\n {\n kind: 'quantity',\n key: ingredient.key,\n text: quantityText(ingredient.quantity, ingredient.unit),\n row,\n col: 0,\n rowSpan: 1,\n colSpan: 1,\n },\n {\n kind: 'ingredient',\n key: ingredient.key,\n text: ingredient.name,\n row,\n col: 1,\n rowSpan: 1,\n colSpan: parent ? columnOf(parent.key) - 1 : 1,\n },\n ];\n });\n\n if (useTree) {\n for (const step of r.steps) {\n const under = leaves.get(step.key)!;\n const row = rowOf.get(under[0]!)!;\n const col = columnOf(step.key);\n const parent = consumer.get(step.key);\n rows[row]!.push({\n kind: 'step',\n key: step.key,\n text: step.text,\n row,\n col,\n rowSpan: under.length,\n colSpan: parent ? columnOf(parent.key) - col : 1,\n ...(parent ? {} : { final: true }),\n });\n }\n for (const cells of rows) cells.sort((a, b) => a.col - b.col);\n }\n\n return { valid, columnCount, banners: [...r.prerequisites], rows };\n}\n","import type { Recipe } from './model';\nimport { layout, type Cell, type LayoutOptions } from './layout';\n\nexport interface RenderHtmlOptions extends LayoutOptions {\n /** CSS class prefix (default `trd`). Must match the stylesheet you use. */\n classPrefix?: string;\n /** Keep the quantity and ingredient columns visible while scrolling horizontally. */\n stickyIngredients?: boolean;\n /** Optional `<caption>` text (e.g. the recipe name). */\n caption?: string;\n /**\n * Orientation of the step texts (default `'horizontal'`).\n * - `'vertical'`: step texts read bottom to top, so step columns stay narrow (mobile-friendly).\n * - `'auto'`: vertical only when the wrapper is narrower than 40rem (CSS container query);\n * the wrapper must then take its width from its parent, not from its content.\n */\n stepOrientation?: 'horizontal' | 'vertical' | 'auto';\n}\n\nconst ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\n\n/** Escapes text for HTML element content and attribute values. */\nexport function escapeHtml(text: string): string {\n return text.replace(/[&<>\"']/g, (ch) => ESCAPES[ch]!);\n}\n\n/**\n * Renders a recipe as a standalone HTML table (string), framework-free.\n * All text is escaped. Pair it with `ln-tabular-recipes-dsl/style.css`, or\n * style the classes yourself:\n *\n * `{p}-wrap` (scroll container, + `{p}-auto`), `{p}-table` (+ `{p}-sticky`,\n * `{p}-invalid`, `{p}-vsteps`), `{p}-banner`, `{p}-qty`, `{p}-ingredient`,\n * `{p}-step` (+ `{p}-final`), `{p}-step-text` (step text wrapper, only when\n * `stepOrientation` is not horizontal).\n * Cells carry `data-key` with the ingredient/step key.\n */\nexport function renderHtml(recipe: Recipe, options: RenderHtmlOptions = {}): string {\n const p = options.classPrefix ?? 'trd';\n const grid = layout(recipe, options);\n const tableClasses = [`${p}-table`];\n if (options.stickyIngredients) tableClasses.push(`${p}-sticky`);\n if (!grid.valid) tableClasses.push(`${p}-invalid`);\n const orientation = options.stepOrientation ?? 'horizontal';\n if (orientation === 'vertical') tableClasses.push(`${p}-vsteps`);\n const wrapClasses = [`${p}-wrap`];\n if (orientation === 'auto') wrapClasses.push(`${p}-auto`);\n const wrapStepText = orientation !== 'horizontal';\n\n const cellHtml = (cell: Cell) => {\n const classes = [`${p}-${cell.kind === 'quantity' ? 'qty' : cell.kind}`];\n if (cell.final) classes.push(`${p}-final`);\n const attrs = [`class=\"${classes.join(' ')}\"`, `data-key=\"${escapeHtml(cell.key)}\"`];\n if (cell.rowSpan > 1) attrs.push(`rowspan=\"${cell.rowSpan}\"`);\n if (cell.colSpan > 1) attrs.push(`colspan=\"${cell.colSpan}\"`);\n const text = escapeHtml(cell.text);\n const content =\n wrapStepText && cell.kind === 'step' ? `<span class=\"${p}-step-text\">${text}</span>` : text;\n return `<td ${attrs.join(' ')}>${content}</td>`;\n };\n\n const parts: string[] = [\n `<div class=\"${wrapClasses.join(' ')}\">`,\n `<table class=\"${tableClasses.join(' ')}\">`,\n ];\n if (options.caption !== undefined)\n parts.push(`<caption>${escapeHtml(options.caption)}</caption>`);\n parts.push('<tbody>');\n for (const banner of grid.banners) {\n parts.push(\n `<tr class=\"${p}-banner-row\"><td class=\"${p}-banner\" colspan=\"${grid.columnCount}\">${escapeHtml(banner)}</td></tr>`,\n );\n }\n for (const cells of grid.rows) parts.push(`<tr>${cells.map(cellHtml).join('')}</tr>`);\n parts.push('</tbody>', '</table>', '</div>');\n return parts.join('\\n');\n}\n"],"mappings":"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","names":["diagnostic","result"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -195,6 +195,13 @@ interface RenderHtmlOptions extends LayoutOptions {
|
|
|
195
195
|
stickyIngredients?: boolean;
|
|
196
196
|
/** Optional `<caption>` text (e.g. the recipe name). */
|
|
197
197
|
caption?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Orientation of the step texts (default `'horizontal'`).
|
|
200
|
+
* - `'vertical'`: step texts read bottom to top, so step columns stay narrow (mobile-friendly).
|
|
201
|
+
* - `'auto'`: vertical only when the wrapper is narrower than 40rem (CSS container query);
|
|
202
|
+
* the wrapper must then take its width from its parent, not from its content.
|
|
203
|
+
*/
|
|
204
|
+
stepOrientation?: 'horizontal' | 'vertical' | 'auto';
|
|
198
205
|
}
|
|
199
206
|
/** Escapes text for HTML element content and attribute values. */
|
|
200
207
|
declare function escapeHtml(text: string): string;
|
|
@@ -203,8 +210,10 @@ declare function escapeHtml(text: string): string;
|
|
|
203
210
|
* All text is escaped. Pair it with `ln-tabular-recipes-dsl/style.css`, or
|
|
204
211
|
* style the classes yourself:
|
|
205
212
|
*
|
|
206
|
-
* `{p}-wrap` (scroll container), `{p}-table` (+ `{p}-sticky`,
|
|
207
|
-
* `{p}-
|
|
213
|
+
* `{p}-wrap` (scroll container, + `{p}-auto`), `{p}-table` (+ `{p}-sticky`,
|
|
214
|
+
* `{p}-invalid`, `{p}-vsteps`), `{p}-banner`, `{p}-qty`, `{p}-ingredient`,
|
|
215
|
+
* `{p}-step` (+ `{p}-final`), `{p}-step-text` (step text wrapper, only when
|
|
216
|
+
* `stepOrientation` is not horizontal).
|
|
208
217
|
* Cells carry `data-key` with the ingredient/step key.
|
|
209
218
|
*/
|
|
210
219
|
declare function renderHtml(recipe: Recipe, options?: RenderHtmlOptions): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -195,6 +195,13 @@ interface RenderHtmlOptions extends LayoutOptions {
|
|
|
195
195
|
stickyIngredients?: boolean;
|
|
196
196
|
/** Optional `<caption>` text (e.g. the recipe name). */
|
|
197
197
|
caption?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Orientation of the step texts (default `'horizontal'`).
|
|
200
|
+
* - `'vertical'`: step texts read bottom to top, so step columns stay narrow (mobile-friendly).
|
|
201
|
+
* - `'auto'`: vertical only when the wrapper is narrower than 40rem (CSS container query);
|
|
202
|
+
* the wrapper must then take its width from its parent, not from its content.
|
|
203
|
+
*/
|
|
204
|
+
stepOrientation?: 'horizontal' | 'vertical' | 'auto';
|
|
198
205
|
}
|
|
199
206
|
/** Escapes text for HTML element content and attribute values. */
|
|
200
207
|
declare function escapeHtml(text: string): string;
|
|
@@ -203,8 +210,10 @@ declare function escapeHtml(text: string): string;
|
|
|
203
210
|
* All text is escaped. Pair it with `ln-tabular-recipes-dsl/style.css`, or
|
|
204
211
|
* style the classes yourself:
|
|
205
212
|
*
|
|
206
|
-
* `{p}-wrap` (scroll container), `{p}-table` (+ `{p}-sticky`,
|
|
207
|
-
* `{p}-
|
|
213
|
+
* `{p}-wrap` (scroll container, + `{p}-auto`), `{p}-table` (+ `{p}-sticky`,
|
|
214
|
+
* `{p}-invalid`, `{p}-vsteps`), `{p}-banner`, `{p}-qty`, `{p}-ingredient`,
|
|
215
|
+
* `{p}-step` (+ `{p}-final`), `{p}-step-text` (step text wrapper, only when
|
|
216
|
+
* `stepOrientation` is not horizontal).
|
|
208
217
|
* Cells carry `data-key` with the ingredient/step key.
|
|
209
218
|
*/
|
|
210
219
|
declare function renderHtml(recipe: Recipe, options?: RenderHtmlOptions): string;
|
package/dist/index.js
CHANGED
|
@@ -986,15 +986,25 @@ function renderHtml(recipe, options = {}) {
|
|
|
986
986
|
const tableClasses = [`${p}-table`];
|
|
987
987
|
if (options.stickyIngredients) tableClasses.push(`${p}-sticky`);
|
|
988
988
|
if (!grid.valid) tableClasses.push(`${p}-invalid`);
|
|
989
|
+
const orientation = options.stepOrientation ?? "horizontal";
|
|
990
|
+
if (orientation === "vertical") tableClasses.push(`${p}-vsteps`);
|
|
991
|
+
const wrapClasses = [`${p}-wrap`];
|
|
992
|
+
if (orientation === "auto") wrapClasses.push(`${p}-auto`);
|
|
993
|
+
const wrapStepText = orientation !== "horizontal";
|
|
989
994
|
const cellHtml = (cell) => {
|
|
990
995
|
const classes = [`${p}-${cell.kind === "quantity" ? "qty" : cell.kind}`];
|
|
991
996
|
if (cell.final) classes.push(`${p}-final`);
|
|
992
997
|
const attrs = [`class="${classes.join(" ")}"`, `data-key="${escapeHtml(cell.key)}"`];
|
|
993
998
|
if (cell.rowSpan > 1) attrs.push(`rowspan="${cell.rowSpan}"`);
|
|
994
999
|
if (cell.colSpan > 1) attrs.push(`colspan="${cell.colSpan}"`);
|
|
995
|
-
|
|
1000
|
+
const text = escapeHtml(cell.text);
|
|
1001
|
+
const content = wrapStepText && cell.kind === "step" ? `<span class="${p}-step-text">${text}</span>` : text;
|
|
1002
|
+
return `<td ${attrs.join(" ")}>${content}</td>`;
|
|
996
1003
|
};
|
|
997
|
-
const parts = [
|
|
1004
|
+
const parts = [
|
|
1005
|
+
`<div class="${wrapClasses.join(" ")}">`,
|
|
1006
|
+
`<table class="${tableClasses.join(" ")}">`
|
|
1007
|
+
];
|
|
998
1008
|
if (options.caption !== void 0)
|
|
999
1009
|
parts.push(`<caption>${escapeHtml(options.caption)}</caption>`);
|
|
1000
1010
|
parts.push("<tbody>");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/model.ts","../src/diagnostics.ts","../src/validate.ts","../src/normalize.ts","../src/quantity.ts","../src/dsl/label.ts","../src/dsl/lexer.ts","../src/dsl/parser.ts","../src/dsl/serializer.ts","../src/layout.ts","../src/html.ts"],"sourcesContent":["/**\n * Core data model of a tabular recipe.\n *\n * A recipe is a tree: ingredients are leaves, steps are nodes and the last\n * step is the root (\"final step\"). `prerequisites` are steps that use no\n * ingredient (e.g. \"Preheat the oven\"); they are rendered as full-width\n * banners above the table.\n */\nexport interface Recipe {\n prerequisites: string[];\n ingredients: Ingredient[];\n steps: Step[];\n}\n\nexport interface Ingredient {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n name: string;\n /** Finite, strictly positive. */\n quantity?: number;\n /** Only meaningful together with `quantity`. */\n unit?: string;\n}\n\nexport interface Step {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n text: string;\n /** Keys of the ingredients and/or earlier steps this step combines, in row order. */\n inputs: string[];\n}\n\n/** Allowed node keys: 1 to 40 ASCII letters, digits, `_` or `-`. */\nexport const KEY_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;\n\nexport const MAX_KEY_LENGTH = 40;\n\n/** Default unit vocabulary. Matching is case-insensitive; the spelling below is canonical. */\nexport const DEFAULT_UNITS: readonly string[] = [\n 'pcs',\n 'g',\n 'kg',\n 'ml',\n 'cl',\n 'l',\n 'cs',\n 'cc',\n 'pincee',\n];\n\nexport type DecimalSeparator = '.' | ',';\n\n/** Options shared by most functions of the library. */\nexport interface RecipeOptions {\n /** Unit vocabulary (case-insensitive, canonical spelling taken from this list). */\n units?: readonly string[];\n /** Decimal separator used when *writing* quantities. Parsing always accepts both. Default `.`. */\n decimalSeparator?: DecimalSeparator;\n}\n\n/** Builds a lower-case → canonical spelling lookup for a unit list. */\nexport function unitLookup(units: readonly string[] = DEFAULT_UNITS): Map<string, string> {\n const map = new Map<string, string>();\n for (const unit of units) {\n const lower = unit.toLowerCase();\n if (!map.has(lower)) map.set(lower, unit);\n }\n return map;\n}\n","/**\n * Diagnostics are language-neutral: `code` + `params` identify the problem,\n * `message` is an English default. Consumers translate by providing their own\n * templates to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_CODES = [\n // --- DSL syntax ---\n 'SYNTAX_ERROR',\n 'UNTERMINATED_STRING',\n 'UNTERMINATED_LABEL',\n 'FAN_OUT',\n // --- DSL references ---\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n 'CYCLE',\n // --- JSON model ---\n 'INVALID_SHAPE',\n 'UNKNOWN_REFERENCE',\n 'FORWARD_REFERENCE',\n // --- shared ---\n 'INVALID_KEY',\n 'DUPLICATE_KEY',\n 'EMPTY_TEXT',\n 'INVALID_QUANTITY',\n 'UNIT_WITHOUT_QUANTITY',\n 'UNKNOWN_UNIT',\n // --- tree rules ---\n 'NO_INPUT',\n 'MULTIPLE_CONSUMERS',\n 'UNCONSUMED',\n 'MULTIPLE_ROOTS',\n] as const;\n\nexport type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];\n\nexport type Severity = 'error' | 'warning';\n\nexport type DiagnosticParams = Record<string, string | number>;\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n /** English default message, built from {@link DIAGNOSTIC_MESSAGES}. */\n message: string;\n severity: Severity;\n /** 1-based. DSL diagnostics only. */\n line?: number;\n /** 1-based, in UTF-16 code units (same as JS string indices / CodeMirror). */\n column?: number;\n endLine?: number;\n /** 1-based, exclusive. */\n endColumn?: number;\n /** JSON path of the offending value. Model (JSON) diagnostics only. */\n path?: (string | number)[];\n params?: DiagnosticParams;\n}\n\n/**\n * English message templates. `{name}` placeholders are replaced by `params.name`.\n * Copy this object, translate the values and pass it to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_MESSAGES: Record<DiagnosticCode, string> = {\n SYNTAX_ERROR: 'Syntax error: expected {expected}, found {found}.',\n UNTERMINATED_STRING: 'Unterminated quoted string.',\n UNTERMINATED_LABEL: 'Missing closing \"{close}\".',\n FAN_OUT: 'A node can only feed one step: \"&\" is not allowed after \"-->\".',\n UNDEFINED_NODE: 'Node \"{key}\" is used but never defined.',\n REDEFINED_NODE: 'Node \"{key}\" is already defined on line {firstLine}.',\n INGREDIENT_AS_TARGET: 'Ingredient \"{key}\" cannot be the target of \"-->\"; only steps can.',\n CYCLE: 'Steps form a cycle: {keys}.',\n INVALID_SHAPE: 'Invalid value: expected {expected}.',\n UNKNOWN_REFERENCE: 'Input \"{key}\" does not match any ingredient or step.',\n FORWARD_REFERENCE: 'Input \"{key}\" refers to a step that does not come before \"{step}\".',\n INVALID_KEY: 'Invalid key \"{key}\": use 1 to 40 letters, digits, \"_\" or \"-\".',\n DUPLICATE_KEY: 'Key \"{key}\" is used more than once.',\n EMPTY_TEXT: 'Text must not be empty.',\n INVALID_QUANTITY: 'Invalid quantity \"{quantity}\": it must be a number greater than 0.',\n UNIT_WITHOUT_QUANTITY: 'Unit \"{unit}\" is set without a quantity.',\n UNKNOWN_UNIT: 'Unknown unit \"{unit}\".',\n NO_INPUT: 'Step \"{key}\" has no input.',\n MULTIPLE_CONSUMERS:\n '\"{key}\" is already used by step \"{consumer}\"; each node feeds exactly one step.',\n UNCONSUMED: 'Ingredient \"{key}\" is not used by any step.',\n MULTIPLE_ROOTS:\n 'Step \"{key}\" is not used by any later step; only the final step \"{root}\" may be unused.',\n};\n\n/** Replaces `{name}` placeholders of `template` with `params`. Unknown placeholders are kept. */\nexport function interpolate(template: string, params: DiagnosticParams = {}): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match,\n );\n}\n\n/** Formats a diagnostic with custom (e.g. translated) templates, falling back to English. */\nexport function formatDiagnostic(\n diagnostic: Pick<Diagnostic, 'code' | 'params'>,\n templates: Partial<Record<DiagnosticCode, string>> = DIAGNOSTIC_MESSAGES,\n): string {\n const template = templates[diagnostic.code] ?? DIAGNOSTIC_MESSAGES[diagnostic.code];\n return interpolate(template, diagnostic.params);\n}\n\n/** Internal helper to build a diagnostic with its English message. */\nexport function diagnostic(\n code: DiagnosticCode,\n params: DiagnosticParams,\n location: Omit<Diagnostic, 'code' | 'message' | 'severity' | 'params'> = {},\n severity: Severity = 'error',\n): Diagnostic {\n return {\n code,\n message: interpolate(DIAGNOSTIC_MESSAGES[code], params),\n severity,\n ...location,\n params,\n };\n}\n\nexport function hasErrors(diagnostics: readonly Diagnostic[]): boolean {\n return diagnostics.some((d) => d.severity === 'error');\n}\n","import { diagnostic, type Diagnostic } from './diagnostics';\nimport { KEY_PATTERN, unitLookup, type Recipe, type RecipeOptions } from './model';\n\ntype Path = (string | number)[];\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Validates a recipe given as JSON (e.g. received by an API). Returns every\n * problem found, each with a `path` into the input. An empty array (or only\n * warnings) means the recipe is valid.\n *\n * Accepts `unknown` so it can be used on untrusted input: shape problems are\n * reported as INVALID_SHAPE instead of throwing.\n */\nexport function validate(recipe: Recipe | unknown, options: RecipeOptions = {}): Diagnostic[] {\n const out: Diagnostic[] = [];\n const shape = (path: Path, expected: string) =>\n out.push(diagnostic('INVALID_SHAPE', { expected }, { path }));\n\n if (!isObject(recipe)) {\n shape([], 'an object with prerequisites, ingredients and steps');\n return out;\n }\n const { prerequisites, ingredients, steps } = recipe;\n if (!Array.isArray(prerequisites)) shape(['prerequisites'], 'an array of strings');\n if (!Array.isArray(ingredients)) shape(['ingredients'], 'an array');\n if (!Array.isArray(steps)) shape(['steps'], 'an array');\n if (!Array.isArray(prerequisites) || !Array.isArray(ingredients) || !Array.isArray(steps)) {\n return out;\n }\n\n prerequisites.forEach((text, i) => {\n if (typeof text !== 'string') shape(['prerequisites', i], 'a string');\n else if (text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: ['prerequisites', i] }));\n });\n\n // --- keys -------------------------------------------------------------------------------\n const kinds = new Map<string, { kind: 'ingredient' | 'step'; index: number }>();\n const checkKey = (key: unknown, path: Path, kind: 'ingredient' | 'step', index: number) => {\n if (typeof key !== 'string') {\n shape([...path, 'key'], 'a string');\n return;\n }\n if (!KEY_PATTERN.test(key)) {\n out.push(diagnostic('INVALID_KEY', { key }, { path: [...path, 'key'] }));\n }\n if (kinds.has(key)) {\n out.push(diagnostic('DUPLICATE_KEY', { key }, { path: [...path, 'key'] }));\n } else {\n kinds.set(key, { kind, index });\n }\n };\n\n // --- ingredients ------------------------------------------------------------------------\n const units = unitLookup(options.units);\n let shapeOk = true;\n ingredients.forEach((ingredient, i) => {\n const path: Path = ['ingredients', i];\n if (!isObject(ingredient)) {\n shape(path, 'an ingredient object');\n shapeOk = false;\n return;\n }\n checkKey(ingredient.key, path, 'ingredient', i);\n if (typeof ingredient.name !== 'string') shape([...path, 'name'], 'a string');\n else if (ingredient.name.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'name'] }));\n\n const { quantity, unit } = ingredient;\n if (quantity !== undefined) {\n if (typeof quantity !== 'number') shape([...path, 'quantity'], 'a number');\n else if (!Number.isFinite(quantity) || quantity <= 0) {\n out.push(\n diagnostic(\n 'INVALID_QUANTITY',\n { quantity: String(quantity) },\n { path: [...path, 'quantity'] },\n ),\n );\n }\n }\n if (unit !== undefined) {\n if (typeof unit !== 'string') shape([...path, 'unit'], 'a string');\n else if (unit.trim() !== '') {\n if (quantity === undefined) {\n out.push(diagnostic('UNIT_WITHOUT_QUANTITY', { unit }, { path: [...path, 'unit'] }));\n }\n if (!units.has(unit.trim().toLowerCase())) {\n out.push(diagnostic('UNKNOWN_UNIT', { unit }, { path: [...path, 'unit'] }, 'warning'));\n }\n }\n }\n });\n\n // --- steps ------------------------------------------------------------------------------\n steps.forEach((step, i) => {\n const path: Path = ['steps', i];\n if (!isObject(step)) {\n shape(path, 'a step object');\n shapeOk = false;\n return;\n }\n checkKey(step.key, path, 'step', i);\n if (typeof step.text !== 'string') shape([...path, 'text'], 'a string');\n else if (step.text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'text'] }));\n if (!Array.isArray(step.inputs) || step.inputs.some((input) => typeof input !== 'string')) {\n shape([...path, 'inputs'], 'an array of keys');\n shapeOk = false;\n }\n });\n\n if (!shapeOk || steps.length === 0) return out; // drafts: no tree rules\n\n // --- tree rules -------------------------------------------------------------------------\n const consumers = new Map<string, string>();\n steps.forEach((rawStep, i) => {\n const step = rawStep as { key: string; inputs: string[] };\n const path: Path = ['steps', i];\n if (step.inputs.length === 0) {\n out.push(diagnostic('NO_INPUT', { key: String(step.key) }, { path: [...path, 'inputs'] }));\n }\n step.inputs.forEach((input, j) => {\n const inputPath: Path = [...path, 'inputs', j];\n const target = kinds.get(input);\n if (!target) {\n out.push(diagnostic('UNKNOWN_REFERENCE', { key: input }, { path: inputPath }));\n return;\n }\n if (target.kind === 'step' && target.index >= i) {\n out.push(\n diagnostic(\n 'FORWARD_REFERENCE',\n { key: input, step: String(step.key) },\n { path: inputPath },\n ),\n );\n return;\n }\n const consumer = consumers.get(input);\n if (consumer !== undefined) {\n out.push(diagnostic('MULTIPLE_CONSUMERS', { key: input, consumer }, { path: inputPath }));\n return;\n }\n consumers.set(input, String(step.key));\n });\n });\n\n ingredients.forEach((ingredient, i) => {\n const key = (ingredient as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('UNCONSUMED', { key }, { path: ['ingredients', i] }));\n }\n });\n\n const root = String((steps[steps.length - 1] as { key: unknown }).key);\n steps.slice(0, -1).forEach((step, i) => {\n const key = (step as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('MULTIPLE_ROOTS', { key, root }, { path: ['steps', i] }));\n }\n });\n\n return out;\n}\n","import { hasErrors } from './diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from './model';\nimport { validate } from './validate';\n\n/**\n * Returns a cleaned copy of a recipe. Never throws (on well-shaped input):\n *\n * - trims prerequisites, keys, names, step texts and units;\n * - drops empty units and canonicalises known ones (`KG` → `kg`);\n * - omits `quantity`/`unit` properties that are undefined (stable deep equality);\n * - when the recipe is a valid tree, reorders ingredients by depth-first\n * traversal from the final step (inputs in their given order), which makes\n * the ingredients of every step contiguous rows.\n *\n * Steps keep their given order. Drafts (no steps) and invalid recipes keep the\n * given ingredient order — call {@link validate} to know why a recipe is invalid.\n */\nexport function normalize(recipe: Recipe, options: RecipeOptions = {}): Recipe {\n const units = unitLookup(options.units);\n const prerequisites = recipe.prerequisites.map((text) => text.trim());\n const ingredients: Ingredient[] = recipe.ingredients.map((ingredient) => {\n const clean: Ingredient = { key: ingredient.key.trim(), name: ingredient.name.trim() };\n if (ingredient.quantity !== undefined) clean.quantity = ingredient.quantity;\n const unit = ingredient.unit?.trim();\n if (unit) clean.unit = units.get(unit.toLowerCase()) ?? unit;\n return clean;\n });\n const steps: Step[] = recipe.steps.map((step) => ({\n key: step.key.trim(),\n text: step.text.trim(),\n inputs: step.inputs.map((input) => input.trim()),\n }));\n const cleaned: Recipe = { prerequisites, ingredients, steps };\n\n if (steps.length === 0 || hasErrors(validate(cleaned, options))) return cleaned;\n return { ...cleaned, ingredients: treeOrder(cleaned) };\n}\n\n/** Ingredients in depth-first order from the final step. Assumes a valid tree. */\nexport function treeOrder(recipe: Recipe): Ingredient[] {\n const ingredientByKey = new Map(recipe.ingredients.map((i) => [i.key, i]));\n const stepByKey = new Map(recipe.steps.map((s) => [s.key, s]));\n const ordered: Ingredient[] = [];\n const visit = (step: Step) => {\n for (const input of step.inputs) {\n const ingredient = ingredientByKey.get(input);\n if (ingredient) ordered.push(ingredient);\n else {\n const child = stepByKey.get(input);\n if (child) visit(child);\n }\n }\n };\n const root = recipe.steps[recipe.steps.length - 1];\n if (root) visit(root);\n return ordered;\n}\n","import type { DecimalSeparator, Recipe } from './model';\n\nexport interface FormatQuantityOptions {\n decimalSeparator?: DecimalSeparator;\n /** Maximum number of decimals (default 2). Trailing zeros are removed. */\n maxDecimals?: number;\n}\n\n/**\n * Formats a quantity for display: rounded to `maxDecimals` (default 2),\n * trailing zeros removed. Positive values that would round to 0 keep two\n * significant digits instead (e.g. 0.004 → \"0.004\"), so a non-zero amount is\n * never shown as \"0\".\n */\nexport function formatQuantity(value: number, options: FormatQuantityOptions = {}): string {\n const { decimalSeparator = '.', maxDecimals = 2 } = options;\n if (!Number.isFinite(value)) return String(value);\n let text: string;\n const rounded = Number(value.toFixed(maxDecimals));\n if (rounded === 0 && value !== 0) {\n text = trimZeros(Math.abs(value) < 1e-6 ? value.toExponential(1) : value.toPrecision(2));\n } else {\n text = trimZeros(rounded.toFixed(maxDecimals));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\n/**\n * Formats a quantity losslessly (used by the serializer): no rounding, never\n * exponent notation for usual cooking magnitudes.\n */\nexport function formatExactQuantity(\n value: number,\n decimalSeparator: DecimalSeparator = '.',\n): string {\n let text = String(value);\n if (/e/i.test(text)) {\n // 1e-7 / 1e21 style: expand. Precision beyond 20 decimals is not representable in the DSL.\n text = trimZeros(value.toFixed(20));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\nfunction trimZeros(text: string): string {\n if (!text.includes('.') || /e/i.test(text)) return text;\n return text.replace(/0+$/, '').replace(/\\.$/, '');\n}\n\n/**\n * Parses a DSL quantity: integer, decimal with `.` or `,`, or simple fraction `a/b`.\n * Returns `undefined` when the text is not a quantity. May return 0 or Infinity\n * (e.g. \"0\", \"1/0\"); callers report those as INVALID_QUANTITY.\n */\nexport function parseQuantity(text: string): number | undefined {\n const fraction = /^(\\d+)\\/(\\d+)$/.exec(text);\n if (fraction) return Number(fraction[1]) / Number(fraction[2]);\n if (/^\\d+(?:[.,]\\d+)?$/.test(text)) return Number(text.replace(',', '.'));\n return undefined;\n}\n\n/** Regex source of a DSL quantity, shared by the lexer and the label parser. */\nexport const QUANTITY_SOURCE = String.raw`\\d+\\/\\d+|\\d+(?:[.,]\\d+)?`;\n\n/**\n * Multiplies every ingredient quantity by `factor` (e.g. 6/4 to go from 4 to\n * 6 servings). Quantities are not rounded; use {@link formatQuantity} to display.\n * Throws a RangeError if `factor` is not a finite number > 0.\n */\nexport function scale(recipe: Recipe, factor: number): Recipe {\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new RangeError(`scale factor must be a finite number > 0, got ${factor}`);\n }\n return {\n prerequisites: [...recipe.prerequisites],\n ingredients: recipe.ingredients.map((ingredient) =>\n ingredient.quantity === undefined\n ? { ...ingredient }\n : { ...ingredient, quantity: ingredient.quantity * factor },\n ),\n steps: recipe.steps.map((step) => ({ ...step, inputs: [...step.inputs] })),\n };\n}\n","import { parseQuantity, QUANTITY_SOURCE } from '../quantity';\n\nexport interface ParsedIngredientLabel {\n name: string;\n quantity?: number;\n unit?: string;\n /** Original quantity text, kept for diagnostics. */\n quantityText?: string;\n}\n\nconst LEADING_QUANTITY = new RegExp(String.raw`^(?:${QUANTITY_SOURCE})`);\nconst QUOTED_PREFIX = new RegExp(String.raw`^(${QUANTITY_SOURCE})(?:\\s*(\\S+))?$`);\n\n/**\n * Splits an ingredient label into quantity, unit and name.\n *\n * Unquoted `[1,5 kg Pommes de terre]`:\n * - a leading quantity (`3`, `1.5`, `1,5`, `1/2`) is taken when followed by\n * a blank, a letter or the end of the label (so `4-épices` stays a name);\n * - the next word is the unit if it is in `units` (case-insensitive, may be\n * glued: `500g`) **and** a name follows it (`[500 g]` = 500 × \"g\");\n * - the rest, trimmed, is the name.\n *\n * Quoted `[1,5 kg \"Pommes (vapeur\"]` / `[\"4 épices\"]`: the quoted string is\n * the name, verbatim; the prefix is the quantity and optional unit.\n *\n * Quantities may come out as 0 or Infinity; the caller reports them.\n *\n * @param units lower-case → canonical spelling.\n */\nexport function parseIngredientLabel(\n label: { value: string; quoted: boolean; prefix: string },\n units: ReadonlyMap<string, string>,\n): ParsedIngredientLabel {\n if (label.quoted) {\n const name = label.value.trim();\n const match = QUOTED_PREFIX.exec(label.prefix.trim());\n if (!match) return { name };\n const result: ParsedIngredientLabel = {\n name,\n quantityText: match[1]!,\n quantity: parseQuantity(match[1]!),\n };\n if (match[2] !== undefined) result.unit = units.get(match[2].toLowerCase()) ?? match[2];\n return result;\n }\n\n const text = label.value.trim();\n const quantityMatch = LEADING_QUANTITY.exec(text);\n if (!quantityMatch) return { name: text };\n const quantityText = quantityMatch[0];\n let rest = text.slice(quantityText.length);\n if (rest !== '' && !/^[\\s\\p{L}]/u.test(rest)) return { name: text };\n\n const result: ParsedIngredientLabel = {\n name: '',\n quantityText,\n quantity: parseQuantity(quantityText),\n };\n rest = rest.trimStart();\n const unitMatch = /^(\\S+)(\\s+|$)/.exec(rest);\n if (unitMatch) {\n const unit = units.get(unitMatch[1]!.toLowerCase());\n const after = rest.slice(unitMatch[0].length).trim();\n if (unit !== undefined && after !== '') {\n result.unit = unit;\n rest = after;\n }\n }\n result.name = rest.trim();\n return result;\n}\n","import { diagnostic, type Diagnostic } from '../diagnostics';\nimport { QUANTITY_SOURCE } from '../quantity';\n\n/** Position of a token on its line. Columns are 1-based UTF-16 offsets, `endColumn` exclusive. */\nexport interface Span {\n line: number;\n column: number;\n endColumn: number;\n}\n\nexport interface IdentToken extends Span {\n type: 'ident';\n value: string;\n}\n\nexport interface LabelToken extends Span {\n type: 'label';\n /** `square` = `[ingredient]`, `round` = `(step)`. */\n shape: 'square' | 'round';\n /** Label text: raw content between the delimiters, or the unescaped quoted string. */\n value: string;\n quoted: boolean;\n /** Square labels only: the `quantity [unit]` written before a quoted name (may be ''). */\n prefix: string;\n}\n\nexport interface PunctToken extends Span {\n type: 'amp' | 'arrow';\n}\n\nexport interface BeforeToken extends Span {\n type: 'before';\n text: string;\n quoted: boolean;\n}\n\nexport type Token = IdentToken | LabelToken | PunctToken | BeforeToken;\n\nexport interface LexedLine {\n line: number;\n tokens: Token[];\n /** True when the line has a lexical error; its tokens are then incomplete. */\n failed: boolean;\n}\n\nexport interface LexResult {\n lines: LexedLine[];\n diagnostics: Diagnostic[];\n}\n\nconst IDENT_CHAR = /[A-Za-z0-9_-]/;\nconst CLOSE = { square: ']', round: ')' } as const;\nconst OPEN = { square: '[', round: '(' } as const;\n\n/** Splits a document into lines (`\\n`, `\\r\\n` or `\\r`). */\nexport function splitLines(source: string): string[] {\n return source.split(/\\r\\n|\\r|\\n/);\n}\n\n/**\n * Tokenizes a whole document, line by line. Comments (`%%` outside labels and\n * quoted strings) and blank lines produce no tokens. A lexical error stops\n * the current line (marked `failed`) but not the following ones.\n *\n * @param units lower-cased unit vocabulary, used to recognise `[2 kg \"quoted name\"]`.\n */\nexport function lex(source: string, units: ReadonlySet<string>): LexResult {\n const diagnostics: Diagnostic[] = [];\n const lines = splitLines(source).map((text, i) => lexLine(text, i + 1, units, diagnostics));\n return { lines, diagnostics };\n}\n\nexport function lexLine(\n text: string,\n line: number,\n units: ReadonlySet<string>,\n diagnostics: Diagnostic[],\n): LexedLine {\n const tokens: Token[] = [];\n let pos = 0;\n const at = (offset = 0) => text[pos + offset] ?? '';\n const fail = (\n code: Parameters<typeof diagnostic>[0],\n params: Record<string, string>,\n start: number,\n end: number,\n ) => {\n diagnostics.push(\n diagnostic(code, params, { line, column: start + 1, endLine: line, endColumn: end + 1 }),\n );\n return { line, tokens, failed: true };\n };\n const skipSpaces = () => {\n while (pos < text.length && /\\s/.test(at())) pos++;\n };\n\n skipSpaces();\n\n // `before: text` statement (only at the start of a line).\n const beforeMatch = /^before\\s*:/.exec(text.slice(pos));\n if (beforeMatch) {\n const start = pos;\n pos += beforeMatch[0].length;\n skipSpaces();\n if (at() === '\"') {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) return fail('UNTERMINATED_STRING', {}, pos, text.length);\n pos = quoted.end;\n skipSpaces();\n if (pos < text.length && !text.startsWith('%%', pos)) {\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'end of line', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n tokens.push({\n type: 'before',\n text: quoted.value,\n quoted: true,\n line,\n column: start + 1,\n endColumn: quoted.end + 1,\n });\n } else {\n const comment = text.indexOf('%%', pos);\n const end = comment === -1 ? text.length : comment;\n tokens.push({\n type: 'before',\n text: text.slice(pos, end).trim(),\n quoted: false,\n line,\n column: start + 1,\n endColumn: end + 1,\n });\n }\n return { line, tokens, failed: false };\n }\n\n while (pos < text.length) {\n skipSpaces();\n if (pos >= text.length) break;\n const start = pos;\n const ch = at();\n\n if (text.startsWith('%%', pos)) break; // comment until end of line\n if (text.startsWith('-->', pos)) {\n pos += 3;\n tokens.push({ type: 'arrow', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (ch === '&') {\n pos++;\n tokens.push({ type: 'amp', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (IDENT_CHAR.test(ch)) {\n while (pos < text.length && IDENT_CHAR.test(at()) && !text.startsWith('-->', pos)) pos++;\n tokens.push({\n type: 'ident',\n value: text.slice(start, pos),\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n if (ch === '[' || ch === '(') {\n const shape = ch === '[' ? 'square' : 'round';\n const label = readLabel(text, pos, shape, units);\n if (!label.ok) return fail(label.code, label.params, label.start, label.end);\n pos = label.end;\n tokens.push({\n type: 'label',\n shape,\n value: label.value,\n quoted: label.quoted,\n prefix: label.prefix,\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'a node, \"&\" or \"-->\"', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n return { line, tokens, failed: false };\n}\n\n/** Human description of the character at `pos`, for SYNTAX_ERROR's `found` param. */\nexport function describe(text: string, pos: number): string {\n if (pos >= text.length) return 'end of line';\n return `\"${String.fromCodePoint(text.codePointAt(pos) ?? 0)}\"`;\n}\n\ntype QuotedResult = { ok: true; value: string; end: number } | { ok: false };\n\n/** Reads a `\"…\"` string starting at `start` (which must be `\"`). Escapes: `\\\"` and `\\\\`. */\nexport function readQuoted(text: string, start: number): QuotedResult {\n let value = '';\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\\\\' && (text[pos + 1] === '\"' || text[pos + 1] === '\\\\')) {\n value += text[pos + 1];\n pos += 2;\n continue;\n }\n if (ch === '\"') return { ok: true, value, end: pos + 1 };\n value += ch;\n pos++;\n }\n return { ok: false };\n}\n\ntype LabelResult =\n | { ok: true; value: string; quoted: boolean; prefix: string; end: number }\n | {\n ok: false;\n code: 'UNTERMINATED_LABEL' | 'UNTERMINATED_STRING' | 'SYNTAX_ERROR';\n params: Record<string, string>;\n start: number;\n end: number;\n };\n\nconst QUOTE_PREFIX_SOURCE = String.raw`^\\s*(?:(?:${QUANTITY_SOURCE})(?:\\s*([^\\s\"\\[\\]()]+))?\\s*)?$`;\nconst QUOTE_PREFIX = new RegExp(QUOTE_PREFIX_SOURCE);\n\n/**\n * Reads a label starting at the opening delimiter.\n *\n * - Unquoted: everything up to the matching closing delimiter; only the label's\n * own delimiter kind is counted for nesting (`[Ail (gousse)]`, `(Mix (well))`).\n * - Quoted: `(\"text\")`, `[\"name\"]`, or for ingredients `[qty unit \"name\"]` —\n * a `\"` opens a quoted string when it is the first non-blank character, or\n * (square labels) when everything before it is a quantity optionally\n * followed by a known unit. Any other `\"` is a literal character.\n */\nfunction readLabel(\n text: string,\n start: number,\n shape: 'square' | 'round',\n units: ReadonlySet<string>,\n): LabelResult {\n const open = OPEN[shape];\n const close = CLOSE[shape];\n let depth = 0;\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\"') {\n const before = text.slice(start + 1, pos);\n const opensQuote =\n before.trim() === '' ||\n (shape === 'square' && depth === 0 && isQuantityPrefix(before, units));\n if (opensQuote) {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) {\n return {\n ok: false,\n code: 'UNTERMINATED_STRING',\n params: {},\n start: pos,\n end: text.length,\n };\n }\n let end = quoted.end;\n while (end < text.length && /\\s/.test(text[end]!)) end++;\n if (text[end] !== close) {\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected: `\"${close}\"`, found: describe(text, end) },\n start: end,\n end: end + 1,\n };\n }\n return { ok: true, value: quoted.value, quoted: true, prefix: before.trim(), end: end + 1 };\n }\n }\n if (ch === open) depth++;\n if (ch === close) {\n if (depth === 0) {\n return {\n ok: true,\n value: text.slice(start + 1, pos),\n quoted: false,\n prefix: '',\n end: pos + 1,\n };\n }\n depth--;\n }\n pos++;\n }\n return { ok: false, code: 'UNTERMINATED_LABEL', params: { close }, start, end: text.length };\n}\n\nfunction isQuantityPrefix(before: string, units: ReadonlySet<string>): boolean {\n const match = QUOTE_PREFIX.exec(before);\n if (!match || before.trim() === '') return false;\n return match[1] === undefined || units.has(match[1].toLowerCase());\n}\n","import { diagnostic, hasErrors, type Diagnostic, type DiagnosticCode } from '../diagnostics';\nimport {\n MAX_KEY_LENGTH,\n unitLookup,\n type Ingredient,\n type Recipe,\n type RecipeOptions,\n type Step,\n} from '../model';\nimport { normalize } from '../normalize';\nimport { validate } from '../validate';\nimport { parseIngredientLabel } from './label';\nimport { lex, type IdentToken, type LabelToken, type Span, type Token } from './lexer';\n\nexport type ParseResult =\n | { ok: true; recipe: Recipe; diagnostics: Diagnostic[] }\n | { ok: false; diagnostics: Diagnostic[]; partial?: Recipe };\n\nexport type ParseOptions = Pick<RecipeOptions, 'units'>;\n\n/** One occurrence of a node in the text: `id` or `id[label]` / `id(label)`. */\ninterface NodeRef {\n key: string;\n ident: IdentToken;\n label?: LabelToken;\n span: Span;\n}\n\ninterface NodeInfo {\n key: string;\n /** Order of first mention in the document. */\n seq: number;\n first: NodeRef;\n def?: NodeRef;\n kind?: 'ingredient' | 'step';\n ingredient?: Ingredient;\n text?: string;\n inputs: string[];\n consumer?: string;\n}\n\ninterface Edge {\n from: NodeRef;\n to: NodeRef;\n}\n\n/**\n * Codes that make tree-structure checks (NO_INPUT, CYCLE, UNCONSUMED,\n * MULTIPLE_ROOTS) meaningless: they would only cascade. Tree rules are checked\n * once the document is syntactically valid and every reference resolves.\n */\nconst BLOCKING: ReadonlySet<DiagnosticCode> = new Set([\n 'SYNTAX_ERROR',\n 'UNTERMINATED_LABEL',\n 'UNTERMINATED_STRING',\n 'FAN_OUT',\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n]);\n\n/**\n * Parses a tabular-recipe document.\n *\n * On success, `recipe` is normalized (see {@link normalize}). On failure,\n * every diagnostic found is returned (sorted by position) together with a\n * best-effort `partial` recipe, handy for live previews.\n */\nexport function parse(source: string, options: ParseOptions = {}): ParseResult {\n const unitMap = unitLookup(options.units);\n const { lines, diagnostics } = lex(source, new Set(unitMap.keys()));\n const nodes = new Map<string, NodeInfo>();\n const prerequisites: string[] = [];\n const edges: Edge[] = [];\n const suppressed = new Set<string>(); // idents seen on failed lines: no UNDEFINED_NODE for them\n let seq = 0;\n\n const at = (span: Span) => ({\n line: span.line,\n column: span.column,\n endLine: span.line,\n endColumn: span.endColumn,\n });\n const report = (code: DiagnosticCode, params: Record<string, string | number>, span: Span) =>\n diagnostics.push(diagnostic(code, params, at(span)));\n\n const register = (ref: NodeRef): NodeInfo => {\n let info = nodes.get(ref.key);\n if (!info) {\n info = { key: ref.key, seq: seq++, first: ref, inputs: [] };\n nodes.set(ref.key, info);\n if (ref.key.length > MAX_KEY_LENGTH) report('INVALID_KEY', { key: ref.key }, ref.ident);\n }\n if (!ref.label) return info;\n if (info.def) {\n report('REDEFINED_NODE', { key: ref.key, firstLine: info.def.ident.line }, ref.ident);\n return info;\n }\n info.def = ref;\n if (ref.label.shape === 'square') {\n info.kind = 'ingredient';\n const parsed = parseIngredientLabel(ref.label, unitMap);\n const ingredient: Ingredient = { key: ref.key, name: parsed.name };\n if (parsed.quantity !== undefined) {\n if (!Number.isFinite(parsed.quantity) || parsed.quantity <= 0) {\n report('INVALID_QUANTITY', { quantity: parsed.quantityText ?? '' }, ref.label);\n } else {\n ingredient.quantity = parsed.quantity;\n if (parsed.unit !== undefined) ingredient.unit = parsed.unit;\n }\n }\n if (parsed.name === '') report('EMPTY_TEXT', {}, ref.label);\n info.ingredient = ingredient;\n } else {\n info.kind = 'step';\n info.text = ref.label.value.trim();\n if (info.text === '') report('EMPTY_TEXT', {}, ref.label);\n }\n return info;\n };\n\n for (const lexed of lines) {\n if (lexed.failed) {\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n const statement = parseStatement(lexed.tokens);\n if (!statement.ok) {\n report(statement.code, statement.params, statement.span);\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n if (statement.kind === 'empty') continue;\n if (statement.kind === 'before') {\n if (statement.token.text.trim() === '') report('EMPTY_TEXT', {}, statement.token);\n prerequisites.push(statement.token.text.trim());\n continue;\n }\n if (statement.kind === 'definition') {\n register(statement.node);\n continue;\n }\n // Edge statement: sources --> t1 --> t2 ...\n statement.sources.forEach(register);\n let previous = statement.sources;\n for (const target of statement.targets) {\n register(target);\n if (target.label?.shape === 'square') {\n report('INGREDIENT_AS_TARGET', { key: target.key }, target.span);\n } else {\n for (const from of previous) edges.push({ from, to: target });\n }\n previous = [target];\n }\n if (statement.fanOut) report('FAN_OUT', {}, statement.fanOut);\n }\n\n // --- resolution -------------------------------------------------------------------------\n for (const info of nodes.values()) {\n if (!info.def && !suppressed.has(info.key)) {\n report('UNDEFINED_NODE', { key: info.key }, info.first.ident);\n }\n }\n for (const edge of edges) {\n const from = nodes.get(edge.from.key)!;\n const to = nodes.get(edge.to.key)!;\n if (!from.def || !to.def) continue;\n if (to.kind === 'ingredient') {\n report('INGREDIENT_AS_TARGET', { key: to.key }, edge.to.span);\n continue;\n }\n if (from.consumer !== undefined) {\n report('MULTIPLE_CONSUMERS', { key: from.key, consumer: from.consumer }, edge.from.span);\n continue;\n }\n from.consumer = to.key;\n to.inputs.push(from.key);\n }\n\n const all = [...nodes.values()].filter((n) => n.def).sort((a, b) => a.seq - b.seq);\n const ingredients = all.filter((n) => n.kind === 'ingredient');\n const stepNodes = all.filter((n) => n.kind === 'step');\n const order = topologicalOrder(stepNodes, nodes);\n\n // --- tree rules ---------------------------------------------------------------------------\n const blocked = diagnostics.some((d) => BLOCKING.has(d.code));\n if (!blocked && stepNodes.length > 0) {\n for (const step of stepNodes) {\n if (step.inputs.length === 0) report('NO_INPUT', { key: step.key }, step.def!.span);\n }\n if (order.cyclic.length > 0) {\n const first = order.cyclic[0]!;\n report('CYCLE', { keys: order.cyclic.map((s) => s.key).join(', ') }, first.def!.span);\n }\n for (const ingredient of ingredients) {\n if (ingredient.consumer === undefined) {\n report('UNCONSUMED', { key: ingredient.key }, ingredient.def!.span);\n }\n }\n const roots = order.sorted.filter((s) => s.consumer === undefined);\n const root = roots[roots.length - 1];\n for (const extra of roots.slice(0, -1)) {\n report('MULTIPLE_ROOTS', { key: extra.key, root: root!.key }, extra.def!.span);\n }\n }\n\n const recipe: Recipe = {\n prerequisites,\n ingredients: ingredients.map((n) => n.ingredient!),\n steps: [...order.sorted, ...order.cyclic].map((n): Step => ({\n key: n.key,\n text: n.text!,\n inputs: n.inputs,\n })),\n };\n\n diagnostics.sort((a, b) => (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0));\n if (hasErrors(diagnostics)) {\n return { ok: false, diagnostics, partial: normalize(recipe, options) };\n }\n const normalized = normalize(recipe, options);\n // Safety net: the DSL checks above mirror validate(); anything left is a bug, but report it.\n const leftovers = validate(normalized, options).filter((d) => d.severity === 'error');\n if (leftovers.length > 0) return { ok: false, diagnostics: leftovers, partial: normalized };\n return { ok: true, recipe: normalized, diagnostics: [] };\n}\n\n/**\n * Stable topological order of steps: a step comes after the steps it consumes;\n * ties are broken by order of first mention. Steps on a cycle are returned apart.\n */\nfunction topologicalOrder(steps: NodeInfo[], nodes: Map<string, NodeInfo>) {\n const pending = new Map<string, number>();\n for (const step of steps) {\n pending.set(step.key, step.inputs.filter((input) => nodes.get(input)?.kind === 'step').length);\n }\n const sorted: NodeInfo[] = [];\n const done = new Set<string>();\n // Steps are few (< 100): a simple repeated scan keeps the tie-breaking obvious.\n for (;;) {\n const next = steps.find((s) => !done.has(s.key) && pending.get(s.key) === 0);\n if (!next) break;\n done.add(next.key);\n sorted.push(next);\n if (next.consumer !== undefined && pending.has(next.consumer)) {\n pending.set(next.consumer, pending.get(next.consumer)! - 1);\n }\n }\n return { sorted, cyclic: steps.filter((s) => !done.has(s.key)) };\n}\n\n// --- statements -----------------------------------------------------------------------------\n\ntype Statement =\n | { ok: true; kind: 'empty' }\n | { ok: true; kind: 'before'; token: Extract<Token, { type: 'before' }> }\n | { ok: true; kind: 'definition'; node: NodeRef }\n | { ok: true; kind: 'edges'; sources: NodeRef[]; targets: NodeRef[]; fanOut?: Span }\n | { ok: false; code: DiagnosticCode; params: Record<string, string>; span: Span };\n\nfunction found(token: Token | undefined): string {\n if (!token) return 'end of line';\n switch (token.type) {\n case 'ident':\n return `\"${token.value}\"`;\n case 'label':\n return token.shape === 'square' ? '\"[\"' : '\"(\"';\n case 'amp':\n return '\"&\"';\n case 'arrow':\n return '\"-->\"';\n case 'before':\n return '\"before:\"';\n }\n}\n\n/**\n * Grammar of one line (tokens already lexed):\n *\n * statement = before | definition | edges\n * definition = node-def\n * edges = node ( \"&\" node )* ( \"-->\" node )+\n * node = IDENT [ LABEL ]\n */\nfunction parseStatement(tokens: Token[]): Statement {\n if (tokens.length === 0) return { ok: true, kind: 'empty' };\n const first = tokens[0]!;\n if (first.type === 'before') return { ok: true, kind: 'before', token: first };\n\n let pos = 0;\n const lastSpan = tokens[tokens.length - 1]!;\n const eol: Span = {\n line: lastSpan.line,\n column: lastSpan.endColumn,\n endColumn: lastSpan.endColumn + 1,\n };\n const error = (expected: string): Statement => {\n const token = tokens[pos];\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected, found: found(token) },\n span: token ?? eol,\n };\n };\n const readNode = (): NodeRef | undefined => {\n const ident = tokens[pos];\n if (ident?.type !== 'ident') return undefined;\n pos++;\n const label = tokens[pos];\n if (label?.type === 'label') {\n pos++;\n return {\n key: ident.value,\n ident,\n label,\n span: { line: ident.line, column: ident.column, endColumn: label.endColumn },\n };\n }\n return { key: ident.value, ident, span: ident };\n };\n\n const sources: NodeRef[] = [];\n const firstNode = readNode();\n if (!firstNode) return error('a node id');\n sources.push(firstNode);\n while (tokens[pos]?.type === 'amp') {\n pos++;\n const node = readNode();\n if (!node) return error('a node id after \"&\"');\n sources.push(node);\n }\n\n if (pos === tokens.length) {\n if (sources.length === 1 && firstNode.label)\n return { ok: true, kind: 'definition', node: firstNode };\n return error(sources.length === 1 ? '\"[\", \"(\" or \"-->\"' : '\"-->\"');\n }\n\n const targets: NodeRef[] = [];\n while (tokens[pos]?.type === 'arrow') {\n pos++;\n const node = readNode();\n if (!node) return error('a step after \"-->\"');\n targets.push(node);\n }\n if (targets.length === 0) return error('\"&\" or \"-->\"');\n if (pos === tokens.length) return { ok: true, kind: 'edges', sources, targets };\n const extra = tokens[pos]!;\n if (extra.type === 'amp') return { ok: true, kind: 'edges', sources, targets, fanOut: extra };\n return error('\"-->\" or end of line');\n}\n","import type { Diagnostic } from '../diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from '../model';\nimport { normalize } from '../normalize';\nimport { formatExactQuantity } from '../quantity';\nimport { parseIngredientLabel } from './label';\nimport { lexLine } from './lexer';\n\nexport type SerializeOptions = RecipeOptions;\n\n/**\n * Writes a recipe in canonical DSL form:\n *\n * 1. `before: …` lines, then a blank line if anything follows;\n * 2. drafts (no steps): one ingredient definition per line;\n * 3. otherwise one line per step, in step order: `inputs --> key(text)`,\n * each ingredient defined inline at its use, earlier steps as bare ids.\n * A step whose single input is the step just before it is appended to\n * that line as a chain: `… --> a(x) --> b(y)`.\n *\n * Labels are quoted only when needed (the serializer checks that each label\n * reads back identically). Quantities are written losslessly with\n * `decimalSeparator` (default `.`). The recipe is normalized first, so\n * `parse(serialize(r)).recipe` deep-equals `normalize(r)` for every valid r.\n *\n * Invalid recipes are still written, best effort (unused ingredients become\n * standalone definitions at the end), so an editor can switch to text mode\n * and let the parser point at the problems.\n */\nexport function serialize(recipe: Recipe, options: SerializeOptions = {}): string {\n const r = normalize(recipe, options);\n const separator = options.decimalSeparator ?? '.';\n const unitSet = new Set(unitLookup(options.units).keys());\n const unitMap = unitLookup(options.units);\n const lines: string[] = r.prerequisites.map((text) => `before: ${prerequisiteText(text)}`);\n const body: string[] = [];\n\n const ingredientByKey = new Map(r.ingredients.map((i) => [i.key, i]));\n const defined = new Set<string>();\n const ingredientRef = (ingredient: Ingredient) => {\n if (defined.has(ingredient.key)) return ingredient.key;\n defined.add(ingredient.key);\n return `${ingredient.key}[${ingredientLabel(ingredient, separator, unitSet, unitMap)}]`;\n };\n const stepRef = (step: Step) => `${step.key}(${stepLabel(step.text, unitSet)})`;\n const inputRef = (key: string) => {\n const ingredient = ingredientByKey.get(key);\n return ingredient ? ingredientRef(ingredient) : key;\n };\n\n for (let i = 0; i < r.steps.length; i++) {\n let step = r.steps[i]!;\n let line =\n step.inputs.length > 0\n ? `${step.inputs.map(inputRef).join(' & ')} --> ${stepRef(step)}`\n : stepRef(step);\n let next = r.steps[i + 1];\n while (next && next.inputs.length === 1 && next.inputs[0] === step.key) {\n line += ` --> ${stepRef(next)}`;\n step = next;\n i++;\n next = r.steps[i + 1];\n }\n body.push(line);\n }\n for (const ingredient of r.ingredients) {\n if (!defined.has(ingredient.key)) body.push(ingredientRef(ingredient));\n }\n\n if (lines.length > 0 && body.length > 0) lines.push('');\n lines.push(...body);\n return lines.length > 0 ? `${lines.join('\\n')}\\n` : '';\n}\n\nfunction quote(text: string): string {\n return `\"${text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\nfunction prerequisiteText(text: string): string {\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(`before: ${text}`, 1, new Set(), diagnostics);\n const token = lexed.tokens[0];\n const readsBack = diagnostics.length === 0 && token?.type === 'before' && token.text === text;\n return readsBack && text !== '' ? text : quote(text);\n}\n\n/** Lexes `x<open>content<close>` and returns the label token if it reads back cleanly. */\nfunction relex(content: string, shape: 'square' | 'round', units: ReadonlySet<string>) {\n const [open, close] = shape === 'square' ? ['[', ']'] : ['(', ')'];\n const source = `x${open}${content}${close}`;\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(source, 1, units, diagnostics);\n const label = lexed.tokens[1];\n if (diagnostics.length > 0 || lexed.tokens.length !== 2 || label?.type !== 'label')\n return undefined;\n return label.endColumn === source.length + 1 ? label : undefined;\n}\n\nfunction stepLabel(text: string, units: ReadonlySet<string>): string {\n const label = relex(text, 'round', units);\n return label && !label.quoted && label.value.trim() === text && text !== '' ? text : quote(text);\n}\n\nfunction ingredientLabel(\n ingredient: Ingredient,\n separator: '.' | ',',\n units: ReadonlySet<string>,\n unitMap: ReadonlyMap<string, string>,\n): string {\n const prefix =\n ingredient.quantity === undefined\n ? ''\n : [formatExactQuantity(ingredient.quantity, separator), ingredient.unit]\n .filter((part) => part !== undefined)\n .join(' ');\n const raw = [prefix, ingredient.name].filter((part) => part !== '').join(' ');\n const label = relex(raw, 'square', units);\n if (label && !label.quoted) {\n const parsed = parseIngredientLabel(label, unitMap);\n if (\n parsed.name === ingredient.name &&\n parsed.quantity === ingredient.quantity &&\n parsed.unit === ingredient.unit &&\n ingredient.name !== ''\n ) {\n return raw;\n }\n }\n return prefix === '' ? quote(ingredient.name) : `${prefix} ${quote(ingredient.name)}`;\n}\n","import { hasErrors } from './diagnostics';\nimport type { Recipe, RecipeOptions, Step } from './model';\nimport { normalize } from './normalize';\nimport { formatQuantity } from './quantity';\nimport { validate } from './validate';\n\nexport type CellKind = 'quantity' | 'ingredient' | 'step';\n\nexport interface Cell {\n kind: CellKind;\n /** Ingredient key (quantity / ingredient cells) or step key. */\n key: string;\n text: string;\n /** 0-based grid position of the top-left corner. */\n row: number;\n col: number;\n rowSpan: number;\n colSpan: number;\n /** True for the final step (rightmost column). */\n final?: boolean;\n}\n\nexport interface Layout {\n /** False when the recipe is not a valid tree: only ingredients are laid out (draft layout). */\n valid: boolean;\n /** 2 (quantity + ingredient) + depth of the final step. */\n columnCount: number;\n /** Prerequisites, rendered as full-width rows above the table. */\n banners: string[];\n /** `rows[r]` = cells that *start* on row r, left to right (HTML table semantics). */\n rows: Cell[][];\n}\n\nexport interface LayoutOptions extends RecipeOptions {\n /** Max decimals of displayed quantities (default 2). */\n maxDecimals?: number;\n /** Display names for units, e.g. `{ pincee: 'pincée', cs: 'c. à s.' }`. Unlisted units show as-is. */\n unitLabels?: Readonly<Record<string, string>>;\n}\n\n/**\n * Computes the tabular grid of a recipe.\n *\n * - Column 0 = quantity, column 1 = ingredient, columns 2… = steps by depth:\n * depth(ingredient) = 0, depth(step) = 1 + max depth of its inputs,\n * step column = 1 + depth. The final step is alone in the last column.\n * - One row per ingredient (normalized depth-first order). A step spans the\n * rows of all the ingredients under it.\n * - No empty cells: a step or ingredient whose consumer sits more than one\n * column to the right stretches (`colSpan`) up to that consumer.\n *\n * Drafts (and invalid recipes, with `valid: false`) get a 2-column layout.\n */\nexport function layout(recipe: Recipe, options: LayoutOptions = {}): Layout {\n const r = normalize(recipe, options);\n const valid = !hasErrors(validate(r, options));\n const quantityText = (quantity?: number, unit?: string) =>\n quantity === undefined\n ? ''\n : [\n formatQuantity(quantity, {\n decimalSeparator: options.decimalSeparator,\n maxDecimals: options.maxDecimals,\n }),\n unit === undefined ? undefined : (options.unitLabels?.[unit] ?? unit),\n ]\n .filter(Boolean)\n .join(' ');\n\n const useTree = valid && r.steps.length > 0;\n const stepByKey = new Map(r.steps.map((s) => [s.key, s]));\n const depth = new Map<string, number>();\n const consumer = new Map<string, Step>();\n const leaves = new Map<string, string[]>(); // step key → ingredient keys under it, in row order\n if (useTree) {\n for (const step of r.steps) {\n // Steps are ordered so that inputs come first: one pass is enough.\n let d = 0;\n const under: string[] = [];\n for (const input of step.inputs) {\n consumer.set(input, step);\n const child = stepByKey.get(input);\n d = Math.max(d, child ? depth.get(input)! : 0);\n under.push(...(child ? leaves.get(input)! : [input]));\n }\n depth.set(step.key, d + 1);\n leaves.set(step.key, under);\n }\n }\n\n const root = r.steps[r.steps.length - 1];\n const columnCount = useTree && root ? 2 + depth.get(root.key)! : 2;\n const columnOf = (stepKey: string) => 1 + depth.get(stepKey)!;\n const rowOf = new Map(r.ingredients.map((ingredient, i) => [ingredient.key, i]));\n\n const rows: Cell[][] = r.ingredients.map((ingredient, row) => {\n const parent = consumer.get(ingredient.key);\n return [\n {\n kind: 'quantity',\n key: ingredient.key,\n text: quantityText(ingredient.quantity, ingredient.unit),\n row,\n col: 0,\n rowSpan: 1,\n colSpan: 1,\n },\n {\n kind: 'ingredient',\n key: ingredient.key,\n text: ingredient.name,\n row,\n col: 1,\n rowSpan: 1,\n colSpan: parent ? columnOf(parent.key) - 1 : 1,\n },\n ];\n });\n\n if (useTree) {\n for (const step of r.steps) {\n const under = leaves.get(step.key)!;\n const row = rowOf.get(under[0]!)!;\n const col = columnOf(step.key);\n const parent = consumer.get(step.key);\n rows[row]!.push({\n kind: 'step',\n key: step.key,\n text: step.text,\n row,\n col,\n rowSpan: under.length,\n colSpan: parent ? columnOf(parent.key) - col : 1,\n ...(parent ? {} : { final: true }),\n });\n }\n for (const cells of rows) cells.sort((a, b) => a.col - b.col);\n }\n\n return { valid, columnCount, banners: [...r.prerequisites], rows };\n}\n","import type { Recipe } from './model';\nimport { layout, type Cell, type LayoutOptions } from './layout';\n\nexport interface RenderHtmlOptions extends LayoutOptions {\n /** CSS class prefix (default `trd`). Must match the stylesheet you use. */\n classPrefix?: string;\n /** Keep the quantity and ingredient columns visible while scrolling horizontally. */\n stickyIngredients?: boolean;\n /** Optional `<caption>` text (e.g. the recipe name). */\n caption?: string;\n}\n\nconst ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\n\n/** Escapes text for HTML element content and attribute values. */\nexport function escapeHtml(text: string): string {\n return text.replace(/[&<>\"']/g, (ch) => ESCAPES[ch]!);\n}\n\n/**\n * Renders a recipe as a standalone HTML table (string), framework-free.\n * All text is escaped. Pair it with `ln-tabular-recipes-dsl/style.css`, or\n * style the classes yourself:\n *\n * `{p}-wrap` (scroll container), `{p}-table` (+ `{p}-sticky`, `{p}-invalid`),\n * `{p}-banner`, `{p}-qty`, `{p}-ingredient`, `{p}-step` (+ `{p}-final`).\n * Cells carry `data-key` with the ingredient/step key.\n */\nexport function renderHtml(recipe: Recipe, options: RenderHtmlOptions = {}): string {\n const p = options.classPrefix ?? 'trd';\n const grid = layout(recipe, options);\n const tableClasses = [`${p}-table`];\n if (options.stickyIngredients) tableClasses.push(`${p}-sticky`);\n if (!grid.valid) tableClasses.push(`${p}-invalid`);\n\n const cellHtml = (cell: Cell) => {\n const classes = [`${p}-${cell.kind === 'quantity' ? 'qty' : cell.kind}`];\n if (cell.final) classes.push(`${p}-final`);\n const attrs = [`class=\"${classes.join(' ')}\"`, `data-key=\"${escapeHtml(cell.key)}\"`];\n if (cell.rowSpan > 1) attrs.push(`rowspan=\"${cell.rowSpan}\"`);\n if (cell.colSpan > 1) attrs.push(`colspan=\"${cell.colSpan}\"`);\n return `<td ${attrs.join(' ')}>${escapeHtml(cell.text)}</td>`;\n };\n\n const parts: string[] = [`<div class=\"${p}-wrap\">`, `<table class=\"${tableClasses.join(' ')}\">`];\n if (options.caption !== undefined)\n parts.push(`<caption>${escapeHtml(options.caption)}</caption>`);\n parts.push('<tbody>');\n for (const banner of grid.banners) {\n parts.push(\n `<tr class=\"${p}-banner-row\"><td class=\"${p}-banner\" colspan=\"${grid.columnCount}\">${escapeHtml(banner)}</td></tr>`,\n );\n }\n for (const cells of grid.rows) parts.push(`<tr>${cells.map(cellHtml).join('')}</tr>`);\n parts.push('</tbody>', '</table>', '</div>');\n return parts.join('\\n');\n}\n"],"mappings":"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","names":["diagnostic","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/model.ts","../src/diagnostics.ts","../src/validate.ts","../src/normalize.ts","../src/quantity.ts","../src/dsl/label.ts","../src/dsl/lexer.ts","../src/dsl/parser.ts","../src/dsl/serializer.ts","../src/layout.ts","../src/html.ts"],"sourcesContent":["/**\n * Core data model of a tabular recipe.\n *\n * A recipe is a tree: ingredients are leaves, steps are nodes and the last\n * step is the root (\"final step\"). `prerequisites` are steps that use no\n * ingredient (e.g. \"Preheat the oven\"); they are rendered as full-width\n * banners above the table.\n */\nexport interface Recipe {\n prerequisites: string[];\n ingredients: Ingredient[];\n steps: Step[];\n}\n\nexport interface Ingredient {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n name: string;\n /** Finite, strictly positive. */\n quantity?: number;\n /** Only meaningful together with `quantity`. */\n unit?: string;\n}\n\nexport interface Step {\n /** Node id, unique across ingredients and steps. See {@link KEY_PATTERN}. */\n key: string;\n text: string;\n /** Keys of the ingredients and/or earlier steps this step combines, in row order. */\n inputs: string[];\n}\n\n/** Allowed node keys: 1 to 40 ASCII letters, digits, `_` or `-`. */\nexport const KEY_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;\n\nexport const MAX_KEY_LENGTH = 40;\n\n/** Default unit vocabulary. Matching is case-insensitive; the spelling below is canonical. */\nexport const DEFAULT_UNITS: readonly string[] = [\n 'pcs',\n 'g',\n 'kg',\n 'ml',\n 'cl',\n 'l',\n 'cs',\n 'cc',\n 'pincee',\n];\n\nexport type DecimalSeparator = '.' | ',';\n\n/** Options shared by most functions of the library. */\nexport interface RecipeOptions {\n /** Unit vocabulary (case-insensitive, canonical spelling taken from this list). */\n units?: readonly string[];\n /** Decimal separator used when *writing* quantities. Parsing always accepts both. Default `.`. */\n decimalSeparator?: DecimalSeparator;\n}\n\n/** Builds a lower-case → canonical spelling lookup for a unit list. */\nexport function unitLookup(units: readonly string[] = DEFAULT_UNITS): Map<string, string> {\n const map = new Map<string, string>();\n for (const unit of units) {\n const lower = unit.toLowerCase();\n if (!map.has(lower)) map.set(lower, unit);\n }\n return map;\n}\n","/**\n * Diagnostics are language-neutral: `code` + `params` identify the problem,\n * `message` is an English default. Consumers translate by providing their own\n * templates to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_CODES = [\n // --- DSL syntax ---\n 'SYNTAX_ERROR',\n 'UNTERMINATED_STRING',\n 'UNTERMINATED_LABEL',\n 'FAN_OUT',\n // --- DSL references ---\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n 'CYCLE',\n // --- JSON model ---\n 'INVALID_SHAPE',\n 'UNKNOWN_REFERENCE',\n 'FORWARD_REFERENCE',\n // --- shared ---\n 'INVALID_KEY',\n 'DUPLICATE_KEY',\n 'EMPTY_TEXT',\n 'INVALID_QUANTITY',\n 'UNIT_WITHOUT_QUANTITY',\n 'UNKNOWN_UNIT',\n // --- tree rules ---\n 'NO_INPUT',\n 'MULTIPLE_CONSUMERS',\n 'UNCONSUMED',\n 'MULTIPLE_ROOTS',\n] as const;\n\nexport type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];\n\nexport type Severity = 'error' | 'warning';\n\nexport type DiagnosticParams = Record<string, string | number>;\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n /** English default message, built from {@link DIAGNOSTIC_MESSAGES}. */\n message: string;\n severity: Severity;\n /** 1-based. DSL diagnostics only. */\n line?: number;\n /** 1-based, in UTF-16 code units (same as JS string indices / CodeMirror). */\n column?: number;\n endLine?: number;\n /** 1-based, exclusive. */\n endColumn?: number;\n /** JSON path of the offending value. Model (JSON) diagnostics only. */\n path?: (string | number)[];\n params?: DiagnosticParams;\n}\n\n/**\n * English message templates. `{name}` placeholders are replaced by `params.name`.\n * Copy this object, translate the values and pass it to {@link formatDiagnostic}.\n */\nexport const DIAGNOSTIC_MESSAGES: Record<DiagnosticCode, string> = {\n SYNTAX_ERROR: 'Syntax error: expected {expected}, found {found}.',\n UNTERMINATED_STRING: 'Unterminated quoted string.',\n UNTERMINATED_LABEL: 'Missing closing \"{close}\".',\n FAN_OUT: 'A node can only feed one step: \"&\" is not allowed after \"-->\".',\n UNDEFINED_NODE: 'Node \"{key}\" is used but never defined.',\n REDEFINED_NODE: 'Node \"{key}\" is already defined on line {firstLine}.',\n INGREDIENT_AS_TARGET: 'Ingredient \"{key}\" cannot be the target of \"-->\"; only steps can.',\n CYCLE: 'Steps form a cycle: {keys}.',\n INVALID_SHAPE: 'Invalid value: expected {expected}.',\n UNKNOWN_REFERENCE: 'Input \"{key}\" does not match any ingredient or step.',\n FORWARD_REFERENCE: 'Input \"{key}\" refers to a step that does not come before \"{step}\".',\n INVALID_KEY: 'Invalid key \"{key}\": use 1 to 40 letters, digits, \"_\" or \"-\".',\n DUPLICATE_KEY: 'Key \"{key}\" is used more than once.',\n EMPTY_TEXT: 'Text must not be empty.',\n INVALID_QUANTITY: 'Invalid quantity \"{quantity}\": it must be a number greater than 0.',\n UNIT_WITHOUT_QUANTITY: 'Unit \"{unit}\" is set without a quantity.',\n UNKNOWN_UNIT: 'Unknown unit \"{unit}\".',\n NO_INPUT: 'Step \"{key}\" has no input.',\n MULTIPLE_CONSUMERS:\n '\"{key}\" is already used by step \"{consumer}\"; each node feeds exactly one step.',\n UNCONSUMED: 'Ingredient \"{key}\" is not used by any step.',\n MULTIPLE_ROOTS:\n 'Step \"{key}\" is not used by any later step; only the final step \"{root}\" may be unused.',\n};\n\n/** Replaces `{name}` placeholders of `template` with `params`. Unknown placeholders are kept. */\nexport function interpolate(template: string, params: DiagnosticParams = {}): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match,\n );\n}\n\n/** Formats a diagnostic with custom (e.g. translated) templates, falling back to English. */\nexport function formatDiagnostic(\n diagnostic: Pick<Diagnostic, 'code' | 'params'>,\n templates: Partial<Record<DiagnosticCode, string>> = DIAGNOSTIC_MESSAGES,\n): string {\n const template = templates[diagnostic.code] ?? DIAGNOSTIC_MESSAGES[diagnostic.code];\n return interpolate(template, diagnostic.params);\n}\n\n/** Internal helper to build a diagnostic with its English message. */\nexport function diagnostic(\n code: DiagnosticCode,\n params: DiagnosticParams,\n location: Omit<Diagnostic, 'code' | 'message' | 'severity' | 'params'> = {},\n severity: Severity = 'error',\n): Diagnostic {\n return {\n code,\n message: interpolate(DIAGNOSTIC_MESSAGES[code], params),\n severity,\n ...location,\n params,\n };\n}\n\nexport function hasErrors(diagnostics: readonly Diagnostic[]): boolean {\n return diagnostics.some((d) => d.severity === 'error');\n}\n","import { diagnostic, type Diagnostic } from './diagnostics';\nimport { KEY_PATTERN, unitLookup, type Recipe, type RecipeOptions } from './model';\n\ntype Path = (string | number)[];\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Validates a recipe given as JSON (e.g. received by an API). Returns every\n * problem found, each with a `path` into the input. An empty array (or only\n * warnings) means the recipe is valid.\n *\n * Accepts `unknown` so it can be used on untrusted input: shape problems are\n * reported as INVALID_SHAPE instead of throwing.\n */\nexport function validate(recipe: Recipe | unknown, options: RecipeOptions = {}): Diagnostic[] {\n const out: Diagnostic[] = [];\n const shape = (path: Path, expected: string) =>\n out.push(diagnostic('INVALID_SHAPE', { expected }, { path }));\n\n if (!isObject(recipe)) {\n shape([], 'an object with prerequisites, ingredients and steps');\n return out;\n }\n const { prerequisites, ingredients, steps } = recipe;\n if (!Array.isArray(prerequisites)) shape(['prerequisites'], 'an array of strings');\n if (!Array.isArray(ingredients)) shape(['ingredients'], 'an array');\n if (!Array.isArray(steps)) shape(['steps'], 'an array');\n if (!Array.isArray(prerequisites) || !Array.isArray(ingredients) || !Array.isArray(steps)) {\n return out;\n }\n\n prerequisites.forEach((text, i) => {\n if (typeof text !== 'string') shape(['prerequisites', i], 'a string');\n else if (text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: ['prerequisites', i] }));\n });\n\n // --- keys -------------------------------------------------------------------------------\n const kinds = new Map<string, { kind: 'ingredient' | 'step'; index: number }>();\n const checkKey = (key: unknown, path: Path, kind: 'ingredient' | 'step', index: number) => {\n if (typeof key !== 'string') {\n shape([...path, 'key'], 'a string');\n return;\n }\n if (!KEY_PATTERN.test(key)) {\n out.push(diagnostic('INVALID_KEY', { key }, { path: [...path, 'key'] }));\n }\n if (kinds.has(key)) {\n out.push(diagnostic('DUPLICATE_KEY', { key }, { path: [...path, 'key'] }));\n } else {\n kinds.set(key, { kind, index });\n }\n };\n\n // --- ingredients ------------------------------------------------------------------------\n const units = unitLookup(options.units);\n let shapeOk = true;\n ingredients.forEach((ingredient, i) => {\n const path: Path = ['ingredients', i];\n if (!isObject(ingredient)) {\n shape(path, 'an ingredient object');\n shapeOk = false;\n return;\n }\n checkKey(ingredient.key, path, 'ingredient', i);\n if (typeof ingredient.name !== 'string') shape([...path, 'name'], 'a string');\n else if (ingredient.name.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'name'] }));\n\n const { quantity, unit } = ingredient;\n if (quantity !== undefined) {\n if (typeof quantity !== 'number') shape([...path, 'quantity'], 'a number');\n else if (!Number.isFinite(quantity) || quantity <= 0) {\n out.push(\n diagnostic(\n 'INVALID_QUANTITY',\n { quantity: String(quantity) },\n { path: [...path, 'quantity'] },\n ),\n );\n }\n }\n if (unit !== undefined) {\n if (typeof unit !== 'string') shape([...path, 'unit'], 'a string');\n else if (unit.trim() !== '') {\n if (quantity === undefined) {\n out.push(diagnostic('UNIT_WITHOUT_QUANTITY', { unit }, { path: [...path, 'unit'] }));\n }\n if (!units.has(unit.trim().toLowerCase())) {\n out.push(diagnostic('UNKNOWN_UNIT', { unit }, { path: [...path, 'unit'] }, 'warning'));\n }\n }\n }\n });\n\n // --- steps ------------------------------------------------------------------------------\n steps.forEach((step, i) => {\n const path: Path = ['steps', i];\n if (!isObject(step)) {\n shape(path, 'a step object');\n shapeOk = false;\n return;\n }\n checkKey(step.key, path, 'step', i);\n if (typeof step.text !== 'string') shape([...path, 'text'], 'a string');\n else if (step.text.trim() === '')\n out.push(diagnostic('EMPTY_TEXT', {}, { path: [...path, 'text'] }));\n if (!Array.isArray(step.inputs) || step.inputs.some((input) => typeof input !== 'string')) {\n shape([...path, 'inputs'], 'an array of keys');\n shapeOk = false;\n }\n });\n\n if (!shapeOk || steps.length === 0) return out; // drafts: no tree rules\n\n // --- tree rules -------------------------------------------------------------------------\n const consumers = new Map<string, string>();\n steps.forEach((rawStep, i) => {\n const step = rawStep as { key: string; inputs: string[] };\n const path: Path = ['steps', i];\n if (step.inputs.length === 0) {\n out.push(diagnostic('NO_INPUT', { key: String(step.key) }, { path: [...path, 'inputs'] }));\n }\n step.inputs.forEach((input, j) => {\n const inputPath: Path = [...path, 'inputs', j];\n const target = kinds.get(input);\n if (!target) {\n out.push(diagnostic('UNKNOWN_REFERENCE', { key: input }, { path: inputPath }));\n return;\n }\n if (target.kind === 'step' && target.index >= i) {\n out.push(\n diagnostic(\n 'FORWARD_REFERENCE',\n { key: input, step: String(step.key) },\n { path: inputPath },\n ),\n );\n return;\n }\n const consumer = consumers.get(input);\n if (consumer !== undefined) {\n out.push(diagnostic('MULTIPLE_CONSUMERS', { key: input, consumer }, { path: inputPath }));\n return;\n }\n consumers.set(input, String(step.key));\n });\n });\n\n ingredients.forEach((ingredient, i) => {\n const key = (ingredient as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('UNCONSUMED', { key }, { path: ['ingredients', i] }));\n }\n });\n\n const root = String((steps[steps.length - 1] as { key: unknown }).key);\n steps.slice(0, -1).forEach((step, i) => {\n const key = (step as { key: unknown }).key;\n if (typeof key === 'string' && !consumers.has(key)) {\n out.push(diagnostic('MULTIPLE_ROOTS', { key, root }, { path: ['steps', i] }));\n }\n });\n\n return out;\n}\n","import { hasErrors } from './diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from './model';\nimport { validate } from './validate';\n\n/**\n * Returns a cleaned copy of a recipe. Never throws (on well-shaped input):\n *\n * - trims prerequisites, keys, names, step texts and units;\n * - drops empty units and canonicalises known ones (`KG` → `kg`);\n * - omits `quantity`/`unit` properties that are undefined (stable deep equality);\n * - when the recipe is a valid tree, reorders ingredients by depth-first\n * traversal from the final step (inputs in their given order), which makes\n * the ingredients of every step contiguous rows.\n *\n * Steps keep their given order. Drafts (no steps) and invalid recipes keep the\n * given ingredient order — call {@link validate} to know why a recipe is invalid.\n */\nexport function normalize(recipe: Recipe, options: RecipeOptions = {}): Recipe {\n const units = unitLookup(options.units);\n const prerequisites = recipe.prerequisites.map((text) => text.trim());\n const ingredients: Ingredient[] = recipe.ingredients.map((ingredient) => {\n const clean: Ingredient = { key: ingredient.key.trim(), name: ingredient.name.trim() };\n if (ingredient.quantity !== undefined) clean.quantity = ingredient.quantity;\n const unit = ingredient.unit?.trim();\n if (unit) clean.unit = units.get(unit.toLowerCase()) ?? unit;\n return clean;\n });\n const steps: Step[] = recipe.steps.map((step) => ({\n key: step.key.trim(),\n text: step.text.trim(),\n inputs: step.inputs.map((input) => input.trim()),\n }));\n const cleaned: Recipe = { prerequisites, ingredients, steps };\n\n if (steps.length === 0 || hasErrors(validate(cleaned, options))) return cleaned;\n return { ...cleaned, ingredients: treeOrder(cleaned) };\n}\n\n/** Ingredients in depth-first order from the final step. Assumes a valid tree. */\nexport function treeOrder(recipe: Recipe): Ingredient[] {\n const ingredientByKey = new Map(recipe.ingredients.map((i) => [i.key, i]));\n const stepByKey = new Map(recipe.steps.map((s) => [s.key, s]));\n const ordered: Ingredient[] = [];\n const visit = (step: Step) => {\n for (const input of step.inputs) {\n const ingredient = ingredientByKey.get(input);\n if (ingredient) ordered.push(ingredient);\n else {\n const child = stepByKey.get(input);\n if (child) visit(child);\n }\n }\n };\n const root = recipe.steps[recipe.steps.length - 1];\n if (root) visit(root);\n return ordered;\n}\n","import type { DecimalSeparator, Recipe } from './model';\n\nexport interface FormatQuantityOptions {\n decimalSeparator?: DecimalSeparator;\n /** Maximum number of decimals (default 2). Trailing zeros are removed. */\n maxDecimals?: number;\n}\n\n/**\n * Formats a quantity for display: rounded to `maxDecimals` (default 2),\n * trailing zeros removed. Positive values that would round to 0 keep two\n * significant digits instead (e.g. 0.004 → \"0.004\"), so a non-zero amount is\n * never shown as \"0\".\n */\nexport function formatQuantity(value: number, options: FormatQuantityOptions = {}): string {\n const { decimalSeparator = '.', maxDecimals = 2 } = options;\n if (!Number.isFinite(value)) return String(value);\n let text: string;\n const rounded = Number(value.toFixed(maxDecimals));\n if (rounded === 0 && value !== 0) {\n text = trimZeros(Math.abs(value) < 1e-6 ? value.toExponential(1) : value.toPrecision(2));\n } else {\n text = trimZeros(rounded.toFixed(maxDecimals));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\n/**\n * Formats a quantity losslessly (used by the serializer): no rounding, never\n * exponent notation for usual cooking magnitudes.\n */\nexport function formatExactQuantity(\n value: number,\n decimalSeparator: DecimalSeparator = '.',\n): string {\n let text = String(value);\n if (/e/i.test(text)) {\n // 1e-7 / 1e21 style: expand. Precision beyond 20 decimals is not representable in the DSL.\n text = trimZeros(value.toFixed(20));\n }\n return decimalSeparator === ',' ? text.replace('.', ',') : text;\n}\n\nfunction trimZeros(text: string): string {\n if (!text.includes('.') || /e/i.test(text)) return text;\n return text.replace(/0+$/, '').replace(/\\.$/, '');\n}\n\n/**\n * Parses a DSL quantity: integer, decimal with `.` or `,`, or simple fraction `a/b`.\n * Returns `undefined` when the text is not a quantity. May return 0 or Infinity\n * (e.g. \"0\", \"1/0\"); callers report those as INVALID_QUANTITY.\n */\nexport function parseQuantity(text: string): number | undefined {\n const fraction = /^(\\d+)\\/(\\d+)$/.exec(text);\n if (fraction) return Number(fraction[1]) / Number(fraction[2]);\n if (/^\\d+(?:[.,]\\d+)?$/.test(text)) return Number(text.replace(',', '.'));\n return undefined;\n}\n\n/** Regex source of a DSL quantity, shared by the lexer and the label parser. */\nexport const QUANTITY_SOURCE = String.raw`\\d+\\/\\d+|\\d+(?:[.,]\\d+)?`;\n\n/**\n * Multiplies every ingredient quantity by `factor` (e.g. 6/4 to go from 4 to\n * 6 servings). Quantities are not rounded; use {@link formatQuantity} to display.\n * Throws a RangeError if `factor` is not a finite number > 0.\n */\nexport function scale(recipe: Recipe, factor: number): Recipe {\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new RangeError(`scale factor must be a finite number > 0, got ${factor}`);\n }\n return {\n prerequisites: [...recipe.prerequisites],\n ingredients: recipe.ingredients.map((ingredient) =>\n ingredient.quantity === undefined\n ? { ...ingredient }\n : { ...ingredient, quantity: ingredient.quantity * factor },\n ),\n steps: recipe.steps.map((step) => ({ ...step, inputs: [...step.inputs] })),\n };\n}\n","import { parseQuantity, QUANTITY_SOURCE } from '../quantity';\n\nexport interface ParsedIngredientLabel {\n name: string;\n quantity?: number;\n unit?: string;\n /** Original quantity text, kept for diagnostics. */\n quantityText?: string;\n}\n\nconst LEADING_QUANTITY = new RegExp(String.raw`^(?:${QUANTITY_SOURCE})`);\nconst QUOTED_PREFIX = new RegExp(String.raw`^(${QUANTITY_SOURCE})(?:\\s*(\\S+))?$`);\n\n/**\n * Splits an ingredient label into quantity, unit and name.\n *\n * Unquoted `[1,5 kg Pommes de terre]`:\n * - a leading quantity (`3`, `1.5`, `1,5`, `1/2`) is taken when followed by\n * a blank, a letter or the end of the label (so `4-épices` stays a name);\n * - the next word is the unit if it is in `units` (case-insensitive, may be\n * glued: `500g`) **and** a name follows it (`[500 g]` = 500 × \"g\");\n * - the rest, trimmed, is the name.\n *\n * Quoted `[1,5 kg \"Pommes (vapeur\"]` / `[\"4 épices\"]`: the quoted string is\n * the name, verbatim; the prefix is the quantity and optional unit.\n *\n * Quantities may come out as 0 or Infinity; the caller reports them.\n *\n * @param units lower-case → canonical spelling.\n */\nexport function parseIngredientLabel(\n label: { value: string; quoted: boolean; prefix: string },\n units: ReadonlyMap<string, string>,\n): ParsedIngredientLabel {\n if (label.quoted) {\n const name = label.value.trim();\n const match = QUOTED_PREFIX.exec(label.prefix.trim());\n if (!match) return { name };\n const result: ParsedIngredientLabel = {\n name,\n quantityText: match[1]!,\n quantity: parseQuantity(match[1]!),\n };\n if (match[2] !== undefined) result.unit = units.get(match[2].toLowerCase()) ?? match[2];\n return result;\n }\n\n const text = label.value.trim();\n const quantityMatch = LEADING_QUANTITY.exec(text);\n if (!quantityMatch) return { name: text };\n const quantityText = quantityMatch[0];\n let rest = text.slice(quantityText.length);\n if (rest !== '' && !/^[\\s\\p{L}]/u.test(rest)) return { name: text };\n\n const result: ParsedIngredientLabel = {\n name: '',\n quantityText,\n quantity: parseQuantity(quantityText),\n };\n rest = rest.trimStart();\n const unitMatch = /^(\\S+)(\\s+|$)/.exec(rest);\n if (unitMatch) {\n const unit = units.get(unitMatch[1]!.toLowerCase());\n const after = rest.slice(unitMatch[0].length).trim();\n if (unit !== undefined && after !== '') {\n result.unit = unit;\n rest = after;\n }\n }\n result.name = rest.trim();\n return result;\n}\n","import { diagnostic, type Diagnostic } from '../diagnostics';\nimport { QUANTITY_SOURCE } from '../quantity';\n\n/** Position of a token on its line. Columns are 1-based UTF-16 offsets, `endColumn` exclusive. */\nexport interface Span {\n line: number;\n column: number;\n endColumn: number;\n}\n\nexport interface IdentToken extends Span {\n type: 'ident';\n value: string;\n}\n\nexport interface LabelToken extends Span {\n type: 'label';\n /** `square` = `[ingredient]`, `round` = `(step)`. */\n shape: 'square' | 'round';\n /** Label text: raw content between the delimiters, or the unescaped quoted string. */\n value: string;\n quoted: boolean;\n /** Square labels only: the `quantity [unit]` written before a quoted name (may be ''). */\n prefix: string;\n}\n\nexport interface PunctToken extends Span {\n type: 'amp' | 'arrow';\n}\n\nexport interface BeforeToken extends Span {\n type: 'before';\n text: string;\n quoted: boolean;\n}\n\nexport type Token = IdentToken | LabelToken | PunctToken | BeforeToken;\n\nexport interface LexedLine {\n line: number;\n tokens: Token[];\n /** True when the line has a lexical error; its tokens are then incomplete. */\n failed: boolean;\n}\n\nexport interface LexResult {\n lines: LexedLine[];\n diagnostics: Diagnostic[];\n}\n\nconst IDENT_CHAR = /[A-Za-z0-9_-]/;\nconst CLOSE = { square: ']', round: ')' } as const;\nconst OPEN = { square: '[', round: '(' } as const;\n\n/** Splits a document into lines (`\\n`, `\\r\\n` or `\\r`). */\nexport function splitLines(source: string): string[] {\n return source.split(/\\r\\n|\\r|\\n/);\n}\n\n/**\n * Tokenizes a whole document, line by line. Comments (`%%` outside labels and\n * quoted strings) and blank lines produce no tokens. A lexical error stops\n * the current line (marked `failed`) but not the following ones.\n *\n * @param units lower-cased unit vocabulary, used to recognise `[2 kg \"quoted name\"]`.\n */\nexport function lex(source: string, units: ReadonlySet<string>): LexResult {\n const diagnostics: Diagnostic[] = [];\n const lines = splitLines(source).map((text, i) => lexLine(text, i + 1, units, diagnostics));\n return { lines, diagnostics };\n}\n\nexport function lexLine(\n text: string,\n line: number,\n units: ReadonlySet<string>,\n diagnostics: Diagnostic[],\n): LexedLine {\n const tokens: Token[] = [];\n let pos = 0;\n const at = (offset = 0) => text[pos + offset] ?? '';\n const fail = (\n code: Parameters<typeof diagnostic>[0],\n params: Record<string, string>,\n start: number,\n end: number,\n ) => {\n diagnostics.push(\n diagnostic(code, params, { line, column: start + 1, endLine: line, endColumn: end + 1 }),\n );\n return { line, tokens, failed: true };\n };\n const skipSpaces = () => {\n while (pos < text.length && /\\s/.test(at())) pos++;\n };\n\n skipSpaces();\n\n // `before: text` statement (only at the start of a line).\n const beforeMatch = /^before\\s*:/.exec(text.slice(pos));\n if (beforeMatch) {\n const start = pos;\n pos += beforeMatch[0].length;\n skipSpaces();\n if (at() === '\"') {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) return fail('UNTERMINATED_STRING', {}, pos, text.length);\n pos = quoted.end;\n skipSpaces();\n if (pos < text.length && !text.startsWith('%%', pos)) {\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'end of line', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n tokens.push({\n type: 'before',\n text: quoted.value,\n quoted: true,\n line,\n column: start + 1,\n endColumn: quoted.end + 1,\n });\n } else {\n const comment = text.indexOf('%%', pos);\n const end = comment === -1 ? text.length : comment;\n tokens.push({\n type: 'before',\n text: text.slice(pos, end).trim(),\n quoted: false,\n line,\n column: start + 1,\n endColumn: end + 1,\n });\n }\n return { line, tokens, failed: false };\n }\n\n while (pos < text.length) {\n skipSpaces();\n if (pos >= text.length) break;\n const start = pos;\n const ch = at();\n\n if (text.startsWith('%%', pos)) break; // comment until end of line\n if (text.startsWith('-->', pos)) {\n pos += 3;\n tokens.push({ type: 'arrow', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (ch === '&') {\n pos++;\n tokens.push({ type: 'amp', line, column: start + 1, endColumn: pos + 1 });\n continue;\n }\n if (IDENT_CHAR.test(ch)) {\n while (pos < text.length && IDENT_CHAR.test(at()) && !text.startsWith('-->', pos)) pos++;\n tokens.push({\n type: 'ident',\n value: text.slice(start, pos),\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n if (ch === '[' || ch === '(') {\n const shape = ch === '[' ? 'square' : 'round';\n const label = readLabel(text, pos, shape, units);\n if (!label.ok) return fail(label.code, label.params, label.start, label.end);\n pos = label.end;\n tokens.push({\n type: 'label',\n shape,\n value: label.value,\n quoted: label.quoted,\n prefix: label.prefix,\n line,\n column: start + 1,\n endColumn: pos + 1,\n });\n continue;\n }\n return fail(\n 'SYNTAX_ERROR',\n { expected: 'a node, \"&\" or \"-->\"', found: describe(text, pos) },\n pos,\n pos + 1,\n );\n }\n return { line, tokens, failed: false };\n}\n\n/** Human description of the character at `pos`, for SYNTAX_ERROR's `found` param. */\nexport function describe(text: string, pos: number): string {\n if (pos >= text.length) return 'end of line';\n return `\"${String.fromCodePoint(text.codePointAt(pos) ?? 0)}\"`;\n}\n\ntype QuotedResult = { ok: true; value: string; end: number } | { ok: false };\n\n/** Reads a `\"…\"` string starting at `start` (which must be `\"`). Escapes: `\\\"` and `\\\\`. */\nexport function readQuoted(text: string, start: number): QuotedResult {\n let value = '';\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\\\\' && (text[pos + 1] === '\"' || text[pos + 1] === '\\\\')) {\n value += text[pos + 1];\n pos += 2;\n continue;\n }\n if (ch === '\"') return { ok: true, value, end: pos + 1 };\n value += ch;\n pos++;\n }\n return { ok: false };\n}\n\ntype LabelResult =\n | { ok: true; value: string; quoted: boolean; prefix: string; end: number }\n | {\n ok: false;\n code: 'UNTERMINATED_LABEL' | 'UNTERMINATED_STRING' | 'SYNTAX_ERROR';\n params: Record<string, string>;\n start: number;\n end: number;\n };\n\nconst QUOTE_PREFIX_SOURCE = String.raw`^\\s*(?:(?:${QUANTITY_SOURCE})(?:\\s*([^\\s\"\\[\\]()]+))?\\s*)?$`;\nconst QUOTE_PREFIX = new RegExp(QUOTE_PREFIX_SOURCE);\n\n/**\n * Reads a label starting at the opening delimiter.\n *\n * - Unquoted: everything up to the matching closing delimiter; only the label's\n * own delimiter kind is counted for nesting (`[Ail (gousse)]`, `(Mix (well))`).\n * - Quoted: `(\"text\")`, `[\"name\"]`, or for ingredients `[qty unit \"name\"]` —\n * a `\"` opens a quoted string when it is the first non-blank character, or\n * (square labels) when everything before it is a quantity optionally\n * followed by a known unit. Any other `\"` is a literal character.\n */\nfunction readLabel(\n text: string,\n start: number,\n shape: 'square' | 'round',\n units: ReadonlySet<string>,\n): LabelResult {\n const open = OPEN[shape];\n const close = CLOSE[shape];\n let depth = 0;\n let pos = start + 1;\n while (pos < text.length) {\n const ch = text[pos]!;\n if (ch === '\"') {\n const before = text.slice(start + 1, pos);\n const opensQuote =\n before.trim() === '' ||\n (shape === 'square' && depth === 0 && isQuantityPrefix(before, units));\n if (opensQuote) {\n const quoted = readQuoted(text, pos);\n if (!quoted.ok) {\n return {\n ok: false,\n code: 'UNTERMINATED_STRING',\n params: {},\n start: pos,\n end: text.length,\n };\n }\n let end = quoted.end;\n while (end < text.length && /\\s/.test(text[end]!)) end++;\n if (text[end] !== close) {\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected: `\"${close}\"`, found: describe(text, end) },\n start: end,\n end: end + 1,\n };\n }\n return { ok: true, value: quoted.value, quoted: true, prefix: before.trim(), end: end + 1 };\n }\n }\n if (ch === open) depth++;\n if (ch === close) {\n if (depth === 0) {\n return {\n ok: true,\n value: text.slice(start + 1, pos),\n quoted: false,\n prefix: '',\n end: pos + 1,\n };\n }\n depth--;\n }\n pos++;\n }\n return { ok: false, code: 'UNTERMINATED_LABEL', params: { close }, start, end: text.length };\n}\n\nfunction isQuantityPrefix(before: string, units: ReadonlySet<string>): boolean {\n const match = QUOTE_PREFIX.exec(before);\n if (!match || before.trim() === '') return false;\n return match[1] === undefined || units.has(match[1].toLowerCase());\n}\n","import { diagnostic, hasErrors, type Diagnostic, type DiagnosticCode } from '../diagnostics';\nimport {\n MAX_KEY_LENGTH,\n unitLookup,\n type Ingredient,\n type Recipe,\n type RecipeOptions,\n type Step,\n} from '../model';\nimport { normalize } from '../normalize';\nimport { validate } from '../validate';\nimport { parseIngredientLabel } from './label';\nimport { lex, type IdentToken, type LabelToken, type Span, type Token } from './lexer';\n\nexport type ParseResult =\n | { ok: true; recipe: Recipe; diagnostics: Diagnostic[] }\n | { ok: false; diagnostics: Diagnostic[]; partial?: Recipe };\n\nexport type ParseOptions = Pick<RecipeOptions, 'units'>;\n\n/** One occurrence of a node in the text: `id` or `id[label]` / `id(label)`. */\ninterface NodeRef {\n key: string;\n ident: IdentToken;\n label?: LabelToken;\n span: Span;\n}\n\ninterface NodeInfo {\n key: string;\n /** Order of first mention in the document. */\n seq: number;\n first: NodeRef;\n def?: NodeRef;\n kind?: 'ingredient' | 'step';\n ingredient?: Ingredient;\n text?: string;\n inputs: string[];\n consumer?: string;\n}\n\ninterface Edge {\n from: NodeRef;\n to: NodeRef;\n}\n\n/**\n * Codes that make tree-structure checks (NO_INPUT, CYCLE, UNCONSUMED,\n * MULTIPLE_ROOTS) meaningless: they would only cascade. Tree rules are checked\n * once the document is syntactically valid and every reference resolves.\n */\nconst BLOCKING: ReadonlySet<DiagnosticCode> = new Set([\n 'SYNTAX_ERROR',\n 'UNTERMINATED_LABEL',\n 'UNTERMINATED_STRING',\n 'FAN_OUT',\n 'UNDEFINED_NODE',\n 'REDEFINED_NODE',\n 'INGREDIENT_AS_TARGET',\n]);\n\n/**\n * Parses a tabular-recipe document.\n *\n * On success, `recipe` is normalized (see {@link normalize}). On failure,\n * every diagnostic found is returned (sorted by position) together with a\n * best-effort `partial` recipe, handy for live previews.\n */\nexport function parse(source: string, options: ParseOptions = {}): ParseResult {\n const unitMap = unitLookup(options.units);\n const { lines, diagnostics } = lex(source, new Set(unitMap.keys()));\n const nodes = new Map<string, NodeInfo>();\n const prerequisites: string[] = [];\n const edges: Edge[] = [];\n const suppressed = new Set<string>(); // idents seen on failed lines: no UNDEFINED_NODE for them\n let seq = 0;\n\n const at = (span: Span) => ({\n line: span.line,\n column: span.column,\n endLine: span.line,\n endColumn: span.endColumn,\n });\n const report = (code: DiagnosticCode, params: Record<string, string | number>, span: Span) =>\n diagnostics.push(diagnostic(code, params, at(span)));\n\n const register = (ref: NodeRef): NodeInfo => {\n let info = nodes.get(ref.key);\n if (!info) {\n info = { key: ref.key, seq: seq++, first: ref, inputs: [] };\n nodes.set(ref.key, info);\n if (ref.key.length > MAX_KEY_LENGTH) report('INVALID_KEY', { key: ref.key }, ref.ident);\n }\n if (!ref.label) return info;\n if (info.def) {\n report('REDEFINED_NODE', { key: ref.key, firstLine: info.def.ident.line }, ref.ident);\n return info;\n }\n info.def = ref;\n if (ref.label.shape === 'square') {\n info.kind = 'ingredient';\n const parsed = parseIngredientLabel(ref.label, unitMap);\n const ingredient: Ingredient = { key: ref.key, name: parsed.name };\n if (parsed.quantity !== undefined) {\n if (!Number.isFinite(parsed.quantity) || parsed.quantity <= 0) {\n report('INVALID_QUANTITY', { quantity: parsed.quantityText ?? '' }, ref.label);\n } else {\n ingredient.quantity = parsed.quantity;\n if (parsed.unit !== undefined) ingredient.unit = parsed.unit;\n }\n }\n if (parsed.name === '') report('EMPTY_TEXT', {}, ref.label);\n info.ingredient = ingredient;\n } else {\n info.kind = 'step';\n info.text = ref.label.value.trim();\n if (info.text === '') report('EMPTY_TEXT', {}, ref.label);\n }\n return info;\n };\n\n for (const lexed of lines) {\n if (lexed.failed) {\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n const statement = parseStatement(lexed.tokens);\n if (!statement.ok) {\n report(statement.code, statement.params, statement.span);\n for (const token of lexed.tokens) if (token.type === 'ident') suppressed.add(token.value);\n continue;\n }\n if (statement.kind === 'empty') continue;\n if (statement.kind === 'before') {\n if (statement.token.text.trim() === '') report('EMPTY_TEXT', {}, statement.token);\n prerequisites.push(statement.token.text.trim());\n continue;\n }\n if (statement.kind === 'definition') {\n register(statement.node);\n continue;\n }\n // Edge statement: sources --> t1 --> t2 ...\n statement.sources.forEach(register);\n let previous = statement.sources;\n for (const target of statement.targets) {\n register(target);\n if (target.label?.shape === 'square') {\n report('INGREDIENT_AS_TARGET', { key: target.key }, target.span);\n } else {\n for (const from of previous) edges.push({ from, to: target });\n }\n previous = [target];\n }\n if (statement.fanOut) report('FAN_OUT', {}, statement.fanOut);\n }\n\n // --- resolution -------------------------------------------------------------------------\n for (const info of nodes.values()) {\n if (!info.def && !suppressed.has(info.key)) {\n report('UNDEFINED_NODE', { key: info.key }, info.first.ident);\n }\n }\n for (const edge of edges) {\n const from = nodes.get(edge.from.key)!;\n const to = nodes.get(edge.to.key)!;\n if (!from.def || !to.def) continue;\n if (to.kind === 'ingredient') {\n report('INGREDIENT_AS_TARGET', { key: to.key }, edge.to.span);\n continue;\n }\n if (from.consumer !== undefined) {\n report('MULTIPLE_CONSUMERS', { key: from.key, consumer: from.consumer }, edge.from.span);\n continue;\n }\n from.consumer = to.key;\n to.inputs.push(from.key);\n }\n\n const all = [...nodes.values()].filter((n) => n.def).sort((a, b) => a.seq - b.seq);\n const ingredients = all.filter((n) => n.kind === 'ingredient');\n const stepNodes = all.filter((n) => n.kind === 'step');\n const order = topologicalOrder(stepNodes, nodes);\n\n // --- tree rules ---------------------------------------------------------------------------\n const blocked = diagnostics.some((d) => BLOCKING.has(d.code));\n if (!blocked && stepNodes.length > 0) {\n for (const step of stepNodes) {\n if (step.inputs.length === 0) report('NO_INPUT', { key: step.key }, step.def!.span);\n }\n if (order.cyclic.length > 0) {\n const first = order.cyclic[0]!;\n report('CYCLE', { keys: order.cyclic.map((s) => s.key).join(', ') }, first.def!.span);\n }\n for (const ingredient of ingredients) {\n if (ingredient.consumer === undefined) {\n report('UNCONSUMED', { key: ingredient.key }, ingredient.def!.span);\n }\n }\n const roots = order.sorted.filter((s) => s.consumer === undefined);\n const root = roots[roots.length - 1];\n for (const extra of roots.slice(0, -1)) {\n report('MULTIPLE_ROOTS', { key: extra.key, root: root!.key }, extra.def!.span);\n }\n }\n\n const recipe: Recipe = {\n prerequisites,\n ingredients: ingredients.map((n) => n.ingredient!),\n steps: [...order.sorted, ...order.cyclic].map((n): Step => ({\n key: n.key,\n text: n.text!,\n inputs: n.inputs,\n })),\n };\n\n diagnostics.sort((a, b) => (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0));\n if (hasErrors(diagnostics)) {\n return { ok: false, diagnostics, partial: normalize(recipe, options) };\n }\n const normalized = normalize(recipe, options);\n // Safety net: the DSL checks above mirror validate(); anything left is a bug, but report it.\n const leftovers = validate(normalized, options).filter((d) => d.severity === 'error');\n if (leftovers.length > 0) return { ok: false, diagnostics: leftovers, partial: normalized };\n return { ok: true, recipe: normalized, diagnostics: [] };\n}\n\n/**\n * Stable topological order of steps: a step comes after the steps it consumes;\n * ties are broken by order of first mention. Steps on a cycle are returned apart.\n */\nfunction topologicalOrder(steps: NodeInfo[], nodes: Map<string, NodeInfo>) {\n const pending = new Map<string, number>();\n for (const step of steps) {\n pending.set(step.key, step.inputs.filter((input) => nodes.get(input)?.kind === 'step').length);\n }\n const sorted: NodeInfo[] = [];\n const done = new Set<string>();\n // Steps are few (< 100): a simple repeated scan keeps the tie-breaking obvious.\n for (;;) {\n const next = steps.find((s) => !done.has(s.key) && pending.get(s.key) === 0);\n if (!next) break;\n done.add(next.key);\n sorted.push(next);\n if (next.consumer !== undefined && pending.has(next.consumer)) {\n pending.set(next.consumer, pending.get(next.consumer)! - 1);\n }\n }\n return { sorted, cyclic: steps.filter((s) => !done.has(s.key)) };\n}\n\n// --- statements -----------------------------------------------------------------------------\n\ntype Statement =\n | { ok: true; kind: 'empty' }\n | { ok: true; kind: 'before'; token: Extract<Token, { type: 'before' }> }\n | { ok: true; kind: 'definition'; node: NodeRef }\n | { ok: true; kind: 'edges'; sources: NodeRef[]; targets: NodeRef[]; fanOut?: Span }\n | { ok: false; code: DiagnosticCode; params: Record<string, string>; span: Span };\n\nfunction found(token: Token | undefined): string {\n if (!token) return 'end of line';\n switch (token.type) {\n case 'ident':\n return `\"${token.value}\"`;\n case 'label':\n return token.shape === 'square' ? '\"[\"' : '\"(\"';\n case 'amp':\n return '\"&\"';\n case 'arrow':\n return '\"-->\"';\n case 'before':\n return '\"before:\"';\n }\n}\n\n/**\n * Grammar of one line (tokens already lexed):\n *\n * statement = before | definition | edges\n * definition = node-def\n * edges = node ( \"&\" node )* ( \"-->\" node )+\n * node = IDENT [ LABEL ]\n */\nfunction parseStatement(tokens: Token[]): Statement {\n if (tokens.length === 0) return { ok: true, kind: 'empty' };\n const first = tokens[0]!;\n if (first.type === 'before') return { ok: true, kind: 'before', token: first };\n\n let pos = 0;\n const lastSpan = tokens[tokens.length - 1]!;\n const eol: Span = {\n line: lastSpan.line,\n column: lastSpan.endColumn,\n endColumn: lastSpan.endColumn + 1,\n };\n const error = (expected: string): Statement => {\n const token = tokens[pos];\n return {\n ok: false,\n code: 'SYNTAX_ERROR',\n params: { expected, found: found(token) },\n span: token ?? eol,\n };\n };\n const readNode = (): NodeRef | undefined => {\n const ident = tokens[pos];\n if (ident?.type !== 'ident') return undefined;\n pos++;\n const label = tokens[pos];\n if (label?.type === 'label') {\n pos++;\n return {\n key: ident.value,\n ident,\n label,\n span: { line: ident.line, column: ident.column, endColumn: label.endColumn },\n };\n }\n return { key: ident.value, ident, span: ident };\n };\n\n const sources: NodeRef[] = [];\n const firstNode = readNode();\n if (!firstNode) return error('a node id');\n sources.push(firstNode);\n while (tokens[pos]?.type === 'amp') {\n pos++;\n const node = readNode();\n if (!node) return error('a node id after \"&\"');\n sources.push(node);\n }\n\n if (pos === tokens.length) {\n if (sources.length === 1 && firstNode.label)\n return { ok: true, kind: 'definition', node: firstNode };\n return error(sources.length === 1 ? '\"[\", \"(\" or \"-->\"' : '\"-->\"');\n }\n\n const targets: NodeRef[] = [];\n while (tokens[pos]?.type === 'arrow') {\n pos++;\n const node = readNode();\n if (!node) return error('a step after \"-->\"');\n targets.push(node);\n }\n if (targets.length === 0) return error('\"&\" or \"-->\"');\n if (pos === tokens.length) return { ok: true, kind: 'edges', sources, targets };\n const extra = tokens[pos]!;\n if (extra.type === 'amp') return { ok: true, kind: 'edges', sources, targets, fanOut: extra };\n return error('\"-->\" or end of line');\n}\n","import type { Diagnostic } from '../diagnostics';\nimport { unitLookup, type Ingredient, type Recipe, type RecipeOptions, type Step } from '../model';\nimport { normalize } from '../normalize';\nimport { formatExactQuantity } from '../quantity';\nimport { parseIngredientLabel } from './label';\nimport { lexLine } from './lexer';\n\nexport type SerializeOptions = RecipeOptions;\n\n/**\n * Writes a recipe in canonical DSL form:\n *\n * 1. `before: …` lines, then a blank line if anything follows;\n * 2. drafts (no steps): one ingredient definition per line;\n * 3. otherwise one line per step, in step order: `inputs --> key(text)`,\n * each ingredient defined inline at its use, earlier steps as bare ids.\n * A step whose single input is the step just before it is appended to\n * that line as a chain: `… --> a(x) --> b(y)`.\n *\n * Labels are quoted only when needed (the serializer checks that each label\n * reads back identically). Quantities are written losslessly with\n * `decimalSeparator` (default `.`). The recipe is normalized first, so\n * `parse(serialize(r)).recipe` deep-equals `normalize(r)` for every valid r.\n *\n * Invalid recipes are still written, best effort (unused ingredients become\n * standalone definitions at the end), so an editor can switch to text mode\n * and let the parser point at the problems.\n */\nexport function serialize(recipe: Recipe, options: SerializeOptions = {}): string {\n const r = normalize(recipe, options);\n const separator = options.decimalSeparator ?? '.';\n const unitSet = new Set(unitLookup(options.units).keys());\n const unitMap = unitLookup(options.units);\n const lines: string[] = r.prerequisites.map((text) => `before: ${prerequisiteText(text)}`);\n const body: string[] = [];\n\n const ingredientByKey = new Map(r.ingredients.map((i) => [i.key, i]));\n const defined = new Set<string>();\n const ingredientRef = (ingredient: Ingredient) => {\n if (defined.has(ingredient.key)) return ingredient.key;\n defined.add(ingredient.key);\n return `${ingredient.key}[${ingredientLabel(ingredient, separator, unitSet, unitMap)}]`;\n };\n const stepRef = (step: Step) => `${step.key}(${stepLabel(step.text, unitSet)})`;\n const inputRef = (key: string) => {\n const ingredient = ingredientByKey.get(key);\n return ingredient ? ingredientRef(ingredient) : key;\n };\n\n for (let i = 0; i < r.steps.length; i++) {\n let step = r.steps[i]!;\n let line =\n step.inputs.length > 0\n ? `${step.inputs.map(inputRef).join(' & ')} --> ${stepRef(step)}`\n : stepRef(step);\n let next = r.steps[i + 1];\n while (next && next.inputs.length === 1 && next.inputs[0] === step.key) {\n line += ` --> ${stepRef(next)}`;\n step = next;\n i++;\n next = r.steps[i + 1];\n }\n body.push(line);\n }\n for (const ingredient of r.ingredients) {\n if (!defined.has(ingredient.key)) body.push(ingredientRef(ingredient));\n }\n\n if (lines.length > 0 && body.length > 0) lines.push('');\n lines.push(...body);\n return lines.length > 0 ? `${lines.join('\\n')}\\n` : '';\n}\n\nfunction quote(text: string): string {\n return `\"${text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\nfunction prerequisiteText(text: string): string {\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(`before: ${text}`, 1, new Set(), diagnostics);\n const token = lexed.tokens[0];\n const readsBack = diagnostics.length === 0 && token?.type === 'before' && token.text === text;\n return readsBack && text !== '' ? text : quote(text);\n}\n\n/** Lexes `x<open>content<close>` and returns the label token if it reads back cleanly. */\nfunction relex(content: string, shape: 'square' | 'round', units: ReadonlySet<string>) {\n const [open, close] = shape === 'square' ? ['[', ']'] : ['(', ')'];\n const source = `x${open}${content}${close}`;\n const diagnostics: Diagnostic[] = [];\n const lexed = lexLine(source, 1, units, diagnostics);\n const label = lexed.tokens[1];\n if (diagnostics.length > 0 || lexed.tokens.length !== 2 || label?.type !== 'label')\n return undefined;\n return label.endColumn === source.length + 1 ? label : undefined;\n}\n\nfunction stepLabel(text: string, units: ReadonlySet<string>): string {\n const label = relex(text, 'round', units);\n return label && !label.quoted && label.value.trim() === text && text !== '' ? text : quote(text);\n}\n\nfunction ingredientLabel(\n ingredient: Ingredient,\n separator: '.' | ',',\n units: ReadonlySet<string>,\n unitMap: ReadonlyMap<string, string>,\n): string {\n const prefix =\n ingredient.quantity === undefined\n ? ''\n : [formatExactQuantity(ingredient.quantity, separator), ingredient.unit]\n .filter((part) => part !== undefined)\n .join(' ');\n const raw = [prefix, ingredient.name].filter((part) => part !== '').join(' ');\n const label = relex(raw, 'square', units);\n if (label && !label.quoted) {\n const parsed = parseIngredientLabel(label, unitMap);\n if (\n parsed.name === ingredient.name &&\n parsed.quantity === ingredient.quantity &&\n parsed.unit === ingredient.unit &&\n ingredient.name !== ''\n ) {\n return raw;\n }\n }\n return prefix === '' ? quote(ingredient.name) : `${prefix} ${quote(ingredient.name)}`;\n}\n","import { hasErrors } from './diagnostics';\nimport type { Recipe, RecipeOptions, Step } from './model';\nimport { normalize } from './normalize';\nimport { formatQuantity } from './quantity';\nimport { validate } from './validate';\n\nexport type CellKind = 'quantity' | 'ingredient' | 'step';\n\nexport interface Cell {\n kind: CellKind;\n /** Ingredient key (quantity / ingredient cells) or step key. */\n key: string;\n text: string;\n /** 0-based grid position of the top-left corner. */\n row: number;\n col: number;\n rowSpan: number;\n colSpan: number;\n /** True for the final step (rightmost column). */\n final?: boolean;\n}\n\nexport interface Layout {\n /** False when the recipe is not a valid tree: only ingredients are laid out (draft layout). */\n valid: boolean;\n /** 2 (quantity + ingredient) + depth of the final step. */\n columnCount: number;\n /** Prerequisites, rendered as full-width rows above the table. */\n banners: string[];\n /** `rows[r]` = cells that *start* on row r, left to right (HTML table semantics). */\n rows: Cell[][];\n}\n\nexport interface LayoutOptions extends RecipeOptions {\n /** Max decimals of displayed quantities (default 2). */\n maxDecimals?: number;\n /** Display names for units, e.g. `{ pincee: 'pincée', cs: 'c. à s.' }`. Unlisted units show as-is. */\n unitLabels?: Readonly<Record<string, string>>;\n}\n\n/**\n * Computes the tabular grid of a recipe.\n *\n * - Column 0 = quantity, column 1 = ingredient, columns 2… = steps by depth:\n * depth(ingredient) = 0, depth(step) = 1 + max depth of its inputs,\n * step column = 1 + depth. The final step is alone in the last column.\n * - One row per ingredient (normalized depth-first order). A step spans the\n * rows of all the ingredients under it.\n * - No empty cells: a step or ingredient whose consumer sits more than one\n * column to the right stretches (`colSpan`) up to that consumer.\n *\n * Drafts (and invalid recipes, with `valid: false`) get a 2-column layout.\n */\nexport function layout(recipe: Recipe, options: LayoutOptions = {}): Layout {\n const r = normalize(recipe, options);\n const valid = !hasErrors(validate(r, options));\n const quantityText = (quantity?: number, unit?: string) =>\n quantity === undefined\n ? ''\n : [\n formatQuantity(quantity, {\n decimalSeparator: options.decimalSeparator,\n maxDecimals: options.maxDecimals,\n }),\n unit === undefined ? undefined : (options.unitLabels?.[unit] ?? unit),\n ]\n .filter(Boolean)\n .join(' ');\n\n const useTree = valid && r.steps.length > 0;\n const stepByKey = new Map(r.steps.map((s) => [s.key, s]));\n const depth = new Map<string, number>();\n const consumer = new Map<string, Step>();\n const leaves = new Map<string, string[]>(); // step key → ingredient keys under it, in row order\n if (useTree) {\n for (const step of r.steps) {\n // Steps are ordered so that inputs come first: one pass is enough.\n let d = 0;\n const under: string[] = [];\n for (const input of step.inputs) {\n consumer.set(input, step);\n const child = stepByKey.get(input);\n d = Math.max(d, child ? depth.get(input)! : 0);\n under.push(...(child ? leaves.get(input)! : [input]));\n }\n depth.set(step.key, d + 1);\n leaves.set(step.key, under);\n }\n }\n\n const root = r.steps[r.steps.length - 1];\n const columnCount = useTree && root ? 2 + depth.get(root.key)! : 2;\n const columnOf = (stepKey: string) => 1 + depth.get(stepKey)!;\n const rowOf = new Map(r.ingredients.map((ingredient, i) => [ingredient.key, i]));\n\n const rows: Cell[][] = r.ingredients.map((ingredient, row) => {\n const parent = consumer.get(ingredient.key);\n return [\n {\n kind: 'quantity',\n key: ingredient.key,\n text: quantityText(ingredient.quantity, ingredient.unit),\n row,\n col: 0,\n rowSpan: 1,\n colSpan: 1,\n },\n {\n kind: 'ingredient',\n key: ingredient.key,\n text: ingredient.name,\n row,\n col: 1,\n rowSpan: 1,\n colSpan: parent ? columnOf(parent.key) - 1 : 1,\n },\n ];\n });\n\n if (useTree) {\n for (const step of r.steps) {\n const under = leaves.get(step.key)!;\n const row = rowOf.get(under[0]!)!;\n const col = columnOf(step.key);\n const parent = consumer.get(step.key);\n rows[row]!.push({\n kind: 'step',\n key: step.key,\n text: step.text,\n row,\n col,\n rowSpan: under.length,\n colSpan: parent ? columnOf(parent.key) - col : 1,\n ...(parent ? {} : { final: true }),\n });\n }\n for (const cells of rows) cells.sort((a, b) => a.col - b.col);\n }\n\n return { valid, columnCount, banners: [...r.prerequisites], rows };\n}\n","import type { Recipe } from './model';\nimport { layout, type Cell, type LayoutOptions } from './layout';\n\nexport interface RenderHtmlOptions extends LayoutOptions {\n /** CSS class prefix (default `trd`). Must match the stylesheet you use. */\n classPrefix?: string;\n /** Keep the quantity and ingredient columns visible while scrolling horizontally. */\n stickyIngredients?: boolean;\n /** Optional `<caption>` text (e.g. the recipe name). */\n caption?: string;\n /**\n * Orientation of the step texts (default `'horizontal'`).\n * - `'vertical'`: step texts read bottom to top, so step columns stay narrow (mobile-friendly).\n * - `'auto'`: vertical only when the wrapper is narrower than 40rem (CSS container query);\n * the wrapper must then take its width from its parent, not from its content.\n */\n stepOrientation?: 'horizontal' | 'vertical' | 'auto';\n}\n\nconst ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\n\n/** Escapes text for HTML element content and attribute values. */\nexport function escapeHtml(text: string): string {\n return text.replace(/[&<>\"']/g, (ch) => ESCAPES[ch]!);\n}\n\n/**\n * Renders a recipe as a standalone HTML table (string), framework-free.\n * All text is escaped. Pair it with `ln-tabular-recipes-dsl/style.css`, or\n * style the classes yourself:\n *\n * `{p}-wrap` (scroll container, + `{p}-auto`), `{p}-table` (+ `{p}-sticky`,\n * `{p}-invalid`, `{p}-vsteps`), `{p}-banner`, `{p}-qty`, `{p}-ingredient`,\n * `{p}-step` (+ `{p}-final`), `{p}-step-text` (step text wrapper, only when\n * `stepOrientation` is not horizontal).\n * Cells carry `data-key` with the ingredient/step key.\n */\nexport function renderHtml(recipe: Recipe, options: RenderHtmlOptions = {}): string {\n const p = options.classPrefix ?? 'trd';\n const grid = layout(recipe, options);\n const tableClasses = [`${p}-table`];\n if (options.stickyIngredients) tableClasses.push(`${p}-sticky`);\n if (!grid.valid) tableClasses.push(`${p}-invalid`);\n const orientation = options.stepOrientation ?? 'horizontal';\n if (orientation === 'vertical') tableClasses.push(`${p}-vsteps`);\n const wrapClasses = [`${p}-wrap`];\n if (orientation === 'auto') wrapClasses.push(`${p}-auto`);\n const wrapStepText = orientation !== 'horizontal';\n\n const cellHtml = (cell: Cell) => {\n const classes = [`${p}-${cell.kind === 'quantity' ? 'qty' : cell.kind}`];\n if (cell.final) classes.push(`${p}-final`);\n const attrs = [`class=\"${classes.join(' ')}\"`, `data-key=\"${escapeHtml(cell.key)}\"`];\n if (cell.rowSpan > 1) attrs.push(`rowspan=\"${cell.rowSpan}\"`);\n if (cell.colSpan > 1) attrs.push(`colspan=\"${cell.colSpan}\"`);\n const text = escapeHtml(cell.text);\n const content =\n wrapStepText && cell.kind === 'step' ? `<span class=\"${p}-step-text\">${text}</span>` : text;\n return `<td ${attrs.join(' ')}>${content}</td>`;\n };\n\n const parts: string[] = [\n `<div class=\"${wrapClasses.join(' ')}\">`,\n `<table class=\"${tableClasses.join(' ')}\">`,\n ];\n if (options.caption !== undefined)\n parts.push(`<caption>${escapeHtml(options.caption)}</caption>`);\n parts.push('<tbody>');\n for (const banner of grid.banners) {\n parts.push(\n `<tr class=\"${p}-banner-row\"><td class=\"${p}-banner\" colspan=\"${grid.columnCount}\">${escapeHtml(banner)}</td></tr>`,\n );\n }\n for (const cells of grid.rows) parts.push(`<tr>${cells.map(cellHtml).join('')}</tr>`);\n parts.push('</tbody>', '</table>', '</div>');\n return parts.join('\\n');\n}\n"],"mappings":"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","names":["diagnostic","result"]}
|
package/dist/style.css
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
--trd-qty-width: 5.5rem;
|
|
17
17
|
--trd-radius: 6px;
|
|
18
18
|
--trd-pad: 0.4rem 0.6rem;
|
|
19
|
+
--trd-step-max-height: 13em;
|
|
19
20
|
--trd-font: inherit;
|
|
20
21
|
|
|
21
22
|
overflow-x: auto;
|
|
@@ -149,3 +150,56 @@
|
|
|
149
150
|
.trd-sticky td.trd-ingredient {
|
|
150
151
|
left: var(--trd-qty-width);
|
|
151
152
|
}
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* Optional: vertical step texts (renderHtml `stepOrientation`), read bottom
|
|
156
|
+
* to top so step columns stay narrow on small screens. `.trd-vsteps` always,
|
|
157
|
+
* `.trd-auto` only when the wrapper is narrower than 40rem.
|
|
158
|
+
*/
|
|
159
|
+
.trd-auto {
|
|
160
|
+
container: trd / inline-size;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.trd-vsteps td.trd-step {
|
|
164
|
+
min-width: 0;
|
|
165
|
+
padding: 0.5rem 0.3rem;
|
|
166
|
+
text-align: center;
|
|
167
|
+
vertical-align: bottom;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.trd-vsteps .trd-step-text {
|
|
171
|
+
display: inline-block;
|
|
172
|
+
writing-mode: sideways-lr;
|
|
173
|
+
max-inline-size: var(--trd-step-max-height);
|
|
174
|
+
text-align: start;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@supports not (writing-mode: sideways-lr) {
|
|
178
|
+
.trd-vsteps .trd-step-text {
|
|
179
|
+
writing-mode: vertical-rl;
|
|
180
|
+
transform: rotate(180deg);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@container trd (max-width: 40rem) {
|
|
185
|
+
.trd-table td.trd-step {
|
|
186
|
+
min-width: 0;
|
|
187
|
+
padding: 0.5rem 0.3rem;
|
|
188
|
+
text-align: center;
|
|
189
|
+
vertical-align: bottom;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.trd-step-text {
|
|
193
|
+
display: inline-block;
|
|
194
|
+
writing-mode: sideways-lr;
|
|
195
|
+
max-inline-size: var(--trd-step-max-height);
|
|
196
|
+
text-align: start;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@supports not (writing-mode: sideways-lr) {
|
|
200
|
+
.trd-step-text {
|
|
201
|
+
writing-mode: vertical-rl;
|
|
202
|
+
transform: rotate(180deg);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ln-tabular-recipes-dsl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Tabular recipes: a Mermaid-like DSL, a tree model, validation and a rowspan/colspan table layout + HTML renderer. Zero runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"recipe",
|