1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.html.serializer;
16
17 import static org.htmlunit.BrowserVersionFeatures.JS_INNER_TEXT_SELECT_EMPTY;
18 import static org.htmlunit.BrowserVersionFeatures.JS_INNER_TEXT_SVG_NL;
19
20 import org.htmlunit.BrowserVersion;
21 import org.htmlunit.SgmlPage;
22 import org.htmlunit.WebWindow;
23 import org.htmlunit.css.ComputedCssStyleDeclaration;
24 import org.htmlunit.css.StyleAttributes.Definition;
25 import org.htmlunit.html.DomElement;
26 import org.htmlunit.html.DomNode;
27 import org.htmlunit.html.DomText;
28 import org.htmlunit.html.HtmlBreak;
29 import org.htmlunit.html.HtmlDefinitionTerm;
30 import org.htmlunit.html.HtmlDetails;
31 import org.htmlunit.html.HtmlHead;
32 import org.htmlunit.html.HtmlListItem;
33 import org.htmlunit.html.HtmlNoFrames;
34 import org.htmlunit.html.HtmlOption;
35 import org.htmlunit.html.HtmlParagraph;
36 import org.htmlunit.html.HtmlScript;
37 import org.htmlunit.html.HtmlSelect;
38 import org.htmlunit.html.HtmlStyle;
39 import org.htmlunit.html.HtmlSummary;
40 import org.htmlunit.html.HtmlSvg;
41 import org.htmlunit.html.HtmlTextArea;
42 import org.htmlunit.html.HtmlTitle;
43 import org.htmlunit.html.ScriptElement;
44 import org.htmlunit.html.serializer.HtmlSerializerInnerOuterText.HtmlSerializerTextBuilder.Mode;
45 import org.htmlunit.svg.SvgTitle;
46 import org.htmlunit.util.StringUtils;
47
48
49
50
51
52
53
54 public class HtmlSerializerInnerOuterText {
55
56 private final BrowserVersion browserVersion_;
57
58
59
60
61
62
63 public HtmlSerializerInnerOuterText(final BrowserVersion browserVersion) {
64 super();
65 browserVersion_ = browserVersion;
66 }
67
68
69
70
71
72
73 public String asText(final DomNode node) {
74 if (node instanceof HtmlBreak) {
75 return "";
76 }
77
78
79 if (node instanceof ScriptElement) {
80 final HtmlSerializerTextBuilder builder = new HtmlSerializerTextBuilder();
81 appendChildren(builder, node, Mode.WHITE_SPACE_NORMAL, false, false);
82 return builder.getText();
83 }
84
85
86 final boolean insideHead = node instanceof HtmlTitle;
87
88 final HtmlSerializerTextBuilder builder = new HtmlSerializerTextBuilder();
89 appendNode(builder, node, whiteSpaceStyle(node, Mode.WHITE_SPACE_NORMAL), insideHead, false);
90 return builder.getText();
91 }
92
93
94
95
96
97
98
99
100
101
102 protected void appendChildren(final HtmlSerializerTextBuilder builder, final DomNode node,
103 final Mode mode, final boolean insideHead, final boolean insideSelect) {
104 for (final DomNode child : node.getChildren()) {
105 appendNode(builder, child, mode, insideHead, insideSelect);
106 }
107 }
108
109
110
111
112
113
114
115
116
117
118
119 protected void appendNode(final HtmlSerializerTextBuilder builder, final DomNode node,
120 final Mode mode, final boolean insideHead, final boolean insideSelect) {
121 if (insideSelect && node instanceof HtmlBreak) {
122 return;
123 }
124
125 if (node instanceof DomText text) {
126 appendText(builder, text, mode);
127 }
128 else if (node instanceof HtmlBreak break1) {
129 appendBreak(builder, break1);
130 }
131 else if (node instanceof HtmlParagraph paragraph) {
132 appendParagraph(builder, paragraph, mode, insideHead, insideSelect);
133 }
134 else if (node instanceof HtmlListItem item) {
135 appendListItem(builder, item, mode, insideHead, insideSelect);
136 }
137 else if (node instanceof HtmlDetails details) {
138 appendDetails(builder, details, mode, insideHead, insideSelect);
139 }
140 else if (node instanceof HtmlHead) {
141 appendChildren(builder, node, mode, true, insideSelect);
142 }
143 else if (node instanceof HtmlNoFrames) {
144 appendChildren(builder, node, Mode.PLAIN, insideHead, insideSelect);
145 }
146 else if (node instanceof HtmlTitle && !insideHead) {
147
148 }
149 else if (node instanceof HtmlTextArea) {
150
151 }
152 else if (node instanceof ScriptElement) {
153 if (insideHead) {
154 appendChildren(builder, node, mode, insideHead, insideSelect);
155 }
156 }
157 else if (node instanceof HtmlSelect select) {
158 appendSelect(builder, select, mode, insideHead, insideSelect);
159 }
160 else if (node instanceof HtmlDefinitionTerm item) {
161 appendDefinitionTerm(builder, item, mode, insideHead, insideSelect);
162 }
163 else if (node instanceof HtmlSvg) {
164 if (browserVersion_.hasFeature(JS_INNER_TEXT_SVG_NL)) {
165 builder.appendRequiredLineBreak();
166 appendChildren(builder, node, mode, insideHead, insideSelect);
167 builder.appendRequiredLineBreak();
168 }
169 else {
170 appendChildren(builder, node, mode, insideHead, insideSelect);
171 }
172 }
173 else if (node instanceof SvgTitle) {
174
175 }
176 else {
177 appendChildren(builder, node, mode, insideHead, insideSelect);
178 }
179 }
180
181
182
183
184
185
186
187
188 protected void appendText(final HtmlSerializerTextBuilder builder, final DomText domText, final Mode mode) {
189 final DomNode parent = domText.getParentNode();
190 if (parent instanceof HtmlTitle
191 || parent instanceof HtmlStyle
192 || parent instanceof HtmlScript) {
193 builder.append(domText.getData(), Mode.PLAIN);
194 return;
195 }
196
197 if (parent == null
198 || parent instanceof HtmlNoFrames
199 || parent.isDisplayed()) {
200 builder.append(domText.getData(), mode);
201 }
202 }
203
204
205
206
207
208
209
210 protected void appendBreak(final HtmlSerializerTextBuilder builder,
211 final HtmlBreak htmlBreak) {
212 builder.appendRequiredLineBreak();
213 }
214
215
216
217
218
219
220
221
222
223
224 protected void appendParagraph(final HtmlSerializerTextBuilder builder,
225 final HtmlParagraph htmlParagraph, final Mode mode, final boolean insideHead, final boolean insideSelect) {
226 builder.appendRequiredLineBreak();
227 appendChildren(builder, htmlParagraph, mode, insideHead, insideSelect);
228 builder.appendRequiredLineBreak();
229 }
230
231
232
233
234
235
236
237
238
239
240 protected void appendListItem(final HtmlSerializerTextBuilder builder,
241 final HtmlListItem htmlListItem, final Mode mode, final boolean insideHead, final boolean insideSelect) {
242 builder.appendRequiredLineBreak();
243 appendChildren(builder, htmlListItem, mode, insideHead, insideSelect);
244 builder.appendRequiredLineBreak();
245 }
246
247
248
249
250
251
252
253
254
255 protected void appendDetails(final HtmlSerializerTextBuilder builder,
256 final HtmlDetails htmlDetails, final Mode mode,
257 final boolean insideHead, final boolean insideSelect) {
258 if (htmlDetails.isOpen()) {
259 appendChildren(builder, htmlDetails, mode, insideHead, insideSelect);
260 return;
261 }
262
263 for (final DomNode child : htmlDetails.getChildren()) {
264 if (child instanceof HtmlSummary) {
265 appendNode(builder, child, mode, insideHead, insideSelect);
266 }
267 }
268 }
269
270
271
272
273
274
275
276
277
278
279 protected void appendDefinitionTerm(final HtmlSerializerTextBuilder builder,
280 final HtmlDefinitionTerm htmlDefinitionTerm, final Mode mode,
281 final boolean insideHead, final boolean insideSelect) {
282 builder.appendRequiredLineBreak();
283 appendChildren(builder, htmlDefinitionTerm, mode, insideHead, insideSelect);
284 builder.appendRequiredLineBreak();
285 }
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302 protected void appendSelect(final HtmlSerializerTextBuilder builder,
303 final HtmlSelect htmlSelect, final Mode mode, final boolean insideHead, final boolean insideSelect) {
304 if (browserVersion_.hasFeature(JS_INNER_TEXT_SELECT_EMPTY)) {
305 return;
306 }
307
308 appendDescendantOptions(builder, htmlSelect, Mode.WHITE_SPACE_NORMAL, insideHead, true);
309 }
310
311
312
313
314
315
316
317
318
319
320 protected void appendDescendantOptions(final HtmlSerializerTextBuilder builder, final DomNode node,
321 final Mode mode, final boolean insideHead, final boolean insideSelect) {
322 for (final DomNode child : node.getChildNodes()) {
323 if (child instanceof HtmlOption) {
324 builder.appendRequiredLineBreak();
325 appendChildren(builder, child, Mode.WHITE_SPACE_NORMAL, insideHead, true);
326 }
327 else {
328 appendDescendantOptions(builder, child, Mode.WHITE_SPACE_NORMAL, insideHead, true);
329 }
330 }
331 }
332
333 private static Mode whiteSpaceStyle(final DomNode domNode, final Mode defaultMode) {
334 if (domNode instanceof DomElement) {
335 final SgmlPage page = domNode.getPage();
336 if (page != null) {
337 if (page.getWebClient().getOptions().isCssEnabled()) {
338 DomNode node = domNode;
339 while (node != null) {
340 if (node instanceof DomElement domElement) {
341 final WebWindow window = page.getEnclosingWindow();
342 if (window != null) {
343 final ComputedCssStyleDeclaration style =
344 window.getComputedStyle(domElement, null);
345 final String value = style.getStyleAttribute(Definition.WHITE_SPACE, false);
346
347 if (!StringUtils.isEmptyOrNull(value)) {
348 if ("normal".equalsIgnoreCase(value)) {
349 return Mode.WHITE_SPACE_NORMAL;
350 }
351 if ("nowrap".equalsIgnoreCase(value)) {
352 return Mode.WHITE_SPACE_NORMAL;
353 }
354 if ("pre".equalsIgnoreCase(value)) {
355 return Mode.WHITE_SPACE_PRE;
356 }
357 if ("pre-wrap".equalsIgnoreCase(value)) {
358 return Mode.WHITE_SPACE_PRE;
359 }
360 if ("pre-line".equalsIgnoreCase(value)) {
361 return Mode.WHITE_SPACE_PRE_LINE;
362 }
363 }
364 }
365 }
366 node = node.getParentNode();
367 }
368 }
369 }
370 }
371 return defaultMode;
372 }
373
374
375
376
377 protected static class HtmlSerializerTextBuilder {
378
379
380 protected enum Mode {
381
382
383
384 PLAIN,
385
386
387
388
389
390
391 WHITE_SPACE_NORMAL,
392
393
394
395
396
397 WHITE_SPACE_PRE,
398
399
400
401
402
403
404 WHITE_SPACE_PRE_LINE
405 }
406
407 private enum State {
408 DEFAULT,
409 EMPTY,
410 BLANK_AT_END,
411 BLANK_AT_END_AFTER_NEWLINE,
412 NEWLINE_AT_END,
413 BREAK_AT_END,
414 BLOCK_SEPARATOR_AT_END,
415 REQUIRED_LINE_BREAK_AT_END
416 }
417
418 private State state_;
419 private final StringBuilder builder_;
420 private int trimRightPos_;
421
422
423
424
425 public HtmlSerializerTextBuilder() {
426 builder_ = new StringBuilder();
427 state_ = State.EMPTY;
428 trimRightPos_ = 0;
429 }
430
431
432
433
434 public void appendRequiredLineBreak() {
435 if (state_ == State.EMPTY) {
436 return;
437 }
438
439
440 builder_.setLength(trimRightPos_);
441 if (trimRightPos_ == 0) {
442 state_ = State.EMPTY;
443 }
444
445 builder_.append('\n');
446 state_ = State.REQUIRED_LINE_BREAK_AT_END;
447 }
448
449
450
451
452
453
454
455
456 public void append(final String content, final Mode mode) {
457 if (content == null) {
458 return;
459 }
460 int length = content.length();
461 if (length == 0) {
462 return;
463 }
464
465 if (mode == Mode.PLAIN) {
466 builder_.append(content);
467 state_ = State.DEFAULT;
468 trimRightPos_ = builder_.length();
469 return;
470 }
471
472 length--;
473 final int contentLength = content.length();
474 for (int i = 0; i < contentLength; i++) {
475 char c = content.charAt(i);
476
477
478 if (c == '\r') {
479 if (length != i) {
480 continue;
481 }
482 c = '\n';
483 }
484
485 if (c == '\n') {
486 if (mode == Mode.WHITE_SPACE_PRE) {
487 switch (state_) {
488 case EMPTY:
489 case BLOCK_SEPARATOR_AT_END:
490 break;
491 default:
492 builder_.append('\n');
493 state_ = State.NEWLINE_AT_END;
494 trimRightPos_ = builder_.length();
495 break;
496 }
497 continue;
498 }
499
500 if (mode == Mode.WHITE_SPACE_PRE_LINE) {
501 switch (state_) {
502 case EMPTY:
503 case BLOCK_SEPARATOR_AT_END:
504 break;
505 case BLANK_AT_END:
506 builder_.setLength(trimRightPos_);
507 builder_.append('\n');
508 state_ = State.NEWLINE_AT_END;
509 trimRightPos_ = builder_.length();
510 break;
511 default:
512 builder_.append('\n');
513 state_ = State.NEWLINE_AT_END;
514 trimRightPos_ = builder_.length();
515 break;
516 }
517 continue;
518 }
519
520 switch (state_) {
521 case EMPTY:
522 case BLANK_AT_END:
523 case BLANK_AT_END_AFTER_NEWLINE:
524 case BLOCK_SEPARATOR_AT_END:
525 case NEWLINE_AT_END:
526 case BREAK_AT_END:
527 case REQUIRED_LINE_BREAK_AT_END:
528 break;
529 default:
530 builder_.append(' ');
531 state_ = State.BLANK_AT_END;
532 break;
533 }
534 continue;
535 }
536
537 if (c == ' ' || c == '\t' || c == '\f') {
538 if (mode == Mode.WHITE_SPACE_PRE) {
539 if (c == '\t') {
540 builder_.append('\t');
541 }
542 else {
543 builder_.append(' ');
544 }
545 state_ = State.BLANK_AT_END;
546 trimRightPos_ = builder_.length();
547
548 continue;
549 }
550
551 if (mode == Mode.WHITE_SPACE_PRE_LINE) {
552 switch (state_) {
553 case EMPTY:
554 case BLANK_AT_END:
555 case BLANK_AT_END_AFTER_NEWLINE:
556 case BREAK_AT_END:
557 case NEWLINE_AT_END:
558 break;
559 default:
560 builder_.append(' ');
561 state_ = State.BLANK_AT_END;
562 break;
563 }
564 continue;
565 }
566
567 switch (state_) {
568 case EMPTY:
569 case BLANK_AT_END:
570 case BLANK_AT_END_AFTER_NEWLINE:
571 case BLOCK_SEPARATOR_AT_END:
572 case NEWLINE_AT_END:
573 case BREAK_AT_END:
574 case REQUIRED_LINE_BREAK_AT_END:
575 break;
576 default:
577 builder_.append(' ');
578 state_ = State.BLANK_AT_END;
579 break;
580 }
581 continue;
582 }
583
584 builder_.append(c);
585 state_ = State.DEFAULT;
586 trimRightPos_ = builder_.length();
587 }
588 }
589
590
591
592
593
594
595 public String getText() {
596 return builder_.substring(0, trimRightPos_);
597 }
598 }
599 }