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