Skip to main content

rdx_transform/transforms/
strip_target.rs

1use rdx_ast::*;
2
3use crate::Transform;
4
5// ---------------------------------------------------------------------------
6// Transform
7// ---------------------------------------------------------------------------
8
9/// Removes nodes that are targeted at an output that does not match the
10/// current render target.
11///
12/// Rules applied to every [`Node::Component`]:
13///
14/// | Component name / attribute | `target="web"` | `target="print"` |
15/// |---------------------------|---------------|-----------------|
16/// | `target="web"` attr       | **kept**      | removed          |
17/// | `target="print"` attr     | removed       | **kept**          |
18/// | `target="all"` or absent  | **kept**      | **kept**          |
19/// | Name == `WebOnly`         | **kept**      | removed          |
20/// | Name == `PrintOnly`       | removed       | **kept**          |
21///
22/// The component is removed by filtering it out of its parent's `children`
23/// vector.  All other node types are kept and recursed into.
24///
25/// # Example
26///
27/// ```rust
28/// use rdx_transform::{StripTarget, Transform, parse};
29///
30/// let mut root = parse("<WebOnly>\nweb content\n</WebOnly>\n<PrintOnly>\nprint\n</PrintOnly>\n");
31/// StripTarget { target: "web".into() }.transform(&mut root, "");
32/// // Only the WebOnly component remains.
33/// assert_eq!(root.children.len(), 1);
34/// ```
35pub struct StripTarget {
36    /// The current output target, e.g. `"web"` or `"print"`.
37    pub target: String,
38}
39
40/// Returns `true` if this component should be **removed** for `current_target`.
41fn should_strip(comp: &ComponentNode, current_target: &str) -> bool {
42    // Special component names take precedence.
43    match comp.name.as_str() {
44        "WebOnly" => return current_target != "web",
45        "PrintOnly" => return current_target != "print",
46        _ => {}
47    }
48
49    // Fall back to the `target` attribute.
50    let target_attr = comp.attributes.iter().find_map(|a| {
51        if a.name == "target" {
52            if let AttributeValue::String(s) = &a.value {
53                Some(s.as_str())
54            } else {
55                None
56            }
57        } else {
58            None
59        }
60    });
61
62    match target_attr {
63        Some("all") | None => false,
64        Some(t) => t != current_target,
65    }
66}
67
68impl Transform for StripTarget {
69    fn name(&self) -> &str {
70        "strip-target"
71    }
72
73    fn transform(&self, root: &mut Root, _source: &str) {
74        strip_nodes(&mut root.children, &self.target);
75    }
76}
77
78fn strip_nodes(nodes: &mut Vec<Node>, current_target: &str) {
79    nodes.retain_mut(|node| {
80        if let Node::Component(comp) = &*node
81            && should_strip(comp, current_target)
82        {
83            return false; // Drop this node.
84        }
85        true
86    });
87
88    // Now recurse into surviving nodes' children.
89    for node in nodes.iter_mut() {
90        if let Some(children) = node.children_mut() {
91            strip_nodes(children, current_target);
92        }
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Tests
98// ---------------------------------------------------------------------------
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use rdx_parser::parse;
104
105    #[test]
106    fn web_mode_strips_print_only() {
107        let mut root = parse(
108            "<PrintOnly>\nprint content\n</PrintOnly>\n\
109             <WebOnly>\nweb content\n</WebOnly>\n",
110        );
111        StripTarget {
112            target: "web".into(),
113        }
114        .transform(&mut root, "");
115        assert_eq!(root.children.len(), 1, "Only WebOnly should remain");
116        match &root.children[0] {
117            Node::Component(c) => assert_eq!(c.name, "WebOnly"),
118            other => panic!("Expected WebOnly, got {:?}", other),
119        }
120    }
121
122    #[test]
123    fn print_mode_strips_web_only() {
124        let mut root = parse(
125            "<PrintOnly>\nprint content\n</PrintOnly>\n\
126             <WebOnly>\nweb content\n</WebOnly>\n",
127        );
128        StripTarget {
129            target: "print".into(),
130        }
131        .transform(&mut root, "");
132        assert_eq!(root.children.len(), 1, "Only PrintOnly should remain");
133        match &root.children[0] {
134            Node::Component(c) => assert_eq!(c.name, "PrintOnly"),
135            other => panic!("Expected PrintOnly, got {:?}", other),
136        }
137    }
138
139    #[test]
140    fn target_attribute_web_stripped_in_print() {
141        let mut root = parse("<Note target=\"web\">\ncontent\n</Note>\n");
142        StripTarget {
143            target: "print".into(),
144        }
145        .transform(&mut root, "");
146        assert!(
147            root.children.is_empty(),
148            "web-targeted Note should be removed in print mode"
149        );
150    }
151
152    #[test]
153    fn target_attribute_print_kept_in_print() {
154        let mut root = parse("<Note target=\"print\">\ncontent\n</Note>\n");
155        StripTarget {
156            target: "print".into(),
157        }
158        .transform(&mut root, "");
159        assert_eq!(root.children.len(), 1);
160    }
161
162    #[test]
163    fn target_all_always_kept() {
164        let mut root = parse("<Note target=\"all\">\ncontent\n</Note>\n");
165        StripTarget {
166            target: "web".into(),
167        }
168        .transform(&mut root, "");
169        assert_eq!(root.children.len(), 1);
170        StripTarget {
171            target: "print".into(),
172        }
173        .transform(&mut root, "");
174        assert_eq!(root.children.len(), 1);
175    }
176
177    #[test]
178    fn no_target_attribute_always_kept() {
179        let mut root = parse("<Notice>\ncontent\n</Notice>\n");
180        StripTarget {
181            target: "web".into(),
182        }
183        .transform(&mut root, "");
184        assert_eq!(root.children.len(), 1);
185    }
186
187    #[test]
188    fn nested_strip_works() {
189        // A neutral outer component contains a PrintOnly child.
190        let mut root = parse(
191            "<Notice>\n\
192             <PrintOnly>\nprint\n</PrintOnly>\n\
193             Keep this.\n\
194             </Notice>\n",
195        );
196        StripTarget {
197            target: "web".into(),
198        }
199        .transform(&mut root, "");
200        // Outer Notice must still be present.
201        assert_eq!(root.children.len(), 1);
202        match &root.children[0] {
203            Node::Component(c) => {
204                // PrintOnly child should have been stripped.
205                let has_print_only = c
206                    .children
207                    .iter()
208                    .any(|n| matches!(n, Node::Component(inner) if inner.name == "PrintOnly"));
209                assert!(
210                    !has_print_only,
211                    "PrintOnly should be stripped from nested position"
212                );
213            }
214            other => panic!("Expected Notice, got {:?}", other),
215        }
216    }
217
218    #[test]
219    fn both_kept_when_target_matches() {
220        let mut root = parse(
221            "<WebOnly>\nweb\n</WebOnly>\n\
222             <PrintOnly>\nprint\n</PrintOnly>\n",
223        );
224        // Neither should be stripped because there are no conflicting targets.
225        // Run with a third unknown target: both should be stripped.
226        StripTarget {
227            target: "pdf".into(),
228        }
229        .transform(&mut root, "");
230        assert!(
231            root.children.is_empty(),
232            "Both WebOnly and PrintOnly should be stripped for unknown target"
233        );
234    }
235}