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