sgmlish/transforms/
transform.rs

1use crate::{SgmlEvent, SgmlFragment};
2
3/// A convenience helper to insert and remove events from a [`SgmlFragment`].
4#[derive(Clone, Debug, Default)]
5pub struct Transform<'a> {
6    insertions: Vec<(usize, SgmlEvent<'a>)>,
7    deletions: Vec<usize>,
8}
9
10impl<'a> Transform<'a> {
11    /// Creates a new empty `Transform`.
12    pub fn new() -> Self {
13        Default::default()
14    }
15
16    /// Returns `true` if no operations were recorded.
17    pub fn is_empty(&self) -> bool {
18        self.insertions.is_empty() && self.deletions.is_empty()
19    }
20
21    /// Removes the event at the given position.
22    ///
23    /// The position is always relative to the original list --- you should not
24    /// adjust indices to acommodate other insertions or removals.
25    ///
26    /// Removing the same `index` multiple times is a no-op, since indices are not readjusted.
27    ///
28    /// # Example
29    ///
30    /// ```rust
31    /// # use sgmlish::{SgmlEvent, SgmlFragment};
32    /// # use sgmlish::transforms::Transform;
33    /// let fragment = SgmlFragment::from(vec![
34    ///     /* 0 */ SgmlEvent::OpenStartTag { name: "A".into() },
35    ///     /* 1 */ SgmlEvent::Attribute { name: "HREF".into(), value: Some("/".into()) },
36    ///     /* 2 */ SgmlEvent::CloseStartTag,
37    ///     /* 3 */ SgmlEvent::Character("hello".into()),
38    ///     /* 4 */ SgmlEvent::EndTag { name: "A".into() },
39    ///     /* 5 */ SgmlEvent::Character("!".into()),
40    ///     /* 6 */
41    /// ]);
42    ///
43    /// let mut transform = Transform::new();
44    /// // Remove the attribute
45    /// transform.remove_at(1);
46    /// // Remove the final exclamation mark
47    /// transform.remove_at(5);
48    /// let result = transform.apply(fragment);
49    ///
50    /// assert_eq!(
51    ///     result.into_vec(),
52    ///     vec![
53    ///         SgmlEvent::OpenStartTag { name: "A".into() },
54    ///         SgmlEvent::CloseStartTag,
55    ///         SgmlEvent::Character("hello".into()),
56    ///         SgmlEvent::EndTag { name: "A".into() },
57    ///     ]
58    /// );
59    /// ```
60    pub fn remove_at(&mut self, index: usize) {
61        self.deletions.push(index);
62    }
63
64    /// Inserts an event at the given position.
65    ///
66    /// The position is always relative to the original list --- you should not
67    /// adjust indices to acommodate other insertions or removals.
68    ///
69    /// Inserting multiple events at the same `index` will place them in the order they were passed.
70    ///
71    /// # Example
72    ///
73    /// ```rust
74    /// # use sgmlish::{SgmlEvent, SgmlFragment};
75    /// # use sgmlish::transforms::Transform;
76    /// let fragment = SgmlFragment::from(vec![
77    ///     /* 0 */ SgmlEvent::OpenStartTag { name: "A".into() },
78    ///     /* 1 */ SgmlEvent::Attribute { name: "HREF".into(), value: Some("/".into()) },
79    ///     /* 2 */ SgmlEvent::CloseStartTag,
80    ///     /* 3 */ SgmlEvent::Character("hello".into()),
81    ///     /* 4 */
82    /// ]);
83    ///
84    /// let mut transform = Transform::new();
85    /// // Insert another attribute
86    /// transform.insert_at(2, SgmlEvent::Attribute { name: "TARGET".into(), value: Some("_blank".into()) });
87    /// // Insert end tag
88    /// transform.insert_at(4, SgmlEvent::EndTag { name: "A".into() });
89    /// let result = transform.apply(fragment);
90    ///
91    /// assert_eq!(
92    ///     result.into_vec(),
93    ///     vec![
94    ///         SgmlEvent::OpenStartTag { name: "A".into() },
95    ///         SgmlEvent::Attribute { name: "HREF".into(), value: Some("/".into()) },
96    ///         SgmlEvent::Attribute { name: "TARGET".into(), value: Some("_blank".into()) },
97    ///         SgmlEvent::CloseStartTag,
98    ///         SgmlEvent::Character("hello".into()),
99    ///         SgmlEvent::EndTag { name: "A".into() },
100    ///     ]
101    /// );
102    /// ```
103    pub fn insert_at(&mut self, index: usize, event: SgmlEvent<'a>) {
104        self.insertions.push((index, event));
105    }
106
107    /// Applies the recorded changes to the given fragment.
108    pub fn apply(self, fragment: SgmlFragment<'a>) -> SgmlFragment<'a> {
109        if self.is_empty() {
110            return fragment;
111        }
112
113        let mut deletions = self.deletions;
114        deletions.sort_unstable();
115        deletions.dedup();
116        let mut deletions = deletions.into_iter().peekable();
117
118        let mut insertions = self.insertions;
119        insertions.sort_by_key(|(pos, _)| *pos);
120        let mut insertions = insertions.into_iter().peekable();
121
122        let final_size = fragment.len().saturating_sub(deletions.len()) + insertions.len();
123        let mut result = Vec::with_capacity(final_size);
124
125        for (i, event) in fragment.into_iter().enumerate() {
126            while let Some((_, event_to_insert)) =
127                insertions.next_if(|(index_to_insert, _)| *index_to_insert == i)
128            {
129                result.push(event_to_insert);
130            }
131
132            if deletions.next_if_eq(&i).is_none() {
133                result.push(event);
134            }
135        }
136
137        // Insert remaining events at the end
138        result.extend(insertions.map(|(_, event)| event));
139
140        result.into()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_insert_multiple_times_same_index() {
150        let fragment = SgmlFragment::from(vec![
151            SgmlEvent::OpenStartTag { name: "IMG".into() },
152            SgmlEvent::Attribute {
153                name: "SRC".into(),
154                value: Some("example.gif".into()),
155            },
156            SgmlEvent::CloseStartTag,
157        ]);
158
159        let mut transform = Transform::new();
160        transform.insert_at(
161            2,
162            SgmlEvent::Attribute {
163                name: "BORDER".into(),
164                value: Some("0".into()),
165            },
166        );
167        transform.insert_at(
168            2,
169            SgmlEvent::Attribute {
170                name: "ISMAP".into(),
171                value: None,
172            },
173        );
174
175        let result = transform.apply(fragment);
176
177        assert_eq!(
178            result,
179            SgmlFragment::from(vec![
180                SgmlEvent::OpenStartTag { name: "IMG".into() },
181                SgmlEvent::Attribute {
182                    name: "SRC".into(),
183                    value: Some("example.gif".into()),
184                },
185                SgmlEvent::Attribute {
186                    name: "BORDER".into(),
187                    value: Some("0".into()),
188                },
189                SgmlEvent::Attribute {
190                    name: "ISMAP".into(),
191                    value: None,
192                },
193                SgmlEvent::CloseStartTag,
194            ])
195        );
196    }
197
198    #[test]
199    fn test_remove_multiple_times_same_index() {
200        let fragment = SgmlFragment::from(vec![
201            SgmlEvent::OpenStartTag { name: "IMG".into() },
202            SgmlEvent::Attribute {
203                name: "SRC".into(),
204                value: Some("example.gif".into()),
205            },
206            SgmlEvent::Attribute {
207                name: "BORDER".into(),
208                value: Some("0".into()),
209            },
210            SgmlEvent::Attribute {
211                name: "ISMAP".into(),
212                value: None,
213            },
214            SgmlEvent::CloseStartTag,
215        ]);
216
217        let mut transform = Transform::new();
218        transform.remove_at(2);
219        transform.remove_at(3);
220        transform.remove_at(3);
221
222        let result = transform.apply(fragment);
223
224        assert_eq!(
225            result,
226            SgmlFragment::from(vec![
227                SgmlEvent::OpenStartTag { name: "IMG".into() },
228                SgmlEvent::Attribute {
229                    name: "SRC".into(),
230                    value: Some("example.gif".into()),
231                },
232                SgmlEvent::CloseStartTag,
233            ])
234        );
235    }
236
237    #[test]
238    fn test_insert_remove_at_same_index() {
239        let fragment = SgmlFragment::from(vec![
240            SgmlEvent::OpenStartTag { name: "A".into() },
241            SgmlEvent::Attribute {
242                name: "HREF".into(),
243                value: Some("/".into()),
244            },
245            SgmlEvent::CloseStartTag,
246            SgmlEvent::Character("hello".into()),
247            SgmlEvent::EndTag { name: "A".into() },
248        ]);
249
250        let mut transform = Transform::new();
251        transform.insert_at(
252            1,
253            SgmlEvent::Attribute {
254                name: "NAME".into(),
255                value: Some("greeting".into()),
256            },
257        );
258        transform.remove_at(1);
259        let result = transform.apply(fragment);
260
261        assert_eq!(
262            result,
263            SgmlFragment::from(vec![
264                SgmlEvent::OpenStartTag { name: "A".into() },
265                SgmlEvent::Attribute {
266                    name: "NAME".into(),
267                    value: Some("greeting".into()),
268                },
269                SgmlEvent::CloseStartTag,
270                SgmlEvent::Character("hello".into()),
271                SgmlEvent::EndTag { name: "A".into() },
272            ])
273        );
274    }
275}