1/*
2 * Copyright (C) 2010 Google Inc. All rights reserved.
3 * Copyright (C) 2015, 2016 Apple Inc. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "DOMTokenList.h"
28
29#include "HTMLParserIdioms.h"
30#include "SpaceSplitString.h"
31#include <wtf/HashSet.h>
32#include <wtf/SetForScope.h>
33#include <wtf/text/AtomicStringHash.h>
34#include <wtf/text/StringBuilder.h>
35
36namespace WebCore {
37
38DOMTokenList::DOMTokenList(Element& element, const QualifiedName& attributeName, IsSupportedTokenFunction&& isSupportedToken)
39 : m_element(element)
40 , m_attributeName(attributeName)
41 , m_isSupportedToken(WTFMove(isSupportedToken))
42{
43}
44
45static inline bool tokenContainsHTMLSpace(const String& token)
46{
47 return token.find(isHTMLSpace) != notFound;
48}
49
50ExceptionOr<void> DOMTokenList::validateToken(const String& token)
51{
52 if (token.isEmpty())
53 return Exception { SyntaxError };
54
55 if (tokenContainsHTMLSpace(token))
56 return Exception { InvalidCharacterError };
57
58 return { };
59}
60
61ExceptionOr<void> DOMTokenList::validateTokens(const String* tokens, size_t length)
62{
63 for (size_t i = 0; i < length; ++i) {
64 auto result = validateToken(tokens[i]);
65 if (result.hasException())
66 return result;
67 }
68 return { };
69}
70
71bool DOMTokenList::contains(const AtomicString& token) const
72{
73 return tokens().contains(token);
74}
75
76inline ExceptionOr<void> DOMTokenList::addInternal(const String* newTokens, size_t length)
77{
78 // This is usually called with a single token.
79 Vector<AtomicString, 1> uniqueNewTokens;
80 uniqueNewTokens.reserveInitialCapacity(length);
81
82 auto& tokens = this->tokens();
83
84 for (size_t i = 0; i < length; ++i) {
85 auto result = validateToken(newTokens[i]);
86 if (result.hasException())
87 return result;
88 if (!tokens.contains(newTokens[i]) && !uniqueNewTokens.contains(newTokens[i]))
89 uniqueNewTokens.uncheckedAppend(newTokens[i]);
90 }
91
92 if (!uniqueNewTokens.isEmpty())
93 tokens.appendVector(uniqueNewTokens);
94
95 updateAssociatedAttributeFromTokens();
96
97 return { };
98}
99
100ExceptionOr<void> DOMTokenList::add(const Vector<String>& tokens)
101{
102 return addInternal(tokens.data(), tokens.size());
103}
104
105ExceptionOr<void> DOMTokenList::add(const AtomicString& token)
106{
107 return addInternal(&token.string(), 1);
108}
109
110inline ExceptionOr<void> DOMTokenList::removeInternal(const String* tokensToRemove, size_t length)
111{
112 auto result = validateTokens(tokensToRemove, length);
113 if (result.hasException())
114 return result;
115
116 auto& tokens = this->tokens();
117 for (size_t i = 0; i < length; ++i)
118 tokens.removeFirst(tokensToRemove[i]);
119
120 updateAssociatedAttributeFromTokens();
121
122 return { };
123}
124
125ExceptionOr<void> DOMTokenList::remove(const Vector<String>& tokens)
126{
127 return removeInternal(tokens.data(), tokens.size());
128}
129
130ExceptionOr<void> DOMTokenList::remove(const AtomicString& token)
131{
132 return removeInternal(&token.string(), 1);
133}
134
135ExceptionOr<bool> DOMTokenList::toggle(const AtomicString& token, Optional<bool> force)
136{
137 auto result = validateToken(token);
138 if (result.hasException())
139 return result.releaseException();
140
141 auto& tokens = this->tokens();
142
143 if (tokens.contains(token)) {
144 if (!force.valueOr(false)) {
145 tokens.removeFirst(token);
146 updateAssociatedAttributeFromTokens();
147 return false;
148 }
149 return true;
150 }
151
152 if (force && !force.value())
153 return false;
154
155 tokens.append(token);
156 updateAssociatedAttributeFromTokens();
157 return true;
158}
159
160static inline void replaceInOrderedSet(Vector<AtomicString>& tokens, size_t tokenIndex, const AtomicString& newToken)
161{
162 ASSERT(tokenIndex != notFound);
163 ASSERT(tokenIndex < tokens.size());
164
165 auto newTokenIndex = tokens.find(newToken);
166 if (newTokenIndex == notFound) {
167 tokens[tokenIndex] = newToken;
168 return;
169 }
170
171 if (newTokenIndex == tokenIndex)
172 return;
173
174 if (newTokenIndex > tokenIndex) {
175 tokens[tokenIndex] = newToken;
176 tokens.remove(newTokenIndex);
177 } else
178 tokens.remove(tokenIndex);
179}
180
181// https://dom.spec.whatwg.org/#dom-domtokenlist-replace
182ExceptionOr<bool> DOMTokenList::replace(const AtomicString& token, const AtomicString& newToken)
183{
184 if (token.isEmpty() || newToken.isEmpty())
185 return Exception { SyntaxError };
186
187 if (tokenContainsHTMLSpace(token) || tokenContainsHTMLSpace(newToken))
188 return Exception { InvalidCharacterError };
189
190 auto& tokens = this->tokens();
191
192 auto tokenIndex = tokens.find(token);
193 if (tokenIndex == notFound)
194 return false;
195
196 replaceInOrderedSet(tokens, tokenIndex, newToken);
197 ASSERT(token == newToken || tokens.find(token) == notFound);
198
199 updateAssociatedAttributeFromTokens();
200
201 return true;
202}
203
204// https://dom.spec.whatwg.org/#concept-domtokenlist-validation
205ExceptionOr<bool> DOMTokenList::supports(StringView token)
206{
207 if (!m_isSupportedToken)
208 return Exception { TypeError };
209 return m_isSupportedToken(m_element.document(), token);
210}
211
212// https://dom.spec.whatwg.org/#dom-domtokenlist-value
213const AtomicString& DOMTokenList::value() const
214{
215 return m_element.getAttribute(m_attributeName);
216}
217
218void DOMTokenList::setValue(const String& value)
219{
220 m_element.setAttribute(m_attributeName, value);
221}
222
223void DOMTokenList::updateTokensFromAttributeValue(const String& value)
224{
225 // Clear tokens but not capacity.
226 m_tokens.shrink(0);
227
228 HashSet<AtomicString> addedTokens;
229 // https://dom.spec.whatwg.org/#ordered%20sets
230 for (unsigned start = 0; ; ) {
231 while (start < value.length() && isHTMLSpace(value[start]))
232 ++start;
233 if (start >= value.length())
234 break;
235 unsigned end = start + 1;
236 while (end < value.length() && !isHTMLSpace(value[end]))
237 ++end;
238
239 AtomicString token = value.substring(start, end - start);
240 if (!addedTokens.contains(token)) {
241 m_tokens.append(token);
242 addedTokens.add(token);
243 }
244
245 start = end + 1;
246 }
247
248 m_tokens.shrinkToFit();
249 m_tokensNeedUpdating = false;
250}
251
252void DOMTokenList::associatedAttributeValueChanged(const AtomicString&)
253{
254 // Do not reset the DOMTokenList value if the attribute value was changed by us.
255 if (m_inUpdateAssociatedAttributeFromTokens)
256 return;
257
258 m_tokensNeedUpdating = true;
259}
260
261// https://dom.spec.whatwg.org/#concept-dtl-update
262void DOMTokenList::updateAssociatedAttributeFromTokens()
263{
264 ASSERT(!m_tokensNeedUpdating);
265
266 if (m_tokens.isEmpty() && !m_element.hasAttribute(m_attributeName))
267 return;
268
269 // https://dom.spec.whatwg.org/#concept-ordered-set-serializer
270 StringBuilder builder;
271 for (auto& token : tokens()) {
272 if (!builder.isEmpty())
273 builder.append(' ');
274 builder.append(token);
275 }
276 AtomicString serializedValue = builder.toAtomicString();
277
278 SetForScope<bool> inAttributeUpdate(m_inUpdateAssociatedAttributeFromTokens, true);
279 m_element.setAttribute(m_attributeName, serializedValue);
280}
281
282Vector<AtomicString>& DOMTokenList::tokens()
283{
284 if (m_tokensNeedUpdating)
285 updateTokensFromAttributeValue(m_element.getAttribute(m_attributeName));
286 ASSERT(!m_tokensNeedUpdating);
287 return m_tokens;
288}
289
290} // namespace WebCore
291