Skip to main content

script/dom/
domtokenlist.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use dom_struct::dom_struct;
6use html5ever::LocalName;
7use js::context::JSContext;
8use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
9use style::str::HTML_SPACE_CHARACTERS;
10use stylo_atoms::Atom;
11
12use crate::dom::attr::Attr;
13use crate::dom::bindings::codegen::Bindings::DOMTokenListBinding::DOMTokenListMethods;
14use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
15use crate::dom::bindings::root::{Dom, DomRoot};
16use crate::dom::bindings::str::DOMString;
17use crate::dom::element::Element;
18use crate::dom::node::NodeTraits;
19
20#[dom_struct]
21pub(crate) struct DOMTokenList {
22    reflector_: Reflector,
23    element: Dom<Element>,
24    #[no_trace]
25    local_name: LocalName,
26    #[no_trace]
27    supported_tokens: Option<Vec<Atom>>,
28}
29
30impl DOMTokenList {
31    pub(crate) fn new_inherited(
32        element: &Element,
33        local_name: LocalName,
34        supported_tokens: Option<Vec<Atom>>,
35    ) -> DOMTokenList {
36        DOMTokenList {
37            reflector_: Reflector::new(),
38            element: Dom::from_ref(element),
39            local_name,
40            supported_tokens,
41        }
42    }
43
44    pub(crate) fn new(
45        cx: &mut JSContext,
46        element: &Element,
47        local_name: &LocalName,
48        supported_tokens: Option<Vec<Atom>>,
49    ) -> DomRoot<DOMTokenList> {
50        reflect_dom_object_with_cx(
51            Box::new(DOMTokenList::new_inherited(
52                element,
53                local_name.clone(),
54                supported_tokens,
55            )),
56            &*element.owner_window(),
57            cx,
58        )
59    }
60
61    fn attribute(&self) -> Option<DomRoot<Attr>> {
62        self.element.get_attribute(&self.local_name)
63    }
64
65    fn check_token_exceptions(&self, token: &DOMString) -> Fallible<Atom> {
66        let token = token.str();
67        match &*token {
68            "" => Err(Error::Syntax(None)),
69            slice if slice.find(HTML_SPACE_CHARACTERS).is_some() => {
70                Err(Error::InvalidCharacter(None))
71            },
72            slice => Ok(Atom::from(slice)),
73        }
74    }
75
76    /// <https://dom.spec.whatwg.org/#concept-dtl-update>
77    fn perform_update_steps(&self, cx: &mut JSContext, atoms: Vec<Atom>) {
78        // Step 1
79        if !self.element.has_attribute(&self.local_name) && atoms.is_empty() {
80            return;
81        }
82        // step 2
83        self.element
84            .set_attribute(cx, &self.local_name, atoms.into())
85    }
86
87    /// <https://dom.spec.whatwg.org/#concept-domtokenlist-validation>
88    fn validation_steps(&self, token: &str) -> Fallible<bool> {
89        match &self.supported_tokens {
90            None => Err(Error::Type(
91                c"This attribute has no supported tokens".to_owned(),
92            )),
93            Some(supported_tokens) => {
94                let token = Atom::from(token).to_ascii_lowercase();
95                if supported_tokens.contains(&token) {
96                    return Ok(true);
97                }
98                Ok(false)
99            },
100        }
101    }
102}
103
104/// <https://dom.spec.whatwg.org/#domtokenlist>
105impl DOMTokenListMethods<crate::DomTypeHolder> for DOMTokenList {
106    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-length>
107    fn Length(&self) -> u32 {
108        self.attribute()
109            .map_or(0, |attr| attr.value().as_tokens().len()) as u32
110    }
111
112    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-item>
113    fn Item(&self, index: u32) -> Option<DOMString> {
114        self.attribute().and_then(|attr| {
115            // FIXME(ajeffrey): Convert directly from Atom to DOMString
116            attr.value()
117                .as_tokens()
118                .get(index as usize)
119                .map(|token| DOMString::from(&**token))
120        })
121    }
122
123    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-contains>
124    fn Contains(&self, token: DOMString) -> bool {
125        let token = Atom::from(token);
126        self.attribute()
127            .is_some_and(|attr| attr.value().as_tokens().contains(&token))
128    }
129
130    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-add>
131    fn Add(&self, cx: &mut JSContext, tokens: Vec<DOMString>) -> ErrorResult {
132        let mut atoms = self.element.get_tokenlist_attribute(&self.local_name);
133        for token in &tokens {
134            let token = self.check_token_exceptions(token)?;
135            if !atoms.contains(&token) {
136                atoms.push(token);
137            }
138        }
139        self.perform_update_steps(cx, atoms);
140        Ok(())
141    }
142
143    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-remove>
144    fn Remove(&self, cx: &mut JSContext, tokens: Vec<DOMString>) -> ErrorResult {
145        let mut atoms = self.element.get_tokenlist_attribute(&self.local_name);
146        for token in &tokens {
147            let token = self.check_token_exceptions(token)?;
148            atoms
149                .iter()
150                .position(|atom| *atom == token)
151                .map(|index| atoms.remove(index));
152        }
153        self.perform_update_steps(cx, atoms);
154        Ok(())
155    }
156
157    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-toggle>
158    fn Toggle(&self, cx: &mut JSContext, token: DOMString, force: Option<bool>) -> Fallible<bool> {
159        let mut atoms = self.element.get_tokenlist_attribute(&self.local_name);
160        let token = self.check_token_exceptions(&token)?;
161        match atoms.iter().position(|atom| *atom == token) {
162            Some(index) => match force {
163                Some(true) => Ok(true),
164                _ => {
165                    atoms.remove(index);
166                    self.perform_update_steps(cx, atoms);
167                    Ok(false)
168                },
169            },
170            None => match force {
171                Some(false) => Ok(false),
172                _ => {
173                    atoms.push(token);
174                    self.perform_update_steps(cx, atoms);
175                    Ok(true)
176                },
177            },
178        }
179    }
180
181    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-value>
182    fn Value(&self) -> DOMString {
183        self.element.get_string_attribute(&self.local_name)
184    }
185
186    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-value>
187    fn SetValue(&self, cx: &mut JSContext, value: DOMString) {
188        self.element
189            .set_tokenlist_attribute(cx, &self.local_name, value);
190    }
191
192    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-replace>
193    fn Replace(
194        &self,
195        cx: &mut JSContext,
196        token: DOMString,
197        new_token: DOMString,
198    ) -> Fallible<bool> {
199        if token.is_empty() || new_token.is_empty() {
200            // Step 1.
201            return Err(Error::Syntax(None));
202        }
203        if token.contains_html_space_characters() || new_token.contains_html_space_characters() {
204            // Step 2.
205            return Err(Error::InvalidCharacter(None));
206        }
207        // Steps 3-4.
208        let token = Atom::from(token);
209        let new_token = Atom::from(new_token);
210        let mut atoms = self.element.get_tokenlist_attribute(&self.local_name);
211        let mut result = false;
212        if let Some(pos) = atoms.iter().position(|atom| *atom == token) {
213            match atoms.iter().position(|atom| *atom == new_token) {
214                Some(redundant_pos) if redundant_pos > pos => {
215                    // The replacement is already in the list, later,
216                    // so we perform the replacement and remove the
217                    // later copy.
218                    atoms[pos] = new_token;
219                    atoms.remove(redundant_pos);
220                },
221                Some(redundant_pos) if redundant_pos < pos => {
222                    // The replacement is already in the list, earlier,
223                    // so we remove the index where we'd be putting the
224                    // later copy.
225                    atoms.remove(pos);
226                },
227                Some(_) => {
228                    // Else we are replacing the token with itself, nothing to change
229                },
230                None => {
231                    // The replacement is not in the list already
232                    atoms[pos] = new_token;
233                },
234            }
235
236            // Step 5.
237            self.perform_update_steps(cx, atoms);
238            result = true;
239        }
240        Ok(result)
241    }
242
243    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-supports>
244    fn Supports(&self, token: DOMString) -> Fallible<bool> {
245        self.validation_steps(&token.str())
246    }
247
248    // check-tidy: no specs after this line
249    fn IndexedGetter(&self, index: u32) -> Option<DOMString> {
250        self.Item(index)
251    }
252}