001/* 002 * Copyright (c) 2012-2018 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others 003 * 004 * This program and the accompanying materials are made available under the 005 * terms of the Eclipse Public License 2.0 which is available at 006 * http://www.eclipse.org/legal/epl-2.0. 007 * 008 * SPDX-License-Identifier: EPL-2.0 009 */ 010 011package org.eclipse.golo.doc; 012 013import gololang.FunctionReference; 014import gololang.IO; 015import org.eclipse.golo.compiler.PackageAndClass; 016 017import java.nio.file.*; 018import java.io.*; 019import java.util.*; 020import java.util.stream.Collectors; 021import java.util.stream.Stream; 022 023import com.github.rjeschke.txtmark.BlockEmitter; 024import com.github.rjeschke.txtmark.Configuration; 025import com.github.rjeschke.txtmark.Processor; 026 027public class HtmlProcessor extends AbstractProcessor { 028 029 private Path srcFile; 030 private final DocIndex globalIndex = new DocIndex(); 031 032 public static final Configuration CONFIG = Configuration.builder() 033 .forceExtentedProfile() 034 .setCodeBlockEmitter(blockHighlighter()) 035 .build(); 036 037 @Override 038 protected String fileExtension() { 039 return "html"; 040 } 041 042 public DocIndex globalIndex() { 043 return globalIndex; 044 } 045 046 /** 047 * Returns the direct link to the given documentation element from a given filename. 048 */ 049 public String linkToDoc(String src, DocumentationElement dst) { 050 return linkToDoc(outputFile(src), dst); 051 } 052 053 /** 054 * Returns the direct link to the given documentation element from a given element. 055 */ 056 public String link(DocumentationElement src, DocumentationElement dst) { 057 return linkToDoc(docFile(src), dst); 058 } 059 060 private String linkToDoc(Path src, DocumentationElement dst) { 061 Path from = src; 062 if (from.getParent() != null) { 063 from = from.getParent(); 064 } 065 // The replace is to have a valid relative uri on Windows... 066 // I'd rather use URI::relativize, but it only works when one URI is the strict prefix of the other 067 // i.e. can't generate relative URIs containing '..' (what a shame!) 068 return from.relativize(docFile(dst)).toString().replace(FileSystems.getDefault().getSeparator(), "/") 069 + (dst.id().isEmpty() ? "" : ("#" + dst.id())); 070 071 } 072 073 @Override 074 public String render(ModuleDocumentation documentation) throws Throwable { 075 FunctionReference template = template("template", fileExtension()); 076 globalIndex.update(documentation); 077 Path doc = docFile(documentation); 078 if (doc.getParent() != null) { 079 doc = doc.getParent(); 080 } 081 return (String) template.invoke(this, documentation, doc.relativize(srcFile), getSubmodulesOf(documentation)); 082 } 083 084 @Override 085 public void process(Collection<ModuleDocumentation> docs, Path targetFolder) throws Throwable { 086 setTargetFolder(targetFolder); 087 for (ModuleDocumentation doc : docs) { 088 addModule(doc); 089 } 090 Set<String> donePackages = new HashSet<>(); 091 for (ModuleDocumentation doc : docs) { 092 if (doc.isEmpty()) { 093 renderPackage(doc); 094 } else { 095 renderModule(doc); 096 } 097 donePackages.add(doc.moduleName()); 098 } 099 renderRemainingPackages(donePackages); 100 renderIndex("index"); 101 renderIndex("index-all"); 102 } 103 104 private void renderRemainingPackages(Set<String> done) throws Throwable { 105 for (Map.Entry<String, Set<ModuleDocumentation>> e : getPackages()) { 106 if (done.contains(e.getKey())) { 107 continue; 108 } 109 if (e.getValue().size() < 1) { 110 continue; 111 } 112 ModuleDocumentation doc = createPackageDoc(e.getKey(), e.getValue()); 113 addModule(doc); 114 renderPackage(doc); 115 } 116 } 117 118 private ModuleDocumentation createPackageDoc(String name, Set<ModuleDocumentation> modules) throws Throwable { 119 ModuleDocumentation doc = ModuleDocumentation.empty(name); 120 List<Path> docs = modules.stream() 121 .map(ModuleDocumentation::sourceFile) 122 .map(Paths::get) 123 .flatMap(p -> packageDocumentation(p, name)) 124 .distinct() 125 .filter(Files::exists) 126 .collect(Collectors.toList()); 127 if (docs.size() > 1) { 128 org.eclipse.golo.runtime.Warnings.multiplePackageDescription(name); 129 } 130 for (Path f : docs) { 131 try { 132 doc.moduleDocumentation(IO.fileToText(f, null)); 133 break; 134 } catch (IOException e) { 135 continue; 136 } 137 } 138 return doc; 139 } 140 141 private static Stream<Path> packageDocumentation(Path mod, String name) { 142 String basename = PackageAndClass.of(name).className(); 143 Stream.Builder<Path> docs = Stream.builder(); 144 Path parent = mod.getParent(); 145 if (parent != null) { 146 if (parent.getFileName().toString().equals(name)) { 147 docs.add(mod.resolveSibling("README.md")); 148 docs.add(mod.resolveSibling("package.md")); 149 } else { 150 docs.add(parent.resolve(String.format("%s.md", basename))); 151 } 152 } 153 return docs.build(); 154 } 155 156 private void renderPackage(ModuleDocumentation documentation) throws Throwable { 157 if (documentation != null) { 158 FunctionReference template = template("package", fileExtension()); 159 IO.textToFile((String) template.invoke(this, documentation, getSubmodulesOf(documentation)), 160 outputFile(documentation.moduleName())); 161 } 162 } 163 164 private void renderModule(ModuleDocumentation documentation) throws Throwable { 165 String moduleName = documentation.moduleName(); 166 this.srcFile = outputFile(moduleName + "-src"); 167 IO.textToFile(renderSource(moduleName, documentation.sourceFile()), srcFile); 168 IO.textToFile(render(documentation), outputFile(moduleName)); 169 } 170 171 private String renderSource(String moduleName, String filename) throws Throwable { 172 FunctionReference template = template("src", fileExtension()); 173 String content = IO.fileToText(filename, "UTF-8"); 174 int nbLines = 0; 175 for (int i = 0; i < content.length(); i++) { 176 if (content.charAt(i) == '\n') { 177 nbLines++; 178 } 179 } 180 return (String) template.invoke(moduleName, content, nbLines); 181 } 182 183 public static BlockEmitter blockHighlighter() { 184 return new BlockEmitter() { 185 @Override 186 public void emitBlock(StringBuilder out, List<String> lines, String meta) { 187 String language; 188 if ("".equals(meta)) { 189 language = "golo"; 190 } else { 191 language = meta; 192 } 193 out.append("<pre class=\"listing\">"); 194 out.append(String.format("<code class=\"lang-%s\" data-lang=\"%s\">", language, language)); 195 for (String rawLine : lines) { 196 String line = rawLine 197 .replace("&", "&") 198 .replace(">", ">") 199 .replace("<", "<"); 200 out.append(line); 201 out.append('\n'); 202 } 203 out.append("</code></pre>"); 204 out.append('\n'); 205 } 206 }; 207 } 208 209 public static String sectionTitle(int level, DocumentationElement doc, Path src) { 210 String permalink = String.format("<a class=\"permalink\" href=\"#%s\" title=\"link to this section\">¶</a>", 211 doc.id()); 212 String srclink = src == null ? "" 213 : String.format("<nav class=\"srclink\"><a href=\"%s#l-%s\" rel=\"source\" title=\"Link to the corresponding source\">Source</a></nav>", 214 src, doc.line()); 215 return String.format("<h%s id=\"%s\">%s%s</h%s>%s", level, doc.id(), doc.label(), permalink, level, srclink); 216 } 217 218 public static String tocItem(DocumentationElement doc) { 219 return String.format("<a href=\"#%s\">%s</a>", doc.id(), doc.label()); 220 } 221 222 public static String moduleListItem(ModuleDocumentation doc, String target) { 223 StringBuilder item = new StringBuilder("<dt><a"); 224 if (doc.isEmpty()) { 225 item.append(" class=\"package\""); 226 } 227 item.append(" href=\"").append(target).append("\">").append(doc.moduleName()).append("</a></dt><dd>"); 228 if (doc.hasDocumentation()) { 229 String first = doc.documentation().trim().split("[.!?]")[0].trim(); 230 if (!first.isEmpty()) { 231 item.append(process(first)); 232 } 233 } 234 item.append("</dd>"); 235 return item.toString(); 236 } 237 238 public static String process(String documentation, int rootLevel, Configuration configuration) { 239 return Processor.process(AbstractProcessor.adaptSections(documentation, rootLevel), configuration); 240 } 241 242 public static String process(String documentation, int rootLevel) { 243 return process(documentation, rootLevel, CONFIG); 244 } 245 246 public static String process(String documentation) { 247 return process(documentation, 0, CONFIG); 248 } 249}