docs: add language spec, domain glossary, and fixture update

This commit is contained in:
2026-07-08 22:35:35 -04:00
parent 220f73d8f7
commit d37988ec59
4 changed files with 382 additions and 12 deletions

223
SPEC.md Normal file
View 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.