diff --git a/src/domain/source/adapter.rs b/src/domain/source/adapter.rs index cfd8b6e6..8e997e84 100644 --- a/src/domain/source/adapter.rs +++ b/src/domain/source/adapter.rs @@ -19,14 +19,55 @@ 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" + && frag.syntax != "BlockBegin" + && frag.syntax != "BlockEnd" + { + 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/domain/source/typst.rs b/src/domain/source/typst.rs index a9468503..d956c07c 100644 --- a/src/domain/source/typst.rs +++ b/src/domain/source/typst.rs @@ -46,7 +46,13 @@ 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 { out.raw(&format!( diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 2e0f05f8..51f5906d 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"); } } @@ -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, ""); - for element in &procedure.elements { + // remaining elements + + for element in elements { self.append_element(element); } } @@ -720,6 +730,7 @@ impl<'i> Formatter<'i> { } fn append_step(&mut self, step: &'i Scope) { + self.add_fragment_reference(Syntax::BlockBegin, ""); match step { Scope::DependentBlock { ordinal, @@ -768,6 +779,8 @@ impl<'i> Formatter<'i> { } _ => panic!("Shouldn't be calling append_step() with a non-step Scope"), } + + self.add_fragment_reference(Syntax::BlockEnd, ""); } fn append_responses(&mut self, responses: &'i Vec) { @@ -801,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); } } @@ -1249,7 +1270,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; diff --git a/src/formatting/syntax.rs b/src/formatting/syntax.rs index eb016ecf..eeb259f5 100644 --- a/src/formatting/syntax.rs +++ b/src/formatting/syntax.rs @@ -28,6 +28,8 @@ pub enum Syntax { Language, Attribute, Structure, + BlockBegin, + BlockEnd, } /// Trait for different rendering backends (the no-op no-markup one, ANSI @@ -41,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 66fbeed3..9a564b9f 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::BlockBegin | Syntax::BlockEnd => String::new(), } } } diff --git a/src/highlighting/typst.rs b/src/highlighting/typst.rs index 6d92c555..d8052d62 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::BlockBegin | Syntax::BlockEnd => 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) = { 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 }