# 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. The quantity/unit split is **grammar-driven**: the parser matches the quantity greedily against the quantity grammar (mixed number → fraction → decimal → integer) anchored at the start of the braces, and any non-empty remainder after the delimiting space is the unit. `@butter{1 1/2 tbsp}` → quantity `1 1/2`, unit `tbsp`. `@butter{1 1/2}` → quantity `1 1/2`, no unit. A plain "last space" rule cannot express this, because a mixed-number quantity itself contains a space; the grammar is authoritative. If the brace content does not begin with a grammar-valid quantity (e.g. `@stock{a splash}`), the entire content is preserved as the quantity string and the unit is left empty, so no annotation is dropped. The unit is **normalised**: a unit matching a known alias is rewritten to its canonical abbreviation (case-insensitive), and any unit not in the table is passed through verbatim. See the [Units](#units) section for the alias table. **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. Same grammar-driven quantity/unit split and canonical [unit normalisation](#units) 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, matched **case-insensitively** and 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 ``` --- ## Units Ingredient and Cookware units are normalised: a unit matching a known alias is rewritten to its canonical abbreviation, and any unit not in the table is passed through verbatim (its original casing and spacing preserved). Lookup is **case-insensitive**, and multi-word aliases (`fluid ounce`) match on the whole unit string. Only the canonical unit is retained; the author's original spelling is not kept. | Canonical | Aliases | |-----------|---------| | `g` | g, gram, grams | | `kg` | kg, kilogram, kilograms, kilo, kilos | | `mg` | mg, milligram, milligrams | | `oz` | oz, ounce, ounces | | `lb` | lb, lbs, pound, pounds | | `ml` | ml, milliliter, millilitre, milliliters, millilitres | | `l` | l, liter, litre, liters, litres | | `tsp` | tsp, teaspoon, teaspoons | | `tbsp` | tbsp, tablespoon, tablespoons | | `cup` | cup, cups | | `fl oz` | fl oz, fluid ounce, fluid ounces | | `pt` | pt, pint, pints | | `qt` | qt, quart, quarts | | `gal` | gal, gallon, gallons | Single-letter cooking abbreviations (`t`, `T`, `c`) are intentionally excluded as ambiguous. Count and descriptive units (`clove`, `pinch`, `dash`, `can`, `large`, `to taste`) have no canonical form and pass through unchanged. Timer units are normalised from their own known set (see [Timer](#timer)), also case-insensitively. --- ## 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. > At parse time the quantity is matched greedily against the `quantity` production anchored at the start of `{}`; any non-empty remainder after the delimiting space is the unit. > If the content does not begin with a grammar-valid `quantity`, the whole brace content becomes the quantity string and the unit is empty.