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}