This repository has been archived on 2026-07-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kitchen-md/SPEC.md
alexion 9958b6e6d5 docs: sync core-parser spec and add unit normalisation
Bring core-parser.md in line with the resolved design in CONTEXT.md and
ADRs 0004/0005, and promote a new decision to normalise ingredient/cookware
units.

- Full sync of the core-parser spec: diagnostics/total-function channel,
  container blocks (ListItemBlock, BlockquoteBlock), RawInline alongside
  RawBlock via position-slicing, the unified WikilinkNode/TransclusionNode
  shape as two typed nodes, and the minimal remark plugin set.
- New ADR 0006: normalise ingredient/cookware units via a known-alias table
  with passthrough, case-insensitive, canonical-only; align Timer matching
  to case-insensitive too.
- SPEC.md: units are normalised (new Units section, alias table); Timer table
  marked case-insensitive.
- CONTEXT.md: add Unit Normalisation entry, resolve the open item.
- fixtures/basic.md: add a blockquote (callout) carrying an annotation and a
  non-canonical unit so the integration test exercises containers and
  normalisation.
- Expand the core test.todo checklists to cover the new behaviour (containers,
  raw fallbacks, diagnostics, unit normalisation, wikilink/transclusion
  variants); still pending, suite stays green.
2026-07-14 21:55:09 -04:00

8.9 KiB

KitchenMD Language Specification

KitchenMD is 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 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 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 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), 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)

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.