JavadocFormatter.java revision 3738:6ef8a1453577
1/*
2 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25package jdk.internal.shellsupport.doc;
26
27import java.io.IOException;
28import java.net.URI;
29import java.net.URISyntaxException;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Collections;
33import java.util.IdentityHashMap;
34import java.util.LinkedHashMap;
35import java.util.List;
36import java.util.Map;
37import java.util.ResourceBundle;
38import java.util.Stack;
39
40import javax.lang.model.element.Name;
41import javax.tools.JavaFileObject.Kind;
42import javax.tools.SimpleJavaFileObject;
43import javax.tools.ToolProvider;
44
45import com.sun.source.doctree.AttributeTree;
46import com.sun.source.doctree.DocCommentTree;
47import com.sun.source.doctree.DocTree;
48import com.sun.source.doctree.EndElementTree;
49import com.sun.source.doctree.EntityTree;
50import com.sun.source.doctree.InlineTagTree;
51import com.sun.source.doctree.LinkTree;
52import com.sun.source.doctree.LiteralTree;
53import com.sun.source.doctree.ParamTree;
54import com.sun.source.doctree.ReturnTree;
55import com.sun.source.doctree.StartElementTree;
56import com.sun.source.doctree.TextTree;
57import com.sun.source.doctree.ThrowsTree;
58import com.sun.source.util.DocTreeScanner;
59import com.sun.source.util.DocTrees;
60import com.sun.source.util.JavacTask;
61import com.sun.tools.doclint.Entity;
62import com.sun.tools.doclint.HtmlTag;
63import com.sun.tools.javac.util.DefinedBy;
64import com.sun.tools.javac.util.DefinedBy.Api;
65import com.sun.tools.javac.util.StringUtils;
66
67/**A javadoc to plain text formatter.
68 *
69 */
70public class JavadocFormatter {
71
72    private static final String CODE_RESET = "\033[0m";
73    private static final String CODE_HIGHLIGHT = "\033[1m";
74    private static final String CODE_UNDERLINE = "\033[4m";
75
76    private final int lineLimit;
77    private final boolean escapeSequencesSupported;
78
79    /** Construct the formatter.
80     *
81     * @param lineLimit maximum line length
82     * @param escapeSequencesSupported whether escape sequences are supported
83     */
84    public JavadocFormatter(int lineLimit, boolean escapeSequencesSupported) {
85        this.lineLimit = lineLimit;
86        this.escapeSequencesSupported = escapeSequencesSupported;
87    }
88
89    private static final int MAX_LINE_LENGTH = 95;
90    private static final int SHORTEST_LINE = 30;
91    private static final int INDENT = 4;
92
93    /**Format javadoc to plain text.
94     *
95     * @param header element caption that should be used
96     * @param javadoc to format
97     * @return javadoc formatted to plain text
98     */
99    public String formatJavadoc(String header, String javadoc) {
100        try {
101            StringBuilder result = new StringBuilder();
102
103            result.append(escape(CODE_HIGHLIGHT)).append(header).append(escape(CODE_RESET)).append("\n");
104
105            if (javadoc == null) {
106                return result.toString();
107            }
108
109            JavacTask task = (JavacTask) ToolProvider.getSystemJavaCompiler().getTask(null, null, null, null, null, null);
110            DocTrees trees = DocTrees.instance(task);
111            DocCommentTree docComment = trees.getDocCommentTree(new SimpleJavaFileObject(new URI("mem://doc.html"), Kind.HTML) {
112                @Override @DefinedBy(Api.COMPILER)
113                public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
114                    return "<body>" + javadoc + "</body>";
115                }
116            });
117
118            new FormatJavadocScanner(result, task).scan(docComment, null);
119
120            addNewLineIfNeeded(result);
121
122            return result.toString();
123        } catch (URISyntaxException ex) {
124            throw new InternalError("Unexpected exception", ex);
125        }
126    }
127
128    private class FormatJavadocScanner extends DocTreeScanner<Object, Object> {
129        private final StringBuilder result;
130        private final JavacTask task;
131        private int reflownTo;
132        private int indent;
133        private int limit = Math.min(lineLimit, MAX_LINE_LENGTH);
134        private boolean pre;
135        private Map<StartElementTree, Integer> tableColumns;
136
137        public FormatJavadocScanner(StringBuilder result, JavacTask task) {
138            this.result = result;
139            this.task = task;
140        }
141
142        @Override @DefinedBy(Api.COMPILER_TREE)
143        public Object visitDocComment(DocCommentTree node, Object p) {
144            tableColumns = countTableColumns(node);
145            reflownTo = result.length();
146            scan(node.getFirstSentence(), p);
147            scan(node.getBody(), p);
148            reflow(result, reflownTo, indent, limit);
149            for (Sections current : docSections.keySet()) {
150                boolean seenAny = false;
151                for (DocTree t : node.getBlockTags()) {
152                    if (current.matches(t)) {
153                        if (!seenAny) {
154                            seenAny = true;
155                            if (result.charAt(result.length() - 1) != '\n')
156                                result.append("\n");
157                            result.append("\n");
158                            result.append(escape(CODE_UNDERLINE))
159                                  .append(docSections.get(current))
160                                  .append(escape(CODE_RESET))
161                                  .append("\n");
162                        }
163
164                        scan(t, null);
165                    }
166                }
167            }
168            return null;
169        }
170
171        @Override @DefinedBy(Api.COMPILER_TREE)
172        public Object visitText(TextTree node, Object p) {
173            String text = node.getBody();
174            if (!pre) {
175                text = text.replaceAll("[ \t\r\n]+", " ").trim();
176                if (text.isEmpty()) {
177                    text = " ";
178                }
179            } else {
180                text = text.replaceAll("\n", "\n" + indentString(indent));
181            }
182            result.append(text);
183            return null;
184        }
185
186        @Override @DefinedBy(Api.COMPILER_TREE)
187        public Object visitLink(LinkTree node, Object p) {
188            if (!node.getLabel().isEmpty()) {
189                scan(node.getLabel(), p);
190            } else {
191                result.append(node.getReference().getSignature());
192            }
193            return null;
194        }
195
196        @Override @DefinedBy(Api.COMPILER_TREE)
197        public Object visitParam(ParamTree node, Object p) {
198            return formatDef(node.getName().getName(), node.getDescription());
199        }
200
201        @Override @DefinedBy(Api.COMPILER_TREE)
202        public Object visitThrows(ThrowsTree node, Object p) {
203            return formatDef(node.getExceptionName().getSignature(), node.getDescription());
204        }
205
206        public Object formatDef(CharSequence name, List<? extends DocTree> description) {
207            result.append(name);
208            result.append(" - ");
209            reflownTo = result.length();
210            indent = name.length() + 3;
211
212            if (limit - indent < SHORTEST_LINE) {
213                result.append("\n");
214                result.append(indentString(INDENT));
215                indent = INDENT;
216                reflownTo += INDENT;
217            }
218            try {
219                return scan(description, null);
220            } finally {
221                reflow(result, reflownTo, indent, limit);
222                result.append("\n");
223            }
224        }
225
226        @Override @DefinedBy(Api.COMPILER_TREE)
227        public Object visitLiteral(LiteralTree node, Object p) {
228            return scan(node.getBody(), p);
229        }
230
231        @Override @DefinedBy(Api.COMPILER_TREE)
232        public Object visitReturn(ReturnTree node, Object p) {
233            reflownTo = result.length();
234            try {
235                return super.visitReturn(node, p);
236            } finally {
237                reflow(result, reflownTo, 0, limit);
238            }
239        }
240
241        Stack<Integer> listStack = new Stack<>();
242        Stack<Integer> defStack = new Stack<>();
243        Stack<Integer> tableStack = new Stack<>();
244        Stack<List<Integer>> cellsStack = new Stack<>();
245        Stack<List<Boolean>> headerStack = new Stack<>();
246
247        @Override @DefinedBy(Api.COMPILER_TREE)
248        public Object visitStartElement(StartElementTree node, Object p) {
249            switch (HtmlTag.get(node.getName())) {
250                case P:
251                    if (lastNode!= null && lastNode.getKind() == DocTree.Kind.START_ELEMENT &&
252                        HtmlTag.get(((StartElementTree) lastNode).getName()) == HtmlTag.LI) {
253                        //ignore
254                        break;
255                    }
256                    reflowTillNow();
257                    addNewLineIfNeeded(result);
258                    result.append(indentString(indent));
259                    reflownTo = result.length();
260                    break;
261                case BLOCKQUOTE:
262                    reflowTillNow();
263                    indent += INDENT;
264                    break;
265                case PRE:
266                    reflowTillNow();
267                    pre = true;
268                    break;
269                case UL:
270                    reflowTillNow();
271                    listStack.push(-1);
272                    indent += INDENT;
273                    break;
274                case OL:
275                    reflowTillNow();
276                    listStack.push(1);
277                    indent += INDENT;
278                    break;
279                case DL:
280                    reflowTillNow();
281                    defStack.push(indent);
282                    break;
283                case LI:
284                    reflowTillNow();
285                    if (!listStack.empty()) {
286                        addNewLineIfNeeded(result);
287
288                        int top = listStack.pop();
289
290                        if (top == (-1)) {
291                            result.append(indentString(indent - 2));
292                            result.append("* ");
293                        } else {
294                            result.append(indentString(indent - 3));
295                            result.append("" + top++ + ". ");
296                        }
297
298                        listStack.push(top);
299
300                        reflownTo = result.length();
301                    }
302                    break;
303                case DT:
304                    reflowTillNow();
305                    if (!defStack.isEmpty()) {
306                        addNewLineIfNeeded(result);
307                        indent = defStack.peek();
308                        result.append(escape(CODE_HIGHLIGHT));
309                    }
310                    break;
311                case DD:
312                    reflowTillNow();
313                    if (!defStack.isEmpty()) {
314                        if (indent == defStack.peek()) {
315                            result.append(escape(CODE_RESET));
316                        }
317                        addNewLineIfNeeded(result);
318                        indent = defStack.peek() + INDENT;
319                        result.append(indentString(indent));
320                    }
321                    break;
322                case H1: case H2: case H3:
323                case H4: case H5: case H6:
324                    reflowTillNow();
325                    addNewLineIfNeeded(result);
326                    result.append("\n")
327                          .append(escape(CODE_UNDERLINE));
328                    reflownTo = result.length();
329                    break;
330                case TABLE:
331                    int columns = tableColumns.get(node);
332
333                    if (columns == 0) {
334                        break; //broken input
335                    }
336
337                    reflowTillNow();
338                    addNewLineIfNeeded(result);
339                    reflownTo = result.length();
340
341                    tableStack.push(limit);
342
343                    limit = (limit - 1) / columns - 3;
344
345                    for (int sep = 0; sep < (limit + 3) * columns + 1; sep++) {
346                        result.append("-");
347                    }
348
349                    result.append("\n");
350
351                    break;
352                case TR:
353                    if (cellsStack.size() >= tableStack.size()) {
354                        //unclosed <tr>:
355                        handleEndElement(node.getName());
356                    }
357                    cellsStack.push(new ArrayList<>());
358                    headerStack.push(new ArrayList<>());
359                    break;
360                case TH:
361                case TD:
362                    if (cellsStack.isEmpty()) {
363                        //broken code
364                        break;
365                    }
366                    reflowTillNow();
367                    result.append("\n");
368                    reflownTo = result.length();
369                    cellsStack.peek().add(result.length());
370                    headerStack.peek().add(HtmlTag.get(node.getName()) == HtmlTag.TH);
371                    break;
372                case IMG:
373                    for (DocTree attr : node.getAttributes()) {
374                        if (attr.getKind() != DocTree.Kind.ATTRIBUTE) {
375                            continue;
376                        }
377                        AttributeTree at = (AttributeTree) attr;
378                        if ("alt".equals(StringUtils.toLowerCase(at.getName().toString()))) {
379                            addSpaceIfNeeded(result);
380                            scan(at.getValue(), null);
381                            addSpaceIfNeeded(result);
382                            break;
383                        }
384                    }
385                    break;
386                default:
387                    addSpaceIfNeeded(result);
388                    break;
389            }
390            return null;
391        }
392
393        @Override @DefinedBy(Api.COMPILER_TREE)
394        public Object visitEndElement(EndElementTree node, Object p) {
395            handleEndElement(node.getName());
396            return super.visitEndElement(node, p);
397        }
398
399        private void handleEndElement(Name name) {
400            switch (HtmlTag.get(name)) {
401                case BLOCKQUOTE:
402                    indent -= INDENT;
403                    break;
404                case PRE:
405                    pre = false;
406                    addNewLineIfNeeded(result);
407                    reflownTo = result.length();
408                    break;
409                case UL: case OL:
410                    if (listStack.isEmpty()) { //ignore stray closing tag
411                        break;
412                    }
413                    reflowTillNow();
414                    listStack.pop();
415                    indent -= INDENT;
416                    addNewLineIfNeeded(result);
417                    break;
418                case DL:
419                    if (defStack.isEmpty()) {//ignore stray closing tag
420                        break;
421                    }
422                    reflowTillNow();
423                    if (indent == defStack.peek()) {
424                        result.append(escape(CODE_RESET));
425                    }
426                    indent = defStack.pop();
427                    addNewLineIfNeeded(result);
428                    break;
429                case H1: case H2: case H3:
430                case H4: case H5: case H6:
431                    reflowTillNow();
432                    result.append(escape(CODE_RESET))
433                          .append("\n");
434                    reflownTo = result.length();
435                    break;
436                case TABLE:
437                    if (cellsStack.size() >= tableStack.size()) {
438                        //unclosed <tr>:
439                        handleEndElement(task.getElements().getName("tr"));
440                    }
441
442                    if (tableStack.isEmpty()) {
443                        break;
444                    }
445
446                    limit = tableStack.pop();
447                    break;
448                case TR:
449                    if (cellsStack.isEmpty()) {
450                        break;
451                    }
452
453                    reflowTillNow();
454
455                    List<Integer> cells = cellsStack.pop();
456                    List<Boolean> headerFlags = headerStack.pop();
457                    List<String[]> content = new ArrayList<>();
458                    int maxLines = 0;
459
460                    result.append("\n");
461
462                    while (!cells.isEmpty()) {
463                        int currentCell = cells.remove(cells.size() - 1);
464                        String[] lines = result.substring(currentCell, result.length()).split("\n");
465
466                        result.delete(currentCell - 1, result.length());
467
468                        content.add(lines);
469                        maxLines = Math.max(maxLines, lines.length);
470                    }
471
472                    Collections.reverse(content);
473
474                    for (int line = 0; line < maxLines; line++) {
475                        for (int column = 0; column < content.size(); column++) {
476                            String[] lines = content.get(column);
477                            String currentLine = line < lines.length ? lines[line] : "";
478                            result.append("| ");
479                            boolean header = headerFlags.get(column);
480                            if (header) {
481                                result.append(escape(CODE_HIGHLIGHT));
482                            }
483                            result.append(currentLine);
484                            if (header) {
485                                result.append(escape(CODE_RESET));
486                            }
487                            int padding = limit - currentLine.length();
488                            if (padding > 0)
489                                result.append(indentString(padding));
490                            result.append(" ");
491                        }
492                        result.append("|\n");
493                    }
494
495                    for (int sep = 0; sep < (limit + 3) * content.size() + 1; sep++) {
496                        result.append("-");
497                    }
498
499                    result.append("\n");
500
501                    reflownTo = result.length();
502                    break;
503                case TD:
504                case TH:
505                    break;
506                default:
507                    addSpaceIfNeeded(result);
508                    break;
509            }
510        }
511
512        @Override @DefinedBy(Api.COMPILER_TREE)
513        public Object visitEntity(EntityTree node, Object p) {
514            String name = node.getName().toString();
515            int code = -1;
516            if (name.startsWith("#")) {
517                try {
518                    int v = StringUtils.toLowerCase(name).startsWith("#x")
519                            ? Integer.parseInt(name.substring(2), 16)
520                            : Integer.parseInt(name.substring(1), 10);
521                    if (Entity.isValid(v)) {
522                        code = v;
523                    }
524                } catch (NumberFormatException ex) {
525                    //ignore
526                }
527            } else {
528                Entity entity = Entity.get(name);
529                if (entity != null) {
530                    code = entity.code;
531                }
532            }
533            if (code != (-1)) {
534                result.appendCodePoint(code);
535            } else {
536                result.append(node.toString());
537            }
538            return super.visitEntity(node, p);
539        }
540
541        private DocTree lastNode;
542
543        @Override @DefinedBy(Api.COMPILER_TREE)
544        public Object scan(DocTree node, Object p) {
545            if (node instanceof InlineTagTree) {
546                addSpaceIfNeeded(result);
547            }
548            try {
549                return super.scan(node, p);
550            } finally {
551                if (node instanceof InlineTagTree) {
552                    addSpaceIfNeeded(result);
553                }
554                lastNode = node;
555            }
556        }
557
558        private void reflowTillNow() {
559            while (result.length() > 0 && result.charAt(result.length() - 1) == ' ')
560                result.delete(result.length() - 1, result.length());
561            reflow(result, reflownTo, indent, limit);
562            reflownTo = result.length();
563        }
564    };
565
566    private String escape(String sequence) {
567        return this.escapeSequencesSupported ? sequence : "";
568    }
569
570    private static final Map<Sections, String> docSections = new LinkedHashMap<>();
571
572    static {
573        ResourceBundle bundle =
574                ResourceBundle.getBundle("jdk.internal.shellsupport.doc.resources.javadocformatter");
575        docSections.put(Sections.TYPE_PARAMS, bundle.getString("CAP_TypeParameters"));
576        docSections.put(Sections.PARAMS, bundle.getString("CAP_Parameters"));
577        docSections.put(Sections.RETURNS, bundle.getString("CAP_Returns"));
578        docSections.put(Sections.THROWS, bundle.getString("CAP_Thrown_Exceptions"));
579    }
580
581    private static String indentString(int indent) {
582        char[] content = new char[indent];
583        Arrays.fill(content, ' ');
584        return new String(content);
585    }
586
587    private static void reflow(StringBuilder text, int from, int indent, int limit) {
588        int lineStart = from;
589
590        while (lineStart > 0 && text.charAt(lineStart - 1) != '\n') {
591            lineStart--;
592        }
593
594        int lineChars = from - lineStart;
595        int pointer = from;
596        int lastSpace = -1;
597
598        while (pointer < text.length()) {
599            if (text.charAt(pointer) == ' ')
600                lastSpace = pointer;
601            if (lineChars >= limit) {
602                if (lastSpace != (-1)) {
603                    text.setCharAt(lastSpace, '\n');
604                    text.insert(lastSpace + 1, indentString(indent));
605                    lineChars = indent + pointer - lastSpace - 1;
606                    pointer += indent;
607                    lastSpace = -1;
608                }
609            }
610            lineChars++;
611            pointer++;
612        }
613    }
614
615    private static void addNewLineIfNeeded(StringBuilder text) {
616        if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
617            text.append("\n");
618        }
619    }
620
621    private static void addSpaceIfNeeded(StringBuilder text) {
622        if (text.length() == 0)
623            return ;
624
625        char last = text.charAt(text.length() - 1);
626
627        if (last != ' ' && last != '\n') {
628            text.append(" ");
629        }
630    }
631
632    private static Map<StartElementTree, Integer> countTableColumns(DocCommentTree dct) {
633        Map<StartElementTree, Integer> result = new IdentityHashMap<>();
634
635        new DocTreeScanner<Void, Void>() {
636            private StartElementTree currentTable;
637            private int currentMaxColumns;
638            private int currentRowColumns;
639
640            @Override @DefinedBy(Api.COMPILER_TREE)
641            public Void visitStartElement(StartElementTree node, Void p) {
642                switch (HtmlTag.get(node.getName())) {
643                    case TABLE: currentTable = node; break;
644                    case TR:
645                        currentMaxColumns = Math.max(currentMaxColumns, currentRowColumns);
646                        currentRowColumns = 0;
647                        break;
648                    case TD:
649                    case TH: currentRowColumns++; break;
650                }
651                return super.visitStartElement(node, p);
652            }
653
654            @Override @DefinedBy(Api.COMPILER_TREE)
655            public Void visitEndElement(EndElementTree node, Void p) {
656                if (HtmlTag.get(node.getName()) == HtmlTag.TABLE) {
657                    closeTable();
658                }
659                return super.visitEndElement(node, p);
660            }
661
662            @Override @DefinedBy(Api.COMPILER_TREE)
663            public Void visitDocComment(DocCommentTree node, Void p) {
664                try {
665                    return super.visitDocComment(node, p);
666                } finally {
667                    closeTable();
668                }
669            }
670
671            private void closeTable() {
672                if (currentTable != null) {
673                    result.put(currentTable, Math.max(currentMaxColumns, currentRowColumns));
674                    currentTable = null;
675                }
676            }
677        }.scan(dct, null);
678
679        return result;
680    }
681
682    private enum Sections {
683        TYPE_PARAMS {
684            @Override public boolean matches(DocTree t) {
685                return t.getKind() == DocTree.Kind.PARAM && ((ParamTree) t).isTypeParameter();
686            }
687        },
688        PARAMS {
689            @Override public boolean matches(DocTree t) {
690                return t.getKind() == DocTree.Kind.PARAM && !((ParamTree) t).isTypeParameter();
691            }
692        },
693        RETURNS {
694            @Override public boolean matches(DocTree t) {
695                return t.getKind() == DocTree.Kind.RETURN;
696            }
697        },
698        THROWS {
699            @Override public boolean matches(DocTree t) {
700                return t.getKind() == DocTree.Kind.THROWS;
701            }
702        };
703
704        public abstract boolean matches(DocTree t);
705    }
706}
707