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}