View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html.serializer;
16  
17  import static org.htmlunit.css.CssStyleSheet.BLOCK;
18  
19  import java.util.List;
20  
21  import org.htmlunit.Page;
22  import org.htmlunit.SgmlPage;
23  import org.htmlunit.WebWindow;
24  import org.htmlunit.css.ComputedCssStyleDeclaration;
25  import org.htmlunit.css.StyleAttributes.Definition;
26  import org.htmlunit.html.DomCDataSection;
27  import org.htmlunit.html.DomComment;
28  import org.htmlunit.html.DomElement;
29  import org.htmlunit.html.DomNode;
30  import org.htmlunit.html.DomText;
31  import org.htmlunit.html.HtmlBody;
32  import org.htmlunit.html.HtmlBreak;
33  import org.htmlunit.html.HtmlCheckBoxInput;
34  import org.htmlunit.html.HtmlDetails;
35  import org.htmlunit.html.HtmlHiddenInput;
36  import org.htmlunit.html.HtmlInlineFrame;
37  import org.htmlunit.html.HtmlInput;
38  import org.htmlunit.html.HtmlMenu;
39  import org.htmlunit.html.HtmlNoFrames;
40  import org.htmlunit.html.HtmlNoScript;
41  import org.htmlunit.html.HtmlOption;
42  import org.htmlunit.html.HtmlOrderedList;
43  import org.htmlunit.html.HtmlPreformattedText;
44  import org.htmlunit.html.HtmlRadioButtonInput;
45  import org.htmlunit.html.HtmlResetInput;
46  import org.htmlunit.html.HtmlScript;
47  import org.htmlunit.html.HtmlSelect;
48  import org.htmlunit.html.HtmlStyle;
49  import org.htmlunit.html.HtmlSubmitInput;
50  import org.htmlunit.html.HtmlSummary;
51  import org.htmlunit.html.HtmlTable;
52  import org.htmlunit.html.HtmlTableCell;
53  import org.htmlunit.html.HtmlTableFooter;
54  import org.htmlunit.html.HtmlTableHeader;
55  import org.htmlunit.html.HtmlTableRow;
56  import org.htmlunit.html.HtmlTextArea;
57  import org.htmlunit.html.HtmlTitle;
58  import org.htmlunit.html.HtmlUnorderedList;
59  import org.htmlunit.html.TableRowGroup;
60  import org.htmlunit.html.serializer.HtmlSerializerVisibleText.HtmlSerializerTextBuilder.Mode;
61  import org.htmlunit.util.StringUtils;
62  
63  /**
64   * Special serializer to generate the output we need
65   * at least for selenium WebElement#getText().
66   * <p>This is also used from estimations by ComputedCSSStyleDeclaration.</p>
67   *
68   * @author Ronald Brill
69   * @author cd alexndr
70   */
71  public class HtmlSerializerVisibleText {
72  
73      /**
74       * Converts an HTML node to text.
75       * @param node a node
76       * @return the text representation according to the setting of this serializer
77       */
78      public String asText(final DomNode node) {
79          if (node instanceof HtmlBreak) {
80              return "";
81          }
82          final HtmlSerializerTextBuilder builder = new HtmlSerializerTextBuilder();
83          appendNode(builder, node, whiteSpaceStyle(node, Mode.WHITE_SPACE_NORMAL));
84          return builder.getText();
85      }
86  
87      /**
88       * Iterate over all Children and call appendNode() for every.
89       *
90       * @param builder the StringBuilder to add to
91       * @param node the node to process
92       * @param mode the {@link Mode} to use for processing
93       */
94      protected void appendChildren(final HtmlSerializerTextBuilder builder, final DomNode node, final Mode mode) {
95          for (final DomNode child : node.getChildren()) {
96              appendNode(builder, child, updateWhiteSpaceStyle(node, mode));
97          }
98      }
99  
100     /**
101      * The core distribution method call the different appendXXX
102      * methods depending on the type of the given node.
103      *
104      * @param builder the StringBuilder to add to
105      * @param node the node to process
106      * @param mode the {@link Mode} to use for processing
107      */
108     protected void appendNode(final HtmlSerializerTextBuilder builder, final DomNode node, final Mode mode) {
109         if (node instanceof DomCDataSection) {
110             // ignore
111         }
112         else if (node instanceof DomText) {
113             appendText(builder, (DomText) node, mode);
114         }
115         else if (node instanceof DomComment) {
116             appendComment(builder, (DomComment) node, mode);
117         }
118         else if (node instanceof HtmlBreak) {
119             appendBreak(builder, (HtmlBreak) node, mode);
120         }
121         else if (node instanceof HtmlHiddenInput) {
122             appendHiddenInput(builder, (HtmlHiddenInput) node, mode);
123         }
124         else if (node instanceof HtmlScript) {
125             appendScript(builder, (HtmlScript) node, mode);
126         }
127         else if (node instanceof HtmlStyle) {
128             appendStyle(builder, (HtmlStyle) node, mode);
129         }
130         else if (node instanceof HtmlNoFrames) {
131             appendNoFrames(builder, (HtmlNoFrames) node, mode);
132         }
133         else if (node instanceof HtmlTextArea) {
134             appendTextArea(builder, (HtmlTextArea) node, mode);
135         }
136         else if (node instanceof HtmlTitle) {
137             appendTitle(builder, (HtmlTitle) node, mode);
138         }
139         else if (node instanceof HtmlTableRow) {
140             appendTableRow(builder, (HtmlTableRow) node, mode);
141         }
142         else if (node instanceof HtmlSelect) {
143             appendSelect(builder, (HtmlSelect) node, mode);
144         }
145         else if (node instanceof HtmlOption) {
146             appendOption(builder, (HtmlOption) node, mode);
147         }
148         else if (node instanceof HtmlSubmitInput) {
149             appendSubmitInput(builder, (HtmlSubmitInput) node, mode);
150         }
151         else if (node instanceof HtmlResetInput) {
152             appendResetInput(builder, (HtmlResetInput) node, mode);
153         }
154         else if (node instanceof HtmlCheckBoxInput) {
155             appendCheckBoxInput(builder, (HtmlCheckBoxInput) node, mode);
156         }
157         else if (node instanceof HtmlRadioButtonInput) {
158             appendRadioButtonInput(builder, (HtmlRadioButtonInput) node, mode);
159         }
160         else if (node instanceof HtmlInput) {
161             // nothing
162         }
163         else if (node instanceof HtmlTable) {
164             appendTable(builder, (HtmlTable) node, mode);
165         }
166         else if (node instanceof HtmlOrderedList) {
167             appendOrderedList(builder, (HtmlOrderedList) node, mode);
168         }
169         else if (node instanceof HtmlUnorderedList) {
170             appendUnorderedList(builder, (HtmlUnorderedList) node, mode);
171         }
172         else if (node instanceof HtmlPreformattedText) {
173             appendPreformattedText(builder, (HtmlPreformattedText) node, mode);
174         }
175         else if (node instanceof HtmlInlineFrame) {
176             appendInlineFrame(builder, (HtmlInlineFrame) node, mode);
177         }
178         else if (node instanceof HtmlMenu) {
179             appendMenu(builder, (HtmlMenu) node, mode);
180         }
181         else if (node instanceof HtmlDetails) {
182             appendDetails(builder, (HtmlDetails) node, mode);
183         }
184         else if (node instanceof HtmlNoScript && node.getPage().getWebClient().isJavaScriptEnabled()) {
185             appendNoScript(builder, (HtmlNoScript) node, mode);
186         }
187         else {
188             appendDomNode(builder, node, mode);
189         }
190     }
191 
192     /**
193      * Process {@link DomNode}.
194      *
195      * @param builder the StringBuilder to add to
196      * @param domNode the target to process
197      * @param mode the {@link Mode} to use for processing
198      */
199     protected void appendDomNode(final HtmlSerializerTextBuilder builder,
200             final DomNode domNode, final Mode mode) {
201         final boolean block;
202         if (domNode instanceof HtmlBody) {
203             block = false;
204         }
205         else if (domNode instanceof DomElement) {
206             final WebWindow window = domNode.getPage().getEnclosingWindow();
207             final String display = window.getComputedStyle((DomElement) domNode, null).getDisplay();
208             block = BLOCK.equals(display);
209         }
210         else {
211             block = false;
212         }
213 
214         if (block) {
215             builder.appendBlockSeparator();
216         }
217         appendChildren(builder, domNode, mode);
218         if (block) {
219             builder.appendBlockSeparator();
220         }
221     }
222 
223     /**
224      * Process {@link HtmlHiddenInput}.
225      *
226      * @param builder the StringBuilder to add to
227      * @param htmlHiddenInput the target to process
228      * @param mode the {@link Mode} to use for processing
229      */
230     protected void appendHiddenInput(final HtmlSerializerTextBuilder builder,
231             final HtmlHiddenInput htmlHiddenInput, final Mode mode) {
232         // nothing to do
233     }
234 
235     /**
236      * Process {@link HtmlScript}.
237      *
238      * @param builder the StringBuilder to add to
239      * @param htmlScript the target to process
240      * @param mode the {@link Mode} to use for processing
241      */
242     protected void appendScript(final HtmlSerializerTextBuilder builder,
243             final HtmlScript htmlScript, final Mode mode) {
244         // nothing to do
245     }
246 
247     /**
248      * Process {@link HtmlStyle}.
249      *
250      * @param builder the StringBuilder to add to
251      * @param htmlStyle the target to process
252      * @param mode the {@link Mode} to use for processing
253      */
254     protected void appendStyle(final HtmlSerializerTextBuilder builder,
255             final HtmlStyle htmlStyle, final Mode mode) {
256         // nothing to do
257     }
258 
259     /**
260      * Process {@link HtmlNoScript}.
261      *
262      * @param builder the StringBuilder to add to
263      * @param htmlNoScript the target to process
264      * @param mode the {@link Mode} to use for processing
265      */
266     protected void appendNoScript(final HtmlSerializerTextBuilder builder,
267             final HtmlNoScript htmlNoScript, final Mode mode) {
268         // nothing to do
269     }
270 
271     /**
272      * Process {@link HtmlNoFrames}.
273      *
274      * @param builder the StringBuilder to add to
275      * @param htmlNoFrames the target to process
276      * @param mode the {@link Mode} to use for processing
277      */
278     protected void appendNoFrames(final HtmlSerializerTextBuilder builder,
279             final HtmlNoFrames htmlNoFrames, final Mode mode) {
280         // nothing to do
281     }
282 
283     /**
284      * Process {@link HtmlSubmitInput}.
285      *
286      * @param builder the StringBuilder to add to
287      * @param htmlSubmitInput the target to process
288      * @param mode the {@link Mode} to use for processing
289      */
290     protected void appendSubmitInput(final HtmlSerializerTextBuilder builder,
291             final HtmlSubmitInput htmlSubmitInput, final Mode mode) {
292         // nothing to do
293     }
294 
295     /**
296      * Process {@link HtmlInput}.
297      *
298      * @param builder the StringBuilder to add to
299      * @param htmlInput the target to process
300      * @param mode the {@link Mode} to use for processing
301      */
302     protected void appendInput(final HtmlSerializerTextBuilder builder,
303             final HtmlInput htmlInput, final Mode mode) {
304         builder.append(htmlInput.getValueAttribute(), mode);
305     }
306 
307     /**
308      * Process {@link HtmlResetInput}.
309      *
310      * @param builder the StringBuilder to add to
311      * @param htmlResetInput the target to process
312      * @param mode the {@link Mode} to use for processing
313      */
314     protected void appendResetInput(final HtmlSerializerTextBuilder builder,
315             final HtmlResetInput htmlResetInput, final Mode mode) {
316         // nothing to do
317     }
318 
319     /**
320      * Process {@link HtmlMenu}.
321      * @param builder the StringBuilder to add to
322      * @param htmlMenu the target to process
323      * @param mode the {@link Mode} to use for processing
324      */
325     protected void appendMenu(final HtmlSerializerTextBuilder builder,
326                     final HtmlMenu htmlMenu, final Mode mode) {
327         builder.appendBlockSeparator();
328         boolean first = true;
329         for (final DomNode item : htmlMenu.getChildren()) {
330             if (!first) {
331                 builder.appendBlockSeparator();
332             }
333             first = false;
334             appendNode(builder, item, mode);
335         }
336         builder.appendBlockSeparator();
337     }
338 
339     /**
340      * Process {@link HtmlDetails}.
341      * @param builder the StringBuilder to add to
342      * @param htmlDetails the target to process
343      * @param mode the {@link Mode} to use for processing
344      */
345     protected void appendDetails(final HtmlSerializerTextBuilder builder,
346                     final HtmlDetails htmlDetails, final Mode mode) {
347         if (htmlDetails.isOpen()) {
348             appendChildren(builder, htmlDetails, mode);
349             return;
350         }
351 
352         for (final DomNode child : htmlDetails.getChildren()) {
353             if (child instanceof HtmlSummary) {
354                 appendNode(builder, child, mode);
355             }
356         }
357     }
358 
359     /**
360      * Process {@link HtmlTitle}.
361      * @param builder the StringBuilder to add to
362      * @param htmlTitle the target to process
363      * @param mode the {@link Mode} to use for processing
364      */
365     protected void appendTitle(final HtmlSerializerTextBuilder builder,
366             final HtmlTitle htmlTitle, final Mode mode) {
367         // nothing to do
368     }
369 
370     /**
371      * Process {@link HtmlTableRow}.
372      *
373      * @param builder the StringBuilder to add to
374      * @param htmlTableRow the target to process
375      * @param mode the {@link Mode} to use for processing
376      */
377     protected void appendTableRow(final HtmlSerializerTextBuilder builder,
378             final HtmlTableRow htmlTableRow, final Mode mode) {
379         boolean first = true;
380         for (final HtmlTableCell cell : htmlTableRow.getCells()) {
381             if (!first) {
382                 builder.appendBlank();
383             }
384             else {
385                 first = false;
386             }
387             appendChildren(builder, cell, mode); // trim?
388         }
389     }
390 
391     /**
392      * Check domNode visibility.
393      * @param domNode the node to check
394      * @return true or false
395      */
396     protected boolean isDisplayed(final DomNode domNode) {
397         return domNode.isDisplayed();
398     }
399 
400     /**
401      * Process {@link HtmlTextArea}.
402      *
403      * @param builder the StringBuilder to add to
404      * @param htmlTextArea the target to process
405      * @param mode the {@link Mode} to use for processing
406      */
407     protected void appendTextArea(final HtmlSerializerTextBuilder builder,
408             final HtmlTextArea htmlTextArea, final Mode mode) {
409         if (isDisplayed(htmlTextArea)) {
410             builder.append(htmlTextArea.getDefaultValue(), whiteSpaceStyle(htmlTextArea, Mode.PRE));
411             builder.trimRight(Mode.PRE);
412         }
413     }
414 
415     /**
416      * Process {@link HtmlTable}.
417      *
418      * @param builder the StringBuilder to add to
419      * @param htmlTable the target to process
420      * @param mode the {@link Mode} to use for processing
421      */
422     protected void appendTable(final HtmlSerializerTextBuilder builder,
423             final HtmlTable htmlTable, final Mode mode) {
424         builder.appendBlockSeparator();
425         final String caption = htmlTable.getCaptionText();
426         if (caption != null) {
427             builder.append(caption, mode);
428             builder.appendBlockSeparator();
429         }
430 
431         boolean first = true;
432 
433         // first thead has to be displayed first and first tfoot has to be displayed last
434         final HtmlTableHeader tableHeader = htmlTable.getHeader();
435         if (tableHeader != null) {
436             first = appendTableRows(builder, mode, tableHeader.getRows(), true, null, null);
437         }
438         final HtmlTableFooter tableFooter = htmlTable.getFooter();
439 
440         final List<HtmlTableRow> tableRows = htmlTable.getRows();
441         first = appendTableRows(builder, mode, tableRows, first, tableHeader, tableFooter);
442 
443         if (tableFooter != null) {
444             first = appendTableRows(builder, mode, tableFooter.getRows(), first, null, null);
445         }
446         else if (tableRows.isEmpty()) {
447             final DomNode firstChild = htmlTable.getFirstChild();
448             if (firstChild != null) {
449                 appendNode(builder, firstChild, mode);
450             }
451         }
452 
453         builder.appendBlockSeparator();
454     }
455 
456     /**
457      * Process {@link HtmlTableRow}.
458      *
459      * @param builder the StringBuilder to add to
460      * @param mode the {@link Mode} to use for processing
461      * @param rows the rows
462      * @param first if true this is the first one
463      * @param skipParent1 skip row if the parent is this
464      * @param skipParent2 skip row if the parent is this
465      * @return true if this was the first one
466      */
467     protected boolean appendTableRows(final HtmlSerializerTextBuilder builder, final Mode mode,
468             final List<HtmlTableRow> rows, boolean first, final TableRowGroup skipParent1,
469             final TableRowGroup skipParent2) {
470         for (final HtmlTableRow row : rows) {
471             if (row.getParentNode() == skipParent1 || row.getParentNode() == skipParent2) {
472                 continue;
473             }
474             if (!first) {
475                 builder.appendBlockSeparator();
476             }
477             first = false;
478             appendTableRow(builder, row, mode);
479         }
480         return first;
481     }
482 
483     /**
484      * Process {@link HtmlSelect}.
485      *
486      * @param builder the StringBuilder to add to
487      * @param htmlSelect the target to process
488      * @param mode the {@link Mode} to use for processing
489      */
490     protected void appendSelect(final HtmlSerializerTextBuilder builder,
491             final HtmlSelect htmlSelect, final Mode mode) {
492         builder.appendBlockSeparator();
493         boolean leadingNlPending = false;
494         final Mode selectMode = whiteSpaceStyle(htmlSelect, mode);
495         for (final DomNode item : htmlSelect.getChildren()) {
496             if (leadingNlPending) {
497                 builder.appendBlockSeparator();
498                 leadingNlPending = false;
499             }
500 
501             builder.resetContentAdded();
502             appendNode(builder, item, whiteSpaceStyle(item, selectMode));
503             if (!leadingNlPending && builder.contentAdded_) {
504                 leadingNlPending = true;
505             }
506         }
507         builder.appendBlockSeparator();
508     }
509 
510     /**
511      * Process {@link HtmlSelect}.
512      *
513      * @param builder the StringBuilder to add to
514      * @param htmlOption the target to process
515      * @param mode the {@link Mode} to use for processing
516      */
517     protected void appendOption(final HtmlSerializerTextBuilder builder,
518             final HtmlOption htmlOption, final Mode mode) {
519         appendChildren(builder, htmlOption, mode);
520     }
521 
522     /**
523      * Process {@link HtmlOrderedList}.
524      *
525      * @param builder the StringBuilder to add to
526      * @param htmlOrderedList the OL element
527      * @param mode the {@link Mode} to use for processing
528      */
529     protected void appendOrderedList(final HtmlSerializerTextBuilder builder,
530             final HtmlOrderedList htmlOrderedList, final Mode mode) {
531         builder.appendBlockSeparator();
532         boolean leadingNlPending = false;
533         final Mode olMode = whiteSpaceStyle(htmlOrderedList, mode);
534         for (final DomNode item : htmlOrderedList.getChildren()) {
535             if (leadingNlPending) {
536                 builder.appendBlockSeparator();
537                 leadingNlPending = false;
538             }
539 
540             builder.resetContentAdded();
541             appendNode(builder, item, whiteSpaceStyle(item, olMode));
542             if (!leadingNlPending && builder.contentAdded_) {
543                 leadingNlPending = true;
544             }
545         }
546         builder.appendBlockSeparator();
547     }
548 
549     /**
550      * Process {@link HtmlUnorderedList}.
551      * @param builder the StringBuilder to add to
552      * @param htmlUnorderedList the target to process
553      * @param mode the {@link Mode} to use for processing
554      */
555     protected void appendUnorderedList(final HtmlSerializerTextBuilder builder,
556                     final HtmlUnorderedList htmlUnorderedList, final Mode mode) {
557         builder.appendBlockSeparator();
558         boolean leadingNlPending = false;
559         final Mode ulMode = whiteSpaceStyle(htmlUnorderedList, mode);
560         for (final DomNode item : htmlUnorderedList.getChildren()) {
561             if (leadingNlPending) {
562                 builder.appendBlockSeparator();
563                 leadingNlPending = false;
564             }
565 
566             builder.resetContentAdded();
567             appendNode(builder, item, whiteSpaceStyle(item, ulMode));
568             if (!leadingNlPending && builder.contentAdded_) {
569                 leadingNlPending = true;
570             }
571         }
572         builder.appendBlockSeparator();
573     }
574 
575     /**
576      * Process {@link HtmlPreformattedText}.
577      *
578      * @param builder the StringBuilder to add to
579      * @param htmlPreformattedText the target to process
580      * @param mode the {@link Mode} to use for processing
581      */
582     protected void appendPreformattedText(final HtmlSerializerTextBuilder builder,
583             final HtmlPreformattedText htmlPreformattedText, final Mode mode) {
584         if (isDisplayed(htmlPreformattedText)) {
585             builder.appendBlockSeparator();
586             appendChildren(builder, htmlPreformattedText, whiteSpaceStyle(htmlPreformattedText, Mode.PRE));
587             builder.appendBlockSeparator();
588         }
589     }
590 
591     /**
592      * Process {@link HtmlInlineFrame}.
593      *
594      * @param builder the StringBuilder to add to
595      * @param htmlInlineFrame the target to process
596      * @param mode the {@link Mode} to use for processing
597      */
598     protected void appendInlineFrame(final HtmlSerializerTextBuilder builder,
599             final HtmlInlineFrame htmlInlineFrame, final Mode mode) {
600         if (isDisplayed(htmlInlineFrame)) {
601             builder.appendBlockSeparator();
602             final Page page = htmlInlineFrame.getEnclosedPage();
603             if (page instanceof SgmlPage) {
604                 builder.append(((SgmlPage) page).asNormalizedText(), mode);
605             }
606             builder.appendBlockSeparator();
607         }
608     }
609 
610     /**
611      * Process {@link DomText}.
612      *
613      * @param builder the StringBuilder to add to
614      * @param domText the target to process
615      * @param mode the {@link Mode} to use for processing
616      */
617     protected void appendText(final HtmlSerializerTextBuilder builder, final DomText domText, final Mode mode) {
618         final DomNode parent = domText.getParentNode();
619         if (parent instanceof HtmlTitle
620                 || parent instanceof HtmlScript) {
621             builder.append(domText.getData(), Mode.WHITE_SPACE_PRE_LINE);
622         }
623 
624         if (parent == null
625                 || parent instanceof HtmlTitle
626                 || parent instanceof HtmlScript
627                 || isDisplayed(parent)) {
628             builder.append(domText.getData(), mode);
629         }
630     }
631 
632     /**
633      * Process {@link DomComment}.
634      *
635      * @param builder the StringBuilder to add to
636      * @param domComment the target to process
637      * @param mode the {@link Mode} to use for processing
638      */
639     protected void appendComment(final HtmlSerializerTextBuilder builder,
640             final DomComment domComment, final Mode mode) {
641         // nothing to do
642     }
643 
644     /**
645      * Process {@link HtmlBreak}.
646      *
647      * @param builder the StringBuilder to add to
648      * @param htmlBreak the target to process
649      * @param mode the {@link Mode} to use for processing
650      */
651     protected void appendBreak(final HtmlSerializerTextBuilder builder,
652             final HtmlBreak htmlBreak, final Mode mode) {
653         builder.appendBreak(mode);
654     }
655 
656     /**
657      * Process {@link HtmlCheckBoxInput}.
658      *
659      * @param builder the StringBuilder to add to
660      * @param htmlCheckBoxInput the target to process
661      * @param mode the {@link Mode} to use for processing
662      */
663     protected void appendCheckBoxInput(final HtmlSerializerTextBuilder builder,
664                     final HtmlCheckBoxInput htmlCheckBoxInput, final Mode mode) {
665         // nothing to do
666     }
667 
668     /**
669      * Process {@link HtmlRadioButtonInput}.
670      *
671      * @param builder the StringBuilder to add to
672      * @param htmlRadioButtonInput the target to process
673      * @param mode the {@link Mode} to use for processing
674      */
675     protected void appendRadioButtonInput(final HtmlSerializerTextBuilder builder,
676             final HtmlRadioButtonInput htmlRadioButtonInput, final Mode mode) {
677         // nothing to do
678     }
679 
680     protected Mode whiteSpaceStyle(final DomNode domNode, final Mode defaultMode) {
681         final Page page = domNode.getPage();
682         if (page != null) {
683             final WebWindow window = page.getEnclosingWindow();
684             if (window.getWebClient().getOptions().isCssEnabled()) {
685                 DomNode node = domNode;
686                 while (node != null) {
687                     if (node instanceof DomElement) {
688                         final ComputedCssStyleDeclaration style = window.getComputedStyle((DomElement) node, null);
689                         final String value = style.getStyleAttribute(Definition.WHITE_SPACE, false);
690                         if (!StringUtils.isEmptyOrNull(value)) {
691                             if ("normal".equalsIgnoreCase(value)) {
692                                 return Mode.WHITE_SPACE_NORMAL;
693                             }
694                             if ("nowrap".equalsIgnoreCase(value)) {
695                                 return Mode.WHITE_SPACE_NORMAL;
696                             }
697                             if ("pre".equalsIgnoreCase(value)) {
698                                 return Mode.WHITE_SPACE_PRE;
699                             }
700                             if ("pre-wrap".equalsIgnoreCase(value)) {
701                                 return Mode.WHITE_SPACE_PRE;
702                             }
703                             if ("pre-line".equalsIgnoreCase(value)) {
704                                 return Mode.WHITE_SPACE_PRE_LINE;
705                             }
706                         }
707                     }
708                     node = node.getParentNode();
709                 }
710             }
711         }
712         return defaultMode;
713     }
714 
715     protected Mode updateWhiteSpaceStyle(final DomNode domNode, final Mode defaultMode) {
716         final Page page = domNode.getPage();
717         if (page != null) {
718             final WebWindow window = page.getEnclosingWindow();
719             if (window.getWebClient().getOptions().isCssEnabled()) {
720                 if (domNode instanceof DomElement) {
721                     final ComputedCssStyleDeclaration style = window.getComputedStyle((DomElement) domNode, null);
722                     final String value = style.getStyleAttribute(Definition.WHITE_SPACE, false);
723                     if (!StringUtils.isEmptyOrNull(value)) {
724                         if ("normal".equalsIgnoreCase(value)) {
725                             return Mode.WHITE_SPACE_NORMAL;
726                         }
727                         if ("nowrap".equalsIgnoreCase(value)) {
728                             return Mode.WHITE_SPACE_NORMAL;
729                         }
730                         if ("pre".equalsIgnoreCase(value)) {
731                             return Mode.WHITE_SPACE_PRE;
732                         }
733                         if ("pre-wrap".equalsIgnoreCase(value)) {
734                             return Mode.WHITE_SPACE_PRE;
735                         }
736                         if ("pre-line".equalsIgnoreCase(value)) {
737                             return Mode.WHITE_SPACE_PRE_LINE;
738                         }
739                     }
740                 }
741             }
742         }
743         return defaultMode;
744     }
745 
746     /**
747      * Helper to compose the text for the serializer based on several modes.
748      */
749     protected static class HtmlSerializerTextBuilder {
750         /** Mode. */
751         protected enum Mode {
752             /**
753              * The mode for the pre tag.
754              */
755             PRE,
756 
757             /**
758              * Sequences of white space are collapsed. Newline characters
759              * in the source are handled the same as other white space.
760              * Lines are broken as necessary to fill line boxes.
761              */
762             WHITE_SPACE_NORMAL,
763 
764             /**
765              * Sequences of white space are preserved. Lines are only broken
766              * at newline characters in the source and at <br> elements.
767              */
768             WHITE_SPACE_PRE,
769 
770             /**
771              * Sequences of white space are collapsed. Lines are broken
772              * at newline characters, at <br>, and as necessary
773              * to fill line boxes.
774              */
775             WHITE_SPACE_PRE_LINE
776         }
777 
778         private enum State {
779             DEFAULT,
780             EMPTY,
781             BLANK_AT_END,
782             BLANK_AT_END_AFTER_NEWLINE,
783             NEWLINE_AT_END,
784             BREAK_AT_END,
785             BLOCK_SEPARATOR_AT_END
786         }
787 
788         private State state_;
789         private final StringBuilder builder_;
790         private int trimRightPos_;
791         private boolean contentAdded_;
792 
793         /**
794          * Ctor.
795          */
796         public HtmlSerializerTextBuilder() {
797             builder_ = new StringBuilder();
798             state_ = State.EMPTY;
799             trimRightPos_ = 0;
800         }
801 
802         /**
803          * Append the provided content.
804          * see https://drafts.csswg.org/css-text-3/#white-space
805          *
806          * @param content the content to add
807          * @param mode the {@link Mode}
808          */
809         public void append(final String content, final Mode mode) {
810             if (content == null) {
811                 return;
812             }
813             int length = content.length();
814             if (length == 0) {
815                 return;
816             }
817 
818             length--;
819             final int contentLenght = content.length();
820             for (int i = 0; i < contentLenght; i++) {
821                 char c = content.charAt(i);
822 
823                 // handle \r
824                 if (c == '\r') {
825                     if (length != i) {
826                         continue;
827                     }
828                     c = '\n';
829                 }
830 
831                 if (c == '\n') {
832                     if (mode == Mode.WHITE_SPACE_PRE) {
833                         switch (state_) {
834                             case EMPTY:
835                             case BLOCK_SEPARATOR_AT_END:
836                                 break;
837                             default:
838                                 builder_.append('\n');
839                                 state_ = State.NEWLINE_AT_END;
840                                 trimRightPos_ = builder_.length();
841                                 break;
842                         }
843                         continue;
844                     }
845 
846                     if (mode == Mode.PRE) {
847                         builder_.append('\n');
848                         state_ = State.NEWLINE_AT_END;
849                         trimRightPos_ = builder_.length();
850 
851                         continue;
852                     }
853 
854                     if (mode == Mode.WHITE_SPACE_PRE_LINE) {
855                         switch (state_) {
856                             case EMPTY:
857                             case BLOCK_SEPARATOR_AT_END:
858                                 break;
859                             default:
860                                 builder_.append('\n');
861                                 state_ = State.NEWLINE_AT_END;
862                                 trimRightPos_ = builder_.length();
863                                 break;
864                         }
865                         continue;
866                     }
867 
868                     switch (state_) {
869                         case EMPTY:
870                         case BLANK_AT_END:
871                         case BLANK_AT_END_AFTER_NEWLINE:
872                         case BLOCK_SEPARATOR_AT_END:
873                         case NEWLINE_AT_END:
874                         case BREAK_AT_END:
875                             break;
876                         default:
877                             builder_.append(' ');
878                             state_ = State.BLANK_AT_END;
879                             break;
880                     }
881                     continue;
882                 }
883 
884                 if (c == ' ' || c == '\t' || c == '\f') {
885                     if (mode == Mode.WHITE_SPACE_PRE || mode == Mode.PRE) {
886                         appendBlank();
887                         continue;
888                     }
889 
890                     if (mode == Mode.WHITE_SPACE_PRE_LINE) {
891                         switch (state_) {
892                             case EMPTY:
893                             case BLANK_AT_END:
894                             case BLANK_AT_END_AFTER_NEWLINE:
895                             case BREAK_AT_END:
896                                 break;
897                             default:
898                                 builder_.append(' ');
899                                 state_ = State.BLANK_AT_END;
900                                 break;
901                         }
902                         continue;
903                     }
904 
905                     switch (state_) {
906                         case EMPTY:
907                         case BLANK_AT_END:
908                         case BLANK_AT_END_AFTER_NEWLINE:
909                         case BLOCK_SEPARATOR_AT_END:
910                         case NEWLINE_AT_END:
911                         case BREAK_AT_END:
912                             break;
913                         default:
914                             builder_.append(' ');
915                             state_ = State.BLANK_AT_END;
916                             break;
917                     }
918                     continue;
919                 }
920 
921                 if (c == (char) 160) {
922                     appendBlank();
923                     if (mode == Mode.WHITE_SPACE_NORMAL || mode == Mode.WHITE_SPACE_PRE_LINE) {
924                         state_ = State.DEFAULT;
925                     }
926                     continue;
927                 }
928                 builder_.append(c);
929                 state_ = State.DEFAULT;
930                 trimRightPos_ = builder_.length();
931                 contentAdded_ = true;
932             }
933         }
934 
935         /**
936          * Append a block separator.
937          */
938         public void appendBlockSeparator() {
939             switch (state_) {
940                 case EMPTY:
941                     break;
942                 case BLANK_AT_END:
943                     builder_.setLength(trimRightPos_);
944                     if (builder_.length() == 0) {
945                         state_ = State.EMPTY;
946                     }
947                     else {
948                         builder_.append('\n');
949                         state_ = State.BLOCK_SEPARATOR_AT_END;
950                     }
951                     break;
952                 case BLANK_AT_END_AFTER_NEWLINE:
953                     builder_.setLength(trimRightPos_ - 1);
954                     if (builder_.length() == 0) {
955                         state_ = State.EMPTY;
956                     }
957                     else {
958                         builder_.append('\n');
959                         state_ = State.BLOCK_SEPARATOR_AT_END;
960                     }
961                     break;
962                 case BLOCK_SEPARATOR_AT_END:
963                     break;
964                 case NEWLINE_AT_END:
965                 case BREAK_AT_END:
966                     builder_.setLength(builder_.length() - 1);
967                     trimRightPos_ = trimRightPos_ - 1;
968                     if (builder_.length() == 0) {
969                         state_ = State.EMPTY;
970                     }
971                     else {
972                         builder_.append('\n');
973                         state_ = State.BLOCK_SEPARATOR_AT_END;
974                     }
975                     break;
976                 default:
977                     builder_.append('\n');
978                     state_ = State.BLOCK_SEPARATOR_AT_END;
979                     break;
980             }
981         }
982 
983         /**
984          * Append a break.
985          *
986          * @param mode the {@link Mode}
987          */
988         public void appendBreak(final Mode mode) {
989             builder_.setLength(trimRightPos_);
990 
991             builder_.append('\n');
992             state_ = State.BREAK_AT_END;
993             trimRightPos_ = builder_.length();
994         }
995 
996         /**
997          * Append a blank.
998          */
999         public void appendBlank() {
1000             builder_.append(' ');
1001             state_ = State.BLANK_AT_END;
1002             trimRightPos_ = builder_.length();
1003         }
1004 
1005         /**
1006          * Remove all trailing whitespace from the end.
1007          *
1008          * @param mode the {@link Mode}
1009          */
1010         public void trimRight(final Mode mode) {
1011             if (mode == Mode.PRE) {
1012                 switch (state_) {
1013                     case BLOCK_SEPARATOR_AT_END:
1014                     case NEWLINE_AT_END:
1015                     case BREAK_AT_END:
1016                         if (trimRightPos_ == builder_.length()) {
1017                             trimRightPos_--;
1018                         }
1019                         break;
1020                     default:
1021                         break;
1022                 }
1023             }
1024 
1025             builder_.setLength(trimRightPos_);
1026             state_ = State.DEFAULT;
1027             if (builder_.length() == 0) {
1028                 state_ = State.EMPTY;
1029             }
1030         }
1031 
1032         /**
1033          * @return true if some content was already added
1034          */
1035         public boolean wasContentAdded() {
1036             return contentAdded_;
1037         }
1038 
1039         /**
1040          * Resets the contentAdded state to false.
1041          */
1042         public void resetContentAdded() {
1043             contentAdded_ = false;
1044         }
1045 
1046         /**
1047          * @return the constructed text.
1048          */
1049         public String getText() {
1050             return builder_.substring(0, trimRightPos_);
1051         }
1052     }
1053 }