From 40f4987753a07f5d3e8fa8886974d357f00f2625 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 09:37:02 +0000 Subject: [PATCH 1/2] feat: support backslash line breaks for formatting in steps and notes The Cooklang parser converts backslash-newline sequences into literal newline characters in text items. This change makes both the CLI and web UI honor those newlines for precise formatting control. CLI (human-readable output): - Step text containing newlines is split into paragraphs, each properly indented under the step number - Note text containing newlines preserves line breaks with consistent indentation Web UI: - Added LineBreak variant to StepItem enum, rendered as
in HTML - Text items are split on newline characters into separate Text and LineBreak items during template data construction - Note paragraphs use white-space: pre-line for newline rendering https://claude.ai/code/session_01YUd1SFaAakySfqdq4qBYnD --- src/server/templates.rs | 1 + src/server/ui.rs | 10 +++++++++- src/util/cooklang_to_human.rs | 31 +++++++++++++++++++++++++------ templates/recipe.html | 4 ++-- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/server/templates.rs b/src/server/templates.rs index ff54714..1268a44 100644 --- a/src/server/templates.rs +++ b/src/server/templates.rs @@ -235,6 +235,7 @@ pub enum StepItem { Cookware(String), Timer(String), Quantity(String), + LineBreak, } #[derive(Debug, Clone, Serialize)] diff --git a/src/server/ui.rs b/src/server/ui.rs index c5fd696..038683c 100644 --- a/src/server/ui.rs +++ b/src/server/ui.rs @@ -336,7 +336,15 @@ async fn recipe_page( match item { Item::Text { value } => { - step_items.push(StepItem::Text(value.to_string())); + let parts: Vec<&str> = value.split('\n').collect(); + for (i, part) in parts.iter().enumerate() { + if i > 0 { + step_items.push(StepItem::LineBreak); + } + if !part.is_empty() { + step_items.push(StepItem::Text(part.to_string())); + } + } } Item::Ingredient { index } => { section_ingredient_indices.insert(*index); diff --git a/src/util/cooklang_to_human.rs b/src/util/cooklang_to_human.rs index 9462d4e..e3525dc 100644 --- a/src/util/cooklang_to_human.rs +++ b/src/util/cooklang_to_human.rs @@ -416,8 +416,17 @@ fn steps(w: &mut impl io::Write, recipe: &Recipe) -> Result { match content { cooklang::Content::Step(step) => { let (step_text, step_ingredients) = step_text(recipe, section, step); - let step_text = format!("{:>2}. {}", step.number, step_text.trim()); - print_wrapped_with_options(w, &step_text, |o| o.subsequent_indent(" "))?; + let paragraphs: Vec<&str> = step_text.trim().split('\n').collect(); + for (i, paragraph) in paragraphs.iter().enumerate() { + if i == 0 { + let first = format!("{:>2}. {}", step.number, paragraph.trim_start()); + print_wrapped_with_options(w, &first, |o| o.subsequent_indent(" "))?; + } else { + print_wrapped_with_options(w, paragraph.trim_start(), |o| { + o.initial_indent(" ").subsequent_indent(" ") + })?; + } + } print_wrapped_with_options(w, &step_ingredients, |o| { let indent = " "; // 5 o.initial_indent(indent) @@ -440,10 +449,20 @@ fn steps(w: &mut impl io::Write, recipe: &Recipe) -> Result { // Format as a note with a visual indicator let note_style = yansi::Style::new().italic().fg(yansi::Color::Blue); let note_prefix = "📝 Note: ".paint(note_style); - write!(w, " {note_prefix}")?; - print_wrapped_with_options(w, t.trim(), |o| { - o.initial_indent("").subsequent_indent(" ") - })?; + let note_indent = " "; + let paragraphs: Vec<&str> = t.trim().split('\n').collect(); + for (i, paragraph) in paragraphs.iter().enumerate() { + if i == 0 { + write!(w, " {note_prefix}")?; + print_wrapped_with_options(w, paragraph.trim(), |o| { + o.initial_indent("").subsequent_indent(note_indent) + })?; + } else { + print_wrapped_with_options(w, paragraph.trim(), |o| { + o.initial_indent(note_indent).subsequent_indent(note_indent) + })?; + } + } writeln!(w)?; } } diff --git a/templates/recipe.html b/templates/recipe.html index f435db8..1afe8b6 100644 --- a/templates/recipe.html +++ b/templates/recipe.html @@ -299,7 +299,7 @@

{% for step_item in step.items %} {% match step_item %} - {% when crate::server::templates::StepItem::Text with (text) %}{{ text }}{% when crate::server::templates::StepItem::Ingredient with { name, reference_path } %}{% match reference_path %}{% when Some with (path) %}{{ name }}{% when None %}{{ name }}{% endmatch %}{% when crate::server::templates::StepItem::Cookware with (name) %}{{ name }}{% when crate::server::templates::StepItem::Timer with (name) %}⏱️ {{ name }}{% when crate::server::templates::StepItem::Quantity with (qty) %}{{ qty }}{% endmatch %}{% endfor %} + {% when crate::server::templates::StepItem::Text with (text) %}{{ text }}{% when crate::server::templates::StepItem::Ingredient with { name, reference_path } %}{% match reference_path %}{% when Some with (path) %}{{ name }}{% when None %}{{ name }}{% endmatch %}{% when crate::server::templates::StepItem::Cookware with (name) %}{{ name }}{% when crate::server::templates::StepItem::Timer with (name) %}⏱️ {{ name }}{% when crate::server::templates::StepItem::Quantity with (qty) %}{{ qty }}{% when crate::server::templates::StepItem::LineBreak %}
{% endmatch %}{% endfor %} {% if step.ingredients.len() > 0 %}
@@ -318,7 +318,7 @@

📝 -

{{ note }}

+

{{ note }}

{% endmatch %} From 22440a6f95a29ae6f16261da72e22ad2e3c9fdd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 13:46:53 +0000 Subject: [PATCH 2/2] fix: use if-let instead of is_some + unwrap for ingredient references Fixes clippy::unnecessary_unwrap warnings in latex, markdown, and typst output formatters. https://claude.ai/code/session_01YUd1SFaAakySfqdq4qBYnD --- src/util/cooklang_to_latex.rs | 4 ++-- src/util/cooklang_to_md.rs | 4 ++-- src/util/cooklang_to_typst.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/util/cooklang_to_latex.rs b/src/util/cooklang_to_latex.rs index 2e98506..d576e40 100644 --- a/src/util/cooklang_to_latex.rs +++ b/src/util/cooklang_to_latex.rs @@ -239,9 +239,9 @@ fn write_ingredients(w: &mut impl io::Write, recipe: &Recipe, converter: &Conver )?; } - if ingredient.reference.is_some() { + if let Some(reference) = &ingredient.reference { let sep = std::path::MAIN_SEPARATOR.to_string(); - let path = ingredient.reference.as_ref().unwrap().components.join(&sep); + let path = reference.components.join(&sep); write!( w, r"\ingredient{{{}}}", diff --git a/src/util/cooklang_to_md.rs b/src/util/cooklang_to_md.rs index 4abfdd4..b4a5f4a 100644 --- a/src/util/cooklang_to_md.rs +++ b/src/util/cooklang_to_md.rs @@ -332,9 +332,9 @@ fn ingredients( } } - if ingredient.reference.is_some() { + if let Some(reference) = &ingredient.reference { let sep = std::path::MAIN_SEPARATOR.to_string(); - let path = ingredient.reference.as_ref().unwrap().components.join(&sep); + let path = reference.components.join(&sep); write!( w, "[{}]({}{}{})", diff --git a/src/util/cooklang_to_typst.rs b/src/util/cooklang_to_typst.rs index f4b5271..e05da06 100644 --- a/src/util/cooklang_to_typst.rs +++ b/src/util/cooklang_to_typst.rs @@ -206,9 +206,9 @@ fn write_ingredients(w: &mut impl io::Write, recipe: &Recipe, converter: &Conver write!(w, r"*{}* ", escape_typst(&entry.quantity.to_string()))?; } - if ingredient.reference.is_some() { + if let Some(reference) = &ingredient.reference { let sep = std::path::MAIN_SEPARATOR.to_string(); - let path = ingredient.reference.as_ref().unwrap().components.join(&sep); + let path = reference.components.join(&sep); write!( w, r#"#ingredient("{}")"#,