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}