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 | |
36 | namespace WebCore { |
37 | |
38 | DOMTokenList::DOMTokenList(Element& element, const QualifiedName& attributeName, IsSupportedTokenFunction&& isSupportedToken) |
39 | : m_element(element) |
40 | , m_attributeName(attributeName) |
41 | , m_isSupportedToken(WTFMove(isSupportedToken)) |
42 | { |
43 | } |
44 | |
45 | static inline bool tokenContainsHTMLSpace(const String& token) |
46 | { |
47 | return token.find(isHTMLSpace) != notFound; |
48 | } |
49 | |
50 | ExceptionOr<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 | |
61 | ExceptionOr<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 | |
71 | bool DOMTokenList::contains(const AtomicString& token) const |
72 | { |
73 | return tokens().contains(token); |
74 | } |
75 | |
76 | inline 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 | |
100 | ExceptionOr<void> DOMTokenList::add(const Vector<String>& tokens) |
101 | { |
102 | return addInternal(tokens.data(), tokens.size()); |
103 | } |
104 | |
105 | ExceptionOr<void> DOMTokenList::add(const AtomicString& token) |
106 | { |
107 | return addInternal(&token.string(), 1); |
108 | } |
109 | |
110 | inline 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 | |
125 | ExceptionOr<void> DOMTokenList::remove(const Vector<String>& tokens) |
126 | { |
127 | return removeInternal(tokens.data(), tokens.size()); |
128 | } |
129 | |
130 | ExceptionOr<void> DOMTokenList::remove(const AtomicString& token) |
131 | { |
132 | return removeInternal(&token.string(), 1); |
133 | } |
134 | |
135 | ExceptionOr<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 | |
160 | static 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 |
182 | ExceptionOr<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 |
205 | ExceptionOr<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 |
213 | const AtomicString& DOMTokenList::value() const |
214 | { |
215 | return m_element.getAttribute(m_attributeName); |
216 | } |
217 | |
218 | void DOMTokenList::setValue(const String& value) |
219 | { |
220 | m_element.setAttribute(m_attributeName, value); |
221 | } |
222 | |
223 | void 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 | |
252 | void 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 |
262 | void 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 | |
282 | Vector<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 | |