servo-script 0.1.0

A component of the servo web-engine.
Documentation
/* 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;
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<Element>,
    #[no_trace]
    local_name: LocalName,
    #[no_trace]
    supported_tokens: Option<Vec<Atom>>,
}

impl DOMTokenList {
    pub(crate) fn new_inherited(
        element: &Element,
        local_name: LocalName,
        supported_tokens: Option<Vec<Atom>>,
    ) -> 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<Vec<Atom>>,
        can_gc: CanGc,
    ) -> DomRoot<DOMTokenList> {
        reflect_dom_object(
            Box::new(DOMTokenList::new_inherited(
                element,
                local_name.clone(),
                supported_tokens,
            )),
            &*element.owner_window(),
            can_gc,
        )
    }

    fn attribute(&self) -> Option<DomRoot<Attr>> {
        self.element.get_attribute(&self.local_name)
    }

    fn check_token_exceptions(&self, token: &DOMString) -> Fallible<Atom> {
        let token = token.str();
        match &*token {
            "" => Err(Error::Syntax(None)),
            slice if slice.find(HTML_SPACE_CHARACTERS).is_some() => {
                Err(Error::InvalidCharacter(None))
            },
            slice => Ok(Atom::from(slice)),
        }
    }

    /// <https://dom.spec.whatwg.org/#concept-dtl-update>
    fn perform_update_steps(&self, atoms: Vec<Atom>, 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)
    }

    /// <https://dom.spec.whatwg.org/#concept-domtokenlist-validation>
    fn validation_steps(&self, token: &str) -> Fallible<bool> {
        match &self.supported_tokens {
            None => Err(Error::Type(
                c"This attribute has no supported tokens".to_owned(),
            )),
            Some(supported_tokens) => {
                let token = Atom::from(token).to_ascii_lowercase();
                if supported_tokens.contains(&token) {
                    return Ok(true);
                }
                Ok(false)
            },
        }
    }
}

/// <https://dom.spec.whatwg.org/#domtokenlist>
impl DOMTokenListMethods<crate::DomTypeHolder> for DOMTokenList {
    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-length>
    fn Length(&self) -> u32 {
        self.attribute()
            .map_or(0, |attr| attr.value().as_tokens().len()) as u32
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-item>
    fn Item(&self, index: u32) -> Option<DOMString> {
        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))
        })
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-contains>
    fn Contains(&self, token: DOMString) -> bool {
        let token = Atom::from(token);
        self.attribute()
            .is_some_and(|attr| attr.value().as_tokens().contains(&token))
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-add>
    fn Add(&self, tokens: Vec<DOMString>, 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.contains(&token) {
                atoms.push(token);
            }
        }
        self.perform_update_steps(atoms, can_gc);
        Ok(())
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-remove>
    fn Remove(&self, tokens: Vec<DOMString>, 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(())
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-toggle>
    fn Toggle(&self, token: DOMString, force: Option<bool>, can_gc: CanGc) -> Fallible<bool> {
        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)
                },
            },
        }
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-value>
    fn Value(&self) -> DOMString {
        self.element.get_string_attribute(&self.local_name)
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-value>
    fn SetValue(&self, value: DOMString, can_gc: CanGc) {
        self.element
            .set_tokenlist_attribute(&self.local_name, value, can_gc);
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-replace>
    fn Replace(&self, token: DOMString, new_token: DOMString, can_gc: CanGc) -> Fallible<bool> {
        if token.is_empty() || new_token.is_empty() {
            // Step 1.
            return Err(Error::Syntax(None));
        }
        if token.contains_html_space_characters() || new_token.contains_html_space_characters() {
            // Step 2.
            return Err(Error::InvalidCharacter(None));
        }
        // 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)
    }

    /// <https://dom.spec.whatwg.org/#dom-domtokenlist-supports>
    fn Supports(&self, token: DOMString) -> Fallible<bool> {
        self.validation_steps(&token.str())
    }

    // check-tidy: no specs after this line
    fn IndexedGetter(&self, index: u32) -> Option<DOMString> {
        self.Item(index)
    }
}