diff options
author | Timothy Flynn <trflynn89@pm.me> | 2021-10-18 11:53:40 -0400 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-10-18 23:33:56 +0200 |
commit | d24ae8063b154c997c91ff004d668ed16d1b4a7f (patch) | |
tree | 209b933ea9c7bd26c85cacf23736d925c58e0445 /Userland | |
parent | 4d8320a49a0e35843ed94414c227389adcf03689 (diff) | |
download | serenity-d24ae8063b154c997c91ff004d668ed16d1b4a7f.zip |
LibWeb: Implement DOMTokenList for managing space-separated tokens lists
DOMTokenList is used as the return type of, e.g., the Element.classList
property.
Diffstat (limited to 'Userland')
-rw-r--r-- | Userland/Libraries/LibWeb/CMakeLists.txt | 3 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/DOM/DOMTokenList.cpp | 258 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/DOM/DOMTokenList.h | 63 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/DOM/DOMTokenList.idl | 12 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Forward.h | 2 |
5 files changed, 338 insertions, 0 deletions
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 92028e30f6..5334e05ec6 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -61,6 +61,8 @@ set(SOURCES DOM/Comment.cpp DOM/CustomEvent.cpp DOM/DOMImplementation.cpp + DOM/DOMTokenList.cpp + DOM/DOMTokenList.idl DOM/Document.cpp DOM/DocumentFragment.cpp DOM/DocumentLoadEventDelayer.cpp @@ -380,6 +382,7 @@ libweb_js_wrapper(DOM/DocumentFragment) libweb_js_wrapper(DOM/DocumentType) libweb_js_wrapper(DOM/DOMException) libweb_js_wrapper(DOM/DOMImplementation) +libweb_js_wrapper(DOM/DOMTokenList) libweb_js_wrapper(DOM/Element) libweb_js_wrapper(DOM/Event) libweb_js_wrapper(DOM/EventTarget) diff --git a/Userland/Libraries/LibWeb/DOM/DOMTokenList.cpp b/Userland/Libraries/LibWeb/DOM/DOMTokenList.cpp new file mode 100644 index 0000000000..838f933ca1 --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/DOMTokenList.cpp @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2021, Tim Flynn <trflynn89@pm.me> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/CharacterTypes.h> +#include <AK/StringBuilder.h> +#include <LibWeb/DOM/DOMException.h> +#include <LibWeb/DOM/DOMTokenList.h> +#include <LibWeb/DOM/Element.h> + +namespace { + +// https://infra.spec.whatwg.org/#set-append +inline void append_to_ordered_set(Vector<String>& set, String item) +{ + if (!set.contains_slow(item)) + set.append(move(item)); +} + +// https://infra.spec.whatwg.org/#list-remove +inline void remove_from_ordered_set(Vector<String>& set, StringView item) +{ + set.remove_first_matching([&](auto const& value) { return value == item; }); +} + +// https://infra.spec.whatwg.org/#set-replace +inline void replace_in_ordered_set(Vector<String>& set, StringView item, String replacement) +{ + auto item_index = set.find_first_index(item); + VERIFY(item_index.has_value()); + + auto replacement_index = set.find_first_index(replacement); + if (!replacement_index.has_value()) { + set[*item_index] = move(replacement); + return; + } + + auto index_to_set = min(*item_index, *replacement_index); + auto index_to_remove = max(*item_index, *replacement_index); + if (index_to_set == index_to_remove) + return; + + set[index_to_set] = move(replacement); + set.remove(index_to_remove); +} + +} + +namespace Web::DOM { + +NonnullRefPtr<DOMTokenList> DOMTokenList::create(Element const& associated_element, FlyString associated_attribute) +{ + return adopt_ref(*new DOMTokenList(associated_element, move(associated_attribute))); +} + +// https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A2 +DOMTokenList::DOMTokenList(Element const& associated_element, FlyString associated_attribute) + : m_associated_element(associated_element) + , m_associated_attribute(move(associated_attribute)) +{ + auto value = associated_element.get_attribute(m_associated_attribute); + associated_attribute_changed(value); +} + +// https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A1 +void DOMTokenList::associated_attribute_changed(StringView value) +{ + m_token_set.clear(); + + if (value.is_empty()) + return; + + auto split_values = value.split_view(' '); + for (auto const& split_value : split_values) + append_to_ordered_set(m_token_set, split_value); +} + +// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-indices%E2%91%A3 +bool DOMTokenList::is_supported_property_index(u32 index) const +{ + return index < m_token_set.size(); +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-item +String const& DOMTokenList::item(size_t index) const +{ + static const String null_string {}; + + // 1. If index is equal to or greater than this’s token set’s size, then return null. + if (index >= m_token_set.size()) + return null_string; + + // 2. Return this’s token set[index]. + return m_token_set[index]; +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-contains +bool DOMTokenList::contains(StringView token) +{ + return m_token_set.contains_slow(token); +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-add +ExceptionOr<void> DOMTokenList::add(Vector<String> const& tokens) +{ + // 1. For each token in tokens: + for (auto const& token : tokens) { + // a. If token is the empty string, then throw a "SyntaxError" DOMException. + // b. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException. + if (auto exception = validate_token(token); exception.is_exception()) + return exception; + + // 2. For each token in tokens, append token to this’s token set. + append_to_ordered_set(m_token_set, token); + } + + // 3. Run the update steps. + run_update_steps(); + return {}; +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-remove +ExceptionOr<void> DOMTokenList::remove(Vector<String> const& tokens) +{ + // 1. For each token in tokens: + for (auto const& token : tokens) { + // a. If token is the empty string, then throw a "SyntaxError" DOMException. + // b. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException. + if (auto exception = validate_token(token); exception.is_exception()) + return exception; + + // 2. For each token in tokens, remove token from this’s token set. + remove_from_ordered_set(m_token_set, token); + } + + // 3. Run the update steps. + run_update_steps(); + return {}; +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-toggle +ExceptionOr<bool> DOMTokenList::toggle(String const& token, Optional<bool> force) +{ + // 1. If token is the empty string, then throw a "SyntaxError" DOMException. + // 2. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException. + if (auto exception = validate_token(token); exception.is_exception()) + return exception.exception(); + + // 3. If this’s token set[token] exists, then: + if (contains(token)) { + // a. If force is either not given or is false, then remove token from this’s token set, run the update steps and return false. + if (!force.has_value() || !force.value()) { + remove_from_ordered_set(m_token_set, token); + run_update_steps(); + return false; + } + + // b. Return true. + return true; + } + + // 4. Otherwise, if force not given or is true, append token to this’s token set, run the update steps, and return true. + if (!force.has_value() || force.value()) { + append_to_ordered_set(m_token_set, token); + run_update_steps(); + return true; + } + + // 5. Return false. + return false; +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-replace +ExceptionOr<bool> DOMTokenList::replace(String const& token, String const& new_token) +{ + // 1. If either token or newToken is the empty string, then throw a "SyntaxError" DOMException. + // 2. If either token or newToken contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException. + if (auto exception = validate_token(token); exception.is_exception()) + return exception.exception(); + if (auto exception = validate_token(new_token); exception.is_exception()) + return exception.exception(); + + // 3. If this’s token set does not contain token, then return false. + if (!contains(token)) + return false; + + // 4. Replace token in this’s token set with newToken. + replace_in_ordered_set(m_token_set, token, new_token); + + // 5. Run the update steps. + run_update_steps(); + + // 6. Return true. + return true; +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-supports +// https://dom.spec.whatwg.org/#concept-domtokenlist-validation +ExceptionOr<bool> DOMTokenList::supports([[maybe_unused]] StringView token) +{ + // FIXME: Implement this fully when any use case defines supported tokens. + + // 1. If the associated attribute’s local name does not define supported tokens, throw a TypeError. + return DOM::SimpleException { + DOM::SimpleExceptionType::TypeError, + String::formatted("Attribute {} does not define any supported tokens", m_associated_attribute) + }; + + // 2. Let lowercase token be a copy of token, in ASCII lowercase. + // 3. If lowercase token is present in supported tokens, return true. + // 4. Return false. +} + +// https://dom.spec.whatwg.org/#dom-domtokenlist-value +String DOMTokenList::value() const +{ + StringBuilder builder; + builder.join(' ', m_token_set); + return builder.build(); +} + +// https://dom.spec.whatwg.org/#ref-for-concept-element-attributes-set-value%E2%91%A2 +void DOMTokenList::set_value(String value) +{ + auto associated_element = m_associated_element.strong_ref(); + if (!associated_element) + return; + + associated_element->set_attribute(m_associated_attribute, move(value)); +} + +ExceptionOr<void> DOMTokenList::validate_token(StringView token) const +{ + if (token.is_empty()) + return SyntaxError::create("Non-empty DOM tokens are not allowed"); + if (any_of(token, is_ascii_space)) + return InvalidCharacterError::create("DOM tokens containing ASCII whitespace are not allowed"); + return {}; +} + +// https://dom.spec.whatwg.org/#concept-dtl-update +void DOMTokenList::run_update_steps() +{ + auto associated_element = m_associated_element.strong_ref(); + if (!associated_element) + return; + + // 1. If the associated element does not have an associated attribute and token set is empty, then return. + if (!associated_element->has_attribute(m_associated_attribute) && m_token_set.is_empty()) + return; + + // 2. Set an attribute value for the associated element using associated attribute’s local name and the result of running the ordered set serializer for token set. + associated_element->set_attribute(m_associated_attribute, value()); +} + +} diff --git a/Userland/Libraries/LibWeb/DOM/DOMTokenList.h b/Userland/Libraries/LibWeb/DOM/DOMTokenList.h new file mode 100644 index 0000000000..49a4bf08bd --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/DOMTokenList.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021, Tim Flynn <trflynn89@pm.me> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/FlyString.h> +#include <AK/Optional.h> +#include <AK/RefCounted.h> +#include <AK/String.h> +#include <AK/StringView.h> +#include <AK/Vector.h> +#include <LibWeb/Bindings/Wrappable.h> +#include <LibWeb/DOM/ExceptionOr.h> +#include <LibWeb/Forward.h> + +namespace Web::DOM { + +// https://dom.spec.whatwg.org/#domtokenlist +class DOMTokenList final + : public RefCounted<DOMTokenList> + , public Bindings::Wrappable { + +public: + using WrapperType = Bindings::DOMTokenListWrapper; + + static NonnullRefPtr<DOMTokenList> create(Element const& associated_element, FlyString associated_attribute); + ~DOMTokenList() = default; + + void associated_attribute_changed(StringView value); + bool is_supported_property_index(u32 index) const; + + size_t length() const { return m_token_set.size(); } + String const& item(size_t index) const; + bool contains(StringView token); + ExceptionOr<void> add(Vector<String> const& tokens); + ExceptionOr<void> remove(Vector<String> const& tokens); + ExceptionOr<bool> toggle(String const& token, Optional<bool> force); + ExceptionOr<bool> replace(String const& token, String const& new_token); + ExceptionOr<bool> supports(StringView token); + String value() const; + void set_value(String value); + +private: + DOMTokenList(Element const& associated_element, FlyString associated_attribute); + + ExceptionOr<void> validate_token(StringView token) const; + void run_update_steps(); + + WeakPtr<Element> m_associated_element; + FlyString m_associated_attribute; + Vector<String> m_token_set; +}; + +} + +namespace Web::Bindings { + +DOMTokenListWrapper* wrap(JS::GlobalObject&, DOM::DOMTokenList&); + +} diff --git a/Userland/Libraries/LibWeb/DOM/DOMTokenList.idl b/Userland/Libraries/LibWeb/DOM/DOMTokenList.idl new file mode 100644 index 0000000000..210703da50 --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/DOMTokenList.idl @@ -0,0 +1,12 @@ +interface DOMTokenList { + readonly attribute unsigned long length; + getter DOMString? item(unsigned long index); + boolean contains(DOMString token); + [CEReactions] undefined add(DOMString... tokens); + [CEReactions] undefined remove(DOMString... tokens); + [CEReactions] boolean toggle(DOMString token, optional boolean force); + [CEReactions] boolean replace(DOMString token, DOMString newToken); + boolean supports(DOMString token); + [CEReactions] stringifier attribute DOMString value; + iterable<DOMString>; +}; diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 791a3b0fc3..864b2f5f1e 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -81,6 +81,7 @@ class DocumentLoadEventDelayer; class DocumentType; class DOMException; class DOMImplementation; +class DOMTokenList; class Element; class Event; class EventHandler; @@ -313,6 +314,7 @@ class DOMParserWrapper; class DOMRectReadOnlyWrapper; class DOMRectWrapper; class DOMStringMapWrapper; +class DOMTokenListWrapper; class ElementWrapper; class EventListenerWrapper; class EventTargetWrapper; |