Skip to main content

rdx_transform/
lib.rs

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