001/* 002 * JDrupes MDoclet 003 * Copyright 2013 Raffael Herzog 004 * Copyright (C) 2017 Michael N. Lipp 005 * 006 * This program is free software; you can redistribute it and/or modify it 007 * under the terms of the GNU General Public License as published by 008 * the Free Software Foundation; either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, but 012 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 013 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * for more details. 015 * 016 * You should have received a copy of the GNU General Public License along 017 * with this program; if not, see <http://www.gnu.org/licenses/>. 018 */ 019package org.jdrupes.mdoclet; 020 021import java.io.File; 022import java.io.FileOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.lang.reflect.Field; 027import java.nio.file.Files; 028import java.nio.file.StandardCopyOption; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036import java.util.regex.Pattern; 037 038import org.jdrupes.mdoclet.renderers.ParamTagRenderer; 039import org.jdrupes.mdoclet.renderers.SeeTagRenderer; 040import org.jdrupes.mdoclet.renderers.SimpleTagRenderer; 041import org.jdrupes.mdoclet.renderers.TagRenderer; 042import org.jdrupes.mdoclet.renderers.ThrowsTagRenderer; 043 044import com.sun.javadoc.AnnotationTypeDoc; 045import com.sun.javadoc.ClassDoc; 046import com.sun.javadoc.Doc; 047import com.sun.javadoc.DocErrorReporter; 048import com.sun.javadoc.Doclet; 049import com.sun.javadoc.LanguageVersion; 050import com.sun.javadoc.MemberDoc; 051import com.sun.javadoc.PackageDoc; 052import com.sun.javadoc.RootDoc; 053import com.sun.javadoc.SourcePosition; 054import com.sun.javadoc.Tag; 055import com.sun.tools.doclets.standard.Standard; 056import com.sun.tools.javadoc.Main; 057 058 059/** 060 * The Doclet implementation. It converts the Markdown from the JavaDoc comments and tags 061 * to HTML and sets a resulting JavaDoc comment using 062 * {@link Doc#setRawCommentText(String)}. It then passes the `RootDoc` to the standard 063 * Doclet. 064 * 065 * @see "[The Doclet Specification](http://docs.oracle.com/javase/1.5.0/docs/guide/javadoc/doclet/spec/index.html)" 066 */ 067public class MDoclet extends Doclet implements DocErrorReporter { 068 069 public static final String HIGHLIGHT_JS_HTML = 070 "<script type=\"text/javascript\" charset=\"utf-8\" " 071 + "src=\"" + "{@docRoot}/highlight.pack.js" + "\"></script>\n" 072 + "<script type=\"text/javascript\"><!--\n" 073 + "var cssId = 'highlightCss';\n" 074 + "if (!document.getElementById(cssId))\n" 075 + "{\n" 076 + " var head = document.getElementsByTagName('head')[0];\n" 077 + " var link = document.createElement('link');\n" 078 + " link.id = cssId;\n" 079 + " link.rel = 'stylesheet';\n" 080 + " link.type = 'text/css';\n" 081 + " link.charset = 'utf-8';\n" 082 + " link.href = '{@docRoot}/highlight.css';\n" 083 + " link.media = 'all';\n" 084 + " head.appendChild(link);\n" 085 + "}" 086 + "hljs.initHighlightingOnLoad();\n" 087 + "//--></script>"; 088 private static final Pattern LINE_START = Pattern.compile("^ ", Pattern.MULTILINE); 089 090 private final Map<String, TagRenderer<?>> tagRenderers = new HashMap<>(); 091 092 private final Set<PackageDoc> packages = new HashSet<>(); 093 private final Options options; 094 private final RootDoc rootDoc; 095 private MarkdownProcessor processor = null; 096 097 private boolean error = false; 098 099 /** 100 * Construct a new doclet. 101 * 102 * @param options The command line options. 103 * @param rootDoc The root document. 104 */ 105 public MDoclet(Options options, RootDoc rootDoc) { 106 this.options = options; 107 this.rootDoc = rootDoc; 108 tagRenderers.put("@author", SimpleTagRenderer.INSTANCE); 109 tagRenderers.put("@version", SimpleTagRenderer.INSTANCE); 110 tagRenderers.put("@return", SimpleTagRenderer.INSTANCE); 111 tagRenderers.put("@deprecated", SimpleTagRenderer.INSTANCE); 112 tagRenderers.put("@since", SimpleTagRenderer.INSTANCE); 113 tagRenderers.put("@param", ParamTagRenderer.INSTANCE); 114 tagRenderers.put("@throws", ThrowsTagRenderer.INSTANCE); 115 tagRenderers.put("@see", SeeTagRenderer.INSTANCE); 116 for (String tag: options.getMarkedDownTags()) { 117 tagRenderers.put("@" + tag, SimpleTagRenderer.INSTANCE); 118 } 119 } 120 121 /** 122 * As specified by the Doclet specification. 123 * 124 * @return Java 1.5. 125 * 126 * @see com.sun.javadoc.Doclet#languageVersion() 127 */ 128 public static LanguageVersion languageVersion() { 129 return LanguageVersion.JAVA_1_5; 130 } 131 132 /** 133 * As specified by the Doclet specification. 134 * 135 * @param option The option name. 136 * 137 * @return The length of the option. 138 * 139 * @see com.sun.javadoc.Doclet#optionLength(String) 140 */ 141 public static int optionLength(String option) { 142 return Options.optionLength(option); 143 } 144 145 /** 146 * As specified by the Doclet specification. 147 * 148 * @param options The command line options. 149 * @param errorReporter An error reporter to print errors. 150 * 151 * @return `true`, if the options are valid. 152 */ 153 public static boolean validOptions(String[][] options, DocErrorReporter errorReporter) { 154 return Options.validOptions(options, errorReporter); 155 } 156 157 /** 158 * As specified by the Doclet specification. 159 * 160 * @param rootDoc The root doc. 161 * 162 * @return `true`, if process was successful. 163 * 164 * @see com.sun.javadoc.Doclet#start(RootDoc) 165 */ 166 public static boolean start(RootDoc rootDoc) { 167 Options options = new Options(); 168 String[][] forwardedOptions = options.load(rootDoc.options(), rootDoc); 169 if ( forwardedOptions == null ) { 170 return false; 171 } 172 MDoclet doclet = new MDoclet(options, rootDoc); 173 doclet.process(); 174 if ( doclet.isError() ) { 175 return false; 176 } 177 RootDocWrapper rootDocWrapper = new RootDocWrapper(rootDoc, forwardedOptions); 178 if ( options.isHighlightEnabled() ) { 179 // find the footer option 180 int i = 0; 181 for ( ; i < rootDocWrapper.options().length; i++ ) { 182 if ( rootDocWrapper.options()[i][0].equals("-footer") ) { 183 rootDocWrapper.options()[i][1] += HIGHLIGHT_JS_HTML; 184 break; 185 } 186 } 187 if ( i >= rootDocWrapper.options().length ) { 188 rootDocWrapper.appendOption("-footer", HIGHLIGHT_JS_HTML); 189 } 190 if (Standard.optionLength("--allow-script-in-comments") == 1) { 191 if (rootDocWrapper.findOption("--allow-script-in-comments") == null) { 192 rootDocWrapper.appendOption("--allow-script-in-comments"); 193 } 194 } 195 } 196 return Standard.start(rootDocWrapper) && doclet.postProcess(); 197 } 198 199 /** 200 * Removes all tag renderers. 201 */ 202 public void clearTagRenderers() { 203 tagRenderers.clear(); 204 } 205 206 /** 207 * Adds a tag renderer for the specified {@link com.sun.javadoc.Tag#kind() kind}. 208 * 209 * @param kind The kind of the tag the renderer renders. 210 * @param renderer The tag renderer. 211 */ 212 public void addTagRenderer(String kind, TagRenderer<?> renderer) { 213 tagRenderers.put(kind, renderer); 214 } 215 216 /** 217 * Removes a tag renderer for the specified {@link com.sun.javadoc.Tag#kind() kind}. 218 * 219 * @param kind The kind of the tag. 220 */ 221 public void removeTagRenderer(String kind) { 222 tagRenderers.remove(kind); 223 } 224 225 /** 226 * Get the options. 227 * 228 * @return The options. 229 */ 230 public Options getOptions() { 231 return options; 232 } 233 234 /** 235 * Get the root doc. 236 * 237 * @return The root doc. 238 */ 239 public RootDoc getRootDoc() { 240 return rootDoc; 241 } 242 243 /** 244 * Process the documentation tree. If any errors occur during processing, 245 * {@link #isError()} will return `true` afterwards. 246 */ 247 public void process() { 248 processor = options.getProcessor(); 249 try { 250 processor.start(options.getProcessorOptions()); 251 } catch (Throwable e) { 252 printError(e.getMessage()); 253 return; 254 } 255 processOverview(); 256 for ( ClassDoc doc : rootDoc.classes() ) { 257 packages.add(doc.containingPackage()); 258 processClass(doc); 259 } 260 for ( PackageDoc doc : packages ) { 261 processPackage(doc); 262 } 263 } 264 265 /** 266 * Called after the standard Doclet *successfully* did its work. 267 * 268 * @return `true` if postprocessing succeeded. 269 */ 270 public boolean postProcess() { 271 boolean success = true; 272 if ( options.isHighlightEnabled() ) { 273 success &= copyResource("highlight.pack.js", 274 "highlight.pack.js", "highlight.js"); 275 success &= copyResource("highlight-LICENSE.txt", 276 "highlight-LICENSE.txt", "highlight.js license"); 277 success &= copyResource("highlight-styles/" + options.getHighlightStyle() + ".css", 278 "highlight.css", "highlight.js style '" + options.getHighlightStyle() + "'"); 279 } 280 return success; 281 } 282 283 private boolean copyResource(String resource, String destination, String description) { 284 try ( 285 InputStream in = MDoclet.class.getResourceAsStream(resource); 286 OutputStream out = new FileOutputStream 287 (new File(options.getDestinationDir(), destination)) 288 ) 289 { 290 Files.copy(in, options.getDestinationDir().toPath().resolve(destination), 291 StandardCopyOption.REPLACE_EXISTING); 292 return true; 293 } 294 catch ( IOException e ) { 295 printError("Error writing " + description + ": " + e.getLocalizedMessage()); 296 return false; 297 } 298 } 299 300 /** 301 * Check whether any errors occurred during processing of the documentation tree. 302 * 303 * @return `true` if there were errors processing the documentation tree. 304 */ 305 public boolean isError() { 306 return error; 307 } 308 309 /** 310 * Process the overview file, if specified. 311 */ 312 protected void processOverview() { 313 if ( options.getOverviewFile() != null ) { 314 try { 315 rootDoc.setRawCommentText(new String(Files.readAllBytes 316 (options.getOverviewFile().toPath()), options.getEncoding())); 317 defaultProcess(rootDoc, false); 318 } 319 catch ( IOException e ) { 320 printError("Error loading overview from " + options.getOverviewFile() + ": " + e.getLocalizedMessage()); 321 rootDoc.setRawCommentText(""); 322 } 323 } 324 } 325 326 /** 327 * Process the class documentation. 328 * 329 * @param doc The class documentation. 330 */ 331 protected void processClass(ClassDoc doc) { 332 defaultProcess(doc, true); 333 for ( MemberDoc member : doc.fields() ) { 334 processMember(member); 335 } 336 for ( MemberDoc member : doc.constructors() ) { 337 processMember(member); 338 } 339 for ( MemberDoc member : doc.methods() ) { 340 processMember(member); 341 } 342 if ( doc instanceof AnnotationTypeDoc ) { 343 for ( MemberDoc member : ((AnnotationTypeDoc)doc).elements() ) { 344 processMember(member); 345 } 346 } 347 } 348 349 /** 350 * Process the member documentation. 351 * 352 * @param doc The member documentation. 353 */ 354 protected void processMember(MemberDoc doc) { 355 defaultProcess(doc, true); 356 } 357 358 /** 359 * Process the package documentation. 360 * 361 * @param doc The package documentation. 362 */ 363 protected void processPackage(PackageDoc doc) { 364 // (#1) Set foundDoc to false if possible. 365 // foundDoc will be set to true when setRawCommentText() is called, if the method 366 // is called again, JavaDoc will issue a warning about multiple sources for the 367 // package documentation. If there actually *are* multiple sources, the warning 368 // has already been issued at this point, we will, however, use it to set the 369 // resulting HTML. So, we're setting it back to false here, to suppress the 370 // warning. 371 try { 372 Field foundDoc = doc.getClass().getDeclaredField("foundDoc"); 373 foundDoc.setAccessible(true); 374 foundDoc.set(doc, false); 375 } 376 catch ( Exception e ) { 377 printWarning(doc.position(), 378 "Cannot suppress warning about multiple package sources: " + e); 379 } 380 defaultProcess(doc, true); 381 } 382 383 /** 384 * Default processing of any documentation node. 385 * 386 * @param doc The documentation. 387 * @param fixLeadingSpaces `true` if leading spaces should be fixed. 388 */ 389 protected void defaultProcess(Doc doc, boolean fixLeadingSpaces) { 390 try { 391 StringBuilder buf = new StringBuilder(); 392 buf.append(toHtml(doc.commentText(), fixLeadingSpaces)); 393 buf.append('\n'); 394 for ( Tag tag : doc.tags() ) { 395 processTag(tag, buf); 396 buf.append('\n'); 397 } 398 doc.setRawCommentText(buf.toString()); 399 } 400 catch ( Throwable e ) { 401 if ( doc instanceof RootDoc ) { 402 printError(new SourcePosition() { 403 @Override 404 public File file() { 405 return options.getOverviewFile(); 406 } 407 @Override 408 public int line() { 409 return 0; 410 } 411 @Override 412 public int column() { 413 return 0; 414 } 415 }, e.getMessage()); 416 } 417 else { 418 printError(doc.position(), e.getMessage()); 419 } 420 } 421 } 422 423 /** 424 * Process a tag. 425 * 426 * @param tag The tag. 427 * @param target The target string builder. 428 */ 429 @SuppressWarnings("unchecked") 430 protected void processTag(Tag tag, StringBuilder target) { 431 TagRenderer<Tag> renderer = (TagRenderer<Tag>)tagRenderers.get(tag.kind()); 432 if ( renderer == null ) { 433 renderer = TagRenderer.VERBATIM; 434 } 435 renderer.render(tag, target, this); 436 } 437 438 /** 439 * Clears the processor. 440 */ 441 public void clearProcessor() { 442 processor = null; 443 } 444 445 /** 446 * Convert the given markup to HTML according to the {@link Options}. 447 * 448 * @param markup The Markdown source. 449 * 450 * @return The resulting HTML. 451 */ 452 public String toHtml(String markup) { 453 return toHtml(markup, true); 454 } 455 456 /** 457 * Converts Markdown source to HTML according to the options object. If 458 * `fixLeadingSpaces` is `true`, exactly one leading whitespace character ('\\u0020') 459 * will be removed, if it exists. 460 * 461 * @param markup The Markdown source. 462 * @param fixLeadingSpaces `true` if leading spaces should be fixed. 463 * 464 * @return The resulting HTML. 465 */ 466 public String toHtml(String markup, boolean fixLeadingSpaces) { 467 if ( fixLeadingSpaces ) { 468 markup = LINE_START.matcher(markup).replaceAll(""); 469 } 470 List<String> tags = new ArrayList<>(); 471 String html = processor.toHtml(Tags.extractInlineTags(markup, tags)); 472 return Tags.insertInlineTags(html, tags); 473 } 474 475 /** 476 * Indicate that an error occurred. This method will also be called by 477 * {@link #printError(String)} and 478 * {@link #printError(com.sun.javadoc.SourcePosition, String)}. 479 */ 480 public void error() { 481 error = true; 482 } 483 484 @Override 485 public void printError(String msg) { 486 error(); 487 rootDoc.printError(msg); 488 } 489 490 @Override 491 public void printError(SourcePosition pos, String msg) { 492 error(); 493 rootDoc.printError(pos, msg); 494 } 495 496 @Override 497 public void printWarning(String msg) { 498 rootDoc.printWarning(msg); 499 } 500 501 @Override 502 public void printWarning(SourcePosition pos, 503 String msg) 504 { 505 rootDoc.printWarning(pos, msg); 506 } 507 508 @Override 509 public void printNotice(String msg) { 510 rootDoc.printNotice(msg); 511 } 512 513 @Override 514 public void printNotice(SourcePosition pos, String msg) { 515 rootDoc.printNotice(pos, msg); 516 } 517 518 /** 519 * Returns a prefix for relative URLs from a documentation element relative to the 520 * given package. This prefix can be used to refer to the root URL of the 521 * documentation: 522 * 523 * ```java 524 * doc = "<script type=\"text/javascript\" src=\"" 525 * + rootUrlPrefix(classDoc.containingPackage()) + "highlight.js" 526 * + "\"></script>"; 527 * ``` 528 * 529 * @param doc The package containing the element from where to reference the root. 530 * 531 * @return A URL prefix for URLs referring to the doc root. 532 */ 533 public String rootUrlPrefix(PackageDoc doc) { 534 if ( doc == null || doc.name().isEmpty() ) { 535 return ""; 536 } 537 else { 538 StringBuilder buf = new StringBuilder(); 539 buf.append("../"); 540 for ( int i = 0; i < doc.name().length(); i++ ) { 541 if ( doc.name().charAt(i) == '.' ) { 542 buf.append("../"); 543 } 544 } 545 return buf.toString(); 546 } 547 } 548 549 /** 550 * Just a main method for debugging. 551 * 552 * @param args The command line arguments. 553 * 554 * @throws Exception If anything goes wrong. 555 */ 556 public static void main(String[] args) throws Exception { 557 args = Arrays.copyOf(args, args.length + 2); 558 args[args.length - 2] = "-doclet"; 559 args[args.length - 1] = MDoclet.class.getName(); 560 Main.main(args); 561 } 562 563}