--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocFormatter.java Wed Nov 02 07:38:37 2016 +0100
@@ -0,0 +1,706 @@
+/*
+ * 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 (HtmlTag.get(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 (HtmlTag.get(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 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 (HtmlTag.get(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);
+ }
+}