1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host.dom;
16
17 import java.util.ArrayList;
18 import java.util.List;
19
20 import org.apache.commons.lang3.StringUtils;
21 import org.htmlunit.WebClient;
22 import org.htmlunit.corejs.javascript.Context;
23 import org.htmlunit.corejs.javascript.ContextAction;
24 import org.htmlunit.corejs.javascript.Function;
25 import org.htmlunit.corejs.javascript.Scriptable;
26 import org.htmlunit.corejs.javascript.VarScope;
27 import org.htmlunit.html.DomAttr;
28 import org.htmlunit.html.DomElement;
29 import org.htmlunit.html.DomNode;
30 import org.htmlunit.javascript.HtmlUnitContextFactory;
31 import org.htmlunit.javascript.HtmlUnitScriptable;
32 import org.htmlunit.javascript.JavaScriptEngine;
33 import org.htmlunit.javascript.configuration.JsxClass;
34 import org.htmlunit.javascript.configuration.JsxConstructor;
35 import org.htmlunit.javascript.configuration.JsxFunction;
36 import org.htmlunit.javascript.configuration.JsxGetter;
37 import org.htmlunit.javascript.configuration.JsxSetter;
38 import org.htmlunit.javascript.configuration.JsxSymbol;
39
40
41
42
43
44
45
46
47
48
49
50 @JsxClass
51 public class DOMTokenList extends HtmlUnitScriptable {
52
53 private static final String WHITESPACE_CHARS = " \t\r\n\u000C";
54
55 private String attributeName_;
56
57
58
59
60 public DOMTokenList() {
61 super();
62 }
63
64
65
66
67 @JsxConstructor
68 public void jsConstructor() {
69
70 }
71
72
73
74
75
76
77 public DOMTokenList(final Node node, final String attributeName) {
78 super();
79 setDomNode(node.getDomNodeOrDie(), false);
80 setParentScope(node.getParentScope());
81 setPrototype(getPrototype(getClass()));
82 attributeName_ = attributeName;
83 }
84
85
86
87
88
89 @JsxGetter
90 public String getValue() {
91 final DomNode node = getDomNodeOrNull();
92 if (node != null) {
93 final DomAttr attr = (DomAttr) node.getAttributes().getNamedItem(attributeName_);
94 if (attr != null) {
95 return attr.getValue();
96 }
97 }
98 return null;
99 }
100
101
102
103
104
105 @JsxSetter
106 public void setValue(final String value) {
107 final DomNode node = getDomNodeOrNull();
108 if (node != null) {
109 updateAttribute(value);
110 }
111 }
112
113
114
115
116
117 @JsxGetter
118 public int getLength() {
119 final String value = getValue();
120 if (org.htmlunit.util.StringUtils.isBlank(value)) {
121 return 0;
122 }
123
124 return split(value).size();
125 }
126
127
128
129
130 @Override
131 public String getDefaultValue(final Class<?> hint) {
132 if (getPrototype() == null) {
133 return (String) super.getDefaultValue(hint);
134 }
135
136 final String value = getValue();
137 if (value != null) {
138 return String.join(" ", StringUtils.split(value, WHITESPACE_CHARS));
139 }
140 return "";
141 }
142
143
144
145
146
147
148
149
150
151
152 @JsxFunction
153 public static void add(final Context context, final VarScope scope,
154 final Scriptable thisObj, final Object[] args, final Function function) {
155 if (args.length > 0) {
156 final DOMTokenList list = (DOMTokenList) thisObj;
157 final List<String> parts = split(list.getValue());
158
159 for (final Object arg : args) {
160 final String token = JavaScriptEngine.toString(arg);
161
162 if (org.htmlunit.util.StringUtils.isEmptyOrNull(token)) {
163 throw JavaScriptEngine.asJavaScriptException(
164 (HtmlUnitScriptable) getTopLevelScope(scope).getGlobalThis(),
165 "DOMTokenList: add() does not support empty tokens",
166 DOMException.SYNTAX_ERR);
167 }
168 if (StringUtils.containsAny(token, WHITESPACE_CHARS)) {
169 throw JavaScriptEngine.asJavaScriptException(
170 (HtmlUnitScriptable) getTopLevelScope(scope).getGlobalThis(),
171 "DOMTokenList: add() does not support whitespace chars in tokens",
172 DOMException.INVALID_CHARACTER_ERR);
173 }
174
175 if (!parts.contains(token)) {
176 parts.add(token);
177 }
178 }
179 list.updateAttribute(String.join(" ", parts));
180 }
181 }
182
183
184
185
186
187
188
189
190
191
192 @JsxFunction
193 public static void remove(final Context context, final VarScope scope,
194 final Scriptable thisObj, final Object[] args, final Function function) {
195 final DOMTokenList list = (DOMTokenList) thisObj;
196
197 final String value = list.getValue();
198 if (value == null) {
199 return;
200 }
201
202 if (args.length > 0) {
203 final List<String> parts = split(list.getValue());
204
205 for (final Object arg : args) {
206 final String token = JavaScriptEngine.toString(arg);
207
208 if (org.htmlunit.util.StringUtils.isEmptyOrNull(token)) {
209 throw JavaScriptEngine.asJavaScriptException(
210 (HtmlUnitScriptable) getTopLevelScope(scope).getGlobalThis(),
211 "DOMTokenList: remove() does not support empty tokens",
212 DOMException.SYNTAX_ERR);
213 }
214 if (StringUtils.containsAny(token, WHITESPACE_CHARS)) {
215 throw JavaScriptEngine.asJavaScriptException(
216 (HtmlUnitScriptable) getTopLevelScope(scope).getGlobalThis(),
217 "DOMTokenList: remove() does not support whitespace chars in tokens",
218 DOMException.INVALID_CHARACTER_ERR);
219 }
220
221 parts.remove(token);
222 }
223 list.updateAttribute(String.join(" ", parts));
224 }
225 }
226
227
228
229
230
231
232
233
234 @JsxFunction
235 public boolean replace(final String oldToken, final String newToken) {
236 if (org.htmlunit.util.StringUtils.isEmptyOrNull(oldToken)) {
237 throw JavaScriptEngine.asJavaScriptException(
238 getWindow(),
239 "Empty oldToken not allowed",
240 DOMException.SYNTAX_ERR);
241 }
242 if (StringUtils.containsAny(oldToken, WHITESPACE_CHARS)) {
243 throw JavaScriptEngine.asJavaScriptException(
244 getWindow(),
245 "DOMTokenList: replace() oldToken contains whitespace",
246 DOMException.INVALID_CHARACTER_ERR);
247 }
248
249 if (org.htmlunit.util.StringUtils.isEmptyOrNull(newToken)) {
250 throw JavaScriptEngine.asJavaScriptException(
251 getWindow(),
252 "Empty newToken not allowed",
253 DOMException.SYNTAX_ERR);
254 }
255 if (StringUtils.containsAny(newToken, WHITESPACE_CHARS)) {
256 throw JavaScriptEngine.asJavaScriptException(
257 getWindow(),
258 "DOMTokenList: replace() newToken contains whitespace",
259 DOMException.INVALID_CHARACTER_ERR);
260 }
261
262 final String value = getValue();
263 if (value == null) {
264 return false;
265 }
266 final List<String> parts = split(value);
267 final int pos = parts.indexOf(oldToken);
268 if (pos == -1) {
269 return false;
270 }
271
272 parts.set(pos, newToken);
273 updateAttribute(String.join(" ", parts));
274 return true;
275 }
276
277
278
279
280
281
282 @JsxFunction
283 public boolean toggle(final String token) {
284 if (org.htmlunit.util.StringUtils.isEmptyOrNull(token)) {
285 throw JavaScriptEngine.asJavaScriptException(
286 getWindow(),
287 "DOMTokenList: toggle() does not support empty tokens",
288 DOMException.SYNTAX_ERR);
289 }
290 if (StringUtils.containsAny(token, WHITESPACE_CHARS)) {
291 throw JavaScriptEngine.asJavaScriptException(
292 getWindow(),
293 "DOMTokenList: toggle() does not support whitespace chars in tokens",
294 DOMException.INVALID_CHARACTER_ERR);
295 }
296
297 final List<String> parts = split(getValue());
298 if (parts.contains(token)) {
299 parts.remove(token);
300 updateAttribute(String.join(" ", parts));
301 return false;
302 }
303
304 parts.add(token);
305 updateAttribute(String.join(" ", parts));
306 return true;
307 }
308
309
310
311
312
313
314 @JsxFunction
315 public boolean contains(final String token) {
316 if (org.htmlunit.util.StringUtils.isBlank(token)) {
317 return false;
318 }
319
320 if (org.htmlunit.util.StringUtils.isEmptyOrNull(token)) {
321 throw JavaScriptEngine.reportRuntimeError("DOMTokenList: contains() does not support empty tokens");
322 }
323 if (StringUtils.containsAny(token, WHITESPACE_CHARS)) {
324 throw JavaScriptEngine.reportRuntimeError(
325 "DOMTokenList: contains() does not support whitespace chars in tokens");
326 }
327
328 final List<String> parts = split(getValue());
329 return parts.contains(token);
330 }
331
332
333
334
335
336
337 @JsxFunction
338 public String item(final int index) {
339 if (index < 0) {
340 return null;
341 }
342
343 final String value = getValue();
344 if (org.htmlunit.util.StringUtils.isEmptyOrNull(value)) {
345 return null;
346 }
347
348 final List<String> parts = split(value);
349 if (index < parts.size()) {
350 return parts.get(index);
351 }
352
353 return null;
354 }
355
356
357
358
359
360 @JsxFunction
361 public Scriptable keys() {
362 return JavaScriptEngine.newArrayIteratorTypeKeys(getParentScope(), this);
363 }
364
365
366
367
368 @Override
369 public Object[] getIds() {
370 final Object[] normalIds = super.getIds();
371
372 final String value = getValue();
373 if (org.htmlunit.util.StringUtils.isEmptyOrNull(value)) {
374 return normalIds;
375 }
376
377 final List<String> parts = split(getValue());
378 final Object[] ids = new Object[parts.size() + normalIds.length];
379 final int size = parts.size();
380 for (int i = 0; i < size; i++) {
381 ids[i] = i;
382 }
383 System.arraycopy(normalIds, 0, ids, parts.size(), normalIds.length);
384
385 return ids;
386 }
387
388
389
390
391
392 @JsxFunction
393 @JsxSymbol(symbolName = "iterator")
394 public Scriptable values() {
395 return JavaScriptEngine.newArrayIteratorTypeValues(getParentScope(), this);
396 }
397
398
399
400
401
402 @JsxFunction
403 public Scriptable entries() {
404 return JavaScriptEngine.newArrayIteratorTypeEntries(getParentScope(), this);
405 }
406
407
408
409
410
411 @JsxFunction
412 public void forEach(final Object callback) {
413 if (!(callback instanceof Function function)) {
414 throw JavaScriptEngine.typeError(
415 "Foreach callback '" + JavaScriptEngine.toString(callback) + "' is not a function");
416 }
417
418 final String value = getValue();
419 if (org.htmlunit.util.StringUtils.isEmptyOrNull(value)) {
420 return;
421 }
422
423 final WebClient client = getWindow().getWebWindow().getWebClient();
424 final HtmlUnitContextFactory cf = client.getJavaScriptEngine().getContextFactory();
425
426 final ContextAction<Object> contextAction = cx -> {
427 final VarScope scope = getParentScope();
428
429 List<String> parts = split(value);
430 final int size = parts.size();
431 int i = 0;
432 while (i < size && i < parts.size()) {
433 function.call(cx, scope, this, new Object[] {parts.get(i), i, this});
434
435
436 parts = split(getValue());
437 i++;
438 }
439 return null;
440 };
441 cf.call(contextAction);
442 }
443
444
445
446
447 @Override
448 public Object get(final int index, final Scriptable start) {
449 final Object value = item(index);
450 if (value == null) {
451 return JavaScriptEngine.UNDEFINED;
452 }
453 return value;
454 }
455
456 private void updateAttribute(final String value) {
457 final DomElement domNode = (DomElement) getDomNodeOrDie();
458
459
460
461 final DomAttr attr = domNode.getPage().createAttribute(attributeName_);
462 attr.setValue(value);
463 domNode.setAttributeNode(attr);
464 }
465
466 private static List<String> split(final String value) {
467 if (org.htmlunit.util.StringUtils.isEmptyOrNull(value)) {
468 return new ArrayList<>();
469 }
470
471 final String[] parts = StringUtils.split(value, WHITESPACE_CHARS);
472
473
474 final List<String> result = new ArrayList<>();
475 for (final String part : parts) {
476 if (!result.contains(part)) {
477 result.add(part);
478 }
479 }
480 return result;
481 }
482 }