Skip to main content

ftml/tree/attribute/
mod.rs

1/*
2 * tree/attribute/mod.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2026 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21mod safe;
22
23use super::clone::string_to_owned;
24use crate::id_prefix::isolate_ids;
25use crate::parsing::parse_boolean;
26use crate::settings::WikitextSettings;
27use crate::url::normalize_href;
28use std::borrow::Cow;
29use std::collections::{BTreeMap, HashMap};
30use std::fmt::{self, Debug};
31use unicase::UniCase;
32
33pub use self::safe::{
34    BOOLEAN_ATTRIBUTES, SAFE_ATTRIBUTE_PREFIXES, SAFE_ATTRIBUTES, URL_ATTRIBUTES,
35    is_safe_attribute,
36};
37
38#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
39pub struct AttributeMap<'t> {
40    #[serde(flatten)]
41    inner: BTreeMap<Cow<'t, str>, Cow<'t, str>>,
42}
43
44impl<'t> AttributeMap<'t> {
45    #[inline]
46    pub fn new() -> Self {
47        AttributeMap::default()
48    }
49
50    pub fn from_arguments(arguments: &HashMap<UniCase<&'t str>, Cow<'t, str>>) -> Self {
51        let inner = arguments
52            .iter()
53            .filter(|&(key, _)| is_safe_attribute(*key))
54            .filter_map(|(key, value)| {
55                let mut value = Cow::clone(value);
56
57                // Check for special boolean behavior
58                if BOOLEAN_ATTRIBUTES.contains(key)
59                    && let Ok(boolean_value) = parse_boolean(&value)
60                {
61                    // It's a boolean HTML attribute, like "checked".
62                    if boolean_value {
63                        // true: Have a key-only attribute
64                        value = cow!("");
65                    } else {
66                        // false: Exclude the key entirely
67                        return None;
68                    }
69                }
70
71                // Check for URL-sensitive attributes
72                if URL_ATTRIBUTES.contains(key) {
73                    let url = normalize_href(&value, None).into_owned();
74                    value = Cow::Owned(url);
75                }
76
77                // Add key/value pair to map
78                let key = key.into_inner().to_ascii_lowercase();
79                Some((Cow::Owned(key), value))
80            })
81            .collect();
82
83        AttributeMap { inner }
84    }
85
86    pub fn insert(&mut self, attribute: &'t str, value: Cow<'t, str>) -> bool {
87        let will_insert = is_safe_attribute(UniCase::ascii(attribute));
88        if will_insert {
89            self.inner.insert(cow!(attribute), value);
90        }
91
92        will_insert
93    }
94
95    #[inline]
96    pub fn remove(&mut self, attribute: &str) -> Option<Cow<'t, str>> {
97        self.inner.remove(attribute)
98    }
99
100    #[inline]
101    pub fn get(&self) -> &BTreeMap<Cow<'t, str>, Cow<'t, str>> {
102        &self.inner
103    }
104
105    pub fn isolate_id(&mut self, settings: &WikitextSettings) {
106        if settings.isolate_user_ids
107            && let Some(value) = self.inner.get_mut("id")
108        {
109            trace!("Found 'id' attribute, isolating value");
110            *value = Cow::Owned(isolate_ids(value));
111        }
112    }
113
114    pub fn to_owned(&self) -> AttributeMap<'static> {
115        let mut inner = BTreeMap::new();
116
117        for (key, value) in self.inner.iter() {
118            let key = string_to_owned(key);
119            let value = string_to_owned(value);
120
121            inner.insert(key, value);
122        }
123
124        AttributeMap { inner }
125    }
126}
127
128impl Debug for AttributeMap<'_> {
129    #[inline]
130    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
131        self.inner.fmt(f)
132    }
133}
134
135impl<'t> From<BTreeMap<Cow<'t, str>, Cow<'t, str>>> for AttributeMap<'t> {
136    #[inline]
137    fn from(map: BTreeMap<Cow<'t, str>, Cow<'t, str>>) -> AttributeMap<'t> {
138        AttributeMap { inner: map }
139    }
140}