Skip to main content

dmc_transform/
pipeline.rs

1use crate::config::PipelineConfig;
2use dmc_diagnostic::{Code, metadata::SourceMeta};
3use dmc_parser::ast::Document;
4use duck_diagnostic::DiagnosticEngine;
5
6/// One AST-to-AST pass. `transform` takes `&self` so a transformer is cheap
7/// to share across threads or reuse across files.
8pub trait Transformer {
9  /// Stable identifier for logging / error reporting.
10  fn name(&self) -> &str {
11    "anonymous"
12  }
13  /// Mutate `doc` in place. May be a no-op when preconditions (config,
14  /// environment, feature flags) aren't met.
15  fn transform(&self, doc: &mut Document, meta: &SourceMeta, diag_engine: &mut DiagnosticEngine<Code>);
16}
17
18/// Ordered list of transformers run in registration order. Boxed + `Send +
19/// Sync` so a `Pipeline` can be shared across worker threads.
20#[derive(Default)]
21pub struct Pipeline {
22  transformers: Vec<Box<dyn Transformer + Send + Sync>>,
23}
24
25impl Pipeline {
26  pub fn new() -> Self {
27    Self { transformers: Vec::new() }
28  }
29
30  /// Append `t` to the run order. Returns `self` for builder chaining.
31  #[allow(clippy::should_implement_trait)]
32  pub fn add<T: Transformer + Send + Sync + 'static>(mut self, t: T) -> Self {
33    self.transformers.push(Box::new(t));
34    self
35  }
36
37  /// Default pipeline. Equivalent to `with_defaults_for(&PipelineConfig::default())`.
38  pub fn with_defaults() -> Self {
39    Self::with_defaults_for(&PipelineConfig::default())
40  }
41
42  /// Build the default pipeline tuned by `cfg`. Single uniform place where
43  /// every config-dependent and feature-gated transformer is wired up:
44  /// callers don't sprinkle `cfg!(feature = ...)` of their own.
45  pub fn with_defaults_for(cfg: &PipelineConfig) -> Self {
46    #[allow(unused_mut)]
47    let mut p = Self::new().add(crate::CodeImport::new()).add(crate::BareUrlAutolink);
48    if cfg.autolink_headings != Some(false) {
49      p = p.add(crate::AutolinkHeadings::new());
50    }
51
52    if cfg.markdown_gfm == Some(false) {
53      p = p.add(crate::DisableGfm);
54    }
55
56    #[cfg(feature = "npm-command")]
57    {
58      p = p.add(crate::NpmCommand);
59    }
60
61    #[cfg(feature = "mermaid")]
62    {
63      p = p.add(crate::Mermaid::default());
64    }
65
66    #[cfg(feature = "emoji")]
67    {
68      if cfg.emoji != Some(false) {
69        p = p.add(crate::Emoji);
70      }
71    }
72
73    #[cfg(feature = "math")]
74    {
75      if let Some(engine) = cfg.math_engine {
76        crate::Math::set_engine(engine);
77      }
78      if cfg.math != Some(false) {
79        p = p.add(crate::Math);
80      }
81    }
82
83    #[cfg(feature = "pretty-code")]
84    {
85      if cfg.pretty_code_enabled != Some(false) {
86        let pc = cfg.pretty_code.as_ref().map(crate::PrettyCode::from_options).unwrap_or_default();
87        p = p.add(pc);
88      }
89    }
90
91    #[cfg(feature = "assets")]
92    if let Some(opts) = &cfg.copy_linked_files {
93      p =
94        p.add(crate::CopyLinkedFiles::new(opts.source_dir.clone(), opts.assets_dir.clone(), opts.public_base.clone()));
95    }
96
97    p
98  }
99
100  /// Apply every registered transformer to `doc` in registration order.
101  pub fn run(&self, doc: &mut Document, meta: &SourceMeta, engine: &'_ mut DiagnosticEngine<Code>) {
102    for t in &self.transformers {
103      t.transform(doc, meta, engine);
104    }
105  }
106
107  /// Run with a synthesised `Origin::Inline` meta and a throwaway engine,
108  /// discarding diagnostics. For tests + tooling without a `SourceMeta`.
109  pub fn run_silent(&self, doc: &mut Document) {
110    use dmc_diagnostic::metadata::Origin;
111    use std::sync::Arc;
112    let meta = SourceMeta { path: Arc::from("<test>"), origin: Origin::Inline("<test>") };
113    let mut engine = DiagnosticEngine::new();
114    self.run(doc, &meta, &mut engine);
115  }
116}