mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 18:42:43 +00:00 
			
		
		
		
	LibWeb: Implement DOMTokenList for managing space-separated tokens lists
DOMTokenList is used as the return type of, e.g., the Element.classList property.
This commit is contained in:
		
							parent
							
								
									4d8320a49a
								
							
						
					
					
						commit
						d24ae8063b
					
				
					 6 changed files with 339 additions and 0 deletions
				
			
		|  | @ -2869,6 +2869,7 @@ void generate_prototype_implementation(IDL::Interface const& interface) | ||||||
| #include <LibWeb/Bindings/DOMImplementationWrapper.h> | #include <LibWeb/Bindings/DOMImplementationWrapper.h> | ||||||
| #include <LibWeb/Bindings/DOMRectWrapper.h> | #include <LibWeb/Bindings/DOMRectWrapper.h> | ||||||
| #include <LibWeb/Bindings/DOMStringMapWrapper.h> | #include <LibWeb/Bindings/DOMStringMapWrapper.h> | ||||||
|  | #include <LibWeb/Bindings/DOMTokenListWrapper.h> | ||||||
| #include <LibWeb/Bindings/DocumentFragmentWrapper.h> | #include <LibWeb/Bindings/DocumentFragmentWrapper.h> | ||||||
| #include <LibWeb/Bindings/DocumentTypeWrapper.h> | #include <LibWeb/Bindings/DocumentTypeWrapper.h> | ||||||
| #include <LibWeb/Bindings/DocumentWrapper.h> | #include <LibWeb/Bindings/DocumentWrapper.h> | ||||||
|  |  | ||||||
|  | @ -61,6 +61,8 @@ set(SOURCES | ||||||
|     DOM/Comment.cpp |     DOM/Comment.cpp | ||||||
|     DOM/CustomEvent.cpp |     DOM/CustomEvent.cpp | ||||||
|     DOM/DOMImplementation.cpp |     DOM/DOMImplementation.cpp | ||||||
|  |     DOM/DOMTokenList.cpp | ||||||
|  |     DOM/DOMTokenList.idl | ||||||
|     DOM/Document.cpp |     DOM/Document.cpp | ||||||
|     DOM/DocumentFragment.cpp |     DOM/DocumentFragment.cpp | ||||||
|     DOM/DocumentLoadEventDelayer.cpp |     DOM/DocumentLoadEventDelayer.cpp | ||||||
|  | @ -380,6 +382,7 @@ libweb_js_wrapper(DOM/DocumentFragment) | ||||||
| libweb_js_wrapper(DOM/DocumentType) | libweb_js_wrapper(DOM/DocumentType) | ||||||
| libweb_js_wrapper(DOM/DOMException) | libweb_js_wrapper(DOM/DOMException) | ||||||
| libweb_js_wrapper(DOM/DOMImplementation) | libweb_js_wrapper(DOM/DOMImplementation) | ||||||
|  | libweb_js_wrapper(DOM/DOMTokenList) | ||||||
| libweb_js_wrapper(DOM/Element) | libweb_js_wrapper(DOM/Element) | ||||||
| libweb_js_wrapper(DOM/Event) | libweb_js_wrapper(DOM/Event) | ||||||
| libweb_js_wrapper(DOM/EventTarget) | libweb_js_wrapper(DOM/EventTarget) | ||||||
|  |  | ||||||
							
								
								
									
										258
									
								
								Userland/Libraries/LibWeb/DOM/DOMTokenList.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								Userland/Libraries/LibWeb/DOM/DOMTokenList.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								Userland/Libraries/LibWeb/DOM/DOMTokenList.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Userland/Libraries/LibWeb/DOM/DOMTokenList.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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&); | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								Userland/Libraries/LibWeb/DOM/DOMTokenList.idl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Userland/Libraries/LibWeb/DOM/DOMTokenList.idl
									
										
									
									
									
										Normal file
									
								
							|  | @ -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>; | ||||||
|  | }; | ||||||
|  | @ -81,6 +81,7 @@ class DocumentLoadEventDelayer; | ||||||
| class DocumentType; | class DocumentType; | ||||||
| class DOMException; | class DOMException; | ||||||
| class DOMImplementation; | class DOMImplementation; | ||||||
|  | class DOMTokenList; | ||||||
| class Element; | class Element; | ||||||
| class Event; | class Event; | ||||||
| class EventHandler; | class EventHandler; | ||||||
|  | @ -313,6 +314,7 @@ class DOMParserWrapper; | ||||||
| class DOMRectReadOnlyWrapper; | class DOMRectReadOnlyWrapper; | ||||||
| class DOMRectWrapper; | class DOMRectWrapper; | ||||||
| class DOMStringMapWrapper; | class DOMStringMapWrapper; | ||||||
|  | class DOMTokenListWrapper; | ||||||
| class ElementWrapper; | class ElementWrapper; | ||||||
| class EventListenerWrapper; | class EventListenerWrapper; | ||||||
| class EventTargetWrapper; | class EventTargetWrapper; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Flynn
						Timothy Flynn