#![forbid(unsafe_code)]
#![deny(missing_docs)]
use std::error::Error;
use std::fmt;
use stack_compiler::diagnostic as compiler_diagnostic;
mod labels;
mod resources;
mod routing;
mod scene;
mod svg;
#[cfg(test)]
mod layout_quality;
#[cfg(test)]
mod layout_congestion;
#[cfg(test)]
mod placement_quality;
mod language;
mod provider;
pub use language::{
CompletionItem, CompletionKind, CompletionOutput, Hover, HoverKind, HoverOutput,
LANGUAGE_INTELLIGENCE_SCHEMA_VERSION, TextEdit,
};
pub use provider::{ProviderAsset, ProviderPack};
pub const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
pub type OperationResult<T> = Result<T, OperationalError>;
#[derive(Debug, Clone, Copy)]
pub struct Engine<'catalog> {
catalog: &'catalog stack_theme::Catalog,
catalog_revision: &'catalog str,
provider_packs: &'catalog [ProviderPack],
}
#[derive(Debug)]
struct PreparedScene<'catalog> {
scene: scene::Scene,
resources: resources::Resources<'catalog>,
diagnostics: Vec<Diagnostic>,
}
impl Engine<'static> {
#[must_use]
pub fn bundled() -> Self {
Self {
catalog: stack_theme::catalog(),
catalog_revision: stack_theme::CATALOG_REVISION,
provider_packs: &[],
}
}
}
impl Default for Engine<'static> {
fn default() -> Self {
Self::bundled()
}
}
impl<'catalog> Engine<'catalog> {
pub fn with_provider_packs(provider_packs: &'catalog [ProviderPack]) -> OperationResult<Self> {
Self::with_catalog_and_provider_packs(
stack_theme::catalog(),
stack_theme::CATALOG_REVISION,
provider_packs,
)
}
pub fn with_catalog(
catalog: &'catalog stack_theme::Catalog,
catalog_revision: &'catalog str,
) -> OperationResult<Self> {
Self::with_catalog_and_provider_packs(catalog, catalog_revision, &[])
}
pub fn with_catalog_and_provider_packs(
catalog: &'catalog stack_theme::Catalog,
catalog_revision: &'catalog str,
provider_packs: &'catalog [ProviderPack],
) -> OperationResult<Self> {
if !valid_catalog_revision(catalog_revision) {
return Err(OperationalError::InvalidCatalog {
reason: "catalog revision must be a lowercase sha256 digest",
});
}
if !catalog
.themes
.iter()
.any(|theme| theme.id == catalog.fallbacks.missing_theme_id)
{
return Err(OperationalError::InvalidCatalog {
reason: "missing-theme fallback does not reference an active theme",
});
}
if catalog.themes.iter().any(|theme| {
!theme
.icons
.iter()
.any(|icon| icon.id == catalog.fallbacks.missing_icon_id)
}) {
return Err(OperationalError::InvalidCatalog {
reason: "missing-icon fallback is not present in every theme",
});
}
if provider_packs.len() > 32 {
return Err(OperationalError::InvalidProviderPack {
reason: "an engine may contain at most 32 provider packs",
});
}
for (index, pack) in provider_packs.iter().enumerate() {
if provider_packs[..index]
.iter()
.any(|candidate| candidate.manifest().provider.id == pack.manifest().provider.id)
{
return Err(OperationalError::InvalidProviderPack {
reason: "provider namespaces must be unique",
});
}
}
Ok(Self {
catalog,
catalog_revision,
provider_packs,
})
}
pub fn format(&self, source: &[u8]) -> OperationResult<FormatOutput> {
let formatted = stack_formatter::format_bytes(source);
Ok(FormatOutput {
formatted_source: formatted.source,
diagnostics: portable_diagnostics(formatted.diagnostics),
metadata: self.metadata(declared_language_version(source)),
})
}
pub fn check(&self, source: &[u8]) -> OperationResult<CheckOutput> {
let compiled = stack_compiler::compile_bytes_with_source_map(source);
let mut diagnostics = portable_diagnostics(compiled.diagnostics);
if let Some(diagram) = &compiled.diagram {
let source_map = compiled.source_map.as_ref().ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "compiler omitted the source map for normalized IR",
},
)?;
diagnostics.extend(self.prepare_scene(diagram, source_map)?.diagnostics);
}
Ok(CheckOutput {
diagnostics,
metadata: self.metadata(declared_language_version(source)),
})
}
pub fn render(&self, source: &[u8]) -> OperationResult<RenderOutput> {
let compiled = stack_compiler::compile_bytes_with_source_map(source);
let metadata = self.metadata(declared_language_version(source));
if compiled.diagram.is_none() {
return Ok(RenderOutput {
svg: None,
diagnostics: portable_diagnostics(compiled.diagnostics),
metadata,
provider_notices: Vec::new(),
});
}
let diagram = compiled.diagram.as_ref().ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "compiler omitted normalized IR after successful compilation",
},
)?;
let source_map = compiled.source_map.as_ref().ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "compiler omitted the source map for normalized IR",
},
)?;
let prepared = self.prepare_scene(diagram, source_map)?;
let mut diagnostics = portable_diagnostics(compiled.diagnostics);
diagnostics.extend(prepared.diagnostics);
let svg = svg::render(diagram, &prepared.scene, &prepared.resources, &metadata).map_err(
|error| OperationalError::InvalidIntermediateRepresentation {
reason: error.reason(),
},
)?;
Ok(RenderOutput {
svg: Some(svg),
diagnostics,
metadata,
provider_notices: prepared.resources.provider_notices(),
})
}
fn metadata(&self, language_version: Option<LanguageVersion>) -> EngineMetadata {
EngineMetadata {
engine_version: ENGINE_VERSION.to_owned(),
language_version,
theme_catalog_version: self.catalog.catalog_version.clone(),
theme_catalog_revision: self.catalog_revision.to_owned(),
}
}
fn prepare_scene(
&self,
diagram: &stack_compiler::ir::Diagram,
source_map: &stack_compiler::source_map::SourceMap,
) -> OperationResult<PreparedScene<'catalog>> {
let resources = resources::Resources::resolve(diagram, self.catalog, self.provider_packs)
.map_err(|error| OperationalError::InvalidCatalog {
reason: error.reason(),
})?;
let scene = scene::layout(diagram, self.catalog).map_err(|error| {
OperationalError::InvalidIntermediateRepresentation {
reason: error.reason(),
}
})?;
if !scene.geometry_is_valid() {
return Err(OperationalError::InvalidIntermediateRepresentation {
reason: "layout produced invalid containment or overlap geometry",
});
}
let mut diagnostics = resources
.warnings
.iter()
.map(|warning| resource_diagnostic(warning, source_map))
.collect::<OperationResult<Vec<_>>>()?;
diagnostics.extend(
scene
.unsatisfied_orders
.iter()
.map(|scope| order_diagnostic(scope, source_map))
.collect::<OperationResult<Vec<_>>>()?,
);
Ok(PreparedScene {
scene,
resources,
diagnostics,
})
}
}
fn resource_diagnostic(
warning: &resources::ResourceWarning,
source_map: &stack_compiler::source_map::SourceMap,
) -> OperationResult<Diagnostic> {
let (code, message, help, origin) = match warning {
resources::ResourceWarning::MissingTheme(identifier) => (
"STK6001",
format!("theme '{identifier}' is unavailable; default theme was used"),
"Install the requested theme or select an available theme.",
source_map.theme(),
),
resources::ResourceWarning::MissingIcon { node_id, icon_id } => (
"STK5001",
format!("icon '{icon_id}' is unavailable; the missing-icon fallback was used"),
"Install the icon in the effective theme or remove the icon property.",
source_map.node_icon(node_id).ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "source map omitted a normalized node",
},
)?,
),
};
let span = origin
.span()
.ok_or(OperationalError::InvalidIntermediateRepresentation {
reason: "source map omitted an authored resource identifier",
})?;
Ok(Diagnostic {
code: code.to_owned(),
severity: Severity::Warning,
message,
range: SourceRange::from(span),
expected: Vec::new(),
help: Some(help.to_owned()),
related: Vec::new(),
})
}
fn order_diagnostic(
scope: &scene::SceneScope,
source_map: &stack_compiler::source_map::SourceMap,
) -> OperationResult<Diagnostic> {
let origin = match scope {
scene::SceneScope::Diagram => source_map.diagram_order(),
scene::SceneScope::Group(identifier) => source_map.group_order(identifier).ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "source map omitted a normalized group",
},
)?,
};
let span = origin
.span()
.ok_or(OperationalError::InvalidIntermediateRepresentation {
reason: "source map omitted an authored order hint",
})?;
Ok(Diagnostic {
code: "STK4001".to_owned(),
severity: Severity::Warning,
message: "order hint could not be satisfied by deterministic layout".to_owned(),
range: SourceRange::from(span),
expected: Vec::new(),
help: Some("Adjust the order hint or same-rank constraints.".to_owned()),
related: Vec::new(),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum OperationalError {
InvalidCatalog {
reason: &'static str,
},
InvalidProviderPack {
reason: &'static str,
},
InvalidLanguageIntelligenceInput {
reason: &'static str,
},
InvalidIntermediateRepresentation {
reason: &'static str,
},
}
impl fmt::Display for OperationalError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidCatalog { reason } => write!(formatter, "invalid theme catalog: {reason}"),
Self::InvalidProviderPack { reason } => {
write!(formatter, "invalid provider pack: {reason}")
}
Self::InvalidLanguageIntelligenceInput { reason } => {
write!(formatter, "invalid language-intelligence input: {reason}")
}
Self::InvalidIntermediateRepresentation { reason } => {
write!(formatter, "invalid intermediate representation: {reason}")
}
}
}
}
impl Error for OperationalError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EngineMetadata {
pub engine_version: String,
pub language_version: Option<LanguageVersion>,
pub theme_catalog_version: String,
pub theme_catalog_revision: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LanguageVersion {
pub major: u32,
pub minor: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatOutput {
pub formatted_source: Option<String>,
pub diagnostics: Vec<Diagnostic>,
pub metadata: EngineMetadata,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckOutput {
pub diagnostics: Vec<Diagnostic>,
pub metadata: EngineMetadata,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderOutput {
pub svg: Option<String>,
pub diagnostics: Vec<Diagnostic>,
pub metadata: EngineMetadata,
pub provider_notices: Vec<ProviderNotice>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderNotice {
pub provider_id: String,
pub provider_name: String,
pub pack_version: String,
pub pack_revision: String,
pub source_release: String,
pub archive_sha256: String,
pub terms_url: String,
pub sources: Vec<ProviderNoticeSource>,
pub attribution: String,
pub terms_summary: String,
pub non_endorsement: String,
pub icons: Vec<ProviderNoticeIcon>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderNoticeIcon {
pub id: String,
pub product_name: String,
pub brand_source_url: Option<String>,
pub brand_guidelines_url: Option<String>,
pub source_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderNoticeSource {
pub id: String,
pub page_url: String,
pub release: String,
pub archive_sha256: String,
pub terms_url: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub code: String,
pub severity: Severity,
pub message: String,
pub range: SourceRange,
pub expected: Vec<String>,
pub help: Option<String>,
pub related: Vec<RelatedInformation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelatedInformation {
pub message: String,
pub range: SourceRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceRange {
pub start: SourcePosition,
pub end: SourcePosition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourcePosition {
pub byte_offset: u64,
pub line: u64,
pub column: u64,
}
fn valid_catalog_revision(revision: &str) -> bool {
revision.strip_prefix("sha256:").is_some_and(|digest| {
digest.len() == 64
&& digest
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
})
}
fn declared_language_version(source: &[u8]) -> Option<LanguageVersion> {
stack_compiler::parse_bytes(source)
.document
.map(|document| LanguageVersion {
major: document.version.major,
minor: document.version.minor,
})
}
fn portable_diagnostics(diagnostics: Vec<compiler_diagnostic::Diagnostic>) -> Vec<Diagnostic> {
diagnostics.into_iter().map(Diagnostic::from).collect()
}
impl From<compiler_diagnostic::Diagnostic> for Diagnostic {
fn from(diagnostic: compiler_diagnostic::Diagnostic) -> Self {
Self {
code: diagnostic.code.to_owned(),
severity: match diagnostic.severity {
compiler_diagnostic::Severity::Error => Severity::Error,
compiler_diagnostic::Severity::Warning => Severity::Warning,
},
message: diagnostic.message,
range: SourceRange::from(diagnostic.span),
expected: diagnostic.expected,
help: diagnostic.help,
related: diagnostic
.related
.into_iter()
.map(|related| RelatedInformation {
message: related.message,
range: SourceRange::from(related.span),
})
.collect(),
}
}
}
impl From<compiler_diagnostic::Span> for SourceRange {
fn from(span: compiler_diagnostic::Span) -> Self {
Self {
start: SourcePosition::from(span.start),
end: SourcePosition::from(span.end),
}
}
}
impl From<compiler_diagnostic::SourcePosition> for SourcePosition {
fn from(position: compiler_diagnostic::SourcePosition) -> Self {
Self {
byte_offset: position.byte_offset as u64,
line: position.line as u64,
column: position.column as u64,
}
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use stack_compiler::diagnostic as compiler_diagnostic;
use super::{
Diagnostic, ENGINE_VERSION, Engine, LanguageVersion, OperationalError, Severity,
SourcePosition,
};
const VALID_SOURCE: &[u8] = b"stack 1.0 diagram \"API\" { node api \"API\" }";
#[test]
fn valid_wide_connectors_render_labelled_edges_in_both_directions() -> Result<(), Box<dyn Error>>
{
for width in [1_500, 15_000, 15_998, 15_999, 16_000, 32_000] {
let mut catalog = stack_theme::catalog().clone();
for theme in &mut catalog.themes {
theme.connector.width_milli_px = width;
}
let engine = Engine::with_catalog(&catalog, stack_theme::CATALOG_REVISION)?;
for direction in ["right", "down"] {
let source = format!(
"stack 1.0 diagram \"Wide stroke\" {{ layout {{ direction {direction} }} node a \"A\" node b \"B\" edge a -> b \"Request\" }}"
);
let output = engine.render(source.as_bytes())?;
assert!(
output.diagnostics.is_empty(),
"width={width}, direction={direction}"
);
assert!(
output
.svg
.ok_or("missing wide-stroke SVG")?
.contains("data-edge-label=\"Request\"")
);
}
}
Ok(())
}
#[test]
fn bundled_engine_reports_all_version_metadata() {
let engine = Engine::bundled();
let result = engine.check(VALID_SOURCE);
assert!(result.is_ok());
if let Ok(output) = result {
assert!(output.diagnostics.is_empty());
assert_eq!(output.metadata.engine_version, ENGINE_VERSION);
assert_eq!(
output.metadata.language_version,
Some(LanguageVersion { major: 1, minor: 0 })
);
assert_eq!(output.metadata.theme_catalog_version, "0.7.0");
assert_eq!(
output.metadata.theme_catalog_revision,
stack_theme::CATALOG_REVISION
);
assert_eq!(Engine::default().check(VALID_SOURCE), Ok(output));
}
}
#[test]
fn bundled_catalog_resolves_explicit_core_icons() -> Result<(), Box<dyn Error>> {
let expected_icons = [
("api", "Application programming interface"),
("web", "Web application"),
("mobile", "Mobile application"),
("desktop", "Desktop application"),
("server", "Server host"),
("container", "Application container"),
("cluster", "Compute cluster"),
("cloud", "Cloud environment"),
("scheduler", "Scheduled execution"),
("webhook", "Webhook endpoint"),
("identity", "Identity and access"),
("observability", "Observability system"),
("gateway", "Network gateway"),
("load-balancer", "Load balancer"),
("dns", "Domain name service"),
("cdn", "Content delivery network"),
("firewall", "Network firewall"),
("network", "Network topology"),
("event", "Discrete event"),
("stream", "Event stream"),
("search", "Search service"),
("analytics", "Analytics system"),
("repository", "Source code repository"),
("pipeline", "Delivery pipeline"),
("secret", "Secret or credential"),
("document", "Document or knowledge base"),
("task", "Task or issue tracker"),
("chat", "Chat or messaging tool"),
("email", "Email delivery"),
("ai", "Artificial intelligence system"),
];
let catalog = stack_theme::catalog();
assert_eq!(catalog.catalog_version, "0.7.0");
assert_eq!(
stack_theme::CATALOG_REVISION,
"sha256:4a8b94b746c6b120998bfbe701edd722449a28c89c424b0a33f67561756ded5a"
);
for theme in &catalog.themes {
for (identifier, subject) in expected_icons {
let icon = theme
.icons
.iter()
.find(|icon| icon.id == identifier)
.ok_or("core icon is unavailable in a bundled theme")?;
assert_eq!(icon.subject, subject);
assert_eq!(icon.asset.path, format!("assets/core/{identifier}.svg"));
}
}
let source = b"stack 1.0 diagram \"Core icon\" { theme dark node gateway \"Gateway\" { kind service detail \"Public API\" icon \"gateway\" } }";
let checked = Engine::bundled().check(source)?;
let rendered = Engine::bundled().render(source)?;
assert!(checked.diagnostics.is_empty());
assert!(rendered.diagnostics.is_empty());
assert_eq!(rendered.metadata.theme_catalog_version, "0.7.0");
assert_eq!(
rendered.metadata.theme_catalog_revision,
stack_theme::CATALOG_REVISION
);
let svg = rendered.svg.ok_or("explicit icon render produced no SVG")?;
assert!(svg.contains("data-icon-id=\"gateway\""));
assert!(!svg.contains("data-icon-id=\"kind-external\""));
Ok(())
}
#[test]
fn format_preserves_semantic_diagnostics_but_not_syntax_failures() {
let engine = Engine::bundled();
let semantic_error = b"stack 1.0 diagram \"API\" { node api \"A\" node api \"B\" }";
let semantic_result = engine.format(semantic_error);
assert!(semantic_result.is_ok());
if let Ok(semantic) = semantic_result {
assert!(semantic.formatted_source.is_some());
assert!(!semantic.diagnostics.is_empty());
}
let encoding_result = engine.format(b"stack 1.0\n\xff");
assert!(encoding_result.is_ok());
if let Ok(encoding) = encoding_result {
assert!(encoding.formatted_source.is_none());
assert_eq!(encoding.diagnostics[0].code, "STK1001");
assert_eq!(encoding.metadata.language_version, None);
}
}
#[test]
fn check_keeps_compiler_diagnostic_order_and_positions() {
let source =
b"stack 1.0 diagram \"API\" { node api \"A\" node api \"B\" edge api -> missing }";
let expected = stack_compiler::compile_bytes(source)
.diagnostics
.into_iter()
.map(|diagnostic| diagnostic.code)
.collect::<Vec<_>>();
let result = Engine::bundled().check(source);
assert!(result.is_ok());
if let Ok(output) = result {
assert_eq!(
output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>(),
expected
);
assert!(
output
.diagnostics
.windows(2)
.all(|pair| pair[0].range.start.byte_offset <= pair[1].range.start.byte_offset)
);
}
}
#[test]
fn check_emits_order_warning_at_the_authored_statement() -> Result<(), Box<dyn Error>> {
let source = "stack 1.0 diagram \"Order\" { layout { direction right order [b, a] } node a \"A\" node b \"B\" }";
let output = Engine::bundled().check(source.as_bytes())?;
assert_eq!(output.diagnostics.len(), 1);
let diagnostic = &output.diagnostics[0];
assert_eq!(diagnostic.code, "STK4001");
assert_eq!(diagnostic.severity, Severity::Warning);
let start = source
.find("order [b, a]")
.ok_or("missing order statement")?;
let end = start + "order [b, a]".len();
assert_eq!(diagnostic.range.start.byte_offset, start as u64);
assert_eq!(diagnostic.range.end.byte_offset, end as u64);
assert_eq!(diagnostic.range.start.line, 1);
assert_eq!(diagnostic.range.start.column, start as u64 + 1);
assert_eq!(diagnostic.range.end.column, end as u64 + 1);
Ok(())
}
#[test]
fn check_omits_order_warning_when_rank_placement_satisfies_it() -> Result<(), Box<dyn Error>> {
let source = b"stack 1.0 diagram \"Order\" { layout { direction right rank same [a, b] order [b, a] } node a \"A\" node b \"B\" }";
let output = Engine::bundled().check(source)?;
assert!(output.diagnostics.is_empty());
Ok(())
}
#[test]
fn group_order_warning_uses_the_group_source_map_entry() -> Result<(), Box<dyn Error>> {
let source = "stack 1.0 diagram \"Group order\" { group pair \"Pair\" { layout { direction down order [b, a] } node a \"A\" node b \"B\" } }";
let output = Engine::bundled().check(source.as_bytes())?;
assert_eq!(output, Engine::bundled().check(source.as_bytes())?);
assert_eq!(
output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>(),
vec!["STK4001"]
);
let start = source
.find("order [b, a]")
.ok_or("missing order statement")?;
assert_eq!(output.diagnostics[0].range.start.byte_offset, start as u64);
Ok(())
}
#[test]
fn layout_warnings_follow_compiler_warnings() -> Result<(), Box<dyn Error>> {
let mut source = String::from(
"stack 1.0 diagram \"Warnings\" { layout { direction right order [hub, n0] } node hub \"Hub\" ",
);
for index in 0..13 {
source.push_str(&format!(
"node n{index} \"N {index}\" edge hub -> n{index} "
));
}
source.push('}');
let output = Engine::bundled().check(source.as_bytes())?;
assert_eq!(
output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>(),
vec!["STK4002", "STK4001"]
);
Ok(())
}
#[test]
fn resource_fallbacks_report_authored_ranges_and_render_svg() -> Result<(), Box<dyn Error>> {
let source = "stack 1.0 diagram \"Fallbacks\" { theme neon layout { direction right order [b, a] } node a \"A\" { icon \"missing\" } node b \"B\" }";
let checked = Engine::bundled().check(source.as_bytes())?;
let rendered = Engine::bundled().render(source.as_bytes())?;
assert_eq!(checked.diagnostics, rendered.diagnostics);
assert_eq!(
rendered
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>(),
vec!["STK6001", "STK5001", "STK4001"]
);
let theme_start = source.find("neon").ok_or("missing theme identifier")?;
assert_eq!(
rendered.diagnostics[0].range.start.byte_offset,
theme_start as u64
);
assert_eq!(
rendered.diagnostics[0].range.end.byte_offset,
(theme_start + "neon".len()) as u64
);
let icon_start = source.find("\"missing\"").ok_or("missing icon string")?;
assert_eq!(
rendered.diagnostics[1].range.start.byte_offset,
icon_start as u64
);
assert_eq!(
rendered.diagnostics[1].range.end.byte_offset,
(icon_start + "\"missing\"".len()) as u64
);
let svg = rendered.svg.ok_or("render produced no SVG")?;
assert!(svg.contains("data-theme-id=\"default\""));
assert!(svg.contains("data-icon-id=\"kind-external\""));
Ok(())
}
#[test]
fn render_is_repeatable_and_escapes_source_text() -> Result<(), Box<dyn Error>> {
let source = b"stack 1.0 diagram \"<script>&\" { node client \"\\\" onload=\\\"alert(1)<&>\" edge client -> api \"javascript:alert(1)\" node api \"API\" }";
let first = Engine::bundled().render(source)?;
let second = Engine::bundled().render(source)?;
assert_eq!(first, second);
let svg = first.svg.ok_or("render produced no SVG")?;
assert!(svg.contains("<script>&"));
assert!(svg.contains("" onload="alert(1)<&>"));
assert!(!svg.contains("<script"));
assert!(!svg.contains("href="));
Ok(())
}
#[cfg(feature = "conformance")]
#[test]
fn canonical_valid_fixtures_render_standalone_svg() -> Result<(), Box<dyn Error>> {
let specification = std::env::var("STACK_SPECIFICATION_DIR")?;
let valid_root = std::path::Path::new(&specification).join("conformance/valid");
let mut cases = std::fs::read_dir(&valid_root)?.collect::<Result<Vec<_>, _>>()?;
cases.sort_by_key(|entry| entry.file_name());
if cases.is_empty() {
return Err(format!("no valid fixtures found in {}", valid_root.display()).into());
}
for case in cases {
let source_path = case.path().join("source.stack");
if !source_path.is_file() {
continue;
}
let output = Engine::bundled().render(&std::fs::read(&source_path)?)?;
if output
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error)
{
return Err(
format!("{} produced an error diagnostic", source_path.display()).into(),
);
}
let svg = output
.svg
.ok_or_else(|| format!("{} produced no standalone SVG", source_path.display()))?;
assert!(svg.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
assert!(svg.ends_with("</svg>\n"));
}
Ok(())
}
#[test]
fn render_separates_invalid_input_from_success() -> Result<(), Box<dyn Error>> {
let engine = Engine::bundled();
let result = engine.render(b"\xff");
assert!(result.is_ok());
if let Ok(output) = result {
assert!(output.svg.is_none());
assert_eq!(output.diagnostics[0].code, "STK1001");
assert_eq!(output.metadata.language_version, None);
}
let output = engine.render(VALID_SOURCE)?;
assert!(output.diagnostics.is_empty());
assert!(
output
.svg
.as_deref()
.is_some_and(|svg| svg.contains("<svg"))
);
Ok(())
}
#[test]
fn provided_catalog_requires_usable_fallbacks_and_revision() {
let catalog = stack_theme::catalog().clone();
assert!(Engine::with_catalog(&catalog, stack_theme::CATALOG_REVISION).is_ok());
assert!(matches!(
Engine::with_catalog(&catalog, "sha256:NOT-A-DIGEST"),
Err(OperationalError::InvalidCatalog { .. })
));
let mut missing_theme = catalog.clone();
missing_theme.fallbacks.missing_theme_id = "missing".to_owned();
assert!(matches!(
Engine::with_catalog(&missing_theme, stack_theme::CATALOG_REVISION),
Err(OperationalError::InvalidCatalog { .. })
));
let mut missing_icon = catalog;
missing_icon.fallbacks.missing_icon_id = "missing".to_owned();
assert!(matches!(
Engine::with_catalog(&missing_icon, stack_theme::CATALOG_REVISION),
Err(OperationalError::InvalidCatalog { .. })
));
}
#[test]
fn diagnostic_conversion_keeps_expected_help_and_related_ranges() {
let start = compiler_diagnostic::SourcePosition {
byte_offset: 3,
line: 2,
column: 4,
};
let end = compiler_diagnostic::SourcePosition {
byte_offset: 7,
line: 2,
column: 8,
};
let diagnostic = compiler_diagnostic::Diagnostic {
code: "STK4002",
severity: compiler_diagnostic::Severity::Warning,
message: "warning".to_owned(),
span: compiler_diagnostic::Span { start, end },
expected: vec!["right".to_owned(), "down".to_owned()],
help: Some("help".to_owned()),
related: vec![compiler_diagnostic::RelatedInformation {
message: "related".to_owned(),
span: compiler_diagnostic::Span::point(start),
}],
};
let portable = Diagnostic::from(diagnostic);
assert_eq!(portable.severity, Severity::Warning);
assert_eq!(portable.expected, ["right", "down"]);
assert_eq!(portable.help.as_deref(), Some("help"));
assert_eq!(portable.related[0].message, "related");
assert_eq!(
portable.range.start,
SourcePosition {
byte_offset: 3,
line: 2,
column: 4,
}
);
}
#[test]
fn operational_error_messages_are_stable() {
assert_eq!(
OperationalError::InvalidCatalog { reason: "reason" }.to_string(),
"invalid theme catalog: reason"
);
assert_eq!(
OperationalError::InvalidIntermediateRepresentation { reason: "reason" }.to_string(),
"invalid intermediate representation: reason"
);
assert_eq!(
OperationalError::InvalidProviderPack { reason: "reason" }.to_string(),
"invalid provider pack: reason"
);
assert_eq!(
OperationalError::InvalidLanguageIntelligenceInput { reason: "reason" }.to_string(),
"invalid language-intelligence input: reason"
);
}
}