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("&", "&amp;")
198            .replace(">", "&gt;")
199            .replace("<", "&lt;");
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\">&#182;</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}