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("\"", """)); 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}