rdx_transform/transforms/
print_fallback.rs1use rdx_ast::*;
2
3use crate::{Transform, synthetic_pos};
4
5pub struct PrintFallback;
36
37impl Transform for PrintFallback {
38 fn name(&self) -> &str {
39 "print-fallback"
40 }
41
42 fn transform(&self, root: &mut Root, _source: &str) {
43 replace_fallbacks(&mut root.children);
44 }
45}
46
47fn looks_like_image(path: &str) -> bool {
49 let lower = path.to_ascii_lowercase();
50 lower.ends_with(".png")
51 || lower.ends_with(".jpg")
52 || lower.ends_with(".jpeg")
53 || lower.ends_with(".gif")
54 || lower.ends_with(".svg")
55 || lower.ends_with(".webp")
56}
57
58fn replace_fallbacks(nodes: &mut [Node]) {
59 for node in nodes.iter_mut() {
60 if let Node::Component(ref comp) = *node {
61 let fallback = comp.attributes.iter().find_map(|a| {
63 if a.name == "printFallback" {
64 if let AttributeValue::String(s) = &a.value {
65 Some(s.clone())
66 } else {
67 None
68 }
69 } else {
70 None
71 }
72 });
73
74 if let Some(fb) = fallback {
75 let replacement = if looks_like_image(&fb) {
76 Node::Image(ImageNode {
77 url: fb,
78 title: None,
79 alt: None,
80 children: vec![],
81 position: synthetic_pos(),
82 })
83 } else {
84 Node::Text(TextNode {
85 value: fb,
86 position: synthetic_pos(),
87 })
88 };
89 *node = replacement;
90 continue;
91 }
92 }
93
94 if let Some(children) = node.children_mut() {
96 replace_fallbacks(children);
97 }
98 }
99}
100
101#[cfg(test)]
106mod tests {
107 use super::*;
108 use rdx_parser::parse;
109
110 #[test]
111 fn text_fallback_replaces_component() {
112 let mut root = parse("<Widget printFallback=\"Widget content\" />\n");
113 PrintFallback.transform(&mut root, "");
114 match &root.children[0] {
115 Node::Text(t) => assert_eq!(t.value, "Widget content"),
116 other => panic!("Expected Text, got {:?}", other),
117 }
118 }
119
120 #[test]
121 fn image_fallback_for_png_path() {
122 let mut root = parse("<Chart printFallback=\"chart.png\" />\n");
123 PrintFallback.transform(&mut root, "");
124 match &root.children[0] {
125 Node::Image(i) => assert_eq!(i.url, "chart.png"),
126 other => panic!("Expected Image, got {:?}", other),
127 }
128 }
129
130 #[test]
131 fn image_fallback_for_svg_path() {
132 let mut root = parse("<Diagram printFallback=\"diagram.svg\" />\n");
133 PrintFallback.transform(&mut root, "");
134 match &root.children[0] {
135 Node::Image(i) => assert_eq!(i.url, "diagram.svg"),
136 other => panic!("Expected Image, got {:?}", other),
137 }
138 }
139
140 #[test]
141 fn image_fallback_for_jpg_path() {
142 let mut root = parse("<Photo printFallback=\"photo.jpg\" />\n");
143 PrintFallback.transform(&mut root, "");
144 match &root.children[0] {
145 Node::Image(i) => assert_eq!(i.url, "photo.jpg"),
146 other => panic!("Expected Image, got {:?}", other),
147 }
148 }
149
150 #[test]
151 fn no_fallback_attribute_keeps_component() {
152 let mut root = parse("<Widget />\n");
153 PrintFallback.transform(&mut root, "");
154 match &root.children[0] {
155 Node::Component(c) => assert_eq!(c.name, "Widget"),
156 other => panic!("Expected Component, got {:?}", other),
157 }
158 }
159
160 #[test]
161 fn nested_fallback_replaced() {
162 let mut root = parse(
163 "<Outer>\n\
164 <Chart printFallback=\"inner.png\" />\n\
165 </Outer>\n",
166 );
167 PrintFallback.transform(&mut root, "");
168 match &root.children[0] {
169 Node::Component(outer) => {
170 assert_eq!(outer.name, "Outer");
171 match &outer.children[0] {
172 Node::Image(i) => assert_eq!(i.url, "inner.png"),
173 other => panic!("Expected Image inside Outer, got {:?}", other),
174 }
175 }
176 other => panic!("Expected Outer component, got {:?}", other),
177 }
178 }
179
180 #[test]
181 fn case_insensitive_extension_check() {
182 let mut root = parse("<Chart printFallback=\"chart.PNG\" />\n");
184 PrintFallback.transform(&mut root, "");
185 assert!(
186 matches!(&root.children[0], Node::Image(_)),
187 "Expected Image for .PNG extension"
188 );
189 }
190}