Move the markdown renderer from Bazel repo into Stardoc repo (#162)

Moving from https://github.com/bazelbuild/bazel/tree/3d2604806a9747f317c5b560bbf89cdc1be033ba

* src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java and src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java have been changed to use JCommander instead of Bazel's options library for command-line arg parsing. All other .java files have been copied from Bazel without any changes and without any file/directory renames.
* integration tests from src/test/java/com/google/devtools/build/skydoc/testdata in Bazel have been merged into tests/testdata in Stardoc where appropriate (filter_rules_test, function_basic_test, function_wrap_multiple_lines_test, multi_level_namespace_test_with_allowlist, provider_basic_test, pure_markdown_template_test, repo_rules_test)
* updated Stardoc's deps and to allow building the renderer. In particular, we now are forced to depend on rules_jvm_external unconditionally, even in releases.
* removed renderer_binary.jar
diff --git a/BUILD b/BUILD
index a9859d7..85e0841 100644
--- a/BUILD
+++ b/BUILD
@@ -32,6 +32,11 @@
         "CONTRIBUTORS",
         "LICENSE",
         "MODULE.bazel",
+        "maven_install.json",
+        "rules_jvm_external.patch",
+        "//src/main/java/com/google/devtools/build/skydoc/renderer:srcs",
+        "//src/main/java/com/google/devtools/build/skydoc/rendering:srcs",
+        "//src/test/java/com/google/devtools/build/skydoc/rendering:srcs",
         "//stardoc:distro_srcs",
         "//stardoc/private:distro_srcs",
         "//stardoc/proto:distro_srcs",
diff --git a/MODULE.bazel b/MODULE.bazel
index 521a1a9..98866ac 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -5,5 +5,18 @@
 )
 
 bazel_dep(name = "bazel_skylib", version = "1.2.1")
-bazel_dep(name = "rules_java", version = "6.0.0")
-bazel_dep(name = "rules_license", version = "0.0.3")
+bazel_dep(name = "rules_java", version = "6.1.1")
+bazel_dep(name = "rules_jvm_external", version = "4.5")
+bazel_dep(name = "rules_license", version = "0.0.7")
+bazel_dep(name = "protobuf", version = "21.7", repo_name = "com_google_protobuf")
+
+maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
+maven.install(
+    # We don't yet specify the maven coordinates in the MODULE.bazel to avoid duplicating information.
+    # Always respect the maven_install.json file generated by rules_jvm_external from the WORKSPACE file.
+    lock_file = "//:maven_install.json",
+    repositories = [
+        "https://repo1.maven.org/maven2",
+    ],
+)
+use_repo(maven, "maven")
diff --git a/WORKSPACE b/WORKSPACE
index 2191a2c..f2a36f7 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -5,6 +5,88 @@
 
 stardoc_repositories()
 
+RULES_JVM_EXTERNAL_TAG = "4.5"
+
+RULES_JVM_EXTERNAL_SHA = "b17d7388feb9bfa7f2fa09031b32707df529f26c91ab9e5d909eb1676badd9a6"
+
+http_archive(
+    name = "rules_jvm_external",
+    patch_args = ["-p1"],
+    patches = ["//:rules_jvm_external.patch"],
+    sha256 = RULES_JVM_EXTERNAL_SHA,
+    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
+    url = "https://github.com/bazelbuild/rules_jvm_external/archive/refs/tags/%s.zip" % RULES_JVM_EXTERNAL_TAG,
+)
+
+load("@rules_jvm_external//:repositories.bzl", "rules_jvm_external_deps")
+
+rules_jvm_external_deps()
+
+load("@rules_jvm_external//:setup.bzl", "rules_jvm_external_setup")
+
+rules_jvm_external_setup()
+
+load("@rules_jvm_external//:defs.bzl", "maven_install")
+
+maven_install(
+    artifacts = [
+        "com.beust:jcommander:1.82",
+        "com.google.guava:guava:31.1-jre",
+        "com.google.truth:truth:1.1.3",
+        "org.apache.velocity:velocity:1.7",
+        "junit:junit:4.13.2",
+        # Artifacts below this line are only needed for building @io_bazel for
+        # stardoc_binary.jar integration tests. They should be removed once we
+        # fully migrate to starlark_doc_extract after Bazel 7.
+        "com.github.ben-manes.caffeine:caffeine:3.0.5",
+        "com.github.stephenc.jcip:jcip-annotations:1.0-1",
+        "com.google.auto.service:auto-service-annotations:1.0.1",
+        "com.google.auto.service:auto-service:1.0",
+        "com.google.auto.value:auto-value-annotations:1.9",
+        "com.google.auto.value:auto-value:1.8.2",
+        "com.google.auto:auto-common:1.2.1",
+        "com.google.code.findbugs:jsr305:3.0.2",
+        "com.google.code.gson:gson:2.9.0",
+        "com.google.errorprone:error_prone_annotations:2.18.0",
+        "com.google.errorprone:error_prone_type_annotations:2.18.0",
+        "com.google.flogger:flogger-system-backend:0.5.1",
+        "com.google.flogger:flogger:0.5.1",
+        "com.google.flogger:google-extensions:0.5.1",
+        "com.google.guava:failureaccess:1.0.1",
+        "com.google.j2objc:j2objc-annotations:1.3",
+        "com.ryanharter.auto.value:auto-value-gson-extension:1.3.1",
+        "com.ryanharter.auto.value:auto-value-gson-runtime:1.3.1",
+        "com.ryanharter.auto.value:auto-value-gson-factory:1.3.1",
+        "com.squareup:javapoet:1.12.0",
+        "commons-collections:commons-collections:3.2.2",
+        "commons-lang:commons-lang:2.6",
+        "org.apache.tomcat:tomcat-annotations-api:8.0.5",
+        "org.checkerframework:checker-qual:3.19.0",
+    ],
+    fail_if_repin_required = True,
+    maven_install_json = "//:maven_install.json",
+    repositories = [
+        "https://repo1.maven.org/maven2",
+    ],
+    strict_visibility = True,
+)
+
+load("@maven//:defs.bzl", "pinned_maven_install")
+
+pinned_maven_install()
+
+http_archive(
+    name = "com_google_protobuf",
+    sha256 = "75be42bd736f4df6d702a0e4e4d30de9ee40eac024c4b845d17ae4cc831fe4ae",
+    strip_prefix = "protobuf-21.7",
+    urls = [
+        "https://mirror.bazel.build/github.com/protocolbuffers/protobuf/archive/v21.7.tar.gz",
+        "https://github.com/protocolbuffers/protobuf/archive/v21.7.tar.gz",
+    ],
+)
+
+load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
+
 ### INTERNAL ONLY - lines after this are not included in the release packaging.
 #
 # Include dependencies which are only needed for development of Stardoc here.
@@ -34,70 +116,6 @@
     actual = "@io_bazel//third_party:error_prone_annotations",
 )
 
