/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use dom_struct::dom_struct; use html5ever::{LocalName, namespace_url, ns}; use style::str::HTML_SPACE_CHARACTERS; use stylo_atoms::Atom; use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::DOMTokenListBinding::DOMTokenListMethods; use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; use crate::dom::element::Element; use crate::dom::node::NodeTraits; use crate::script_runtime::CanGc; #[dom_struct] pub(crate) struct DOMTokenList { reflector_: Reflector, element: Dom, #[no_trace] local_name: LocalName, #[no_trace] supported_tokens: Option>, } impl DOMTokenList { pub(crate) fn new_inherited( element: &Element, local_name: LocalName, supported_tokens: Option>, ) -> DOMTokenList { DOMTokenList { reflector_: Reflector::new(), element: Dom::from_ref(element), local_name, supported_tokens, } } pub(crate) fn new( element: &Element, local_name: &LocalName, supported_tokens: Option>, can_gc: CanGc, ) -> DomRoot { reflect_dom_object( Box::new(DOMTokenList::new_inherited( element, local_name.clone(), supported_tokens, )), &*element.owner_window(), can_gc, ) } fn attribute(&self) -> Option> { self.element.get_attribute(&ns!(), &self.local_name) } fn check_token_exceptions(&self, token: &str) -> Fallible { match token { "" => Err(Error::Syntax), slice if slice.find(HTML_SPACE_CHARACTERS).is_some() => Err(Error::InvalidCharacter), slice => Ok(Atom::from(slice)), } } /// fn perform_update_steps(&self, atoms: Vec, can_gc: CanGc) { // Step 1 if !self.element.has_attribute(&self.local_name) && atoms.is_empty() { return; } // step 2 self.element .set_atomic_tokenlist_attribute(&self.local_name, atoms, can_gc) } /// fn validation_steps(&self, token: &str) -> Fallible { match &self.supported_tokens { None => Err(Error::Type( "This attribute has no supported tokens".to_owned(), )), Some(supported_tokens) => { let token = Atom::from(token).to_ascii_lowercase(); if supported_tokens .iter() .any(|supported_token| *supported_token == token) { return Ok(true); } Ok(false) }, } } } /// impl DOMTokenListMethods for DOMTokenList { /// fn Length(&self) -> u32 { self.attribute() .map_or(0, |attr| attr.value().as_tokens().len()) as u32 } /// fn Item(&self, index: u32) -> Option { self.attribute().and_then(|attr| { // FIXME(ajeffrey): Convert directly from Atom to DOMString attr.value() .as_tokens() .get(index as usize) .map(|token| DOMString::from(&**token)) }) } /// fn Contains(&self, token: DOMString) -> bool { let token = Atom::from(token); self.attribute().is_some_and(|attr| { attr.value() .as_tokens() .iter() .any(|atom: &Atom| *atom == token) }) } /// fn Add(&self, tokens: Vec, can_gc: CanGc) -> ErrorResult { let mut atoms = self.element.get_tokenlist_attribute(&self.local_name); for token in &tokens { let token = self.check_token_exceptions(token)?; if !atoms.iter().any(|atom| *atom == token) { atoms.push(token); } } self.perform_update_steps(atoms, can_gc); Ok(()) } /// fn Remove(&self, tokens: Vec, can_gc: CanGc) -> ErrorResult { let mut atoms = self.element.get_tokenlist_attribute(&self.local_name); for token in &tokens { let token = self.check_token_exceptions(token)?; atoms .iter() .position(|atom| *atom == token) .map(|index| atoms.remove(index)); } self.perform_update_steps(atoms, can_gc); Ok(()) } /// fn Toggle(&self, token: DOMString, force: Option, can_gc: CanGc) -> Fallible { let mut atoms = self.element.get_tokenlist_attribute(&self.local_name); let token = self.check_token_exceptions(&token)?; match atoms.iter().position(|atom| *atom == token) { Some(index) => match force { Some(true) => Ok(true), _ => { atoms.remove(index); self.perform_update_steps(atoms, can_gc); Ok(false) }, }, None => match force { Some(false) => Ok(false), _ => { atoms.push(token); self.perform_update_steps(atoms, can_gc); Ok(true) }, }, } } /// fn Value(&self) -> DOMString { self.element.get_string_attribute(&self.local_name) } /// fn SetValue(&self, value: DOMString, can_gc: CanGc) { self.element .set_tokenlist_attribute(&self.local_name, value, can_gc); } /// fn Replace(&self, token: DOMString, new_token: DOMString, can_gc: CanGc) -> Fallible { if token.is_empty() || new_token.is_empty() { // Step 1. return Err(Error::Syntax); } if token.contains(HTML_SPACE_CHARACTERS) || new_token.contains(HTML_SPACE_CHARACTERS) { // Step 2. return Err(Error::InvalidCharacter); } // Steps 3-4. let token = Atom::from(token); let new_token = Atom::from(new_token); let mut atoms = self.element.get_tokenlist_attribute(&self.local_name); let mut result = false; if let Some(pos) = atoms.iter().position(|atom| *atom == token) { match atoms.iter().position(|atom| *atom == new_token) { Some(redundant_pos) if redundant_pos > pos => { // The replacement is already in the list, later, // so we perform the replacement and remove the // later copy. atoms[pos] = new_token; atoms.remove(redundant_pos); }, Some(redundant_pos) if redundant_pos < pos => { // The replacement is already in the list, earlier, // so we remove the index where we'd be putting the // later copy. atoms.remove(pos); }, Some(_) => { // Else we are replacing the token with itself, nothing to change }, None => { // The replacement is not in the list already atoms[pos] = new_token; }, } // Step 5. self.perform_update_steps(atoms, can_gc); result = true; } Ok(result) } /// fn Supports(&self, token: DOMString) -> Fallible { self.validation_steps(&token) } // check-tidy: no specs after this line fn IndexedGetter(&self, index: u32) -> Option { self.Item(index) } }