Skip to main content

rdx_transform/
lib.rs

1pub use rdx_ast::*;
2pub use rdx_parser::parse;
3
4mod transforms;
5pub use transforms::slug::AutoSlug;
6pub use transforms::toc::TableOfContents;
7
8/// A transform that operates on an RDX AST in place.
9///
10/// Implement this trait to create custom RDX plugins. Transforms receive
11/// a mutable reference to the full document root and the original source text.
12///
13/// # Example
14///
15/// ```rust
16/// use rdx_transform::{Transform, Root};
17///
18/// struct MyPlugin;
19///
20/// impl Transform for MyPlugin {
21///     fn name(&self) -> &str { "my-plugin" }
22///     fn transform(&self, root: &mut Root, _source: &str) {
23///         // modify the AST
24///     }
25/// }
26/// ```
27pub trait Transform {
28    /// A short identifier for this transform (used in error messages / debugging).
29    fn name(&self) -> &str;
30
31    /// Apply the transform to the AST. `source` is the original document text,
32    /// available for transforms that need to reference raw content.
33    fn transform(&self, root: &mut Root, source: &str);
34}
35
36/// A composable pipeline that parses an RDX document and runs a chain of transforms.
37///
38/// # Example
39///
40/// ```rust
41/// use rdx_transform::{Pipeline, AutoSlug, TableOfContents};
42///
43/// let root = Pipeline::new()
44///     .add(AutoSlug::new())
45///     .add(TableOfContents::default())
46///     .run("# Hello\n\n## World\n");
47/// ```
48pub struct Pipeline {
49    transforms: Vec<Box<dyn Transform>>,
50}
51
52impl Pipeline {
53    pub fn new() -> Self {
54        Pipeline {
55            transforms: Vec::new(),
56        }
57    }
58
59    /// Append a transform to the pipeline. Transforms run in insertion order.
60    #[allow(clippy::should_implement_trait)]
61    pub fn add(mut self, transform: impl Transform + 'static) -> Self {
62        self.transforms.push(Box::new(transform));
63        self
64    }
65
66    /// Parse the input and run all transforms in order.
67    pub fn run(&self, input: &str) -> Root {
68        let mut root = parse(input);
69        for t in &self.transforms {
70            t.transform(&mut root, input);
71        }
72        root
73    }
74
75    /// Run transforms on an already-parsed AST.
76    pub fn apply(&self, root: &mut Root, source: &str) {
77        for t in &self.transforms {
78            t.transform(root, source);
79        }
80    }
81}
82
83impl Default for Pipeline {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89/// Convenience: parse + apply built-in transforms (slug + toc).
90pub fn parse_with_defaults(input: &str) -> Root {
91    Pipeline::new()
92        .add(AutoSlug::new())
93        .add(TableOfContents::default())
94        .run(input)
95}
96
97/// Walk all nodes in the AST, calling `f` on each with a mutable reference.
98/// Useful for implementing transforms.
99#[allow(clippy::ptr_arg)]
100pub fn walk_mut(nodes: &mut Vec<Node>, f: &mut dyn FnMut(&mut Node)) {
101    for node in nodes.iter_mut() {
102        f(node);
103        if let Some(children) = node.children_mut() {
104            walk_mut(children, f);
105        }
106    }
107}
108
109/// Walk all nodes immutably.
110pub fn walk<'a>(nodes: &'a [Node], f: &mut dyn FnMut(&'a Node)) {
111    for node in nodes {
112        f(node);
113        if let Some(children) = node.children() {
114            walk(children, f);
115        }
116    }
117}
118
119/// Extract plain text from a list of nodes (for generating slugs, alt text, etc).
120pub fn collect_text(nodes: &[Node]) -> String {
121    let mut out = String::new();
122    walk(nodes, &mut |node| {
123        if let Node::Text(t) = node {
124            out.push_str(&t.value);
125        }
126    });
127    out
128}