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_SVG_NL;
18
19 import java.util.ArrayList;
20 import java.util.IdentityHashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24
25 import org.htmlunit.Page;
26 import org.htmlunit.WebWindow;
27 import org.htmlunit.css.ComputedCssStyleDeclaration;
28 import org.htmlunit.css.StyleAttributes.Definition;
29 import org.htmlunit.html.DomCDataSection;
30 import org.htmlunit.html.DomComment;
31 import org.htmlunit.html.DomElement;
32 import org.htmlunit.html.DomNode;
33 import org.htmlunit.html.DomText;
34 import org.htmlunit.html.HtmlBody;
35 import org.htmlunit.html.HtmlBreak;
36 import org.htmlunit.html.HtmlCheckBoxInput;
37 import org.htmlunit.html.HtmlDetails;
38 import org.htmlunit.html.HtmlElement;
39 import org.htmlunit.html.HtmlExample;
40 import org.htmlunit.html.HtmlHiddenInput;
41 import org.htmlunit.html.HtmlInlineFrame;
42 import org.htmlunit.html.HtmlInput;
43 import org.htmlunit.html.HtmlListing;
44 import org.htmlunit.html.HtmlMenu;
45 import org.htmlunit.html.HtmlNoFrames;
46 import org.htmlunit.html.HtmlNoScript;
47 import org.htmlunit.html.HtmlOption;
48 import org.htmlunit.html.HtmlOrderedList;
49 import org.htmlunit.html.HtmlPlainText;
50 import org.htmlunit.html.HtmlPreformattedText;
51 import org.htmlunit.html.HtmlRadioButtonInput;
52 import org.htmlunit.html.HtmlResetInput;
53 import org.htmlunit.html.HtmlScript;
54 import org.htmlunit.html.HtmlSelect;
55 import org.htmlunit.html.HtmlStyle;
56 import org.htmlunit.html.HtmlSubmitInput;
57 import org.htmlunit.html.HtmlSummary;
58 import org.htmlunit.html.HtmlSvg;
59 import org.htmlunit.html.HtmlTable;
60 import org.htmlunit.html.HtmlTableBody;
61 import org.htmlunit.html.HtmlTableCell;
62 import org.htmlunit.html.HtmlTableFooter;
63 import org.htmlunit.html.HtmlTableHeader;
64 import org.htmlunit.html.HtmlTableRow;
65 import org.htmlunit.html.HtmlTextArea;
66 import org.htmlunit.html.HtmlTitle;
67 import org.htmlunit.html.HtmlUnorderedList;
68 import org.htmlunit.html.TableRowGroup;
69 import org.htmlunit.html.serializer.HtmlSerializerVisibleText.HtmlSerializerTextBuilder.Mode;
70 import org.htmlunit.svg.SvgDesc;
71 import org.htmlunit.svg.SvgTitle;
72 import org.htmlunit.util.StringUtils;
73
74
75
76
77
78
79
80
81
82 public class HtmlSerializerVisibleText {
83
84
85 private static final int ESTIMATED_CHAR_WIDTH = 16;
86
87
88 private static final int ESTIMATED_LINE_HEIGHT = 20;
89
90
91
92
93
94 private static final Set<String> BLOCK_DISPLAYS = Set.of(
95 "block", "list-item", "flex", "grid", "flow-root", "contents",
96 "table", "table-row", "table-caption",
97 "table-row-group", "table-header-group", "table-footer-group",
98 "ruby", "ruby-base", "ruby-text", "ruby-text-container");
99
100
101
102
103
104
105
106 private final Map<DomNode, Boolean> displayedCache_ = new IdentityHashMap<>();
107
108 private final Map<DomNode, Boolean> transparentCache_ = new IdentityHashMap<>();
109
110 private final Map<DomNode, Mode> whiteSpaceCache_ = new IdentityHashMap<>();
111
112
113 private final boolean webDriverVisibility_;
114
115
116
117
118 public HtmlSerializerVisibleText() {
119 this(true);
120 }
121
122
123
124
125
126
127
128
129
130
131 public HtmlSerializerVisibleText(final boolean webDriverVisibility) {
132 webDriverVisibility_ = webDriverVisibility;
133 }
134
135
136
137
138
139
140 public String asText(final DomNode node) {
141 if (node instanceof HtmlBreak) {
142 return "";
143 }
144
145 displayedCache_.clear();
146 transparentCache_.clear();
147 whiteSpaceCache_.clear();
148
149 final HtmlSerializerTextBuilder builder = new HtmlSerializerTextBuilder();
150 appendNode(builder, node, whiteSpaceStyle(node, Mode.WHITE_SPACE_NORMAL));
151 return builder.getText();
152 }
153
154
155
156
157
158
159
160
161 protected void appendChildren(final HtmlSerializerTextBuilder builder, final DomNode node, final Mode mode) {
162 final Mode childMode = updateWhiteSpaceStyle(node, mode);
163 for (final DomNode child : node.getChildren()) {
164 appendNode(builder, child, childMode);
165 }
166 }
167
168
169
170
171
172
173
174
175
176 protected void appendNode(final HtmlSerializerTextBuilder builder, final DomNode node, final Mode mode) {
177 if (node instanceof DomCDataSection) {
178
179 }
180 else if (node instanceof DomText text1) {
181 appendText(builder, text1, mode);
182 }
183 else if (node instanceof DomComment comment) {
184 appendComment(builder, comment, mode);
185 }
186 else if (node instanceof HtmlBreak break1) {
187 appendBreak(builder, break1, mode);
188 }
189 else if (node instanceof HtmlHiddenInput input4) {
190 appendHiddenInput(builder, input4, mode);
191 }
192 else if (node instanceof HtmlScript script1) {
193 appendScript(builder, script1, mode);
194 }
195 else if (node instanceof HtmlStyle style) {
196 appendStyle(builder, style, mode);
197 }
198 else if (node instanceof HtmlNoFrames frames) {
199 appendNoFrames(builder, frames, mode);
200 }
201 else if (node instanceof HtmlTextArea area) {
202 appendTextArea(builder, area, mode);
203 }
204 else if (node instanceof HtmlTitle title) {
205 appendTitle(builder, title, mode);
206 }
207 else if (node instanceof HtmlTableRow row) {
208 appendTableRow(builder, row, mode);
209 }
210 else if (node instanceof HtmlSelect select) {
211 appendSelect(builder, select, mode);
212 }
213 else if (node instanceof HtmlOption option) {
214 appendOption(builder, option, mode);
215 }
216 else if (node instanceof HtmlSubmitInput input3) {
217 appendSubmitInput(builder, input3, mode);
218 }
219 else if (node instanceof HtmlResetInput input2) {
220 appendResetInput(builder, input2, mode);
221 }
222 else if (node instanceof HtmlCheckBoxInput input1) {
223 appendCheckBoxInput(builder, input1, mode);
224 }
225 else if (node instanceof HtmlRadioButtonInput input) {
226 appendRadioButtonInput(builder, input, mode);
227 }
228 else if (node instanceof HtmlInput) {
229
230 }
231 else if (node instanceof HtmlTable table) {
232 appendTable(builder, table, mode);
233 }
234 else if (node instanceof HtmlOrderedList list1) {
235 appendOrderedList(builder, list1, mode);
236 }
237 else if (node instanceof HtmlUnorderedList list) {
238 appendUnorderedList(builder, list, mode);
239 }
240 else if (node instanceof HtmlPreformattedText
241 || node instanceof HtmlListing
242 || node instanceof HtmlExample
243 || node instanceof HtmlPlainText) {
244 appendPreformattedText(builder, (HtmlElement) node, mode);
245 }
246 else if (node instanceof HtmlInlineFrame frame) {
247 appendInlineFrame(builder, frame, mode);
248 }
249 else if (node instanceof HtmlMenu menu) {
250 appendMenu(builder, menu, mode);
251 }
252 else if (node instanceof HtmlDetails details) {
253 appendDetails(builder, details, mode);
254 }
255 else if (node instanceof HtmlSvg svg) {
256 appendSvg(builder, svg, mode);
257 }
258 else if (node instanceof HtmlNoScript script && node.getPage().getWebClient().isJavaScriptEnabled()) {
259 appendNoScript(builder, script, mode);
260 }
261 else {
262 appendDomNode(builder, node, mode);
263 }
264 }
265
266
267
268
269
270
271
272
273 protected void appendDomNode(final HtmlSerializerTextBuilder builder,
274 final DomNode domNode, final Mode mode) {
275 final boolean block;
276 if (domNode instanceof HtmlBody) {
277 block = false;
278 }
279 else if (domNode instanceof DomElement element) {
280 block = isBlockLevel(element);
281 }
282 else {
283 block = false;
284 }
285
286 if (block) {
287 builder.appendBlockSeparator();
288 }
289 appendChildren(builder, domNode, mode);
290 if (block) {
291 builder.appendBlockSeparator();
292 }
293 else if (isCssTableCell(domNode) && isCssTableCell(domNode.getNextElementSibling())) {
294 builder.appendCellSeparator();
295 }
296 }
297
298
299
300
301
302
303
304 private static boolean isCssTableCell(final DomNode node) {
305 if (node instanceof DomElement element && !(node instanceof HtmlTableCell)) {
306 final WebWindow window = node.getPage().getEnclosingWindow();
307 return "table-cell".equals(window.getComputedStyle(element, null).getDisplay());
308 }
309 return false;
310 }
311
312
313
314
315
316
317
318
319 protected void appendHiddenInput(final HtmlSerializerTextBuilder builder,
320 final HtmlHiddenInput htmlHiddenInput, final Mode mode) {
321
322 }
323
324
325
326
327
328
329
330
331 protected void appendScript(final HtmlSerializerTextBuilder builder,
332 final HtmlScript htmlScript, final Mode mode) {
333
334 }
335
336
337
338
339
340
341
342
343 protected void appendStyle(final HtmlSerializerTextBuilder builder,
344 final HtmlStyle htmlStyle, final Mode mode) {
345
346 }
347
348
349
350
351
352
353
354
355 protected void appendNoScript(final HtmlSerializerTextBuilder builder,
356 final HtmlNoScript htmlNoScript, final Mode mode) {
357
358 }
359
360
361
362
363
364
365
366
367 protected void appendNoFrames(final HtmlSerializerTextBuilder builder,
368 final HtmlNoFrames htmlNoFrames, final Mode mode) {
369
370 }
371
372
373
374
375
376
377
378
379 protected void appendSubmitInput(final HtmlSerializerTextBuilder builder,
380 final HtmlSubmitInput htmlSubmitInput, final Mode mode) {
381
382 }
383
384
385
386
387
388
389
390
391 protected void appendInput(final HtmlSerializerTextBuilder builder,
392 final HtmlInput htmlInput, final Mode mode) {
393 builder.append(htmlInput.getValueAttribute(), mode);
394 }
395
396
397
398
399
400
401
402
403 protected void appendResetInput(final HtmlSerializerTextBuilder builder,
404 final HtmlResetInput htmlResetInput, final Mode mode) {
405
406 }
407
408
409
410
411
412
413
414
415 private static boolean isBlockLevel(final DomNode node) {
416 if (node instanceof DomElement element) {
417 final WebWindow window = node.getPage().getEnclosingWindow();
418 final ComputedCssStyleDeclaration style = window.getComputedStyle(element, null);
419 final String display = style.getDisplay();
420 if (BLOCK_DISPLAYS.contains(display)) {
421 return true;
422 }
423
424
425 final String position = style.getPosition();
426 if ("absolute".equals(position) || "fixed".equals(position)) {
427 return true;
428 }
429 final String floatValue = style.getStyleAttribute(Definition.FLOAT, false);
430 if ("left".equals(floatValue) || "right".equals(floatValue)) {
431 return true;
432 }
433
434
435 if (element.getParentNode() instanceof DomElement parent) {
436 final String parentDisplay = window.getComputedStyle(parent, null).getDisplay();
437 return "flex".equals(parentDisplay) || "inline-flex".equals(parentDisplay)
438 || "grid".equals(parentDisplay) || "inline-grid".equals(parentDisplay);
439 }
440 }
441 return false;
442 }
443
444
445
446
447
448
449
450
451 protected void appendMenu(final HtmlSerializerTextBuilder builder,
452 final HtmlMenu htmlMenu, final Mode mode) {
453 builder.appendBlockSeparator();
454 boolean first = true;
455 for (final DomNode item : htmlMenu.getChildren()) {
456 if (!first) {
457 builder.appendBlockSeparator();
458 }
459 first = false;
460 appendNode(builder, item, mode);
461 }
462 builder.appendBlockSeparator();
463 }
464
465
466
467
468
469
470
471
472 protected void appendDetails(final HtmlSerializerTextBuilder builder,
473 final HtmlDetails htmlDetails, final Mode mode) {
474 if (htmlDetails.isOpen()) {
475 appendChildren(builder, htmlDetails, mode);
476 return;
477 }
478
479
480
481 for (final DomNode child : htmlDetails.getChildren()) {
482 if (child instanceof HtmlSummary || child instanceof DomText) {
483 appendNode(builder, child, mode);
484 }
485 }
486 }
487
488
489
490
491
492
493
494
495 protected void appendTitle(final HtmlSerializerTextBuilder builder,
496 final HtmlTitle htmlTitle, final Mode mode) {
497
498 }
499
500
501
502
503
504
505
506
507 protected void appendTableRow(final HtmlSerializerTextBuilder builder,
508 final HtmlTableRow htmlTableRow, final Mode mode) {
509 boolean first = true;
510 for (final HtmlTableCell cell : htmlTableRow.getCells()) {
511 if (first) {
512 first = false;
513 }
514 else {
515 builder.appendCellSeparator();
516 }
517 appendChildren(builder, cell, mode);
518 }
519 }
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534 protected boolean isDisplayed(final DomNode domNode) {
535 return displayedCache_.computeIfAbsent(domNode, node -> node.isDisplayed() && !isTransparent(node));
536 }
537
538
539
540
541
542
543
544
545
546
547
548 private static boolean isOffscreen(final DomElement element, final ComputedCssStyleDeclaration style) {
549 final String position = style.getPosition();
550 if (!"absolute".equals(position) && !"fixed".equals(position)) {
551 return false;
552 }
553
554 final int left = pixelValue(style.getStyleAttribute(Definition.LEFT, false));
555 if (left < 0) {
556 int width = pixelValue(style.getStyleAttribute(Definition.WIDTH, false));
557 if (width <= 0) {
558 width = element.getTextContent().length() * ESTIMATED_CHAR_WIDTH;
559 }
560 if (left + width <= 0) {
561 return true;
562 }
563 }
564
565 final int top = pixelValue(style.getStyleAttribute(Definition.TOP, false));
566 if (top < 0) {
567 int height = pixelValue(style.getStyleAttribute(Definition.HEIGHT, false));
568 if (height <= 0) {
569 height = ESTIMATED_LINE_HEIGHT;
570 }
571 return top + height <= 0;
572 }
573 return false;
574 }
575
576
577
578
579
580
581
582 private static int pixelValue(final String length) {
583 if (StringUtils.isEmptyOrNull(length) || !length.endsWith("px")) {
584 return 0;
585 }
586 try {
587 return (int) Double.parseDouble(length.substring(0, length.length() - 2).trim());
588 }
589 catch (final NumberFormatException e) {
590 return 0;
591 }
592 }
593
594
595
596
597
598
599
600
601
602 private boolean isTransparent(final DomNode domNode) {
603 if (!webDriverVisibility_) {
604 return false;
605 }
606
607 final Page page = domNode.getPage();
608 if (page == null) {
609 return false;
610 }
611 final WebWindow window = page.getEnclosingWindow();
612 if (!window.getWebClient().getOptions().isCssEnabled()) {
613 return false;
614 }
615
616
617
618 final List<DomNode> path = new ArrayList<>();
619 boolean transparent = false;
620 DomNode node = domNode;
621 while (node != null) {
622 final Boolean cached = transparentCache_.get(node);
623 if (cached != null) {
624 transparent = cached;
625 break;
626 }
627 path.add(node);
628 if (node instanceof DomElement element
629 && isTransparentElement(element, window.getComputedStyle(element, null))) {
630 transparent = true;
631 break;
632 }
633 node = node.getParentNode();
634 }
635
636 for (final DomNode pathNode : path) {
637 transparentCache_.put(pathNode, transparent);
638 }
639 return transparent;
640 }
641
642
643
644
645
646
647
648
649
650 private static boolean isTransparentElement(final DomElement element, final ComputedCssStyleDeclaration style) {
651
652
653
654 if ("hidden".equals(style.getStyleAttribute("content-visibility"))
655 || isOffscreen(element, style)) {
656 return true;
657 }
658
659 final String opacity = style.getStyleAttribute(Definition.OPACITY, false);
660 if (!StringUtils.isEmptyOrNull(opacity)) {
661 try {
662 return Double.parseDouble(opacity.trim()) <= 0;
663 }
664 catch (final NumberFormatException e) {
665
666 }
667 }
668 return false;
669 }
670
671
672
673
674
675
676
677
678 protected void appendTextArea(final HtmlSerializerTextBuilder builder,
679 final HtmlTextArea htmlTextArea, final Mode mode) {
680 if (isDisplayed(htmlTextArea)) {
681 builder.append(htmlTextArea.getDefaultValue(), whiteSpaceStyle(htmlTextArea, Mode.PRE));
682 builder.trimRight(Mode.PRE);
683 }
684 }
685
686
687
688
689
690
691
692
693 protected void appendTable(final HtmlSerializerTextBuilder builder,
694 final HtmlTable htmlTable, final Mode mode) {
695 builder.appendBlockSeparator();
696 final String caption = htmlTable.getCaptionText();
697 if (caption != null) {
698 builder.append(caption, mode);
699 builder.appendBlockSeparator();
700 }
701
702 boolean first = true;
703
704
705 final HtmlTableHeader tableHeader = htmlTable.getHeader();
706 if (tableHeader != null) {
707 first = appendTableRows(builder, mode, tableHeader.getRows(), true, null, null);
708 }
709 final HtmlTableFooter tableFooter = htmlTable.getFooter();
710
711
712 final boolean footerFirst = tableFooter != null && isFooterBeforeBody(htmlTable);
713 if (footerFirst) {
714 first = appendTableRows(builder, mode, tableFooter.getRows(), first, null, null);
715 }
716
717 final List<HtmlTableRow> tableRows = htmlTable.getRows();
718 first = appendTableRows(builder, mode, tableRows, first, tableHeader, tableFooter);
719
720 if (tableFooter != null && !footerFirst) {
721 first = appendTableRows(builder, mode, tableFooter.getRows(), first, null, null);
722 }
723 else if (tableRows.isEmpty()) {
724 final DomNode firstChild = htmlTable.getFirstChild();
725 if (firstChild != null) {
726 appendNode(builder, firstChild, mode);
727 }
728 }
729
730 builder.appendBlockSeparator();
731 }
732
733
734
735
736
737
738
739
740
741
742
743
744 protected boolean appendTableRows(final HtmlSerializerTextBuilder builder, final Mode mode,
745 final List<HtmlTableRow> rows, boolean first, final TableRowGroup skipParent1,
746 final TableRowGroup skipParent2) {
747 for (final HtmlTableRow row : rows) {
748 if (row.getParentNode() == skipParent1 || row.getParentNode() == skipParent2) {
749 continue;
750 }
751 if (!first) {
752 builder.appendBlockSeparator();
753 }
754 first = false;
755 appendTableRow(builder, row, mode);
756 }
757 return first;
758 }
759
760
761
762
763
764
765
766
767 private static boolean isFooterBeforeBody(final HtmlTable htmlTable) {
768 for (final DomNode child : htmlTable.getChildren()) {
769 if (child instanceof HtmlTableFooter) {
770 return true;
771 }
772 if (child instanceof HtmlTableBody || child instanceof HtmlTableRow) {
773 return false;
774 }
775 }
776 return false;
777 }
778
779
780
781
782
783
784
785
786 protected void appendSelect(final HtmlSerializerTextBuilder builder,
787 final HtmlSelect htmlSelect, final Mode mode) {
788
789 final List<HtmlOption> options = htmlSelect.getOptions();
790 if (options.isEmpty()) {
791 return;
792 }
793
794 builder.appendBlockSeparator();
795 boolean leadingNlPending = false;
796
797
798 for (final HtmlOption item : options) {
799 if (leadingNlPending) {
800 builder.appendBlockSeparator();
801 }
802
803 builder.resetContentAdded();
804 appendNode(builder, item, mode);
805 leadingNlPending = builder.wasContentAdded();
806 }
807 builder.appendBlockSeparator();
808 }
809
810
811
812
813
814
815
816
817 protected void appendOption(final HtmlSerializerTextBuilder builder,
818 final HtmlOption htmlOption, final Mode mode) {
819
820
821 if (htmlOption.getEnclosingSelect() == null) {
822 builder.appendBlockSeparator();
823 builder.appendBlockSeparator();
824 return;
825 }
826
827
828
829 for (final DomNode child : htmlOption.getChildren()) {
830 appendNode(builder, child, Mode.WHITE_SPACE_NORMAL);
831 }
832 }
833
834
835
836
837
838
839
840
841 protected void appendOrderedList(final HtmlSerializerTextBuilder builder,
842 final HtmlOrderedList htmlOrderedList, final Mode mode) {
843 appendListItems(builder, htmlOrderedList, mode);
844 }
845
846
847
848
849
850
851
852
853 protected void appendUnorderedList(final HtmlSerializerTextBuilder builder,
854 final HtmlUnorderedList htmlUnorderedList, final Mode mode) {
855 appendListItems(builder, htmlUnorderedList, mode);
856 }
857
858
859
860
861
862
863
864
865
866 private void appendListItems(final HtmlSerializerTextBuilder builder,
867 final HtmlElement list, final Mode mode) {
868 builder.appendBlockSeparator();
869 boolean leadingNlPending = false;
870 final Mode listMode = whiteSpaceStyle(list, mode);
871 for (final DomNode item : list.getChildren()) {
872 if (leadingNlPending) {
873 builder.appendBlockSeparator();
874 }
875
876 builder.resetContentAdded();
877 appendNode(builder, item, whiteSpaceStyle(item, listMode));
878 leadingNlPending = builder.wasContentAdded() && isBlockLevel(item);
879 }
880 builder.appendBlockSeparator();
881 }
882
883
884
885
886
887
888
889
890 protected void appendPreformattedText(final HtmlSerializerTextBuilder builder,
891 final HtmlElement htmlPreformattedText, final Mode mode) {
892 if (isDisplayed(htmlPreformattedText)) {
893 builder.appendBlockSeparator();
894 appendChildren(builder, htmlPreformattedText, whiteSpaceStyle(htmlPreformattedText, Mode.PRE));
895 builder.appendBlockSeparator();
896 }
897 }
898
899
900
901
902
903
904
905
906
907 protected void appendSvg(final HtmlSerializerTextBuilder builder,
908 final HtmlSvg htmlSvg, final Mode mode) {
909 final boolean block = htmlSvg.getPage().getWebClient().getBrowserVersion()
910 .hasFeature(JS_INNER_TEXT_SVG_NL)
911 && hasSvgText(htmlSvg);
912 if (block) {
913 builder.appendBlockSeparator();
914 }
915 appendChildren(builder, htmlSvg, mode);
916 if (block) {
917 builder.appendBlockSeparator();
918 }
919 }
920
921
922
923
924
925
926
927 private static boolean hasSvgText(final DomNode node) {
928 for (final DomNode child : node.getChildren()) {
929 if (child instanceof SvgTitle || child instanceof SvgDesc) {
930 continue;
931 }
932 if (child instanceof DomText text) {
933 if (!StringUtils.isBlank(text.getData())) {
934 return true;
935 }
936 }
937 else if (hasSvgText(child)) {
938 return true;
939 }
940 }
941 return false;
942 }
943
944
945
946
947
948
949
950
951 protected void appendInlineFrame(final HtmlSerializerTextBuilder builder,
952 final HtmlInlineFrame htmlInlineFrame, final Mode mode) {
953
954
955 if (isDisplayed(htmlInlineFrame)) {
956 appendChildren(builder, htmlInlineFrame, mode);
957 }
958 }
959
960
961
962
963
964
965
966
967 protected void appendText(final HtmlSerializerTextBuilder builder, final DomText domText, final Mode mode) {
968 final DomNode parent = domText.getParentNode();
969 if (parent instanceof HtmlTitle
970 || parent instanceof HtmlScript) {
971 builder.append(domText.getData(), Mode.WHITE_SPACE_PRE_LINE);
972 return;
973 }
974
975
976 if (parent == null
977 || parent instanceof HtmlOption
978 || isDisplayed(parent)) {
979 String data = domText.getData();
980
981 if (data.indexOf('\u200B') > -1) {
982 data = data.replace("\u200B", "");
983 }
984 builder.append(data, mode);
985 }
986 }
987
988
989
990
991
992
993
994
995 protected void appendComment(final HtmlSerializerTextBuilder builder,
996 final DomComment domComment, final Mode mode) {
997
998 }
999
1000
1001
1002
1003
1004
1005
1006
1007 protected void appendBreak(final HtmlSerializerTextBuilder builder,
1008 final HtmlBreak htmlBreak, final Mode mode) {
1009
1010 if (builder.isEmpty()) {
1011 return;
1012 }
1013 builder.appendBreak(mode);
1014 }
1015
1016
1017
1018
1019
1020
1021
1022
1023 protected void appendCheckBoxInput(final HtmlSerializerTextBuilder builder,
1024 final HtmlCheckBoxInput htmlCheckBoxInput, final Mode mode) {
1025
1026 }
1027
1028
1029
1030
1031
1032
1033
1034
1035 protected void appendRadioButtonInput(final HtmlSerializerTextBuilder builder,
1036 final HtmlRadioButtonInput htmlRadioButtonInput, final Mode mode) {
1037
1038 }
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048 protected Mode whiteSpaceStyle(final DomNode domNode, final Mode defaultMode) {
1049 final Page page = domNode.getPage();
1050 if (page == null) {
1051 return defaultMode;
1052 }
1053 final WebWindow window = page.getEnclosingWindow();
1054 if (!window.getWebClient().getOptions().isCssEnabled()) {
1055 return defaultMode;
1056 }
1057
1058
1059
1060 final List<DomNode> path = new ArrayList<>();
1061 Mode mode = null;
1062 DomNode node = domNode;
1063 while (node != null) {
1064 if (whiteSpaceCache_.containsKey(node)) {
1065 mode = whiteSpaceCache_.get(node);
1066 break;
1067 }
1068 path.add(node);
1069 if (node instanceof DomElement element) {
1070 mode = whiteSpaceMode(window.getComputedStyle(element, null));
1071 if (mode != null) {
1072 break;
1073 }
1074 }
1075 node = node.getParentNode();
1076 }
1077
1078 for (final DomNode pathNode : path) {
1079 whiteSpaceCache_.put(pathNode, mode);
1080 }
1081 return mode == null ? defaultMode : mode;
1082 }
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092 protected Mode updateWhiteSpaceStyle(final DomNode domNode, final Mode defaultMode) {
1093 if (domNode instanceof DomElement element) {
1094 final Page page = element.getPage();
1095 if (page != null) {
1096 final WebWindow window = page.getEnclosingWindow();
1097 if (window.getWebClient().getOptions().isCssEnabled()) {
1098 final Mode mode = whiteSpaceMode(window.getComputedStyle(element, null));
1099 if (mode != null) {
1100 return mode;
1101 }
1102 }
1103 }
1104 }
1105 return defaultMode;
1106 }
1107
1108
1109
1110
1111
1112
1113
1114 private static Mode whiteSpaceMode(final ComputedCssStyleDeclaration style) {
1115 final String value = style.getStyleAttribute(Definition.WHITE_SPACE, false);
1116 if (!StringUtils.isEmptyOrNull(value)) {
1117 if ("normal".equalsIgnoreCase(value) || "nowrap".equalsIgnoreCase(value)) {
1118 return Mode.WHITE_SPACE_NORMAL;
1119 }
1120 if ("pre".equalsIgnoreCase(value) || "pre-wrap".equalsIgnoreCase(value)) {
1121 return Mode.WHITE_SPACE_PRE;
1122 }
1123
1124 if ("pre-line".equalsIgnoreCase(value) || "break-spaces".equalsIgnoreCase(value)) {
1125 return Mode.WHITE_SPACE_PRE_LINE;
1126 }
1127 }
1128
1129 final String collapse = style.getStyleAttribute(Definition.WHITE_SPACE_COLLAPSE, false);
1130 if ("preserve".equalsIgnoreCase(collapse)) {
1131 return Mode.WHITE_SPACE_PRE;
1132 }
1133 if ("preserve-breaks".equalsIgnoreCase(collapse)
1134 || "break-spaces".equalsIgnoreCase(collapse)) {
1135 return Mode.WHITE_SPACE_PRE_LINE;
1136 }
1137 return null;
1138 }
1139
1140
1141
1142
1143 protected static class HtmlSerializerTextBuilder {
1144
1145 protected enum Mode {
1146
1147
1148
1149 PRE,
1150
1151
1152
1153
1154
1155
1156 WHITE_SPACE_NORMAL,
1157
1158
1159
1160
1161
1162 WHITE_SPACE_PRE,
1163
1164
1165
1166
1167
1168
1169 WHITE_SPACE_PRE_LINE
1170 }
1171
1172 private enum State {
1173 DEFAULT,
1174 EMPTY,
1175 BLANK_AT_END,
1176 NEWLINE_AT_END,
1177 BREAK_AT_END,
1178 BLOCK_SEPARATOR_AT_END
1179 }
1180
1181 private State state_;
1182 private final StringBuilder builder_;
1183 private int trimRightPos_;
1184 private boolean contentAdded_;
1185 private int lineStart_;
1186
1187
1188
1189
1190 public HtmlSerializerTextBuilder() {
1191 builder_ = new StringBuilder();
1192 state_ = State.EMPTY;
1193 trimRightPos_ = 0;
1194 }
1195
1196
1197
1198
1199
1200
1201
1202
1203 public void append(final String content, final Mode mode) {
1204 if (content == null) {
1205 return;
1206 }
1207 final int length = content.length();
1208 if (length == 0) {
1209 return;
1210 }
1211
1212 final int lastIndex = length - 1;
1213 for (int i = 0; i < length; i++) {
1214 char c = content.charAt(i);
1215
1216
1217 if (c == '\r') {
1218 if (i != lastIndex) {
1219 continue;
1220 }
1221 c = '\n';
1222 }
1223
1224 if (c == '\n') {
1225 if (mode == Mode.WHITE_SPACE_PRE) {
1226 switch (state_) {
1227 case EMPTY:
1228 case BLOCK_SEPARATOR_AT_END:
1229 break;
1230 default:
1231 builder_.append('\n');
1232 state_ = State.NEWLINE_AT_END;
1233 trimRightPos_ = builder_.length();
1234 break;
1235 }
1236 continue;
1237 }
1238
1239 if (mode == Mode.PRE) {
1240
1241 if (state_ == State.EMPTY) {
1242 continue;
1243 }
1244 builder_.append('\n');
1245 state_ = State.NEWLINE_AT_END;
1246 trimRightPos_ = builder_.length();
1247
1248 continue;
1249 }
1250
1251 if (mode == Mode.WHITE_SPACE_PRE_LINE) {
1252 switch (state_) {
1253 case EMPTY:
1254 case BLOCK_SEPARATOR_AT_END:
1255 break;
1256 default:
1257 builder_.append('\n');
1258 state_ = State.NEWLINE_AT_END;
1259 trimRightPos_ = builder_.length();
1260 break;
1261 }
1262 continue;
1263 }
1264
1265 switch (state_) {
1266 case EMPTY:
1267 case BLANK_AT_END:
1268 case BLOCK_SEPARATOR_AT_END:
1269 case NEWLINE_AT_END:
1270 case BREAK_AT_END:
1271 break;
1272 default:
1273 builder_.append(' ');
1274 state_ = State.BLANK_AT_END;
1275 break;
1276 }
1277 continue;
1278 }
1279
1280 if (c == ' ' || c == '\t' || c == '\f') {
1281 if (mode == Mode.WHITE_SPACE_PRE || mode == Mode.PRE) {
1282 appendBlank();
1283 continue;
1284 }
1285
1286 if (mode == Mode.WHITE_SPACE_PRE_LINE) {
1287 switch (state_) {
1288 case EMPTY:
1289 case BLANK_AT_END:
1290 case BREAK_AT_END:
1291 break;
1292 default:
1293 builder_.append(' ');
1294 state_ = State.BLANK_AT_END;
1295 break;
1296 }
1297 continue;
1298 }
1299
1300 switch (state_) {
1301 case EMPTY:
1302 case BLANK_AT_END:
1303 case BLOCK_SEPARATOR_AT_END:
1304 case NEWLINE_AT_END:
1305 case BREAK_AT_END:
1306 break;
1307 default:
1308 builder_.append(' ');
1309 state_ = State.BLANK_AT_END;
1310 break;
1311 }
1312 continue;
1313 }
1314
1315 if (c == (char) 160) {
1316 appendBlank();
1317 if (mode == Mode.WHITE_SPACE_NORMAL || mode == Mode.WHITE_SPACE_PRE_LINE) {
1318 state_ = State.DEFAULT;
1319 }
1320 continue;
1321 }
1322 builder_.append(c);
1323 state_ = State.DEFAULT;
1324 trimRightPos_ = builder_.length();
1325 contentAdded_ = true;
1326 }
1327 }
1328
1329
1330
1331
1332 public void appendBlockSeparator() {
1333
1334 if (state_ == State.BLANK_AT_END
1335 && trimRightPos_ == builder_.length()
1336 && isWhitespaceOnly(lineStart_)) {
1337 return;
1338 }
1339
1340 final int length = builder_.length();
1341 appendBlockSeparatorWorker();
1342 if (builder_.length() != length) {
1343 lineStart_ = builder_.length();
1344 }
1345 }
1346
1347
1348
1349
1350
1351
1352
1353 private boolean isWhitespaceOnly(final int start) {
1354 for (int i = start; i < builder_.length(); i++) {
1355 final char c = builder_.charAt(i);
1356 if (c != ' ' && c != '\t' && c != '\n' && c != (char) 160) {
1357 return false;
1358 }
1359 }
1360 return true;
1361 }
1362
1363
1364
1365
1366 private void appendBlockSeparatorWorker() {
1367 switch (state_) {
1368 case EMPTY:
1369 break;
1370 case BLANK_AT_END:
1371 builder_.setLength(trimRightPos_);
1372 if (builder_.length() == 0) {
1373 state_ = State.EMPTY;
1374 }
1375 else {
1376 builder_.append('\n');
1377 state_ = State.BLOCK_SEPARATOR_AT_END;
1378 }
1379 break;
1380 case BLOCK_SEPARATOR_AT_END:
1381 break;
1382 case NEWLINE_AT_END:
1383 case BREAK_AT_END:
1384 builder_.setLength(builder_.length() - 1);
1385 trimRightPos_ = trimRightPos_ - 1;
1386 if (builder_.length() == 0) {
1387 state_ = State.EMPTY;
1388 }
1389 else {
1390 builder_.append('\n');
1391 state_ = State.BLOCK_SEPARATOR_AT_END;
1392 }
1393 break;
1394 default:
1395 builder_.append('\n');
1396 state_ = State.BLOCK_SEPARATOR_AT_END;
1397 break;
1398 }
1399 }
1400
1401
1402
1403
1404
1405
1406 public void appendBreak(final Mode mode) {
1407
1408 if (state_ != State.BLOCK_SEPARATOR_AT_END) {
1409 builder_.setLength(trimRightPos_);
1410 }
1411
1412 builder_.append('\n');
1413 state_ = State.BREAK_AT_END;
1414 trimRightPos_ = builder_.length();
1415 lineStart_ = builder_.length();
1416 }
1417
1418
1419
1420
1421 public void appendBlank() {
1422 builder_.append(' ');
1423 state_ = State.BLANK_AT_END;
1424 trimRightPos_ = builder_.length();
1425 }
1426
1427
1428
1429
1430
1431 public void appendCellSeparator() {
1432 switch (state_) {
1433 case EMPTY:
1434 case BLANK_AT_END:
1435 case BLOCK_SEPARATOR_AT_END:
1436 case NEWLINE_AT_END:
1437 case BREAK_AT_END:
1438 return;
1439 default:
1440 builder_.append(' ');
1441 state_ = State.BLANK_AT_END;
1442 }
1443 }
1444
1445
1446
1447
1448
1449
1450 public void trimRight(final Mode mode) {
1451 if (mode == Mode.PRE) {
1452 switch (state_) {
1453 case BLOCK_SEPARATOR_AT_END:
1454 case NEWLINE_AT_END:
1455 case BREAK_AT_END:
1456 if (trimRightPos_ == builder_.length()) {
1457 trimRightPos_--;
1458 }
1459 break;
1460 default:
1461 break;
1462 }
1463 }
1464
1465 builder_.setLength(trimRightPos_);
1466 state_ = State.DEFAULT;
1467 if (builder_.length() == 0) {
1468 state_ = State.EMPTY;
1469 }
1470 }
1471
1472
1473
1474
1475
1476
1477 public boolean isEmpty() {
1478 return state_ == State.EMPTY;
1479 }
1480
1481
1482
1483
1484
1485
1486 public boolean wasContentAdded() {
1487 return contentAdded_;
1488 }
1489
1490
1491
1492
1493 public void resetContentAdded() {
1494 contentAdded_ = false;
1495 }
1496
1497
1498
1499
1500
1501
1502 public String getText() {
1503 return builder_.substring(0, trimRightPos_);
1504 }
1505 }
1506 }