001/*
002 * JDrupes MDoclet
003 * Copyright (C) 2021 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.mdoclet;
020
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.io.InputStream;
024import java.lang.reflect.InvocationTargetException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Locale;
030import java.util.Set;
031
032import javax.lang.model.SourceVersion;
033import javax.tools.Diagnostic;
034import javax.tools.DocumentationTool.Location;
035import javax.tools.JavaFileManager;
036
037import org.jdrupes.mdoclet.internal.doclets.formats.html.HtmlDoclet;
038import org.jdrupes.mdoclet.processors.FlexmarkProcessor;
039
040import com.sun.source.doctree.DocCommentTree;
041
042import jdk.javadoc.doclet.Doclet;
043import jdk.javadoc.doclet.DocletEnvironment;
044import jdk.javadoc.doclet.Reporter;
045
046/**
047 * The Doclet implementation, which converts the Markdown from the JavaDoc 
048 * comments and tags to HTML.
049 * 
050 * The doclet works by installing wrappers to intercept the 
051 * {@link HtmlDoclet}'s calls to access the {@link DocCommentTree}s 
052 * (see {@link DocCommentTreeWrapper}). At the root of this interception
053 * is a modified doclet environment ({@link MDocletEnvironment}) that 
054 * installs a wrapper around doc trees access.
055 * 
056 * For some strange reason, the `StandardDoclet` does not work
057 * with interface {@link DocletEnvironment} but insists on the instance
058 * being a `DocEnvImpl`. Therefore {@link MDocletEnvironment} has
059 * to extend this class which requires to allow module access with
060 * `--add-exports=jdk.javadoc/org.jdrupes.mdoclet.internal.tool=ALL-UNNAMED`.
061 */
062public class MDoclet implements Doclet {
063
064    public static final String HIGHLIGHT_JS_HTML
065        = "<script type=\"text/javascript\" charset=\"utf-8\" "
066            + "src=\"" + "{@docRoot}/highlight.pack.js" + "\"></script>\n"
067            + "<script type=\"text/javascript\"><!--\n"
068            + "var cssId = 'highlightCss';\n"
069            + "if (!document.getElementById(cssId))\n"
070            + "{\n"
071            + "    var head  = document.getElementsByTagName('head')[0];\n"
072            + "    var link  = document.createElement('link');\n"
073            + "    link.id   = cssId;\n"
074            + "    link.rel  = 'stylesheet';\n"
075            + "    link.type = 'text/css';\n"
076            + "    link.charset = 'utf-8';\n"
077            + "    link.href = '{@docRoot}/highlight.css';\n"
078            + "    link.media = 'all';\n"
079            + "    head.appendChild(link);\n"
080            + "}"
081            + "hljs.initHighlightingOnLoad();\n"
082            + "//--></script>";
083
084    private Reporter reporter;
085    private JavaFileManager fileManager;
086
087    private String markdownProcessorName = FlexmarkProcessor.class.getName();
088    private MarkdownProcessor processor;
089    private List<String> processorOptions = new ArrayList<>();
090    private Option origHeaderOpt;
091    private String bufferedHeader = "";
092    private Option allowScriptsOpt;
093    private boolean disableHighlight;
094    private boolean disableAutoHighlight;
095    private String highlightStyle = "default";
096
097    private final HtmlDoclet htmlDoclet;
098
099    public MDoclet() {
100        htmlDoclet = new HtmlDoclet(this);
101    }
102
103    @Override
104    public void init(Locale locale, Reporter reporter) {
105        this.reporter = reporter;
106        htmlDoclet.init(locale, reporter);
107    }
108
109    @Override
110    public String getName() {
111        return getClass().getSimpleName();
112    }
113
114    @Override
115    public Set<? extends Option> getSupportedOptions() {
116        Set<Option> options = new HashSet<>();
117        for (Option opt : htmlDoclet.getSupportedOptions()) {
118            if (opt.getNames().contains("-header")) {
119                origHeaderOpt = opt;
120            } else {
121                options.add(opt);
122            }
123            if (opt.getNames().contains("--allow-script-in-comments")) {
124                allowScriptsOpt = opt;
125            }
126        }
127        options.add(new MDocletOption("markdown-processor", 1) {
128            @Override
129            public boolean process(String option, List<String> arguments) {
130                markdownProcessorName = arguments.get(0);
131                return true;
132            }
133        });
134        options.add(new MDocletOption("disable-highlight", 0) {
135            @Override
136            public boolean process(String option, List<String> arguments) {
137                disableHighlight = true;
138                return true;
139            }
140        });
141        options.add(new MDocletOption("disable-auto-highlight", 0) {
142            @Override
143            public boolean process(String option, List<String> arguments) {
144                disableAutoHighlight = true;
145                return true;
146            }
147        });
148        options.add(new MDocletOption("highlight-style", 1) {
149            @Override
150            public boolean process(String option, List<String> arguments) {
151                highlightStyle = arguments.get(0);
152                return true;
153            }
154        });
155        options.add(new MDocletOption("M", 1) {
156            @Override
157            public boolean process(String option, List<String> arguments) {
158                return processorOptions.add(arguments.get(0));
159            }
160        });
161        options.add(new HeaderOverride());
162        return options;
163    }
164
165    private class HeaderOverride implements Option {
166
167        @Override
168        public int getArgumentCount() {
169            return 1;
170        }
171
172        @Override
173        public String getDescription() {
174            return origHeaderOpt.getDescription();
175        }
176
177        @Override
178        public Kind getKind() {
179            return origHeaderOpt.getKind();
180        }
181
182        @Override
183        public List<String> getNames() {
184            return origHeaderOpt.getNames();
185        }
186
187        @Override
188        public String getParameters() {
189            return origHeaderOpt.getParameters();
190        }
191
192        @Override
193        public boolean process(String option, List<String> arguments) {
194            bufferedHeader = arguments.get(0);
195            return true;
196        }
197
198    }
199
200    @Override
201    public SourceVersion getSupportedSourceVersion() {
202        return SourceVersion.latest();
203    }
204
205    @Override
206    public boolean run(DocletEnvironment environment) {
207        fileManager = environment.getJavaFileManager();
208        if (disableHighlight) {
209            if (bufferedHeader.length() > 0) {
210                origHeaderOpt.process("-header", List.of(bufferedHeader));
211            }
212        } else {
213            bufferedHeader += HIGHLIGHT_JS_HTML;
214            origHeaderOpt.process("-header", List.of(bufferedHeader));
215            allowScriptsOpt.process("--allow-script-in-comments",
216                Collections.emptyList());
217        }
218
219        MDocletEnvironment env = new MDocletEnvironment(this, environment);
220        processor = createProcessor();
221        processor.start(processorOptions.toArray(new String[0]));
222        return htmlDoclet.run(env) && postProcess();
223    }
224
225    private MarkdownProcessor createProcessor() {
226        try {
227            @SuppressWarnings("unchecked")
228            Class<MarkdownProcessor> mpc = (Class<MarkdownProcessor>) getClass()
229                .getClassLoader().loadClass(markdownProcessorName);
230            MarkdownProcessor mdp = (MarkdownProcessor) mpc
231                .getDeclaredConstructor().newInstance();
232            if (disableAutoHighlight && mdp.isSupportedOption(
233                MarkdownProcessor.INTERNAL_OPT_DISABLE_AUTO_HIGHLIGHT) >= 0) {
234                processorOptions
235                    .add(MarkdownProcessor.INTERNAL_OPT_DISABLE_AUTO_HIGHLIGHT);
236            }
237            return mdp;
238        } catch (ClassNotFoundException | InstantiationException
239                | IllegalAccessException | ClassCastException
240                | IllegalArgumentException | InvocationTargetException
241                | NoSuchMethodException | SecurityException e) {
242            reporter.print(Diagnostic.Kind.ERROR,
243                "Markdown processor \"" + markdownProcessorName
244                    + "\" cannot be loaded (" + e.getMessage()
245                    + "), check name and docletpath");
246            return null;
247        }
248    }
249
250    /**
251     * Returns the processor selected by the options.
252     * 
253     * @return the processor
254     */
255    public MarkdownProcessor getProcessor() {
256        return processor;
257    }
258
259    private boolean postProcess() {
260        if (disableHighlight) {
261            return true;
262        }
263        return copyResource("highlight.pack.js", "highlight.pack.js",
264            "highlight.js")
265            && copyResource("highlight-LICENSE.txt", "highlight-LICENSE.txt",
266                "highlight.js license")
267            && copyResource("highlight-styles/" + highlightStyle + ".css",
268                "highlight.css",
269                "highlight.js style '" + highlightStyle + "'");
270    }
271
272    private boolean copyResource(String resource, String destination,
273            String description) {
274        try {
275            InputStream in = MDoclet.class.getResourceAsStream(resource);
276            if (in == null) {
277                throw new FileNotFoundException();
278            }
279            in.transferTo(
280                fileManager.getFileForOutput(Location.DOCUMENTATION_OUTPUT, "",
281                    destination, null).openOutputStream());
282            return true;
283        } catch (IOException e) {
284            reporter.print(javax.tools.Diagnostic.Kind.ERROR,
285                "Error writing " + description + ": "
286                    + e.getLocalizedMessage());
287            return false;
288        }
289    }
290
291}