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