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