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
diff --git a/src/test/java/com/google/devtools/build/skydoc/BUILD b/src/test/java/com/google/devtools/build/skydoc/BUILD
index d235bf8..c12329f 100644
--- a/src/test/java/com/google/devtools/build/skydoc/BUILD
+++ b/src/test/java/com/google/devtools/build/skydoc/BUILD
@@ -67,6 +67,13 @@
 )
 
 skydoc_test(
+    name = "cpp_basic_test",
+    golden_file = "testdata/cpp_basic_test/golden.txt",
+    input_file = "testdata/cpp_basic_test/input.bzl",
+    skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+)
+
+skydoc_test(
     name = "java_basic_test",
     golden_file = "testdata/java_basic_test/golden.txt",
     input_file = "testdata/java_basic_test/input.bzl",
diff --git a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
index 870fa67..a1d7acb 100644
--- a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
@@ -97,11 +97,16 @@
     Entry<String, RuleInfo> ruleInfo = Iterables.getOnlyElement(ruleInfos.entrySet());
     assertThat(ruleInfo.getKey()).isEqualTo("my_rule");
     assertThat(ruleInfo.getValue().getDocString()).isEqualTo("This is my rule. It does stuff.");
-    assertThat(ruleInfo.getValue().getAttrNames()).containsExactly(
-        "first", "second", "third", "fourth");
+    assertThat(getAttrNames(ruleInfo.getValue())).containsExactly(
+        "first", "fourth", "second", "third").inOrder();
     assertThat(unexportedRuleInfos.build()).isEmpty();
   }
 
+  private static Iterable<String> getAttrNames(RuleInfo ruleInfo) {
+    return ruleInfo.getAttributes().stream().map(attr -> attr.getName())
+        .collect(Collectors.toList());
+  }
+
   @Test
   public void testMultipleRuleNames() throws Exception {
     scratch.file(
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt
index c7ed05d..e7c4c5f 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt
@@ -1,4 +1,37 @@
-android_related_rule
+<a name="#android_related_rule"></a>
+## android_related_rule
+
+<pre>
+android_related_rule(first, fourth, second, third)
+</pre>
+
 This rule does android-related things.
-first,second,third,fourth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#android_related_rule_first">
+      <td><code>first</code></td>
+      <td></td>
+    </tr>
+    <tr id="#android_related_rule_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#android_related_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+    <tr id="#android_related_rule_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt
index 36642e4..882f540 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt
@@ -1,4 +1,37 @@
-apple_related_rule
+<a name="#apple_related_rule"></a>
+## apple_related_rule
+
+<pre>
+apple_related_rule(first, fourth, second, third)
+</pre>
+
 This rule does apple-related things.
-first,second,third,fourth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#apple_related_rule_first">
+      <td><code>first</code></td>
+      <td></td>
+    </tr>
+    <tr id="#apple_related_rule_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#apple_related_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+    <tr id="#apple_related_rule_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt
index 45699d1..9674b82 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt
@@ -1,4 +1,37 @@
-cpp_related_rule
+<a name="#cpp_related_rule"></a>
+## cpp_related_rule
+
+<pre>
+cpp_related_rule(first, fourth, second, third)
+</pre>
+
 This rule does cpp-related things.
-first,second,third,fourth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#cpp_related_rule_first">
+      <td><code>first</code></td>
+      <td></td>
+    </tr>
+    <tr id="#cpp_related_rule_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#cpp_related_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+    <tr id="#cpp_related_rule_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt
index 274e1f2..7ab9fc0 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt
@@ -1,4 +1,37 @@
-java_related_rule
+<a name="#java_related_rule"></a>
+## java_related_rule
+
+<pre>
+java_related_rule(first, fourth, second, third)
+</pre>
+
 This rule does java-related things.
-first,second,third,fourth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#java_related_rule_first">
+      <td><code>first</code></td>
+      <td></td>
+    </tr>
+    <tr id="#java_related_rule_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#java_related_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+    <tr id="#java_related_rule_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt
index 5880c8c..0619f29 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt
@@ -1,4 +1,37 @@
-my_rule
+<a name="#my_rule"></a>
+## my_rule
+
+<pre>
+my_rule(first, fourth, second, third)
+</pre>
+
 This rule exercises some of the build API.
-first,second,third,fourth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#my_rule_first">
+      <td><code>first</code></td>
+      <td></td>
+    </tr>
+    <tr id="#my_rule_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#my_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+    <tr id="#my_rule_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt
index b1a4a66..1b2d0a0 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt
@@ -1,12 +1,83 @@
-my_rule
+<a name="#my_rule"></a>
+## my_rule
+
+<pre>
+my_rule(first, second)
+</pre>
+
 This is my rule. It does stuff.
-first,second
 
-other_rule
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#my_rule_first">
+      <td><code>first</code></td>
+      <td>first my_rule doc string</td>
+    </tr>
+    <tr id="#my_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
+
+<a name="#other_rule"></a>
+## other_rule
+
+<pre>
+other_rule(fourth, third)
+</pre>
+
 This is another rule.
-third,fourth
 
-yet_another_rule
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#other_rule_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#other_rule_third">
+      <td><code>third</code></td>
+      <td>third other_rule doc string</td>
+    </tr>
+  </tbody>
+</table>
+
+
+<a name="#yet_another_rule"></a>
+## yet_another_rule
+
+<pre>
+yet_another_rule(fifth)
+</pre>
+
 This is yet another rule
-fifth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#yet_another_rule_fifth">
+      <td><code>fifth</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl
index b331e60..efd5ed4 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl
@@ -4,7 +4,8 @@
     implementation = my_rule_impl,
     doc = "This is my rule. It does stuff.",
     attrs = {
-        "first": attr.label(mandatory = True, allow_files = True, single_file = True),
+        "first": attr.label(mandatory = True, doc = "first my_rule doc string",
+                            allow_files = True, single_file = True),
         "second": attr.string_dict(mandatory = True),
     },
 )
@@ -13,7 +14,8 @@
     implementation = my_rule_impl,
     doc = "This is another rule.",
     attrs = {
-        "third": attr.label(mandatory = True, allow_files = True, single_file = True),
+        "third": attr.label(mandatory = True, doc = "third other_rule doc string",
+                            allow_files = True, single_file = True),
         "fourth": attr.string_dict(mandatory = True),
     },
 )
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt
index b1a4a66..7d37f79 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt
@@ -1,12 +1,83 @@
-my_rule
+<a name="#my_rule"></a>
+## my_rule
+
+<pre>
+my_rule(first, second)
+</pre>
+
 This is my rule. It does stuff.
