docs: add language spec, domain glossary, and fixture update
This commit is contained in:
114
.claude/CONTEXT.md
Normal file
114
.claude/CONTEXT.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# KitchenMD — Domain Glossary
|
||||
|
||||
A living glossary of canonical terms for this project.
|
||||
Implementation details belong in specs or code, not here.
|
||||
|
||||
---
|
||||
|
||||
## Annotation
|
||||
|
||||
An inline sigil-based marker embedded in recipe prose that carries structured data.
|
||||
There are exactly three annotation types: **Ingredient**, **Cookware**, and **Timer**.
|
||||
An annotation is valid Markdown plain text — it does not break rendering in Obsidian or any standard Markdown renderer.
|
||||
Annotations follow CommonMark's inline processing rules: parsed wherever CommonMark processes inline content (paragraphs, headings, list items, blockquotes); ignored wherever CommonMark treats content as literal (code spans, code blocks, HTML comments, raw HTML blocks).
|
||||
|
||||
## Ingredient Annotation
|
||||
|
||||
Syntax: `@name{quantity}` or `@name{quantity unit}`
|
||||
|
||||
The `{` character terminates the name, so multi-word names work without escaping.
|
||||
The `{}` wrapper is mandatory — bare `@name` without braces is not valid.
|
||||
Quantity is optional; `@black pepper{}` is valid with no quantity.
|
||||
Valid quantity formats: integer (`2`), decimal (`0.5`), simple fraction (`1/2`), mixed number (`1 1/2`).
|
||||
Unit is optional and free-form — the format places no constraints on what unit string is written.
|
||||
When both quantity and unit are present, they are delimited by the last space inside the braces — so `@butter{1 1/2 tbsp}` parses as quantity `1 1/2`, unit `tbsp`.
|
||||
Valid name characters: any character except `{`, `}`, `@`, `$`, `~`.
|
||||
|
||||
## Cookware Annotation
|
||||
|
||||
Syntax: `$name{}`, `$name{quantity}`, or `$name{quantity unit}`
|
||||
|
||||
Same `{`-terminated name rule as Ingredient.
|
||||
The `{}` wrapper is mandatory.
|
||||
Quantity is optional; `$pan{}` is valid with no quantity.
|
||||
Quantity and unit follow the same rules as Ingredient: valid formats are integer, decimal, simple fraction, mixed number; unit is free-form; delimited by the last space inside the braces.
|
||||
Valid name characters: any character except `{`, `}`, `@`, `$`, `~`.
|
||||
|
||||
## Timer Annotation
|
||||
|
||||
Syntax: `~N unit` or `~N-N unit`
|
||||
|
||||
No braces.
|
||||
The `~` sigil is followed immediately by a number or `N–N` range, a space, then a time unit word from the known set.
|
||||
N follows the same format rules as ingredient/cookware quantity: integer, decimal, simple fraction, or mixed number.
|
||||
Supports natural range syntax: `~10-15 mins`.
|
||||
In range form, the `-` separator follows the first quantity expression — since negative quantities don't exist, the `-` is unambiguous. `~1/2-1 hr` parses as a range of 1/2 hr to 1 hr.
|
||||
The annotation terminates after the unit word — trailing prose is ignored. `~1 min more` parses as a timer of 1 min; "more" is plain text.
|
||||
Known unit set (aliases → canonical abbreviation): `sec`, `secs`, `second`, `seconds` → `s`; `min`, `mins`, `minute`, `minutes` → `min`; `hr`, `hrs`, `hour`, `hours` → `hr`.
|
||||
|
||||
## Recipe File
|
||||
|
||||
A valid `.md` file that may contain any combination of Obsidian Flavored Markdown, YAML frontmatter, and Annotations.
|
||||
The file renders correctly in Obsidian without a plugin.
|
||||
A Recipe File contains prose instructions only — there is no separate ingredients list section.
|
||||
Ingredients are derived entirely by the Parser from inline Ingredient Annotations.
|
||||
|
||||
## Step
|
||||
|
||||
The atomic unit of a recipe instruction.
|
||||
A step is a single non-empty line in a prose context, or a single ordered list item.
|
||||
Headings are not steps.
|
||||
Step numbers are section-local — they reset at each heading and do not change when steps are added to other sections.
|
||||
|
||||
## Combined Recipe
|
||||
|
||||
A Recipe File whose purpose is to sequence steps from two or more other Recipe Files.
|
||||
It contains no steps or ingredients of its own — only Step References in the order they should be executed.
|
||||
|
||||
## Step Reference
|
||||
|
||||
A KitchenMD extension of Obsidian transclusion syntax for embedding a specific step from another Recipe File.
|
||||
|
||||
Syntax:
|
||||
- `![[filename#section-heading:N]]` — step N within a named section.
|
||||
- `![[filename#N]]` — step N in a headingless recipe (bare number, no section prefix).
|
||||
|
||||
- `filename` — the Recipe File to reference (no `.md` extension, matching Obsidian wikilink convention).
|
||||
- `section-heading` — the heading text of the section containing the step.
|
||||
- `N` — the 1-based step number within that section or file.
|
||||
|
||||
Examples:
|
||||
- `![[italian meatballs#rolling:2]]` — second step of the "rolling" section.
|
||||
- `![[basic brine#2]]` — second step of a headingless recipe.
|
||||
|
||||
This syntax is a compatibility break with vanilla Obsidian: `#section:N` does not match any Obsidian heading anchor, so Step References will not render correctly without KitchenMD tooling.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
YAML metadata block at the top of a Recipe File (standard `---` delimiters).
|
||||
The parser passes frontmatter through as-is with no schema enforcement.
|
||||
Consumers are responsible for reading whatever fields they need.
|
||||
|
||||
## Sigil
|
||||
|
||||
The single punctuation character that opens each annotation type:
|
||||
- `@` — Ingredient
|
||||
- `$` — Cookware
|
||||
- `~` — Timer
|
||||
|
||||
## Parser
|
||||
|
||||
The TypeScript library (`@kitchen-md/core`) that accepts a Recipe File string and returns a structured data object.
|
||||
It is a pure function of its input string — no filesystem access.
|
||||
|
||||
## Structured Output
|
||||
|
||||
The data object returned by the Parser for a given Recipe File.
|
||||
Shape is not yet fully specified — see open questions below.
|
||||
|
||||
---
|
||||
|
||||
## Open / Unresolved
|
||||
|
||||
- **Structured Output shape** — exact fields, types, and nesting of the parse result.
|
||||
- **Unit normalisation** — the parser should normalise known aliases to a canonical abbreviation (`grams` → `g`, `kilograms` → `kg`); exact alias table and canonical forms are TBD. This is a parser concern, not a format constraint.
|
||||
44
.claude/adr/0001-step-reference-syntax.md
Normal file
44
.claude/adr/0001-step-reference-syntax.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# ADR 0001 — Step Reference Syntax
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
KitchenMD supports Combined Recipes: files whose sole purpose is to sequence steps from two or more other Recipe Files into a single cooking order (e.g. "penne alla vodka + italian meatballs").
|
||||
To express this, a Combined Recipe needs to embed specific steps from other files — not entire files or sections, but individual lines of instruction.
|
||||
|
||||
Obsidian's existing transclusion syntax (`![[file#heading]]`) transclude a section from heading to the next heading.
|
||||
It has no native concept of step-level granularity.
|
||||
|
||||
Two approaches were considered:
|
||||
|
||||
**Option A — Extend Obsidian's anchor syntax with a `:N` suffix**
|
||||
`![[file#section:N]]` for a named section, `![[file#N]]` for a headingless recipe.
|
||||
The `:N` part is a KitchenMD-specific extension; vanilla Obsidian does not recognise it and renders an "unable to find" error for the anchor.
|
||||
|
||||
**Option B — Use Obsidian block references**
|
||||
Obsidian supports `![[file#^blockid]]` to transclude a specific block.
|
||||
Authors would manually assign block IDs to every step (e.g. `^step-1`, `^step-2`).
|
||||
This is valid Obsidian syntax and would render correctly without KitchenMD tooling.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option A** — extend Obsidian's anchor syntax with `:N` for section-scoped steps and bare `#N` for headingless recipes.
|
||||
|
||||
## Rationale
|
||||
|
||||
Option B (block references) requires authors to manually annotate every step with a block ID, which is tedious, error-prone, and pollutes the source file with machine IDs (`^step-1`) that are meaningless to human readers.
|
||||
The authoring cost is too high for a format designed to be written naturally.
|
||||
|
||||
Option A requires no author effort beyond normal recipe writing.
|
||||
Step numbers are derived automatically by the parser from document structure.
|
||||
The compatibility cost is acceptable: vanilla Obsidian renders a visible "unable to find" error rather than silently corrupting content, so the break is obvious and recoverable.
|
||||
Combined Recipes are a specialised document type — authors who create them are expected to use KitchenMD tooling.
|
||||
|
||||
Step numbers are **section-local** (reset at each heading) rather than global, so adding or removing steps in one section does not renumber steps in other sections and does not break existing Step References elsewhere.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Combined Recipe files will not render correctly in vanilla Obsidian without the KitchenMD plugin; they will show "unable to find" errors for step anchors.
|
||||
- Individual Recipe Files (non-combined) are unaffected — they contain no Step References and render correctly in vanilla Obsidian.
|
||||
- The KitchenMD parser and future Obsidian plugin must implement step counting to resolve `![[file#section:N]]` and `![[file#N]]` references.
|
||||
223
SPEC.md
Normal file
223
SPEC.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# KitchenMD Language Specification
|
||||
|
||||
KitchenMD is [Obsidian Flavored Markdown](https://help.obsidian.md/obsidian-flavored-markdown) with three inline annotation extensions (**Ingredient**, **Cookware**, **Timer**) and one structural extension (**Step Reference**).
|
||||
A KitchenMD file is a valid `.md` file and renders correctly in Obsidian without a plugin.
|
||||
|
||||
---
|
||||
|
||||
## Base language
|
||||
|
||||
[Obsidian Flavored Markdown](https://help.obsidian.md/obsidian-flavored-markdown) is itself a superset of CommonMark.
|
||||
All Obsidian Flavored Markdown constructs are valid and carry their standard meaning: headings, paragraphs, lists, blockquotes, code spans, fenced code blocks, links, wikilinks (`[[page]]`), transclusion (`![[page]]`), YAML frontmatter, and so on.
|
||||
|
||||
YAML frontmatter (standard `---` delimiters) is passed through as-is.
|
||||
The format places no schema on frontmatter fields; consumers read whatever fields they need.
|
||||
|
||||
---
|
||||
|
||||
## Annotations
|
||||
|
||||
Annotations are inline markers that carry structured data about ingredients, cookware, and timers.
|
||||
They are written as plain text and do not conflict with any CommonMark syntax.
|
||||
|
||||
### Scope
|
||||
|
||||
Annotations are processed wherever CommonMark processes inline content:
|
||||
|
||||
- **Parsed**: paragraphs, headings, list items, blockquotes
|
||||
- **Ignored**: code spans, fenced/indented code blocks, HTML comments, raw HTML blocks
|
||||
|
||||
### No escape mechanism
|
||||
|
||||
A bare `@` or `$` not followed by a valid annotation pattern is passed through as plain text — no backslash escape is needed.
|
||||
For `~`, the restricted unit vocabulary (see Timer) prevents false positives.
|
||||
|
||||
---
|
||||
|
||||
## Ingredient
|
||||
|
||||
```
|
||||
@name{}
|
||||
@name{quantity}
|
||||
@name{quantity unit}
|
||||
```
|
||||
|
||||
**Sigil**: `@`
|
||||
|
||||
**Name**: everything between `@` and `{`.
|
||||
Valid characters: any character except `{`, `}`, `@`, `$`, `~`.
|
||||
Multi-word names work naturally: `@unsalted butter{30 g}`.
|
||||
|
||||
**Braces**: mandatory. Bare `@name` without braces is not a valid annotation.
|
||||
|
||||
**Quantity**: optional. Valid formats:
|
||||
- Integer: `2`
|
||||
- Decimal: `0.5`
|
||||
- Simple fraction: `1/2`
|
||||
- Mixed number: `1 1/2`
|
||||
|
||||
**Unit**: optional, free-form string.
|
||||
When both quantity and unit are present, they are delimited by the **last space** inside the braces.
|
||||
`@butter{1 1/2 tbsp}` → quantity `1 1/2`, unit `tbsp`.
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
@eggs{2}
|
||||
@flour{200 g}
|
||||
@unsalted butter{30 g}
|
||||
@butter{1 1/2 tbsp}
|
||||
@black pepper{}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cookware
|
||||
|
||||
```
|
||||
$name{}
|
||||
$name{quantity}
|
||||
$name{quantity unit}
|
||||
```
|
||||
|
||||
**Sigil**: `$`
|
||||
|
||||
**Name**: same rules as Ingredient — any character except `{`, `}`, `@`, `$`, `~`.
|
||||
|
||||
**Braces**: mandatory.
|
||||
|
||||
**Quantity**: optional. Follows the same format rules as Ingredient quantity (integer, decimal, fraction, mixed number).
|
||||
|
||||
**Unit**: optional, free-form. Same last-space delimiter rule as Ingredient.
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
$mixing bowl{}
|
||||
$non-stick frying pan{}
|
||||
$plate{3}
|
||||
$cupcake liner{12 large}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timer
|
||||
|
||||
```
|
||||
~N unit
|
||||
~N-N unit
|
||||
```
|
||||
|
||||
**Sigil**: `~`
|
||||
|
||||
**Number (N)**: follows the same format rules as Ingredient/Cookware quantity (integer, decimal, simple fraction, mixed number).
|
||||
|
||||
**Range**: two quantities separated by `-`. Since negative quantities do not exist, the `-` is unambiguous.
|
||||
`~1/2-1 hr` → range of 1/2 hr to 1 hr.
|
||||
|
||||
**Unit**: one word from the known set, normalised to its canonical abbreviation:
|
||||
|
||||
| Written | Canonical |
|
||||
|---------|-----------|
|
||||
| `sec`, `secs`, `second`, `seconds` | `s` |
|
||||
| `min`, `mins`, `minute`, `minutes` | `min` |
|
||||
| `hr`, `hrs`, `hour`, `hours` | `hr` |
|
||||
|
||||
A `~` followed by a number but not a known unit word is not a valid annotation and passes through as plain text.
|
||||
|
||||
**Termination**: the annotation ends after the unit word. Trailing prose is ignored.
|
||||
`~1 min more` → timer of 1 min; "more" is plain text.
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
~1 min
|
||||
~2-3 mins
|
||||
~1.5 hrs
|
||||
~1/2-1 hr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recipe structure
|
||||
|
||||
A KitchenMD file is prose instructions only.
|
||||
There is no separate ingredients list section.
|
||||
Ingredients, cookware, and timers are derived entirely by tooling from inline annotations.
|
||||
|
||||
---
|
||||
|
||||
## Steps
|
||||
|
||||
A **step** is the atomic unit of a recipe instruction.
|
||||
Steps are counted by tooling; authors write naturally without numbering them.
|
||||
|
||||
A step is one of:
|
||||
- A single non-empty line in a prose context (paragraph or heading body).
|
||||
- A single ordered list item.
|
||||
|
||||
Headings are not steps.
|
||||
Step numbers are **1-based** and **section-local**: they reset at each heading and do not change when steps are added to other sections.
|
||||
|
||||
---
|
||||
|
||||
## Step References
|
||||
|
||||
A Step Reference embeds a specific step from another Recipe File.
|
||||
It is used in Combined Recipes to sequence steps across files.
|
||||
|
||||
```
|
||||
![[filename#section:N]] (step N within a named section)
|
||||
![[filename#N]] (step N in a headingless recipe)
|
||||
```
|
||||
|
||||
- `filename` — the target Recipe File, without the `.md` extension.
|
||||
- `section` — the heading text of the section containing the step, exactly as written (case-sensitive).
|
||||
- `N` — the 1-based step number within that section.
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
![[italian meatballs#rolling:2]]
|
||||
![[basic brine#3]]
|
||||
```
|
||||
|
||||
### Combined Recipes
|
||||
|
||||
A Combined Recipe is a Recipe File that contains only Step References (and optionally frontmatter and prose headings for organisation).
|
||||
It has no annotations of its own — ingredients and timers belong to the referenced recipes.
|
||||
Its purpose is to define the execution order of steps across two or more recipes, such as an index card for a multi-component meal.
|
||||
|
||||
**Compatibility note**: Step References are a KitchenMD extension.
|
||||
Vanilla Obsidian does not recognise `#section:N` or `#N` as valid anchors and will show an "unable to find" error.
|
||||
Individual (non-combined) Recipe Files are unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Formal grammar (ABNF sketch)
|
||||
|
||||
```abnf
|
||||
ingredient = "@" name "{" [quantity [SP unit]] "}"
|
||||
cookware = "$" name "{" [quantity [SP unit]] "}"
|
||||
timer = "~" quantity SP time-unit
|
||||
|
||||
name = 1*(any-char)
|
||||
any-char = %x00-7E except "{" / "}" / "@" / "$" / "~"
|
||||
|
||||
quantity = mixed-number / fraction / decimal / integer
|
||||
integer = 1*DIGIT
|
||||
decimal = 1*DIGIT "." 1*DIGIT
|
||||
fraction = integer "/" integer
|
||||
mixed-number = integer SP fraction
|
||||
|
||||
time-unit = "sec" / "secs" / "second" / "seconds"
|
||||
/ "min" / "mins" / "minute" / "minutes"
|
||||
/ "hr" / "hrs" / "hour" / "hours"
|
||||
|
||||
timer-range = "~" quantity "-" quantity SP time-unit
|
||||
|
||||
step-ref-sectioned = "![[" filename "#" section ":" step-num "]]"
|
||||
step-ref-headingless = "![[" filename "#" step-num "]]"
|
||||
filename = 1*(any-char)
|
||||
section = 1*(any-char except ":" / "]")
|
||||
step-num = 1*DIGIT
|
||||
```
|
||||
|
||||
> Note: `unit` in ingredient/cookware is free-form and not captured in the grammar above.
|
||||
> The last-space rule applies at parse time: everything after the last space inside `{}` is the unit; everything before it is the quantity.
|
||||
@@ -10,18 +10,7 @@ source: https://example.com/pancakes
|
||||
A simple weekend breakfast that comes together in minutes.
|
||||
Pairs well with [[maple syrup]] or fresh fruit from the [[farmers market]].
|
||||
|
||||
## Ingredients
|
||||
|
||||
- @flour{200 g}
|
||||
- @milk{300 ml}
|
||||
- @eggs{2}
|
||||
- @unsalted butter{30 g}
|
||||
- @baking powder{1 tsp}
|
||||
- @salt{0.5 tsp}
|
||||
|
||||
## Method
|
||||
|
||||
### Batter
|
||||
## Batter
|
||||
|
||||
Sift @flour{200 g} and @baking powder{1 tsp} into a $mixing bowl{} along with @salt{0.5 tsp}.
|
||||
Make a well in the centre and crack in @eggs{2}.
|
||||
|
||||
Reference in New Issue
Block a user