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}", "&#64;") // {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}