Skip to main content

rbook/ebook/element/
write.rs

1use crate::ebook::element::{Attribute, Attributes, Properties};
2use crate::util::str::StringExt;
3use std::iter::FusedIterator;
4
5impl Properties {
6    /// Returns [`Some`] if populated, otherwise [`None`].
7    pub(crate) fn as_option_str(&self) -> Option<&str> {
8        (!self.0.is_empty()).then_some(self.0.as_str())
9    }
10
11    /// Inserts one or more properties, returning `true` if at least one new property was added.
12    /// `false` is returned if all given properties are already present.
13    ///
14    /// The given input is split by whitespace and each property is inserted individually.
15    ///
16    /// # Examples
17    /// - Inserting multiple properties:
18    /// ```
19    /// # use rbook::ebook::element::Attribute;
20    /// # let mut attribute = Attribute::new("properties", "");
21    /// # let mut properties = attribute.as_properties_mut();
22    ///
23    /// // Inserting multiple properties
24    /// # // `assert_eq` is used over `assert` as it is more readable
25    /// assert_eq!(true, properties.insert("nav scripted"));
26    /// assert_eq!("nav scripted", properties.as_str());
27    ///
28    /// // Inserting an existing and new property
29    /// // Returns `true` as `cover` is new (`nav is skipped)
30    /// assert_eq!(true, properties.insert("nav cover"));
31    /// assert_eq!("nav scripted cover", properties.as_str());
32    ///
33    /// // Inserting an already existing property
34    /// // Returns `false` as `scripted` is already present
35    /// assert_eq!(false, properties.insert("scripted"));
36    /// assert_eq!("nav scripted cover", properties.as_str());
37    /// ```
38    pub fn insert(&mut self, properties: &str) -> bool {
39        let mut any_inserted = false;
40
41        for property in properties.split_whitespace() {
42            if !self.has_property(property) {
43                any_inserted = true;
44
45                if !self.0.is_empty() {
46                    self.0.push(' ');
47                }
48                self.0.push_str(property);
49            }
50        }
51        any_inserted
52    }
53
54    /// Removes one or more properties, returning `true` if at least one property was removed.
55    /// `false` is returned if all given properties are not present.
56    ///
57    /// The given input is split by whitespace and each property is removed individually.
58    ///
59    /// # Examples
60    /// - Removing multiple properties:
61    /// ```
62    /// # use rbook::ebook::element::Attribute;
63    /// # let mut attribute = Attribute::new("properties", "nav scripted cover");
64    /// # let mut properties = attribute.as_properties_mut();
65    ///
66    /// // Initial state
67    /// assert_eq!("nav scripted cover", properties.as_str());
68    ///
69    /// assert_eq!(false, properties.remove("script"));
70    /// assert_eq!("nav scripted cover", properties.as_str());
71    ///
72    /// // Removing an existing property
73    /// // Returns `true` as the property was present and now removed
74    /// assert_eq!(true, properties.remove("scripted"));
75    /// assert_eq!("nav cover", properties.as_str());
76    ///
77    /// // Removing multiple properties
78    /// assert_eq!(true, properties.remove("nav scripted cover"));
79    /// assert!(properties.is_empty());
80    /// ```
81    pub fn remove(&mut self, properties: &str) -> bool {
82        if self.is_empty() {
83            return false;
84        }
85        let mut any_removed = false;
86
87        for property in properties.split_whitespace() {
88            // Check if the property to remove exists
89            // - In most, if not nearly all cases, `self` is empty or contains one property
90            let match_pos = self.0.match_indices(property).find(|&(start, _)| {
91                let end = start + property.len();
92                // Check spacing to ensure a proper space-separated match
93                let leading_ok = start == 0 || self.0.as_bytes()[start - 1] == b' ';
94                let trailing_ok = end == self.0.len() || self.0.as_bytes()[end] == b' ';
95                leading_ok && trailing_ok
96            });
97
98            if let Some((start, _)) = match_pos {
99                let end = start + property.len();
100                any_removed = true;
101
102                if end < self.0.len() {
103                    // Has a trailing space: "xyz "
104                    self.0.drain(start..=end);
105                } else if start > 0 {
106                    // Has a leading space: " xyz"
107                    self.0.drain(start - 1..end);
108                } else {
109                    // The only property
110                    self.0.clear();
111                }
112            }
113        }
114        any_removed
115    }
116
117    /// Removes all properties.
118    pub fn clear(&mut self) {
119        self.0.clear();
120    }
121}
122
123impl Attribute {
124    /// Creates a new attribute with the given [`name`](Self::name) and [`value`](Self::value).
125    ///
126    /// The given input has their leading and trailing whitespace trimmed in-place.
127    ///
128    /// The value is stored as plain text (e.g. `"1 < 2 & 3"`)
129    /// and is XML-escaped automatically during [writing](crate::Epub::write).
130    ///
131    /// # Examples
132    /// - Creating an attribute:
133    /// ```
134    /// # use rbook::ebook::element::Attribute;
135    /// let attribute = Attribute::new("rbook:val", " 123 ");
136    /// let name = attribute.name();
137    /// assert_eq!("rbook:val", name);
138    /// assert_eq!(Some("rbook"), name.prefix());
139    /// assert_eq!("val", name.local());
140    /// assert_eq!("123", attribute.value());
141    ///
142    /// let into_attribute: Attribute = (" val ", "456").into();
143    /// assert_eq!("val", into_attribute.name());
144    /// assert_eq!("456", into_attribute.value());
145    /// ```
146    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
147        Self::create(name, value)
148    }
149
150    /// Sets the attribute value and returns the previous value.
151    ///
152    /// The given `value` has its leading and trailing whitespace trimmed in-place.
153    ///
154    /// The value is stored as plain text (e.g. `"1 < 2 & 3"`)
155    /// and is XML-escaped automatically during [writing](crate::Epub::write).
156    ///
157    /// # Examples
158    /// - Setting an attribute value:
159    /// ```
160    /// # use rbook::ebook::element::Attribute;
161    /// # fn main() {
162    /// let mut attribute = Attribute::new("val", "123");
163    /// assert_eq!("123", attribute.value());
164    ///
165    /// attribute.set_value("\t 456 \n");
166    /// assert_eq!("456", attribute.value());
167    /// # }
168    /// ```
169    pub fn set_value(&mut self, value: impl Into<String>) -> String {
170        let mut value = value.into();
171        value.trim_in_place();
172        std::mem::replace(&mut self.value.0, value)
173    }
174
175    /// The attribute [`value`](Self::value) in the form of mutable [`Properties`].
176    ///
177    /// # Examples
178    /// - Modifying an attribute value as a list of properties:
179    /// ```
180    /// # use rbook::ebook::element::Attribute;
181    /// let mut attribute = Attribute::new("class", "title main section-1");
182    ///
183    /// let properties = attribute.as_properties_mut();
184    /// properties.insert("new title");
185    /// properties.insert("section-1");
186    /// properties.remove("main");
187    ///
188    /// assert_eq!("title section-1 new", attribute.value());
189    /// ```
190    pub fn as_properties_mut(&mut self) -> &mut Properties {
191        &mut self.value
192    }
193}
194
195impl<N: Into<String>, V: Into<String>> From<(N, V)> for Attribute {
196    fn from((name, value): (N, V)) -> Self {
197        Self::new(name.into(), value.into())
198    }
199}
200
201impl Attributes {
202    pub(crate) fn iter_key_value(&self) -> impl Iterator<Item = (&str, &str)> {
203        self.iter().map(|attr| (attr.name().as_str(), attr.value()))
204    }
205
206    /// Inserts the given attribute and returns the previous attribute with the same name, if any.
207    ///
208    /// # Examples
209    /// - Inserting a custom attribute into a detached spine entry:
210    /// ```
211    /// # use rbook::epub::spine::DetachedEpubSpineEntry;
212    /// let mut detached = DetachedEpubSpineEntry::new("c1");
213    /// let mut entry_mut = detached.as_mut();
214    /// let attributes = entry_mut.attributes_mut();
215    ///
216    /// // Insert a custom vendor attribute
217    /// attributes.insert(("this:appearance", "omit"));
218    ///
219    /// assert_eq!(Some("omit"), attributes.get_value("this:appearance"));
220    /// ```
221    pub fn insert(&mut self, attribute: impl Into<Attribute>) -> Option<Attribute> {
222        self.0.insert(attribute.into())
223    }
224
225    /// Returns the mutable [`Attribute`] with the given `name` if present, otherwise [`None`].
226    pub fn by_name_mut(&mut self, name: &str) -> Option<&mut Attribute> {
227        self.0.by_key_mut(name)
228    }
229
230    /// Returns an iterator over **all** mutable [`Attribute`] entries.
231    pub fn iter_mut(&mut self) -> AttributesIterMut<'_> {
232        AttributesIterMut(self.0.0.iter_mut())
233    }
234
235    /// Removes and returns the attribute with the given name, if present.
236    pub fn remove(&mut self, name: &str) -> Option<Attribute> {
237        self.0.remove(name)
238    }
239
240    /// Retains only the attributes specified by the predicate.
241    ///
242    /// If the closure returns `false`, the attribute is retained.
243    /// Otherwise, the attribute is removed.
244    ///
245    /// This method operates in place and visits every attribute exactly once.
246    ///
247    /// # See Also
248    /// - [`Self::extract_if`] to get an iterator of the removed attributes.
249    pub fn retain(&mut self, f: impl FnMut(&Attribute) -> bool) {
250        self.0.retain(f);
251    }
252
253    /// Removes and returns only the attributes specified by the predicate.
254    ///
255    /// If the closure returns `true`, the attribute is removed and yielded.
256    /// Otherwise, the attribute is retained.
257    ///
258    /// # Drop
259    /// If the returned iterator is not exhausted,
260    /// (e.g. dropped without iterating or iteration short-circuits),
261    /// then the remaining attributes are retained.
262    ///
263    /// Prefer [`Self::retain`] with a negated predicate if the returned iterator is not needed.
264    pub fn extract_if(
265        &mut self,
266        f: impl FnMut(&Attribute) -> bool,
267    ) -> impl Iterator<Item = Attribute> {
268        self.0.extract_if(f)
269    }
270
271    /// Removes and returns all attributes.
272    pub fn drain(&mut self) -> impl Iterator<Item = Attribute> {
273        self.0.drain()
274    }
275
276    /// Removes all attributes.
277    ///
278    /// # See Also
279    /// - [`Self::drain`] to get an iterator of the removed attributes.
280    pub fn clear(&mut self) {
281        self.0.clear();
282    }
283}
284
285impl Extend<Attribute> for Attributes {
286    fn extend<I: IntoIterator<Item = Attribute>>(&mut self, iter: I) {
287        for attr in iter {
288            self.insert(attr);
289        }
290    }
291}
292
293impl<'a> IntoIterator for &'a mut Attributes {
294    type Item = &'a mut Attribute;
295    type IntoIter = AttributesIterMut<'a>;
296
297    fn into_iter(self) -> Self::IntoIter {
298        self.iter_mut()
299    }
300}
301
302/// An iterator over all mutable [`Attribute`] entries within [`Attributes`].
303///
304/// # See Also
305/// - [`Attributes::iter_mut`] to create an instance of this struct.
306pub struct AttributesIterMut<'a>(std::slice::IterMut<'a, Attribute>);
307
308impl<'a> Iterator for AttributesIterMut<'a> {
309    // AttributeData is not returned directly here
310    // to allow greater flexibility in the future.
311    type Item = &'a mut Attribute;
312
313    fn next(&mut self) -> Option<Self::Item> {
314        self.0.next()
315    }
316
317    fn size_hint(&self) -> (usize, Option<usize>) {
318        self.0.size_hint()
319    }
320}
321
322impl DoubleEndedIterator for AttributesIterMut<'_> {
323    fn next_back(&mut self) -> Option<Self::Item> {
324        self.0.next_back()
325    }
326}
327
328impl ExactSizeIterator for AttributesIterMut<'_> {
329    fn len(&self) -> usize {
330        self.0.len()
331    }
332}
333
334impl FusedIterator for AttributesIterMut<'_> {}