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.module.apt; 020 021import javax.swing.text.MutableAttributeSet; 022 023import java.io.PrintWriter; 024import java.io.Writer; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Stack; 030 031import org.apache.commons.lang3.StringUtils; 032import org.apache.maven.doxia.sink.SinkEventAttributes; 033import org.apache.maven.doxia.sink.impl.AbstractTextSink; 034import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 035import org.apache.maven.doxia.sink.impl.SinkUtils; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039/** 040 * APT generator implementation. 041 * <br> 042 * <b>Note</b>: The encoding used is UTF-8. 043 * 044 * @author eredmond 045 * @since 1.0 046 */ 047public class AptSink extends AbstractTextSink implements AptMarkup { 048 private static final Logger LOGGER = LoggerFactory.getLogger(AptSink.class); 049 050 // ---------------------------------------------------------------------- 051 // Instance fields 052 // ---------------------------------------------------------------------- 053 054 /** A buffer that holds the current text when headerFlag or bufferFlag set to <code>true</code>. */ 055 private StringBuffer buffer; 056 057 /** A buffer that holds the table caption. */ 058 private StringBuilder tableCaptionBuffer; 059 060 /** authors. */ 061 private Collection<String> authors; 062 063 /** title. */ 064 private String title; 065 066 /** date. */ 067 private String date; 068 069 /** startFlag. */ 070 private boolean startFlag; 071 072 /** tableCaptionFlag. */ 073 private boolean tableCaptionFlag; 074 075 /** tableCellFlag. */ 076 private boolean tableCellFlag; 077 078 /** headerFlag. */ 079 private boolean headerFlag; 080 081 /** bufferFlag. */ 082 private boolean bufferFlag; 083 084 /** itemFlag. */ 085 private boolean itemFlag; 086 087 /** verbatimFlag. */ 088 private boolean verbatimFlag; 089 090 /** verbatim source. */ 091 private boolean isSource; 092 093 /** gridFlag for tables. */ 094 private boolean gridFlag; 095 096 /** number of cells in a table. */ 097 private int cellCount; 098 099 /** The writer to use. */ 100 private final PrintWriter writer; 101 102 /** justification of table cells. */ 103 private int[] cellJustif; 104 105 /** a line of a row in a table. */ 106 private String rowLine; 107 108 /** listNestingIndent. */ 109 private String listNestingIndent; 110 111 /** listStyles. */ 112 private final Stack<String> listStyles; 113 114 /** Keep track of the closing tags for inline events. */ 115 protected Stack<List<String>> inlineStack = new Stack<>(); 116 117 // ---------------------------------------------------------------------- 118 // Public protected methods 119 // ---------------------------------------------------------------------- 120 121 /** 122 * Constructor, initialize the Writer and the variables. 123 * 124 * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer. 125 * You could use <code>newWriter</code> methods from {@code org.codehaus.plexus.util.WriterFactory}. 126 */ 127 protected AptSink(Writer writer) { 128 this.writer = new PrintWriter(writer); 129 this.listStyles = new Stack<>(); 130 131 init(); 132 } 133 134 /** 135 * Returns the buffer that holds the current text. 136 * 137 * @return A StringBuffer. 138 */ 139 protected StringBuffer getBuffer() { 140 return buffer; 141 } 142 143 /** 144 * Used to determine whether we are in head mode. 145 * 146 * @param headFlag True for head mode. 147 */ 148 protected void setHeadFlag(boolean headFlag) { 149 this.headerFlag = headFlag; 150 } 151 152 /** 153 * {@inheritDoc} 154 */ 155 protected void init() { 156 super.init(); 157 158 resetBuffer(); 159 160 this.tableCaptionBuffer = new StringBuilder(); 161 this.listNestingIndent = ""; 162 163 this.authors = new LinkedList<>(); 164 this.title = null; 165 this.date = null; 166 this.startFlag = true; 167 this.tableCaptionFlag = false; 168 this.tableCellFlag = false; 169 this.headerFlag = false; 170 this.bufferFlag = false; 171 this.itemFlag = false; 172 this.verbatimFlag = false; 173 this.isSource = false; 174 this.gridFlag = false; 175 this.cellCount = 0; 176 this.cellJustif = null; 177 this.rowLine = null; 178 this.listStyles.clear(); 179 this.inlineStack.clear(); 180 } 181 182 /** 183 * Reset the StringBuilder. 184 */ 185 protected void resetBuffer() { 186 buffer = new StringBuffer(); 187 } 188 189 /** 190 * Reset the TableCaptionBuffer. 191 */ 192 protected void resetTableCaptionBuffer() { 193 tableCaptionBuffer = new StringBuilder(); 194 } 195 196 @Override 197 public void head(SinkEventAttributes attributes) { 198 boolean startFlag = this.startFlag; 199 200 init(); 201 202 headerFlag = true; 203 this.startFlag = startFlag; 204 } 205 206 /** 207 * {@inheritDoc} 208 */ 209 public void head_() { 210 headerFlag = false; 211 212 if (!startFlag) { 213 write(EOL); 214 } 215 write(HEADER_START_MARKUP + EOL); 216 if (title != null) { 217 write(" " + title + EOL); 218 } 219 write(HEADER_START_MARKUP + EOL); 220 for (String author : authors) { 221 write(" " + author + EOL); 222 } 223 write(HEADER_START_MARKUP + EOL); 224 if (date != null) { 225 write(" " + date + EOL); 226 } 227 write(HEADER_START_MARKUP + EOL); 228 } 229 230 /** 231 * {@inheritDoc} 232 */ 233 public void title_() { 234 if (buffer.length() > 0) { 235 title = buffer.toString(); 236 resetBuffer(); 237 } 238 } 239 240 /** 241 * {@inheritDoc} 242 */ 243 public void author_() { 244 if (buffer.length() > 0) { 245 authors.add(buffer.toString()); 246 resetBuffer(); 247 } 248 } 249 250 /** 251 * {@inheritDoc} 252 */ 253 public void date_() { 254 if (buffer.length() > 0) { 255 date = buffer.toString(); 256 resetBuffer(); 257 } 258 } 259 260 @Override 261 public void section_(int level) { 262 write(EOL); 263 } 264 265 @Override 266 public void sectionTitle(int level, SinkEventAttributes attributes) { 267 if (level > 5) { 268 LOGGER.warn( 269 "{}Replacing unsupported section title level {} in APT with level 5", 270 getLocationLogPrefix(), 271 level); 272 level = 5; 273 } 274 if (level == 1) { 275 write(EOL); 276 } else if (level > 1) { 277 write(EOL + StringUtils.repeat(SECTION_TITLE_START_MARKUP, level - 1)); 278 } 279 } 280 281 @Override 282 public void sectionTitle_(int level) { 283 if (level >= 1) { 284 write(EOL + EOL); 285 } 286 } 287 288 @Override 289 public void list(SinkEventAttributes attributes) { 290 listNestingIndent += " "; 291 listStyles.push(LIST_START_MARKUP); 292 write(EOL); 293 } 294 295 /** 296 * {@inheritDoc} 297 */ 298 public void list_() { 299 if (listNestingIndent.length() <= 1) { 300 write(EOL + listNestingIndent + LIST_END_MARKUP + EOL); 301 } else { 302 write(EOL); 303 } 304 listNestingIndent = StringUtils.chomp(listNestingIndent, " "); 305 listStyles.pop(); 306 itemFlag = false; 307 } 308 309 @Override 310 public void listItem(SinkEventAttributes attributes) { 311 // if (!numberedList) 312 // write(EOL + listNestingIndent + "*"); 313 // else 314 numberedListItem(); 315 itemFlag = true; 316 } 317 318 /** 319 * {@inheritDoc} 320 */ 321 public void listItem_() { 322 write(EOL); 323 itemFlag = false; 324 } 325 326 @Override 327 public void numberedList(int numbering, SinkEventAttributes attributes) { 328 listNestingIndent += " "; 329 write(EOL); 330 331 String style; 332 switch (numbering) { 333 case NUMBERING_UPPER_ALPHA: 334 style = String.valueOf(NUMBERING_UPPER_ALPHA_CHAR); 335 break; 336 case NUMBERING_LOWER_ALPHA: 337 style = String.valueOf(NUMBERING_LOWER_ALPHA_CHAR); 338 break; 339 case NUMBERING_UPPER_ROMAN: 340 style = String.valueOf(NUMBERING_UPPER_ROMAN_CHAR); 341 break; 342 case NUMBERING_LOWER_ROMAN: 343 style = String.valueOf(NUMBERING_LOWER_ROMAN_CHAR); 344 break; 345 case NUMBERING_DECIMAL: 346 default: 347 style = String.valueOf(NUMBERING); 348 } 349 350 listStyles.push(style); 351 } 352 353 /** 354 * {@inheritDoc} 355 */ 356 public void numberedList_() { 357 if (listNestingIndent.length() <= 1) { 358 write(EOL + listNestingIndent + LIST_END_MARKUP + EOL); 359 } else { 360 write(EOL); 361 } 362 listNestingIndent = StringUtils.chomp(listNestingIndent, " "); 363 listStyles.pop(); 364 itemFlag = false; 365 } 366 367 @Override 368 public void numberedListItem(SinkEventAttributes attributes) { 369 String style = listStyles.peek(); 370 if (style.equals(String.valueOf(STAR))) { 371 write(EOL + listNestingIndent + STAR + SPACE); 372 } else { 373 write(EOL 374 + listNestingIndent 375 + LEFT_SQUARE_BRACKET 376 + LEFT_SQUARE_BRACKET 377 + style 378 + RIGHT_SQUARE_BRACKET 379 + RIGHT_SQUARE_BRACKET 380 + SPACE); 381 } 382 itemFlag = true; 383 } 384 385 /** 386 * {@inheritDoc} 387 */ 388 public void numberedListItem_() { 389 write(EOL); 390 itemFlag = false; 391 } 392 393 @Override 394 public void definitionList(SinkEventAttributes attributes) { 395 listNestingIndent += " "; 396 listStyles.push(""); 397 write(EOL); 398 } 399 400 /** 401 * {@inheritDoc} 402 */ 403 public void definitionList_() { 404 if (listNestingIndent.length() <= 1) { 405 write(EOL + listNestingIndent + LIST_END_MARKUP + EOL); 406 } else { 407 write(EOL); 408 } 409 listNestingIndent = StringUtils.chomp(listNestingIndent, " "); 410 listStyles.pop(); 411 itemFlag = false; 412 } 413 414 @Override 415 public void definedTerm(SinkEventAttributes attributes) { 416 write(EOL + " ["); 417 } 418 419 /** 420 * {@inheritDoc} 421 */ 422 public void definedTerm_() { 423 write("] "); 424 } 425 426 @Override 427 public void definition(SinkEventAttributes attributes) { 428 itemFlag = true; 429 } 430 431 /** 432 * {@inheritDoc} 433 */ 434 public void definition_() { 435 write(EOL); 436 itemFlag = false; 437 } 438 439 /** 440 * {@inheritDoc} 441 */ 442 public void pageBreak() { 443 write(EOL + PAGE_BREAK + EOL); 444 } 445 446 @Override 447 public void paragraph(SinkEventAttributes attributes) { 448 if (tableCellFlag) { 449 // ignore paragraphs in table cells 450 } else if (itemFlag) { 451 write(EOL + EOL + " " + listNestingIndent); 452 } else { 453 write(EOL + " "); 454 } 455 } 456 457 /** 458 * {@inheritDoc} 459 */ 460 public void paragraph_() { 461 if (tableCellFlag) { 462 // ignore paragraphs in table cells 463 } else { 464 write(EOL + EOL); 465 } 466 } 467 468 @Override 469 public void verbatim(SinkEventAttributes attributes) { 470 MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_VERBATIM_ATTRIBUTES); 471 472 boolean source = false; 473 474 if (atts != null && atts.isDefined(SinkEventAttributes.DECORATION)) { 475 source = "source" 476 .equals(atts.getAttribute(SinkEventAttributes.DECORATION).toString()); 477 } 478 479 verbatimFlag = true; 480 this.isSource = source; 481 write(EOL); 482 if (source) { 483 write(EOL + VERBATIM_SOURCE_START_MARKUP + EOL); 484 } else { 485 write(EOL + VERBATIM_START_MARKUP + EOL); 486 } 487 } 488 489 /** 490 * {@inheritDoc} 491 */ 492 public void verbatim_() { 493 if (isSource) { 494 write(EOL + VERBATIM_SOURCE_END_MARKUP + EOL); 495 } else { 496 write(EOL + VERBATIM_END_MARKUP + EOL); 497 } 498 isSource = false; 499 verbatimFlag = false; 500 } 501 502 @Override 503 public void horizontalRule(SinkEventAttributes attributes) { 504 write(EOL + HORIZONTAL_RULE_MARKUP + EOL); 505 } 506 507 @Override 508 public void table(SinkEventAttributes attributes) { 509 write(EOL); 510 } 511 512 /** 513 * {@inheritDoc} 514 */ 515 public void table_() { 516 if (rowLine != null) { 517 write(rowLine); 518 } 519 rowLine = null; 520 521 if (tableCaptionBuffer.length() > 0) { 522 text(tableCaptionBuffer.toString() + EOL); 523 } 524 525 resetTableCaptionBuffer(); 526 } 527 528 @Override 529 public void tableRows(int[] justification, boolean grid) { 530 cellJustif = justification; 531 gridFlag = grid; 532 } 533 534 /** 535 * {@inheritDoc} 536 */ 537 public void tableRows_() { 538 cellJustif = null; 539 gridFlag = false; 540 } 541 542 @Override 543 public void tableRow(SinkEventAttributes attributes) { 544 bufferFlag = true; 545 cellCount = 0; 546 } 547 548 /** 549 * {@inheritDoc} 550 */ 551 public void tableRow_() { 552 bufferFlag = false; 553 554 // write out the header row first, then the data in the buffer 555 buildRowLine(); 556 557 write(rowLine); 558 559 // TODO: This will need to be more clever, for multi-line cells 560 if (gridFlag) { 561 write(TABLE_ROW_SEPARATOR_MARKUP); 562 } 563 564 write(buffer.toString()); 565 566 resetBuffer(); 567 568 write(EOL); 569 570 // only reset cell count if this is the last row 571 cellCount = 0; 572 } 573 574 /** Construct a table row. */ 575 private void buildRowLine() { 576 StringBuilder rLine = new StringBuilder(); 577 rLine.append(TABLE_ROW_START_MARKUP); 578 579 for (int i = 0; i < cellCount; i++) { 580 if (cellJustif != null) { 581 switch (cellJustif[i]) { 582 case 1: 583 rLine.append(TABLE_COL_LEFT_ALIGNED_MARKUP); 584 break; 585 case 2: 586 rLine.append(TABLE_COL_RIGHT_ALIGNED_MARKUP); 587 break; 588 default: 589 rLine.append(TABLE_COL_CENTERED_ALIGNED_MARKUP); 590 } 591 } else { 592 rLine.append(TABLE_COL_CENTERED_ALIGNED_MARKUP); 593 } 594 } 595 rLine.append(EOL); 596 597 this.rowLine = rLine.toString(); 598 } 599 600 @Override 601 public void tableCell(SinkEventAttributes attributes) { 602 tableCell(false); 603 } 604 605 @Override 606 public void tableHeaderCell(SinkEventAttributes attributes) { 607 tableCell(true); 608 } 609 610 /** 611 * Starts a table cell. 612 * 613 * @param headerRow If this cell is part of a header row. 614 */ 615 public void tableCell(boolean headerRow) { 616 if (headerRow) { 617 buffer.append(TABLE_CELL_SEPARATOR_MARKUP); 618 } 619 tableCellFlag = true; 620 } 621 622 /** 623 * {@inheritDoc} 624 */ 625 public void tableCell_() { 626 endTableCell(); 627 } 628 629 /** 630 * {@inheritDoc} 631 */ 632 public void tableHeaderCell_() { 633 endTableCell(); 634 } 635 636 /** 637 * Ends a table cell. 638 */ 639 private void endTableCell() { 640 tableCellFlag = false; 641 buffer.append(TABLE_CELL_SEPARATOR_MARKUP); 642 cellCount++; 643 } 644 645 @Override 646 public void tableCaption(SinkEventAttributes attributes) { 647 tableCaptionFlag = true; 648 } 649 650 /** 651 * {@inheritDoc} 652 */ 653 public void tableCaption_() { 654 tableCaptionFlag = false; 655 } 656 657 /** 658 * {@inheritDoc} 659 */ 660 public void figureCaption_() { 661 write(EOL); 662 } 663 664 @Override 665 public void figureGraphics(String name, SinkEventAttributes attributes) { 666 write(EOL + "[" + name + "] "); 667 } 668 669 @Override 670 public void anchor(String name, SinkEventAttributes attributes) { 671 write(ANCHOR_START_MARKUP); 672 } 673 674 /** 675 * {@inheritDoc} 676 */ 677 public void anchor_() { 678 write(ANCHOR_END_MARKUP); 679 } 680 681 @Override 682 public void link(String name, SinkEventAttributes attributes) { 683 if (!headerFlag) { 684 write(LINK_START_1_MARKUP); 685 text(name.startsWith("#") ? name.substring(1) : name); 686 write(LINK_START_2_MARKUP); 687 } 688 } 689 690 /** 691 * {@inheritDoc} 692 */ 693 public void link_() { 694 if (!headerFlag) { 695 write(LINK_END_MARKUP); 696 } 697 } 698 699 /** 700 * A link with a target. 701 * 702 * @param name The name of the link. 703 * @param target The link target. 704 */ 705 public void link(String name, String target) { 706 if (!headerFlag) { 707 write(LINK_START_1_MARKUP); 708 text(target); 709 write(LINK_START_2_MARKUP); 710 text(name); 711 } 712 } 713 714 /** {@inheritDoc} */ 715 public void inline(SinkEventAttributes attributes) { 716 if (!headerFlag) { 717 List<String> tags = new ArrayList<>(); 718 719 if (attributes != null) { 720 721 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "italic")) { 722 write(ITALIC_START_MARKUP); 723 tags.add(0, ITALIC_END_MARKUP); 724 } 725 726 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "bold")) { 727 write(BOLD_START_MARKUP); 728 tags.add(0, BOLD_END_MARKUP); 729 } 730 731 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "code")) { 732 write(MONOSPACED_START_MARKUP); 733 tags.add(0, MONOSPACED_END_MARKUP); 734 } 735 } 736 737 inlineStack.push(tags); 738 } 739 } 740 741 /** 742 * {@inheritDoc} 743 */ 744 public void inline_() { 745 if (!headerFlag) { 746 for (String tag : inlineStack.pop()) { 747 write(tag); 748 } 749 } 750 } 751 752 /** 753 * {@inheritDoc} 754 */ 755 public void italic() { 756 inline(SinkEventAttributeSet.Semantics.ITALIC); 757 } 758 759 /** 760 * {@inheritDoc} 761 */ 762 public void italic_() { 763 inline_(); 764 } 765 766 /** 767 * {@inheritDoc} 768 */ 769 public void bold() { 770 inline(SinkEventAttributeSet.Semantics.BOLD); 771 } 772 773 /** 774 * {@inheritDoc} 775 */ 776 public void bold_() { 777 inline_(); 778 } 779 780 /** 781 * {@inheritDoc} 782 */ 783 public void monospaced() { 784 inline(SinkEventAttributeSet.Semantics.CODE); 785 } 786 787 /** 788 * {@inheritDoc} 789 */ 790 public void monospaced_() { 791 inline_(); 792 } 793 794 @Override 795 public void lineBreak(SinkEventAttributes attributes) { 796 if (headerFlag || bufferFlag) { 797 buffer.append(EOL); 798 } else if (verbatimFlag) { 799 write(EOL); 800 } else { 801 write("\\" + EOL); 802 } 803 } 804 805 /** 806 * {@inheritDoc} 807 */ 808 public void nonBreakingSpace() { 809 if (headerFlag || bufferFlag) { 810 buffer.append(NON_BREAKING_SPACE_MARKUP); 811 } else { 812 write(NON_BREAKING_SPACE_MARKUP); 813 } 814 } 815 816 @Override 817 public void text(String text, SinkEventAttributes attributes) { 818 if (attributes != null) { 819 inline(attributes); 820 } 821 if (tableCaptionFlag) { 822 tableCaptionBuffer.append(text); 823 } else if (headerFlag || bufferFlag) { 824 buffer.append(text); 825 } else if (verbatimFlag) { 826 verbatimContent(text); 827 } else { 828 content(text); 829 } 830 if (attributes != null) { 831 inline_(); 832 } 833 } 834 835 /** {@inheritDoc} */ 836 public void rawText(String text) { 837 write(text); 838 } 839 840 /** {@inheritDoc} */ 841 public void comment(String comment) { 842 rawText((startFlag ? "" : EOL) + COMMENT + COMMENT + comment); 843 } 844 845 /** 846 * {@inheritDoc} 847 * 848 * Unkown events just log a warning message but are ignored otherwise. 849 * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes) 850 */ 851 public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) { 852 LOGGER.warn("{}Unknown Sink event '{}', ignoring!", getLocationLogPrefix(), name); 853 } 854 855 /** 856 * Write text to output. 857 * 858 * @param text The text to write. 859 */ 860 protected void write(String text) { 861 startFlag = false; 862 if (tableCellFlag) { 863 buffer.append(text); 864 } else { 865 writer.write(unifyEOLs(text)); 866 } 867 } 868 869 /** 870 * Write Apt escaped text to output. 871 * 872 * @param text The text to write. 873 */ 874 protected void content(String text) { 875 write(escapeAPT(text)); 876 } 877 878 /** 879 * Write Apt escaped text to output. 880 * 881 * @param text The text to write. 882 */ 883 protected void verbatimContent(String text) { 884 write(escapeAPT(text)); 885 } 886 887 /** 888 * {@inheritDoc} 889 */ 890 public void flush() { 891 writer.flush(); 892 } 893 894 /** 895 * {@inheritDoc} 896 */ 897 public void close() { 898 writer.close(); 899 900 init(); 901 } 902 903 // ---------------------------------------------------------------------- 904 // Private methods 905 // ---------------------------------------------------------------------- 906 907 /** 908 * Escape special characters in a text in APT: 909 * 910 * <pre> 911 * \~, \=, \-, \+, \*, \[, \], \<, \>, \{, \}, \\ 912 * </pre> 913 * 914 * @param text the String to escape, may be null 915 * @return the text escaped, "" if null String input 916 */ 917 private static String escapeAPT(String text) { 918 if (text == null) { 919 return ""; 920 } 921 922 int length = text.length(); 923 StringBuilder buffer = new StringBuilder(length); 924 925 for (int i = 0; i < length; ++i) { 926 char c = text.charAt(i); 927 switch (c) { // 0080 928 case '\\': 929 case '~': 930 case '=': 931 case '-': 932 case '+': 933 case '*': 934 case '[': 935 case ']': 936 case '<': 937 case '>': 938 case '{': 939 case '}': 940 buffer.append('\\'); 941 buffer.append(c); 942 break; 943 default: 944 buffer.append(c); 945 } 946 } 947 948 return buffer.toString(); 949 } 950}