-first,second
 
-other_rule
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#my_rule_first">
+      <td><code>first</code></td>
+      <td></td>
+    </tr>
+    <tr id="#my_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
+
+<a name="#other_rule"></a>
+## other_rule
+
+<pre>
+other_rule(fourth, third)
+</pre>
+
 This is another rule.
-third,fourth
 
-yet_another_rule
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#other_rule_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#other_rule_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
+
+<a name="#yet_another_rule"></a>
+## yet_another_rule
+
+<pre>
+yet_another_rule(fifth)
+</pre>
+
 This is yet another rule
-fifth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#yet_another_rule_fifth">
+      <td><code>fifth</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt
index 50b71f0..d86c0c7 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt
@@ -1,4 +1,37 @@
-my_rule
+<a name="#my_rule"></a>
+## my_rule
+
+<pre>
+my_rule(first, fourth, second, third)
+</pre>
+
 This is my rule. It does stuff.
-first,second,third,fourth
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#my_rule_first">
+      <td><code>first</code></td>
+      <td>first doc string</td>
+    </tr>
+    <tr id="#my_rule_fourth">
+      <td><code>fourth</code></td>
+      <td>fourth doc string</td>
+    </tr>
+    <tr id="#my_rule_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+    <tr id="#my_rule_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/input.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/input.bzl
index de1548b..08550fe 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/input.bzl
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/input.bzl
@@ -5,9 +5,10 @@
     implementation = my_rule_impl,
     doc = "This is my rule. It does stuff.",
     attrs = {
-        "first": attr.label(mandatory = True, allow_files = True, single_file = True),
+        "first": attr.label(mandatory = True, doc = "first doc string",
+                            allow_files = True, single_file = True),
         "second": attr.string_dict(mandatory = True),
         "third": attr.output(mandatory = True),
-        "fourth": attr.bool(default = False, mandatory = False),
+        "fourth": attr.bool(default = False, doc = "fourth doc string", mandatory = False),
     },
 )
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt
index ab79bcf..44040ee 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt
@@ -1,3 +1,37 @@
-<unknown name>
-first,second,third,fourth
+<a name="#<unknown name>"></a>
+## <unknown name>
+
+<pre>
+<unknown name>(first, fourth, second, third)
+</pre>
+
+
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#<unknown name>_first">
+      <td><code>first</code></td>
+      <td></td>
+    </tr>
+    <tr id="#<unknown name>_fourth">
+      <td><code>fourth</code></td>
+      <td></td>
+    </tr>
+    <tr id="#<unknown name>_second">
+      <td><code>second</code></td>
+      <td></td>
+    </tr>
+    <tr id="#<unknown name>_third">
+      <td><code>third</code></td>
+      <td></td>
+    </tr>
+  </tbody>
+</table>
+