Update DOMTokenList.replace() to match the latest DOM specification
[WebKit.git] / Source / WebCore / html / DOMTokenList.cpp
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 "ExceptionCode.h"
30 #include "HTMLParserIdioms.h"
31 #include "SpaceSplitString.h"
32 #include <wtf/HashSet.h>
33 #include <wtf/SetForScope.h>
34 #include <wtf/text/AtomicStringHash.h>
35 #include <wtf/text/StringBuilder.h>
36
37 namespace WebCore {
38
39 DOMTokenList::DOMTokenList(Element& element, const QualifiedName& attributeName, WTF::Function<bool(StringView)>&& isSupportedToken)
40     : m_element(element)
41     , m_attributeName(attributeName)
42     , m_isSupportedToken(WTFMove(isSupportedToken))
43 {
44 }
45
46 static inline bool tokenContainsHTMLSpace(const String& token)
47 {
48     return token.find(isHTMLSpace) != notFound;
49 }
50
51 ExceptionOr<void> DOMTokenList::validateToken(const String& token)
52 {
53     if (token.isEmpty())
54         return Exception { SYNTAX_ERR };
55
56     if (tokenContainsHTMLSpace(token))
57         return Exception { INVALID_CHARACTER_ERR };
58
59     return { };
60 }
61
62 ExceptionOr<void> DOMTokenList::validateTokens(const String* tokens, size_t length)
63 {
64     for (size_t i = 0; i < length; ++i) {
65         auto result = validateToken(tokens[i]);
66         if (result.hasException())
67             return result;
68     }
69     return { };
70 }
71
72 bool DOMTokenList::contains(const AtomicString& token) const
73 {
74     return tokens().contains(token);
75 }
76
77 inline ExceptionOr<void> DOMTokenList::addInternal(const String* newTokens, size_t length)
78 {
79     // This is usually called with a single token.
80     Vector<AtomicString, 1> uniqueNewTokens;
81     uniqueNewTokens.reserveInitialCapacity(length);
82
83     auto& tokens = this->tokens();
84
85     for (size_t i = 0; i < length; ++i) {
86         auto result = validateToken(newTokens[i]);
87         if (result.hasException())
88             return result;
89         if (!tokens.contains(newTokens[i]) && !uniqueNewTokens.contains(newTokens[i]))
90             uniqueNewTokens.uncheckedAppend(newTokens[i]);
91     }
92
93     if (!uniqueNewTokens.isEmpty())
94         tokens.appendVector(uniqueNewTokens);
95
96     updateAssociatedAttributeFromTokens();
97
98     return { };
99 }
100
101 ExceptionOr<void> DOMTokenList::add(const Vector<String>& tokens)
102 {
103     return addInternal(tokens.data(), tokens.size());
104 }
105
106 ExceptionOr<void> DOMTokenList::add(const AtomicString& token)
107 {
108     return addInternal(&token.string(), 1);
109 }
110
111 inline ExceptionOr<void> DOMTokenList::removeInternal(const String* tokensToRemove, size_t length)
112 {
113     auto result = validateTokens(tokensToRemove, length);
114     if (result.hasException())
115         return result;
116
117     auto& tokens = this->tokens();
118     for (size_t i = 0; i < length; ++i)
119         tokens.removeFirst(tokensToRemove[i]);
120
121     updateAssociatedAttributeFromTokens();
122
123     return { };
124 }
125
126 ExceptionOr<void> DOMTokenList::remove(const Vector<String>& tokens)
127 {
128     return removeInternal(tokens.data(), tokens.size());
129 }
130
131 ExceptionOr<void> DOMTokenList::remove(const AtomicString& token)
132 {
133     return removeInternal(&token.string(), 1);
134 }
135
136 ExceptionOr<bool> DOMTokenList::toggle(const AtomicString& token, std::optional<bool> force)
137 {
138     auto result = validateToken(token);
139     if (result.hasException())
140         return result.releaseException();
141
142     auto& tokens = this->tokens();
143
144     if (tokens.contains(token)) {
145         if (!force.value_or(false)) {
146             tokens.removeFirst(token);
147             updateAssociatedAttributeFromTokens();
148             return false;
149         }
150         return true;
151     }
152
153     if (force && !force.value())
154         return false;
155
156     tokens.append(token);
157     updateAssociatedAttributeFromTokens();
158     return true;
159 }
160
161 // https://dom.spec.whatwg.org/#dom-domtokenlist-replace
162 ExceptionOr<void> DOMTokenList::replace(const AtomicString& item, const AtomicString& replacement)
163 {
164     if (item.isEmpty() || replacement.isEmpty())
165         return Exception { SYNTAX_ERR };
166
167     if (tokenContainsHTMLSpace(item) || tokenContainsHTMLSpace(replacement))
168         return Exception { INVALID_CHARACTER_ERR };
169
170     auto& tokens = this->tokens();
171
172     auto matchesItemOrReplacement = [&](auto& token) {
173         return token == item || token == replacement;
174     };
175
176     size_t index = tokens.findMatching(matchesItemOrReplacement);
177     if (index == notFound)
178         return { };
179
180     tokens[index] = replacement;
181     tokens.removeFirstMatching(matchesItemOrReplacement, index + 1);
182     ASSERT(tokens.find(item) == notFound);
183     ASSERT(tokens.reverseFind(replacement) == index);
184
185     updateAssociatedAttributeFromTokens();
186
187     return { };
188 }
189
190 // https://dom.spec.whatwg.org/#concept-domtokenlist-validation
191 ExceptionOr<bool> DOMTokenList::supports(StringView token)
192 {
193     if (!m_isSupportedToken)
194         return Exception { TypeError };
195     return m_isSupportedToken(token);
196 }
197
198 // https://dom.spec.whatwg.org/#dom-domtokenlist-value
199 const AtomicString& DOMTokenList::value() const
200 {
201     return m_element.getAttribute(m_attributeName);
202 }
203
204 void DOMTokenList::setValue(const String& value)
205 {
206     m_element.setAttribute(m_attributeName, value);
207 }
208
209 void DOMTokenList::updateTokensFromAttributeValue(const String& value)
210 {
211     // Clear tokens but not capacity.
212     m_tokens.shrink(0);
213
214     HashSet<AtomicString> addedTokens;
215     // https://dom.spec.whatwg.org/#ordered%20sets
216     for (unsigned start = 0; ; ) {
217         while (start < value.length() && isHTMLSpace(value[start]))
218             ++start;
219         if (start >= value.length())
220             break;
221         unsigned end = start + 1;
222         while (end < value.length() && !isHTMLSpace(value[end]))
223             ++end;
224
225         AtomicString token = value.substring(start, end - start);
226         if (!addedTokens.contains(token)) {
227             m_tokens.append(token);
228             addedTokens.add(token);
229         }
230
231         start = end + 1;
232     }
233
234     m_tokens.shrinkToFit();
235     m_tokensNeedUpdating = false;
236 }
237
238 void DOMTokenList::associatedAttributeValueChanged(const AtomicString&)
239 {
240     // Do not reset the DOMTokenList value if the attribute value was changed by us.
241     if (m_inUpdateAssociatedAttributeFromTokens)
242         return;
243
244     m_tokensNeedUpdating = true;
245 }
246
247 // https://dom.spec.whatwg.org/#concept-dtl-update
248 void DOMTokenList::updateAssociatedAttributeFromTokens()
249 {
250     ASSERT(!m_tokensNeedUpdating);
251
252     // https://dom.spec.whatwg.org/#concept-ordered-set-serializer
253     StringBuilder builder;
254     for (auto& token : tokens()) {
255         if (!builder.isEmpty())
256             builder.append(' ');
257         builder.append(token);
258     }
259     AtomicString serializedValue = builder.toAtomicString();
260
261     SetForScope<bool> inAttributeUpdate(m_inUpdateAssociatedAttributeFromTokens, true);
262     m_element.setAttribute(m_attributeName, serializedValue);
263 }
264
265 Vector<AtomicString>& DOMTokenList::tokens()
266 {
267     if (m_tokensNeedUpdating)
268         updateTokensFromAttributeValue(m_element.getAttribute(m_attributeName));
269     ASSERT(!m_tokensNeedUpdating);
270     return m_tokens;
271 }
272
273 } // namespace WebCore