| /* |
| * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package jdk.internal.shellsupport.doc; |
| |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.IdentityHashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.ResourceBundle; |
| import java.util.Stack; |
| |
| import javax.lang.model.element.Name; |
| import javax.tools.JavaFileObject.Kind; |
| import javax.tools.SimpleJavaFileObject; |
| import javax.tools.ToolProvider; |
| |
| import com.sun.source.doctree.AttributeTree; |
| import com.sun.source.doctree.DocCommentTree; |
| import com.sun.source.doctree.DocTree; |
| import com.sun.source.doctree.EndElementTree; |
| import com.sun.source.doctree.EntityTree; |
| import com.sun.source.doctree.InlineTagTree; |
| import com.sun.source.doctree.LinkTree; |
| import com.sun.source.doctree.LiteralTree; |
| import com.sun.source.doctree.ParamTree; |
| import com.sun.source.doctree.ReturnTree; |
| import com.sun.source.doctree.StartElementTree; |
| import com.sun.source.doctree.TextTree; |
| import com.sun.source.doctree.ThrowsTree; |
| import com.sun.source.util.DocTreeScanner; |
| import com.sun.source.util.DocTrees; |
| import com.sun.source.util.JavacTask; |
| import com.sun.tools.doclint.Entity; |
| import com.sun.tools.doclint.HtmlTag; |
| import com.sun.tools.javac.util.DefinedBy; |
| import com.sun.tools.javac.util.DefinedBy.Api; |
| import com.sun.tools.javac.util.StringUtils; |
| |
| /**A javadoc to plain text formatter. |
| * |
| */ |
| public class JavadocFormatter { |
| |
| private static final String CODE_RESET = "\033[0m"; |
| private static final String CODE_HIGHLIGHT = "\033[1m"; |
| private static final String CODE_UNDERLINE = "\033[4m"; |
| |
| private final int lineLimit; |
| private final boolean escapeSequencesSupported; |
| |
| /** Construct the formatter. |
| * |
| * @param lineLimit maximum line length |
| * @param escapeSequencesSupported whether escape sequences are supported |
| */ |
| public JavadocFormatter(int lineLimit, boolean escapeSequencesSupported) { |
| this.lineLimit = lineLimit; |
| this.escapeSequencesSupported = escapeSequencesSupported; |
| } |
| |
| private static final int MAX_LINE_LENGTH = 95; |
| private static final int SHORTEST_LINE = 30; |
| private static final int INDENT = 4; |
| |
| /**Format javadoc to plain text. |
| * |
| * @param header element caption that should be used |
| * @param javadoc to format |
| * @return javadoc formatted to plain text |
| */ |
| public String formatJavadoc(String header, String javadoc) { |
| try { |
| StringBuilder result = new StringBuilder(); |
| |
| result.append(escape(CODE_HIGHLIGHT)).append(header).append(escape(CODE_RESET)).append("\n"); |
| |
| if (javadoc == null) { |
| return result.toString(); |
| } |
| |
| JavacTask task = (JavacTask) ToolProvider.getSystemJavaCompiler().getTask(null, null, null, null, null, null); |
| DocTrees trees = DocTrees.instance(task); |
| DocCommentTree docComment = trees.getDocCommentTree(new SimpleJavaFileObject(new URI("mem://doc.html"), Kind.HTML) { |
| @Override @DefinedBy(Api.COMPILER) |
| public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { |
| return "<body>" + javadoc + "</body>"; |
| } |
| }); |
| |
| new FormatJavadocScanner(result, task).scan(docComment, null); |
| |
| addNewLineIfNeeded(result); |
| |
| return result.toString(); |
| } catch (URISyntaxException ex) { |
| throw new InternalError("Unexpected exception", ex); |
| } |
| } |
| |
| private class FormatJavadocScanner extends DocTreeScanner<Object, Object> { |
| private final StringBuilder result; |
| private final JavacTask task; |
| private int reflownTo; |
| private int indent; |
| private int limit = Math.min(lineLimit, MAX_LINE_LENGTH); |
| private boolean pre; |
| private Map<StartElementTree, Integer> tableColumns; |
| |
| public FormatJavadocScanner(StringBuilder result, JavacTask task) { |
| this.result = result; |
| this.task = task; |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitDocComment(DocCommentTree node, Object p) { |
| tableColumns = countTableColumns(node); |
| reflownTo = result.length(); |
| scan(node.getFirstSentence(), p); |
| scan(node.getBody(), p); |
| reflow(result, reflownTo, indent, limit); |
| for (Sections current : docSections.keySet()) { |
| boolean seenAny = false; |
| for (DocTree t : node.getBlockTags()) { |
| if (current.matches(t)) { |
| if (!seenAny) { |
| seenAny = true; |
| if (result.charAt(result.length() - 1) != '\n') |
| result.append("\n"); |
| result.append("\n"); |
| result.append(escape(CODE_UNDERLINE)) |
| .append(docSections.get(current)) |
| .append(escape(CODE_RESET)) |
| .append("\n"); |
| } |
| |
| scan(t, null); |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitText(TextTree node, Object p) { |
| String text = node.getBody(); |
| if (!pre) { |
| text = text.replaceAll("[ \t\r\n]+", " ").trim(); |
| if (text.isEmpty()) { |
| text = " "; |
| } |
| } else { |
| text = text.replaceAll("\n", "\n" + indentString(indent)); |
| } |
| result.append(text); |
| return null; |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitLink(LinkTree node, Object p) { |
| if (!node.getLabel().isEmpty()) { |
| scan(node.getLabel(), p); |
| } else { |
| result.append(node.getReference().getSignature()); |
| } |
| return null; |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitParam(ParamTree node, Object p) { |
| return formatDef(node.getName().getName(), node.getDescription()); |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitThrows(ThrowsTree node, Object p) { |
| return formatDef(node.getExceptionName().getSignature(), node.getDescription()); |
| } |
| |
| public Object formatDef(CharSequence name, List<? extends DocTree> description) { |
| result.append(name); |
| result.append(" - "); |
| reflownTo = result.length(); |
| indent = name.length() + 3; |
| |
| if (limit - indent < SHORTEST_LINE) { |
| result.append("\n"); |
| result.append(indentString(INDENT)); |
| indent = INDENT; |
| reflownTo += INDENT; |
| } |
| try { |
| return scan(description, null); |
| } finally { |
| reflow(result, reflownTo, indent, limit); |
| result.append("\n"); |
| } |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitLiteral(LiteralTree node, Object p) { |
| return scan(node.getBody(), p); |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitReturn(ReturnTree node, Object p) { |
| reflownTo = result.length(); |
| try { |
| return super.visitReturn(node, p); |
| } finally { |
| reflow(result, reflownTo, 0, limit); |
| } |
| } |
| |
| Stack<Integer> listStack = new Stack<>(); |
| Stack<Integer> defStack = new Stack<>(); |
| Stack<Integer> tableStack = new Stack<>(); |
| Stack<List<Integer>> cellsStack = new Stack<>(); |
| Stack<List<Boolean>> headerStack = new Stack<>(); |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitStartElement(StartElementTree node, Object p) { |
| switch (getHtmlTag(node.getName())) { |
| case P: |
| if (lastNode!= null && lastNode.getKind() == DocTree.Kind.START_ELEMENT && |
| HtmlTag.get(((StartElementTree) lastNode).getName()) == HtmlTag.LI) { |
| //ignore |
| break; |
| } |
| reflowTillNow(); |
| addNewLineIfNeeded(result); |
| result.append(indentString(indent)); |
| reflownTo = result.length(); |
| break; |
| case BLOCKQUOTE: |
| reflowTillNow(); |
| indent += INDENT; |
| break; |
| case PRE: |
| reflowTillNow(); |
| pre = true; |
| break; |
| case UL: |
| reflowTillNow(); |
| listStack.push(-1); |
| indent += INDENT; |
| break; |
| case OL: |
| reflowTillNow(); |
| listStack.push(1); |
| indent += INDENT; |
| break; |
| case DL: |
| reflowTillNow(); |
| defStack.push(indent); |
| break; |
| case LI: |
| reflowTillNow(); |
| if (!listStack.empty()) { |
| addNewLineIfNeeded(result); |
| |
| int top = listStack.pop(); |
| |
| if (top == (-1)) { |
| result.append(indentString(indent - 2)); |
| result.append("* "); |
| } else { |
| result.append(indentString(indent - 3)); |
| result.append("" + top++ + ". "); |
| } |
| |
| listStack.push(top); |
| |
| reflownTo = result.length(); |
| } |
| break; |
| case DT: |
| reflowTillNow(); |
| if (!defStack.isEmpty()) { |
| addNewLineIfNeeded(result); |
| indent = defStack.peek(); |
| result.append(escape(CODE_HIGHLIGHT)); |
| } |
| break; |
| case DD: |
| reflowTillNow(); |
| if (!defStack.isEmpty()) { |
| if (indent == defStack.peek()) { |
| result.append(escape(CODE_RESET)); |
| } |
| addNewLineIfNeeded(result); |
| indent = defStack.peek() + INDENT; |
| result.append(indentString(indent)); |
| } |
| break; |
| case H1: case H2: case H3: |
| case H4: case H5: case H6: |
| reflowTillNow(); |
| addNewLineIfNeeded(result); |
| result.append("\n") |
| .append(escape(CODE_UNDERLINE)); |
| reflownTo = result.length(); |
| break; |
| case TABLE: |
| int columns = tableColumns.get(node); |
| |
| if (columns == 0) { |
| break; //broken input |
| } |
| |
| reflowTillNow(); |
| addNewLineIfNeeded(result); |
| reflownTo = result.length(); |
| |
| tableStack.push(limit); |
| |
| limit = (limit - 1) / columns - 3; |
| |
| for (int sep = 0; sep < (limit + 3) * columns + 1; sep++) { |
| result.append("-"); |
| } |
| |
| result.append("\n"); |
| |
| break; |
| case TR: |
| if (cellsStack.size() >= tableStack.size()) { |
| //unclosed <tr>: |
| handleEndElement(node.getName()); |
| } |
| cellsStack.push(new ArrayList<>()); |
| headerStack.push(new ArrayList<>()); |
| break; |
| case TH: |
| case TD: |
| if (cellsStack.isEmpty()) { |
| //broken code |
| break; |
| } |
| reflowTillNow(); |
| result.append("\n"); |
| reflownTo = result.length(); |
| cellsStack.peek().add(result.length()); |
| headerStack.peek().add(HtmlTag.get(node.getName()) == HtmlTag.TH); |
| break; |
| case IMG: |
| for (DocTree attr : node.getAttributes()) { |
| if (attr.getKind() != DocTree.Kind.ATTRIBUTE) { |
| continue; |
| } |
| AttributeTree at = (AttributeTree) attr; |
| if ("alt".equals(StringUtils.toLowerCase(at.getName().toString()))) { |
| addSpaceIfNeeded(result); |
| scan(at.getValue(), null); |
| addSpaceIfNeeded(result); |
| break; |
| } |
| } |
| break; |
| default: |
| addSpaceIfNeeded(result); |
| break; |
| } |
| return null; |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitEndElement(EndElementTree node, Object p) { |
| handleEndElement(node.getName()); |
| return super.visitEndElement(node, p); |
| } |
| |
| private void handleEndElement(Name name) { |
| switch (getHtmlTag(name)) { |
| case BLOCKQUOTE: |
| indent -= INDENT; |
| break; |
| case PRE: |
| pre = false; |
| addNewLineIfNeeded(result); |
| reflownTo = result.length(); |
| break; |
| case UL: case OL: |
| if (listStack.isEmpty()) { //ignore stray closing tag |
| break; |
| } |
| reflowTillNow(); |
| listStack.pop(); |
| indent -= INDENT; |
| addNewLineIfNeeded(result); |
| break; |
| case DL: |
| if (defStack.isEmpty()) {//ignore stray closing tag |
| break; |
| } |
| reflowTillNow(); |
| if (indent == defStack.peek()) { |
| result.append(escape(CODE_RESET)); |
| } |
| indent = defStack.pop(); |
| addNewLineIfNeeded(result); |
| break; |
| case H1: case H2: case H3: |
| case H4: case H5: case H6: |
| reflowTillNow(); |
| result.append(escape(CODE_RESET)) |
| .append("\n"); |
| reflownTo = result.length(); |
| break; |
| case TABLE: |
| if (cellsStack.size() >= tableStack.size()) { |
| //unclosed <tr>: |
| handleEndElement(task.getElements().getName("tr")); |
| } |
| |
| if (tableStack.isEmpty()) { |
| break; |
| } |
| |
| limit = tableStack.pop(); |
| break; |
| case TR: |
| if (cellsStack.isEmpty()) { |
| break; |
| } |
| |
| reflowTillNow(); |
| |
| List<Integer> cells = cellsStack.pop(); |
| List<Boolean> headerFlags = headerStack.pop(); |
| List<String[]> content = new ArrayList<>(); |
| int maxLines = 0; |
| |
| result.append("\n"); |
| |
| while (!cells.isEmpty()) { |
| int currentCell = cells.remove(cells.size() - 1); |
| String[] lines = result.substring(currentCell, result.length()).split("\n"); |
| |
| result.delete(currentCell - 1, result.length()); |
| |
| content.add(lines); |
| maxLines = Math.max(maxLines, lines.length); |
| } |
| |
| Collections.reverse(content); |
| |
| for (int line = 0; line < maxLines; line++) { |
| for (int column = 0; column < content.size(); column++) { |
| String[] lines = content.get(column); |
| String currentLine = line < lines.length ? lines[line] : ""; |
| result.append("| "); |
| boolean header = headerFlags.get(column); |
| if (header) { |
| result.append(escape(CODE_HIGHLIGHT)); |
| } |
| result.append(currentLine); |
| if (header) { |
| result.append(escape(CODE_RESET)); |
| } |
| int padding = limit - currentLine.length(); |
| if (padding > 0) |
| result.append(indentString(padding)); |
| result.append(" "); |
| } |
| result.append("|\n"); |
| } |
| |
| for (int sep = 0; sep < (limit + 3) * content.size() + 1; sep++) { |
| result.append("-"); |
| } |
| |
| result.append("\n"); |
| |
| reflownTo = result.length(); |
| break; |
| case TD: |
| case TH: |
| break; |
| default: |
| addSpaceIfNeeded(result); |
| break; |
| } |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object visitEntity(EntityTree node, Object p) { |
| String name = node.getName().toString(); |
| int code = -1; |
| if (name.startsWith("#")) { |
| try { |
| int v = StringUtils.toLowerCase(name).startsWith("#x") |
| ? Integer.parseInt(name.substring(2), 16) |
| : Integer.parseInt(name.substring(1), 10); |
| if (Entity.isValid(v)) { |
| code = v; |
| } |
| } catch (NumberFormatException ex) { |
| //ignore |
| } |
| } else { |
| Entity entity = Entity.get(name); |
| if (entity != null) { |
| code = entity.code; |
| } |
| } |
| if (code != (-1)) { |
| result.appendCodePoint(code); |
| } else { |
| result.append(node.toString()); |
| } |
| return super.visitEntity(node, p); |
| } |
| |
| private DocTree lastNode; |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Object scan(DocTree node, Object p) { |
| if (node instanceof InlineTagTree) { |
| addSpaceIfNeeded(result); |
| } |
| try { |
| return super.scan(node, p); |
| } finally { |
| if (node instanceof InlineTagTree) { |
| addSpaceIfNeeded(result); |
| } |
| lastNode = node; |
| } |
| } |
| |
| private void reflowTillNow() { |
| while (result.length() > 0 && result.charAt(result.length() - 1) == ' ') |
| result.delete(result.length() - 1, result.length()); |
| reflow(result, reflownTo, indent, limit); |
| reflownTo = result.length(); |
| } |
| }; |
| |
| private String escape(String sequence) { |
| return this.escapeSequencesSupported ? sequence : ""; |
| } |
| |
| private static final Map<Sections, String> docSections = new LinkedHashMap<>(); |
| |
| static { |
| ResourceBundle bundle = |
| ResourceBundle.getBundle("jdk.internal.shellsupport.doc.resources.javadocformatter"); |
| docSections.put(Sections.TYPE_PARAMS, bundle.getString("CAP_TypeParameters")); |
| docSections.put(Sections.PARAMS, bundle.getString("CAP_Parameters")); |
| docSections.put(Sections.RETURNS, bundle.getString("CAP_Returns")); |
| docSections.put(Sections.THROWS, bundle.getString("CAP_Thrown_Exceptions")); |
| } |
| |
| private static String indentString(int indent) { |
| char[] content = new char[indent]; |
| Arrays.fill(content, ' '); |
| return new String(content); |
| } |
| |
| private static void reflow(StringBuilder text, int from, int indent, int limit) { |
| int lineStart = from; |
| |
| while (lineStart > 0 && text.charAt(lineStart - 1) != '\n') { |
| lineStart--; |
| } |
| |
| int lineChars = from - lineStart; |
| int pointer = from; |
| int lastSpace = -1; |
| |
| while (pointer < text.length()) { |
| if (text.charAt(pointer) == ' ') |
| lastSpace = pointer; |
| if (lineChars >= limit) { |
| if (lastSpace != (-1)) { |
| text.setCharAt(lastSpace, '\n'); |
| text.insert(lastSpace + 1, indentString(indent)); |
| lineChars = indent + pointer - lastSpace - 1; |
| pointer += indent; |
| lastSpace = -1; |
| } |
| } |
| lineChars++; |
| pointer++; |
| } |
| } |
| |
| private static void addNewLineIfNeeded(StringBuilder text) { |
| if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') { |
| text.append("\n"); |
| } |
| } |
| |
| private static void addSpaceIfNeeded(StringBuilder text) { |
| if (text.length() == 0) |
| return ; |
| |
| char last = text.charAt(text.length() - 1); |
| |
| if (last != ' ' && last != '\n') { |
| text.append(" "); |
| } |
| } |
| |
| private static HtmlTag getHtmlTag(Name name) { |
| HtmlTag tag = HtmlTag.get(name); |
| |
| return tag != null ? tag : HtmlTag.HTML; //using HtmlTag.HTML as default no-op value |
| } |
| |
| private static Map<StartElementTree, Integer> countTableColumns(DocCommentTree dct) { |
| Map<StartElementTree, Integer> result = new IdentityHashMap<>(); |
| |
| new DocTreeScanner<Void, Void>() { |
| private StartElementTree currentTable; |
| private int currentMaxColumns; |
| private int currentRowColumns; |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Void visitStartElement(StartElementTree node, Void p) { |
| switch (getHtmlTag(node.getName())) { |
| case TABLE: currentTable = node; break; |
| case TR: |
| currentMaxColumns = Math.max(currentMaxColumns, currentRowColumns); |
| currentRowColumns = 0; |
| break; |
| case TD: |
| case TH: currentRowColumns++; break; |
| } |
| return super.visitStartElement(node, p); |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Void visitEndElement(EndElementTree node, Void p) { |
| if (HtmlTag.get(node.getName()) == HtmlTag.TABLE) { |
| closeTable(); |
| } |
| return super.visitEndElement(node, p); |
| } |
| |
| @Override @DefinedBy(Api.COMPILER_TREE) |
| public Void visitDocComment(DocCommentTree node, Void p) { |
| try { |
| return super.visitDocComment(node, p); |
| } finally { |
| closeTable(); |
| } |
| } |
| |
| private void closeTable() { |
| if (currentTable != null) { |
| result.put(currentTable, Math.max(currentMaxColumns, currentRowColumns)); |
| currentTable = null; |
| } |
| } |
| }.scan(dct, null); |
| |
| return result; |
| } |
| |
| private enum Sections { |
| TYPE_PARAMS { |
| @Override public boolean matches(DocTree t) { |
| return t.getKind() == DocTree.Kind.PARAM && ((ParamTree) t).isTypeParameter(); |
| } |
| }, |
| PARAMS { |
| @Override public boolean matches(DocTree t) { |
| return t.getKind() == DocTree.Kind.PARAM && !((ParamTree) t).isTypeParameter(); |
| } |
| }, |
| RETURNS { |
| @Override public boolean matches(DocTree t) { |
| return t.getKind() == DocTree.Kind.RETURN; |
| } |
| }, |
| THROWS { |
| @Override public boolean matches(DocTree t) { |
| return t.getKind() == DocTree.Kind.THROWS; |
| } |
| }; |
| |
| public abstract boolean matches(DocTree t); |
| } |
| } |