001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.doxia.sink.impl; 020 021import javax.swing.text.AttributeSet; 022import javax.swing.text.MutableAttributeSet; 023import javax.swing.text.html.HTML.Tag; 024 025import java.io.PrintWriter; 026import java.io.StringWriter; 027import java.io.Writer; 028import java.util.Collections; 029import java.util.EmptyStackException; 030import java.util.Enumeration; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Map; 034import java.util.Objects; 035import java.util.Stack; 036import java.util.regex.Pattern; 037 038import org.apache.maven.doxia.markup.HtmlMarkup; 039import org.apache.maven.doxia.markup.Markup; 040import org.apache.maven.doxia.sink.Sink; 041import org.apache.maven.doxia.sink.SinkEventAttributes; 042import org.apache.maven.doxia.util.DoxiaStringUtils; 043import org.apache.maven.doxia.util.DoxiaUtils; 044import org.apache.maven.doxia.util.HtmlTools; 045import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049/** 050 * Abstract base xhtml5 sink implementation. 051 */ 052public class Xhtml5BaseSink extends AbstractXmlSink implements HtmlMarkup { 053 private static final Logger LOGGER = LoggerFactory.getLogger(Xhtml5BaseSink.class); 054 055 // ---------------------------------------------------------------------- 056 // Instance fields 057 // ---------------------------------------------------------------------- 058 059 /** The PrintWriter to write the result. */ 060 private final PrintWriter writer; 061 062 /** Used to identify if a class string contains `hidden` */ 063 private static final Pattern HIDDEN_CLASS_PATTERN = Pattern.compile("(?:.*\\s|^)hidden(?:\\s.*|$)"); 064 065 /** Used to collect text events mainly for the head events. */ 066 private StringBuffer textBuffer = new StringBuffer(); 067 068 /** An indication on if we're inside a head. */ 069 private boolean headFlag; 070 071 /** Keep track of the main and div tags for content events. */ 072 protected Stack<Tag> contentStack = new Stack<>(); 073 074 /** Keep track of the closing tags for inline events. */ 075 protected Stack<List<Tag>> inlineStack = new Stack<>(); 076 077 /** An indication on if we're inside a paragraph flag. */ 078 private boolean paragraphFlag; 079 080 protected enum VerbatimMode { 081 /** not in verbatim mode */ 082 OFF, 083 /** Inside {@code <pre>} */ 084 ON, 085 /** Inside {@code <pre><code>} */ 086 ON_WITH_CODE, 087 /** Same as {@link #ON_WITH_CODE} but after some text has been emitted */ 088 ON_WITH_CODE_AFTER_TEXT 089 } 090 /** An indication on if we're in verbatim mode and if so, surrounded by which tags. */ 091 private VerbatimMode verbatimMode; 092 093 /** Stack of alignment int[] of table cells. */ 094 private final LinkedList<int[]> cellJustifStack; 095 096 /** Stack of justification of table cells. */ 097 private final LinkedList<Boolean> isCellJustifStack; 098 099 /** Stack of current table cell. */ 100 private final LinkedList<Integer> cellCountStack; 101 102 /** Used to style successive table rows differently. */ 103 private boolean evenTableRow = true; 104 105 /** The stack of StringWriter to write the table result temporary, so we could play with the output DOXIA-177. */ 106 private final LinkedList<StringWriter> tableContentWriterStack; 107 108 private final LinkedList<StringWriter> tableCaptionWriterStack; 109 110 private final LinkedList<PrettyPrintXMLWriter> tableCaptionXMLWriterStack; 111 112 /** The stack of table caption */ 113 private final LinkedList<String> tableCaptionStack; 114 115 /** used to store attributes passed to table(). */ 116 protected MutableAttributeSet tableAttributes; 117 118 // ---------------------------------------------------------------------- 119 // Constructor 120 // ---------------------------------------------------------------------- 121 122 /** 123 * Constructor, initialize the PrintWriter. 124 * 125 * @param out The writer to write the result. 126 */ 127 public Xhtml5BaseSink(Writer out) { 128 this.writer = new PrintWriter(out); 129 130 this.cellJustifStack = new LinkedList<>(); 131 this.isCellJustifStack = new LinkedList<>(); 132 this.cellCountStack = new LinkedList<>(); 133 this.tableContentWriterStack = new LinkedList<>(); 134 this.tableCaptionWriterStack = new LinkedList<>(); 135 this.tableCaptionXMLWriterStack = new LinkedList<>(); 136 this.tableCaptionStack = new LinkedList<>(); 137 138 initInternal(); 139 } 140 141 /** 142 * Called from constructor and from {@link #init()} to initialize certain instance fields. 143 */ 144 private void initInternal() { 145 this.headFlag = false; 146 this.paragraphFlag = false; 147 this.verbatimMode = VerbatimMode.OFF; 148 149 this.evenTableRow = true; 150 this.tableAttributes = null; 151 } 152 // ---------------------------------------------------------------------- 153 // Accessor methods 154 // ---------------------------------------------------------------------- 155 156 /** 157 * To use mainly when playing with the head events. 158 * 159 * @return the current buffer of text events. 160 */ 161 protected StringBuffer getTextBuffer() { 162 return this.textBuffer; 163 } 164 165 /** 166 * <p>Setter for the field <code>headFlag</code>.</p> 167 * 168 * @param headFlag an header flag. 169 */ 170 protected void setHeadFlag(boolean headFlag) { 171 this.headFlag = headFlag; 172 } 173 174 /** 175 * <p>isHeadFlag.</p> 176 * 177 * @return the current headFlag. 178 */ 179 protected boolean isHeadFlag() { 180 return this.headFlag; 181 } 182 183 /** 184 * 185 * @return the current verbatim mode. 186 */ 187 protected VerbatimMode getVerbatimMode() { 188 return this.verbatimMode; 189 } 190 191 /** 192 * <p>Setter for the field <code>verbatimMode</code>.</p> 193 * 194 * @param mode a verbatim mode. 195 */ 196 protected void setVerbatimMode(VerbatimMode mode) { 197 this.verbatimMode = mode; 198 } 199 200 /** 201 * 202 * @return {@code true} if inside verbatim section, {@code false} otherwise 203 */ 204 protected boolean isVerbatim() { 205 return this.verbatimMode != VerbatimMode.OFF; 206 } 207 208 /** 209 * <p>Setter for the field <code>cellJustif</code>.</p> 210 * 211 * @param justif the new cell justification array. 212 */ 213 protected void setCellJustif(int[] justif) { 214 this.cellJustifStack.addLast(justif); 215 this.isCellJustifStack.addLast(Boolean.TRUE); 216 } 217 218 /** 219 * <p>Getter for the field <code>cellJustif</code>.</p> 220 * 221 * @return the current cell justification array. 222 */ 223 protected int[] getCellJustif() { 224 return this.cellJustifStack.getLast(); 225 } 226 227 /** 228 * <p>Setter for the field <code>cellCount</code>.</p> 229 * 230 * @param count the new cell count. 231 */ 232 protected void setCellCount(int count) { 233 this.cellCountStack.addLast(count); 234 } 235 236 /** 237 * <p>Getter for the field <code>cellCount</code>.</p> 238 * 239 * @return the current cell count. 240 */ 241 protected int getCellCount() { 242 return this.cellCountStack.getLast(); 243 } 244 245 @Override 246 protected void init() { 247 super.init(); 248 249 resetTextBuffer(); 250 251 this.cellJustifStack.clear(); 252 this.isCellJustifStack.clear(); 253 this.cellCountStack.clear(); 254 this.tableContentWriterStack.clear(); 255 this.tableCaptionWriterStack.clear(); 256 this.tableCaptionXMLWriterStack.clear(); 257 this.tableCaptionStack.clear(); 258 this.inlineStack.clear(); 259 260 initInternal(); 261 } 262 263 /** 264 * Reset the text buffer. 265 */ 266 protected void resetTextBuffer() { 267 this.textBuffer = new StringBuffer(); 268 } 269 270 // ---------------------------------------------------------------------- 271 // Sections 272 // ---------------------------------------------------------------------- 273 274 @Override 275 public void article(SinkEventAttributes attributes) { 276 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 277 278 writeStartTag(HtmlMarkup.ARTICLE, atts); 279 } 280 281 @Override 282 public void article_() { 283 writeEndTag(HtmlMarkup.ARTICLE); 284 } 285 286 @Override 287 public void navigation(SinkEventAttributes attributes) { 288 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 289 290 writeStartTag(HtmlMarkup.NAV, atts); 291 } 292 293 @Override 294 public void navigation_() { 295 writeEndTag(HtmlMarkup.NAV); 296 } 297 298 @Override 299 public void sidebar(SinkEventAttributes attributes) { 300 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 301 302 writeStartTag(HtmlMarkup.ASIDE, atts); 303 } 304 305 @Override 306 public void sidebar_() { 307 writeEndTag(HtmlMarkup.ASIDE); 308 } 309 310 @Override 311 public void section(int level, SinkEventAttributes attributes) { 312 onSection(level, attributes); 313 } 314 315 @Override 316 public void sectionTitle(int level, SinkEventAttributes attributes) { 317 onSectionTitle(level, attributes); 318 } 319 320 @Override 321 public void sectionTitle_(int level) { 322 onSectionTitle_(level); 323 } 324 325 @Override 326 public void section_(int level) { 327 onSection_(level); 328 } 329 330 /** 331 * Starts a section. 332 * 333 * @param depth The level of the section. 334 * @param attributes some attributes. May be null. 335 */ 336 protected void onSection(int depth, SinkEventAttributes attributes) { 337 if (depth >= SECTION_LEVEL_1 && depth <= SECTION_LEVEL_6) { 338 MutableAttributeSet att = new SinkEventAttributeSet(); 339 att.addAttributes(convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES)); 340 341 writeStartTag(HtmlMarkup.SECTION, att); 342 } 343 } 344 345 /** 346 * Ends a section. 347 * 348 * @param depth The level of the section. 349 * @see #SECTION 350 */ 351 protected void onSection_(int depth) { 352 if (depth >= SECTION_LEVEL_1 && depth <= SECTION_LEVEL_6) { 353 writeEndTag(HtmlMarkup.SECTION); 354 } 355 } 356 357 /** 358 * Starts a section title. 359 * 360 * @param depth The level of the section title. 361 * @param attributes some attributes. May be null. 362 * @see #H1 363 * @see #H2 364 * @see #H3 365 * @see #H4 366 * @see #H5 367 * @see #H6 368 */ 369 protected void onSectionTitle(int depth, SinkEventAttributes attributes) { 370 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 371 372 if (depth == SECTION_LEVEL_1) { 373 writeStartTag(HtmlMarkup.H1, atts); 374 } else if (depth == SECTION_LEVEL_2) { 375 writeStartTag(HtmlMarkup.H2, atts); 376 } else if (depth == SECTION_LEVEL_3) { 377 writeStartTag(HtmlMarkup.H3, atts); 378 } else if (depth == SECTION_LEVEL_4) { 379 writeStartTag(HtmlMarkup.H4, atts); 380 } else if (depth == SECTION_LEVEL_5) { 381 writeStartTag(HtmlMarkup.H5, atts); 382 } else if (depth == SECTION_LEVEL_6) { 383 writeStartTag(HtmlMarkup.H6, atts); 384 } 385 } 386 387 /** 388 * Ends a section title. 389 * 390 * @param depth The level of the section title. 391 * @see #H1 392 * @see #H2 393 * @see #H3 394 * @see #H4 395 * @see #H5 396 * @see #H6 397 */ 398 protected void onSectionTitle_(int depth) { 399 if (depth == SECTION_LEVEL_1) { 400 writeEndTag(HtmlMarkup.H1); 401 } else if (depth == SECTION_LEVEL_2) { 402 writeEndTag(HtmlMarkup.H2); 403 } else if (depth == SECTION_LEVEL_3) { 404 writeEndTag(HtmlMarkup.H3); 405 } else if (depth == SECTION_LEVEL_4) { 406 writeEndTag(HtmlMarkup.H4); 407 } else if (depth == SECTION_LEVEL_5) { 408 writeEndTag(HtmlMarkup.H5); 409 } else if (depth == SECTION_LEVEL_6) { 410 writeEndTag(HtmlMarkup.H6); 411 } 412 } 413 414 @Override 415 public void header(SinkEventAttributes attributes) { 416 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 417 418 writeStartTag(HtmlMarkup.HEADER, atts); 419 } 420 421 @Override 422 public void header_() { 423 writeEndTag(HtmlMarkup.HEADER); 424 } 425 426 @Override 427 public void content(SinkEventAttributes attributes) { 428 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 429 430 if (contentStack.empty()) { 431 writeStartTag(contentStack.push(HtmlMarkup.MAIN), atts); 432 } else { 433 if (atts == null) { 434 atts = new SinkEventAttributeSet(1); 435 } 436 437 String divClass = "content"; 438 if (atts.isDefined(SinkEventAttributes.CLASS)) { 439 divClass += " " + atts.getAttribute(SinkEventAttributes.CLASS).toString(); 440 } 441 442 atts.addAttribute(SinkEventAttributes.CLASS, divClass); 443 444 writeStartTag(contentStack.push(HtmlMarkup.DIV), atts); 445 } 446 } 447 448 @Override 449 public void content_() { 450 try { 451 writeEndTag(contentStack.pop()); 452 } catch (EmptyStackException ese) { 453 /* do nothing if the stack is empty */ 454 } 455 } 456 457 @Override 458 public void footer(SinkEventAttributes attributes) { 459 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 460 461 writeStartTag(HtmlMarkup.FOOTER, atts); 462 } 463 464 @Override 465 public void footer_() { 466 writeEndTag(HtmlMarkup.FOOTER); 467 } 468 469 // ----------------------------------------------------------------------- 470 // 471 // ----------------------------------------------------------------------- 472 473 /** 474 * {@inheritDoc} 475 * @see javax.swing.text.html.HTML.Tag#UL 476 */ 477 @Override 478 public void list(SinkEventAttributes attributes) { 479 if (paragraphFlag) { 480 // The content of element type "p" must match 481 // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong| 482 // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)". 483 paragraph_(); 484 } 485 486 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 487 488 writeStartTag(HtmlMarkup.UL, atts); 489 } 490 491 /** 492 * {@inheritDoc} 493 * @see javax.swing.text.html.HTML.Tag#UL 494 */ 495 @Override 496 public void list_() { 497 writeEndTag(HtmlMarkup.UL); 498 } 499 500 /** 501 * {@inheritDoc} 502 * @see javax.swing.text.html.HTML.Tag#LI 503 */ 504 @Override 505 public void listItem(SinkEventAttributes attributes) { 506 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 507 508 writeStartTag(HtmlMarkup.LI, atts); 509 } 510 511 /** 512 * {@inheritDoc} 513 * @see javax.swing.text.html.HTML.Tag#LI 514 */ 515 @Override 516 public void listItem_() { 517 writeEndTag(HtmlMarkup.LI); 518 } 519 520 /** 521 * The default list style depends on the numbering. 522 * 523 * {@inheritDoc} 524 * @see javax.swing.text.html.HTML.Tag#OL 525 */ 526 @Override 527 public void numberedList(int numbering, SinkEventAttributes attributes) { 528 if (paragraphFlag) { 529 // The content of element type "p" must match 530 // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong| 531 // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)". 532 paragraph_(); 533 } 534 535 String olStyle = "list-style-type: "; 536 switch (numbering) { 537 case NUMBERING_UPPER_ALPHA: 538 olStyle += "upper-alpha"; 539 break; 540 case NUMBERING_LOWER_ALPHA: 541 olStyle += "lower-alpha"; 542 break; 543 case NUMBERING_UPPER_ROMAN: 544 olStyle += "upper-roman"; 545 break; 546 case NUMBERING_LOWER_ROMAN: 547 olStyle += "lower-roman"; 548 break; 549 case NUMBERING_DECIMAL: 550 default: 551 olStyle += "decimal"; 552 } 553 olStyle += ";"; 554 555 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 556 557 if (atts == null) { 558 atts = new SinkEventAttributeSet(1); 559 } 560 561 if (atts.isDefined(SinkEventAttributes.STYLE)) { 562 olStyle += " " + atts.getAttribute(SinkEventAttributes.STYLE).toString(); 563 } 564 565 atts.addAttribute(SinkEventAttributes.STYLE, olStyle); 566 567 writeStartTag(HtmlMarkup.OL, atts); 568 } 569 570 /** 571 * {@inheritDoc} 572 * @see javax.swing.text.html.HTML.Tag#OL 573 */ 574 @Override 575 public void numberedList_() { 576 writeEndTag(HtmlMarkup.OL); 577 } 578 579 /** 580 * {@inheritDoc} 581 * @see javax.swing.text.html.HTML.Tag#LI 582 */ 583 @Override 584 public void numberedListItem(SinkEventAttributes attributes) { 585 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 586 587 writeStartTag(HtmlMarkup.LI, atts); 588 } 589 590 /** 591 * {@inheritDoc} 592 * @see javax.swing.text.html.HTML.Tag#LI 593 */ 594 @Override 595 public void numberedListItem_() { 596 writeEndTag(HtmlMarkup.LI); 597 } 598 599 /** 600 * {@inheritDoc} 601 * @see javax.swing.text.html.HTML.Tag#DL 602 */ 603 @Override 604 public void definitionList(SinkEventAttributes attributes) { 605 if (paragraphFlag) { 606 // The content of element type "p" must match 607 // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong| 608 // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)". 609 paragraph_(); 610 } 611 612 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 613 614 writeStartTag(HtmlMarkup.DL, atts); 615 } 616 617 /** 618 * {@inheritDoc} 619 * @see javax.swing.text.html.HTML.Tag#DL 620 */ 621 @Override 622 public void definitionList_() { 623 writeEndTag(HtmlMarkup.DL); 624 } 625 626 /** 627 * {@inheritDoc} 628 * @see javax.swing.text.html.HTML.Tag#DT 629 */ 630 @Override 631 public void definedTerm(SinkEventAttributes attributes) { 632 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 633 634 writeStartTag(HtmlMarkup.DT, atts); 635 } 636 637 /** 638 * {@inheritDoc} 639 * @see javax.swing.text.html.HTML.Tag#DT 640 */ 641 @Override 642 public void definedTerm_() { 643 writeEndTag(HtmlMarkup.DT); 644 } 645 646 /** 647 * {@inheritDoc} 648 * @see javax.swing.text.html.HTML.Tag#DD 649 */ 650 @Override 651 public void definition(SinkEventAttributes attributes) { 652 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 653 654 writeStartTag(HtmlMarkup.DD, atts); 655 } 656 657 /** 658 * {@inheritDoc} 659 * @see javax.swing.text.html.HTML.Tag#DD 660 */ 661 @Override 662 public void definition_() { 663 writeEndTag(HtmlMarkup.DD); 664 } 665 666 @Override 667 public void figure(SinkEventAttributes attributes) { 668 MutableAttributeSet filtered = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 669 writeStartTag(HtmlMarkup.FIGURE, filtered); 670 } 671 672 @Override 673 public void figure_() { 674 writeEndTag(HtmlMarkup.FIGURE); 675 } 676 677 @Override 678 public void figureGraphics(String src, SinkEventAttributes attributes) { 679 MutableAttributeSet filtered = convertAndFilterAttributes(attributes, SinkUtils.SINK_IMG_ATTRIBUTES); 680 if (filtered != null) { 681 filtered.removeAttribute(SinkEventAttributes.SRC.toString()); 682 } 683 684 int count = (attributes == null ? 1 : attributes.getAttributeCount() + 1); 685 686 MutableAttributeSet atts = new SinkEventAttributeSet(count); 687 688 atts.addAttribute(SinkEventAttributes.SRC, HtmlTools.escapeHTML(src, true)); 689 atts.addAttributes(filtered); 690 691 writeStartTag(HtmlMarkup.IMG, atts, true); 692 } 693 694 @Override 695 public void figureCaption(SinkEventAttributes attributes) { 696 MutableAttributeSet filtered = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 697 writeStartTag(HtmlMarkup.FIGCAPTION, filtered); 698 } 699 700 @Override 701 public void figureCaption_() { 702 writeEndTag(HtmlMarkup.FIGCAPTION); 703 } 704 705 /** 706 * {@inheritDoc} 707 * @see javax.swing.text.html.HTML.Tag#P 708 */ 709 @Override 710 public void paragraph(SinkEventAttributes attributes) { 711 paragraphFlag = true; 712 713 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 714 715 writeStartTag(HtmlMarkup.P, atts); 716 } 717 718 /** 719 * {@inheritDoc} 720 * @see javax.swing.text.html.HTML.Tag#P 721 */ 722 @Override 723 public void paragraph_() { 724 if (paragraphFlag) { 725 writeEndTag(HtmlMarkup.P); 726 paragraphFlag = false; 727 } 728 } 729 730 @Override 731 public void data(String value, SinkEventAttributes attributes) { 732 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 733 734 MutableAttributeSet att = new SinkEventAttributeSet(); 735 if (value != null) { 736 att.addAttribute(SinkEventAttributes.VALUE, value); 737 } 738 att.addAttributes(atts); 739 740 writeStartTag(HtmlMarkup.DATA, att); 741 } 742 743 @Override 744 public void data_() { 745 writeEndTag(HtmlMarkup.DATA); 746 } 747 748 @Override 749 public void time(String datetime, SinkEventAttributes attributes) { 750 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 751 752 MutableAttributeSet att = new SinkEventAttributeSet(); 753 if (datetime != null) { 754 att.addAttribute("datetime", datetime); 755 } 756 att.addAttributes(atts); 757 758 writeStartTag(HtmlMarkup.TIME, att); 759 } 760 761 @Override 762 public void time_() { 763 writeEndTag(HtmlMarkup.TIME); 764 } 765 766 /** 767 * {@inheritDoc} 768 * @see javax.swing.text.html.HTML.Tag#ADDRESS 769 */ 770 @Override 771 public void address(SinkEventAttributes attributes) { 772 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 773 774 writeStartTag(HtmlMarkup.ADDRESS, atts); 775 } 776 777 /** 778 * {@inheritDoc} 779 * @see javax.swing.text.html.HTML.Tag#ADDRESS 780 */ 781 @Override 782 public void address_() { 783 writeEndTag(HtmlMarkup.ADDRESS); 784 } 785 786 /** 787 * {@inheritDoc} 788 * @see javax.swing.text.html.HTML.Tag#BLOCKQUOTE 789 */ 790 @Override 791 public void blockquote(SinkEventAttributes attributes) { 792 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 793 794 writeStartTag(HtmlMarkup.BLOCKQUOTE, atts); 795 } 796 797 /** 798 * {@inheritDoc} 799 * @see javax.swing.text.html.HTML.Tag#BLOCKQUOTE 800 */ 801 @Override 802 public void blockquote_() { 803 writeEndTag(HtmlMarkup.BLOCKQUOTE); 804 } 805 806 /** 807 * {@inheritDoc} 808 * @see javax.swing.text.html.HTML.Tag#DIV 809 */ 810 @Override 811 public void division(SinkEventAttributes attributes) { 812 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 813 814 writeStartTag(HtmlMarkup.DIV, atts); 815 } 816 817 /** 818 * {@inheritDoc} 819 * @see javax.swing.text.html.HTML.Tag#DIV 820 */ 821 @Override 822 public void division_() { 823 writeEndTag(HtmlMarkup.DIV); 824 } 825 826 /** 827 * Depending on whether the decoration attribute is "source" or not, this leads 828 * to either emitting {@code <pre><code>} or just {@code <pre>}. 829 * No default classes are emitted but the given attributes are always added to the {@code pre} element only. 830 * 831 * {@inheritDoc} 832 * @see javax.swing.text.html.HTML.Tag#PRE 833 * @see javax.swing.text.html.HTML.Tag#CODE 834 */ 835 @Override 836 public void verbatim(SinkEventAttributes attributes) { 837 if (paragraphFlag) { 838 // The content of element type "p" must match 839 // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong| 840 // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)". 841 paragraph_(); 842 } 843 844 final MutableAttributeSet atts; 845 846 if (attributes == null) { 847 atts = new SinkEventAttributeSet(); 848 } else { 849 atts = new SinkEventAttributeSet(attributes); 850 } 851 852 verbatimMode = VerbatimMode.ON; 853 if (atts.isDefined(SinkEventAttributes.DECORATION)) { 854 if ("source" 855 .equals(atts.getAttribute(SinkEventAttributes.DECORATION).toString())) { 856 verbatimMode = VerbatimMode.ON_WITH_CODE; 857 } 858 } 859 860 atts.removeAttribute(SinkEventAttributes.DECORATION); 861 MutableAttributeSet filtered = convertAndFilterAttributes(attributes, SinkUtils.SINK_VERBATIM_ATTRIBUTES); 862 863 writeStartTag(HtmlMarkup.PRE, filtered); 864 if (verbatimMode == VerbatimMode.ON_WITH_CODE) { 865 writeStartTag(HtmlMarkup.CODE); 866 } 867 } 868 869 /** 870 * {@inheritDoc} 871 * @see javax.swing.text.html.HTML.Tag#CODE 872 * @see javax.swing.text.html.HTML.Tag#PRE 873 */ 874 @Override 875 public void verbatim_() { 876 if (verbatimMode == VerbatimMode.ON_WITH_CODE || verbatimMode == VerbatimMode.ON_WITH_CODE_AFTER_TEXT) { 877 writeEndTag(HtmlMarkup.CODE); 878 } 879 writeEndTag(HtmlMarkup.PRE); 880 881 verbatimMode = VerbatimMode.OFF; 882 } 883 884 /** 885 * {@inheritDoc} 886 * @see javax.swing.text.html.HTML.Tag#HR 887 */ 888 @Override 889 public void horizontalRule(SinkEventAttributes attributes) { 890 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_HR_ATTRIBUTES); 891 892 writeSimpleTag(HtmlMarkup.HR, atts); 893 } 894 895 @Override 896 public void table(SinkEventAttributes attributes) { 897 this.tableContentWriterStack.addLast(new StringWriter()); 898 899 if (paragraphFlag) { 900 // The content of element type "p" must match 901 // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong| 902 // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)". 903 paragraph_(); 904 } 905 906 // start table with tableRows 907 if (attributes == null) { 908 this.tableAttributes = new SinkEventAttributeSet(0); 909 } else { 910 this.tableAttributes = convertAndFilterAttributes(attributes, SinkUtils.SINK_TABLE_ATTRIBUTES); 911 } 912 } 913 914 /** 915 * {@inheritDoc} 916 * @see javax.swing.text.html.HTML.Tag#TABLE 917 */ 918 @Override 919 public void table_() { 920 writeEndTag(HtmlMarkup.TABLE); 921 922 if (!this.cellCountStack.isEmpty()) { 923 this.cellCountStack.removeLast().toString(); 924 } 925 926 if (this.tableContentWriterStack.isEmpty()) { 927 LOGGER.warn("{}No table content", getLocationLogPrefix()); 928 return; 929 } 930 931 String tableContent = this.tableContentWriterStack.removeLast().toString(); 932 933 String tableCaption = null; 934 if (!this.tableCaptionStack.isEmpty() && this.tableCaptionStack.getLast() != null) { 935 tableCaption = this.tableCaptionStack.removeLast(); 936 } 937 938 if (tableCaption != null) { 939 // DOXIA-177 940 StringBuilder sb = new StringBuilder(); 941 sb.append(tableContent, 0, tableContent.indexOf(Markup.GREATER_THAN) + 1); 942 sb.append(tableCaption); 943 sb.append(tableContent.substring(tableContent.indexOf(Markup.GREATER_THAN) + 1)); 944 945 write(sb.toString()); 946 } else { 947 write(tableContent); 948 } 949 } 950 951 /** 952 * The default style class is <code>bodyTable</code>. 953 * 954 * @param grid if {@code true} the style class {@code bodyTableBorder} will be added 955 * 956 * {@inheritDoc} 957 * @see javax.swing.text.html.HTML.Tag#TABLE 958 */ 959 @Override 960 public void tableRows(int[] justification, boolean grid) { 961 setCellJustif(justification); 962 963 MutableAttributeSet att = new SinkEventAttributeSet(); 964 965 String tableClass = "bodyTable" + (grid ? " bodyTableBorder" : ""); 966 if (this.tableAttributes.isDefined(SinkEventAttributes.CLASS.toString())) { 967 tableClass += " " 968 + this.tableAttributes 969 .getAttribute(SinkEventAttributes.CLASS) 970 .toString(); 971 } 972 973 att.addAttribute(SinkEventAttributes.CLASS, tableClass); 974 975 att.addAttributes(this.tableAttributes); 976 this.tableAttributes.removeAttributes(this.tableAttributes); 977 978 writeStartTag(HtmlMarkup.TABLE, att); 979 980 this.cellCountStack.addLast(0); 981 } 982 983 @Override 984 public void tableRows_() { 985 if (!this.cellJustifStack.isEmpty()) { 986 this.cellJustifStack.removeLast(); 987 } 988 if (!this.isCellJustifStack.isEmpty()) { 989 this.isCellJustifStack.removeLast(); 990 } 991 992 this.evenTableRow = true; 993 } 994 995 /** 996 * Rows are striped with two colors by adding the class <code>a</code> or <code>b</code>. If the provided attributes 997 * specify the <code>hidden</code> class, the next call to tableRow will set the same striping class as this one. A 998 * style for <code>hidden</code> or <code>table.bodyTable hidden</code> may need to be provided to actually hide 999 * such a row. {@inheritDoc} 1000 * 1001 * @see javax.swing.text.html.HTML.Tag#TR 1002 */ 1003 @Override 1004 public void tableRow(SinkEventAttributes attributes) { 1005 MutableAttributeSet attrs = convertAndFilterAttributes(attributes, SinkUtils.SINK_TR_ATTRIBUTES); 1006 1007 if (attrs == null) { 1008 attrs = new SinkEventAttributeSet(); 1009 } 1010 1011 String rowClass = evenTableRow ? "a" : "b"; 1012 boolean hidden = false; 1013 if (attrs.isDefined(SinkEventAttributes.CLASS.toString())) { 1014 String givenRowClass = (String) attrs.getAttribute(SinkEventAttributes.CLASS.toString()); 1015 if (HIDDEN_CLASS_PATTERN.matcher(givenRowClass).matches()) { 1016 hidden = true; 1017 } 1018 rowClass += " " + givenRowClass; 1019 } 1020 1021 attrs.addAttribute(SinkEventAttributes.CLASS, rowClass); 1022 1023 writeStartTag(HtmlMarkup.TR, attrs); 1024 1025 if (!hidden) { 1026 evenTableRow = !evenTableRow; 1027 } 1028 1029 if (!this.cellCountStack.isEmpty()) { 1030 this.cellCountStack.removeLast(); 1031 this.cellCountStack.addLast(0); 1032 } 1033 } 1034 1035 /** 1036 * {@inheritDoc} 1037 * @see javax.swing.text.html.HTML.Tag#TR 1038 */ 1039 @Override 1040 public void tableRow_() { 1041 writeEndTag(HtmlMarkup.TR); 1042 } 1043 1044 @Override 1045 public void tableCell(SinkEventAttributes attributes) { 1046 tableCell(false, attributes); 1047 } 1048 1049 @Override 1050 public void tableHeaderCell(SinkEventAttributes attributes) { 1051 tableCell(true, attributes); 1052 } 1053 1054 /** 1055 * @param headerRow true if it is an header row 1056 * @param attributes the cell attributes 1057 * @see javax.swing.text.html.HTML.Tag#TH 1058 * @see javax.swing.text.html.HTML.Tag#TD 1059 */ 1060 private void tableCell(boolean headerRow, SinkEventAttributes attributes) { 1061 Tag t = (headerRow ? HtmlMarkup.TH : HtmlMarkup.TD); 1062 1063 if (!headerRow 1064 && cellCountStack != null 1065 && !cellCountStack.isEmpty() 1066 && cellJustifStack != null 1067 && !cellJustifStack.isEmpty() 1068 && getCellJustif() != null) { 1069 int cellCount = getCellCount(); 1070 int[] cellJustif = getCellJustif(); 1071 int currentCellJust = 1072 cellCount < cellJustif.length ? cellJustif[cellCount] : cellJustif[cellJustif.length - 1]; 1073 if (cellCount < cellJustif.length) { 1074 String tdStyle = getStyleForTableJustification(currentCellJust); 1075 if (tdStyle != null) { 1076 if (attributes == null) { 1077 attributes = new SinkEventAttributeSet(); 1078 } else if (attributes.isDefined(SinkEventAttributes.STYLE)) { 1079 tdStyle += " " 1080 + attributes 1081 .getAttribute(SinkEventAttributes.STYLE) 1082 .toString(); 1083 } 1084 attributes.addAttribute(SinkEventAttributes.STYLE, tdStyle); 1085 } 1086 } 1087 } 1088 1089 if (attributes == null) { 1090 writeStartTag(t, null); 1091 } else { 1092 writeStartTag(t, convertAndFilterAttributes(attributes, SinkUtils.SINK_TD_ATTRIBUTES)); 1093 } 1094 } 1095 1096 private static String getStyleForTableJustification(int justification) { 1097 String style = "text-align: "; 1098 switch (justification) { 1099 case Sink.JUSTIFY_CENTER: 1100 style += "center;"; 1101 break; 1102 case Sink.JUSTIFY_LEFT: 1103 style += "left;"; 1104 break; 1105 case Sink.JUSTIFY_RIGHT: 1106 style += "right;"; 1107 break; 1108 default: 1109 style = null; 1110 } 1111 return style; 1112 } 1113 1114 @Override 1115 public void tableCell_() { 1116 tableCell_(false); 1117 } 1118 1119 @Override 1120 public void tableHeaderCell_() { 1121 tableCell_(true); 1122 } 1123 1124 /** 1125 * Ends a table cell. 1126 * 1127 * @param headerRow true if it is an header row 1128 * @see javax.swing.text.html.HTML.Tag#TH 1129 * @see javax.swing.text.html.HTML.Tag#TD 1130 */ 1131 private void tableCell_(boolean headerRow) { 1132 Tag t = (headerRow ? HtmlMarkup.TH : HtmlMarkup.TD); 1133 1134 writeEndTag(t); 1135 1136 if (!this.isCellJustifStack.isEmpty() 1137 && this.isCellJustifStack.getLast().equals(Boolean.TRUE) 1138 && !this.cellCountStack.isEmpty()) { 1139 int cellCount = Integer.parseInt(this.cellCountStack.removeLast().toString()); 1140 this.cellCountStack.addLast(++cellCount); 1141 } 1142 } 1143 1144 /** 1145 * {@inheritDoc} 1146 * @see javax.swing.text.html.HTML.Tag#CAPTION 1147 */ 1148 @Override 1149 public void tableCaption(SinkEventAttributes attributes) { 1150 StringWriter sw = new StringWriter(); 1151 this.tableCaptionWriterStack.addLast(sw); 1152 this.tableCaptionXMLWriterStack.addLast(new PrettyPrintXMLWriter(sw)); 1153 1154 // TODO: tableCaption should be written before tableRows (DOXIA-177) 1155 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES); 1156 1157 writeStartTag(HtmlMarkup.CAPTION, atts); 1158 } 1159 1160 /** 1161 * {@inheritDoc} 1162 * @see javax.swing.text.html.HTML.Tag#CAPTION 1163 */ 1164 @Override 1165 public void tableCaption_() { 1166 writeEndTag(HtmlMarkup.CAPTION); 1167 1168 if (!this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null) { 1169 this.tableCaptionStack.addLast( 1170 this.tableCaptionWriterStack.removeLast().toString()); 1171 this.tableCaptionXMLWriterStack.removeLast(); 1172 } 1173 } 1174 1175 /** 1176 * {@inheritDoc} 1177 * @see javax.swing.text.html.HTML.Tag#A 1178 */ 1179 @Override 1180 public void anchor(String name, SinkEventAttributes attributes) { 1181 Objects.requireNonNull(name, "name cannot be null"); 1182 1183 if (headFlag) { 1184 return; 1185 } 1186 1187 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES); 1188 1189 String id = name; 1190 1191 if (!DoxiaUtils.isValidId(id)) { 1192 id = DoxiaUtils.encodeId(name); 1193 1194 LOGGER.debug("{}Modified invalid anchor name '{}' to '{}'", getLocationLogPrefix(), name, id); 1195 } 1196 1197 MutableAttributeSet att = new SinkEventAttributeSet(); 1198 att.addAttribute(SinkEventAttributes.ID, id); 1199 att.addAttributes(atts); 1200 1201 writeStartTag(HtmlMarkup.A, att); 1202 } 1203 1204 /** 1205 * {@inheritDoc} 1206 * @see javax.swing.text.html.HTML.Tag#A 1207 */ 1208 @Override 1209 public void anchor_() { 1210 if (!headFlag) { 1211 writeEndTag(HtmlMarkup.A); 1212 } 1213 } 1214 1215 /** 1216 * The default style class for external link is <code>externalLink</code>. 1217 * 1218 * {@inheritDoc} 1219 * @see javax.swing.text.html.HTML.Tag#A 1220 **/ 1221 @Override 1222 public void link(String name, SinkEventAttributes attributes) { 1223 Objects.requireNonNull(name, "name cannot be null"); 1224 1225 if (headFlag) { 1226 return; 1227 } 1228 1229 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_LINK_ATTRIBUTES); 1230 1231 if (atts == null) { 1232 atts = new SinkEventAttributeSet(); 1233 } 1234 1235 if (DoxiaUtils.isExternalLink(name)) { 1236 String linkClass = "externalLink"; 1237 if (atts.isDefined(SinkEventAttributes.CLASS.toString())) { 1238 String givenLinkClass = (String) atts.getAttribute(SinkEventAttributes.CLASS.toString()); 1239 linkClass += " " + givenLinkClass; 1240 } 1241 1242 atts.addAttribute(SinkEventAttributes.CLASS, linkClass); 1243 } 1244 1245 atts.addAttribute(SinkEventAttributes.HREF, HtmlTools.escapeHTML(name)); 1246 1247 writeStartTag(HtmlMarkup.A, atts); 1248 } 1249 1250 /** 1251 * {@inheritDoc} 1252 * @see javax.swing.text.html.HTML.Tag#A 1253 */ 1254 @Override 1255 public void link_() { 1256 if (!headFlag) { 1257 writeEndTag(HtmlMarkup.A); 1258 } 1259 } 1260 1261 @Override 1262 public void inline(SinkEventAttributes attributes) { 1263 if (!headFlag) { 1264 if (attributes != null && !attributes.entrySet().isEmpty()) { 1265 SinkEventAttributes compliantAttributes = convertToHtml5CompliantAttributes(attributes); 1266 Tag tag = HtmlMarkup.SPAN; 1267 // iterates in insertion order 1268 for (Map.Entry<String, Object> attribute : compliantAttributes.entrySet()) { 1269 if (SinkEventAttributes.SEMANTICS.equals(attribute.getKey())) { 1270 switch (attribute.getValue().toString()) { 1271 case "emphasis": 1272 tag = HtmlMarkup.EM; 1273 break; 1274 case "strong": 1275 tag = HtmlMarkup.STRONG; 1276 break; 1277 case "small": 1278 tag = HtmlMarkup.SMALL; 1279 break; 1280 case "line-through": 1281 tag = HtmlMarkup.S; 1282 break; 1283 case "citation": 1284 tag = HtmlMarkup.CITE; 1285 break; 1286 case "quote": 1287 tag = HtmlMarkup.Q; 1288 break; 1289 case "definition": 1290 tag = HtmlMarkup.DFN; 1291 break; 1292 case "abbreviation": 1293 tag = HtmlMarkup.ABBR; 1294 break; 1295 case "italic": 1296 tag = HtmlMarkup.I; 1297 break; 1298 case "bold": 1299 tag = HtmlMarkup.B; 1300 break; 1301 case "code": 1302 tag = HtmlMarkup.CODE; 1303 break; 1304 case "variable": 1305 tag = HtmlMarkup.VAR; 1306 break; 1307 case "sample": 1308 tag = HtmlMarkup.SAMP; 1309 break; 1310 case "keyboard": 1311 tag = HtmlMarkup.KBD; 1312 break; 1313 case "superscript": 1314 tag = HtmlMarkup.SUP; 1315 break; 1316 case "subscript": 1317 tag = HtmlMarkup.SUB; 1318 break; 1319 case "annotation": 1320 tag = HtmlMarkup.U; 1321 break; 1322 case "highlight": 1323 tag = HtmlMarkup.MARK; 1324 break; 1325 case "ruby": 1326 tag = HtmlMarkup.RUBY; 1327 break; 1328 case "rubyBase": 1329 tag = HtmlMarkup.RB; 1330 break; 1331 case "rubyText": 1332 tag = HtmlMarkup.RT; 1333 break; 1334 case "rubyTextContainer": 1335 tag = HtmlMarkup.RTC; 1336 break; 1337 case "rubyParentheses": 1338 tag = HtmlMarkup.RP; 1339 break; 1340 case "bidirectionalIsolation": 1341 tag = HtmlMarkup.BDI; 1342 break; 1343 case "bidirectionalOverride": 1344 tag = HtmlMarkup.BDO; 1345 break; 1346 case "phrase": 1347 tag = HtmlMarkup.SPAN; 1348 break; 1349 case "insert": 1350 tag = HtmlMarkup.INS; 1351 break; 1352 case "delete": 1353 tag = HtmlMarkup.DEL; 1354 break; 1355 default: 1356 LOGGER.warn( 1357 "{}Skipping unsupported semantic attribute '{}'", 1358 getLocationLogPrefix(), 1359 attribute.getValue()); 1360 } 1361 compliantAttributes.removeAttribute(SinkEventAttributes.SEMANTICS); 1362 } 1363 } 1364 writeStartTag(tag, compliantAttributes); 1365 inlineStack.push(Collections.singletonList(tag)); 1366 } else { 1367 inlineStack.push(Collections.emptyList()); 1368 } 1369 } 1370 } 1371 1372 /** 1373 * Adds a style to the given attributes. If the attributes already contain a style, the new style value is appended to it. 1374 * 1375 * @param attributes the attributes to which the style should be added 1376 * @param property the CSS property, e.g. "text-decoration-line" 1377 * @param value the CSS value, e.g. "underline" */ 1378 static void addStyle(SinkEventAttributes attributes, String property, String value) { 1379 Object oldStyleValue = attributes.getAttribute(SinkEventAttributes.STYLE); 1380 // styles may be stored as an AttributeSet or a String 1381 if (oldStyleValue instanceof AttributeSet) { 1382 SinkEventAttributeSet newStyleValue = new SinkEventAttributeSet((AttributeSet) oldStyleValue); 1383 newStyleValue.addAttribute(property, value); 1384 attributes.addAttribute(SinkEventAttributes.STYLE, newStyleValue); 1385 } else { 1386 StringBuilder newStyleValue = new StringBuilder(); 1387 if (oldStyleValue != null) { 1388 // if the old style value is not an AttributeSet, we assume it is a String and append the new style to 1389 // it 1390 newStyleValue.append(oldStyleValue.toString()); 1391 // normalize the old style value by ensuring it ends with a semicolon followed by a space, so that the 1392 // new style can be appended to it 1393 if (!newStyleValue.toString().endsWith(Character.toString(Markup.SEMICOLON))) { 1394 newStyleValue.append(Markup.SEMICOLON).append(Markup.SPACE); 1395 } 1396 } 1397 newStyleValue.append(SinkUtils.asCssDeclaration(property, value)); 1398 attributes.addAttribute(SinkEventAttributes.STYLE, newStyleValue.toString()); 1399 } 1400 } 1401 1402 @Override 1403 public void inline_() { 1404 if (!headFlag) { 1405 for (Tag tag : inlineStack.pop()) { 1406 writeEndTag(tag); 1407 } 1408 } 1409 } 1410 1411 /** 1412 * {@inheritDoc} 1413 * @see javax.swing.text.html.HTML.Tag#I 1414 */ 1415 @Override 1416 public void italic() { 1417 inline(SinkEventAttributeSet.Semantics.ITALIC); 1418 } 1419 1420 /** 1421 * {@inheritDoc} 1422 * @see javax.swing.text.html.HTML.Tag#I 1423 */ 1424 @Override 1425 public void italic_() { 1426 inline_(); 1427 } 1428 1429 /** 1430 * {@inheritDoc} 1431 * @see javax.swing.text.html.HTML.Tag#B 1432 */ 1433 @Override 1434 public void bold() { 1435 inline(SinkEventAttributeSet.Semantics.BOLD); 1436 } 1437 1438 /** 1439 * {@inheritDoc} 1440 * @see javax.swing.text.html.HTML.Tag#B 1441 */ 1442 @Override 1443 public void bold_() { 1444 inline_(); 1445 } 1446 1447 /** 1448 * {@inheritDoc} 1449 * @see javax.swing.text.html.HTML.Tag#CODE 1450 */ 1451 @Override 1452 public void monospaced() { 1453 inline(SinkEventAttributeSet.Semantics.CODE); 1454 } 1455 1456 /** 1457 * {@inheritDoc} 1458 * @see javax.swing.text.html.HTML.Tag#CODE 1459 */ 1460 @Override 1461 public void monospaced_() { 1462 inline_(); 1463 } 1464 1465 /** 1466 * {@inheritDoc} 1467 * @see javax.swing.text.html.HTML.Tag#BR 1468 */ 1469 @Override 1470 public void lineBreak(SinkEventAttributes attributes) { 1471 if (headFlag || isVerbatim()) { 1472 getTextBuffer().append(EOL); 1473 } else { 1474 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BR_ATTRIBUTES); 1475 1476 writeSimpleTag(HtmlMarkup.BR, atts); 1477 } 1478 } 1479 1480 @Override 1481 public void lineBreakOpportunity(SinkEventAttributes attributes) { 1482 if (!headFlag && !isVerbatim()) { 1483 MutableAttributeSet atts = convertAndFilterAttributes(attributes, SinkUtils.SINK_BR_ATTRIBUTES); 1484 1485 writeSimpleTag(HtmlMarkup.WBR, atts); 1486 } 1487 } 1488 1489 @Override 1490 public void pageBreak() { 1491 comment(" PB "); 1492 } 1493 1494 @Override 1495 public void nonBreakingSpace() { 1496 if (headFlag) { 1497 getTextBuffer().append(' '); 1498 } else { 1499 write(" "); 1500 } 1501 } 1502 1503 @Override 1504 public void text(String text, SinkEventAttributes attributes) { 1505 if (attributes != null) { 1506 inline(attributes); 1507 } 1508 if (headFlag) { 1509 getTextBuffer().append(text); 1510 } else { 1511 switch (getVerbatimMode()) { 1512 case ON_WITH_CODE: 1513 // trim the first newline for backwards compatibility 1514 // as used to be emitted inside pre directly 1515 // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element 1516 // "In the HTML syntax, a leading newline character immediately following the pre element start tag 1517 // is stripped." 1518 // as now emitted inside <pre><code> the stripping is no longer performed by the browser and needs 1519 // to be done server-side 1520 text = DoxiaStringUtils.stripStart(text, "\r\n"); 1521 verbatimMode = VerbatimMode.ON_WITH_CODE_AFTER_TEXT; 1522 case ON_WITH_CODE_AFTER_TEXT: 1523 case ON: 1524 verbatimContent(text); 1525 break; 1526 default: 1527 content(text); 1528 break; 1529 } 1530 } 1531 if (attributes != null) { 1532 inline_(); 1533 } 1534 } 1535 1536 @Override 1537 public void rawText(String text) { 1538 if (headFlag) { 1539 getTextBuffer().append(text); 1540 } else { 1541 write(text); 1542 } 1543 } 1544 1545 @Override 1546 public void comment(String comment) { 1547 if (comment != null) { 1548 write(encodeAsHtmlComment(comment, getLocationLogPrefix())); 1549 } 1550 } 1551 1552 public static String encodeAsHtmlComment(String comment, String locationLogPrefix) { 1553 final String originalComment = comment; 1554 1555 // http://www.w3.org/TR/2000/REC-xml-20001006#sec-comments 1556 while (comment.contains("--")) { 1557 comment = comment.replace("--", "- -"); 1558 } 1559 1560 if (comment.endsWith("-")) { 1561 comment += " "; 1562 } 1563 1564 if (!originalComment.equals(comment)) { 1565 LOGGER.warn("{}Modified invalid comment '{}' to '{}'", locationLogPrefix, originalComment, comment); 1566 } 1567 1568 final StringBuilder buffer = new StringBuilder(comment.length() + 7); 1569 1570 buffer.append(LESS_THAN).append(BANG).append(MINUS).append(MINUS); 1571 buffer.append(comment); 1572 buffer.append(MINUS).append(MINUS).append(GREATER_THAN); 1573 return buffer.toString(); 1574 } 1575 1576 @Override 1577 public void markupLineBreak(int indentLevel) { 1578 if (headFlag) { 1579 getTextBuffer().append(EOL); 1580 } else { 1581 write(EOL); 1582 } 1583 } 1584 1585 /** 1586 * {@inheritDoc} 1587 * 1588 * Add an unknown event. 1589 * This can be used to generate html tags for which no corresponding sink event exists. 1590 * 1591 * <p> 1592 * If {@link org.apache.maven.doxia.util.HtmlTools#getHtmlTag(String) HtmlTools.getHtmlTag(name)} 1593 * does not return null, the corresponding tag will be written. 1594 * </p> 1595 * 1596 * <p>For example, the div block</p> 1597 * 1598 * <pre> 1599 * <div class="detail" style="display:inline">text</div> 1600 * </pre> 1601 * 1602 * <p>can be generated via the following event sequence:</p> 1603 * 1604 * <pre> 1605 * SinkEventAttributeSet atts = new SinkEventAttributeSet(); 1606 * atts.addAttribute(SinkEventAttributes.CLASS, "detail"); 1607 * atts.addAttribute(SinkEventAttributes.STYLE, "display:inline"); 1608 * sink.unknown("div", new Object[]{new Integer(HtmlMarkup.TAG_TYPE_START)}, atts); 1609 * sink.text("text"); 1610 * sink.unknown("div", new Object[]{new Integer(HtmlMarkup.TAG_TYPE_END)}, null); 1611 * </pre> 1612 * 1613 * @param name the name of the event. If this is not a valid xhtml tag name 1614 * as defined in {@link org.apache.maven.doxia.markup.HtmlMarkup} then the event is ignored. 1615 * @param requiredParams If this is null or the first argument is not an Integer then the event is ignored. 1616 * The first argument should indicate the type of the unknown event, its integer value should be one of 1617 * {@link org.apache.maven.doxia.markup.HtmlMarkup#TAG_TYPE_START TAG_TYPE_START}, 1618 * {@link org.apache.maven.doxia.markup.HtmlMarkup#TAG_TYPE_END TAG_TYPE_END}, 1619 * {@link org.apache.maven.doxia.markup.HtmlMarkup#TAG_TYPE_SIMPLE TAG_TYPE_SIMPLE}, 1620 * {@link org.apache.maven.doxia.markup.HtmlMarkup#ENTITY_TYPE ENTITY_TYPE}, or 1621 * {@link org.apache.maven.doxia.markup.HtmlMarkup#CDATA_TYPE CDATA_TYPE}, 1622 * otherwise the event will be ignored. 1623 * @param attributes a set of attributes for the event. May be null. 1624 * The attributes will always be written, no validity check is performed. 1625 */ 1626 @Override 1627 public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) { 1628 if (requiredParams == null || !(requiredParams[0] instanceof Integer)) { 1629 LOGGER.warn("{}No type information for unknown event '{}', ignoring!", getLocationLogPrefix(), name); 1630 1631 return; 1632 } 1633 1634 int tagType = (Integer) requiredParams[0]; 1635 1636 if (tagType == ENTITY_TYPE) { 1637 rawText(name); 1638 1639 return; 1640 } 1641 1642 if (tagType == CDATA_TYPE) { 1643 rawText(EOL + "//<![CDATA[" + requiredParams[1] + "]]>" + EOL); 1644 1645 return; 1646 } 1647 1648 Tag tag = HtmlTools.getHtmlTag(name); 1649 1650 if (tag == null) { 1651 LOGGER.warn("[]No HTML tag found for unknown event '{}', ignoring!", getLocationLogPrefix(), name); 1652 } else { 1653 if (tagType == TAG_TYPE_SIMPLE) { 1654 writeSimpleTag(tag, escapeAttributeValues(attributes)); 1655 } else if (tagType == TAG_TYPE_START) { 1656 writeStartTag(tag, escapeAttributeValues(attributes)); 1657 } else if (tagType == TAG_TYPE_END) { 1658 writeEndTag(tag); 1659 } else { 1660 LOGGER.warn("{}No type information for unknown event '{}', ignoring!", getLocationLogPrefix(), name); 1661 } 1662 } 1663 } 1664 1665 private SinkEventAttributes escapeAttributeValues(SinkEventAttributes attributes) { 1666 SinkEventAttributeSet set = new SinkEventAttributeSet(attributes.getAttributeCount()); 1667 1668 Enumeration<?> names = attributes.getAttributeNames(); 1669 1670 while (names.hasMoreElements()) { 1671 Object name = names.nextElement(); 1672 1673 set.addAttribute(name, escapeHTML(attributes.getAttribute(name).toString())); 1674 } 1675 1676 return set; 1677 } 1678 1679 @Override 1680 public void flush() { 1681 writer.flush(); 1682 } 1683 1684 @Override 1685 public void close() { 1686 writer.close(); 1687 1688 init(); 1689 } 1690 1691 // ---------------------------------------------------------------------- 1692 // 1693 // ---------------------------------------------------------------------- 1694 1695 /** 1696 * Write HTML escaped text to output. 1697 * 1698 * @param text The text to write. 1699 */ 1700 protected void content(String text) { 1701 // small hack due to DOXIA-314 1702 String txt = escapeHTML(text); 1703 txt = DoxiaStringUtils.replace(txt, "&#", "&#"); 1704 write(txt); 1705 } 1706 1707 /** 1708 * Write HTML escaped text to output. 1709 * 1710 * @param text The text to write. 1711 */ 1712 protected void verbatimContent(String text) { 1713 write(escapeHTML(text)); 1714 } 1715 1716 /** 1717 * Forward to HtmlTools.escapeHTML(text). 1718 * 1719 * @param text the String to escape, may be null 1720 * @return the text escaped, "" if null String input 1721 * @see org.apache.maven.doxia.util.HtmlTools#escapeHTML(String) 1722 */ 1723 protected static String escapeHTML(String text) { 1724 return HtmlTools.escapeHTML(text, false); 1725 } 1726 1727 /** 1728 * Forward to HtmlTools.encodeURL(text). 1729 * 1730 * @param text the String to encode, may be null. 1731 * @return the text encoded, null if null String input. 1732 * @see org.apache.maven.doxia.util.HtmlTools#encodeURL(String) 1733 */ 1734 protected static String encodeURL(String text) { 1735 return HtmlTools.encodeURL(text); 1736 } 1737 1738 /** 1739 * First converts the given attributes to their HTML5 compliant equivalent for the generally supported attribute values, 1740 * then filters the attributes to only include those whose keys are in the given list of valid attribute keys. 1741 * @param attributes 1742 * @param valids 1743 * @return the converted and filtered attributes 1744 * @see #convertToHtml5CompliantAttributes(SinkEventAttributes) 1745 * @see SinkUtils#filterAttributes(SinkEventAttributes, String[]) 1746 */ 1747 protected SinkEventAttributes convertAndFilterAttributes(SinkEventAttributes attributes, String[] valids) { 1748 return SinkUtils.filterAttributes(convertToHtml5CompliantAttributes(attributes), valids); 1749 } 1750 1751 /** 1752 * Some attributes have generally supported values as defined in {@link SinkEventAttributes}. 1753 * This method converts them to their HTML5 compliant equivalent, e.g. the "underline" value of the "decoration" attribute is converted to a style attribute with value "text-decoration-line: underline". 1754 * 1755 * Other attributes with values outsides of the generally supported ones are passed as is (and may not be supported by all HTML output formats). 1756 * @param attributes 1757 * @return a new set of attributes with HTML5 compliant values for the generally supported attribute values 1758 */ 1759 protected SinkEventAttributes convertToHtml5CompliantAttributes(SinkEventAttributes attributes) { 1760 if (attributes == null) { 1761 return null; 1762 } 1763 SinkEventAttributes compliantAttributes = new SinkEventAttributeSet(); 1764 1765 for (Map.Entry<String, Object> attribute : attributes.entrySet()) { 1766 if (attribute.getKey().equals(SinkEventAttributes.DECORATION)) { 1767 switch (attribute.getValue().toString()) { 1768 case "underline": 1769 addStyle(compliantAttributes, "text-decoration-line", "underline"); 1770 break; 1771 case "overline": 1772 addStyle(compliantAttributes, "text-decoration-line", "overline"); 1773 break; 1774 case "line-through": 1775 addStyle(compliantAttributes, "text-decoration-line", "line-through"); 1776 break; 1777 case "source": 1778 // potentially overwrites other semantics 1779 compliantAttributes.addAttributes(SinkEventAttributeSet.Semantics.CODE); 1780 break; 1781 default: 1782 LOGGER.warn( 1783 "{}Skipping unsupported decoration attribute '{}'", 1784 getLocationLogPrefix(), 1785 attribute.getValue()); 1786 } 1787 } else if (attribute.getKey().equals(SinkEventAttributes.STYLE)) { 1788 switch (attribute.getValue().toString()) { 1789 case "bold": 1790 addStyle(compliantAttributes, "font-weight", "bold"); 1791 break; 1792 case "italic": 1793 addStyle(compliantAttributes, "font-style", "italic"); 1794 break; 1795 case "monospaced": 1796 addStyle(compliantAttributes, "font-family", "monospace"); 1797 break; 1798 default: 1799 // everything else is passed as-is, e.g. "color: red" or "text-decoration: underline" 1800 compliantAttributes.addAttribute(SinkEventAttributes.STYLE, attribute.getValue()); 1801 } 1802 } else { 1803 compliantAttributes.addAttribute(attribute.getKey(), attribute.getValue()); 1804 } 1805 } 1806 return compliantAttributes; 1807 } 1808 1809 protected void write(String text) { 1810 if (!this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null) { 1811 this.tableCaptionXMLWriterStack.getLast().writeMarkup(unifyEOLs(text)); 1812 } else if (!this.tableContentWriterStack.isEmpty() && this.tableContentWriterStack.getLast() != null) { 1813 this.tableContentWriterStack.getLast().write(unifyEOLs(text)); 1814 } else { 1815 writer.write(unifyEOLs(text)); 1816 } 1817 } 1818 1819 @Override 1820 protected void writeStartTag(Tag t, MutableAttributeSet att, boolean isSimpleTag) { 1821 if (this.tableCaptionXMLWriterStack.isEmpty()) { 1822 super.writeStartTag(t, att, isSimpleTag); 1823 } else { 1824 String tag = (getNameSpace() != null ? getNameSpace() + ":" : "") + t.toString(); 1825 this.tableCaptionXMLWriterStack.getLast().startElement(tag); 1826 1827 if (att != null) { 1828 Enumeration<?> names = att.getAttributeNames(); 1829 while (names.hasMoreElements()) { 1830 Object key = names.nextElement(); 1831 Object value = att.getAttribute(key); 1832 1833 this.tableCaptionXMLWriterStack.getLast().addAttribute(key.toString(), value.toString()); 1834 } 1835 } 1836 1837 if (isSimpleTag) { 1838 this.tableCaptionXMLWriterStack.getLast().endElement(); 1839 } 1840 } 1841 } 1842 1843 @Override 1844 protected void writeEndTag(Tag t) { 1845 if (this.tableCaptionXMLWriterStack.isEmpty()) { 1846 super.writeEndTag(t); 1847 } else { 1848 this.tableCaptionXMLWriterStack.getLast().endElement(); 1849 } 1850 } 1851}