-RULES_JVM_EXTERNAL_TAG = "4.5"
-
-RULES_JVM_EXTERNAL_SHA = "b17d7388feb9bfa7f2fa09031b32707df529f26c91ab9e5d909eb1676badd9a6"
-
-http_archive(
-    name = "rules_jvm_external",
-    patch_args = ["-p1"],
-    patches = ["//:rules_jvm_external.patch"],
-    sha256 = RULES_JVM_EXTERNAL_SHA,
-    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
-    url = "https://github.com/bazelbuild/rules_jvm_external/archive/refs/tags/%s.zip" % RULES_JVM_EXTERNAL_TAG,
-)
-
-load("@rules_jvm_external//:repositories.bzl", "rules_jvm_external_deps")
-
-rules_jvm_external_deps()
-
-load("@rules_jvm_external//:setup.bzl", "rules_jvm_external_setup")
-
-rules_jvm_external_setup()
-
-load("@rules_jvm_external//:defs.bzl", "maven_install")
-
-maven_install(
-    artifacts = [
-        "com.github.ben-manes.caffeine:caffeine:3.0.5",
-        "com.github.stephenc.jcip:jcip-annotations:1.0-1",
-        "com.google.auto.service:auto-service-annotations:1.0.1",
-        "com.google.auto.service:auto-service:1.0",
-        "com.google.auto.value:auto-value-annotations:1.9",
-        "com.google.auto.value:auto-value:1.8.2",
-        "com.google.auto:auto-common:1.2.1",
-        "com.google.code.findbugs:jsr305:3.0.2",
-        "com.google.code.gson:gson:2.9.0",
-        "com.google.errorprone:error_prone_annotations:2.18.0",
-        "com.google.errorprone:error_prone_type_annotations:2.18.0",
-        "com.google.flogger:flogger-system-backend:0.5.1",
-        "com.google.flogger:flogger:0.5.1",
-        "com.google.flogger:google-extensions:0.5.1",
-        "com.google.guava:failureaccess:1.0.1",
-        "com.google.guava:guava:31.1-jre",
-        "com.google.j2objc:j2objc-annotations:1.3",
-        "com.ryanharter.auto.value:auto-value-gson-extension:1.3.1",
-        "com.ryanharter.auto.value:auto-value-gson-runtime:1.3.1",
-        "com.ryanharter.auto.value:auto-value-gson-factory:1.3.1",
-        "com.squareup:javapoet:1.12.0",
-        "commons-collections:commons-collections:3.2.2",
-        "commons-lang:commons-lang:2.6",
-        "org.apache.tomcat:tomcat-annotations-api:8.0.5",
-        "org.apache.velocity:velocity:1.7",
-        "org.checkerframework:checker-qual:3.19.0",
-    ],
-    fail_if_repin_required = True,
-    maven_install_json = "//:maven_install.json",
-    repositories = [
-        "https://repo1.maven.org/maven2",
-    ],
-    strict_visibility = True,
-)
-
-load("@maven//:defs.bzl", "pinned_maven_install")
-
-pinned_maven_install()
-
 # Needed only because of java_tools.
 http_archive(
     name = "rules_cc",
@@ -152,3 +170,7 @@
 rules_proto_dependencies()
 
 rules_proto_toolchains()
+
+### END INTERNAL ONLY
+# protobuf_deps() must not be called before @rules_python are loaded (if they are loaded).
+protobuf_deps()
diff --git a/distro/BUILD b/distro/BUILD
index 84a670e..4fc0e1c 100644
--- a/distro/BUILD
+++ b/distro/BUILD
@@ -15,7 +15,7 @@
     name = "distro_workspace",
     srcs = ["//:WORKSPACE"],
     outs = ["WORKSPACE"],
-    cmd = "sed -e '/### INTERNAL ONLY/,$$d' $(location //:WORKSPACE) >$@",
+    cmd = "sed -e '/### INTERNAL ONLY/,/### END INTERNAL ONLY/d' $(location //:WORKSPACE) >$@",
 )
 
 pkg_tar(
diff --git a/docs/maintainers_guide.md b/docs/maintainers_guide.md
index c4358cc..fabd533 100644
--- a/docs/maintainers_guide.md
+++ b/docs/maintainers_guide.md
@@ -1,15 +1,14 @@
 # Stardoc Maintainer's Guide
 
-## Updating Jars
+## Updating Bundled Jar
 
 Stardoc's source code currently lives in the Bazel source tree at
 https://github.com/bazelbuild/bazel/tree/master/src/main/java/com/google/devtools/build/skydoc
 
-For simplicity of use and building, Stardoc bundles two pre-built jars built
-from Bazel source: `stardoc_binary.jar` (emits protobuf documentation format)
-and `renderer_binary.jar` (turns the protobuf into markdown).
+For simplicity of use and building, Stardoc bundles a pre-built jar built
+from Bazel source: `stardoc_binary.jar` (emits protobuf documentation format).
 
-To update the jars:
+To update the jar:
 
 1.  Update `io_bazel` repo commit in `WORKSPACE`. Update transitive deps in
     `WORKSPACE` as needed.
diff --git a/maven_install.json b/maven_install.json
index 0ff1d50..0ed8554 100644
--- a/maven_install.json
+++ b/maven_install.json
@@ -1,11 +1,31 @@
 {
     "dependency_tree": {
         "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
-        "__INPUT_ARTIFACTS_HASH": 1429940565,
-        "__RESOLVED_ARTIFACTS_HASH": 1065334191,
+        "__INPUT_ARTIFACTS_HASH": -568257785,
+        "__RESOLVED_ARTIFACTS_HASH": -1924638958,
         "conflict_resolution": {},
         "dependencies": [
             {
+                "coord": "com.beust:jcommander:1.82",
+                "dependencies": [],
+                "directDependencies": [],
+                "file": "v1/https/repo1.maven.org/maven2/com/beust/jcommander/1.82/jcommander-1.82.jar",
+                "mirror_urls": [
+                    "https://repo1.maven.org/maven2/com/beust/jcommander/1.82/jcommander-1.82.jar"
+                ],
+                "packages": [
+                    "com.beust.ah",
+                    "com.beust.jcommander",
+                    "com.beust.jcommander.converters",
+                    "com.beust.jcommander.defaultprovider",
+                    "com.beust.jcommander.internal",
+                    "com.beust.jcommander.parser",
+                    "com.beust.jcommander.validators"
+                ],
+                "sha256": "deeac157c8de6822878d85d0c7bc8467a19cc8484d37788f7804f039dde280b1",
+                "url": "https://repo1.maven.org/maven2/com/beust/jcommander/1.82/jcommander-1.82.jar"
+            },
+            {
                 "coord": "com.github.ben-manes.caffeine:caffeine:3.0.5",
                 "dependencies": [
                     "com.google.errorprone:error_prone_annotations:2.18.0",
@@ -367,6 +387,35 @@
                 "url": "https://repo1.maven.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar"
             },
             {
+                "coord": "com.google.truth:truth:1.1.3",
+                "dependencies": [
+                    "com.google.auto.value:auto-value-annotations:1.9",
+                    "com.google.errorprone:error_prone_annotations:2.18.0",
+                    "com.google.guava:guava:31.1-jre",
+                    "junit:junit:4.13.2",
+                    "org.checkerframework:checker-qual:3.19.0",
+                    "org.hamcrest:hamcrest-core:1.3",
+                    "org.ow2.asm:asm:9.1"
+                ],
+                "directDependencies": [
+                    "com.google.auto.value:auto-value-annotations:1.9",
+                    "com.google.errorprone:error_prone_annotations:2.18.0",
+                    "com.google.guava:guava:31.1-jre",
+                    "junit:junit:4.13.2",
+                    "org.checkerframework:checker-qual:3.19.0",
+                    "org.ow2.asm:asm:9.1"
+                ],
+                "file": "v1/https/repo1.maven.org/maven2/com/google/truth/truth/1.1.3/truth-1.1.3.jar",
+                "mirror_urls": [
+                    "https://repo1.maven.org/maven2/com/google/truth/truth/1.1.3/truth-1.1.3.jar"
+                ],
+                "packages": [
+                    "com.google.common.truth"
+                ],
+                "sha256": "fc0b67782289a2aabfddfdf99eff1dcd5edc890d49143fcd489214b107b8f4f3",
+                "url": "https://repo1.maven.org/maven2/com/google/truth/truth/1.1.3/truth-1.1.3.jar"
+            },
+            {
                 "coord": "com.ryanharter.auto.value:auto-value-gson-extension:1.3.1",
                 "dependencies": [
                     "com.google.auto.value:auto-value-annotations:1.9",
@@ -552,6 +601,55 @@
                 "url": "https://repo1.maven.org/maven2/io/sweers/autotransient/autotransient/1.0.0/autotransient-1.0.0.jar"
             },
             {
+                "coord": "junit:junit:4.13.2",
+                "dependencies": [
+                    "org.hamcrest:hamcrest-core:1.3"
+                ],
+                "directDependencies": [
+                    "org.hamcrest:hamcrest-core:1.3"
+                ],
+                "file": "v1/https/repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar",
+                "mirror_urls": [
+                    "https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar"
+                ],
+                "packages": [
+                    "junit.extensions",
+                    "junit.framework",
+                    "junit.runner",
+                    "junit.textui",
+                    "org.junit",
+                    "org.junit.experimental",
+                    "org.junit.experimental.categories",
+                    "org.junit.experimental.max",
+                    "org.junit.experimental.results",
+                    "org.junit.experimental.runners",
+                    "org.junit.experimental.theories",
+                    "org.junit.experimental.theories.internal",
+                    "org.junit.experimental.theories.suppliers",
+                    "org.junit.function",
+                    "org.junit.internal",
+                    "org.junit.internal.builders",
+                    "org.junit.internal.management",
+                    "org.junit.internal.matchers",
+                    "org.junit.internal.requests",
+                    "org.junit.internal.runners",
+                    "org.junit.internal.runners.model",
+                    "org.junit.internal.runners.rules",
+                    "org.junit.internal.runners.statements",
+                    "org.junit.matchers",
+                    "org.junit.rules",
+                    "org.junit.runner",
+                    "org.junit.runner.manipulation",
+                    "org.junit.runner.notification",
+                    "org.junit.runners",
+                    "org.junit.runners.model",
+                    "org.junit.runners.parameterized",
+                    "org.junit.validator"
+                ],
+                "sha256": "8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3",
+                "url": "https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar"
+            },
+            {
                 "coord": "org.apache.tomcat:tomcat-annotations-api:8.0.5",
                 "dependencies": [],
                 "directDependencies": [],
@@ -670,6 +768,37 @@
                 ],
                 "sha256": "a827c49183f3a632277d27a0a4673686cb341507447b9d570261094bd748aa68",
                 "url": "https://repo1.maven.org/maven2/org/checkerframework/checker-qual/3.19.0/checker-qual-3.19.0.jar"
+            },
+            {
+                "coord": "org.hamcrest:hamcrest-core:1.3",
+                "dependencies": [],
+                "directDependencies": [],
+                "file": "v1/https/repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar",
+                "mirror_urls": [
+                    "https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar"
+                ],
+                "packages": [
+                    "org.hamcrest",
+                    "org.hamcrest.core",
+                    "org.hamcrest.internal"
+                ],
+                "sha256": "66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9",
+                "url": "https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar"
+            },
+            {
+                "coord": "org.ow2.asm:asm:9.1",
+                "dependencies": [],
+                "directDependencies": [],
+                "file": "v1/https/repo1.maven.org/maven2/org/ow2/asm/asm/9.1/asm-9.1.jar",
+                "mirror_urls": [
+                    "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.1/asm-9.1.jar"
+                ],
+                "packages": [
+                    "org.objectweb.asm",
+                    "org.objectweb.asm.signature"
+                ],
+                "sha256": "cda4de455fab48ff0bcb7c48b4639447d4de859a7afc30a094a986f0936beba2",
+                "url": "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.1/asm-9.1.jar"
             }
         ],
         "version": "0.1.0"
diff --git a/setup.bzl b/setup.bzl
index 720c5ce..9797de6 100644
--- a/setup.bzl
+++ b/setup.bzl
@@ -35,18 +35,17 @@
         http_archive,
         name = "rules_java",
         urls = [
-            "https://mirror.bazel.build/github.com/bazelbuild/rules_java/releases/download/6.0.0/rules_java-6.0.0.tar.gz",
-            "https://github.com/bazelbuild/rules_java/releases/download/6.0.0/rules_java-6.0.0.tar.gz",
+            "https://mirror.bazel.build/github.com/bazelbuild/rules_java/releases/download/6.1.1/rules_java-6.1.1.tar.gz",
+            "https://github.com/bazelbuild/rules_java/releases/download/6.1.1/rules_java-6.1.1.tar.gz",
         ],
-        sha256 = "469b7f3b580b4fcf8112f4d6d0d5a4ce8e1ad5e21fee67d8e8335d5f8b3debab",
+        sha256 = "76402a50ae6859d50bd7aed8c1b8ef09dae5c1035bb3ca7d276f7f3ce659818a",
     )
     _include_if_not_defined(
         http_archive,
         name = "rules_license",
-        # TODO: Bazel@HEAD fails in the analysis phase with rules_license 0.0.4.
         urls = [
-            "https://mirror.bazel.build/github.com/bazelbuild/rules_license/releases/download/0.0.3/rules_license-0.0.3.tar.gz",
-            "https://github.com/bazelbuild/rules_license/releases/download/0.0.3/rules_license-0.0.3.tar.gz",
+            "https://mirror.bazel.build/github.com/bazelbuild/rules_license/releases/download/0.0.7/rules_license-0.0.7.tar.gz",
+            "https://github.com/bazelbuild/rules_license/releases/download/0.0.7/rules_license-0.0.7.tar.gz",
         ],
-        sha256 = "00ccc0df21312c127ac4b12880ab0f9a26c1cff99442dc6c5a331750360de3c3",
+        sha256 = "4531deccb913639c30e5c7512a054d5d875698daeb75d8cf90f284375fe7c360",
     )
diff --git a/src/main/java/com/google/devtools/build/skydoc/renderer/BUILD b/src/main/java/com/google/devtools/build/skydoc/renderer/BUILD
new file mode 100644
index 0000000..db17567
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/BUILD
@@ -0,0 +1,45 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_library")
+
+package(
+    default_applicable_licenses = ["//:license"],
+    default_visibility = [
+        "//src:__subpackages__",
+        "//stardoc:__subpackages__",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+    visibility = ["//:__pkg__"],
+)
+
+java_binary(
+    name = "renderer",
+    jvm_flags = [
+        # quiet warnings from com.google.protobuf.UnsafeUtil,
+        # see: https://github.com/google/protobuf/issues/3781
+        # and: https://github.com/bazelbuild/bazel/issues/5599
+        "--add-opens=java.base/java.nio=ALL-UNNAMED",
+        "--add-opens=java.base/java.lang=ALL-UNNAMED",
+        # ... but only on JDK >= 9
+        "-XX:+IgnoreUnrecognizedVMOptions",
+    ],
+    main_class = "com.google.devtools.build.skydoc.renderer.RendererMain",
+    visibility = ["//visibility:public"],
+    runtime_deps = [
+        ":renderer_lib",
+    ],
+)
+
+java_library(
+    name = "renderer_lib",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/main/java/com/google/devtools/build/skydoc/rendering",
+        "//stardoc/proto:stardoc_output_java_proto",
+        "@com_google_protobuf//:protobuf_java",
+        "@maven//:com_beust_jcommander",
+        "@maven//:com_google_guava_guava",
+    ],
+)
diff --git a/src/main/java/com/google/devtools/build/skydoc/renderer/FileSystemAccessor.java b/src/main/java/com/google/devtools/build/skydoc/renderer/FileSystemAccessor.java
new file mode 100644
index 0000000..3e424d8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/FileSystemAccessor.java
@@ -0,0 +1,45 @@
+// Copyright 2019 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.renderer;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/** Implementation of {@link ProtoFileAccessor} which uses the real filesystem. */
+public class FileSystemAccessor implements ProtoFileAccessor {
+
+  @Override
+  public byte[] getProtoContent(String inputPathString) throws IOException {
+    Path inputPath = Paths.get(inputPathString);
+    byte[] inputContent = Files.readAllBytes(inputPath);
+    return inputContent;
+  }
+
+  @Override
+  public boolean fileExists(String pathString) {
+    return Files.exists(Paths.get(pathString));
+  }
+
+  @Override
+  public void writeToOutputLocation(String outputPathString, byte[] content) throws IOException {
+    try (FileOutputStream outputStream = new FileOutputStream(outputPathString)) {
+      for (byte byteContent : content) {
+        outputStream.write(byteContent);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/renderer/ProtoFileAccessor.java b/src/main/java/com/google/devtools/build/skydoc/renderer/ProtoFileAccessor.java
new file mode 100644
index 0000000..3b75d9b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/ProtoFileAccessor.java
@@ -0,0 +1,42 @@
+// Copyright 2019 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.renderer;
+
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Helper to handle Proto file I/O. This abstraction is useful for tests which don't involve actual
+ * file I/O.
+ */
+public interface ProtoFileAccessor {
+  /**
+   * Returns the bytes from the raw proto file.
+   *
+   * @param inputPathString the path of the input raw {@link StardocOutputProtos} file.
+   */
+  byte[] getProtoContent(String inputPathString) throws IOException;
+
+  /** Returns true if a file exists at the current path. */
+  boolean fileExists(String pathString);
+
+  /**
+   * Creates a {@link FileOutputStream} and writes the bytes to the output location.
+   *
+   * @param outputPathString the output location that is being written to
+   * @param content the bytes from input proto file
+   */
+  void writeToOutputLocation(String outputPathString, byte[] content) throws IOException;
+}
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
new file mode 100644
index 0000000..55cdd84
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java
@@ -0,0 +1,184 @@
+// Copyright 2019 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.renderer;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.comparing;
+
+import com.beust.jcommander.JCommander;
+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.proto.StardocOutputProtos.AspectInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.StarlarkFunctionInfo;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Main entry point for Renderer binary.
+ *
+ * <p>This Renderer takes in raw stardoc_proto protos as input and produces rich markdown output.
+ */
+public class RendererMain {
+
+  public static void main(String[] args) throws IOException {
+
+    RendererOptions rendererOptions = new RendererOptions();
+    JCommander jcommander =
+        JCommander.newBuilder().addObject(rendererOptions).build();
+    jcommander.setProgramName("renderer");
+    jcommander.parse(args);
+    if (rendererOptions.printHelp) {
+      jcommander.usage();
+      return;
+    }
+
+    String inputPath = rendererOptions.inputPath;
+    String outputPath = rendererOptions.outputFilePath;
+    String headerTemplatePath = rendererOptions.headerTemplateFilePath;
+    String ruleTemplatePath = rendererOptions.ruleTemplateFilePath;
+    String providerTemplatePath = rendererOptions.providerTemplateFilePath;
+    String funcTemplatePath = rendererOptions.funcTemplateFilePath;
+    String aspectTemplatePath = rendererOptions.aspectTemplateFilePath;
+
+    MarkdownRenderer renderer =
+        new MarkdownRenderer(
+            headerTemplatePath,
+            ruleTemplatePath,
+            providerTemplatePath,
+            funcTemplatePath,
+            aspectTemplatePath);
+    try (PrintWriter printWriter =
+        new PrintWriter(outputPath, UTF_8) {
+          // Use consistent line endings on all platforms.
+          @Override
+          public void println() {
+            write("\n");
+          }
+        }) {
+      ModuleInfo moduleInfo = ModuleInfo.parseFrom(new FileInputStream(inputPath));
+      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());
+    } catch (InvalidProtocolBufferException e) {
+      throw new IllegalArgumentException("Input file is not a valid ModuleInfo proto.", e);
+    }
+  }
+
+  // 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 =
+      ImmutableMap.<String, Integer>builder()
+          .put("name", -99)
+          .put("deps", -98)
+          .put("src", -97)
+          .put("srcs", -96)
+          .put("data", -95)
+          .put("resource", -94)
+          .put("resources", -93)
+          .put("out", -92)
+          .put("outs", -91)
+          .put("hdrs", -90)
+          .buildOrThrow();
+
+  private static final Comparator<String> ATTRIBUTE_NAME_COMPARATOR =
+      (a, b) -> {
+        int aOrdering = ATTRIBUTE_ORDERING.getOrDefault(a, 0);
+        int bOrdering = ATTRIBUTE_ORDERING.getOrDefault(b, 0);
+        if (aOrdering > bOrdering) {
+          return 1;
+        } else if (aOrdering < bOrdering) {
+          return -1;
+        } else {
+          return Comparator.<String>naturalOrder().compare(a, b);
+        }
+      };
+
+  private static RuleInfo withSortedRuleAttributes(RuleInfo ruleInfo) {
+    return ruleInfo.toBuilder()
+        .clearAttribute()
+        .addAllAttribute(
+            ImmutableList.sortedCopyOf(
+                comparing(AttributeInfo::getName, ATTRIBUTE_NAME_COMPARATOR),
+                ruleInfo.getAttributeList()))
+        .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();
+    }
+  }
+}
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
new file mode 100644
index 0000000..5abef40
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java
@@ -0,0 +1,71 @@
+// Copyright 2023 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.renderer;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+
+/** Contains options for running {@link RendererMain}. */
+@Parameters(separators = "=")
+class RendererOptions {
+
+  @Parameter(
+      names = "--input",
+      required = true,
+      description = "The path of the proto file that will be converted to markdown")
+  String inputPath;
+
+  @Parameter(
+      names = "--output",
+      required = true,
+      description = "The path of the file to output documentation into")
+  String outputFilePath;
+
+  @Parameter(
+      names = "--header_template",
+      required = true,
+      description = "The template for the header string")
+  String headerTemplateFilePath;
+
+  @Parameter(
+      names = "--rule_template",
+      required = true,
+      description = "The template for the documentation of a rule")
+  String ruleTemplateFilePath;
+
+  @Parameter(
+      names = "--provider_template",
+      required = true,
+      description = "The template for the documentation of a provider")
+  String providerTemplateFilePath;
+
+  @Parameter(
+      names = "--func_template",
+      required = true,
+      description = "The template for the documentation of a function")
+  String funcTemplateFilePath;
+
+  @Parameter(
+      names = "--aspect_template",
+      required = true,
+      description = "The template for the documentation of an aspect")
+  String aspectTemplateFilePath;
+
+  @Parameter(
+      names = {"--help", "-h"},
+      description = "Print help and exit",
+      help = true)
+  boolean printHelp;
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
new file mode 100644
index 0000000..92c9021
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
@@ -0,0 +1,27 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(
+    default_applicable_licenses = ["//:license"],
+    default_visibility = [
+        "//src:__subpackages__",
+        "//stardoc:__subpackages__",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+    visibility = ["//:__pkg__"],
+)
+
+java_library(
+    name = "rendering",
+    srcs = glob(
+        ["*.java"],
+    ),
+    deps = [
+        "//stardoc/proto:stardoc_output_java_proto",
+        "@maven//:com_google_guava_guava",
+        "@maven//:org_apache_velocity_velocity",
+    ],
+)
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..3a2085c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
@@ -0,0 +1,191 @@
+// 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.StarlarkFunctionInfo;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+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 {
+  // TODO(kendalllane): Refactor MarkdownRenderer to take in something other than filepaths.
+  private final String headerTemplateFilename;
+  private final String ruleTemplateFilename;
+  private final String providerTemplateFilename;
+  private final String functionTemplateFilename;
+  private final String aspectTemplateFilename;
+
+  private final VelocityEngine velocityEngine;
+
+  public MarkdownRenderer(
+      String headerTemplate,
+      String ruleTemplate,
+      String providerTemplate,
+      String functionTemplate,
+      String aspectTemplate) {
+    this.headerTemplateFilename = headerTemplate;
+    this.ruleTemplateFilename = ruleTemplate;
+    this.providerTemplateFilename = providerTemplate;
+    this.functionTemplateFilename = functionTemplate;
+    this.aspectTemplateFilename = aspectTemplate;
+
+    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);
+
+    // Ensure formatting is the same on Velocity 1.7 and 2.x.
+    velocityEngine.setProperty("parser.space_gobbling", "bc");
+  }
+
+  /**
+   * Returns a markdown header string that should appear at the top of Stardoc's output, providing a
+   * summary for the input Starlark module.
+   */
+  public String renderMarkdownHeader(ModuleInfo moduleInfo) throws IOException {
+    VelocityContext context = new VelocityContext();
+    context.put("util", new MarkdownUtil());
+    context.put("moduleDocstring", moduleInfo.getModuleDocstring());
+
+    StringWriter stringWriter = new StringWriter();
+    Reader reader = readerFromPath(headerTemplateFilename);
+    try {
+      velocityEngine.evaluate(context, stringWriter, headerTemplateFilename, reader);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+
+  /**
+   * 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();
+    context.put("util", new MarkdownUtil());
+    context.put("ruleName", ruleName);
+    context.put("ruleInfo", ruleInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    Reader reader = readerFromPath(ruleTemplateFilename);
+    try {
+      velocityEngine.evaluate(context, stringWriter, ruleTemplateFilename, reader);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+
+  /**
+   * 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 {
+    VelocityContext context = new VelocityContext();
+    context.put("util", new MarkdownUtil());
+    context.put("providerName", providerName);
+    context.put("providerInfo", providerInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    Reader reader = readerFromPath(providerTemplateFilename);
+    try {
+      velocityEngine.evaluate(context, stringWriter, providerTemplateFilename, reader);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+
+  /**
+   * Returns a markdown rendering of a user-defined function's documentation for the function info
+   * object.
+   */
+  public String render(StarlarkFunctionInfo functionInfo) throws IOException {
+    VelocityContext context = new VelocityContext();
+    context.put("util", new MarkdownUtil());
+    context.put("funcInfo", functionInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    Reader reader = readerFromPath(functionTemplateFilename);
+    try {
+      velocityEngine.evaluate(context, stringWriter, functionTemplateFilename, reader);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+
+  /**
+   * 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 {
+    VelocityContext context = new VelocityContext();
+    context.put("util", new MarkdownUtil());
+    context.put("aspectName", aspectName);
+    context.put("aspectInfo", aspectInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    Reader reader = readerFromPath(aspectTemplateFilename);
+    try {
+      velocityEngine.evaluate(context, stringWriter, aspectTemplateFilename, reader);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+  /**
+   * Returns a reader from the given path.
+   *
+   * @param filePath The given path, either a filesystem path or a java Resource
+   */
+  private static Reader readerFromPath(String filePath) throws IOException {
+    if (Files.exists(Paths.get(filePath))) {
+      Path path = Paths.get(filePath);
+      return Files.newBufferedReader(path);
+    }
+
+    InputStream inputStream = MarkdownRenderer.class.getClassLoader().getResourceAsStream(filePath);
+    if (inputStream == null) {
+      throw new FileNotFoundException(filePath + " was not found as a resource.");
+    }
+    return new InputStreamReader(inputStream, UTF_8);
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java
new file mode 100644
index 0000000..4dd6310
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java
@@ -0,0 +1,307 @@
+// 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 static java.util.Comparator.naturalOrder;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+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.AttributeType;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.FunctionParamInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderNameGroup;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.StarlarkFunctionInfo;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/** Contains a number of utility methods for markdown rendering. */
+public final class MarkdownUtil {
+  private static final int MAX_LINE_LENGTH = 100;
+
+  /**
+   * Return a string that formats the input string so it is displayable in a markdown table cell.
+   * This performs the following operations:
+   *
+   * <ul>
+   *   <li>Trims the string of leading/trailing whitespace.
+   *   <li>Transforms the string using {@link #htmlEscape}.
+   *   <li>Transforms multline code (```) tags into preformatted code HTML tags.
+   *   <li>Transforms single-tick code (`) tags into code HTML tags.
+   *   <li>Transforms 'new paraphgraph' patterns (two or more sequential newline characters) into
+   *       line break HTML tags.
+   *   <li>Turns lingering new line tags into spaces (as they generally indicate intended line wrap.
+   * </ul>
+   */
+  public String markdownCellFormat(String docString) {
+    String resultString = htmlEscape(docString.trim());
+
+    resultString = replaceWithTag(resultString, "```", "<pre><code>", "</code></pre>");
+    resultString = replaceWithTag(resultString, "`", "<code>", "</code>");
+
+    return resultString.replaceAll("\n(\\s*\n)+", "<br><br>").replace('\n', ' ');
+  }
+
+  private static String replaceWithTag(
+      String wholeString, String stringToReplace, String openTag, String closeTag) {
+    String remainingString = wholeString;
+    StringBuilder resultString = new StringBuilder();
+
+    boolean openTagNext = true;
+    int index = remainingString.indexOf(stringToReplace);
+    while (index > -1) {
+      resultString.append(remainingString, 0, index);
+      resultString.append(openTagNext ? openTag : closeTag);
+      openTagNext = !openTagNext;
+      remainingString = remainingString.substring(index + stringToReplace.length());
+      index = remainingString.indexOf(stringToReplace);
+    }
+    resultString.append(remainingString);
+    return resultString.toString();
+  }
+
+  /**
+   * Return a string that escapes angle brackets for HTML.
+   *
+   * <p>For example: 'Information with <brackets>.' becomes 'Information with &lt;brackets&gt;'.
+   */
+  public String htmlEscape(String docString) {
+    return docString.replace("<", "&lt;").replace(">", "&gt;");
+  }
+
+  private static final Pattern CONSECUTIVE_BACKTICKS = Pattern.compile("`+");
+
+  /**
+   * Returns a Markdown code span (e.g. {@code `return foo;`}) that contains the given literal text,
+   * which may itself contain backticks.
+   *
+   * <p>For example:
+   *
+   * <ul>
+   *   <li>{@code markdownCodeSpan("foo")} returns {@code "`foo`"}
+   *   <li>{@code markdownCodeSpan("fo`o")} returns {@code "``fo`o``"}
+   *   <li>{@code markdownCodeSpan("`foo`")} returns {@code "`` foo` ``""}
+   * </ul>
+   */
+  public static String markdownCodeSpan(String code) {
+    // https://github.github.com/gfm/#code-span
+    int numConsecutiveBackticks =
+        CONSECUTIVE_BACKTICKS
+            .matcher(code)
+            .results()
+            .map(match -> match.end() - match.start())
+            .max(naturalOrder())
+            .orElse(0);
+    String padding = code.startsWith("`") || code.endsWith("`") ? " " : "";
+    return String.format(
+        "%1$s%2$s%3$s%2$s%1$s", "`".repeat(numConsecutiveBackticks + 1), padding, code);
+  }
+
+  /**
+   * Return a string representing the rule summary for the given rule with the given name.
+   *
+   * For example: 'my_rule(foo, bar)'.
+   * The summary will contain hyperlinks for each attribute.
+   */
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String ruleSummary(String ruleName, RuleInfo ruleInfo) {
+    List<String> attributeNames =
+        ruleInfo.getAttributeList().stream()
+            .map(AttributeInfo::getName)
+            .collect(Collectors.toList());
+    return summary(ruleName, attributeNames);
+  }
+
+  /**
+   * Return a string representing the summary for the given provider with the given name.
+   *
+   * For example: 'MyInfo(foo, bar)'.
+   * The summary will contain hyperlinks for each field.
+   */
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String providerSummary(String providerName, ProviderInfo providerInfo) {
+    List<String> fieldNames =
+        providerInfo.getFieldInfoList().stream()
+            .map(field -> field.getName())
+            .collect(Collectors.toList());
+    return summary(providerName, fieldNames);
+  }
+
+  /**
+   * Return a string representing the aspect summary for the given aspect with the given name.
+   *
+   * <p>For example: 'my_aspect(foo, bar)'. The summary will contain hyperlinks for each attribute.
+   */
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String aspectSummary(String aspectName, AspectInfo aspectInfo) {
+    List<String> attributeNames =
+        aspectInfo.getAttributeList().stream()
+            .map(AttributeInfo::getName)
+            .collect(Collectors.toList());
+    return summary(aspectName, attributeNames);
+  }
+
+  /**
+   * Return a string representing the summary for the given user-defined function.
+   *
+   * <p>For example: 'my_func(foo, bar)'. The summary will contain hyperlinks for each parameter.
+   */
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String funcSummary(StarlarkFunctionInfo funcInfo) {
+    List<String> paramNames =
+        funcInfo.getParameterList().stream()
+            .map(FunctionParamInfo::getName)
+            .collect(Collectors.toList());
+    return summary(funcInfo.getFunctionName(), paramNames);
+  }
+
+  private static String summary(String functionName, List<String> paramNames) {
+    List<List<String>> paramLines = wrap(functionName, paramNames, MAX_LINE_LENGTH);
+    List<String> paramLinksLines = new ArrayList<>();
+    for (List<String> params : paramLines) {
+      String paramLinksLine =
+          params.stream()
+              .map(param -> String.format("<a href=\"#%s-%s\">%s</a>", functionName, param, param))
+              .collect(Collectors.joining(", "));
+      paramLinksLines.add(paramLinksLine);
+    }
+    String paramList =
+        Joiner.on(",\n" + " ".repeat(functionName.length() + 1)).join(paramLinksLines);
+    return String.format("%s(%s)", functionName, paramList);
+  }
+
+  /**
+   * Wraps the given function parameter names to be able to construct a function summary that stays
+   * within the provided line length limit.
+   *
+   * @param functionName the function name.
+   * @param paramNames the function parameter names.
+   * @param maxLineLength the maximal line length.
+   * @return the lines with the wrapped parameter names.
+   */
+  private static List<List<String>> wrap(
+      String functionName, List<String> paramNames, int maxLineLength) {
+    List<List<String>> paramLines = new ArrayList<>();
+    ImmutableList.Builder<String> linesBuilder = new ImmutableList.Builder<>();
+    int leading = functionName.length();
+    int length = leading;
+    int punctuation = 2; // cater for left parenthesis/space before and comma after parameter
+    for (String paramName : paramNames) {
+      length += paramName.length() + punctuation;
+      if (length > maxLineLength) {
+        paramLines.add(linesBuilder.build());
+        length = leading + paramName.length();
+        linesBuilder = new ImmutableList.Builder<>();
+      }
+      linesBuilder.add(paramName);
+    }
+    paramLines.add(linesBuilder.build());
+    return paramLines;
+  }
+
+  /**
+   * Returns a string describing the given attribute's type. The description consists of a hyperlink
+   * if there is a relevant hyperlink to Bazel documentation available.
+   */
+  public String attributeTypeString(AttributeInfo attrInfo) {
+    String typeLink;
+    switch (attrInfo.getType()) {
+      case LABEL:
+      case LABEL_LIST:
+      case OUTPUT:
+        typeLink = "https://bazel.build/concepts/labels";
+        break;
+      case NAME:
+        typeLink = "https://bazel.build/concepts/labels#target-names";
+        break;
+      case STRING_DICT:
+      case STRING_LIST_DICT:
+      case LABEL_STRING_DICT:
+        typeLink = "https://bazel.build/rules/lib/dict";
+        break;
+      default:
+        typeLink = null;
+        break;
+    }
+    if (typeLink == null) {
+      return attributeTypeDescription(attrInfo.getType());
+    } else {
+      return String.format(
+          "<a href=\"%s\">%s</a>", typeLink, attributeTypeDescription(attrInfo.getType()));
+    }
+  }
+
+  public String mandatoryString(AttributeInfo attrInfo) {
+    return attrInfo.getMandatory() ? "required" : "optional";
+  }
+
+  /**
+   * Returns "required" if providing a value for this parameter is mandatory. Otherwise, returns
+   * "optional".
+   */
+  public String mandatoryString(FunctionParamInfo paramInfo) {
+    return paramInfo.getMandatory() ? "required" : "optional";
+  }
+
+  /**
+   * Return a string explaining what providers an attribute requires. Adds hyperlinks to providers.
+   */
+  public String attributeProviders(AttributeInfo attributeInfo) {
+    List<ProviderNameGroup> providerNames = attributeInfo.getProviderNameGroupList();
+    List<String> finalProviderNames = new ArrayList<>();
+    for (ProviderNameGroup providerNameList : providerNames) {
+      List<String> providers = providerNameList.getProviderNameList();
+      finalProviderNames.add(Joiner.on(", ").join(providers));
+    }
+    return Joiner.on("; or ").join(finalProviderNames);
+  }
+
+  private static String attributeTypeDescription(AttributeType attributeType) {
+    switch (attributeType) {
+      case NAME:
+        return "Name";
+      case INT:
+        return "Integer";
+      case STRING:
+        return "String";
+      case STRING_LIST:
+        return "List of strings";
+      case INT_LIST:
+        return "List of integers";
+      case BOOLEAN:
+        return "Boolean";
+      case LABEL_STRING_DICT:
+        return "Dictionary: Label -> String";
+      case STRING_DICT:
+        return "Dictionary: String -> String";
+      case STRING_LIST_DICT:
+        return "Dictionary: String -> List of strings";
+      case LABEL:
+      case OUTPUT:
+        return "Label";
+      case LABEL_LIST:
+      case OUTPUT_LIST:
+        return "List of labels";
+      case UNKNOWN:
+      case UNRECOGNIZED:
+        throw new IllegalArgumentException("Unhandled type " + attributeType);
+    }
+    throw new IllegalArgumentException("Unhandled type " + attributeType);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/skydoc/rendering/BUILD b/src/test/java/com/google/devtools/build/skydoc/rendering/BUILD
new file mode 100644
index 0000000..3405102
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/rendering/BUILD
@@ -0,0 +1,27 @@
+load("@rules_java//java:defs.bzl", "java_test")
+
+package(
+    default_applicable_licenses = ["//:license"],
+    default_visibility = [
+        "//src:__subpackages__",
+        "//stardoc:__subpackages__",
+    ],
+)
+
+filegroup(
+    name = "srcs",
+    testonly = 0,
+    srcs = glob(["*"]),
+    visibility = ["//:__pkg__"],
+)
+
+java_test(
+    name = "MarkdownUtilTest",
+    size = "small",
+    srcs = ["MarkdownUtilTest.java"],
+    deps = [
+        "//src/main/java/com/google/devtools/build/skydoc/rendering",
+        "@maven//:com_google_truth_truth",
+        "@maven//:junit_junit",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/skydoc/rendering/MarkdownUtilTest.java b/src/test/java/com/google/devtools/build/skydoc/rendering/MarkdownUtilTest.java
new file mode 100644
index 0000000..cf84964
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/rendering/MarkdownUtilTest.java
@@ -0,0 +1,50 @@
+// Copyright 2023 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 static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for MarkdownUtil. */
+@RunWith(JUnit4.class)
+public class MarkdownUtilTest {
+
+  MarkdownUtil util = new MarkdownUtil();
+
+  @Test
+  public void markdownCodeSpan() {
+    assertThat(MarkdownUtil.markdownCodeSpan("")).isEqualTo("``");
+    assertThat(MarkdownUtil.markdownCodeSpan("foo bar ")).isEqualTo("`foo bar `");
+  }
+
+  @Test
+  public void markdownCodeSpan_backticks() {
+    assertThat(MarkdownUtil.markdownCodeSpan("foo`bar")).isEqualTo("``foo`bar``");
+    assertThat(MarkdownUtil.markdownCodeSpan("foo``bar")).isEqualTo("```foo``bar```");
+    assertThat(MarkdownUtil.markdownCodeSpan("foo`bar```baz``quz"))
+        .isEqualTo("````foo`bar```baz``quz````");
+  }
+
+  @Test
+  public void markdownCodeSpan_backticksPadding() {
+    assertThat(MarkdownUtil.markdownCodeSpan("`foo")).isEqualTo("`` `foo ``");
+    assertThat(MarkdownUtil.markdownCodeSpan("``foo")).isEqualTo("``` ``foo ```");
+    assertThat(MarkdownUtil.markdownCodeSpan("foo`")).isEqualTo("`` foo` ``");
+    assertThat(MarkdownUtil.markdownCodeSpan("foo``")).isEqualTo("``` foo`` ```");
+  }
+}
diff --git a/stardoc/BUILD b/stardoc/BUILD
index 045774f..da02ce0 100644
--- a/stardoc/BUILD
+++ b/stardoc/BUILD
@@ -63,18 +63,9 @@
     visibility = ["//visibility:public"],
 )
 
-java_binary(
+alias(
     name = "renderer",
-    main_class = "com.google.devtools.build.skydoc.renderer.RendererMain",
-    runtime_deps = [
-        ":prebuilt_renderer_binary",
-    ],
-)
-
-java_import(
-    name = "prebuilt_renderer_binary",
-    jars = ["renderer_binary.jar"],
-    visibility = ["//visibility:private"],
+    actual = "//src/main/java/com/google/devtools/build/skydoc/renderer",
 )
 
 # Sources needed for release tarball.
diff --git a/stardoc/proto/BUILD b/stardoc/proto/BUILD
index 6e3782c..4e9619b 100644
--- a/stardoc/proto/BUILD
+++ b/stardoc/proto/BUILD
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "java_proto_library")
+
 licenses(["notice"])
 
 package(
@@ -15,3 +17,13 @@
     ] + glob(["*.proto"]),
     visibility = ["//:__pkg__"],
 )
+
+proto_library(
+    name = "stardoc_output_proto",
+    srcs = ["stardoc_output.proto"],
+)
+
+java_proto_library(
+    name = "stardoc_output_java_proto",
+    deps = [":stardoc_output_proto"],
+)
diff --git a/stardoc/proto/stardoc_output.proto b/stardoc/proto/stardoc_output.proto
index e35e714..7176213 100644
--- a/stardoc/proto/stardoc_output.proto
+++ b/stardoc/proto/stardoc_output.proto
@@ -12,6 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 //
+// Vendored from src/main/java/com/google/devtools/build/skydoc/rendering/proto/stardoc_output.proto
+// in the Bazel source tree at commit e33c2d14728aea6914b165fe11f08ff4982196ca
+//
 // Protos for Stardoc data.
 //
 // Stardoc collects information about Starlark functions, providers, and rules.
@@ -23,9 +26,9 @@
 option java_package = "com.google.devtools.build.skydoc.rendering.proto";
 option java_outer_classname = "StardocOutputProtos";
 
-// The root output proto of Stardoc. A single invocation of Stardoc will output
-// exactly one instance of this proto, representing all documentation for
-// the input Starlark file.
+// The root output proto of Stardoc. An invocation of Stardoc on a single file
+// will output exactly one instance of this proto, representing all
+// documentation for the input Starlark file.
 message ModuleInfo {
   repeated RuleInfo rule_info = 1;
 
@@ -37,6 +40,15 @@
 
   // The docstring present at the top of the input Starlark file.
   string module_docstring = 5;
+
+  // The display form of the label of the module file (as seen from the
+  // starlark_doc_extract or Stardoc target's repo). Unset when there is no
+  // module file (e.g. when the module is a REPL, or in Bazel's internal tests).
+  string file = 6;
+
+  repeated ModuleExtensionInfo module_extension_info = 7;
+
+  repeated RepositoryRuleInfo repository_rule_info = 8;
 }
 
 // Representation of a Starlark rule attribute type. These generally
@@ -63,7 +75,8 @@
 
 // Representation of a Starlark rule definition.
 message RuleInfo {
-  // The name of the rule.
+  // The name under which the rule is made accessible to a user of this module,
+  // including any structs it is nested in, for example "foo.foo_library".
   string rule_name = 1;
 
   // The documentation string of the rule.
@@ -71,11 +84,22 @@
 
   // The attributes of the rule.
   repeated AttributeInfo attribute = 3;
+
+  // The module where and the name under which the rule was originally declared.
+  //
+  // Note: legacy Stardoc (0.5.x and earlier) does not set this field.
+  OriginKey origin_key = 4;
+
+  // The list of providers that the rule's implementation must return. Unset if
+  // the rule lists no advertised providers.
+  //
+  // Note: legacy Stardoc (0.5.x and earlier) does not set this field.
+  ProviderNameGroup advertised_providers = 5;
 }
 
-// Representation of a Starlark rule attribute definition, comprised of an
-// attribute name, and a schema defined by a call to one of the 'attr' module
-// methods enumerated at
+// Representation of a Starlark rule, repository rule, or module extension tag
+// attribute definition, comprised of an attribute name, and a schema defined by
+// a call to one of the 'attr' module methods enumerated at
 // https://bazel.build/rules/lib/attr
 message AttributeInfo {
   // The name of the attribute.
@@ -95,26 +119,46 @@
   // The target(s) in this attribute must define all the providers of at least
   // one of the ProviderNameGroups in this list. If the Attribute Type is not a
   // label, a label list, or a label-keyed string dictionary, the field will be
-  // left empty.
+  // left empty. For attributes of a repository rule or a module extension tag,
+  // this attribute is meaningless and may be ignored.
+  // TODO(b/290788853): ensure this field is always empty for attributes of a
+  // repository rule or a module extension tag.
   repeated ProviderNameGroup provider_name_group = 5;
 
   // The string representation of the default value of this attribute.
   string default_value = 6;
 }
 
-// Representation of a set of providers that a rule attribute may be required to
-// have.
+// Representation of a set of providers.
 message ProviderNameGroup {
-  // The names of the providers that must be given by any dependency appearing
-  // in this attribute. The name will be "Unknown Provider" if the name is
-  // unidentifiable, for example, if the provider is part of a namespace.
-  // TODO(kendalllane): Fix documentation of providers from namespaces.
+  // The names of the providers.
+  //
+  // This field is only intended for rendering human-readable output.
+  // Please use origin_key (a list of the same length and in the same order as
+  // this field) for cross-references and tooling.
+  //
+  // Note: legacy Stardoc (0.5.x and earlier) is unable to extract the name in
+  // some circumstances (for example, if the provider is nested in a struct),
+  // and in that case, the provider name will be "Unknown Provider".
   repeated string provider_name = 1;
+
+  // A list of unambiguous references to providers, of the same length and in
+  // the same order as the provider_name list.
+  //
+  // For provider symbols, this means modules where and the names under which
+  // the providers were originally declared.
+  //
+  // For legacy struct providers, origin_key.file is unset.
+  //
+  // Note: legacy Stardoc (0.5.x and earlier) does not set this field.
+  repeated OriginKey origin_key = 2;
 }
 
 // Representation of Starlark function definition.
 message StarlarkFunctionInfo {
-  // The name of the function.
+  // The name under which the function is made accessible to a user of this
+  // module, including any structs it is nested in, for example
+  // "foo.frobnicate".
   string function_name = 1;
 
   // The parameters for the function.
@@ -129,6 +173,12 @@
 
   // The deprecation for the function.
   FunctionDeprecationInfo deprecated = 5;
+
+  // The module where and the name under which the function was originally
+  // declared.
+  //
+  // Note: legacy Stardoc (0.5.x and earlier) does not set this field.
+  OriginKey origin_key = 6;
 }
 
 // Representation of a Starlark function parameter definition.
@@ -174,7 +224,8 @@
 
 // Representation of a Starlark provider definition.
 message ProviderInfo {
-  // The name of the provider.
+  // The name under which the provider is made accessible to a user of this
+  // module, including any structs it is nested in, for example "foo.FooInfo".
   string provider_name = 1;
 
   // The description of the provider.
@@ -182,11 +233,19 @@
 
   // The fields of the provider.
   repeated ProviderFieldInfo field_info = 3;
+
+  // The module where and the name under which the provider was originally
+  // declared.
+  //
+  // Note: legacy Stardoc (0.5.x and earlier) does not set this field.
+  OriginKey origin_key = 4;
 }
 
 // Representation of a Starlark aspect definition.
 message AspectInfo {
-  // The name of the aspect.
+  // The name under which the aspect is made accessible to a user of this
+  // module, including any structs it is nested in, for example
+  // "foo.foo_aspect".
   string aspect_name = 1;
 
   // The documentation string of the aspect.
@@ -197,4 +256,85 @@
 
   // The attributes of the aspect.
   repeated AttributeInfo attribute = 4;
+
+  // The module where and the name under which the aspect was originally
+  // declared.
+  //
+  // Note: legacy Stardoc (0.5.x and earlier) does not set this field.
+  OriginKey origin_key = 5;
+}
+
+// Representation of a Bazel module extension, i.e. the object returned by
+// calling `module_extension(...)`.
+//
+// Note: legacy Stardoc (0.5.x and earlier) does not emit this message.
+message ModuleExtensionInfo {
+  // The name under which the extension is made accessible to a user of this
+  // Starlark module.
+  string extension_name = 1;
+
+  // The documentation string of the extension.
+  string doc_string = 2;
+
+  // The tag classes of the extension.
+  repeated ModuleExtensionTagClassInfo tag_class = 3;
+
+  // The Starlark module where the Bazel module extension was originally
+  // declared; origin_key.name is currently never set.
+  // TODO(arostovtsev): attempt to retrieve the name under which the module
+  // extension was originally declared if it was declared as a global.
+  OriginKey origin_key = 4;
+}
+
+// Representation of a Bazel module extension tag class.
+message ModuleExtensionTagClassInfo {
+  // The name of the tag for this tag class.
+  string tag_name = 1;
+
+  // The documentation string of the tag class.
+  string doc_string = 2;
+
+  // The tag class's attributes.
+  repeated AttributeInfo attribute = 3;
+}
+
+// Representation of a Bazel repository rule, i.e. the object returned by
+// calling `repository_rule(...)`.
+//
+// Note: legacy Stardoc (0.5.x and earlier) does not emit this message, instead
+// using RuleInfo.
+message RepositoryRuleInfo {
+  // The name under which the repository rule is made accessible to a user of
+  // this Starlark module.
+  string rule_name = 1;
+
+  // The documentation string of the repository rule.
+  string doc_string = 2;
+
+  // The attributes of the repository rule.
+  repeated AttributeInfo attribute = 3;
+
+  // Environment variables that this repository rule depends on.
+  repeated string environ = 4;
+
+  // The Starlark module where and the name under which the repository rule was
+  // originally declared.
+  OriginKey origin_key = 5;
+}
+
+// Representation of the origin of a rule, provider, aspect, or function.
+// Intended to be used for building unambiguous cross-references: for example,
+// between an element of a ProviderNameGroup required by a rule attribute and
+// its corresponding ProviderInfo.
+message OriginKey {
+  // The name under which the entity was originally exported. Unset when the
+  // entity was not exported in its module.
+  string name = 1;
+
+  // The display form of the label of the module file in which the entity was
+  // originally declared (as seen from the starlark_doc_extract or Stardoc
+  // target's repo), or "<native>" for Bazel's built-in entities implemented in
+  // Java. Unset when there is no module file (such as for legacy struct
+  // providers, when the module is a REPL, or in Bazel's internal tests).
+  string file = 2;
 }
diff --git a/stardoc/renderer_binary.jar b/stardoc/renderer_binary.jar
deleted file mode 100755
index a2f8897..0000000
--- a/stardoc/renderer_binary.jar
+++ /dev/null
Binary files differ
diff --git a/test/BUILD b/test/BUILD
index 1dd7b70..6dfe44e 100644
--- a/test/BUILD
+++ b/test/BUILD
@@ -144,7 +144,7 @@
     input_file = "testdata/filter_rules_test/input.bzl",
     symbol_names = [
         "my_rule",
-        "whitelisted_dep_rule",
+        "allowlisted_dep_rule",
     ],
     deps = [
         "testdata/filter_rules_test/dep.bzl",
@@ -164,13 +164,19 @@
 )
 
 stardoc_test(
+    name = "function_wrap_multiple_lines_test",
+    golden_file = "testdata/function_wrap_multiple_lines_test/golden.md",
+    input_file = "testdata/function_wrap_multiple_lines_test/input.bzl",
+)
+
+stardoc_test(
     name = "namespace_test",
     golden_file = "testdata/namespace_test/golden.md",
     input_file = "testdata/namespace_test/input.bzl",
 )
 
 stardoc_test(
-    name = "namespace_test_with_whitelist",
+    name = "namespace_test_with_allowlist",
     golden_file = "testdata/namespace_test/golden.md",
     input_file = "testdata/namespace_test/input.bzl",
     symbol_names = [
@@ -185,9 +191,9 @@
 )
 
 stardoc_test(
-    name = "multi_level_namespace_test_with_whitelist",
-    golden_file = "testdata/multi_level_namespace_test_with_whitelist/golden.md",
-    input_file = "testdata/multi_level_namespace_test_with_whitelist/input.bzl",
+    name = "multi_level_namespace_test_with_allowlist",
+    golden_file = "testdata/multi_level_namespace_test_with_allowlist/golden.md",
+    input_file = "testdata/multi_level_namespace_test_with_allowlist/input.bzl",
     symbol_names = [
         "my_namespace",
         "other_namespace.foo.nothing",
@@ -201,6 +207,12 @@
 )
 
 stardoc_test(
+    name = "pure_markdown_template_test",
+    golden_file = "testdata/pure_markdown_template_test/golden.md",
+    input_file = "testdata/pure_markdown_template_test/input.bzl",
+)
+
+stardoc_test(
     name = "py_rule_test",
     golden_file = "testdata/py_rule_test/golden.md",
     input_file = "testdata/py_rule_test/input.bzl",
diff --git a/test/stardoc_test.bzl b/test/stardoc_test.bzl
index cb356af..fc44ba8 100644
--- a/test/stardoc_test.bzl
+++ b/test/stardoc_test.bzl
@@ -60,7 +60,6 @@
         input_file = input_file,
         golden_file = golden_file,
         stardoc_bin = "@io_bazel//src/main/java/com/google/devtools/build/skydoc:skydoc_deploy.jar",
-        renderer_bin = "@io_bazel//src/main/java/com/google/devtools/build/skydoc/renderer",
         test = test,
         **kwargs
     )
@@ -71,7 +70,6 @@
         input_file = input_file,
         golden_file = golden_file,
         stardoc_bin = "@io_bazel//src/main/java/com/google/devtools/build/skydoc:skydoc_deploy.jar",
-        renderer_bin = "@io_bazel//src/main/java/com/google/devtools/build/skydoc/renderer",
         test = test,
         **kwargs
     )
@@ -83,7 +81,6 @@
         input_file,
         golden_file,
         stardoc_bin,
-        renderer_bin,
         test,
         **kwargs):
     actual_generated_doc = "%s.out" % genrule_name
@@ -107,7 +104,6 @@
             out = actual_generated_doc,
             input = input_file,
             deps = [lib_name],
-            renderer = renderer_bin,
             stardoc = stardoc_bin,
             **kwargs
         )
@@ -117,7 +113,6 @@
             out = actual_generated_doc,
             input = input_file,
             deps = [lib_name],
-            renderer = renderer_bin,
             stardoc = stardoc_bin,
             **kwargs
         )
diff --git a/test/testdata/filter_rules_test/golden.md b/test/testdata/filter_rules_test/golden.md
index 7e85581..2801a16 100755
--- a/test/testdata/filter_rules_test/golden.md
+++ b/test/testdata/filter_rules_test/golden.md
@@ -2,6 +2,26 @@
 
 
 
+<a id="allowlisted_dep_rule"></a>
+
+## allowlisted_dep_rule
+
+<pre>
+allowlisted_dep_rule(<a href="#allowlisted_dep_rule-name">name</a>, <a href="#allowlisted_dep_rule-first">first</a>, <a href="#allowlisted_dep_rule-second">second</a>)
+</pre>
+
+This is the dep rule. It does stuff.
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="allowlisted_dep_rule-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="allowlisted_dep_rule-first"></a>first |  dep's my_rule doc string   | <a href="https://bazel.build/concepts/labels">Label</a> | required |  |
+| <a id="allowlisted_dep_rule-second"></a>second |  -   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
+
+
 <a id="my_rule"></a>
 
 ## my_rule
@@ -22,23 +42,3 @@
 | <a id="my_rule-second"></a>second |  -   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
 
 
-<a id="whitelisted_dep_rule"></a>
-
-## whitelisted_dep_rule
-
-<pre>
-whitelisted_dep_rule(<a href="#whitelisted_dep_rule-name">name</a>, <a href="#whitelisted_dep_rule-first">first</a>, <a href="#whitelisted_dep_rule-second">second</a>)
-</pre>
-
-This is the dep rule. It does stuff.
-
-**ATTRIBUTES**
-
-
-| Name  | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :------------- | :------------- | :------------- |
-| <a id="whitelisted_dep_rule-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="whitelisted_dep_rule-first"></a>first |  dep's my_rule doc string   | <a href="https://bazel.build/concepts/labels">Label</a> | required |  |
-| <a id="whitelisted_dep_rule-second"></a>second |  -   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
-
-
diff --git a/test/testdata/filter_rules_test/input.bzl b/test/testdata/filter_rules_test/input.bzl
index 5d05b9b..08cc012 100644
--- a/test/testdata/filter_rules_test/input.bzl
+++ b/test/testdata/filter_rules_test/input.bzl
@@ -1,13 +1,10 @@
 # buildifier: disable=module-docstring
 load(
     ":testdata/filter_rules_test/dep.bzl",
+    "my_rule_impl",
     dep_rule = "my_rule",
 )
 
-def my_rule_impl(ctx):
-    _ignore = [ctx]  # @unused
-    return []
-
 my_rule = rule(
     implementation = my_rule_impl,
     doc = "This is my rule. It does stuff.",
@@ -29,7 +26,7 @@
     },
 )
 
-whitelisted_dep_rule = dep_rule
+allowlisted_dep_rule = dep_rule
 
 yet_another_rule = rule(
     implementation = my_rule_impl,
diff --git a/test/testdata/function_basic_test/golden.md b/test/testdata/function_basic_test/golden.md
index 138f3c2..3f72514 100755
--- a/test/testdata/function_basic_test/golden.md
+++ b/test/testdata/function_basic_test/golden.md
@@ -48,6 +48,24 @@
 Use literally anything but this function.
 
 
+<a id="param_doc_multiline"></a>
+
+## param_doc_multiline
+
+<pre>
+param_doc_multiline(<a href="#param_doc_multiline-complex">complex</a>)
+</pre>
+
+Has a complex parameter.
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="param_doc_multiline-complex"></a>complex |  A parameter with some non-obvious behavior.<br><br> For example, it does things that require **multiple paragraphs** to explain.<br><br>Note: we should preserve the nested indent in the following code:<br><br><pre><code>json {     "key": "value" } </code></pre>   |  none |
+
+
 <a id="returns_a_thing"></a>
 
 ## returns_a_thing
diff --git a/test/testdata/function_basic_test/input.bzl b/test/testdata/function_basic_test/input.bzl
index 93bf711..2d75025 100644
--- a/test/testdata/function_basic_test/input.bzl
+++ b/test/testdata/function_basic_test/input.bzl
@@ -54,6 +54,25 @@
     """
     pass
 
+# buildifier: disable=unused-variable
 def undocumented_function(a, b, c):
-    _ignore = [a, b, c]  # @unused
+    pass
+
+# buildifier: disable=unused-variable
+def param_doc_multiline(complex):
+    """Has a complex parameter.
+
+    Args:
+        complex: A parameter with some non-obvious behavior.
+
+            For example, it does things that require **multiple paragraphs** to explain.
+
+           Note: we should preserve the nested indent in the following code:
+
+           ```json
+           {
+               "key": "value"
+           }
+           ```
+    """
     pass
diff --git a/test/testdata/function_wrap_multiple_lines_test/golden.md b/test/testdata/function_wrap_multiple_lines_test/golden.md
new file mode 100644
index 0000000..2f1a8a4
--- /dev/null
+++ b/test/testdata/function_wrap_multiple_lines_test/golden.md
@@ -0,0 +1,56 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+
+Rules for ANTLR 3.
+
+<a id="antlr"></a>
+
+## antlr
+
+<pre>
+antlr(<a href="#antlr-name">name</a>, <a href="#antlr-deps">deps</a>, <a href="#antlr-srcs">srcs</a>, <a href="#antlr-Xconversiontimeout">Xconversiontimeout</a>, <a href="#antlr-Xdbgconversion">Xdbgconversion</a>, <a href="#antlr-Xdbgst">Xdbgst</a>, <a href="#antlr-Xdfa">Xdfa</a>, <a href="#antlr-Xdfaverbose">Xdfaverbose</a>, <a href="#antlr-Xgrtree">Xgrtree</a>, <a href="#antlr-Xm">Xm</a>,
+      <a href="#antlr-Xmaxdfaedges">Xmaxdfaedges</a>, <a href="#antlr-Xmaxinlinedfastates">Xmaxinlinedfastates</a>, <a href="#antlr-Xminswitchalts">Xminswitchalts</a>, <a href="#antlr-Xmultithreaded">Xmultithreaded</a>, <a href="#antlr-Xnfastates">Xnfastates</a>, <a href="#antlr-Xnocollapse">Xnocollapse</a>,
+      <a href="#antlr-Xnomergestopstates">Xnomergestopstates</a>, <a href="#antlr-Xnoprune">Xnoprune</a>, <a href="#antlr-XsaveLexer">XsaveLexer</a>, <a href="#antlr-Xwatchconversion">Xwatchconversion</a>, <a href="#antlr-debug">debug</a>, <a href="#antlr-depend">depend</a>, <a href="#antlr-dfa">dfa</a>, <a href="#antlr-dump">dump</a>, <a href="#antlr-imports">imports</a>,
+      <a href="#antlr-language">language</a>, <a href="#antlr-message_format">message_format</a>, <a href="#antlr-nfa">nfa</a>, <a href="#antlr-package">package</a>, <a href="#antlr-profile">profile</a>, <a href="#antlr-report">report</a>, <a href="#antlr-trace">trace</a>)
+</pre>
+
+Runs [ANTLR 3](https://www.antlr3.org//) on a set of grammars.
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="antlr-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="antlr-deps"></a>deps |  The dependencies to use. Defaults to the most recent ANTLR 3 release, but if you need to use a different version, you can specify the dependencies here.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[Label("@antlr3_runtimes//:tool")]</code> |
+| <a id="antlr-srcs"></a>srcs |  The grammar files to process.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | required |  |
+| <a id="antlr-Xconversiontimeout"></a>Xconversiontimeout |  Set NFA conversion timeout for each decision.   | Integer | optional | <code>0</code> |
+| <a id="antlr-Xdbgconversion"></a>Xdbgconversion |  Dump lots of info during NFA conversion.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xdbgst"></a>Xdbgst |  Put tags at start/stop of all templates in output.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xdfa"></a>Xdfa |  Print DFA as text.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xdfaverbose"></a>Xdfaverbose |  Generate DFA states in DOT with NFA configs.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xgrtree"></a>Xgrtree |  Print the grammar AST.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xm"></a>Xm |  Max number of rule invocations during conversion.   | Integer | optional | <code>0</code> |
+| <a id="antlr-Xmaxdfaedges"></a>Xmaxdfaedges |  Max &quot;comfortable&quot; number of edges for single DFA state.   | Integer | optional | <code>0</code> |
+| <a id="antlr-Xmaxinlinedfastates"></a>Xmaxinlinedfastates |  Max DFA states before table used rather than inlining.   | Integer | optional | <code>0</code> |
+| <a id="antlr-Xminswitchalts"></a>Xminswitchalts |  Don't generate switch() statements for dfas smaller than given number.   | Integer | optional | <code>0</code> |
+| <a id="antlr-Xmultithreaded"></a>Xmultithreaded |  Run the analysis in 2 threads.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xnfastates"></a>Xnfastates |  For nondeterminisms, list NFA states for each path.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xnocollapse"></a>Xnocollapse |  Collapse incident edges into DFA states.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xnomergestopstates"></a>Xnomergestopstates |  Max DFA states before table used rather than inlining.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xnoprune"></a>Xnoprune |  Do not test EBNF block exit branches.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-XsaveLexer"></a>XsaveLexer |  For nondeterminisms, list NFA states for each path.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-Xwatchconversion"></a>Xwatchconversion |  Don't delete temporary lexers generated from combined grammars.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-debug"></a>debug |  Generate a parser that emits debugging events.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-depend"></a>depend |  Generate file dependencies; don't actually run antlr.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-dfa"></a>dfa |  Generate a DFA for each decision point.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-dump"></a>dump |  Print out the grammar without actions.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-imports"></a>imports |  The grammar and .tokens files to import. Must be all in the same directory.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[]</code> |
+| <a id="antlr-language"></a>language |  The code generation target language. Either C, Cpp, CSharp2, CSharp3, JavaScript, Java, ObjC, Python, Python3 or Ruby (case-sensitive).   | String | optional | <code>""</code> |
+| <a id="antlr-message_format"></a>message_format |  Specify output style for messages.   | String | optional | <code>""</code> |
+| <a id="antlr-nfa"></a>nfa |  Generate an NFA for each rule.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-package"></a>package |  The package/namespace for the generated code.   | String | optional | <code>""</code> |
+| <a id="antlr-profile"></a>profile |  Generate a parser that computes profiling information.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-report"></a>report |  Print out a report about the grammar(s) processed.   | Boolean | optional | <code>False</code> |
+| <a id="antlr-trace"></a>trace |  Generate a parser with trace output. If the default output is not enough, you can override the traceIn and traceOut methods.   | Boolean | optional | <code>False</code> |
+
+
diff --git a/test/testdata/function_wrap_multiple_lines_test/input.bzl b/test/testdata/function_wrap_multiple_lines_test/input.bzl
new file mode 100644
index 0000000..c822551
--- /dev/null
+++ b/test/testdata/function_wrap_multiple_lines_test/input.bzl
@@ -0,0 +1,54 @@
+"""Rules for ANTLR 3."""
+
+# buildifier: disable=unused-variable
+def _generate(ctx):
+    return None
+
+antlr = rule(
+    implementation = _generate,
+    doc = "Runs [ANTLR 3](https://www.antlr3.org//) on a set of grammars.",
+    attrs = {
+        "debug": attr.bool(default = False, doc = "Generate a parser that emits debugging events."),
+        "depend": attr.bool(default = False, doc = "Generate file dependencies; don't actually run antlr."),
+        "deps": attr.label_list(
+            default = [Label("@antlr3_runtimes//:tool")],
+            doc = """
+The dependencies to use. Defaults to the most recent ANTLR 3 release,
+but if you need to use a different version, you can specify the
+dependencies here.
+""",
+        ),
+        "dfa": attr.bool(default = False, doc = "Generate a DFA for each decision point."),
+        "dump": attr.bool(default = False, doc = "Print out the grammar without actions."),
+        "imports": attr.label_list(allow_files = True, doc = "The grammar and .tokens files to import. Must be all in the same directory."),
+        "language": attr.string(doc = "The code generation target language. Either C, Cpp, CSharp2, CSharp3, JavaScript, Java, ObjC, Python, Python3 or Ruby (case-sensitive)."),
+        "message_format": attr.string(doc = "Specify output style for messages."),
+        "nfa": attr.bool(default = False, doc = "Generate an NFA for each rule."),
+        "package": attr.string(doc = "The package/namespace for the generated code."),
+        "profile": attr.bool(default = False, doc = "Generate a parser that computes profiling information."),
+        "report": attr.bool(default = False, doc = "Print out a report about the grammar(s) processed."),
+        "srcs": attr.label_list(allow_files = True, mandatory = True, doc = "The grammar files to process."),
+        "trace": attr.bool(default = False, doc = "Generate a parser with trace output. If the default output is not enough, you can override the traceIn and traceOut methods."),
+        "Xconversiontimeout": attr.int(doc = "Set NFA conversion timeout for each decision."),
+        "Xdbgconversion": attr.bool(default = False, doc = "Dump lots of info during NFA conversion."),
+        "Xdbgst": attr.bool(default = False, doc = "Put tags at start/stop of all templates in output."),
+        "Xdfa": attr.bool(default = False, doc = "Print DFA as text."),
+        "Xdfaverbose": attr.bool(default = False, doc = "Generate DFA states in DOT with NFA configs."),
+        "Xgrtree": attr.bool(default = False, doc = "Print the grammar AST."),
+        "Xm": attr.int(doc = "Max number of rule invocations during conversion."),
+        "Xmaxdfaedges": attr.int(doc = "Max &quot;comfortable&quot; number of edges for single DFA state."),
+        "Xmaxinlinedfastates": attr.int(doc = "Max DFA states before table used rather than inlining."),
+        "Xminswitchalts": attr.int(doc = "Don't generate switch() statements for dfas smaller than given number."),
+        "Xmultithreaded": attr.bool(default = False, doc = "Run the analysis in 2 threads."),
+        "Xnfastates": attr.bool(default = False, doc = "For nondeterminisms, list NFA states for each path."),
+        "Xnocollapse": attr.bool(default = False, doc = "Collapse incident edges into DFA states."),
+        "Xnoprune": attr.bool(default = False, doc = "Do not test EBNF block exit branches."),
+        "Xnomergestopstates": attr.bool(default = False, doc = "Max DFA states before table used rather than inlining."),
+        "XsaveLexer": attr.bool(default = False, doc = "For nondeterminisms, list NFA states for each path."),
+        "Xwatchconversion": attr.bool(default = False, doc = "Don't delete temporary lexers generated from combined grammars."),
+        "_tool": attr.label(
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/test/testdata/misc_apis_test/golden.md b/test/testdata/misc_apis_test/golden.md
index 7b94d9f..34686ee 100755
--- a/test/testdata/misc_apis_test/golden.md
+++ b/test/testdata/misc_apis_test/golden.md
@@ -7,7 +7,7 @@
 ## my_rule
 
 <pre>
-my_rule(<a href="#my_rule-name">name</a>, <a href="#my_rule-deps">deps</a>, <a href="#my_rule-extra_arguments">extra_arguments</a>, <a href="#my_rule-out">out</a>, <a href="#my_rule-src">src</a>, <a href="#my_rule-tool">tool</a>)
+my_rule(<a href="#my_rule-name">name</a>, <a href="#my_rule-deps">deps</a>, <a href="#my_rule-src">src</a>, <a href="#my_rule-out">out</a>, <a href="#my_rule-extra_arguments">extra_arguments</a>, <a href="#my_rule-tool">tool</a>)
 </pre>
 
 This rule exercises some of the build API.
@@ -19,9 +19,9 @@
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <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-deps"></a>deps |  A list of dependencies.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[]</code> |
-| <a id="my_rule-extra_arguments"></a>extra_arguments |  -   | List of strings | optional | <code>[]</code> |
-| <a id="my_rule-out"></a>out |  The output file.   | <a href="https://bazel.build/concepts/labels">Label</a> | required |  |
 | <a id="my_rule-src"></a>src |  The source file.   | <a href="https://bazel.build/concepts/labels">Label</a> | optional | <code>None</code> |
+| <a id="my_rule-out"></a>out |  The output file.   | <a href="https://bazel.build/concepts/labels">Label</a> | required |  |
+| <a id="my_rule-extra_arguments"></a>extra_arguments |  -   | List of strings | optional | <code>[]</code> |
 | <a id="my_rule-tool"></a>tool |  The location of the tool to use.   | <a href="https://bazel.build/concepts/labels">Label</a> | optional | <code>//foo/bar/baz:target</code> |
 
 
diff --git a/test/testdata/multi_level_namespace_test/golden.md b/test/testdata/multi_level_namespace_test/golden.md
index 85791c9..3e753c0 100755
--- a/test/testdata/multi_level_namespace_test/golden.md
+++ b/test/testdata/multi_level_namespace_test/golden.md
@@ -2,26 +2,16 @@
 
 A test that verifies documenting a multi-leveled namespace of functions.
 
-<a id="my_namespace.min"></a>
+<a id="my_namespace.foo.bar.baz"></a>
 
-## my_namespace.min
+## my_namespace.foo.bar.baz
 
 <pre>
-my_namespace.min(<a href="#my_namespace.min-integers">integers</a>)
+my_namespace.foo.bar.baz()
 </pre>
 
-Returns the minimum of given elements.
+This function does nothing.
 
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="my_namespace.min-integers"></a>integers |  A list of integers. Must not be empty.   |  none |
-
-**RETURNS**
-
-The minimum integer in the given list.
 
 
 <a id="my_namespace.math.min"></a>
@@ -46,12 +36,34 @@
 The minimum integer in the given list.
 
 
-<a id="my_namespace.foo.bar.baz"></a>
+<a id="my_namespace.min"></a>
 
-## my_namespace.foo.bar.baz
+## my_namespace.min
 
 <pre>
-my_namespace.foo.bar.baz()
+my_namespace.min(<a href="#my_namespace.min-integers">integers</a>)
+</pre>
+
+Returns the minimum of given elements.
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="my_namespace.min-integers"></a>integers |  A list of integers. Must not be empty.   |  none |
+
+**RETURNS**
+
+The minimum integer in the given list.
+
+
+<a id="my_namespace.one.three.does_nothing"></a>
+
+## my_namespace.one.three.does_nothing
+
+<pre>
+my_namespace.one.three.does_nothing()
 </pre>
 
 This function does nothing.
@@ -80,15 +92,3 @@
 The minimum integer in the given list.
 
 
-<a id="my_namespace.one.three.does_nothing"></a>
-
-## my_namespace.one.three.does_nothing
-
-<pre>
-my_namespace.one.three.does_nothing()
-</pre>
-
-This function does nothing.
-
-
-
diff --git a/test/testdata/multi_level_namespace_test_with_whitelist/golden.md b/test/testdata/multi_level_namespace_test_with_allowlist/golden.md
similarity index 91%
rename from test/testdata/multi_level_namespace_test_with_whitelist/golden.md
rename to test/testdata/multi_level_namespace_test_with_allowlist/golden.md
index 1ca1608..f35b10d 100644
--- a/test/testdata/multi_level_namespace_test_with_whitelist/golden.md
+++ b/test/testdata/multi_level_namespace_test_with_allowlist/golden.md
@@ -1,27 +1,9 @@
 <!-- Generated with Stardoc: http://skydoc.bazel.build -->
 
-A test that verifies documenting a multi-leveled namespace of functions with whitelist symbols.
-The whitelist symbols should cause everything in my_namespace to to be documented, but only a
+A test that verifies documenting a multi-leveled namespace of functions with allowlist symbols.
+The allowlist symbols should cause everything in my_namespace to to be documented, but only a
 specific symbol in other_namespace to be documented.
 
-<a id="my_namespace.min"></a>
-
-## my_namespace.min
-
-<pre>
-my_namespace.min(<a href="#my_namespace.min-integers">integers</a>)
-</pre>
-
-Returns the minimum of given elements.
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="my_namespace.min-integers"></a>integers |  <p align="center"> - </p>   |  none |
-
-
 <a id="my_namespace.math.min"></a>
 
 ## my_namespace.math.min
@@ -40,6 +22,24 @@
 | <a id="my_namespace.math.min-integers"></a>integers |  <p align="center"> - </p>   |  none |
 
 
+<a id="my_namespace.min"></a>
+
+## my_namespace.min
+
+<pre>
+my_namespace.min(<a href="#my_namespace.min-integers">integers</a>)
+</pre>
+
+Returns the minimum of given elements.
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="my_namespace.min-integers"></a>integers |  <p align="center"> - </p>   |  none |
+
+
 <a id="other_namespace.foo.nothing"></a>
 
 ## other_namespace.foo.nothing
diff --git a/test/testdata/multi_level_namespace_test_with_whitelist/input.bzl b/test/testdata/multi_level_namespace_test_with_allowlist/input.bzl
similarity index 84%
rename from test/testdata/multi_level_namespace_test_with_whitelist/input.bzl
rename to test/testdata/multi_level_namespace_test_with_allowlist/input.bzl
index f070309..5146d11 100644
--- a/test/testdata/multi_level_namespace_test_with_whitelist/input.bzl
+++ b/test/testdata/multi_level_namespace_test_with_allowlist/input.bzl
@@ -1,5 +1,5 @@
-"""A test that verifies documenting a multi-leveled namespace of functions with whitelist symbols.
-The whitelist symbols should cause everything in my_namespace to to be documented, but only a
+"""A test that verifies documenting a multi-leveled namespace of functions with allowlist symbols.
+The allowlist symbols should cause everything in my_namespace to to be documented, but only a
 specific symbol in other_namespace to be documented."""
 
 def _min(integers):
diff --git a/test/testdata/namespace_test/golden.md b/test/testdata/namespace_test/golden.md
index f840fcf..850dcf1 100755
--- a/test/testdata/namespace_test/golden.md
+++ b/test/testdata/namespace_test/golden.md
@@ -21,28 +21,6 @@
 | <a id="my_namespace.assert_non_empty-other_list"></a>other_list |  The second list   |  none |
 
 
-<a id="my_namespace.min"></a>
-
-## my_namespace.min
-
-<pre>
-my_namespace.min(<a href="#my_namespace.min-integers">integers</a>)
-</pre>
-
-Returns the minimum of given elements.
-
-**PARAMETERS**
-
-
-| Name  | Description | Default Value |
-| :------------- | :------------- | :------------- |
-| <a id="my_namespace.min-integers"></a>integers |  A list of integers. Must not be empty.   |  none |
-
-**RETURNS**
-
-The minimum integer in the given list.
-
-
 <a id="my_namespace.join_strings"></a>
 
 ## my_namespace.join_strings
@@ -66,3 +44,25 @@
 The joined string.
 
 
+<a id="my_namespace.min"></a>
+
+## my_namespace.min
+
+<pre>
+my_namespace.min(<a href="#my_namespace.min-integers">integers</a>)
+</pre>
+
+Returns the minimum of given elements.
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="my_namespace.min-integers"></a>integers |  A list of integers. Must not be empty.   |  none |
+
+**RETURNS**
+
+The minimum integer in the given list.
+
+
diff --git a/test/testdata/provider_basic_test/input.bzl b/test/testdata/provider_basic_test/input.bzl
index bb76c0d..959986f 100644
--- a/test/testdata/provider_basic_test/input.bzl
+++ b/test/testdata/provider_basic_test/input.bzl
@@ -18,3 +18,8 @@
         "favorite_color": "A string representing my favorite color",
     },
 )
+
+named_providers_are_hashable = {
+    MyFooInfo: "MyFooInfo is hashable",
+    MyVeryDocumentedInfo: "So is MyVeryDocumentedInfo",
+}
diff --git a/test/testdata/pure_markdown_template_test/golden.md b/test/testdata/pure_markdown_template_test/golden.md
new file mode 100644
index 0000000..83a0e5c
--- /dev/null
+++ b/test/testdata/pure_markdown_template_test/golden.md
@@ -0,0 +1,92 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+
+Input file for markdown template test
+
+<a id="example_rule"></a>
+
+## example_rule
+
+<pre>
+example_rule(<a href="#example_rule-name">name</a>, <a href="#example_rule-first">first</a>, <a href="#example_rule-second">second</a>)
+</pre>
+
+Small example of rule using a markdown template.
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="example_rule-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="example_rule-first"></a>first |  This is the first attribute   | String | optional | <code>""</code> |
+| <a id="example_rule-second"></a>second |  -   | String | optional | <code>"2"</code> |
+
+
+<a id="ExampleProviderInfo"></a>
+
+## ExampleProviderInfo
+
+<pre>
+ExampleProviderInfo(<a href="#ExampleProviderInfo-foo">foo</a>, <a href="#ExampleProviderInfo-bar">bar</a>, <a href="#ExampleProviderInfo-baz">baz</a>)
+</pre>
+
+Small example of provider using a markdown template.
+
+**FIELDS**
+
+
+| Name  | Description |
+| :------------- | :------------- |
+| <a id="ExampleProviderInfo-foo"></a>foo |  A string representing foo    |
+| <a id="ExampleProviderInfo-bar"></a>bar |  A string representing bar    |
+| <a id="ExampleProviderInfo-baz"></a>baz |  A string representing baz    |
+
+
+<a id="example_function"></a>
+
+## example_function
+
+<pre>
+example_function(<a href="#example_function-foo">foo</a>, <a href="#example_function-bar">bar</a>)
+</pre>
+
+Small example of function using a markdown template.
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="example_function-foo"></a>foo |  This parameter does foo related things.   |  none |
+| <a id="example_function-bar"></a>bar |  This parameter does bar related things.<br><br>For example, it does things that require **multiple paragraphs** to explain.<br><br>Note: we should preserve the nested indent in the following code:<br><br><pre><code>json {     "key": "value" } </code></pre>   |  <code>"bar"</code> |
+
+
+<a id="example_aspect"></a>
+
+## example_aspect
+
+<pre>
+example_aspect(<a href="#example_aspect-name">name</a>, <a href="#example_aspect-first">first</a>, <a href="#example_aspect-second">second</a>)
+</pre>
+
+Small example of aspect using a markdown template.
+
+**ASPECT ATTRIBUTES**
+
+
+| Name | Type |
+| :------------- | :------------- |
+| deps| String |
+| attr_aspect| String |
+
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="example_aspect-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |   |
+| <a id="example_aspect-first"></a>first |  -   | <a href="https://bazel.build/concepts/labels">Label</a> | required |   |
+| <a id="example_aspect-second"></a>second |  This is the second attribute.   | String | optional |  <code>""</code>   |
+
+
diff --git a/test/testdata/pure_markdown_template_test/input.bzl b/test/testdata/pure_markdown_template_test/input.bzl
new file mode 100644
index 0000000..814acdd
--- /dev/null
+++ b/test/testdata/pure_markdown_template_test/input.bzl
@@ -0,0 +1,57 @@
+"""Input file for markdown template test"""
+
+# buildifier: disable=unused-variable
+def example_function(foo, bar = "bar"):
+    """Small example of function using a markdown template.
+
+    Args:
+        foo: This parameter does foo related things.
+        bar: This parameter does bar related things.
+
+            For example, it does things that require **multiple paragraphs** to explain.
+
+            Note: we should preserve the nested indent in the following code:
+
+            ```json
+            {
+                "key": "value"
+            }
+            ```
+    """
+    pass
+
+ExampleProviderInfo = provider(
+    doc = "Small example of provider using a markdown template.",
+    fields = {
+        "foo": "A string representing foo",
+        "bar": "A string representing bar",
+        "baz": "A string representing baz",
+    },
+)
+
+# buildifier: disable=unused-variable
+def _rule_impl(ctx):
+    return []
+
+example_rule = rule(
+    implementation = _rule_impl,
+    doc = "Small example of rule using a markdown template.",
+    attrs = {
+        "first": attr.string(doc = "This is the first attribute"),
+        "second": attr.string(default = "2"),
+    },
+)
+
+# buildifier: disable=unused-variable
+def _aspect_impl(ctx):
+    return []
+
+example_aspect = aspect(
+    implementation = _aspect_impl,
+    doc = "Small example of aspect using a markdown template.",
+    attr_aspects = ["deps", "attr_aspect"],
+    attrs = {
+        "first": attr.label(mandatory = True, allow_single_file = True),
+        "second": attr.string(doc = "This is the second attribute."),
+    },
+)
diff --git a/test/testdata/repo_rules_test/golden.md b/test/testdata/repo_rules_test/golden.md
index 87391e3..da8a1ba 100755
--- a/test/testdata/repo_rules_test/golden.md
+++ b/test/testdata/repo_rules_test/golden.md
@@ -19,6 +19,6 @@
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <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 |  A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.&lt;p&gt;For example, an entry <code>"@foo": "@bar"</code> declares that, for any time this repository depends on <code>@foo</code> (such as a dependency on <code>@foo//some:target</code>, it should actually resolve that dependency within globally-declared <code>@bar</code> (<code>@bar//some:target</code>).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
-| <a id="my_repo-useless"></a>useless |  This argument will be ingored. You don't have to specify it, but you may.   | String | optional | <code>"ignoreme"</code> |
+| <a id="my_repo-useless"></a>useless |  This argument will be ignored. You don't have to specify it, but you may.   | String | optional | <code>"ignoreme"</code> |
 
 
diff --git a/test/testdata/repo_rules_test/input.bzl b/test/testdata/repo_rules_test/input.bzl
index 023d92d..aca64f8 100644
--- a/test/testdata/repo_rules_test/input.bzl
+++ b/test/testdata/repo_rules_test/input.bzl
@@ -7,7 +7,7 @@
     doc = "Minimal example of a repository rule.",
     attrs = {
         "useless": attr.string(
-            doc = "This argument will be ingored. You don't have to specify it, but you may.",
+            doc = "This argument will be ignored. You don't have to specify it, but you may.",
             default = "ignoreme",
         ),
     },
diff --git a/update-release-binary.sh b/update-release-binary.sh
index 1423fbf..bf1307a 100755
--- a/update-release-binary.sh
+++ b/update-release-binary.sh
@@ -28,15 +28,6 @@
 
 echo "** Stardoc copied."
 
-echo "** Building Renderer from source..."
-bazel build --java_language_version=11 @io_bazel//src/main/java/com/google/devtools/build/skydoc/renderer:renderer_deploy.jar
-
-echo "** Copying Renderer binary..."
-cp bazel-bin/external/io_bazel/src/main/java/com/google/devtools/build/skydoc/renderer/renderer_deploy.jar \
-    stardoc/renderer_binary.jar
-
-echo "** Renderer copied."
-
 echo "** Copying stardoc_output.proto from source..."
 cp $(bazel info output_base)/external/io_bazel/src/main/java/com/google/devtools/build/skydoc/rendering/proto/stardoc_output.proto stardoc/proto/stardoc_output.proto
 echo "** stardoc_output.proto copied."