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.Asciidoctor.Factory.create;
026
027/**
028 * Doclet renderer using and configuring Asciidoctor.
029 *
030 * @author John Ericksen
031 */
032public class AsciidoctorRenderer implements DocletRenderer {
033
034    private static AttributesBuilder defaultAttributes() {
035        return AttributesBuilder.attributes()
036                .attribute("at", "@")
037                .attribute("slash", "/")
038                .attribute("icons", null)
039                .attribute("idprefix", "")
040                .attribute("idseparator", "-")
041                .attribute("javadoc", "")
042                .attribute("showtitle", true)
043                .attribute("source-highlighter", "coderay")
044                .attribute("coderay-css", "class");
045    }
046
047    private static OptionsBuilder defaultOptions() {
048        return OptionsBuilder.options()
049                .safe(SafeMode.SAFE)
050                .backend("html5");
051    }
052
053    protected static final String INLINE_DOCTYPE = "inline";
054
055    private final Asciidoctor asciidoctor;
056    private final Optional<OutputTemplates> templates;
057    private final Options options;
058
059    public AsciidoctorRenderer(DocletOptions docletOptions, DocErrorReporter errorReporter) {
060        this(docletOptions, errorReporter, OutputTemplates.create(errorReporter), create(docletOptions.gemPath()));
061    }
062
063    /**
064     * Constructor used directly for testing purposes only.
065     */
066    protected AsciidoctorRenderer(DocletOptions docletOptions, DocErrorReporter errorReporter, Optional<OutputTemplates> templates, Asciidoctor asciidoctor) {
067        this.asciidoctor = asciidoctor;
068        this.templates = templates;
069        this.options = buildOptions(docletOptions, errorReporter);
070    }
071
072    private Options buildOptions(DocletOptions docletOptions, DocErrorReporter errorReporter) {
073        OptionsBuilder opts = defaultOptions();
074        if (docletOptions.baseDir().isPresent()) {
075            opts.baseDir(docletOptions.baseDir().get());
076        }
077        if (templates.isPresent()) {
078            opts.templateDir(templates.get().templateDir());
079        }
080        opts.attributes(buildAttributes(docletOptions, errorReporter));
081        if (docletOptions.requires().size() > 0) {
082            for (String require : docletOptions.requires()) {
083                asciidoctor.rubyExtensionRegistry().requireLibrary(require);
084            }
085        }
086        return opts.get();
087    }
088
089    private Attributes buildAttributes(DocletOptions docletOptions, DocErrorReporter errorReporter) {
090        return defaultAttributes()
091                .attributes(new AttributesLoader(asciidoctor, docletOptions, errorReporter).load())
092                .get();
093    }
094
095    /**
096     * Renders a generic document (class, field, method, etc)
097     *
098     * @param doc input
099     */
100    @Override
101    public void renderDoc(Doc doc) {
102        // hide text that looks like tags (such as annotations in source code) from Javadoc
103        doc.setRawCommentText(doc.getRawCommentText().replaceAll("@([A-Z])", "{@literal @}$1"));
104
105        StringBuilder buffer = new StringBuilder();
106        buffer.append(render(doc.commentText(), false));
107        buffer.append('\n');
108        for (Tag tag : doc.tags()) {
109            renderTag(tag, buffer);
110            buffer.append('\n');
111        }
112        doc.setRawCommentText(buffer.toString());
113    }
114
115    public void cleanup() {
116        if (templates.isPresent()) {
117            templates.get().delete();
118        }
119    }
120
121    /**
122     * Renders a document tag in the standard way.
123     *
124     * @param tag input
125     * @param buffer output buffer
126     */
127    private void renderTag(Tag tag, StringBuilder buffer) {
128        buffer.append(tag.name()).append(' ');
129        // Special handling for @param <T> tags
130        // See http://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javadoc.html#@param
131        if ((tag instanceof ParamTag) && ((ParamTag) tag).isTypeParameter()) {
132            ParamTag paramTag = (ParamTag) tag;
133            buffer.append("<" + paramTag.parameterName() + ">");
134            String text = paramTag.parameterComment();
135            if (text.length() > 0) {
136                buffer.append(' ').append(render(text, true));
137            }
138            return;
139        }
140        buffer.append(render(tag.text(), true));
141    }
142
143    /**
144     * Renders the input using Asciidoctor.
145     *
146     * The source is first cleaned by stripping any trailing space after an
147     * end line (e.g., `"\n "`), which gets left behind by the Javadoc
148     * processor.
149     *
150     * @param input AsciiDoc source
151     * @return content rendered by Asciidoctor
152     */
153    private String render(String input, boolean inline) {
154        if (input.trim().isEmpty()) {
155            return "";
156        }
157        options.setDocType(inline ? INLINE_DOCTYPE : null);
158        return asciidoctor.render(cleanJavadocInput(input), options);
159    }
160
161    protected static String cleanJavadocInput(String input) {
162        return input.trim()
163            .replaceAll("\n ", "\n") // Newline space to accommodate javadoc newlines.
164            .replaceAll("\\{at}", "&#64;") // {at} is translated into @.
165            .replaceAll("\\{slash}", "/") // {slash} is translated into /.
166            .replaceAll("(?m)^( *)\\*\\\\/$", "$1*/") // Multi-line comment end tag is translated into */.
167            .replaceAll("\\{@literal (.*?)}", "$1"); // {@literal _} is translated into _ (standard javadoc).
168    }
169}