Skip to content

Instantly share code, notes, and snippets.

@AdrianVollmer
Created November 2, 2025 17:43
Show Gist options
  • Select an option

  • Save AdrianVollmer/07edab2b1fba2747fbf45291ab73d81e to your computer and use it in GitHub Desktop.

Select an option

Save AdrianVollmer/07edab2b1fba2747fbf45291ab73d81e to your computer and use it in GitHub Desktop.
Typst program to render a tandoor recipe
// Inspired by https://github.com/maxdinech/typst-recipe
// Call like this:
// typst compile tandoor-recipe.typ --input 'data={"recipe": "recipe.json", "image": "image.jpg"}'
#let primary_colour = rgb("#ce1f36")
#let text_colour = rgb("#333")
// Font constants
#let body_font = "Libertinus Serif"
#let title_font = "DejaVu Sans"
#let author_font = "DejaVu Sans"
#let heading_font = "DejaVu Sans"
#let image-height = 15em
#set page(
margin: (x: 54pt, y: 52pt),
numbering: "1",
number-align: right,
fill: rgb("ede8d0"),
)
#let recipes(title, doc) = {
set text(10pt, font: body_font)
text(fill: primary_colour, font: title_font, size: 30pt, weight: 100, title)
set align(left)
show heading.where(level: 1): it => [
#pagebreak()
#v(300pt)
#set align(center)
#text(
fill: primary_colour,
font: heading_font,
weight: 300,
size: 20pt,
{ it.body },
)
#text(" ")
#pagebreak()
]
doc
}
#let format_ingredient(ingredient) = {
set list(tight: false)
set par(spacing: 0.8em, leading: 0.3em)
let amount = if ingredient.amount > 0 { str(ingredient.amount) } else { "" }
let unit = if ingredient.unit != none { ingredient.unit.name } else { "" }
let food = ingredient.food.name
let note_text = if ingredient.note != none { footnote(ingredient.note) } else { "" }
if amount != "" and unit != "" {
[- #amount #unit #food#note_text]
} else if amount != "" {
[- #amount #food#note_text]
} else {
[- #food#note_text]
}
}
#let display_ingredients_list(ingredients) = {
// panic(ingredients)
// grid(
// columns: (2fr, 1fr),
// gutter: .5em,
// ..ingredients
// .map(it => (
// it.food.name + { if (it.note != none) { (footnote(it.note)) } else { "" } },
// str(it.amount) + " " + it.unit.name,
// ))
// .flatten(),
// )
for ingredient in ingredients {
format_ingredient(ingredient)
}
}
#let display_steps_list(steps) = {
set enum(
numbering: n => text(
fill: primary_colour,
font: heading_font,
size: 14pt,
weight: "bold",
str(n),
),
)
for step in steps {
enum.item()[#step.instruction]
}
}
#let display_ingredients(ingredients) = {
emph(display_ingredients_list(ingredients))
}
#let display_steps(steps) = {
[== Preparation]
set par(justify: true)
set enum(spacing: 1em)
display_steps_list(steps)
}
#let display_pairings(pairings) = {
[== Pairing Suggestions]
emph(pairings)
}
#let recipe(
title: "",
author: "",
description: "",
image_path: "",
servings: 1,
servings_text: "",
working_time: "",
waiting_time: "",
ingredients: (),
steps: [],
remarks: [],
pairings: [],
) = {
show heading.where(level: 2): it => text(
fill: primary_colour,
font: heading_font,
weight: 300,
size: 11pt,
grid(
columns: (auto, auto),
column-gutter: 5pt,
[#{ upper(it.body) }],
[
#v(5pt)
#line(length: 100%, stroke: 0.4pt + primary_colour)
],
),
)
{
grid(
columns: (380pt, 100pt),
[
#text(fill: primary_colour, font: title_font, size: 24pt, weight: 200, upper(title))
#h(3pt)
#text(fill: text_colour, font: author_font, size: 20pt, author)
#v(0pt)
#emph(description)
],
[
#v(2pt)
#set align(right)
#if (working_time != "") {
[_Preparation: #working_time _]
}
#if (waiting_time != "") {
[\ _Waiting: #waiting_time _]
}
],
)
// Display image if it exists
if image_path != "" {
context { place(image(image_path, width: page.width, height: image-height), dx: -page.margin.left) }
v(image-height + 2em)
}
grid(
columns: (90pt, 380pt),
column-gutter: 15pt,
[
#set list(marker: [], body-indent: 0pt)
#set align(right)
#text(fill: primary_colour, font: heading_font, weight: 300, size: 11pt, upper([Ingredients\ ]))
#[#servings servings]
#servings_text
#display_ingredients(ingredients)
],
[
#display_steps(steps)
#if remarks != [] {
[== Chef's Tips]
emph(remarks)
}
#if pairings != [] {
display_pairings(pairings)
}
],
)
v(30pt)
}
}
#let recipe_from_json(data) = {
let recipe_data = json(data.recipe)
let all_ingredients = ()
for step in recipe_data.steps {
for ingredient in step.ingredients {
all_ingredients.push((ingredient))
}
}
let working_time = if recipe_data.working_time > 0 { str(recipe_data.working_time) + " min" } else { "" }
let waiting_time = if recipe_data.waiting_time > 0 { str(recipe_data.waiting_time) + " min" } else { "" }
// Check if image.jpg exists and include it
let image_path = data.at("image", default: "")
recipe(
title: recipe_data.name,
description: recipe_data.description,
servings: recipe_data.servings,
servings_text: recipe_data.servings_text,
working_time: working_time,
waiting_time: waiting_time,
ingredients: all_ingredients,
steps: recipe_data.steps,
image_path: image_path,
)
}
#recipe_from_json(json(bytes(sys.inputs.data)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment