From 6c8f714d650ffb915cb0eb6ae0b7da5c3f8b066b Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Sun, 15 Mar 2026 12:41:44 +1100 Subject: [PATCH 1/5] Coalesce fragments into single strings when rendering source --- src/domain/source/adapter.rs | 50 +++++++++++++++++++++++++++++++----- src/formatting/formatter.rs | 2 +- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/domain/source/adapter.rs b/src/domain/source/adapter.rs index cfd8b6e6..cd609a28 100644 --- a/src/domain/source/adapter.rs +++ b/src/domain/source/adapter.rs @@ -19,14 +19,50 @@ impl Adapter for SourceAdapter { fn extract(&self, document: &language::Document) -> Document { let fragments = format_with_renderer(document, WIDTH); + let fragments: Vec = fragments + .into_iter() + .map(|(syntax, content)| Fragment { + syntax: format!("{:?}", syntax), + content: content.into_owned(), + }) + .collect(); + Document { - fragments: fragments - .into_iter() - .map(|(syntax, content)| Fragment { - syntax: format!("{:?}", syntax), - content: content.into_owned(), - }) - .collect(), + fragments: coalesce(fragments), } } } + +/// Merge adjacent fragments to reduce verbosity in serialized output. +/// Same-syntax fragments are concatenated. Whitespace-only fragments +/// tagged Neutral or Description are absorbed into the preceding +/// fragment, allowing subsequent same-syntax merges to collapse runs +/// of words into single strings. +fn coalesce(fragments: Vec) -> Vec { + let mut result: Vec = Vec::with_capacity(fragments.len()); + + for frag in fragments { + if let Some(last) = result.last_mut() { + if last.syntax == frag.syntax && frag.syntax != "Newline" { + last.content.push_str(&frag.content); + continue; + } + if is_text_whitespace(&frag) { + last.content.push_str(&frag.content); + continue; + } + } + result.push(frag); + } + + result +} + +fn is_text_whitespace(frag: &Fragment) -> bool { + (frag.syntax == "Neutral" || frag.syntax == "Description") + && !frag.content.is_empty() + && frag + .content + .bytes() + .all(|b| b == b' ') +} diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 2e0f05f8..aad12df7 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -1249,7 +1249,7 @@ impl<'a, 'i> Line<'a, 'i> { + word.len() as u8; } else { self.current - .push((Syntax::Description, Cow::Borrowed(" "))); + .push((syntax, Cow::Borrowed(" "))); self.current .push((syntax, Cow::Borrowed(word))); self.position += 1 + word.len() as u8; From d2b7b9181e57e52b78ea9b37c02e670fda745053 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Sun, 15 Mar 2026 13:57:45 +1100 Subject: [PATCH 2/5] Wrap blocks around steps in source domain --- src/domain/source/adapter.rs | 6 +++++- src/domain/source/typst.rs | 4 ++++ src/formatting/formatter.rs | 4 +++- src/formatting/syntax.rs | 2 ++ src/highlighting/terminal.rs | 1 + src/highlighting/typst.rs | 1 + src/templating/checklist.typ | 2 ++ src/templating/procedure.typ | 2 ++ 8 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/domain/source/adapter.rs b/src/domain/source/adapter.rs index cd609a28..d4ebb23b 100644 --- a/src/domain/source/adapter.rs +++ b/src/domain/source/adapter.rs @@ -43,7 +43,11 @@ fn coalesce(fragments: Vec) -> Vec { for frag in fragments { if let Some(last) = result.last_mut() { - if last.syntax == frag.syntax && frag.syntax != "Newline" { + if last.syntax == frag.syntax + && frag.syntax != "Newline" + && frag.syntax != "StepBegin" + && frag.syntax != "StepEnd" + { last.content.push_str(&frag.content); continue; } diff --git a/src/domain/source/typst.rs b/src/domain/source/typst.rs index a9468503..3f38ef6c 100644 --- a/src/domain/source/typst.rs +++ b/src/domain/source/typst.rs @@ -48,6 +48,10 @@ impl Render for Fragment { if self.syntax == "Newline" { out.raw(&format!("#{}()\n", func)); + } else if self.syntax == "StepBegin" { + out.raw("#block(breakable: false)["); + } else if self.syntax == "StepEnd" { + out.raw("]"); } else { out.raw(&format!( "#{}(\"{}\")", diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index aad12df7..28d5762e 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -63,7 +63,7 @@ pub fn format_with_renderer<'i>(technique: &'i Document, width: u8) -> Vec<(Synt .fragments .last() { - if !last_content.ends_with('\n') { + if !last_content.is_empty() && !last_content.ends_with('\n') { output.add_fragment_reference(Syntax::Description, "\n"); } } @@ -720,6 +720,7 @@ impl<'i> Formatter<'i> { } fn append_step(&mut self, step: &'i Scope) { + self.add_fragment_reference(Syntax::StepBegin, ""); match step { Scope::DependentBlock { ordinal, @@ -768,6 +769,7 @@ impl<'i> Formatter<'i> { } _ => panic!("Shouldn't be calling append_step() with a non-step Scope"), } + self.add_fragment_reference(Syntax::StepEnd, ""); } fn append_responses(&mut self, responses: &'i Vec) { diff --git a/src/formatting/syntax.rs b/src/formatting/syntax.rs index eb016ecf..8cf5468d 100644 --- a/src/formatting/syntax.rs +++ b/src/formatting/syntax.rs @@ -28,6 +28,8 @@ pub enum Syntax { Language, Attribute, Structure, + StepBegin, + StepEnd, } /// Trait for different rendering backends (the no-op no-markup one, ANSI diff --git a/src/highlighting/terminal.rs b/src/highlighting/terminal.rs index 66fbeed3..4bb94a70 100644 --- a/src/highlighting/terminal.rs +++ b/src/highlighting/terminal.rs @@ -95,6 +95,7 @@ impl Render for Terminal { .color(owo_colors::Rgb(153, 153, 153)) .bold() .to_string(), + Syntax::StepBegin | Syntax::StepEnd => String::new(), } } } diff --git a/src/highlighting/typst.rs b/src/highlighting/typst.rs index 6d92c555..b53f01d3 100644 --- a/src/highlighting/typst.rs +++ b/src/highlighting/typst.rs @@ -38,6 +38,7 @@ impl Render for Typst { Syntax::Language => markup("fill: rgb(0xc4, 0xa0, 0x00), weight: \"bold\"", &content), Syntax::Attribute => markup("weight: \"bold\"", &content), Syntax::Structure => markup("fill: rgb(0x99, 0x99, 0x99), weight: \"bold\"", &content), + Syntax::StepBegin | Syntax::StepEnd => String::new(), } } } diff --git a/src/templating/checklist.typ b/src/templating/checklist.typ index 62ddf6d0..2a6b4656 100644 --- a/src/templating/checklist.typ +++ b/src/templating/checklist.typ @@ -26,6 +26,7 @@ } #let render-step(ordinal: none, title: none, body: (), role: none, responses: none, children: none) = { + block(breakable: false, { if role != none { text(weight: "bold")[#role] parbreak() @@ -43,6 +44,7 @@ parbreak() } if children != none { children } + }) } // -- Default template -------------------------------------------------------- diff --git a/src/templating/procedure.typ b/src/templating/procedure.typ index 1b490a55..45187098 100644 --- a/src/templating/procedure.typ +++ b/src/templating/procedure.typ @@ -61,6 +61,7 @@ } #let render-step(ordinal: none, title: none, body: (), invocations: (), responses: none, children: none) = { + block(breakable: false, { if invocations.len() > 0 { text(size: 7pt, raw(invocations.join(", "))) linebreak() @@ -84,6 +85,7 @@ if children != none { pad(left: 16pt, children) } + }) } #let render-response(value: none, condition: none) = { From faa2605fe6e8653b724c841d8d0bfaa2fb437bdb Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Tue, 17 Mar 2026 01:08:02 +1100 Subject: [PATCH 3/5] Rename to BlockBegin and BlockEnd --- src/domain/source/adapter.rs | 5 +++-- src/domain/source/typst.rs | 12 +++++++----- src/formatting/formatter.rs | 5 +++-- src/formatting/syntax.rs | 11 +++++++---- src/highlighting/terminal.rs | 2 +- src/highlighting/typst.rs | 2 +- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/domain/source/adapter.rs b/src/domain/source/adapter.rs index d4ebb23b..8e997e84 100644 --- a/src/domain/source/adapter.rs +++ b/src/domain/source/adapter.rs @@ -45,8 +45,8 @@ fn coalesce(fragments: Vec) -> Vec { if let Some(last) = result.last_mut() { if last.syntax == frag.syntax && frag.syntax != "Newline" - && frag.syntax != "StepBegin" - && frag.syntax != "StepEnd" + && frag.syntax != "BlockBegin" + && frag.syntax != "BlockEnd" { last.content.push_str(&frag.content); continue; @@ -70,3 +70,4 @@ fn is_text_whitespace(frag: &Fragment) -> bool { .bytes() .all(|b| b == b' ') } + diff --git a/src/domain/source/typst.rs b/src/domain/source/typst.rs index 3f38ef6c..d956c07c 100644 --- a/src/domain/source/typst.rs +++ b/src/domain/source/typst.rs @@ -46,12 +46,14 @@ impl Render for Fragment { _ => "render-neutral", }; - if self.syntax == "Newline" { + if self.syntax == "BlockBegin" { + out.raw("#render-block()[\n"); + return; + } else if self.syntax == "BlockEnd" { + out.raw("]\n"); + return; + } else if self.syntax == "Newline" { out.raw(&format!("#{}()\n", func)); - } else if self.syntax == "StepBegin" { - out.raw("#block(breakable: false)["); - } else if self.syntax == "StepEnd" { - out.raw("]"); } else { out.raw(&format!( "#{}(\"{}\")", diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 28d5762e..9e7240ce 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -720,7 +720,7 @@ impl<'i> Formatter<'i> { } fn append_step(&mut self, step: &'i Scope) { - self.add_fragment_reference(Syntax::StepBegin, ""); + self.add_fragment_reference(Syntax::BlockBegin, ""); match step { Scope::DependentBlock { ordinal, @@ -769,7 +769,8 @@ impl<'i> Formatter<'i> { } _ => panic!("Shouldn't be calling append_step() with a non-step Scope"), } - self.add_fragment_reference(Syntax::StepEnd, ""); + + self.add_fragment_reference(Syntax::BlockEnd, ""); } fn append_responses(&mut self, responses: &'i Vec) { diff --git a/src/formatting/syntax.rs b/src/formatting/syntax.rs index 8cf5468d..eeb259f5 100644 --- a/src/formatting/syntax.rs +++ b/src/formatting/syntax.rs @@ -28,8 +28,8 @@ pub enum Syntax { Language, Attribute, Structure, - StepBegin, - StepEnd, + BlockBegin, + BlockEnd, } /// Trait for different rendering backends (the no-op no-markup one, ANSI @@ -43,7 +43,10 @@ pub trait Render { pub struct Identity; impl Render for Identity { - fn style(&self, _syntax: Syntax, content: &str) -> String { - content.to_string() + fn style(&self, syntax: Syntax, content: &str) -> String { + match syntax { + Syntax::BlockBegin | Syntax::BlockEnd => String::new(), + _ => content.to_string(), + } } } diff --git a/src/highlighting/terminal.rs b/src/highlighting/terminal.rs index 4bb94a70..9a564b9f 100644 --- a/src/highlighting/terminal.rs +++ b/src/highlighting/terminal.rs @@ -95,7 +95,7 @@ impl Render for Terminal { .color(owo_colors::Rgb(153, 153, 153)) .bold() .to_string(), - Syntax::StepBegin | Syntax::StepEnd => String::new(), + Syntax::BlockBegin | Syntax::BlockEnd => String::new(), } } } diff --git a/src/highlighting/typst.rs b/src/highlighting/typst.rs index b53f01d3..d8052d62 100644 --- a/src/highlighting/typst.rs +++ b/src/highlighting/typst.rs @@ -38,7 +38,7 @@ impl Render for Typst { Syntax::Language => markup("fill: rgb(0xc4, 0xa0, 0x00), weight: \"bold\"", &content), Syntax::Attribute => markup("weight: \"bold\"", &content), Syntax::Structure => markup("fill: rgb(0x99, 0x99, 0x99), weight: \"bold\"", &content), - Syntax::StepBegin | Syntax::StepEnd => String::new(), + Syntax::BlockBegin | Syntax::BlockEnd => String::new(), } } } From f741604476467e2b82e13146a1923a56f319e89f Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Tue, 17 Mar 2026 01:08:19 +1100 Subject: [PATCH 4/5] Fix line spacing when blocks wrapped around content --- src/formatting/formatter.rs | 16 +++++++++++++--- src/templating/source.typ | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 9e7240ce..d69e420c 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -473,7 +473,9 @@ impl<'i> Formatter<'i> { self.append_char('\n'); } - // declaration + // declaration and title kept together + + self.add_fragment_reference(Syntax::BlockBegin, ""); let name = &procedure.name; self.add_fragment_reference(Syntax::Declaration, name.0); @@ -497,9 +499,17 @@ impl<'i> Formatter<'i> { self.append_char('\n'); - // elements + // include title in block to keep it with the declaration + let mut elements = procedure.elements.iter(); + if let Some(Element::Title(_)) = procedure.elements.first() { + self.append_element(elements.next().unwrap()); + } + + self.add_fragment_reference(Syntax::BlockEnd, ""); + + // remaining elements - for element in &procedure.elements { + for element in elements { self.append_element(element); } } diff --git a/src/templating/source.typ b/src/templating/source.typ index 96ff7753..0081aa16 100644 --- a/src/templating/source.typ +++ b/src/templating/source.typ @@ -30,11 +30,13 @@ #let render-language(c) = text(fill: rgb(0xc4, 0xa0, 0x00), weight: "bold", raw(c)) #let render-attribute(c) = text(weight: "bold", raw(c)) #let render-structure(c) = text(fill: rgb(0x99, 0x99, 0x99), weight: "bold", raw(c)) +#let render-block(body) = block(breakable: false, spacing: 0.65em, body) // -- Default template -------------------------------------------------------- #let template(body) = { set text(font: "Inconsolata") + set par(spacing: 0.65em, leading: 0.65em) show raw: set block(breakable: true) body } From 56e857d46d8b784655d02c4814531c4215890a81 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Tue, 17 Mar 2026 09:26:21 +1100 Subject: [PATCH 5/5] Keep Attribute lines with their immediately following step --- src/formatting/formatter.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index d69e420c..51f5906d 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -814,25 +814,33 @@ impl<'i> Formatter<'i> { attributes, subscopes, } => { + if subscopes.len() == 0 { + self.indent(); + self.append_attributes(attributes); + self.add_fragment_reference(Syntax::Newline, "\n"); + return; + } + + let is_code = + if let Scope::CodeBlock { .. } = subscopes[0] { true } else { false }; + + // Keep attribute with its first subscope + self.add_fragment_reference(Syntax::BlockBegin, ""); self.indent(); self.append_attributes(attributes); self.add_fragment_reference(Syntax::Newline, "\n"); - if subscopes.len() == 0 { - return; + if !is_code { + self.increase(4); } + self.append_scope(&subscopes[0]); + self.add_fragment_reference(Syntax::BlockEnd, ""); - let first = subscopes - .iter() - .next() - .unwrap(); + for scope in &subscopes[1..] { + self.append_scope(scope); + } - if let Scope::CodeBlock { .. } = first { - // do NOT increase indent - self.append_scopes(subscopes); - } else { - self.increase(4); - self.append_scopes(subscopes); + if !is_code { self.decrease(4); } }