rdx_transform/transforms/
cross_ref_resolve.rs1use rdx_ast::*;
2
3use crate::transforms::auto_number::{NumberEntry, NumberRegistry};
4use crate::{Transform, synthetic_pos};
5
6pub struct CrossRefResolve {
43 pub registry: NumberRegistry,
44 pub target: String,
46}
47
48impl CrossRefResolve {
49 pub fn new(registry: NumberRegistry, target: impl Into<String>) -> Self {
50 CrossRefResolve {
51 registry,
52 target: target.into(),
53 }
54 }
55}
56
57impl Transform for CrossRefResolve {
58 fn name(&self) -> &str {
59 "cross-ref-resolve"
60 }
61
62 fn transform(&self, root: &mut Root, _source: &str) {
63 resolve_nodes(&mut root.children, &self.registry, &self.target);
64 }
65}
66
67fn display_text(entry: &NumberEntry) -> String {
72 format!("{} {}", entry.kind, entry.number)
73}
74
75fn resolve_nodes(nodes: &mut [Node], registry: &NumberRegistry, target: &str) {
80 let mut i = 0;
81 while i < nodes.len() {
82 let replacement = if let Node::CrossRef(ref cr) = nodes[i] {
83 if let Some(entry) = registry.entries.get(&cr.target) {
84 let text = display_text(entry);
85 if target == "print" {
86 Some(Node::Text(TextNode {
87 value: text,
88 position: synthetic_pos(),
89 }))
90 } else {
91 Some(Node::Link(LinkNode {
93 url: format!("#{}", cr.target),
94 title: None,
95 children: vec![Node::Text(TextNode {
96 value: text,
97 position: synthetic_pos(),
98 })],
99 position: synthetic_pos(),
100 }))
101 }
102 } else {
103 None }
105 } else {
106 None
107 };
108
109 if let Some(new_node) = replacement {
110 nodes[i] = new_node;
111 } else {
112 if let Some(children) = nodes[i].children_mut() {
114 resolve_nodes(children, registry, target);
115 }
116 }
117 i += 1;
118 }
119}
120
121#[cfg(test)]
126mod tests {
127 use std::collections::HashMap;
128
129 use super::*;
130 use crate::transforms::auto_number::NumberEntry;
131
132 fn make_registry(entries: Vec<(&str, &str, &str)>) -> NumberRegistry {
133 let mut map = HashMap::new();
134 for (label, kind, number) in entries {
135 map.insert(
136 label.to_string(),
137 NumberEntry {
138 kind: kind.to_string(),
139 number: number.to_string(),
140 title: None,
141 },
142 );
143 }
144 NumberRegistry { entries: map }
145 }
146
147 fn cross_ref_node(target: &str) -> Node {
148 Node::CrossRef(CrossRefNode {
149 target: target.to_string(),
150 position: Position {
151 start: Point {
152 line: 1,
153 column: 1,
154 offset: 0,
155 },
156 end: Point {
157 line: 1,
158 column: 1,
159 offset: 0,
160 },
161 },
162 })
163 }
164
165 fn root_with_children(children: Vec<Node>) -> Root {
166 Root {
167 node_type: RootType::Root,
168 frontmatter: None,
169 children,
170 position: Position {
171 start: Point {
172 line: 1,
173 column: 1,
174 offset: 0,
175 },
176 end: Point {
177 line: 1,
178 column: 1,
179 offset: 0,
180 },
181 },
182 }
183 }
184
185 #[test]
186 fn resolves_known_ref_to_link_for_web() {
187 let registry = make_registry(vec![("fig:arch", "Figure", "1")]);
188 let resolver = CrossRefResolve::new(registry, "web");
189
190 let mut root = root_with_children(vec![cross_ref_node("fig:arch")]);
191 resolver.transform(&mut root, "");
192
193 match &root.children[0] {
194 Node::Link(l) => {
195 assert_eq!(l.url, "#fig:arch");
196 match &l.children[0] {
197 Node::Text(t) => assert_eq!(t.value, "Figure 1"),
198 other => panic!("Expected text child, got {:?}", other),
199 }
200 }
201 other => panic!("Expected Link, got {:?}", other),
202 }
203 }
204
205 #[test]
206 fn resolves_known_ref_to_text_for_print() {
207 let registry = make_registry(vec![("thm:main", "Theorem", "3")]);
208 let resolver = CrossRefResolve::new(registry, "print");
209
210 let mut root = root_with_children(vec![cross_ref_node("thm:main")]);
211 resolver.transform(&mut root, "");
212
213 match &root.children[0] {
214 Node::Text(t) => assert_eq!(t.value, "Theorem 3"),
215 other => panic!("Expected Text, got {:?}", other),
216 }
217 }
218
219 #[test]
220 fn unknown_ref_left_unchanged() {
221 let registry = make_registry(vec![]);
222 let resolver = CrossRefResolve::new(registry, "web");
223
224 let mut root = root_with_children(vec![cross_ref_node("fig:unknown")]);
225 resolver.transform(&mut root, "");
226
227 assert!(
228 matches!(&root.children[0], Node::CrossRef(cr) if cr.target == "fig:unknown"),
229 "Expected CrossRef to remain, got {:?}",
230 root.children[0]
231 );
232 }
233
234 #[test]
235 fn resolves_cross_ref_nested_in_paragraph() {
236 let registry = make_registry(vec![("eq:euler", "Equation", "2")]);
237 let resolver = CrossRefResolve::new(registry, "web");
238
239 let para = Node::Paragraph(StandardBlockNode {
240 depth: None,
241 ordered: None,
242 checked: None,
243 id: None,
244 children: vec![cross_ref_node("eq:euler")],
245 position: Position {
246 start: Point {
247 line: 1,
248 column: 1,
249 offset: 0,
250 },
251 end: Point {
252 line: 1,
253 column: 1,
254 offset: 0,
255 },
256 },
257 });
258
259 let mut root = root_with_children(vec![para]);
260 resolver.transform(&mut root, "");
261
262 match &root.children[0] {
263 Node::Paragraph(p) => match &p.children[0] {
264 Node::Link(l) => {
265 assert_eq!(l.url, "#eq:euler");
266 }
267 other => panic!("Expected link inside paragraph, got {:?}", other),
268 },
269 other => panic!("Expected paragraph, got {:?}", other),
270 }
271 }
272
273 #[test]
274 fn multiple_refs_resolved_independently() {
275 let registry = make_registry(vec![("fig:a", "Figure", "1"), ("fig:b", "Figure", "2")]);
276 let resolver = CrossRefResolve::new(registry, "web");
277
278 let mut root = root_with_children(vec![
279 cross_ref_node("fig:a"),
280 cross_ref_node("fig:b"),
281 cross_ref_node("fig:unknown"),
282 ]);
283 resolver.transform(&mut root, "");
284
285 assert!(matches!(&root.children[0], Node::Link(l) if l.url == "#fig:a"));
286 assert!(matches!(&root.children[1], Node::Link(l) if l.url == "#fig:b"));
287 assert!(matches!(&root.children[2], Node::CrossRef(_)));
288 }
289}