Enable generating a table of contents in Stardoc (#203)

Adds a bool `generate_table_of_contents` to `stardoc()` to enable table of contents. This defaults to False for backwards compatibility. Adds a Label `table_of_contents_template` to `stardoc()` to specify the vm template.

The default table of contents template looks like this (from the test):

> ## Rules
> 
> - [my_rule](#my_rule)
> 
> ## Providers
> 
> - [MyFooInfo](#MyFooInfo)
> - [MyVeryDocumentedInfo](#MyVeryDocumentedInfo)
> 
> ## Functions
> 
> - [check_sources](#check_sources)
> - [returns_a_thing](#returns_a_thing)
> 
> ## Aspects
> 
> - [my_aspect](#my_aspect)
> - [other_aspect](#other_aspect)
> 
> ## Repository Rules
> 
> - [my_repo](#my_repo)
> 
> ## Module Extensions
> 
> - [my_ext](#my_ext)
diff --git a/docs/stardoc_rule.md b/docs/stardoc_rule.md
index 18bb219..cf79770 100644
--- a/docs/stardoc_rule.md
+++ b/docs/stardoc_rule.md
@@ -8,9 +8,9 @@
 
 <pre>
 stardoc(<a href="#stardoc-name">name</a>, <a href="#stardoc-input">input</a>, <a href="#stardoc-out">out</a>, <a href="#stardoc-deps">deps</a>, <a href="#stardoc-format">format</a>, <a href="#stardoc-symbol_names">symbol_names</a>, <a href="#stardoc-semantic_flags">semantic_flags</a>, <a href="#stardoc-stardoc">stardoc</a>, <a href="#stardoc-renderer">renderer</a>,
-        <a href="#stardoc-aspect_template">aspect_template</a>, <a href="#stardoc-func_template">func_template</a>, <a href="#stardoc-header_template">header_template</a>, <a href="#stardoc-provider_template">provider_template</a>, <a href="#stardoc-rule_template">rule_template</a>,
-        <a href="#stardoc-repository_rule_template">repository_rule_template</a>, <a href="#stardoc-module_extension_template">module_extension_template</a>, <a href="#stardoc-use_starlark_doc_extract">use_starlark_doc_extract</a>,
-        <a href="#stardoc-render_main_repo_name">render_main_repo_name</a>, <a href="#stardoc-kwargs">kwargs</a>)
+        <a href="#stardoc-aspect_template">aspect_template</a>, <a href="#stardoc-func_template">func_template</a>, <a href="#stardoc-header_template">header_template</a>, <a href="#stardoc-table_of_contents_template">table_of_contents_template</a>,
+        <a href="#stardoc-provider_template">provider_template</a>, <a href="#stardoc-rule_template">rule_template</a>, <a href="#stardoc-repository_rule_template">repository_rule_template</a>, <a href="#stardoc-module_extension_template">module_extension_template</a>,
+        <a href="#stardoc-use_starlark_doc_extract">use_starlark_doc_extract</a>, <a href="#stardoc-render_main_repo_name">render_main_repo_name</a>, <a href="#stardoc-kwargs">kwargs</a>)
 </pre>
 
 Generates documentation for exported starlark rule definitions in a target starlark file.
@@ -32,6 +32,7 @@
 | <a id="stardoc-aspect_template"></a>aspect_template |  The input file template for generating documentation of aspects   |  `Label("@io_bazel_stardoc//stardoc:templates/markdown_tables/aspect.vm")` |
 | <a id="stardoc-func_template"></a>func_template |  The input file template for generating documentation of functions.   |  `Label("@io_bazel_stardoc//stardoc:templates/markdown_tables/func.vm")` |
 | <a id="stardoc-header_template"></a>header_template |  The input file template for the header of the output documentation.   |  `Label("@io_bazel_stardoc//stardoc:templates/markdown_tables/header.vm")` |
+| <a id="stardoc-table_of_contents_template"></a>table_of_contents_template |  The input file template for the table of contents of the output documentation. This is unset by default for backwards compatibility. Use `Label("@stardoc//stardoc:templates/markdown_tables/table_of_contents.vm")` for the default template.   |  `None` |
 | <a id="stardoc-provider_template"></a>provider_template |  The input file template for generating documentation of providers.   |  `Label("@io_bazel_stardoc//stardoc:templates/markdown_tables/provider.vm")` |
 | <a id="stardoc-rule_template"></a>rule_template |  The input file template for generating documentation of rules.   |  `Label("@io_bazel_stardoc//stardoc:templates/markdown_tables/rule.vm")` |
 | <a id="stardoc-repository_rule_template"></a>repository_rule_template |  The input file template for generating documentation of repository rules. This template is used only when using the native `starlark_doc_extract` rule.   |  `Label("@io_bazel_stardoc//stardoc:templates/markdown_tables/repository_rule.vm")` |
diff --git a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java
index 39b11df..9bf7cf7 100644
--- a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.skydoc.rendering.MarkdownRenderer;
+import com.google.devtools.build.skydoc.rendering.MarkdownRenderer.Renderer;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleExtensionInfo;
@@ -75,6 +76,7 @@
       MarkdownRenderer renderer =
           new MarkdownRenderer(
               rendererOptions.headerTemplateFilePath,
+              rendererOptions.tableOfContentsTemplateFilePath,
               rendererOptions.ruleTemplateFilePath,
               rendererOptions.providerTemplateFilePath,
               rendererOptions.funcTemplateFilePath,
@@ -83,18 +85,78 @@
               rendererOptions.moduleExtensionTemplateFilePath,
               !moduleInfo.getFile().isEmpty() ? moduleInfo.getFile() : "...");
 
+      // rules are printed sorted by their qualified name, and their attributes are sorted by name,
+      // with ATTRIBUTE_ORDERING specifying a fixed sort order for some standard attributes.
+      ImmutableList<RuleInfo> sortedRuleInfos =
+          moduleInfo.getRuleInfoList().stream()
+              .map(RendererMain::withSortedRuleAttributes)
+              .sorted(comparing(RuleInfo::getRuleName))
+              .collect(toImmutableList());
+
+      // providers are printed sorted by their qualified name.
+      ImmutableList<ProviderInfo> sortedProviderInfos =
+          ImmutableList.sortedCopyOf(
+              comparing(ProviderInfo::getProviderName), moduleInfo.getProviderInfoList());
+
+      // functions are printed sorted by their qualified name.
+      ImmutableList<StarlarkFunctionInfo> sortedStarlarkFunctions =
+          ImmutableList.sortedCopyOf(
+              comparing(StarlarkFunctionInfo::getFunctionName), moduleInfo.getFuncInfoList());
+
+      // aspects are printed sorted by their qualified name.
+      ImmutableList<AspectInfo> sortedAspectInfos =
+          ImmutableList.sortedCopyOf(
+              comparing(AspectInfo::getAspectName), moduleInfo.getAspectInfoList());
+
+      // Repository rules are printed sorted by their qualified name, and their attributes are
+      // sorted by name, with ATTRIBUTE_ORDERING specifying a fixed sort order for some standard
+      // attributes.
+      ImmutableList<RepositoryRuleInfo> sortedRepositoryRuleInfos =
+          moduleInfo.getRepositoryRuleInfoList().stream()
+              .map(RendererMain::withSortedRuleAttributes)
+              .sorted(comparing(RepositoryRuleInfo::getRuleName))
+              .collect(toImmutableList());
+
+      // Module extension are printed sorted by their qualified name, and their tag classes'
+      // attributes are sorted by name, with ATTRIBUTE_ORDERING specifying a fixed sort order for
+      // some standard attributes.
+      ImmutableList<ModuleExtensionInfo> sortedModuleExtensionInfos =
+          moduleInfo.getModuleExtensionInfoList().stream()
+              .map(RendererMain::withSortedTagAttributes)
+              .sorted(comparing(ModuleExtensionInfo::getExtensionName))
+              .collect(toImmutableList());
+
       printWriter.println(renderer.renderMarkdownHeader(moduleInfo));
-      printRuleInfos(printWriter, renderer, moduleInfo.getRuleInfoList());
-      printProviderInfos(printWriter, renderer, moduleInfo.getProviderInfoList());
-      printStarlarkFunctions(printWriter, renderer, moduleInfo.getFuncInfoList());
-      printAspectInfos(printWriter, renderer, moduleInfo.getAspectInfoList());
-      printRepositoryRuleInfos(printWriter, renderer, moduleInfo.getRepositoryRuleInfoList());
-      printModuleExtensionInfos(printWriter, renderer, moduleInfo.getModuleExtensionInfoList());
+      if (rendererOptions.tableOfContentsTemplateFilePath != null) {
+        printWriter.println(
+            renderer.renderTableOfContents(
+                sortedRuleInfos,
+                sortedProviderInfos,
+                sortedStarlarkFunctions,
+                sortedAspectInfos,
+                sortedRepositoryRuleInfos,
+                sortedModuleExtensionInfos));
+      }
+      print(printWriter, renderer::render, sortedRuleInfos);
+      print(printWriter, renderer::render, sortedProviderInfos);
+      print(printWriter, renderer::render, sortedStarlarkFunctions);
+      print(printWriter, renderer::render, sortedAspectInfos);
+      print(printWriter, renderer::render, sortedRepositoryRuleInfos);
+      print(printWriter, renderer::render, sortedModuleExtensionInfos);
+
     } catch (InvalidProtocolBufferException e) {
       throw new IllegalArgumentException("Input file is not a valid ModuleInfo proto.", e);
     }
   }
 
+  private static <T> void print(PrintWriter printWriter, Renderer<T> renderer, List<T> infos)
+      throws IOException {
+    for (T info : infos) {
+      printWriter.println(renderer.render(info));
+      printWriter.println();
+    }
+  }
+
   // A copy of com.google.devtools.build.docgen.DocgenConsts.ATTRIBUTE_ORDERING - we duplicate the
   // ordering here because we intend to move this file from the Bazel tree to the Stardoc repo.
   private static final ImmutableMap<String, Integer> ATTRIBUTE_ORDERING =
@@ -167,94 +229,5 @@
         .build();
   }
 
-  private static void printRuleInfos(
-      PrintWriter printWriter, MarkdownRenderer renderer, List<RuleInfo> ruleInfos)
-      throws IOException {
-    // rules are printed sorted by their qualified name, and their attributes are sorted by name,
-    // with ATTRIBUTE_ORDERING specifying a fixed sort order for some standard attributes.
-    ImmutableList<RuleInfo> sortedRuleInfos =
-        ruleInfos.stream()
-            .map(RendererMain::withSortedRuleAttributes)
-            .sorted(comparing(RuleInfo::getRuleName))
-            .collect(toImmutableList());
-    for (RuleInfo ruleProto : sortedRuleInfos) {
-      printWriter.println(renderer.render(ruleProto.getRuleName(), ruleProto));
-      printWriter.println();
-    }
-  }
-
-  private static void printProviderInfos(
-      PrintWriter printWriter, MarkdownRenderer renderer, List<ProviderInfo> providerInfos)
-      throws IOException {
-    // providers are printed sorted by their qualified name.
-    ImmutableList<ProviderInfo> sortedProviderInfos =
-        ImmutableList.sortedCopyOf(comparing(ProviderInfo::getProviderName), providerInfos);
-    for (ProviderInfo providerProto : sortedProviderInfos) {
-      printWriter.println(renderer.render(providerProto.getProviderName(), providerProto));
-      printWriter.println();
-    }
-  }
-
-  private static void printStarlarkFunctions(
-      PrintWriter printWriter,
-      MarkdownRenderer renderer,
-      List<StarlarkFunctionInfo> starlarkFunctions)
-      throws IOException {
-    // functions are printed sorted by their qualified name.
-    ImmutableList<StarlarkFunctionInfo> sortedStarlarkFunctions =
-        ImmutableList.sortedCopyOf(
-            comparing(StarlarkFunctionInfo::getFunctionName), starlarkFunctions);
-    for (StarlarkFunctionInfo funcProto : sortedStarlarkFunctions) {
-      printWriter.println(renderer.render(funcProto));
-      printWriter.println();
-    }
-  }
-
-  private static void printAspectInfos(
-      PrintWriter printWriter, MarkdownRenderer renderer, List<AspectInfo> aspectInfos)
-      throws IOException {
-    // aspects are printed sorted by their qualified name.
-    ImmutableList<AspectInfo> sortedAspectInfos =
-        ImmutableList.sortedCopyOf(comparing(AspectInfo::getAspectName), aspectInfos);
-    for (AspectInfo aspectProto : sortedAspectInfos) {
-      printWriter.println(renderer.render(aspectProto.getAspectName(), aspectProto));
-      printWriter.println();
-    }
-  }
-
-  private static void printRepositoryRuleInfos(
-      PrintWriter printWriter, MarkdownRenderer renderer, List<RepositoryRuleInfo> ruleInfos)
-      throws IOException {
-    // Repository rules are printed sorted by their qualified name, and their attributes are sorted
-    // by name, with ATTRIBUTE_ORDERING specifying a fixed sort order for some standard attributes.
-    ImmutableList<RepositoryRuleInfo> sortedRepositoryRuleInfos =
-        ruleInfos.stream()
-            .map(RendererMain::withSortedRuleAttributes)
-            .sorted(comparing(RepositoryRuleInfo::getRuleName))
-            .collect(toImmutableList());
-    for (RepositoryRuleInfo repositoryRuleProto : sortedRepositoryRuleInfos) {
-      printWriter.println(renderer.render(repositoryRuleProto.getRuleName(), repositoryRuleProto));
-      printWriter.println();
-    }
-  }
-
-  private static void printModuleExtensionInfos(
-      PrintWriter printWriter, MarkdownRenderer renderer, List<ModuleExtensionInfo> ruleInfos)
-      throws IOException {
-    // Module extension are printed sorted by their qualified name, and their tag classes'
-    // attributes are sorted by name, with ATTRIBUTE_ORDERING specifying a fixed sort order for some
-    // standard attributes.
-    ImmutableList<ModuleExtensionInfo> sortedModuleExtensionInfos =
-        ruleInfos.stream()
-            .map(RendererMain::withSortedTagAttributes)
-            .sorted(comparing(ModuleExtensionInfo::getExtensionName))
-            .collect(toImmutableList());
-    for (ModuleExtensionInfo moduleExtensionProto : sortedModuleExtensionInfos) {
-      printWriter.println(
-          renderer.render(moduleExtensionProto.getExtensionName(), moduleExtensionProto));
-      printWriter.println();
-    }
-  }
-
   private RendererMain() {}
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java
index 52ed165..004c428 100644
--- a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java
@@ -40,6 +40,11 @@
   String headerTemplateFilePath;
 
   @Parameter(
+      names = "--table_of_contents_template",
+      description = "The template for the table of contents string")
+  String tableOfContentsTemplateFilePath;
+
+  @Parameter(
       names = "--rule_template",
       required = true,
       description = "The template for the documentation of a rule")
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
index 2ff4a4e..ca07c34 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
@@ -35,11 +35,18 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.List;
 
 /** Produces skydoc output in markdown form. */
 public class MarkdownRenderer {
+
+  public interface Renderer<T> {
+    String render(T info) throws IOException;
+  }
+
   // TODO(kendalllane): Refactor MarkdownRenderer to take in something other than filepaths.
   private final String headerTemplateFilename;
+  private final String tableOfContentsTemplateFilename;
   private final String ruleTemplateFilename;
   private final String providerTemplateFilename;
   private final String functionTemplateFilename;
@@ -50,6 +57,7 @@
 
   public MarkdownRenderer(
       String headerTemplate,
+      String tableOfContentsTemplateFilename,
       String ruleTemplate,
       String providerTemplate,
       String functionTemplate,
@@ -58,6 +66,7 @@
       String moduleExtensionTemplate,
       String extensionBzlFile) {
     this.headerTemplateFilename = headerTemplate;
+    this.tableOfContentsTemplateFilename = tableOfContentsTemplateFilename;
     this.ruleTemplateFilename = ruleTemplate;
     this.providerTemplateFilename = providerTemplate;
     this.functionTemplateFilename = functionTemplate;
@@ -87,13 +96,48 @@
   }
 
   /**
+   * Returns a markdown string of a Table of Contents, appearing after the header and before the
+   * documentation.
+   */
+  public String renderTableOfContents(
+      List<RuleInfo> ruleInfos,
+      List<ProviderInfo> providerInfos,
+      List<StarlarkFunctionInfo> starlarkFunctions,
+      List<AspectInfo> aspectInfos,
+      List<RepositoryRuleInfo> repositoryRuleInfos,
+      List<ModuleExtensionInfo> moduleExtensionInfos)
+      throws IOException {
+
+    ImmutableMap<String, Object> vars =
+        ImmutableMap.of(
+            "util", new MarkdownUtil(extensionBzlFile),
+            "ruleInfos", ruleInfos,
+            "providerInfos", providerInfos,
+            "functionInfos", starlarkFunctions,
+            "aspectInfos", aspectInfos,
+            "repositoryRuleInfos", repositoryRuleInfos,
+            "moduleExtensionInfos", moduleExtensionInfos);
+    Reader reader = readerFromPath(tableOfContentsTemplateFilename);
+    try {
+      return Template.parseFrom(reader).evaluate(vars);
+    } catch (ParseException | EvaluationException e) {
+      throw new IOException(e);
+    }
+  }
+
+  /**
    * 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 {
+  public String render(RuleInfo ruleInfo) throws IOException {
     ImmutableMap<String, Object> vars =
         ImmutableMap.of(
-            "util", new MarkdownUtil(extensionBzlFile), "ruleName", ruleName, "ruleInfo", ruleInfo);
+            "util",
+            new MarkdownUtil(extensionBzlFile),
+            "ruleName",
+            ruleInfo.getRuleName(),
+            "ruleInfo",
+            ruleInfo);
     Reader reader = readerFromPath(ruleTemplateFilename);
     try {
       return Template.parseFrom(reader).evaluate(vars);
@@ -106,13 +150,13 @@
    * Returns a markdown rendering of provider documentation for the given provider information
    * object with the given name.
    */
-  public String render(String providerName, ProviderInfo providerInfo) throws IOException {
+  public String render(ProviderInfo providerInfo) throws IOException {
     ImmutableMap<String, Object> vars =
         ImmutableMap.of(
             "util",
             new MarkdownUtil(extensionBzlFile),
             "providerName",
-            providerName,
+            providerInfo.getProviderName(),
             "providerInfo",
             providerInfo);
     Reader reader = readerFromPath(providerTemplateFilename);
@@ -142,13 +186,13 @@
    * Returns a markdown rendering of aspect documentation for the given aspect information object
    * with the given aspect name.
    */
-  public String render(String aspectName, AspectInfo aspectInfo) throws IOException {
+  public String render(AspectInfo aspectInfo) throws IOException {
     ImmutableMap<String, Object> vars =
         ImmutableMap.of(
             "util",
             new MarkdownUtil(extensionBzlFile),
             "aspectName",
-            aspectName,
+            aspectInfo.getAspectName(),
             "aspectInfo",
             aspectInfo);
     Reader reader = readerFromPath(aspectTemplateFilename);
@@ -163,14 +207,13 @@
    * Returns a markdown rendering of repository rule documentation for the given repository rule
    * information object with the given name.
    */
-  public String render(String repositoryRuleName, RepositoryRuleInfo repositoryRuleInfo)
-      throws IOException {
+  public String render(RepositoryRuleInfo repositoryRuleInfo) throws IOException {
     ImmutableMap<String, Object> vars =
         ImmutableMap.of(
             "util",
             new MarkdownUtil(extensionBzlFile),
             "ruleName",
-            repositoryRuleName,
+            repositoryRuleInfo.getRuleName(),
             "ruleInfo",
             repositoryRuleInfo);
     Reader reader = readerFromPath(repositoryRuleTemplateFilename);
@@ -185,14 +228,13 @@
    * Returns a markdown rendering of module extension documentation for the given module extension
    * information object with the given name.
    */
-  public String render(String moduleExtensionName, ModuleExtensionInfo moduleExtensionInfo)
-      throws IOException {
+  public String render(ModuleExtensionInfo moduleExtensionInfo) throws IOException {
     ImmutableMap<String, Object> vars =
         ImmutableMap.of(
             "util",
             new MarkdownUtil(extensionBzlFile),
             "extensionName",
-            moduleExtensionName,
+            moduleExtensionInfo.getExtensionName(),
             "extensionInfo",
             moduleExtensionInfo);
     Reader reader = readerFromPath(moduleExtensionTemplateFilename);
diff --git a/stardoc/private/stardoc.bzl b/stardoc/private/stardoc.bzl
index 16d1122..1101524 100644
--- a/stardoc/private/stardoc.bzl
+++ b/stardoc/private/stardoc.bzl
@@ -21,27 +21,33 @@
     renderer_args.add("--output=" + str(ctx.outputs.out.path))
     renderer_args.add("--aspect_template=" + str(ctx.file.aspect_template.path))
     renderer_args.add("--header_template=" + str(ctx.file.header_template.path))
+    if ctx.attr.table_of_contents_template:
+        renderer_args.add("--table_of_contents_template=" + str(ctx.file.table_of_contents_template.path))
     renderer_args.add("--func_template=" + str(ctx.file.func_template.path))
     renderer_args.add("--provider_template=" + str(ctx.file.provider_template.path))
     renderer_args.add("--rule_template=" + str(ctx.file.rule_template.path))
     renderer_args.add("--repository_rule_template=" + str(ctx.file.repository_rule_template.path))
     renderer_args.add("--module_extension_template=" + str(ctx.file.module_extension_template.path))
+
+    inputs = [
+        proto_file,
+        ctx.file.aspect_template,
+        ctx.file.header_template,
+        ctx.file.func_template,
+        ctx.file.provider_template,
+        ctx.file.rule_template,
+        ctx.file.repository_rule_template,
+        ctx.file.module_extension_template,
+    ]
+    if ctx.attr.table_of_contents_template:
+        inputs.append(ctx.file.table_of_contents_template)
     renderer = ctx.executable.renderer
     ctx.actions.run(
-        outputs = [out_file],
-        inputs = [
-            proto_file,
-            ctx.file.aspect_template,
-            ctx.file.header_template,
-            ctx.file.func_template,
-            ctx.file.provider_template,
-            ctx.file.rule_template,
-            ctx.file.repository_rule_template,
-            ctx.file.module_extension_template,
-        ],
-        executable = renderer,
         arguments = [renderer_args],
+        executable = renderer,
+        inputs = inputs,
         mnemonic = "Renderer",
+        outputs = [out_file],
         progress_message = ("Converting proto format of %s to markdown format" %
                             (ctx.label.name)),
     )
@@ -114,6 +120,14 @@
         allow_single_file = [".vm"],
         mandatory = True,
     ),
+    "table_of_contents_template": attr.label(
+        doc = "The input file template for the table of contents of the output documentation. " +
+              "This is unset by default for backwards compatibility. Use " +
+              "`Label(\"@stardoc//stardoc:templates/markdown_tables/table_of_contents.vm\")` " +
+              "for the default template.",
+        allow_single_file = [".vm"],
+        mandatory = False,  # Not mandatory for backwards compatibility.
+    ),
     "func_template": attr.label(
         doc = "The input file template for generating documentation of functions.",
         allow_single_file = [".vm"],
diff --git a/stardoc/stardoc.bzl b/stardoc/stardoc.bzl
index 06dbb97..ac312aa 100644
--- a/stardoc/stardoc.bzl
+++ b/stardoc/stardoc.bzl
@@ -32,6 +32,7 @@
         aspect_template = Label("//stardoc:templates/markdown_tables/aspect.vm"),
         func_template = Label("//stardoc:templates/markdown_tables/func.vm"),
         header_template = Label("//stardoc:templates/markdown_tables/header.vm"),
+        table_of_contents_template = None,
         provider_template = Label("//stardoc:templates/markdown_tables/provider.vm"),
         rule_template = Label("//stardoc:templates/markdown_tables/rule.vm"),
         repository_rule_template = Label("//stardoc:templates/markdown_tables/repository_rule.vm"),
@@ -61,6 +62,9 @@
       renderer: The location of the renderer tool.
       aspect_template: The input file template for generating documentation of aspects
       header_template: The input file template for the header of the output documentation.
+      table_of_contents_template: The input file template for the table of contents of the output documentation.
+        This is unset by default for backwards compatibility. Use
+        `Label("@stardoc//stardoc:templates/markdown_tables/table_of_contents.vm")` for the default template.
       func_template: The input file template for generating documentation of functions.
       provider_template: The input file template for generating documentation of providers.
       rule_template: The input file template for generating documentation of rules.
@@ -114,6 +118,7 @@
                 aspect_template = aspect_template,
                 func_template = func_template,
                 header_template = header_template,
+                table_of_contents_template = table_of_contents_template,
                 provider_template = provider_template,
                 rule_template = rule_template,
                 repository_rule_template = repository_rule_template,
diff --git a/stardoc/templates/markdown_tables/table_of_contents.vm b/stardoc/templates/markdown_tables/table_of_contents.vm
new file mode 100644
index 0000000..73d408a
--- /dev/null
+++ b/stardoc/templates/markdown_tables/table_of_contents.vm
@@ -0,0 +1,49 @@
+
+#if (!$ruleInfos.isEmpty())
+#[[##]]# Rules
+
+#foreach ($rule in $ruleInfos)
+- [$rule.ruleName](#$rule.ruleName)
+#end
+
+#end
+#if (!$providerInfos.isEmpty())
+#[[##]]# Providers
+
+#foreach ($providerInfo in $providerInfos)
+- [$providerInfo.providerName](#$providerInfo.providerName)
+#end
+
+#end
+#if (!$functionInfos.isEmpty())
+#[[##]]# Functions
+
+#foreach ($functionInfo in $functionInfos)
+- [$functionInfo.functionName](#$functionInfo.functionName)
+#end
+
+#end
+#if (!$aspectInfos.isEmpty())
+#[[##]]# Aspects
+
+#foreach ($aspectInfo in $aspectInfos)
+- [$aspectInfo.aspectName](#$aspectInfo.aspectName)
+#end
+
+#end
+#if (!$repositoryRuleInfos.isEmpty())
+#[[##]]# Repository Rules
+
+#foreach ($repositoryRuleInfo in $repositoryRuleInfos)
+- [$repositoryRuleInfo.ruleName](#$repositoryRuleInfo.ruleName)
+#end
+
+#end
+#if (!$moduleExtensionInfos.isEmpty())
+#[[##]]# Module Extensions
+
+#foreach ($moduleExtensionInfo in $moduleExtensionInfos)
+- [$moduleExtensionInfo.extensionName](#$moduleExtensionInfo.extensionName)
+#end
+
+#end
diff --git a/test/BUILD b/test/BUILD
index 3c1c284..9f151e5 100644
--- a/test/BUILD
+++ b/test/BUILD
@@ -1,3 +1,4 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
 load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
 load(":stardoc_test.bzl", "self_gen_test", "stardoc_test")
 
@@ -270,6 +271,27 @@
     input_file = "testdata/config_apis_test/input.bzl",
 )
 
+bzl_library(
+    name = "table_of_contents_test_deps",
+    srcs = [
+        "testdata/aspect_test/input.bzl",
+        "testdata/function_basic_test/input.bzl",
+        "testdata/module_extension_test/input.bzl",
+        "testdata/provider_basic_test/input.bzl",
+        "testdata/repo_rules_test/input.bzl",
+        "testdata/simple_test/input.bzl",
+    ],
+)
+
+stardoc_test(
+    name = "table_of_contents_test",
+    golden_file = "testdata/table_of_contents_test/golden.md",
+    input_file = "testdata/table_of_contents_test/input.bzl",
+    table_of_contents_template = "//stardoc:templates/markdown_tables/table_of_contents.vm",
+    test_legacy_extractor = False,
+    deps = [":table_of_contents_test_deps"],
+)
+
 sh_test(
     name = "local_repository_test_e2e_test",
     srcs = ["diff_test_runner.sh"],
diff --git a/test/testdata/table_of_contents_test/golden.md b/test/testdata/table_of_contents_test/golden.md
new file mode 100644
index 0000000..38229b0
--- /dev/null
+++ b/test/testdata/table_of_contents_test/golden.md
@@ -0,0 +1,269 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+
+Test rules / providers / etc for the table of contents generation test.
+
+
+## Rules
+
+- [my_rule](#my_rule)
+
+## Providers
+
+- [MyFooInfo](#MyFooInfo)
+- [MyVeryDocumentedInfo](#MyVeryDocumentedInfo)
+
+## Functions
+
+- [check_sources](#check_sources)
+- [returns_a_thing](#returns_a_thing)
+
+## Aspects
+
+- [my_aspect](#my_aspect)
+- [other_aspect](#other_aspect)
+
+## Repository Rules
+
+- [my_repo](#my_repo)
+
+## Module Extensions
+
+- [my_ext](#my_ext)
+
+
+<a id="my_rule"></a>
+
+## my_rule
+
+<pre>
+my_rule(<a href="#my_rule-name">name</a>, <a href="#my_rule-first">first</a>, <a href="#my_rule-fourth">fourth</a>, <a href="#my_rule-second">second</a>, <a href="#my_rule-third">third</a>)
+</pre>
+
+This is my rule. It does stuff.
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="my_rule-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="my_rule-first"></a>first |  first doc string   | <a href="https://bazel.build/concepts/labels">Label</a> | required |  |
+| <a id="my_rule-fourth"></a>fourth |  fourth doc string   | Boolean | optional |  `False`  |
+| <a id="my_rule-second"></a>second |  -   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
+| <a id="my_rule-third"></a>third |  -   | <a href="https://bazel.build/concepts/labels">Label</a> | required |  |
+
+
+<a id="MyFooInfo"></a>
+
+## MyFooInfo
+
+<pre>
+MyFooInfo(<a href="#MyFooInfo-bar">bar</a>, <a href="#MyFooInfo-baz">baz</a>)
+</pre>
+
+Stores information about a foo.
+
+**FIELDS**
+
+
+| Name  | Description |
+| :------------- | :------------- |
+| <a id="MyFooInfo-bar"></a>bar |  -    |
+| <a id="MyFooInfo-baz"></a>baz |  -    |
+
+
+<a id="MyVeryDocumentedInfo"></a>
+
+## MyVeryDocumentedInfo
+
+<pre>
+MyVeryDocumentedInfo(<a href="#MyVeryDocumentedInfo-favorite_food">favorite_food</a>, <a href="#MyVeryDocumentedInfo-favorite_color">favorite_color</a>)
+</pre>
+
+A provider with some really neat documentation.
+
+Look on my works, ye mighty, and despair!
+
+**FIELDS**
+
+
+| Name  | Description |
+| :------------- | :------------- |
+| <a id="MyVeryDocumentedInfo-favorite_food"></a>favorite_food |  A string representing my favorite food<br><br>Expected to be delicious.    |
+| <a id="MyVeryDocumentedInfo-favorite_color"></a>favorite_color |  A string representing my favorite color    |
+
+
+<a id="check_sources"></a>
+
+## check_sources
+
+<pre>
+check_sources(<a href="#check_sources-name">name</a>, <a href="#check_sources-required_param">required_param</a>, <a href="#check_sources-bool_param">bool_param</a>, <a href="#check_sources-srcs">srcs</a>, <a href="#check_sources-string_param">string_param</a>, <a href="#check_sources-int_param">int_param</a>, <a href="#check_sources-dict_param">dict_param</a>,
+              <a href="#check_sources-struct_param">struct_param</a>)
+</pre>
+
+Runs some checks on the given source files.
+
+This rule runs checks on a given set of source files.
+Use `bazel build` to run the check.
+
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="check_sources-name"></a>name |  A unique name for this rule.   |  none |
+| <a id="check_sources-required_param"></a>required_param |  Use your imagination.   |  none |
+| <a id="check_sources-bool_param"></a>bool_param |  <p align="center"> - </p>   |  `True` |
+| <a id="check_sources-srcs"></a>srcs |  Source files to run the checks against.   |  `[]` |
+| <a id="check_sources-string_param"></a>string_param |  <p align="center"> - </p>   |  `""` |
+| <a id="check_sources-int_param"></a>int_param |  Your favorite number.   |  `2` |
+| <a id="check_sources-dict_param"></a>dict_param |  <p align="center"> - </p>   |  `{}` |
+| <a id="check_sources-struct_param"></a>struct_param |  <p align="center"> - </p>   |  `struct(foo = "bar")` |
+
+
+<a id="returns_a_thing"></a>
+
+## returns_a_thing
+
+<pre>
+returns_a_thing(<a href="#returns_a_thing-name">name</a>)
+</pre>
+
+Returns a suffixed name.
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="returns_a_thing-name"></a>name |  A unique name for this rule.   |  none |
+
+**RETURNS**
+
+A suffixed version of the name.
+
+
+<a id="my_aspect"></a>
+
+## my_aspect
+
+<pre>
+my_aspect(<a href="#my_aspect-name">name</a>, <a href="#my_aspect-first">first</a>, <a href="#my_aspect-second">second</a>)
+</pre>
+
+This is my aspect.
+
+It does stuff.
+
+**ASPECT ATTRIBUTES**
+
+
+| Name | Type |
+| :------------- | :------------- |
+| deps| String |
+| attr_aspect| String |
+
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="my_aspect-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="my_aspect-first"></a>first |  -   | Boolean | required |  |
+| <a id="my_aspect-second"></a>second |  -   | String | required |  |
+
+
+<a id="other_aspect"></a>
+
+## other_aspect
+
+<pre>
+other_aspect(<a href="#other_aspect-name">name</a>, <a href="#other_aspect-third">third</a>)
+</pre>
+
+This is another aspect.
+
+**ASPECT ATTRIBUTES**
+
+
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="other_aspect-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="other_aspect-third"></a>third |  -   | Integer | required |  |
+
+
+<a id="my_repo"></a>
+
+## my_repo
+
+<pre>
+my_repo(<a href="#my_repo-name">name</a>, <a href="#my_repo-repo_mapping">repo_mapping</a>, <a href="#my_repo-useless">useless</a>)
+</pre>
+
+Minimal example of a repository rule.
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="my_repo-name"></a>name |  A unique name for this repository.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="my_repo-repo_mapping"></a>repo_mapping |  In `WORKSPACE` context only: a dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<br><br>For example, an entry `"@foo": "@bar"` declares that, for any time this repository depends on `@foo` (such as a dependency on `@foo//some:target`, it should actually resolve that dependency within globally-declared `@bar` (`@bar//some:target`).<br><br>This attribute is _not_ supported in `MODULE.bazel` context (when invoking a repository rule inside a module extension's implementation function).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional |  |
+| <a id="my_repo-useless"></a>useless |  This argument will be ignored.<br><br>You don't have to specify it, but you may.   | String | optional |  `"ignoreme"`  |
+
+**ENVIRONMENT VARIABLES**
+
+This repository rule depends on the following environment variables:
+
+* `FOO_CC`
+* `BAR_PATH`
+
+
+<a id="my_ext"></a>
+
+## my_ext
+
+<pre>
+my_ext = use_extension("@io_bazel_stardoc//test:testdata/table_of_contents_test/input.bzl", "my_ext")
+my_ext.install(<a href="#my_ext.install-artifacts">artifacts</a>)
+my_ext.artifact(<a href="#my_ext.artifact-artifact">artifact</a>, <a href="#my_ext.artifact-group">group</a>)
+</pre>
+
+Minimal example of a module extension.
+
+
+**TAG CLASSES**
+
+<a id="my_ext.install"></a>
+
+### install
+
+Install tag
+
+**Attributes**
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="my_ext.install-artifacts"></a>artifacts |  Install artifacts   | List of strings | optional |  `[]`  |
+
+<a id="my_ext.artifact"></a>
+
+### artifact
+
+Artifact tag
+
+**Attributes**
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="my_ext.artifact-artifact"></a>artifact |  Artifact   | String | required |  |
+| <a id="my_ext.artifact-group"></a>group |  Group name   | String | optional |  `"my_group"`  |
+
+
diff --git a/test/testdata/table_of_contents_test/input.bzl b/test/testdata/table_of_contents_test/input.bzl
new file mode 100644
index 0000000..5d034b4
--- /dev/null
+++ b/test/testdata/table_of_contents_test/input.bzl
@@ -0,0 +1,26 @@
+"""Test rules / providers / etc for the table of contents generation test."""
+
+load("//test:testdata/aspect_test/input.bzl", _my_aspect = "my_aspect", _other_aspect = "other_aspect")
+load("//test:testdata/function_basic_test/input.bzl", _check_sources = "check_sources", _returns_a_thing = "returns_a_thing")
+load("//test:testdata/module_extension_test/input.bzl", _my_ext = "my_ext")
+load("//test:testdata/provider_basic_test/input.bzl", _MyFooInfo = "MyFooInfo", _MyVeryDocumentedInfo = "MyVeryDocumentedInfo")
+load("//test:testdata/repo_rules_test/input.bzl", _my_repo = "my_repo")
+load("//test:testdata/simple_test/input.bzl", _my_rule = "my_rule")
+
+my_rule = _my_rule
+
+MyFooInfo = _MyFooInfo
+
+MyVeryDocumentedInfo = _MyVeryDocumentedInfo
+
+check_sources = _check_sources
+
+returns_a_thing = _returns_a_thing
+
+my_aspect = _my_aspect
+
+other_aspect = _other_aspect
+
+my_repo = _my_repo
+
+my_ext = _my_ext