001/*
002 * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package org.jdrupes.mdoclet.internal.doclets.formats.html.markup;
027
028import java.io.IOException;
029import java.io.Writer;
030import java.net.URI;
031import java.nio.charset.StandardCharsets;
032import java.util.ArrayList;
033import java.util.BitSet;
034import java.util.Collection;
035import java.util.LinkedHashMap;
036import java.util.List;
037import java.util.Map;
038import java.util.Objects;
039import java.util.function.Function;
040
041import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlAttr.Role;
042import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
043
044/**
045 * A tree node representing an HTML element, containing the name of the element,
046 * a collection of attributes, and content.
047 *
048 * Except where otherwise stated, all methods in this class will throw
049 * {@code NullPointerException} for any arguments that are {@code null}
050 * or that are arrays or collections that contain {@code null}.
051 *
052 * Many methods in this class return {@code this}, to enable a series
053 * of chained method calls on a single object.
054 *
055 * Terminology: An HTML element is typically composed of a start tag, some
056 * enclosed content and typically an end tag. The start tag contains any
057 * attributes for the element. See:
058 * <a href="https://en.wikipedia.org/wiki/HTML_element">HTML element</a>.
059 *
060 * @see <a href="https://html.spec.whatwg.org/multipage/syntax.html#normal-elements">WhatWG: Normal Elements</a>
061 * @see <a href="https://www.w3.org/TR/html51/syntax.html#writing-html-documents-elements">HTML 5.1: Elements</a>
062 */
063public class HtmlTree extends Content {
064
065    /**
066     * The name of the HTML element.
067     * This value is never {@code null}.
068     */
069    public final TagName tagName;
070
071    /**
072     * The attributes for the HTML element.
073     * The keys and values in this map are never {@code null}.
074     */
075    private Map<HtmlAttr, String> attrs = Map.of();
076
077    /**
078     * The enclosed content ("inner HTML") for this HTML element.
079     * The items in this list are never {@code null}.
080     */
081    private List<Content> content = List.of();
082
083    /**
084     * Creates an {@code HTMLTree} object representing an HTML element
085     * with the given name.
086     *
087     * @param tagName the name
088     */
089    public HtmlTree(TagName tagName) {
090        this.tagName = Objects.requireNonNull(tagName);
091    }
092
093    /**
094     * Adds an attribute.
095     *
096     * @param attrName  the name of the attribute
097     * @param attrValue the value of the attribute
098     * @return this object
099     */
100    public HtmlTree put(HtmlAttr attrName, String attrValue) {
101        if (attrs.isEmpty())
102            attrs = new LinkedHashMap<>(3);
103        attrs.put(Objects.requireNonNull(attrName),
104            Entity.escapeHtmlChars(attrValue));
105        return this;
106    }
107
108    /**
109     * Sets the {@code id} attribute.
110     *
111     * @param id the value for the attribute
112     * @return this object
113     */
114    public HtmlTree setId(HtmlId id) {
115        return put(HtmlAttr.ID, id.name());
116    }
117
118    /**
119     * Sets the {@code title} attribute.
120     * Any nested start or end tags in the content will be removed.
121     *
122     * @param body the content for the title attribute
123     * @return this object
124     */
125    public HtmlTree setTitle(Content body) {
126        return put(HtmlAttr.TITLE, stripHtml(body));
127    }
128
129    /**
130     * Sets the {@code role} attribute.
131     *
132     * @param role the role
133     * @return this object
134     */
135    public HtmlTree setRole(Role role) {
136        return put(HtmlAttr.ROLE, role.toString());
137    }
138
139    /**
140     * Sets the {@code class} attribute.
141     *
142     * @param style the value for the attribute
143     * @return this object
144     */
145    public HtmlTree setStyle(HtmlStyle style) {
146        return put(HtmlAttr.CLASS, style.cssName());
147    }
148
149    public HtmlTree addStyle(HtmlStyle style) {
150        return addStyle(style.cssName());
151    }
152
153    public HtmlTree addStyle(String style) {
154        if (attrs.isEmpty())
155            attrs = new LinkedHashMap<>(3);
156        attrs.compute(HtmlAttr.CLASS,
157            (attr, existingStyle) -> existingStyle == null ? style
158                : existingStyle + " " + style);
159        return this;
160    }
161
162    /**
163     * Adds additional content for the HTML element.
164     *
165     * @implSpec In order to facilitate creation of succinct output this method
166     * silently drops discardable content as determined by {@link #isDiscardable()}.
167     * Use {@link #addUnchecked(Content)} to add content unconditionally.
168     *
169     * @param content the content
170     * @return this HTML tree
171     */
172    @Override
173    public HtmlTree add(Content content) {
174        if (content instanceof ContentBuilder cb) {
175            cb.contents.forEach(this::add);
176        } else if (!content.isDiscardable()) {
177            // quietly avoid adding empty or invalid nodes
178            if (this.content.isEmpty())
179                this.content = new ArrayList<>();
180            this.content.add(content);
181        }
182        return this;
183    }
184
185    /**
186     * Adds content to this HTML tree without checking whether it is discardable.
187     *
188     * @param content the content to add
189     * @return this HTML tree
190     */
191    public HtmlTree addUnchecked(Content content) {
192        if (content instanceof ContentBuilder cb) {
193            cb.contents.forEach(this::addUnchecked);
194        } else {
195            if (this.content.isEmpty())
196                this.content = new ArrayList<>();
197            this.content.add(content);
198        }
199        return this;
200    }
201
202    /**
203     * Adds text content for the HTML element.
204     *
205     * If the last content member that was added is a {@code StringContent},
206     * appends the string to that item; otherwise, creates and uses a new {@code StringContent}
207     * for the new text content.
208     *
209     * @param stringContent string content that needs to be added
210     */
211    @Override
212    public HtmlTree add(CharSequence stringContent) {
213        if (!content.isEmpty()) {
214            Content lastContent = content.get(content.size() - 1);
215            if (lastContent instanceof TextBuilder)
216                lastContent.add(stringContent);
217            else {
218                add(new TextBuilder(stringContent));
219            }
220        } else {
221            add(new TextBuilder(stringContent));
222        }
223        return this;
224    }
225
226    /**
227     * Adds each of a list of content items.
228     *
229     * @param list the list
230     * @return this object
231     */
232    public HtmlTree add(List<? extends Content> list) {
233        list.forEach(this::add);
234        return this;
235    }
236
237    /**
238     * Adds each of a collection of items, using a map function to create the content for each item.
239     *
240     * @param items  the items
241     * @param mapper the map function to generate the content for each item
242     *
243     * @return this object
244     */
245    @Override
246    public <T> HtmlTree addAll(Collection<T> items,
247            Function<T, Content> mapper) {
248        items.forEach(item -> add(mapper.apply(item)));
249        return this;
250    }
251
252    @Override
253    public int charCount() {
254        int n = 0;
255        for (Content c : content) {
256            n += c.charCount();
257        }
258        return n;
259    }
260
261    /*
262     * The sets of ASCII URI characters to be left unencoded.
263     * See "Uniform Resource Identifier (URI): Generic Syntax"
264     * IETF RFC 3986. https://tools.ietf.org/html/rfc3986
265     */
266    public static final BitSet MAIN_CHARS;
267    public static final BitSet QUERY_FRAGMENT_CHARS;
268
269    static {
270        BitSet alphaDigit
271            = bitSet(bitSet('A', 'Z'), bitSet('a', 'z'), bitSet('0', '9'));
272        BitSet unreserved = bitSet(alphaDigit, bitSet("-._~"));
273        BitSet genDelims = bitSet(":/?#[]@");
274        BitSet subDelims = bitSet("!$&'()*+,;=");
275        MAIN_CHARS = bitSet(unreserved, genDelims, subDelims);
276        BitSet pchar = bitSet(unreserved, subDelims, bitSet(":@"));
277        QUERY_FRAGMENT_CHARS = bitSet(pchar, bitSet("/?"));
278    }
279
280    private static BitSet bitSet(String s) {
281        BitSet result = new BitSet();
282        for (int i = 0; i < s.length(); i++) {
283            result.set(s.charAt(i));
284        }
285        return result;
286    }
287
288    private static BitSet bitSet(char from, char to) {
289        BitSet result = new BitSet();
290        result.set(from, to + 1);
291        return result;
292    }
293
294    private static BitSet bitSet(BitSet... sets) {
295        BitSet result = new BitSet();
296        for (BitSet set : sets) {
297            result.or(set);
298        }
299        return result;
300    }
301
302    /**
303     * Apply percent-encoding to a URL.
304     * This is similar to {@link java.net.URLEncoder} but
305     * is less aggressive about encoding some characters,
306     * like '(', ')', ',' which are used in the anchor
307     * names for Java methods in HTML5 mode.
308     *
309     * @param url the url to be percent-encoded.
310     * @return a percent-encoded string.
311     */
312    public static String encodeURL(String url) {
313        BitSet nonEncodingChars = MAIN_CHARS;
314        StringBuilder sb = new StringBuilder();
315        for (byte c : url.getBytes(StandardCharsets.UTF_8)) {
316            if (c == '?' || c == '#') {
317                sb.append((char) c);
318                // switch to the more restrictive set inside
319                // the query and/or fragment
320                nonEncodingChars = QUERY_FRAGMENT_CHARS;
321            } else if (nonEncodingChars.get(c & 0xFF)) {
322                sb.append((char) c);
323            } else {
324                sb.append(String.format("%%%02X", c & 0xFF));
325            }
326        }
327        return sb.toString();
328    }
329
330    /**
331     * Creates an HTML {@code A} element.
332     * The {@code ref} argument will be URL-encoded for use as the attribute value.
333     *
334     * @param ref the value for the {@code href} attribute
335     * @param body the content for element
336     * @return the element
337     */
338    public static HtmlTree A(String ref, Content body) {
339        return new HtmlTree(TagName.A)
340            .put(HtmlAttr.HREF, encodeURL(ref))
341            .add(body);
342    }
343
344    /**
345     * Creates an HTML {@code A} element.
346     * The {@code ref} argument is assumed to be already suitably encoded,
347     * and will <i>not</i> be additionally URL-encoded, but will be
348     * {@link URI#toASCIIString() converted} to ASCII for use as the attribute value.
349     *
350     * @param ref the value for the {@code href} attribute
351     * @param body the content for element
352     * @return the element
353     */
354    public static HtmlTree A(URI ref, Content body) {
355        return new HtmlTree(TagName.A)
356            .put(HtmlAttr.HREF, ref.toASCIIString())
357            .add(body);
358    }
359
360    /**
361     * Creates an HTML {@code CAPTION} element with the given content.
362     *
363     * @param body content for the element
364     * @return the element
365     */
366    public static HtmlTree CAPTION(Content body) {
367        return new HtmlTree(TagName.CAPTION)
368            .add(body);
369    }
370
371    /**
372     * Creates an HTML {@code CODE} element with the given content.
373     *
374     * @param body content for the element
375     * @return the element
376     */
377    public static HtmlTree CODE(Content body) {
378        return new HtmlTree(TagName.CODE)
379            .add(body);
380    }
381
382    /**
383     * Creates an HTML {@code DD} element with the given content.
384     *
385     * @param body content for the element
386     * @return the element
387     */
388    public static HtmlTree DD(Content body) {
389        return new HtmlTree(TagName.DD)
390            .add(body);
391    }
392
393    /**
394     * Creates an HTML {@code DETAILS} element.
395     *
396     * @return the element
397     */
398    public static HtmlTree DETAILS() {
399        return new HtmlTree(TagName.DETAILS);
400    }
401
402    /**
403     * Creates an HTML {@code DETAILS} element.
404     *
405     * @return the element
406     */
407    public static HtmlTree DETAILS(HtmlStyle style) {
408        return new HtmlTree(TagName.DETAILS)
409            .setStyle(style);
410    }
411
412    /**
413     * Creates an HTML {@code DL} element with the given style.
414     *
415     * @param style the style
416     * @return the element
417     */
418    public static HtmlTree DL(HtmlStyle style) {
419        return new HtmlTree(TagName.DL)
420            .setStyle(style);
421    }
422
423    /**
424     * Creates an HTML {@code DL} element with the given style and content.
425     *
426     * @param style the style
427     * @param body  the content
428     * @return the element
429     */
430    public static HtmlTree DL(HtmlStyle style, Content body) {
431        return new HtmlTree(TagName.DL)
432            .setStyle(style)
433            .add(body);
434    }
435
436    /**
437     * Creates an HTML {@code DIV} element with the given style.
438     *
439     * @param style the style
440     * @return the element
441     */
442    public static HtmlTree DIV(HtmlStyle style) {
443        return new HtmlTree(TagName.DIV)
444            .setStyle(style);
445    }
446
447    /**
448     * Creates an HTML {@code DIV} element with the given style and content.
449     *
450     * @param style the style
451     * @param body  the content
452     * @return the element
453     */
454    public static HtmlTree DIV(HtmlStyle style, Content body) {
455        return new HtmlTree(TagName.DIV)
456            .setStyle(style)
457            .add(body);
458    }
459
460    /**
461     * Creates an HTML {@code DIV} element with the given content.
462     *
463     * @param body the content
464     * @return the element
465     */
466    public static HtmlTree DIV(Content body) {
467        return new HtmlTree(TagName.DIV)
468            .add(body);
469    }
470
471    /**
472     * Creates an HTML {@code DT} element with the given content.
473     *
474     * @param body the content
475     * @return the element
476     */
477    public static HtmlTree DT(Content body) {
478        return new HtmlTree(TagName.DT)
479            .add(body);
480    }
481
482    /**
483     * Creates an HTML {@code FOOTER} element.
484     * The role is set to {@code contentinfo}.
485     *
486     * @return the element
487     */
488    public static HtmlTree FOOTER() {
489        return new HtmlTree(TagName.FOOTER)
490            .setRole(Role.CONTENTINFO);
491    }
492
493    /**
494     * Creates an HTML {@code HEADER} element.
495     * The role is set to {@code banner}.
496     *
497     * @return the element
498     */
499    public static HtmlTree HEADER() {
500        return new HtmlTree(TagName.HEADER)
501            .setRole(Role.BANNER);
502    }
503
504    /**
505     * Creates an HTML heading element with the given content.
506     *
507     * @param headingTag the tag for the heading
508     * @param body       the content
509     * @return the element
510     */
511    public static HtmlTree HEADING(TagName headingTag, Content body) {
512        return new HtmlTree(checkHeading(headingTag))
513            .add(body);
514    }
515
516    /**
517     * Creates an HTML heading element with the given style and content.
518     *
519     * @param headingTag the tag for the heading
520     * @param style      the stylesheet class
521     * @param body       the content
522     * @return the element
523     */
524    public static HtmlTree HEADING(TagName headingTag, HtmlStyle style,
525            Content body) {
526        return new HtmlTree(checkHeading(headingTag))
527            .setStyle(style)
528            .add(body);
529    }
530
531    /**
532     * Creates an HTML heading element with the given style and content.
533     * The {@code title} attribute is set from the content.
534     *
535     * @param headingTag the tag for the heading
536     * @param style      the stylesheet class
537     * @param body       the content
538     * @return the element
539     */
540    public static HtmlTree HEADING_TITLE(TagName headingTag,
541            HtmlStyle style, Content body) {
542        return new HtmlTree(checkHeading(headingTag))
543            .setTitle(body)
544            .setStyle(style)
545            .add(body);
546    }
547
548    /**
549     * Creates an HTML heading element with the given style and content.
550     * The {@code title} attribute is set from the content.
551     *
552     * @param headingTag the tag for the heading
553     * @param body       the content
554     * @return the element
555     */
556    public static HtmlTree HEADING_TITLE(TagName headingTag, Content body) {
557        return new HtmlTree(checkHeading(headingTag))
558            .setTitle(body)
559            .add(body);
560    }
561
562    private static TagName checkHeading(TagName headingTag) {
563        return switch (headingTag) {
564        case H1, H2, H3, H4, H5, H6 -> headingTag;
565        default -> throw new IllegalArgumentException(headingTag.toString());
566        };
567    }
568
569    /**
570     * Creates an HTML {@code HTML} element with the given {@code lang} attribute,
571     * and {@code HEAD} and {@code BODY} contents.
572     *
573     * @param lang the value for the {@code lang} attribute
574     * @param head the {@code HEAD} element
575     * @param body the {@code BODY} element
576     * @return the {@code HTML} element
577     */
578    public static HtmlTree HTML(String lang, Content head, Content body) {
579        return new HtmlTree(TagName.HTML)
580            .put(HtmlAttr.LANG, lang)
581            .add(head)
582            .add(body);
583    }
584
585    /**
586     * Creates an HTML {@code INPUT} element with the given id.
587     * The element as marked as initially disabled.
588     *
589     * @param type  the type of input
590     * @param id    the id
591     * @return the element
592     */
593    public static HtmlTree INPUT(String type, HtmlId id) {
594        return new HtmlTree(TagName.INPUT)
595            .put(HtmlAttr.TYPE, type)
596            .setId(id)
597            .put(HtmlAttr.DISABLED, "");
598    }
599
600    /**
601     * Creates an HTML {@code LABEL} element with the given content.
602     *
603     * @param forLabel the value of the {@code for} attribute
604     * @param body     the content
605     * @return the element
606     */
607    public static HtmlTree LABEL(String forLabel, Content body) {
608        return new HtmlTree(TagName.LABEL)
609            .put(HtmlAttr.FOR, forLabel)
610            .add(body);
611    }
612
613    /**
614     * Creates an HTML {@code LI} element with the given content.
615     *
616     * @param body the content
617     * @return the element
618     */
619    public static HtmlTree LI(Content body) {
620        return new HtmlTree(TagName.LI)
621            .add(body);
622    }
623
624    /**
625     * Creates an HTML {@code LI} element with the given style and the given content.
626     *
627     * @param style the style
628     * @param body  the content
629     * @return the element
630     */
631    public static HtmlTree LI(HtmlStyle style, Content body) {
632        return LI(body)
633            .setStyle(style);
634    }
635
636    /**
637     * Creates an HTML {@code LINK} tag with the given attributes.
638     *
639     * @param rel   the relevance of the link: the {@code rel} attribute
640     * @param type  the type of link: the {@code type} attribute
641     * @param href  the path for the link: the {@code href} attribute
642     * @param title title for the link: the {@code title} attribute
643     * @return the element
644     */
645    public static HtmlTree LINK(String rel, String type, String href,
646            String title) {
647        return new HtmlTree(TagName.LINK)
648            .put(HtmlAttr.REL, rel)
649            .put(HtmlAttr.TYPE, type)
650            .put(HtmlAttr.HREF, href)
651            .put(HtmlAttr.TITLE, title);
652    }
653
654    /**
655     * Creates an HTML {@code MAIN} element.
656     * The role is set to {@code main}.
657     *
658     * @return the element
659     */
660    public static HtmlTree MAIN() {
661        return new HtmlTree(TagName.MAIN)
662            .setRole(Role.MAIN);
663    }
664
665    /**
666     * Creates an HTML {@code MAIN} element with the given content.
667     * The role is set to {@code main}.
668     *
669     * @return the element
670     */
671    public static HtmlTree MAIN(Content body) {
672        return new HtmlTree(TagName.MAIN)
673            .setRole(Role.MAIN)
674            .add(body);
675    }
676
677    /**
678     * Creates an HTML {@code META} element with {@code http-equiv} and {@code content} attributes.
679     *
680     * @param httpEquiv the value for the {@code http-equiv} attribute
681     * @param content   the type of content, to be used in the {@code content} attribute
682     * @param charset   the character set for the document, to be used in the {@code content} attribute
683     * @return the element
684     */
685    public static HtmlTree META(String httpEquiv, String content,
686            String charset) {
687        return new HtmlTree(TagName.META)
688            .put(HtmlAttr.HTTP_EQUIV, httpEquiv)
689            .put(HtmlAttr.CONTENT, content + "; charset=" + charset);
690    }
691
692    /**
693     * Creates an HTML {@code META} element with {@code name} and {@code content} attributes.
694     *
695     * @param name    the value for the {@code name} attribute
696     * @param content the value for the {@code content} attribute
697     * @return the element
698     */
699    public static HtmlTree META(String name, String content) {
700        return new HtmlTree(TagName.META)
701            .put(HtmlAttr.NAME, name)
702            .put(HtmlAttr.CONTENT, content);
703    }
704
705    /**
706     * Creates an HTML {@code NAV} element.
707     * The role is set to {@code navigation}.
708     *
709     * @return the element
710     */
711    public static HtmlTree NAV() {
712        return new HtmlTree(TagName.NAV)
713            .setRole(Role.NAVIGATION);
714    }
715
716    /**
717     * Creates an HTML {@code NOSCRIPT} element with some content.
718     *
719     * @param body the content
720     * @return the element
721     */
722    public static HtmlTree NOSCRIPT(Content body) {
723        return new HtmlTree(TagName.NOSCRIPT)
724            .add(body);
725    }
726
727    /**
728     * Creates an HTML {@code P} element with some content.
729     *
730     * @param body the content
731     * @return the element
732     */
733    public static HtmlTree P(Content body) {
734        return new HtmlTree(TagName.P)
735            .add(body);
736    }
737
738    /**
739     * Creates an HTML {@code P} element with the given style and some content.
740     *
741     * @param style the style
742     * @param body  the content
743     * @return the element
744     */
745    public static HtmlTree P(HtmlStyle style, Content body) {
746        return P(body)
747            .setStyle(style);
748    }
749
750    /**
751     * Creates an HTML {@code PRE} element with some content.
752     *
753     * @param body  the content
754     * @return the element
755     */
756    public static HtmlTree PRE(Content body) {
757        return new HtmlTree(TagName.PRE).add(body);
758    }
759
760    /**
761     * Creates an HTML {@code SCRIPT} element with some script content.
762     * The type of the script is set to {@code text/javascript}.
763     *
764     * @param src the content
765     * @return the element
766     */
767    public static HtmlTree SCRIPT(String src) {
768        return new HtmlTree(TagName.SCRIPT)
769            .put(HtmlAttr.TYPE, "text/javascript")
770            .put(HtmlAttr.SRC, src);
771
772    }
773
774    /**
775     * Creates an HTML {@code SECTION} element with the given style.
776     *
777     * @param style the style
778     * @return the element
779     */
780    public static HtmlTree SECTION(HtmlStyle style) {
781        return new HtmlTree(TagName.SECTION)
782            .setStyle(style);
783    }
784
785    /**
786     * Creates an HTML {@code SECTION} element with the given style and some content.
787     *
788     * @param style the style
789     * @param body  the content
790     * @return the element
791     */
792    public static HtmlTree SECTION(HtmlStyle style, Content body) {
793        return new HtmlTree(TagName.SECTION)
794            .setStyle(style)
795            .add(body);
796    }
797
798    /**
799     * Creates an HTML {@code SMALL} element with some content.
800     *
801     * @param body  the content
802     * @return the element
803     */
804    public static HtmlTree SMALL(Content body) {
805        return new HtmlTree(TagName.SMALL)
806            .add(body);
807    }
808
809    /**
810     * Creates an HTML {@code SPAN} element with some content.
811     *
812     * @param body  the content
813     * @return the element
814     */
815    public static HtmlTree SPAN(Content body) {
816        return new HtmlTree(TagName.SPAN)
817            .add(body);
818    }
819
820    /**
821     * Creates an HTML {@code SPAN} element with the given style.
822     *
823     * @param styleClass the style
824     * @return the element
825     */
826    public static HtmlTree SPAN(HtmlStyle styleClass) {
827        return new HtmlTree(TagName.SPAN)
828            .setStyle(styleClass);
829    }
830
831    /**
832     * Creates an HTML {@code SPAN} element with the given style and some content.
833     *
834     * @param styleClass the style
835     * @param body       the content
836     * @return the element
837     */
838    public static HtmlTree SPAN(HtmlStyle styleClass, Content body) {
839        return SPAN(body)
840            .setStyle(styleClass);
841    }
842
843    /**
844     * Creates an HTML {@code SPAN} element with the given id and some content.
845     *
846     * @param id    the id
847     * @param body  the content
848     * @return the element
849     */
850    public static HtmlTree SPAN_ID(HtmlId id, Content body) {
851        return new HtmlTree(TagName.SPAN)
852            .setId(id)
853            .add(body);
854    }
855
856    /**
857     * Creates an HTML {@code SPAN} element with the given id and style, and some content.
858     *
859     * @param id    the id
860     * @param style the style
861     * @param body  the content
862     * @return the element
863     */
864    public static HtmlTree SPAN(HtmlId id, HtmlStyle style, Content body) {
865        return new HtmlTree(TagName.SPAN)
866            .setId(id)
867            .setStyle(style)
868            .add(body);
869    }
870
871    /**
872     * Creates an HTML {@code SUMMARY} element with the given content.
873     *
874     * @param body the content
875     * @return the element
876     */
877    public static HtmlTree SUMMARY(Content body) {
878        return new HtmlTree(TagName.SUMMARY)
879            .add(body);
880    }
881
882    /**
883     * Creates an HTML {@code SUP} element with the given content.
884     *
885     * @param body  the content
886     * @return the element
887     */
888    public static HtmlTree SUP(Content body) {
889        return new HtmlTree(TagName.SUP)
890            .add(body);
891    }
892
893    /**
894     * Creates an HTML {@code TD} element with the given style and some content.
895     *
896     * @param style the style
897     * @param body  the content
898     * @return the element
899     */
900    public static HtmlTree TD(HtmlStyle style, Content body) {
901        return new HtmlTree(TagName.TD)
902            .setStyle(style)
903            .add(body);
904    }
905
906    /**
907     * Creates an HTML {@code TH} element with the given style and scope, and some content.
908     *
909     * @param style the style
910     * @param scope the value for the {@code scope} attribute
911     * @param body  the content
912     * @return the element
913     */
914    public static HtmlTree TH(HtmlStyle style, String scope, Content body) {
915        return new HtmlTree(TagName.TH)
916            .setStyle(style)
917            .put(HtmlAttr.SCOPE, scope)
918            .add(body);
919    }
920
921    /**
922     * Creates an HTML {@code TH} element with the given scope, and some content.
923     *
924     * @param scope the value for the {@code scope} attribute
925     * @param body  the content
926     * @return the element
927     */
928    public static HtmlTree TH(String scope, Content body) {
929        return new HtmlTree(TagName.TH)
930            .put(HtmlAttr.SCOPE, scope)
931            .add(body);
932    }
933
934    /**
935     * Creates an HTML {@code TITLE} element with some content.
936     *
937     * @param body the content
938     * @return the element
939     */
940    public static HtmlTree TITLE(String body) {
941        return new HtmlTree(TagName.TITLE)
942            .add(body);
943    }
944
945    /**
946     * Creates an HTML {@code UL} element with the given style.
947     *
948     * @param style the style
949     * @return the element
950     */
951    public static HtmlTree UL(HtmlStyle style) {
952        return new HtmlTree(TagName.UL)
953            .setStyle(style);
954    }
955
956    /**
957     * Creates an HTML {@code UL} element with the given style and some content.
958     *
959     * @param style the style
960     * @param first the initial content
961     * @param more  additional content
962     * @return the element
963     */
964    public static HtmlTree UL(HtmlStyle style, Content first, Content... more) {
965        var ul = new HtmlTree(TagName.UL)
966            .setStyle(style);
967        ul.add(first);
968        for (Content c : more) {
969            ul.add(c);
970        }
971        return ul;
972    }
973
974    /**
975     * Creates an HTML {@code UL} element with the given style and content generated
976     * from a collection of items.
977     *
978     * @param style the style
979     * @param items the items to be added to the list
980     * @param mapper a mapper to create the content for each item
981     * @return the element
982     */
983    public static <T> HtmlTree UL(HtmlStyle style, Collection<T> items,
984            Function<T, Content> mapper) {
985        return new HtmlTree(TagName.UL)
986            .setStyle(style)
987            .addAll(items, mapper);
988    }
989
990    @Override
991    public boolean isEmpty() {
992        return (!hasContent() && !hasAttrs());
993    }
994
995    /**
996     * Returns true if the HTML tree has content.
997     *
998     * @return true if the HTML tree has content else return false
999     */
1000    public boolean hasContent() {
1001        return (!content.isEmpty());
1002    }
1003
1004    /**
1005     * Returns true if the HTML tree has attributes.
1006     *
1007     * @return true if the HTML tree has attributes else return false
1008     */
1009    public boolean hasAttrs() {
1010        return (!attrs.isEmpty());
1011    }
1012
1013    /**
1014     * Returns true if the HTML tree has a specific attribute.
1015     *
1016     * @param attrName name of the attribute to check within the HTML tree
1017     * @return true if the HTML tree has the specified attribute else return false
1018     */
1019    public boolean hasAttr(HtmlAttr attrName) {
1020        return (attrs.containsKey(attrName));
1021    }
1022
1023    /**
1024     * Returns {@code true} if the HTML tree does not affect the output and can be discarded.
1025     * This implementation considers non-void elements without content or {@code id} attribute
1026     * as discardable, with the exception of {@code SCRIPT} which can sometimes be used without
1027     * content.
1028     *
1029     * @return true if the HTML tree can be discarded without affecting the output
1030     */
1031    @Override
1032    public boolean isDiscardable() {
1033        return !isVoid()
1034            && !hasContent()
1035            && !hasAttr(HtmlAttr.ID)
1036            && tagName != TagName.SCRIPT;
1037    }
1038
1039    /**
1040     * Returns true if the element is a normal element that is <em>phrasing content</em>.
1041     *
1042     * @return true if this is an inline element
1043     *
1044     * @see <a href="https://www.w3.org/TR/html51/dom.html#kinds-of-content-phrasing-content">Phrasing Content</a>
1045     */
1046    public boolean isInline() {
1047        return switch (tagName) {
1048        case A, BUTTON, BR, CODE, EM, I, IMG, LABEL, SMALL, SPAN, STRONG, SUB, SUP, WBR -> true;
1049        default -> false;
1050        };
1051    }
1052
1053    /**
1054     * Returns whether or not this is a <em>void</em> element.
1055     *
1056     * @return whether or not this is a void element
1057     *
1058     * @see <a href="https://www.w3.org/TR/html51/syntax.html#void-elements">Void Elements</a>
1059     */
1060    public boolean isVoid() {
1061        return switch (tagName) {
1062        case BR, HR, IMG, INPUT, LINK, META, WBR -> true;
1063        default -> false;
1064        };
1065    }
1066
1067    @Override
1068    public boolean write(Writer out, String newline, boolean atNewline)
1069            throws IOException {
1070        boolean isInline = isInline();
1071        if (!isInline && !atNewline) {
1072            out.write(newline);
1073        }
1074        String tagString = tagName.toString();
1075        out.write("<");
1076        out.write(tagString);
1077        for (var attr : attrs.entrySet()) {
1078            var key = attr.getKey();
1079            var value = attr.getValue();
1080            out.write(" ");
1081            out.write(key.toString());
1082            if (!value.isEmpty()) {
1083                out.write("=\"");
1084                out.write(value.replace("\"", "&quot;"));
1085                out.write("\"");
1086            }
1087        }
1088        out.write(">");
1089        boolean nl = false;
1090        for (Content c : content) {
1091            nl = c.write(out, newline, nl);
1092        }
1093        if (!isVoid()) {
1094            out.write("</");
1095            out.write(tagString);
1096            out.write(">");
1097        }
1098        if (!isInline) {
1099            out.write(newline);
1100            return true;
1101        } else {
1102            return false;
1103        }
1104    }
1105
1106    /**
1107     * Given a Content node, strips all html characters and
1108     * returns the result.
1109     *
1110     * @param body The content node to check.
1111     * @return the plain text from the content node
1112     *
1113     */
1114    private static String stripHtml(Content body) {
1115        String rawString = body.toString();
1116        // remove HTML tags
1117        rawString = rawString.replaceAll("<.*?>", " ");
1118        // consolidate multiple spaces between a word to a single space
1119        rawString = rawString.replaceAll("\\b\\s{2,}\\b", " ");
1120        // remove extra whitespaces
1121        return rawString.trim();
1122    }
1123}