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