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