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.TemplateEngine;
015import gololang.IO;
016
017import java.io.IOException;
018import java.io.InputStream;
019import java.io.InputStreamReader;
020import java.nio.file.Path;
021import java.nio.file.FileSystems;
022import java.nio.charset.StandardCharsets;
023import java.util.*;
024
025public abstract class AbstractProcessor {
026
027  public abstract String render(ModuleDocumentation module) throws Throwable;
028
029  public abstract void process(Collection<ModuleDocumentation> modules, Path targetFolder) throws Throwable;
030
031  private final TemplateEngine templateEngine = new TemplateEngine();
032  private final Map<String, FunctionReference> templateCache = new HashMap<>();
033
034  private Path targetFolder;
035  private final Set<ModuleDocumentation> modules = new TreeSet<>();
036  private Map<String, Set<ModuleDocumentation>> packages = new TreeMap<>();
037
038  public void setTargetFolder(Path target) {
039    this.targetFolder = target.toAbsolutePath();
040  }
041
042  public Path getTargetFolder() {
043    return this.targetFolder;
044  }
045
046  public Set<ModuleDocumentation> modules() {
047    return modules;
048  }
049
050  protected void addModule(ModuleDocumentation module) {
051    this.modules.add(module);
052    if (!module.isEmpty()) {
053      String packageName = module.packageName();
054      if (!packageName.isEmpty()) {
055        packages.putIfAbsent(packageName, new TreeSet<ModuleDocumentation>());
056        packages.get(packageName).add(module);
057      }
058    }
059  }
060
061  protected Set<ModuleDocumentation> getSubmodulesOf(ModuleDocumentation doc) {
062    return this.packages.getOrDefault(doc.moduleName(), Collections.emptySet());
063  }
064
065  public Set<Map.Entry<String, Set<ModuleDocumentation>>> getPackages() {
066    return this.packages.entrySet();
067  }
068
069  protected String fileExtension() {
070    return "";
071  }
072
073  protected FunctionReference template(String name, String format) throws IOException {
074    String key = name + "-" + format;
075    if (templateCache.containsKey(key)) {
076      return templateCache.get(key);
077    }
078    InputStream in = AbstractProcessor.class.getResourceAsStream("/org/eclipse/golo/doc/" + key);
079    if (in == null) {
080      throw new IllegalArgumentException("There is no template " + name + " for format: " + format);
081    }
082    try (InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
083      StringBuilder builder = new StringBuilder();
084      char[] buffer = new char[1024];
085      int nread;
086      while ((nread = reader.read(buffer)) > 0) {
087        builder.append(buffer, 0, nread);
088      }
089      FunctionReference compiledTemplate = templateEngine.compile(builder.toString());
090      templateCache.put(key, compiledTemplate);
091      return compiledTemplate;
092    }
093  }
094
095  public Path outputFile(String name) {
096    if (targetFolder == null) {
097      throw new IllegalStateException("no target folder defined");
098    }
099    return targetFolder.resolve(name.replace(".", FileSystems.getDefault().getSeparator())
100        + (fileExtension().isEmpty() ? "" : ("." + fileExtension())));
101  }
102
103  /**
104   * Return the absolute path of the file containing the given element.
105   */
106  public Path docFile(DocumentationElement doc) {
107    DocumentationElement parent = doc;
108    while (parent.parent() != parent) {
109      parent = parent.parent();
110    }
111    if (parent instanceof ModuleDocumentation) {
112      return outputFile(((ModuleDocumentation) parent).moduleName());
113    }
114    return outputFile(parent.name());
115  }
116
117  /**
118   * Returns the link to the given filename from the given document.
119   */
120  public String linkToFile(DocumentationElement src, String dst) {
121    Path doc = docFile(src);
122    if (doc.getParent() != null) {
123      doc = doc.getParent();
124    }
125    // The replace is to have a valid relative uri on Windows...
126    // I'd rather use URI::relativize, but it only works when one URI is the strict prefix of the other
127    // i.e. can't generate relative URIs containing '..' (what a shame!)
128    return doc.relativize(outputFile(dst)).toString().replace(FileSystems.getDefault().getSeparator(), "/");
129  }
130
131  /**
132   * Returns the link to the given filename from the given filename.
133   */
134  public String linkToFile(String src, String dst) {
135    //
136    Path out = outputFile(src);
137    if (out.getParent() != null) {
138      out = out.getParent();
139    }
140    // The replace is to have a valid relative uri on Windows...
141    // I'd rather use URI::relativize, but it only works when one URI is the strict prefix of the other
142    // i.e. can't generate relative URIs containing '..' (what a shame!)
143    return out.relativize(outputFile(dst)).toString().replace(FileSystems.getDefault().getSeparator(), "/");
144  }
145
146  protected void renderIndex(String templateName) throws Throwable {
147    FunctionReference indexTemplate = template(templateName, fileExtension());
148    String index = (String) indexTemplate.invoke(this);
149    IO.textToFile(index, outputFile(templateName));
150  }
151
152  /**
153   * Change the section level of the given markdown line.
154   * <p>
155   * For instance, {@code subsection("# Title", 1)} gives {@code "## Title"}
156   * <p>
157   * Used when displaying a markdown Golo documentation inside the golodoc, to avoid the user to
158   * remember at which level to start subsections for each documentation element.
159   */
160  private static String subsection(String line, int level) {
161    if (!line.trim().startsWith("#") || line.startsWith("    ")) {
162      return line;
163    }
164    StringBuilder output = new StringBuilder();
165    for (int i = 0; i < level; i++) {
166      output.append('#');
167    }
168    output.append(line.trim());
169    return output.toString();
170  }
171
172  public static String adaptSections(String documentation, int rootLevel) {
173    if (documentation == null || documentation.isEmpty()) {
174      return "";
175    }
176    if (rootLevel == 0) {
177      return documentation;
178    }
179    StringBuilder output = new StringBuilder();
180    for (String line: documentation.split("\n")) {
181      output.append(subsection(line, rootLevel));
182      output.append("\n");
183    }
184    return output.toString();
185  }
186}