001/** 002 * Copyright 2013-2015 John Ericksen 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.asciidoctor.asciidoclet; 017 018import com.google.common.base.Optional; 019import com.sun.javadoc.Doc; 020import com.sun.javadoc.DocErrorReporter; 021import com.sun.javadoc.ParamTag; 022import com.sun.javadoc.Tag; 023import org.asciidoctor.*; 024 025import static org.asciidoctor.jruby.AsciidoctorJRuby.Factory.create; 026 027import java.util.Map; 028import java.util.Map.Entry; 029 030/** 031 * Doclet renderer using and configuring Asciidoctor. 032 * 033 * @author John Ericksen 034 */ 035public class AsciidoctorRenderer implements DocletRenderer { 036 037 private static AttributesBuilder defaultAttributes() { 038 return AttributesBuilder.attributes() 039 .attribute("at", "@") 040 .attribute("slash", "/") 041 .attribute("icons", null) 042 .attribute("idprefix", "") 043 .attribute("idseparator", "-") 044 .attribute("javadoc", "") 045 .attribute("showtitle", true) 046 .attribute("source-highlighter", "coderay") 047 .attribute("coderay-css", "class") 048 .attribute("env-asciidoclet") 049 .attribute("env", "asciidoclet"); 050 } 051 052 private static OptionsBuilder defaultOptions() { 053 return OptionsBuilder.options() 054 .safe(SafeMode.SAFE) 055 .backend("html5"); 056 } 057 058 protected static final String INLINE_DOCTYPE = "Inline"; 059 060 private final Asciidoctor asciidoctor; 061 private final Optional<OutputTemplates> templates; 062 private final Options options; 063 064 public AsciidoctorRenderer(DocletOptions docletOptions, DocErrorReporter errorReporter) { 065 this(docletOptions, errorReporter, OutputTemplates.create(errorReporter), create(docletOptions.gemPath())); 066 } 067 068 /** 069 * Constructor used directly for testing purposes only. 070 */ 071 protected AsciidoctorRenderer(DocletOptions docletOptions, DocErrorReporter errorReporter, Optional<OutputTemplates> templates, Asciidoctor asciidoctor) { 072 this.asciidoctor = asciidoctor; 073 this.templates = templates; 074 this.options = buildOptions(docletOptions, errorReporter); 075 } 076 077 private Options buildOptions(DocletOptions docletOptions, DocErrorReporter errorReporter) { 078 OptionsBuilder opts = defaultOptions(); 079 if (docletOptions.baseDir().isPresent()) { 080 opts.baseDir(docletOptions.baseDir().get()); 081 } 082 if (templates.isPresent()) { 083 opts.templateDir(templates.get().templateDir()); 084 } 085 opts.attributes(buildAttributes(docletOptions, errorReporter)); 086 if (docletOptions.requires().size() > 0) { 087 for (String require : docletOptions.requires()) { 088 asciidoctor.rubyExtensionRegistry().requireLibrary(require); 089 } 090 } 091 return opts.get(); 092 } 093 094 private Attributes buildAttributes(DocletOptions docletOptions, DocErrorReporter errorReporter) { 095 return defaultAttributes() 096 .attributes(new AttributesLoader(asciidoctor, docletOptions, errorReporter).load()) 097 .get(); 098 } 099 100 /** 101 * Renders a generic document (class, field, method, etc) 102 * 103 * @param doc input 104 */ 105 @Override 106 public void renderDoc(Doc doc) { 107 // hide text that looks like tags (such as annotations in source code) from Javadoc 108 doc.setRawCommentText(doc.getRawCommentText().replaceAll("@([A-Z])", "{@literal @}$1")); 109 110 StringBuilder buffer = new StringBuilder(); 111 buffer.append(render(doc.commentText(), false)); 112 buffer.append('\n'); 113 for (Tag tag : doc.tags()) { 114 renderTag(tag, buffer); 115 buffer.append('\n'); 116 } 117 doc.setRawCommentText(buffer.toString()); 118 } 119 120 public void cleanup() { 121 if (templates.isPresent()) { 122 templates.get().delete(); 123 } 124 } 125 126 /** 127 * Renders a document tag in the standard way. 128 * 129 * @param tag input 130 * @param buffer output buffer 131 */ 132 private void renderTag(Tag tag, StringBuilder buffer) { 133 buffer.append(tag.name()).append(' '); 134 // Special handling for @param <T> tags 135 // See http://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javadoc.html#@param 136 if ((tag instanceof ParamTag) && ((ParamTag) tag).isTypeParameter()) { 137 ParamTag paramTag = (ParamTag) tag; 138 buffer.append("<" + paramTag.parameterName() + ">"); 139 String text = paramTag.parameterComment(); 140 if (text.length() > 0) { 141 buffer.append(' ').append(render(text, true)); 142 } 143 return; 144 } 145 buffer.append(render(tag.text(), true)); 146 } 147 148 /** 149 * Renders the input using Asciidoctor. 150 * 151 * The source is first cleaned by stripping any trailing space after an 152 * end line (e.g., `"\n "`), which gets left behind by the Javadoc 153 * processor. 154 * 155 * @param input AsciiDoc source 156 * @return content rendered by Asciidoctor 157 */ 158 private String render(String input, boolean inline) { 159 if (input.trim().isEmpty()) { 160 return ""; 161 } 162 if (inline) 163 options.setDocType(INLINE_DOCTYPE); 164 return asciidoctor.convert(cleanJavadocInput(input), options); 165 } 166 167 protected static String cleanJavadocInput(String input) { 168 return input.trim() 169 .replaceAll("\n ", "\n") // Newline space to accommodate javadoc newlines. 170 .replaceAll("\\{at}", "@") // {at} is translated into @. 171 .replaceAll("\\{slash}", "/") // {slash} is translated into /. 172 .replaceAll("(?m)^( *)\\*\\\\/$", "$1*/") // Multi-line comment end tag is translated into */. 173 .replaceAll("\\{@literal (.*?)}", "$1"); // {@literal _} is translated into _ (standard javadoc). 174 } 175}