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}