Simple Markdown rendering for skydoc

This uses apache velocity engine templates to create markdown-HTML. There are other alternatives, but there is already precedent for depending on this library from docgen.

RELNOTES: None.
PiperOrigin-RevId: 203795431
diff --git a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
index c47a20a..7f66473 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -62,6 +62,7 @@
 import com.google.devtools.build.skydoc.fakebuildapi.platform.FakePlatformCommon;
 import com.google.devtools.build.skydoc.fakebuildapi.repository.FakeRepositoryModule;
 import com.google.devtools.build.skydoc.fakebuildapi.test.FakeTestingModule;
+import com.google.devtools.build.skydoc.rendering.MarkdownRenderer;
 import com.google.devtools.build.skydoc.rendering.RuleInfo;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -116,29 +117,33 @@
 
     new SkydocMain(new FilesystemFileAccessor()).eval(path, ruleInfoMap, unexportedRuleInfos);
 
+    MarkdownRenderer renderer = new MarkdownRenderer();
+
     try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
-      printRuleInfos(printWriter, ruleInfoMap.build(), unexportedRuleInfos.build());
+      printRuleInfos(printWriter, renderer, ruleInfoMap.build(), unexportedRuleInfos.build());
     }
   }
 
   // TODO(cparsons): Improve output (markdown or HTML).
   private static void printRuleInfos(
       PrintWriter printWriter,
+      MarkdownRenderer renderer,
       Map<String, RuleInfo> ruleInfos,
       List<RuleInfo> unexportedRuleInfos) throws IOException {
     for (Entry<String, RuleInfo> ruleInfoEntry : ruleInfos.entrySet()) {
-      printRuleInfo(printWriter, ruleInfoEntry.getKey(), ruleInfoEntry.getValue());
+      printRuleInfo(printWriter, renderer, ruleInfoEntry.getKey(), ruleInfoEntry.getValue());
+      printWriter.println();
     }
     for (RuleInfo unexportedRuleInfo : unexportedRuleInfos) {
-      printRuleInfo(printWriter, "<unknown name>", unexportedRuleInfo);
+      printRuleInfo(printWriter, renderer, "<unknown name>", unexportedRuleInfo);
+      printWriter.println();
     }
   }
 
   private static void printRuleInfo(
-      PrintWriter printWriter, String exportedName, RuleInfo ruleInfo) {
-    printWriter.println(exportedName);
-    printWriter.println(ruleInfo.getDescription());
-    printWriter.println();
+      PrintWriter printWriter, MarkdownRenderer renderer,
+      String exportedName, RuleInfo ruleInfo) throws IOException {
+    printWriter.println(renderer.render(exportedName, ruleInfo));
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java
index 77c912d..249ee60 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java
@@ -21,10 +21,19 @@
  * Fake implementation of {@link Descriptor}.
  */
 public class FakeDescriptor implements Descriptor {
+  private final String docString;
+
+  public FakeDescriptor(String docString) {
+    this.docString = docString;
+  }
+
+  public String getDocString() {
+    return docString;
+  }
 
   @Override
   public void repr(SkylarkPrinter printer) {}
 
   // TODO(cparsons): This class should store information about the attribute definition, for
   // example, the attribute type.
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java
index f9e5f8c..3dcfaa2 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java
@@ -30,13 +30,13 @@
   @Override
   public Descriptor intAttribute(Integer defaultInt, String doc, Boolean mandatory,
       SkylarkList<?> values, FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor stringAttribute(String defaultString, String doc, Boolean mandatory,
       SkylarkList<?> values, FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
@@ -44,21 +44,21 @@
       Object allowFiles, Object allowSingleFile, Boolean mandatory, SkylarkList<?> providers,
       Object allowRules, Boolean singleFile, Object cfg, SkylarkList<?> aspects,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor stringListAttribute(Boolean mandatory, Boolean nonEmpty, Boolean allowEmpty,
       SkylarkList<?> defaultList, String doc, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor intListAttribute(Boolean mandatory, Boolean nonEmpty, Boolean allowEmpty,
       SkylarkList<?> defaultList, String doc, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
@@ -66,7 +66,7 @@
       Object allowFiles, Object allowRules, SkylarkList<?> providers, SkylarkList<?> flags,
       Boolean mandatory, Boolean nonEmpty, Object cfg, SkylarkList<?> aspects,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
@@ -74,46 +74,46 @@
       String doc, Object allowFiles, Object allowRules, SkylarkList<?> providers,
       SkylarkList<?> flags, Boolean mandatory, Boolean nonEmpty, Object cfg, SkylarkList<?> aspects,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor boolAttribute(Boolean defaultO, String doc, Boolean mandatory,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor outputAttribute(Object defaultO, String doc, Boolean mandatory,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor outputListAttribute(Boolean allowEmpty, SkylarkList<?> defaultList, String doc,
       Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor stringDictAttribute(Boolean allowEmpty, SkylarkDict<?, ?> defaultO, String doc,
       Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor stringListDictAttribute(Boolean allowEmpty, SkylarkDict<?, ?> defaultO,
       String doc, Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
   public Descriptor licenseAttribute(Object defaultO, String doc, Boolean mandatory,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor();
+    return new FakeDescriptor(doc);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java
index 16635fc..9b1081b 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java
@@ -14,14 +14,13 @@
 
 package com.google.devtools.build.skydoc.fakebuildapi;
 
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkbuildapi.FileApi;
 import com.google.devtools.build.lib.skylarkbuildapi.FileTypeApi;
 import com.google.devtools.build.lib.skylarkbuildapi.ProviderApi;
 import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAspectApi;
-import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAttrApi.Descriptor;
 import com.google.devtools.build.lib.skylarkbuildapi.SkylarkRuleFunctionsApi;
 import com.google.devtools.build.lib.syntax.BaseFunction;
 import com.google.devtools.build.lib.syntax.Environment;
@@ -30,10 +29,11 @@
 import com.google.devtools.build.lib.syntax.Runtime;
 import com.google.devtools.build.lib.syntax.SkylarkDict;
 import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.skydoc.rendering.AttributeInfo;
 import com.google.devtools.build.skydoc.rendering.RuleInfo;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /**
@@ -68,19 +68,24 @@
       SkylarkList<?> toolchains, String doc, SkylarkList<?> providesArg,
       Boolean executionPlatformConstraintsAllowed, SkylarkList<?> execCompatibleWith,
       FuncallExpression ast, Environment funcallEnv) throws EvalException {
-    Set<String> attrNames;
+    List<AttributeInfo> attrInfos;
+    // TODO(cparsons): Include implicit "Name" attribute.
     if (attrs != null && attrs != Runtime.NONE) {
       SkylarkDict<?, ?> attrsDict = (SkylarkDict<?, ?>) attrs;
-      Map<String, Descriptor> attrsMap =
-          attrsDict.getContents(String.class, Descriptor.class, "attrs");
-      attrNames = attrsMap.keySet();
+      Map<String, FakeDescriptor> attrsMap =
+          attrsDict.getContents(String.class, FakeDescriptor.class, "attrs");
+      // TODO(cparsons): Include better attribute details. For example, attribute type.
+      attrInfos = attrsMap.entrySet().stream()
+          .map(entry -> new AttributeInfo(entry.getKey(), entry.getValue().getDocString()))
+          .sorted((o1, o2) -> o1.getName().compareTo(o2.getName()))
+          .collect(Collectors.toList());
     } else {
-      attrNames = ImmutableSet.of();
+      attrInfos = ImmutableList.of();
     }
 
     RuleDefinitionIdentifier functionIdentifier = new RuleDefinitionIdentifier();
-    // TODO(cparsons): Improve details given to RuleInfo (for example, attribute types).
-    ruleInfoList.add(new RuleInfo(functionIdentifier, ast.getLocation(), doc, attrNames));
+
+    ruleInfoList.add(new RuleInfo(functionIdentifier, ast.getLocation(), doc, attrInfos));
     return functionIdentifier;
   }
 
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/AttributeInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/AttributeInfo.java
new file mode 100644
index 0000000..8051057
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/AttributeInfo.java
@@ -0,0 +1,37 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.rendering;
+
+/**
+ * Stores information about a skylark attribute definition.
+ */
+public class AttributeInfo {
+
+  private final String name;
+  private final String docString;
+
+  public AttributeInfo(String name, String docString) {
+    this.name = name;
+    this.docString = docString;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getDocString() {
+    return docString;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
index 0dc5f08..6a2633b 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
@@ -12,11 +12,20 @@
 java_library(
     name = "rendering",
     srcs = glob(["*.java"]),
+    resources = [":template_files"],
     deps = [
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
         "//src/main/java/com/google/devtools/build/lib:syntax",
+        "//third_party:apache_velocity",
         "//third_party:guava",
         "//third_party:jsr305",
     ],
 )
+
+filegroup(
+    name = "template_files",
+    srcs = glob([
+        "templates/*.vm",
+    ]),
+)
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
new file mode 100644
index 0000000..f9f5495
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
@@ -0,0 +1,77 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.rendering;
+
+import com.google.common.base.Joiner;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
+import org.apache.velocity.runtime.resource.loader.JarResourceLoader;
+
+/**
+ * Produces skydoc output in markdown form.
+ */
+public class MarkdownRenderer {
+
+  private static final String TEMPLATE_FILENAME =
+      "com/google/devtools/build/skydoc/rendering/templates/test.vm";
+
+  private final VelocityEngine velocityEngine;
+
+  public MarkdownRenderer() {
+    this.velocityEngine = new VelocityEngine();
+    velocityEngine.setProperty("resource.loader", "classpath, jar");
+    velocityEngine.setProperty("classpath.resource.loader.class",
+        ClasspathResourceLoader.class.getName());
+    velocityEngine.setProperty("jar.resource.loader.class", JarResourceLoader.class.getName());
+    velocityEngine.setProperty("input.encoding", "UTF-8");
+    velocityEngine.setProperty("output.encoding", "UTF-8");
+    velocityEngine.setProperty("runtime.references.strict", true);
+  }
+
+  /**
+   * Returns a markdown rendering of rule documentation for the given rule information object with
+   * the given rule name.
+   */
+  public String render(String ruleName, RuleInfo ruleInfo) throws IOException {
+    VelocityContext context = new VelocityContext();
+    // TODO(cparsons): Attributes in summary form should have links.
+    context.put("summaryform", getSummaryForm(ruleName, ruleInfo));
+    context.put("ruleName", ruleName);
+    context.put("ruleInfo", ruleInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    try {
+      velocityEngine.mergeTemplate(TEMPLATE_FILENAME, "UTF-8", context, stringWriter);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+
+  private static String getSummaryForm(String ruleName, RuleInfo ruleInfo) {
+    List<String> attributeNames = ruleInfo.getAttributes().stream()
+        .map(attr -> attr.getName())
+        .collect(Collectors.toList());
+    return String.format("%s(%s)", ruleName, Joiner.on(", ").join(attributeNames));
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/RuleInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/RuleInfo.java
index dee5e0f..6f231cf 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/RuleInfo.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/RuleInfo.java
@@ -14,8 +14,6 @@
 
 package com.google.devtools.build.skydoc.rendering;
 
-import com.google.common.base.Joiner;
-import com.google.common.base.Strings;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.syntax.BaseFunction;
 import java.util.Collection;
@@ -28,16 +26,16 @@
   private final BaseFunction identifierFunction;
   private final Location location;
   private final String docString;
-  private final Collection<String> attrNames;
+  private final Collection<AttributeInfo> attrInfos;
 
   public RuleInfo(BaseFunction identifierFunction,
       Location location,
       String docString,
-      Collection<String> attrNames) {
+      Collection<AttributeInfo> attrInfos) {
     this.identifierFunction = identifierFunction;
     this.location = location;
     this.docString = docString;
-    this.attrNames = attrNames;
+    this.attrInfos = attrInfos;
   }
 
   public BaseFunction getIdentifierFunction() {
@@ -52,17 +50,7 @@
     return docString;
   }
 
-  public Collection<String> getAttrNames() {
-    return attrNames;
-  }
-
-  public String getDescription() {
-    StringBuilder stringBuilder = new StringBuilder();
-    if (!Strings.isNullOrEmpty(docString)) {
-      stringBuilder.append(docString);
-      stringBuilder.append("\n");
-    }
-    Joiner.on(",").appendTo(stringBuilder, attrNames);
-    return stringBuilder.toString();
+  public Collection<AttributeInfo> getAttributes() {
+    return attrInfos;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/templates/test.vm b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/test.vm
new file mode 100644
index 0000000..b1600e9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/test.vm
@@ -0,0 +1,27 @@
+<a name="#${ruleName}"></a>
+#[[##]]# ${ruleName}
+
+<pre>
+${summaryform}
+</pre>
+
+${ruleInfo.docString}
+
+#[[###]]# Attributes
+
+#if (!$ruleInfo.attributes.isEmpty())
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+#foreach ($attribute in $ruleInfo.attributes)
+    <tr id="#${ruleName}_${attribute.name}">
+      <td><code>${attribute.name}</code></td>
+      <td>${attribute.docString}</td>
+    </tr>
+#end
+  </tbody>
+</table>
+#end