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}