Skip to main content

typub_passes/
lib.rs

1//! v2 semantic document passes.
2//!
3//! Passes operate on `Document` and may use sidecar context for execution-time
4//! metadata that must not leak into conformance IR.
5
6pub mod apply_node_policy;
7pub mod apply_resolved_publish_urls;
8pub mod flatten_svg;
9pub mod rasterize_svg_to_data_uri;
10pub mod rasterize_svg_to_local_asset;
11pub mod resolve_internal_links;
12mod shared;
13pub mod validate_document;
14pub mod walk;
15
16use anyhow::Result;
17use serde_json::Value as JsonValue;
18use std::collections::BTreeMap;
19
20use typub_ir::Document;
21
22/// Runtime context shared across passes.
23#[derive(Debug, Clone, Default, PartialEq)]
24pub struct PassCtx {
25    /// Structured diagnostics emitted by passes.
26    pub diagnostics: Vec<Diagnostic>,
27    /// Execution-only sidecar data. Keep conformance IR out of this map.
28    pub sidecar: BTreeMap<String, JsonValue>,
29}
30
31impl PassCtx {
32    pub fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
33        self.diagnostics.push(diagnostic);
34    }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum DiagnosticLevel {
39    Info,
40    Warning,
41    Error,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Diagnostic {
46    pub pass: &'static str,
47    pub level: DiagnosticLevel,
48    pub message: String,
49    pub location: Option<String>,
50}
51
52impl Diagnostic {
53    pub fn error(pass: &'static str, message: String, location: Option<String>) -> Self {
54        Self {
55            pass,
56            level: DiagnosticLevel::Error,
57            message,
58            location,
59        }
60    }
61}
62
63/// A semantic pass over v2 `Document`.
64pub trait Pass {
65    fn name(&self) -> &'static str;
66    fn run(&mut self, doc: &mut Document, ctx: &mut PassCtx) -> Result<()>;
67}
68
69pub use apply_node_policy::ApplyNodePolicyPass;
70pub use apply_resolved_publish_urls::ApplyResolvedPublishUrlsPass;
71pub use flatten_svg::FlattenSvgPass;
72pub use rasterize_svg_to_data_uri::RasterizeSvgToDataUriPass;
73pub use rasterize_svg_to_local_asset::{
74    RasterizeSvgToLocalAssetPass, SIDECAR_GENERATED_RENDER_ASSETS,
75};
76pub use resolve_internal_links::ResolveInternalLinksPass;
77pub use validate_document::ValidateDocumentPass;
78
79/// Run passes in order; stop at first pass error.
80pub fn run_passes(
81    doc: &mut Document,
82    ctx: &mut PassCtx,
83    passes: &mut [&mut dyn Pass],
84) -> Result<()> {
85    for pass in passes {
86        pass.run(doc, ctx)?;
87    }
88    Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93    #![allow(clippy::expect_used)]
94
95    use super::*;
96    use typub_ir::DocMeta;
97
98    struct MarkPass(&'static str);
99
100    impl Pass for MarkPass {
101        fn name(&self) -> &'static str {
102            self.0
103        }
104
105        fn run(&mut self, _doc: &mut Document, ctx: &mut PassCtx) -> Result<()> {
106            ctx.push_diagnostic(Diagnostic {
107                pass: self.name(),
108                level: DiagnosticLevel::Info,
109                message: "ran".to_string(),
110                location: None,
111            });
112            Ok(())
113        }
114    }
115
116    #[test]
117    fn run_passes_executes_in_order() {
118        let mut doc = Document {
119            blocks: Vec::new(),
120            footnotes: Default::default(),
121            assets: Default::default(),
122            meta: DocMeta::default(),
123        };
124        let mut ctx = PassCtx::default();
125        let mut pass_a = MarkPass("a");
126        let mut pass_b = MarkPass("b");
127        run_passes(&mut doc, &mut ctx, &mut [&mut pass_a, &mut pass_b]).expect("run passes");
128
129        assert_eq!(ctx.diagnostics.len(), 2);
130        assert_eq!(ctx.diagnostics[0].pass, "a");
131        assert_eq!(ctx.diagnostics[1].pass, "b");
132    }
133}