src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocFormatter.java
/*
* 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);
}
}