Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" ]
Expand Down
2 changes: 1 addition & 1 deletion examples/prototype/AirlockPowerdown.tq
Original file line number Diff line number Diff line change
@@ -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 :

Expand Down
18 changes: 15 additions & 3 deletions src/domain/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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(),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
mod adapter;
pub mod checklist;
pub mod engine;
pub mod nasa_esa_iss;
pub mod procedure;
pub mod recipe;
pub(crate) mod serialize;
Expand Down
129 changes: 129 additions & 0 deletions src/domain/nasa_esa_iss/adapter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! 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
//! 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 NasaEsaIssAdapter;

impl Adapter for NasaEsaIssAdapter {
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<String, String> = 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);
}
// 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<String> = (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()?))
}
1 change: 1 addition & 0 deletions src/domain/nasa_esa_iss/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod adapter;
5 changes: 3 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, NasaEsaIss, Procedure, Recipe, Source};

mod editor;
mod output;
Expand Down Expand Up @@ -124,7 +124,7 @@ fn main() {
Arg::new("domain")
.short('d')
.long("domain")
.value_parser(["checklist", "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."),
)
Expand Down Expand Up @@ -348,6 +348,7 @@ fn main() {
let template: &dyn templating::Template = match domain {
"source" => &Source,
"checklist" => &Checklist,
"nasa-esa-iss" => &NasaEsaIss,
"procedure" => &Procedure,
"recipe" => &Recipe,
other => {
Expand Down
4 changes: 2 additions & 2 deletions src/parsing/checks/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/problem/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
),
)
Expand Down
2 changes: 2 additions & 0 deletions src/templating/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
//! internally.

mod checklist;
mod nasa_esa_iss;
mod procedure;
mod recipe;
mod source;
mod template;

pub use checklist::Checklist;
pub use nasa_esa_iss::NasaEsaIss;
pub use procedure::Procedure;
pub use recipe::Recipe;
pub use source::Source;
Expand Down
30 changes: 30 additions & 0 deletions src/templating/nasa_esa_iss.rs
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading
Loading