diff --git a/.gitignore b/.gitignore index c4a6b556..33c0e46e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ /.vscode /target -# rendered code fragments -/*.pdf +# rendering artifacts +*.pdf +.*.typ # documentation symlinks /doc/references diff --git a/Cargo.lock b/Cargo.lock index 7e95063b..bca15c5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,7 +472,7 @@ dependencies = [ [[package]] name = "technique" -version = "0.5.1" +version = "0.5.2" dependencies = [ "clap", "ignore", diff --git a/Cargo.toml b/Cargo.toml index c7a6b5b4..c8150367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "technique" -version = "0.5.1" +version = "0.5.2" edition = "2021" description = "A domain specific language for procedures." authors = [ "Andrew Cowie" ] diff --git a/README.md b/README.md index 8e657dec..b0b621b4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The _technique_ program has three subcommands: syntax highlighting if run in a terminal. - _render_ \ - Render the Technique document into a printable PDF. This use the Typst + Render the Technique document into a printable PDF. This uses the Typst typestting language and so requires the _typst_ compiler be installed and on `PATH`. diff --git a/examples/prototype/GovernmentForm.tq b/examples/prototype/GovernmentForm.tq index 37e3b6f2..5c3c9bc7 100644 --- a/examples/prototype/GovernmentForm.tq +++ b/examples/prototype/GovernmentForm.tq @@ -6,9 +6,9 @@ drivers_license_application : { [ "Name" = "Kowalski" - "Species" = "Emperor Pengin" + "Species" = "Emperor Penguin" "Age" = 4.2 winters - "Unique Pengiun Identifier" = uuid() + "Unique Penguin Identifier" = uuid() "Occupation" = "Operational Planning Specialist" "Permanent Address" = "Central Park Zoo, New York, NY, 10021, United States" ] diff --git a/src/domain/checklist/adapter.rs b/src/domain/checklist/adapter.rs index a0cf2711..201634c2 100644 --- a/src/domain/checklist/adapter.rs +++ b/src/domain/checklist/adapter.rs @@ -4,8 +4,8 @@ //! role assignments are inherited by sub steps, and SectionChunks are //! rendered as headings with their sub-procedures' steps as children. -use crate::language; use crate::domain::Adapter; +use crate::language; use super::types::{Document, Response, Section, Step}; @@ -209,8 +209,8 @@ fn step_from_scope(scope: &language::Scope, inherited_role: Option<&str>) -> Ste mod check { use std::path::Path; - use crate::parsing; use crate::domain::Adapter; + use crate::parsing; use super::ChecklistAdapter; diff --git a/src/domain/checklist/mod.rs b/src/domain/checklist/mod.rs index 35135afb..93a61742 100644 --- a/src/domain/checklist/mod.rs +++ b/src/domain/checklist/mod.rs @@ -1,2 +1,3 @@ pub mod adapter; pub mod types; +mod typst; diff --git a/src/domain/checklist/types.rs b/src/domain/checklist/types.rs index e2c7a3c8..abe72210 100644 --- a/src/domain/checklist/types.rs +++ b/src/domain/checklist/types.rs @@ -3,8 +3,6 @@ //! A checklist is moderately structured and relatively flat: sections with //! headings, steps with checkboxes, response options, and limited nesting. -use crate::domain::typst::{Data, Render}; - /// A checklist is a document of sections containing steps. pub struct Document { pub sections: Vec
, @@ -18,14 +16,6 @@ impl Document { } } -impl Render for Document { - fn render(&self, data: &mut Data) { - data.open(); - data.list("sections", &self.sections); - data.close(); - } -} - /// A section within a checklist. pub struct Section { pub ordinal: Option, @@ -33,16 +23,6 @@ pub struct Section { pub steps: Vec, } -impl Render for Section { - fn render(&self, data: &mut Data) { - data.open(); - data.field("ordinal", &self.ordinal); - data.field("heading", &self.heading); - data.list("steps", &self.steps); - data.close(); - } -} - /// A step within a checklist section. pub struct Step { #[allow(dead_code)] @@ -55,30 +35,8 @@ pub struct Step { pub children: Vec, } -impl Render for Step { - fn render(&self, data: &mut Data) { - data.open(); - data.field("ordinal", &self.ordinal); - data.field("title", &self.title); - data.list("body", &self.body); - data.field("role", &self.role); - data.list("responses", &self.responses); - data.list("children", &self.children); - data.close(); - } -} - /// A response option with an optional condition. pub struct Response { pub value: String, pub condition: Option, } - -impl Render for Response { - fn render(&self, data: &mut Data) { - data.open(); - data.field("value", &self.value); - data.field("condition", &self.condition); - data.close(); - } -} diff --git a/src/domain/checklist/typst.rs b/src/domain/checklist/typst.rs new file mode 100644 index 00000000..7ace231d --- /dev/null +++ b/src/domain/checklist/typst.rs @@ -0,0 +1,72 @@ +//! Typst serialization for checklist domain types. + +use crate::domain::serialize::{Markup, Render}; + +use super::types::{Document, Response, Section, Step}; + +impl Render for Document { + fn render(&self, out: &mut Markup) { + for section in &self.sections { + section.render(out); + } + } +} + +impl Render for Section { + fn render(&self, out: &mut Markup) { + out.call("render-section"); + out.param_opt("ordinal", &self.ordinal); + out.param_opt("heading", &self.heading); + if !self + .steps + .is_empty() + { + out.content_open("children"); + for step in &self.steps { + step.render(out); + } + out.content_close(); + } + out.close(); + } +} + +impl Render for Step { + fn render(&self, out: &mut Markup) { + out.call("render-step"); + out.param_opt("ordinal", &self.ordinal); + out.param_opt("title", &self.title); + out.param_list("body", &self.body); + out.param_opt("role", &self.role); + if !self + .responses + .is_empty() + { + out.content_open("responses"); + for r in &self.responses { + r.render(out); + } + out.content_close(); + } + if !self + .children + .is_empty() + { + out.content_open("children"); + for child in &self.children { + child.render(out); + } + out.content_close(); + } + out.close(); + } +} + +impl Render for Response { + fn render(&self, out: &mut Markup) { + out.call("render-response"); + out.param("value", &self.value); + out.param_opt("condition", &self.condition); + out.close(); + } +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 434c77f9..6866bbe5 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -13,7 +13,7 @@ mod adapter; pub mod checklist; pub mod engine; pub mod procedure; +pub(crate) mod serialize; pub mod source; -pub mod typst; pub use adapter::Adapter; diff --git a/src/domain/procedure/adapter.rs b/src/domain/procedure/adapter.rs index c0fd4343..ff7c2054 100644 --- a/src/domain/procedure/adapter.rs +++ b/src/domain/procedure/adapter.rs @@ -5,8 +5,8 @@ //! any SectionChunks within) become the body. Remaining top-level procedures //! are appended as Procedure nodes. -use crate::language; use crate::domain::Adapter; +use crate::language; use super::types::{Document, Node, Response}; @@ -192,8 +192,8 @@ fn node_from_step(scope: &language::Scope) -> Node { mod check { use std::path::Path; - use crate::parsing; use crate::domain::Adapter; + use crate::parsing; use super::super::types::Node; use super::ProcedureAdapter; diff --git a/src/domain/procedure/mod.rs b/src/domain/procedure/mod.rs index 35135afb..93a61742 100644 --- a/src/domain/procedure/mod.rs +++ b/src/domain/procedure/mod.rs @@ -1,2 +1,3 @@ pub mod adapter; pub mod types; +mod typst; diff --git a/src/domain/procedure/types.rs b/src/domain/procedure/types.rs index 1afcc7a6..3345f2bd 100644 --- a/src/domain/procedure/types.rs +++ b/src/domain/procedure/types.rs @@ -4,8 +4,6 @@ //! source Technique document. Sections, procedures, steps, role groups — //! whatever the author wrote, the domain model preserves. -use crate::domain::typst::{Data, Render}; - /// A procedure document: title and description from the first procedure, /// then a tree of nodes representing the body. pub struct Document { @@ -24,16 +22,6 @@ impl Document { } } -impl Render for Document { - fn render(&self, data: &mut Data) { - data.open(); - data.field("title", &self.title); - data.list("description", &self.description); - data.list("body", &self.body); - data.close(); - } -} - /// A node in the procedure tree. pub enum Node { Section { @@ -68,69 +56,8 @@ pub enum Node { }, } -impl Render for Node { - fn render(&self, data: &mut Data) { - match self { - Node::Section { ordinal, heading, children } => { - data.open(); - data.tag("section"); - data.field("ordinal", ordinal); - data.field("heading", heading); - data.list("children", children); - data.close(); - } - Node::Procedure { name, title, description, children } => { - data.open(); - data.tag("procedure"); - data.field("name", name); - data.field("title", title); - data.list("description", description); - data.list("children", children); - data.close(); - } - Node::Sequential { ordinal, title, body, invocations, responses, children } => { - data.open(); - data.tag("sequential"); - data.field("ordinal", ordinal); - data.field("title", title); - data.list("body", body); - data.list("invocations", invocations); - data.list("responses", responses); - data.list("children", children); - data.close(); - } - Node::Parallel { title, body, invocations, responses, children } => { - data.open(); - data.tag("parallel"); - data.field("title", title); - data.list("body", body); - data.list("invocations", invocations); - data.list("responses", responses); - data.list("children", children); - data.close(); - } - Node::Attribute { name, children } => { - data.open(); - data.tag("attribute"); - data.field("name", name); - data.list("children", children); - data.close(); - } - } - } -} - /// A response option with an optional condition. pub struct Response { pub value: String, pub condition: Option, } - -impl Render for Response { - fn render(&self, data: &mut Data) { - data.open(); - data.field("value", &self.value); - data.field("condition", &self.condition); - data.close(); - } -} diff --git a/src/domain/procedure/typst.rs b/src/domain/procedure/typst.rs new file mode 100644 index 00000000..6f18d8b7 --- /dev/null +++ b/src/domain/procedure/typst.rs @@ -0,0 +1,197 @@ +//! Typst serialization for procedure domain types. + +use crate::domain::serialize::{escape_string, Markup, Render}; + +use super::types::{Document, Node, Response}; + +impl Render for Document { + fn render(&self, out: &mut Markup) { + out.call("render-document"); + out.param_opt("title", &self.title); + out.param_list("description", &self.description); + out.content_open("children"); + + let has_sections = self + .body + .iter() + .any(|n| { + if let Node::Section { .. } = n { + true + } else { + false + } + }); + + if has_sections { + render_outline(out, &self.body); + } + + for (i, node) in self + .body + .iter() + .enumerate() + { + if i > 0 { + if let Node::Section { .. } = node { + out.call("section-divider"); + out.close(); + } + } + node.render(out); + } + + out.content_close(); + out.close(); + } +} + +fn render_outline(out: &mut Markup, body: &[Node]) { + let sections: Vec<_> = body + .iter() + .filter_map(|n| { + if let Node::Section { + ordinal, heading, .. + } = n + { + Some((ordinal.as_str(), heading.as_deref())) + } else { + None + } + }) + .collect(); + + out.call("render-outline"); + out.raw("sections: ("); + for (ordinal, heading) in §ions { + out.raw(&format!( + "(ordinal: \"{}\", heading: {}), ", + escape_string(ordinal), + match heading { + Some(h) => format!("\"{}\"", escape_string(h)), + None => "none".to_string(), + } + )); + } + out.raw("), "); + out.close(); +} + +impl Render for Node { + fn render(&self, out: &mut Markup) { + match self { + Node::Section { + ordinal, + heading, + children, + } => { + out.call("render-section"); + out.param("ordinal", ordinal); + out.param_opt("heading", heading); + if !children.is_empty() { + out.content_open("children"); + for child in children { + child.render(out); + } + out.content_close(); + } + out.close(); + } + Node::Procedure { + name, + title, + description, + children, + } => { + out.call("render-procedure"); + out.param("name", name); + out.param_opt("title", title); + out.param_list("description", description); + if !children.is_empty() { + out.content_open("children"); + for child in children { + child.render(out); + } + out.content_close(); + } + out.close(); + } + Node::Sequential { + ordinal, + title, + body, + invocations, + responses, + children, + } => { + out.call("render-step"); + out.param("ordinal", ordinal); + out.param_opt("title", title); + out.param_list("body", body); + out.param_list("invocations", invocations); + if !responses.is_empty() { + out.content_open("responses"); + for r in responses { + r.render(out); + } + out.content_close(); + } + if !children.is_empty() { + out.content_open("children"); + for child in children { + child.render(out); + } + out.content_close(); + } + out.close(); + } + Node::Parallel { + title, + body, + invocations, + responses, + children, + } => { + out.call("render-step"); + out.param_opt("title", title); + out.param_list("body", body); + out.param_list("invocations", invocations); + if !responses.is_empty() { + out.content_open("responses"); + for r in responses { + r.render(out); + } + out.content_close(); + } + if !children.is_empty() { + out.content_open("children"); + for child in children { + child.render(out); + } + out.content_close(); + } + out.close(); + } + Node::Attribute { name, children } => { + out.call("render-attribute"); + out.param("name", name); + if !children.is_empty() { + out.content_open("children"); + for child in children { + child.render(out); + } + out.content_close(); + } + out.close(); + } + } + } +} + +impl Render for Response { + fn render(&self, out: &mut Markup) { + out.call("render-response"); + out.param("value", &self.value); + out.param_opt("condition", &self.condition); + out.close(); + } +} diff --git a/src/domain/serialize.rs b/src/domain/serialize.rs new file mode 100644 index 00000000..a8404c5b --- /dev/null +++ b/src/domain/serialize.rs @@ -0,0 +1,174 @@ +//! Typst serialization builders. +//! +//! The `Markup` builder emits Typst function calls — `#render-step(ordinal: +//! "1", ...)` — so that the tree-walk happens in Rust and the `.typ` file +//! shrinks to a set of thin, overridable formatting functions. The `Render` +//! trait is implemented by domain types that serialize themselves via +//! `Markup`. + +const INDENT: &str = " "; + +/// Escape `\` and `"` for Typst string literals. +pub fn escape_string(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('"', "\\\"") +} + +/// Stateful builder for accumulating Typst function-call markup. +pub struct Markup { + out: String, + depth: usize, +} + +impl Markup { + pub fn new() -> Self { + Markup { + out: String::new(), + depth: 0, + } + } + + pub fn finish(self) -> String { + self.out + } + + fn pad(&mut self) { + for _ in 0..self.depth { + self.out + .push_str(INDENT); + } + } + + /// Begin a function call: `#name(`. + pub fn call(&mut self, name: &str) { + self.pad(); + self.out + .push_str(&format!("#{}(", name)); + } + + /// Emit a string parameter: `key: "escaped-value", `. + pub fn param(&mut self, key: &str, value: &str) { + self.out + .push_str(&format!("{}: \"{}\", ", key, escape_string(value))); + } + + /// Emit an optional string parameter: `key: "value", ` or `key: none, `. + pub fn param_opt(&mut self, key: &str, value: &Option) { + match value { + Some(v) => self + .out + .push_str(&format!("{}: \"{}\", ", key, escape_string(v))), + None => self + .out + .push_str(&format!("{}: none, ", key)), + } + } + + /// Emit a tuple of strings: `key: ("a", "b"), `. + pub fn param_list(&mut self, key: &str, items: &[String]) { + self.out + .push_str(&format!("{}: (", key)); + for item in items { + self.out + .push_str(&format!("\"{}\", ", escape_string(item))); + } + self.out + .push_str("), "); + } + + /// Open a content block parameter: `key: [\n` + indent. + pub fn content_open(&mut self, key: &str) { + self.out + .push_str(&format!("{}: [\n", key)); + self.depth += 1; + } + + /// Close a content block: dedent + `], `. + pub fn content_close(&mut self) { + self.depth -= 1; + self.pad(); + self.out + .push_str("], "); + } + + /// Emit raw Typst content (for inline data like outline tuples). + pub fn raw(&mut self, s: &str) { + self.out + .push_str(s); + } + + /// Close a function call: `)\n`. + pub fn close(&mut self) { + self.out + .push_str(")\n"); + } +} + +/// Render a domain type as Typst function-call markup. +pub trait Render { + fn render(&self, out: &mut Markup); +} + +#[cfg(test)] +mod check { + use super::*; + + #[test] + fn escape_string_backslash_and_quote() { + assert_eq!(escape_string(r#"a "b" c\d"#), r#"a \"b\" c\\d"#); + } + + #[test] + fn markup_simple_call() { + let mut m = Markup::new(); + m.call("render-step"); + m.param("ordinal", "1"); + m.close(); + assert_eq!(m.finish(), "#render-step(ordinal: \"1\", )\n"); + } + + #[test] + fn markup_param_opt_some_and_none() { + let mut m = Markup::new(); + m.call("f"); + m.param_opt("a", &Some("yes".into())); + m.param_opt("b", &None); + m.close(); + assert_eq!(m.finish(), "#f(a: \"yes\", b: none, )\n"); + } + + #[test] + fn markup_param_list() { + let mut m = Markup::new(); + m.call("f"); + m.param_list("items", &["x".into(), "y".into()]); + m.close(); + assert_eq!(m.finish(), "#f(items: (\"x\", \"y\", ), )\n"); + } + + #[test] + fn markup_content_block() { + let mut m = Markup::new(); + m.call("render-section"); + m.param("heading", "Prep"); + m.content_open("children"); + m.call("render-step"); + m.param("ordinal", "1"); + m.close(); + m.content_close(); + m.close(); + let result = m.finish(); + assert!(result.contains("children: [\n")); + assert!(result.contains(" #render-step(")); + assert!(result.contains("], )\n")); + } + + #[test] + fn markup_escapes_strings() { + let mut m = Markup::new(); + m.call("f"); + m.param("t", r#"say "hello""#); + m.close(); + assert_eq!(m.finish(), "#f(t: \"say \\\"hello\\\"\", )\n"); + } +} diff --git a/src/domain/source/mod.rs b/src/domain/source/mod.rs index 35135afb..93a61742 100644 --- a/src/domain/source/mod.rs +++ b/src/domain/source/mod.rs @@ -1,2 +1,3 @@ pub mod adapter; pub mod types; +mod typst; diff --git a/src/domain/source/types.rs b/src/domain/source/types.rs index 077031a4..04528c2e 100644 --- a/src/domain/source/types.rs +++ b/src/domain/source/types.rs @@ -4,8 +4,6 @@ //! produced by the code formatter. Each fragment carries a syntax tag //! (e.g. "Declaration", "Keyword") and a content string. -use crate::domain::typst::{Data, Render}; - pub struct Document { pub fragments: Vec, } @@ -14,20 +12,3 @@ pub struct Fragment { pub syntax: String, pub content: String, } - -impl Render for Document { - fn render(&self, data: &mut Data) { - data.open(); - data.list("fragments", &self.fragments); - data.close(); - } -} - -impl Render for Fragment { - fn render(&self, data: &mut Data) { - data.open(); - data.field("syntax", &self.syntax); - data.field("content", &self.content); - data.close(); - } -} diff --git a/src/domain/source/typst.rs b/src/domain/source/typst.rs new file mode 100644 index 00000000..a9468503 --- /dev/null +++ b/src/domain/source/typst.rs @@ -0,0 +1,59 @@ +//! Typst serialization for source domain types. + +use crate::domain::serialize::{escape_string, Markup, Render}; + +use super::types::{Document, Fragment}; + +impl Render for Document { + fn render(&self, out: &mut Markup) { + for fragment in &self.fragments { + fragment.render(out); + } + } +} + +impl Render for Fragment { + fn render(&self, out: &mut Markup) { + let func = match self + .syntax + .as_str() + { + "Neutral" => "render-neutral", + "Indent" => "render-indent", + "Newline" => "render-newline", + "Header" => "render-header", + "Declaration" => "render-declaration", + "Description" => "render-description", + "Forma" => "render-forma", + "StepItem" => "render-stepitem", + "CodeBlock" => "render-codeblock", + "Variable" => "render-variable", + "Section" => "render-section", + "String" => "render-string", + "Numeric" => "render-numeric", + "Response" => "render-response", + "Invocation" => "render-invocation", + "Title" => "render-title", + "Keyword" => "render-keyword", + "Function" => "render-function", + "Multiline" => "render-multiline", + "Label" => "render-label", + "Operator" => "render-operator", + "Quote" => "render-quote", + "Language" => "render-language", + "Attribute" => "render-attribute", + "Structure" => "render-structure", + _ => "render-neutral", + }; + + if self.syntax == "Newline" { + out.raw(&format!("#{}()\n", func)); + } else { + out.raw(&format!( + "#{}(\"{}\")", + func, + escape_string(&self.content) + )); + } + } +} diff --git a/src/domain/typst.rs b/src/domain/typst.rs deleted file mode 100644 index eeba65f6..00000000 --- a/src/domain/typst.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! Typst data literal builder. -//! -//! Domain types serialize themselves as Typst dictionary literals using the -//! `Data` builder. The `Render` trait is implemented by types that emit -//! themselves as complete dictionary entries; `Field` is for individual -//! key-value pairs within a dictionary. - -const INDENT: &str = " "; - -/// Stateful builder for accumulating Typst data literals. -pub struct Data { - out: String, - depth: usize, -} - -impl Data { - pub fn new() -> Self { - Data { - out: String::new(), - depth: 0, - } - } - - /// Consume the builder and return the data as a `#let technique = ...` - /// binding. The trailing comma from `close()` is stripped so the - /// top-level assignment is valid Typst. - pub fn finish(self) -> String { - let out = self - .out - .trim_end() - .trim_end_matches(','); - format!("#let technique = {}\n", out) - } - - fn pad(&mut self) { - for _ in 0..self.depth { - self.out - .push_str(INDENT); - } - } - - /// Open a dictionary: `(`, newline, and increase depth. - pub fn open(&mut self) { - self.pad(); - self.out - .push_str("(\n"); - self.depth += 1; - } - - /// Close a dictionary: decrease depth, closing `),` and newline. - pub fn close(&mut self) { - self.depth -= 1; - self.pad(); - self.out - .push_str("),\n"); - } - - /// Emit a `type: "name",` discriminator field and a newline. - pub fn tag(&mut self, name: &str) { - self.pad(); - self.out - .push_str(&format!("type: \"{}\",\n", name)); - } - - /// Emit a field whose value implements `Field`. - pub fn field(&mut self, key: &str, value: &(impl Field + ?Sized)) { - value.emit(self, key); - } - - /// Emit a list field, calling `Render::render` on each item. - pub fn list(&mut self, key: &str, items: &[T]) { - self.pad(); - self.out - .push_str(&format!("{}: (\n", key)); - self.depth += 1; - for item in items { - item.render(self); - } - self.depth -= 1; - self.pad(); - self.out - .push_str("),\n"); - } -} - -/// Emit a domain type as a Typst data literal. -pub trait Render { - fn render(&self, data: &mut Data); -} - -impl Render for String { - fn render(&self, data: &mut Data) { - data.pad(); - data.out - .push_str(&format!("\"{}\",\n", escape_string(self))); - } -} - -/// Any type that knows how to emit itself as a `key: value,` pair in a Typst -/// dictionary should implement Field, which can then be used by the Data -/// builder's field() method. -pub trait Field { - fn emit(&self, data: &mut Data, key: &str); -} - -impl Field for str { - fn emit(&self, data: &mut Data, key: &str) { - data.pad(); - data.out - .push_str(&format!("{}: \"{}\",\n", key, escape_string(self))); - } -} - -impl Field for String { - fn emit(&self, data: &mut Data, key: &str) { - self.as_str() - .emit(data, key); - } -} - -impl Field for Option { - fn emit(&self, data: &mut Data, key: &str) { - data.pad(); - match self { - Some(v) => data - .out - .push_str(&format!("{}: \"{}\",\n", key, escape_string(v))), - None => data - .out - .push_str(&format!("{}: none,\n", key)), - } - } -} - -/// Escape `\` and `"` for Typst string literals. -pub fn escape_string(s: &str) -> String { - s.replace('\\', "\\\\") - .replace('"', "\\\"") -} - -#[cfg(test)] -mod check { - use super::*; - - #[test] - fn escape_string_backslash_and_quote() { - assert_eq!(escape_string(r#"a "b" c\d"#), r#"a \"b\" c\\d"#); - } - - #[test] - fn field_some() { - let mut d = Data::new(); - d.depth = 1; - d.field("title", &Some("Hello".into())); - assert_eq!(d.out, " title: \"Hello\",\n"); - } - - #[test] - fn field_none() { - let mut d = Data::new(); - d.depth = 1; - d.field("title", &None::); - assert_eq!(d.out, " title: none,\n"); - } - - #[test] - fn open_close_tracks_depth() { - let mut d = Data::new(); - d.open(); - assert_eq!(d.depth, 1); - d.close(); - assert_eq!(d.depth, 0); - assert_eq!(d.out, "(\n),\n"); - } - - #[test] - fn nested_dict() { - let mut d = Data::new(); - d.open(); - d.field("name", "outer"); - d.open(); - d.field("name", "inner"); - d.close(); - d.close(); - assert!(d - .out - .contains(" name: \"outer\",\n")); - assert!(d - .out - .contains(" name: \"inner\",\n")); - } -} diff --git a/src/lib.rs b/src/lib.rs index 430aaa04..24351f25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,5 @@ pub mod formatting; pub mod highlighting; pub mod language; pub mod parsing; -pub mod regex; +pub(crate) mod regex; pub mod templating; diff --git a/src/main.rs b/src/main.rs index 24702664..adeb33d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,7 +109,7 @@ fn main() { .long_about("Render the Technique document into a formatted \ PDF using a template. This allows you to transform the code of \ the procedure into the intended layout suitable to the \ - domain you're app.") + domain of your application.") .arg( Arg::new("output") .short('o') @@ -136,6 +136,13 @@ fn main() { .action(ArgAction::Set) .help("Path to a Typst template file for rendering."), ) + .arg( + Arg::new("keep") + .short('k') + .long("keep") + .action(ArgAction::SetTrue) + .help("Keep the generated intermediate files in place after rendering. This allows you to do iterative development of the template and styling with the Typst compiler without having to regenerate the input document every time. The intermediate pieces are written as hidden files in the same directory as the source document."), + ) .arg( Arg::new("filename") .required(true) @@ -352,12 +359,7 @@ fn main() { } }; - let data = template.data(&technique); - - // If --template is given, use the user-supplied file (expected to - // be a .typ file containing Typst template code) ; otherwise - // inline the built-in template. - let preamble: String = match submatches.get_one::("template") { + let custom = match submatches.get_one::("template") { Some(path) => { if !Path::new(path).exists() { eprintln!( @@ -367,21 +369,30 @@ fn main() { ); std::process::exit(1); } - format!("#import \"{}\": render", path) + Some(path.as_str()) } - None => template - .typst() - .to_string(), + None => None, }; + let markup = template.markup(&technique); + let document = templating::assemble(template.domain(), &markup, custom); + + let keep = *submatches + .get_one::("keep") + .unwrap(); + match output.as_str() { "typst" => { - println!("{}", preamble); - print!("{}", data); - println!("\n#render(technique)"); + print!("{}", document); } "pdf" => { - output::via_typst(filename, &preamble, &data); + output::via_typst( + filename, + template.typst(), + template.domain(), + &document, + keep, + ); } _ => panic!("Unrecognized --output value"), } diff --git a/src/output/mod.rs b/src/output/mod.rs index 55de3c07..f7f7644e 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,16 +1,14 @@ //! Output generation for the Technique CLI application use owo_colors::OwoColorize; -use std::io::Write; use std::path::Path; -use std::process::{Command, Stdio}; +use std::process::Command; use tracing::{debug, info}; -/// Compile a Typst document piped via stdin to a PDF file. -/// -/// The template content, data literal, and render call are written -/// sequentially to the process's stdin. -pub fn via_typst(filename: &Path, template: &str, data: &str) { +/// Write the domain template and assembled document into a (hidden) file +/// beside the input source file, then compile to PDF using the external Typst +/// binary. +pub fn via_typst(filename: &Path, template: &str, domain: &str, document: &str, keep: bool) { info!("Printing file: {}", filename.display()); if filename.to_str() == Some("-") { @@ -27,47 +25,46 @@ pub fn via_typst(filename: &Path, template: &str, data: &str) { ); } - let target = filename.with_extension("pdf"); + let source_dir = filename + .parent() + .unwrap_or(Path::new(".")); + let stem = filename + .file_stem() + .unwrap() + .to_str() + .unwrap(); + + // Write the domain template beside where the source file is + let domain_typ = source_dir.join(format!(".{}.typ", domain)); + std::fs::write(&domain_typ, template).expect("Failed to write domain template"); + + // Write assembled document beside source as well + let target_typ = source_dir.join(format!(".{}.typ", stem)); + std::fs::write(&target_typ, document).expect("Failed to write generated document"); - let mut child = Command::new("typst") + let target_pdf = filename.with_extension("pdf"); + + let status = Command::new("typst") .arg("compile") - .arg("-") - .arg(&target) - .stdin(Stdio::piped()) - .spawn() + .arg("--root") + .arg(".") + .arg(&target_typ) + .arg(&target_pdf) + .status() .unwrap_or_else(|e| { eprintln!("{}: failed to start typst: {}", "error".bright_red(), e); std::process::exit(1); }); - let mut stdin = child - .stdin - .take() - .unwrap(); - - stdin - .write_all(template.as_bytes()) - .expect("Failed attempting to write"); - stdin - .write_all(b"\n") - .expect("Failed attempting to write"); - stdin - .write_all(data.as_bytes()) - .expect("Failed attempting to write"); - stdin - .write_all(b"\n#render(technique)\n") - .expect("Failed attempting to write"); - - drop(stdin); - - let status = child - .wait() - .expect("Failed to wait for Typst process"); - if !status.success() { eprintln!("{}: typst compile failed", "error".bright_red()); std::process::exit(1); } - debug!("Wrote {}", target.display()); + if !keep { + let _ = std::fs::remove_file(&domain_typ); + let _ = std::fs::remove_file(&target_typ); + } + + debug!("Wrote {}", target_pdf.display()); } diff --git a/src/problem/messages.rs b/src/problem/messages.rs index 68b9bd7a..5a397550 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -79,7 +79,7 @@ literal resumes. "! «license»; © «copyright»\n", )); formatted_example - .push_str(&renderer.style(crate::formatting::Syntax::Header, "& «template»")); + .push_str(&renderer.style(crate::formatting::Syntax::Header, "& «domain»")); ( "Invalid header".to_string(), @@ -102,7 +102,7 @@ typically list the year and then the name of the person or entity holding the copyright. The third line optionally specifies the domain or kind of Technique this is, -to be used when rendering the Technique. Common templates include +to be used when rendering the Technique. Common domains include {}, {}, and {}. "#, formatted_example, diff --git a/src/templating/checklist.rs b/src/templating/checklist.rs index 9f9dc782..d7398aaa 100644 --- a/src/templating/checklist.rs +++ b/src/templating/checklist.rs @@ -6,7 +6,7 @@ //! child steps) rather than forming structural containers. use crate::domain::checklist::adapter::ChecklistAdapter; -use crate::domain::typst::{Data, Render}; +use crate::domain::serialize::{Markup, Render}; use crate::domain::Adapter; use crate::language; use crate::templating::template::Template; @@ -16,14 +16,18 @@ pub static TEMPLATE: &str = include_str!("checklist.typ"); pub struct Checklist; impl Template for Checklist { - fn data(&self, document: &language::Document) -> String { + fn markup(&self, document: &language::Document) -> String { let model = ChecklistAdapter.extract(document); - let mut data = Data::new(); - model.render(&mut data); - data.finish() + let mut out = Markup::new(); + model.render(&mut out); + out.finish() } fn typst(&self) -> &str { TEMPLATE } + + fn domain(&self) -> &str { + "checklist" + } } diff --git a/src/templating/checklist.typ b/src/templating/checklist.typ index e41af98b..62ddf6d0 100644 --- a/src/templating/checklist.typ +++ b/src/templating/checklist.typ @@ -1,55 +1,54 @@ -// Built-in checklist template for Technique. +// Checklist domain template for Technique. // -// Expects a `technique` dictionary with shape: -// (sections: ((ordinal, heading, steps: ((ordinal, title, body, role, -// responses, children), ...)), ...)) +// Thin formatting functions called from Rust-generated markup. +// Each function is independently overridable via `--template`. + +// -- Formatting functions ---------------------------------------------------- #let check = box(stroke: 0.5pt, width: 0.8em, height: 0.8em) #let small-check = box(stroke: 0.5pt, width: 0.6em, height: 0.6em) -#let render-responses(responses) = { - for (i, r) in responses.enumerate() { - if i > 0 [ | ] - small-check - if r.condition != none [ _#r.value #r.condition _] - else [ _#r.value _] +#let render-section(ordinal: none, heading: none, children: none) = { + if ordinal != none and heading != none { + std.heading(level: 1, numbering: none, [#ordinal. #heading]) + } else if ordinal != none { + std.heading(level: 1, numbering: none, [#ordinal.]) + } else if heading != none { + std.heading(level: 1, numbering: none, heading) } - if responses.len() > 0 { parbreak() } + if children != none { children } +} + +#let render-response(value: none, condition: none) = { + small-check + if condition != none [ _#value #condition _] + else [ _#value _] } -#let render-step(step) = { - if step.role != none { - text(weight: "bold")[#step.role] +#let render-step(ordinal: none, title: none, body: (), role: none, responses: none, children: none) = { + if role != none { + text(weight: "bold")[#role] parbreak() } check - if step.ordinal != none [ *#step.ordinal.* ] - if step.title != none [ #step.title] + if ordinal != none [ *#ordinal.* ] + if title != none [ #title] parbreak() - for para in step.body { + for para in body { [#para] parbreak() } - render-responses(step.responses) - for child in step.children { - render-step(child) + if responses != none { + responses + parbreak() } + if children != none { children } } -#let render(technique) = [ - #set page(margin: 1.5cm) - #set text(size: 10pt) +// -- Default template -------------------------------------------------------- - #for section in technique.sections [ - #if section.ordinal != none and section.heading != none [ - == #section.ordinal. #section.heading - ] else if section.ordinal != none [ - == #section.ordinal. - ] else if section.heading != none [ - == #section.heading - ] - #for step in section.steps { - render-step(step) - } - ] -] +#let template(body) = { + set page(margin: 1.5cm) + set text(size: 10pt) + body +} diff --git a/src/templating/mod.rs b/src/templating/mod.rs index c76d8ff7..3c7cc157 100644 --- a/src/templating/mod.rs +++ b/src/templating/mod.rs @@ -1,7 +1,8 @@ //! Render Technique documents into formatted output. //! -//! The **Template** trait provides `data()` for Typst data literals. Each -//! domain template composes an adapter from `crate::domain` internally. +//! The **Template** trait provides `markup()` for Typst function-call +//! output. Each domain template composes an adapter from `crate::domain` +//! internally. mod checklist; mod procedure; @@ -13,9 +14,22 @@ pub use procedure::Procedure; pub use source::Source; pub use template::Template; -use crate::language; - -/// Serialize a Technique document as a Typst data literal. -pub fn data(template: &impl Template, document: &language::Document) -> String { - template.data(document) +/// Assemble a complete, compilable Typst document from domain template +/// imports, optional user template, and rendered markup. +/// +/// The custom template import uses a root-relative path (`/`), which assumes +/// the Typst compiler is invoked with `--root .` as done by `output::via_typst`. +/// +/// This second import line is the critical aspect of the ability of a user to +/// customize the output template. Because the import is * any functions that +/// the user redefines in their template will override the names from the +/// default. +pub fn assemble(domain: &str, markup: &str, custom: Option<&str>) -> String { + let mut doc = format!("#import \".{}.typ\": *\n", domain); + if let Some(path) = custom { + doc.push_str(&format!("#import \"/{}\": *\n", path)); + } + doc.push_str("\n#show: template\n\n"); + doc.push_str(markup); + doc } diff --git a/src/templating/procedure.rs b/src/templating/procedure.rs index 070464db..030b3c3f 100644 --- a/src/templating/procedure.rs +++ b/src/templating/procedure.rs @@ -6,7 +6,7 @@ //! distinct items rather than step annotations, and nested children. use crate::domain::procedure::adapter::ProcedureAdapter; -use crate::domain::typst::{Data, Render}; +use crate::domain::serialize::{Markup, Render}; use crate::domain::Adapter; use crate::language; use crate::templating::template::Template; @@ -16,14 +16,18 @@ pub static TEMPLATE: &str = include_str!("procedure.typ"); pub struct Procedure; impl Template for Procedure { - fn data(&self, document: &language::Document) -> String { + fn markup(&self, document: &language::Document) -> String { let model = ProcedureAdapter.extract(document); - let mut data = Data::new(); - model.render(&mut data); - data.finish() + let mut out = Markup::new(); + model.render(&mut out); + out.finish() } fn typst(&self) -> &str { TEMPLATE } + + fn domain(&self) -> &str { + "procedure" + } } diff --git a/src/templating/procedure.typ b/src/templating/procedure.typ index acafc20b..1b490a55 100644 --- a/src/templating/procedure.typ +++ b/src/templating/procedure.typ @@ -1,149 +1,119 @@ -// Built-in procedure template for Technique. +// Procedure domain template for Technique. // -// Expects a `technique` dictionary with shape: -// (title, description, body: ((type: "section"|"procedure"|"sequential" -// |"parallel"|"attribute", ...children), ...)) - -#let render-responses(responses) = { - for r in responses { - if r.condition != none [- _#r.value #r.condition _] - else [- _#r.value _] - } - if responses.len() > 0 { parbreak() } -} +// Thin formatting functions called from Rust-generated markup. +// Each function is independently overridable via `--template`. -#let render-invocations(invocations) = { - if invocations.len() > 0 { - text(size: 7pt)[`#invocations.join(", ")`] - linebreak() - } +// -- Formatting functions ---------------------------------------------------- + +#let render-document(title: none, description: (), children: none) = [ + #block(width: 100%, stroke: 0.1pt, inset: 10pt)[ + #if title != none [ + #text(size: 15pt)[*#title*] + + ] + #if description.len() > 0 or children != none [ + _Overview_ + + #for para in description [ + #para + ] + ] + #if children != none { + children + } + ] +] + +#let render-outline(sections: ()) = { + grid(columns: (auto, 1fr), column-gutter: 6pt, row-gutter: 0.3em, + ..sections.map(s => { + let heading = s.at("heading", default: none) + ([#s.ordinal.], [#if heading != none { heading }]) + }).flatten() + ) + heading(level: 3, numbering: none, outlined: false, [Procedure]) } -#let ordinal-start(children) = { - let first = children.at(0, default: none) - if first != none and first.type == "sequential" { - let o = first.at("ordinal", default: "a") - let c = o.codepoints().at(0, default: "a") - if "abcdefghijklmnopqrstuvwxyz".contains(c) { - "abcdefghijklmnopqrstuvwxyz".position(c) + 1 - } else { 1 } - } else { 1 } +#let render-section(ordinal: none, heading: none, children: none) = { + std.heading(level: 1, numbering: none, + [#ordinal. #h(8pt) #if heading != none { heading }]) + if children != none { children } } -#let render-child(node) = { - if node.type == "sequential" or node.type == "parallel" { - if node.at("title", default: none) != none [+ #node.title] - } +#let section-divider() = { + line(length: 100%, stroke: (thickness: 0.5pt, paint: rgb("#003366"), dash: ("dot", 2pt, 4pt, 2pt))) } -#let render-role(node) = { - [- *#node.name*] - if node.children.len() > 0 { - let start = ordinal-start(node.children) - pad(left: 20pt)[ - #set par(leading: 0.5em) - #set enum(numbering: "a.", start: start, spacing: 0.8em) - #for child in node.children { - if child.type == "attribute" { - render-role(child) - } else { - render-child(child) - } - } - ] +#let render-procedure(name: none, title: none, description: (), children: none) = { + if title != none { + std.heading(level: 2, numbering: none, outlined: false, title) } -} + text(size: 7pt, fill: rgb("#999999"), raw(name)) + linebreak() -#let render-node(node) = { - if node.type == "section" { - if node.at("heading", default: none) != none { - text(size: 14pt)[*#node.ordinal.* #h(8pt) *#node.heading*] - } else { - text(size: 14pt)[*#node.ordinal.*] - } + for para in description { + [#para] parbreak() - for child in node.children { render-node(child) } + } + if children != none { + pad(left: 8pt, children) + } +} - } else if node.type == "procedure" { - text(size: 7pt)[`#node.name`] +#let render-step(ordinal: none, title: none, body: (), invocations: (), responses: none, children: none) = { + if invocations.len() > 0 { + text(size: 7pt, raw(invocations.join(", "))) linebreak() - if node.at("title", default: none) != none { - text(size: 11pt)[*#node.title*] - parbreak() - } - for para in node.description { - [#para] - parbreak() - } - if node.children.len() > 0 { - pad(left: 8pt)[#for child in node.children { render-node(child) }] - } - - } else if node.type == "sequential" or node.type == "parallel" { - render-invocations(node.invocations) - let ordinal = if node.type == "sequential" { node.ordinal } else { none } - if ordinal != none and node.at("title", default: none) != none [ - *#ordinal.* #h(4pt) *#node.title* - ] else if ordinal != none [ - *#ordinal.* - ] else if node.at("title", default: none) != none [ - *#node.title* - ] + } + if ordinal != none and title != none [ + *#ordinal.* #h(4pt) *#title* + ] else if ordinal != none [ + *#ordinal.* + ] else if title != none [ + *#title* + ] + parbreak() + for para in body { + [#para] parbreak() - for para in node.body { - [#para] - parbreak() - } - render-responses(node.responses) - if node.children.len() > 0 { - pad(left: 16pt)[#for child in node.children { render-node(child) }] - } - - } else if node.type == "attribute" { - render-role(node) + } + if responses != none { + responses + parbreak() + } + if children != none { + pad(left: 16pt, children) } } -#let has-sections(body) = { - body.any(n => n.type == "section") +#let render-response(value: none, condition: none) = { + if condition != none [- _#value #condition _] + else [- _#value _] } -#let render-outline(body) = { - grid(columns: (auto, 1fr), column-gutter: 6pt, row-gutter: 0.3em, - ..body.filter(n => n.type == "section").map(n => { - let heading = n.at("heading", default: none) - ([#n.ordinal.], [#if heading != none { heading }]) - }).flatten() - ) +#let render-attribute(name: none, children: none) = { + [- *#name*] + if children != none { + pad(left: 20pt, children) + } } -#let render(technique) = [ - #set page(margin: 1.5cm) - #set par(justify: false) - #show text: set text(size: 9pt, font: "TeX Gyre Heros") +// -- Default template -------------------------------------------------------- - #block(width: 100%, stroke: 0.1pt, inset: 10pt)[ - #if technique.at("title", default: none) != none [ - #text(size: 15pt)[*#technique.title*] +#let template(body) = { + set page(margin: 1.5cm) + set par(justify: false) + set text(size: 9pt, font: "TeX Gyre Heros") - ] - #if technique.description.len() > 0 or has-sections(technique.body) [ - _Overview_ - - #for para in technique.description [ - #para - ] - #if has-sections(technique.body) { - render-outline(technique.body) - } - ] - #block(width: 100%, fill: rgb("#006699"), inset: 5pt)[#text(fill: white)[*Procedure*]] + show heading.where(level: 1): set text(size: 14pt) + show heading.where(level: 2): it => { + block(width: 100%, below: 0.4em, + text(size: 11pt, weight: "bold", it.body)) + } + show heading.where(level: 3): it => { + block(width: 100%, fill: rgb("#006699"), inset: 5pt, + text(fill: white, weight: "bold", it.body)) + } - #for (i, node) in technique.body.enumerate() { - render-node(node) - if i + 1 < technique.body.len() and node.type == "section" { - line(length: 100%, stroke: (thickness: 0.5pt, paint: rgb("#003366"), dash: ("dot", 2pt, 4pt, 2pt))) - } - } - ] -] + body +} diff --git a/src/templating/source.rs b/src/templating/source.rs index e7b5b68e..1e030c27 100644 --- a/src/templating/source.rs +++ b/src/templating/source.rs @@ -4,8 +4,8 @@ //! produced by the code formatter. The Typst template maps each syntax //! tag to a colour and weight. +use crate::domain::serialize::{Markup, Render}; use crate::domain::source::adapter::SourceAdapter; -use crate::domain::typst::{Data, Render}; use crate::domain::Adapter; use crate::language; use crate::templating::template::Template; @@ -15,14 +15,18 @@ pub static TEMPLATE: &str = include_str!("source.typ"); pub struct Source; impl Template for Source { - fn data(&self, document: &language::Document) -> String { + fn markup(&self, document: &language::Document) -> String { let model = SourceAdapter.extract(document); - let mut data = Data::new(); - model.render(&mut data); - data.finish() + let mut out = Markup::new(); + model.render(&mut out); + out.finish() } fn typst(&self) -> &str { TEMPLATE } + + fn domain(&self) -> &str { + "source" + } } diff --git a/src/templating/source.typ b/src/templating/source.typ index 1f8e9d68..96ff7753 100644 --- a/src/templating/source.typ +++ b/src/templating/source.typ @@ -1,49 +1,40 @@ -// Built-in source template for Technique. +// Source domain template for Technique. // -// Expects a `technique` dictionary with shape: -// (fragments: ((syntax, content), ...)) -// -// Each fragment carries a syntax tag and a content string. -// The syntax tag determines the colour and weight applied. +// Per-syntax-tag formatting functions called from Rust-generated markup. +// Each function is independently overridable via `--template`. -#let palette = ( - Neutral: (c) => raw(c), - Indent: (c) => raw(c), - Newline: (_) => linebreak(), - Header: (c) => text(fill: rgb(0x75, 0x50, 0x7b), raw(c)), - Declaration: (c) => text(fill: rgb(0x34, 0x65, 0xa4), weight: "bold", raw(c)), - Description: (c) => raw(c), - Forma: (c) => text(fill: rgb(0x8f, 0x59, 0x02), weight: "bold", raw(c)), - StepItem: (c) => text(weight: "bold", raw(c)), - CodeBlock: (c) => text(fill: rgb(0x99, 0x99, 0x99), weight: "bold", raw(c)), - Variable: (c) => text(fill: rgb(0x72, 0x9f, 0xcf), weight: "bold", raw(c)), - Section: (c) => raw(c), - String: (c) => text(fill: rgb(0x4e, 0x9a, 0x06), weight: "bold", raw(c)), - Numeric: (c) => text(fill: rgb(0xad, 0x7f, 0xa8), weight: "bold", raw(c)), - Response: (c) => text(fill: rgb(0xf5, 0x79, 0x00), weight: "bold", raw(c)), - Invocation: (c) => text(fill: rgb(0x3b, 0x5d, 0x7d), weight: "bold", raw(c)), - Title: (c) => text(weight: "bold", raw(c)), - Keyword: (c) => text(fill: rgb(0x75, 0x50, 0x7b), weight: "bold", raw(c)), - Function: (c) => text(fill: rgb(0x34, 0x65, 0xa4), weight: "bold", raw(c)), - Multiline: (c) => text(fill: rgb(0x4e, 0x9a, 0x06), weight: "bold", raw(c)), - Label: (c) => text(fill: rgb(0x60, 0x98, 0x9a), weight: "bold", raw(c)), - Operator: (c) => text(fill: red, raw(c)), - Quote: (c) => text(fill: rgb(0x99, 0x99, 0x99), weight: "bold", raw(c)), - Language: (c) => text(fill: rgb(0xc4, 0xa0, 0x00), weight: "bold", raw(c)), - Attribute: (c) => text(weight: "bold", raw(c)), - Structure: (c) => text(fill: rgb(0x99, 0x99, 0x99), weight: "bold", raw(c)), -) +// -- Formatting functions ---------------------------------------------------- -#let render-fragment(f) = { - let styler = palette.at(f.syntax, default: (c) => raw(c)) - styler(f.content) -} +#let render-neutral(c) = raw(c) +#let render-indent(c) = raw(c) +#let render-newline() = linebreak() +#let render-header(c) = text(fill: rgb(0x75, 0x50, 0x7b), raw(c)) +#let render-declaration(c) = text(fill: rgb(0x34, 0x65, 0xa4), weight: "bold", raw(c)) +#let render-description(c) = raw(c) +#let render-forma(c) = text(fill: rgb(0x8f, 0x59, 0x02), weight: "bold", raw(c)) +#let render-stepitem(c) = text(weight: "bold", raw(c)) +#let render-codeblock(c) = text(fill: rgb(0x99, 0x99, 0x99), weight: "bold", raw(c)) +#let render-variable(c) = text(fill: rgb(0x72, 0x9f, 0xcf), weight: "bold", raw(c)) +#let render-section(c) = raw(c) +#let render-string(c) = text(fill: rgb(0x4e, 0x9a, 0x06), weight: "bold", raw(c)) +#let render-numeric(c) = text(fill: rgb(0xad, 0x7f, 0xa8), weight: "bold", raw(c)) +#let render-response(c) = text(fill: rgb(0xf5, 0x79, 0x00), weight: "bold", raw(c)) +#let render-invocation(c) = text(fill: rgb(0x3b, 0x5d, 0x7d), weight: "bold", raw(c)) +#let render-title(c) = text(weight: "bold", raw(c)) +#let render-keyword(c) = text(fill: rgb(0x75, 0x50, 0x7b), weight: "bold", raw(c)) +#let render-function(c) = text(fill: rgb(0x34, 0x65, 0xa4), weight: "bold", raw(c)) +#let render-multiline(c) = text(fill: rgb(0x4e, 0x9a, 0x06), weight: "bold", raw(c)) +#let render-label(c) = text(fill: rgb(0x60, 0x98, 0x9a), weight: "bold", raw(c)) +#let render-operator(c) = text(fill: red, raw(c)) +#let render-quote(c) = text(fill: rgb(0x99, 0x99, 0x99), weight: "bold", raw(c)) +#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(technique) = [ - #show text: set text(font: "Inconsolata") - #show raw: set block(breakable: true) +// -- Default template -------------------------------------------------------- - #for f in technique.fragments { - render-fragment(f) - } -] +#let template(body) = { + set text(font: "Inconsolata") + show raw: set block(breakable: true) + body +} diff --git a/src/templating/template.rs b/src/templating/template.rs index f82c7691..028bc6cc 100644 --- a/src/templating/template.rs +++ b/src/templating/template.rs @@ -2,16 +2,15 @@ use crate::language; -/// A template transforms a Technique document into output. Internally this -/// is split into two phases: an adapter, which takes the AST from the parser -/// and converts it to domain types, and rendering which converts that domain -/// into output. Not all templates make this split; `Source` is a special case -/// that delegates directly to the code formatting logic. +/// A template transforms a Technique document into output. pub trait Template { - /// Serialize the document as a Typst data literal. - fn data(&self, document: &language::Document) -> String; + /// Render the document as Typst function-call markup. + fn markup(&self, document: &language::Document) -> String; /// Return the Typst source for this template. fn typst(&self) -> &str; + + /// Return the domain name (used for the template filename on disk). + fn domain(&self) -> &str; } diff --git a/tests/templating/rendering/mod.rs b/tests/templating/rendering/mod.rs index 25270db1..a1f5dd58 100644 --- a/tests/templating/rendering/mod.rs +++ b/tests/templating/rendering/mod.rs @@ -25,28 +25,15 @@ fn check_directory(dir: &Path, template: &impl templating::Template) { assert!(!files.is_empty(), "No .tq files found in {:?}", dir); - let mut failures = Vec::new(); - for file in &files { let source = parsing::load(file).unwrap_or_else(|e| panic!("Failed to load {:?}: {:?}", file, e)); - let doc = parsing::parse(file, &source) + let technique = parsing::parse(file, &source) .unwrap_or_else(|e| panic!("Failed to parse {:?}: {:?}", file, e)); - let output = templating::data(template, &doc); - - if output.is_empty() { - failures.push(file.clone()); - } - } - - if !failures.is_empty() { - panic!( - "Template produced empty output for {} files: {:?}", - failures.len(), - failures - ); + // Exercise the markup path; panics surface as test failures + let _ = template.markup(&technique); } }