From 25460488f17c136444ef0b0dc01c044f578696d0 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Sun, 22 Mar 2026 18:12:55 +1100 Subject: [PATCH 1/5] Introduce NASA flight plan domain --- src/domain/engine.rs | 18 ++- src/domain/mod.rs | 1 + src/domain/nasa_flight_plan/adapter.rs | 112 +++++++++++++++ src/domain/nasa_flight_plan/mod.rs | 1 + src/main.rs | 11 +- src/templating/mod.rs | 2 + src/templating/nasa_flight_plan.rs | 30 ++++ src/templating/nasa_flight_plan.typ | 184 +++++++++++++++++++++++++ 8 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 src/domain/nasa_flight_plan/adapter.rs create mode 100644 src/domain/nasa_flight_plan/mod.rs create mode 100644 src/templating/nasa_flight_plan.rs create mode 100644 src/templating/nasa_flight_plan.typ diff --git a/src/domain/engine.rs b/src/domain/engine.rs index 88c0e9a3..42b5bed7 100644 --- a/src/domain/engine.rs +++ b/src/domain/engine.rs @@ -13,8 +13,8 @@ //! projecting these into domain-specific models. use crate::language::{ - Attribute, Descriptive, Document, Element, Expression, Pair, Paragraph, Procedure, Response, - Scope, Target, Technique, + Attribute, Descriptive, Document, Element, Expression, Numeric, Pair, Paragraph, Piece, + Procedure, Response, Scope, Target, Technique, }; impl<'i> Document<'i> { @@ -318,7 +318,19 @@ fn render_expression(expr: &Expression) -> String { id.0.to_string() } Expression::Binding(inner, _) => render_expression(inner), - _ => String::new(), + Expression::String(pieces) => { + let mut result = String::new(); + for piece in pieces { + match piece { + Piece::Text(t) => result.push_str(t), + Piece::Interpolation(e) => result.push_str(&render_expression(e)), + } + } + result + } + Expression::Number(Numeric::Scientific(q)) => q.to_string(), + Expression::Number(Numeric::Integral(n)) => n.to_string(), + Expression::Tablet(_) => String::new(), } } diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 8599eda2..cbca5a26 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -12,6 +12,7 @@ mod adapter; pub mod checklist; pub mod engine; +pub mod nasa_flight_plan; pub mod procedure; pub mod recipe; pub(crate) mod serialize; diff --git a/src/domain/nasa_flight_plan/adapter.rs b/src/domain/nasa_flight_plan/adapter.rs new file mode 100644 index 00000000..62edff35 --- /dev/null +++ b/src/domain/nasa_flight_plan/adapter.rs @@ -0,0 +1,112 @@ +//! Projects the parser's AST into the NASA flight plan domain model. +//! +//! This isn't really a serious example (because the procedure template used +//! by NASA and ESA for ISS operations is a bit ridiculous), but it shows +//! Technique is usable in many procedural domains. +//! +//! Delegates to the procedure adapter for initial extraction, then +//! post-processes the result: invocation-only steps are replaced by +//! their target procedures (with ordinals transferred), producing +//! the flat numbered-procedure layout used in ISS flight documents. +//! Domain-specific builtins like `cmd()` are recognized and their +//! expressions reformatted for template rendering. + +use std::collections::HashMap; + +use crate::domain::procedure::adapter::ProcedureAdapter; +use crate::domain::procedure::types::{Document, Node}; +use crate::domain::Adapter; +use crate::language; + +pub struct NasaFlightPlanAdapter; + +impl Adapter for NasaFlightPlanAdapter { + type Model = Document; + + fn extract(&self, document: &language::Document) -> Document { + let mut doc = ProcedureAdapter.extract(document); + inline_procedures(&mut doc); + rewrite_builtins(&mut doc.body); + doc + } +} + +// -- Procedure inlining ------------------------------------------------------ + +/// Replace invocation-only steps with their target Procedure nodes, +/// transferring the step's ordinal into the procedure's name field +/// (which the template renders as the step number). +fn inline_procedures(doc: &mut Document) { + let ordinals: HashMap = collect_ordinals(&doc.body) + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(); + + for node in &mut doc.body { + if let Node::Procedure { name, .. } = node { + if let Some(ord) = ordinals.get(name.as_str()) { + *name = ord.clone(); + } + } + } + + doc.body.retain(|node| { + !matches!(node, Node::Sequential { title: None, invocations, .. } if !invocations.is_empty()) + }); +} + +/// Collect ordinals from invocation-only steps: procedure_name -> ordinal. +fn collect_ordinals(nodes: &[Node]) -> HashMap<&str, String> { + let mut map = HashMap::new(); + for node in nodes { + if let Node::Sequential { + ordinal, + title: None, + invocations, + .. + } = node + { + if invocations.len() == 1 { + map.insert(invocations[0].as_str(), ordinal.clone()); + } + } + } + map +} + +// -- Builtin functions ------------------------------------------------------- + +/// Rewrite domain-specific builtin expressions in CodeBlock nodes. +/// `cmd(Inhibit)` becomes `cmd Inhibit` for template styling. +fn rewrite_builtins(nodes: &mut [Node]) { + for node in nodes.iter_mut() { + match node { + Node::CodeBlock { + expression, + children, + .. + } => { + *expression = rewrite_expression(expression); + rewrite_builtins(children); + } + Node::Sequential { children, .. } + | Node::Parallel { children, .. } + | Node::Section { children, .. } + | Node::Procedure { children, .. } + | Node::Attribute { children, .. } => { + rewrite_builtins(children); + } + } + } +} + +/// Rewrite a single expression string if it matches a builtin pattern. +fn rewrite_expression(expr: &str) -> String { + if let Some(arg) = expr + .strip_prefix("cmd(") + .and_then(|s| s.strip_suffix(')')) + { + return format!("cmd {}", arg); + } + expr.to_string() +} diff --git a/src/domain/nasa_flight_plan/mod.rs b/src/domain/nasa_flight_plan/mod.rs new file mode 100644 index 00000000..54f0d460 --- /dev/null +++ b/src/domain/nasa_flight_plan/mod.rs @@ -0,0 +1 @@ +pub mod adapter; diff --git a/src/main.rs b/src/main.rs index 1a1f5b00..c5bb78ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use tracing_subscriber::{self, EnvFilter}; use technique::formatting::{self, Identity}; use technique::highlighting::{self, Terminal}; use technique::parsing; -use technique::templating::{self, Checklist, Procedure, Recipe, Source}; +use technique::templating::{self, Checklist, NasaFlightPlan, Procedure, Recipe, Source}; mod editor; mod output; @@ -124,7 +124,7 @@ fn main() { Arg::new("domain") .short('d') .long("domain") - .value_parser(["checklist", "procedure", "recipe", "source"]) + .value_parser(["checklist", "nasa-flight-plan", "procedure", "recipe", "source"]) .action(ArgAction::Set) .help("The kind of procedure this Technique document represents. By default the value specified in the input document's metadata will be used, falling back to source if unspecified."), ) @@ -342,12 +342,19 @@ fn main() { .unwrap_or("source"), }; + // Strip version suffix (e.g. "nasa-flight-plan,v4.0" -> "nasa-flight-plan") + let domain = domain + .split(',') + .next() + .unwrap_or(domain); + debug!(domain); // Select domain let template: &dyn templating::Template = match domain { "source" => &Source, "checklist" => &Checklist, + "nasa" | "nasa-flight-plan" => &NasaFlightPlan, "procedure" => &Procedure, "recipe" => &Recipe, other => { diff --git a/src/templating/mod.rs b/src/templating/mod.rs index f0fcb8c5..b1224dbd 100644 --- a/src/templating/mod.rs +++ b/src/templating/mod.rs @@ -5,12 +5,14 @@ //! internally. mod checklist; +mod nasa_flight_plan; mod procedure; mod recipe; mod source; mod template; pub use checklist::Checklist; +pub use nasa_flight_plan::NasaFlightPlan; pub use procedure::Procedure; pub use recipe::Recipe; pub use source::Source; diff --git a/src/templating/nasa_flight_plan.rs b/src/templating/nasa_flight_plan.rs new file mode 100644 index 00000000..65185dd1 --- /dev/null +++ b/src/templating/nasa_flight_plan.rs @@ -0,0 +1,30 @@ +//! NASA flight plan domain — renders Technique documents in the style of ISS +//! flight procedures, with bordered tables, role designators, and structured +//! command/verification layout. + +use crate::domain::nasa_flight_plan::adapter::NasaFlightPlanAdapter; +use crate::domain::serialize::{Markup, Render}; +use crate::domain::Adapter; +use crate::language; +use crate::templating::template::Template; + +pub static TEMPLATE: &str = include_str!("nasa_flight_plan.typ"); + +pub struct NasaFlightPlan; + +impl Template for NasaFlightPlan { + fn markup(&self, document: &language::Document) -> String { + let model = NasaFlightPlanAdapter.extract(document); + let mut out = Markup::new(); + model.render(&mut out); + out.finish() + } + + fn typst(&self) -> &str { + TEMPLATE + } + + fn domain(&self) -> &str { + "nasa-flight-plan" + } +} diff --git a/src/templating/nasa_flight_plan.typ b/src/templating/nasa_flight_plan.typ new file mode 100644 index 00000000..f1516686 --- /dev/null +++ b/src/templating/nasa_flight_plan.typ @@ -0,0 +1,184 @@ +// NASA flight plan domain template for Technique. +// +// Renders procedures in the style of ISS flight plan documents: +// bordered table layout, role designators in left margin, underlined +// procedure titles, boxed display names, bold commands, and +// verification checks. + +// -- Formatting functions ---------------------------------------------------- + +#let render-document(source: none, name: none, title: none, description: (), children: none) = [ + #if title != none [ + #text(size: 11pt, weight: "bold")[#upper(title)] + #v(0.5em) + ] + #block(width: 100%, stroke: 0.75pt, { + // Column header + block(width: 100%, fill: rgb("#e8e8e8"), inset: 4pt, above: 0pt, + text(size: 9pt, weight: "bold", align(center, [PROCEDURE]))) + + // Notes from description + if description.len() > 0 { + block(width: 100%, inset: (x: 10pt, y: 6pt), { + align(center, text(size: 8pt, weight: "bold", underline([NOTE]))) + v(2pt) + for para in description { + text(size: 8pt, para) + parbreak() + } + }) + line(length: 100%, stroke: 0.5pt) + } + + // Body content + if children != none { + block(width: 100%, inset: (x: 10pt, y: 6pt), children) + } + }) +] + +#let render-outline(sections: ()) = {} + +#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 section-divider() = { + line(length: 100%, stroke: 0.5pt) +} + +#let render-procedure(name: none, title: none, description: (), children: none) = { + let ordinal-width = 1.6em + block(above: 0.8em, below: 0.6em, { + set par(hanging-indent: ordinal-width + 0.2em) + box(width: ordinal-width, text(size: 9pt, weight: "bold", [#name.])) + h(0.2em) + if title != none { + text(size: 9pt, weight: "bold", underline(offset: 2pt, title)) + } + }) + pad(left: ordinal-width + 0.2em, { + for para in description { + block(above: 0.2em, below: 0.2em, { + set text(size: 8pt) + para + }) + } + if children != none { + children + } + }) +} + +#let render-step(ordinal: none, title: none, body: (), invocations: (), responses: none, children: none) = { + let ordinal-width = 1.2em + + block(above: 0.4em, below: 0.4em, breakable: false, { + set par(spacing: 0.5em, hanging-indent: ordinal-width + 0.2em) + + if ordinal != none { + box(width: ordinal-width, text(size: 8pt, [#ordinal.])) + h(0.2em) + } else if title != none or invocations.len() > 0 { + box(width: ordinal-width)[\u{2013}] + h(0.2em) + } + + if title != none { + text(size: 8pt, title) + } + + if invocations.len() > 0 { + if title != none { h(4pt) } + invocations.map(i => { + text(size: 8pt, fill: rgb("#666666"), raw("<")) + text(size: 8pt, fill: rgb("#3b5d7d"), raw(i)) + text(size: 8pt, fill: rgb("#666666"), raw(">")) + }).join(text(fill: rgb("#666666"), raw(", "))) + } + + if body.len() > 0 { + parbreak() + for para in body { + text(size: 8pt, [#para]) + parbreak() + } + } + + if responses != none { + v(2pt) + responses + } + + if children != none { + pad(left: 14pt, children) + } + }) +} + +#let render-response(value: none, condition: none) = { + h(4pt) + text(size: 8pt, [\u{221a}]) + text(size: 8pt, { + if condition != none [ #value -- #condition ] + else [ #value ] + }) +} + +#let render-attribute(name: none, children: none) = { + block(above: 0.3em, below: 0.3em, { + text(size: 8pt, weight: "bold", upper(name)) + if children != none { + pad(left: 8pt, children) + } + }) +} + +#let render-code-block(expression: none, body: (), responses: none, children: none) = { + if expression != none { + block(above: 0.3em, below: 0.3em, { + if expression.starts-with("cmd ") { + let arg = expression.slice(4) + text(size: 8pt, weight: "bold", [cmd ]) + text(size: 8pt, arg) + } else { + text(size: 8pt, fill: rgb("#444444"), raw(expression)) + } + }) + } + if body.len() > 0 { + pad(left: 14pt, { + for line in body { + text(size: 8pt, fill: rgb("#4e9a06"), weight: "bold", raw(line)) + linebreak() + } + }) + } + if responses != none or children != none { + pad(left: 14pt, { + if responses != none { + responses + } + if children != none { + children + } + }) + } +} + +// -- Default template -------------------------------------------------------- + +#let template(body) = { + set page( + paper: "us-letter", + margin: (top: 1.5cm, bottom: 1.5cm, left: 1.5cm, right: 1.5cm), + numbering: "1", + number-align: center + bottom, + ) + set par(justify: false) + set text(size: 8pt, font: "Liberation Sans") + + body +} From a66c74381d279426a5d7c33f12835834d65408d9 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Sun, 22 Mar 2026 21:11:22 +1100 Subject: [PATCH 2/5] Better name for NASA/ESA Crew Procedures domain --- examples/prototype/AirlockPowerdown.tq | 2 +- src/domain/mod.rs | 2 +- .../adapter.rs | 7 +++-- .../{nasa_flight_plan => nasa_esa_iss}/mod.rs | 0 src/main.rs | 8 ++--- src/parsing/checks/parser.rs | 4 +-- src/problem/messages.rs | 2 +- src/templating/mod.rs | 4 +-- src/templating/nasa_esa_iss.rs | 30 +++++++++++++++++++ ...{nasa_flight_plan.typ => nasa_esa_iss.typ} | 2 +- src/templating/nasa_flight_plan.rs | 30 ------------------- 11 files changed, 46 insertions(+), 45 deletions(-) rename src/domain/{nasa_flight_plan => nasa_esa_iss}/adapter.rs (95%) rename src/domain/{nasa_flight_plan => nasa_esa_iss}/mod.rs (100%) create mode 100644 src/templating/nasa_esa_iss.rs rename src/templating/{nasa_flight_plan.typ => nasa_esa_iss.typ} (98%) delete mode 100644 src/templating/nasa_flight_plan.rs diff --git a/examples/prototype/AirlockPowerdown.tq b/examples/prototype/AirlockPowerdown.tq index 852c0593..e7f2cd4d 100644 --- a/examples/prototype/AirlockPowerdown.tq +++ b/examples/prototype/AirlockPowerdown.tq @@ -1,6 +1,6 @@ % technique v1 ! PD; © 2003 National Aeronautics and Space Administration, Canadian Space Agency, European Space Agency, and Others -& nasa-flight-plan,v4.0 +& nasa-esa-iss,v4.0 emergency_procedures : diff --git a/src/domain/mod.rs b/src/domain/mod.rs index cbca5a26..3e5170ff 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -12,7 +12,7 @@ mod adapter; pub mod checklist; pub mod engine; -pub mod nasa_flight_plan; +pub mod nasa_esa_iss; pub mod procedure; pub mod recipe; pub(crate) mod serialize; diff --git a/src/domain/nasa_flight_plan/adapter.rs b/src/domain/nasa_esa_iss/adapter.rs similarity index 95% rename from src/domain/nasa_flight_plan/adapter.rs rename to src/domain/nasa_esa_iss/adapter.rs index 62edff35..f5ea6b30 100644 --- a/src/domain/nasa_flight_plan/adapter.rs +++ b/src/domain/nasa_esa_iss/adapter.rs @@ -1,4 +1,5 @@ -//! Projects the parser's AST into the NASA flight plan domain model. +//! Projects the parser's AST into the NASA/ESA ISS Crew Procedure domain +//! model. //! //! This isn't really a serious example (because the procedure template used //! by NASA and ESA for ISS operations is a bit ridiculous), but it shows @@ -18,9 +19,9 @@ use crate::domain::procedure::types::{Document, Node}; use crate::domain::Adapter; use crate::language; -pub struct NasaFlightPlanAdapter; +pub struct NasaEsaIssAdapter; -impl Adapter for NasaFlightPlanAdapter { +impl Adapter for NasaEsaIssAdapter { type Model = Document; fn extract(&self, document: &language::Document) -> Document { diff --git a/src/domain/nasa_flight_plan/mod.rs b/src/domain/nasa_esa_iss/mod.rs similarity index 100% rename from src/domain/nasa_flight_plan/mod.rs rename to src/domain/nasa_esa_iss/mod.rs diff --git a/src/main.rs b/src/main.rs index c5bb78ad..05ffdd59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use tracing_subscriber::{self, EnvFilter}; use technique::formatting::{self, Identity}; use technique::highlighting::{self, Terminal}; use technique::parsing; -use technique::templating::{self, Checklist, NasaFlightPlan, Procedure, Recipe, Source}; +use technique::templating::{self, Checklist, NasaEsaIss, Procedure, Recipe, Source}; mod editor; mod output; @@ -124,7 +124,7 @@ fn main() { Arg::new("domain") .short('d') .long("domain") - .value_parser(["checklist", "nasa-flight-plan", "procedure", "recipe", "source"]) + .value_parser(["checklist", "nasa-esa-iss", "procedure", "recipe", "source"]) .action(ArgAction::Set) .help("The kind of procedure this Technique document represents. By default the value specified in the input document's metadata will be used, falling back to source if unspecified."), ) @@ -342,7 +342,7 @@ fn main() { .unwrap_or("source"), }; - // Strip version suffix (e.g. "nasa-flight-plan,v4.0" -> "nasa-flight-plan") + // Strip version suffix (e.g. "nasa-esa-iss,v4.0" -> "nasa-esa-iss") let domain = domain .split(',') .next() @@ -354,7 +354,7 @@ fn main() { let template: &dyn templating::Template = match domain { "source" => &Source, "checklist" => &Checklist, - "nasa" | "nasa-flight-plan" => &NasaFlightPlan, + "nasa-esa-iss" => &NasaEsaIss, "procedure" => &Procedure, "recipe" => &Recipe, other => { diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index c16ba147..1e77c345 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -95,11 +95,11 @@ fn header_domain() { let result = input.read_domain_line(); assert_eq!(result, Ok(Some("checklist"))); - input.initialize("& nasa-flight-plan,v4.0"); + input.initialize("& nasa-esa-iss,v4.0"); assert!(is_domain_line(input.source)); let result = input.read_domain_line(); - assert_eq!(result, Ok(Some("nasa-flight-plan,v4.0"))); + assert_eq!(result, Ok(Some("nasa-esa-iss,v4.0"))); } // now we test incremental parsing diff --git a/src/problem/messages.rs b/src/problem/messages.rs index 5a397550..0b9e03a9 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -110,7 +110,7 @@ to be used when rendering the Technique. Common domains include renderer.style(crate::formatting::Syntax::Header, "CC-BY 4.0"), renderer.style(crate::formatting::Syntax::Header, "Proprietary"), renderer.style(crate::formatting::Syntax::Header, "checklist"), - renderer.style(crate::formatting::Syntax::Header, "nasa-flight-plan,v4.0"), + renderer.style(crate::formatting::Syntax::Header, "nasa-esa-iss,v4.0"), renderer.style(crate::formatting::Syntax::Header, "recipe") ), ) diff --git a/src/templating/mod.rs b/src/templating/mod.rs index b1224dbd..a9d4f932 100644 --- a/src/templating/mod.rs +++ b/src/templating/mod.rs @@ -5,14 +5,14 @@ //! internally. mod checklist; -mod nasa_flight_plan; +mod nasa_esa_iss; mod procedure; mod recipe; mod source; mod template; pub use checklist::Checklist; -pub use nasa_flight_plan::NasaFlightPlan; +pub use nasa_esa_iss::NasaEsaIss; pub use procedure::Procedure; pub use recipe::Recipe; pub use source::Source; diff --git a/src/templating/nasa_esa_iss.rs b/src/templating/nasa_esa_iss.rs new file mode 100644 index 00000000..0a0c0ef7 --- /dev/null +++ b/src/templating/nasa_esa_iss.rs @@ -0,0 +1,30 @@ +//! NASA/ESA ISS Crew Procedure domain — renders Technique documents in the +//! style of ISS crew procedures, with bordered tables, role designators, and +//! structured command/verification layout. + +use crate::domain::nasa_esa_iss::adapter::NasaEsaIssAdapter; +use crate::domain::serialize::{Markup, Render}; +use crate::domain::Adapter; +use crate::language; +use crate::templating::template::Template; + +pub static TEMPLATE: &str = include_str!("nasa_esa_iss.typ"); + +pub struct NasaEsaIss; + +impl Template for NasaEsaIss { + fn markup(&self, document: &language::Document) -> String { + let model = NasaEsaIssAdapter.extract(document); + let mut out = Markup::new(); + model.render(&mut out); + out.finish() + } + + fn typst(&self) -> &str { + TEMPLATE + } + + fn domain(&self) -> &str { + "nasa-esa-iss" + } +} diff --git a/src/templating/nasa_flight_plan.typ b/src/templating/nasa_esa_iss.typ similarity index 98% rename from src/templating/nasa_flight_plan.typ rename to src/templating/nasa_esa_iss.typ index f1516686..8c3d01a9 100644 --- a/src/templating/nasa_flight_plan.typ +++ b/src/templating/nasa_esa_iss.typ @@ -1,4 +1,4 @@ -// NASA flight plan domain template for Technique. +// NASA/ESA ISS Crew Procedure domain template for Technique. // // Renders procedures in the style of ISS flight plan documents: // bordered table layout, role designators in left margin, underlined diff --git a/src/templating/nasa_flight_plan.rs b/src/templating/nasa_flight_plan.rs deleted file mode 100644 index 65185dd1..00000000 --- a/src/templating/nasa_flight_plan.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! NASA flight plan domain — renders Technique documents in the style of ISS -//! flight procedures, with bordered tables, role designators, and structured -//! command/verification layout. - -use crate::domain::nasa_flight_plan::adapter::NasaFlightPlanAdapter; -use crate::domain::serialize::{Markup, Render}; -use crate::domain::Adapter; -use crate::language; -use crate::templating::template::Template; - -pub static TEMPLATE: &str = include_str!("nasa_flight_plan.typ"); - -pub struct NasaFlightPlan; - -impl Template for NasaFlightPlan { - fn markup(&self, document: &language::Document) -> String { - let model = NasaFlightPlanAdapter.extract(document); - let mut out = Markup::new(); - model.render(&mut out); - out.finish() - } - - fn typst(&self) -> &str { - TEMPLATE - } - - fn domain(&self) -> &str { - "nasa-flight-plan" - } -} From 7cae36cf7cfbd30425f662aeca2508f92f21df83 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Sun, 22 Mar 2026 21:56:11 +1100 Subject: [PATCH 3/5] Present foreach ... seq() as boxes --- src/domain/nasa_esa_iss/adapter.rs | 16 +++++++ src/templating/nasa_esa_iss.typ | 77 ++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/domain/nasa_esa_iss/adapter.rs b/src/domain/nasa_esa_iss/adapter.rs index f5ea6b30..630cecce 100644 --- a/src/domain/nasa_esa_iss/adapter.rs +++ b/src/domain/nasa_esa_iss/adapter.rs @@ -109,5 +109,21 @@ fn rewrite_expression(expr: &str) -> String { { return format!("cmd {}", arg); } + // foreach node in seq(1, 6) -> foreach node 1 2 3 4 5 6 + if let Some(rest) = expr.strip_prefix("foreach ") { + if let Some((var, seq_expr)) = rest.split_once(" in ") { + if let Some((start, end)) = parse_seq(seq_expr) { + let values: Vec = (start..=end).map(|n| n.to_string()).collect(); + return format!("foreach {} {}", var.trim(), values.join(" ")); + } + } + } expr.to_string() } + +/// Parse `seq(A, B)` into a (start, end) pair. +fn parse_seq(s: &str) -> Option<(i64, i64)> { + let inner = s.strip_prefix("seq(")?.strip_suffix(')')?; + let (a, b) = inner.split_once(", ")?; + Some((a.trim().parse().ok()?, b.trim().parse().ok()?)) +} diff --git a/src/templating/nasa_esa_iss.typ b/src/templating/nasa_esa_iss.typ index 8c3d01a9..f3e38cb7 100644 --- a/src/templating/nasa_esa_iss.typ +++ b/src/templating/nasa_esa_iss.typ @@ -137,34 +137,61 @@ } #let render-code-block(expression: none, body: (), responses: none, children: none) = { - if expression != none { - block(above: 0.3em, below: 0.3em, { - if expression.starts-with("cmd ") { - let arg = expression.slice(4) - text(size: 8pt, weight: "bold", [cmd ]) - text(size: 8pt, arg) - } else { - text(size: 8pt, fill: rgb("#444444"), raw(expression)) + if expression != none and expression.starts-with("foreach ") { + // "foreach var 1 2 3 4 5 6" — render as numbered boxes + let parts = expression.split(" ") + let var = parts.at(1) + let values = parts.slice(2) + + block(above: 0.3em, below: 0.6em, { + text(size: 8pt, [\[]) + text(size: 8pt, weight: "bold", var) + text(size: 8pt, [\] = ]) + h(2pt) + for val in values { + box(baseline: 15%, stroke: 0.5pt, inset: (x: 3pt, y: 1pt), text(size: 8pt, val)) + h(1pt) } }) - } - if body.len() > 0 { - pad(left: 14pt, { - for line in body { - text(size: 8pt, fill: rgb("#4e9a06"), weight: "bold", raw(line)) - linebreak() - } - }) - } - if responses != none or children != none { - pad(left: 14pt, { - if responses != none { - responses - } - if children != none { - children - } + + // Loop body with left bracket + block(above: 0.2em, below: 0.2em, + stroke: (left: 0.5pt), inset: (left: 8pt), { + if responses != none { responses } + if children != none { children } }) + + block(above: 0.1em, text(size: 8pt, [Repeat])) + } else { + if expression != none { + block(above: 0.3em, below: 0.3em, { + if expression.starts-with("cmd ") { + let arg = expression.slice(4) + text(size: 8pt, weight: "bold", [cmd ]) + text(size: 8pt, arg) + } else { + text(size: 8pt, fill: rgb("#444444"), raw(expression)) + } + }) + } + if body.len() > 0 { + pad(left: 14pt, { + for line in body { + text(size: 8pt, fill: rgb("#4e9a06"), weight: "bold", raw(line)) + linebreak() + } + }) + } + if responses != none or children != none { + pad(left: 14pt, { + if responses != none { + responses + } + if children != none { + children + } + }) + } } } From 0db054f8a622f584ce05eb83f082d6c780dbc66a Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Sun, 22 Mar 2026 22:00:09 +1100 Subject: [PATCH 4/5] Better margins on smaller paper --- src/templating/nasa_esa_iss.typ | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/templating/nasa_esa_iss.typ b/src/templating/nasa_esa_iss.typ index f3e38cb7..081bc8ad 100644 --- a/src/templating/nasa_esa_iss.typ +++ b/src/templating/nasa_esa_iss.typ @@ -32,7 +32,7 @@ // Body content if children != none { - block(width: 100%, inset: (x: 10pt, y: 6pt), children) + block(width: 100%, inset: (x: 10pt, top: 6pt, bottom: 12pt), children) } }) ] @@ -51,7 +51,7 @@ #let render-procedure(name: none, title: none, description: (), children: none) = { let ordinal-width = 1.6em - block(above: 0.8em, below: 0.6em, { + block(above: 0.8em, below: 0.8em, { set par(hanging-indent: ordinal-width + 0.2em) box(width: ordinal-width, text(size: 9pt, weight: "bold", [#name.])) h(0.2em) @@ -199,7 +199,7 @@ #let template(body) = { set page( - paper: "us-letter", + paper: "a5", margin: (top: 1.5cm, bottom: 1.5cm, left: 1.5cm, right: 1.5cm), numbering: "1", number-align: center + bottom, From 0cd267fb7f89cfd2a17875c890faf91f6970a0c8 Mon Sep 17 00:00:00 2001 From: Andrew Cowie Date: Sun, 22 Mar 2026 22:18:02 +1100 Subject: [PATCH 5/5] Remove domain version stripping hack --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bca15c5b..af5991fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,7 +472,7 @@ dependencies = [ [[package]] name = "technique" -version = "0.5.2" +version = "0.5.3" dependencies = [ "clap", "ignore", diff --git a/Cargo.toml b/Cargo.toml index c8150367..7cfa01b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "technique" -version = "0.5.2" +version = "0.5.3" edition = "2021" description = "A domain specific language for procedures." authors = [ "Andrew Cowie" ] diff --git a/src/main.rs b/src/main.rs index 05ffdd59..62781deb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -342,12 +342,6 @@ fn main() { .unwrap_or("source"), }; - // Strip version suffix (e.g. "nasa-esa-iss,v4.0" -> "nasa-esa-iss") - let domain = domain - .split(',') - .next() - .unwrap_or(domain); - debug!(domain); // Select domain