In Markdown table cells, apply HTML escaping only to code blocks, and apply it properly (#167)

In Markdown table cells, apply HTML escaping only to code blocks, and apply it properly

Since #161 removed HTML escaping for defaults and function docstrings, we should do the same for attribute and param docs in table cells.

The only limitations Markdown places on table cells are:
* no pipe characters (they must be escaped with a backslash)
* no newlines (they must be transformed into `<br>` or an HTML entity)

The latter restriction makes it impossible to have a fenced code block inside a table cell.

Therefore:
* we do not escape HTML or Markdown markup outside a fenced code block
* we keep existing logic for escaping newlines outside a fenced code block
* we fix fence detection (e.g. allowing more than 3 fence characters to support embedded code blocks in code blocks, allowing tildes as fence characters, properly handling language names, etc.);
* in code block content, we escape HTML, and we escape newlines as HTML entities (since `<br>` does not work in a `<pre><code>` block) - finally fixing code block newlines in table cells.
    
This is a followup to #161.

Partially addresses #118
diff --git a/docs/stardoc_rule.md b/docs/stardoc_rule.md
index 3c5dc79..0d8f0a8 100644
--- a/docs/stardoc_rule.md
+++ b/docs/stardoc_rule.md
@@ -25,17 +25,17 @@
 | <a id="stardoc-deps"></a>deps |  A list of bzl_library dependencies which the input depends on.   |  `[]` |
 | <a id="stardoc-format"></a>format |  The format of the output file. Valid values: 'markdown' or 'proto'.   |  `"markdown"` |
 | <a id="stardoc-symbol_names"></a>symbol_names |  A list of symbol names to generate documentation for. These should correspond to the names of rule definitions in the input file. If this list is empty, then documentation for all exported rule definitions will be generated.   |  `[]` |
-| <a id="stardoc-semantic_flags"></a>semantic_flags |  A list of canonical flags to affect Starlark semantics for the Starlark interpreter during documentation generation. This should only be used to maintain compatibility with non-default semantic flags required to use the given Starlark symbols.<br><br>For example, if <code>//foo:bar.bzl</code> does not build except when a user would specify <code>--incompatible_foo_semantic=false</code>, then this attribute should contain "--incompatible_foo_semantic=false".   |  `[]` |
-| <a id="stardoc-stardoc"></a>stardoc |  The location of the legacy Stardoc extractor. Ignored when using the native <code>starlark_doc_extract</code> rule.   |  `Label("//stardoc:prebuilt_stardoc_binary")` |
+| <a id="stardoc-semantic_flags"></a>semantic_flags |  A list of canonical flags to affect Starlark semantics for the Starlark interpreter during documentation generation. This should only be used to maintain compatibility with non-default semantic flags required to use the given Starlark symbols.<br><br>For example, if `//foo:bar.bzl` does not build except when a user would specify `--incompatible_foo_semantic=false`, then this attribute should contain "--incompatible_foo_semantic=false".   |  `[]` |
+| <a id="stardoc-stardoc"></a>stardoc |  The location of the legacy Stardoc extractor. Ignored when using the native `starlark_doc_extract` rule.   |  `Label("//stardoc:prebuilt_stardoc_binary")` |
 | <a id="stardoc-renderer"></a>renderer |  The location of the renderer tool.   |  `Label("//stardoc:renderer")` |
 | <a id="stardoc-aspect_template"></a>aspect_template |  The input file template for generating documentation of aspects   |  `Label("//stardoc:templates/markdown_tables/aspect.vm")` |
 | <a id="stardoc-func_template"></a>func_template |  The input file template for generating documentation of functions.   |  `Label("//stardoc:templates/markdown_tables/func.vm")` |
 | <a id="stardoc-header_template"></a>header_template |  The input file template for the header of the output documentation.   |  `Label("//stardoc:templates/markdown_tables/header.vm")` |
 | <a id="stardoc-provider_template"></a>provider_template |  The input file template for generating documentation of providers.   |  `Label("//stardoc:templates/markdown_tables/provider.vm")` |
 | <a id="stardoc-rule_template"></a>rule_template |  The input file template for generating documentation of rules.   |  `Label("//stardoc:templates/markdown_tables/rule.vm")` |
-| <a id="stardoc-repository_rule_template"></a>repository_rule_template |  The input file template for generating documentation of repository rules. This template is used only when using the native <code>starlark_doc_extract</code> rule.   |  `Label("//stardoc:templates/markdown_tables/repository_rule.vm")` |
-| <a id="stardoc-module_extension_template"></a>module_extension_template |  The input file template for generating documentation of module extensions. This template is used only when using the native <code>starlark_doc_extract</code> rule.   |  `Label("//stardoc:templates/markdown_tables/module_extension.vm")` |
-| <a id="stardoc-use_starlark_doc_extract"></a>use_starlark_doc_extract |  Use the native <code>starlark_doc_extract</code> rule if available.   |  `True` |
+| <a id="stardoc-repository_rule_template"></a>repository_rule_template |  The input file template for generating documentation of repository rules. This template is used only when using the native `starlark_doc_extract` rule.   |  `Label("//stardoc:templates/markdown_tables/repository_rule.vm")` |
+| <a id="stardoc-module_extension_template"></a>module_extension_template |  The input file template for generating documentation of module extensions. This template is used only when using the native `starlark_doc_extract` rule.   |  `Label("//stardoc:templates/markdown_tables/module_extension.vm")` |
+| <a id="stardoc-use_starlark_doc_extract"></a>use_starlark_doc_extract |  Use the native `starlark_doc_extract` rule if available.   |  `True` |
 | <a id="stardoc-kwargs"></a>kwargs |  Further arguments to pass to stardoc.   |  none |
 
 
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
index 00580c2..afb2b5e 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java
@@ -24,15 +24,16 @@
 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.RepositoryRuleInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleExtensionInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleExtensionTagClassInfo;
+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.RepositoryRuleInfo;
+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.Matcher;
 import java.util.regex.Pattern;
 
 /** Contains a number of utility methods for markdown rendering. */
@@ -46,44 +47,119 @@
   }
 
   /**
-   * Return a string that formats the input string so it is displayable in a markdown table cell.
-   * This performs the following operations:
+   * Formats the input string so that 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.
+   *   <li>Escapes pipe characters ({@code |}) as {@code \|}.
+   *   <li>Transforms Markdown code blocks ({@code ```}) into HTML preformatted code blocks, and
+   *       transforms newlines within those code blocks into character entities
+   *   <li>Transforms remaining 'new paragraph' patterns (two or more sequential newline characters)
+   *       into line break HTML tags.
+   *   <li>Turns remaining newlines into spaces (as they generally indicate intended line wrap).
    * </ul>
+   *
+   * TODO(https://github.com/bazelbuild/stardoc/issues/118): also format Markdown lists as HTML.
    */
-  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', ' ');
+  public static String markdownCellFormat(String docString) {
+    return new MarkdownCellFormatter(docString).format();
   }
 
-  private static String replaceWithTag(
-      String wholeString, String stringToReplace, String openTag, String closeTag) {
-    String remainingString = wholeString;
-    StringBuilder resultString = new StringBuilder();
+  // See https://github.github.com/gfm
+  private static final class MarkdownCellFormatter {
+    // Lines of the input docstring, without newline terminators.
+    private final ImmutableList<String> lines;
+    // Index of the current line in lines, 0-based.
+    int currentLine;
+    // Formatted result.
+    StringBuilder result;
 
-    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);
+    private static final Pattern CODE_BLOCK_OPENING_FENCE =
+        Pattern.compile("^ {0,3}(?<fence>```+|~~~+) *(?<lang>\\w*)[^`~]*$");
+
+    MarkdownCellFormatter(String docString) {
+      lines = docString.trim().replace("|", "\\|").lines().collect(toImmutableList());
+      currentLine = 0;
+      result = new StringBuilder();
     }
-    resultString.append(remainingString);
-    return resultString.toString();
+
+    /** Consumes the input and yields the formatted result. */
+    String format() {
+      boolean prefixContentWithSpace = false;
+      for (; currentLine < lines.size(); currentLine++) {
+        if (formatParagraphBreak()) {
+          prefixContentWithSpace = false;
+          continue;
+        }
+        if (prefixContentWithSpace) {
+          result.append(" ");
+        }
+        prefixContentWithSpace = true;
+        if (formatFencedCodeBlock()) {
+          continue;
+        }
+        result.append(lines.get(currentLine));
+      }
+      return result.toString();
+    }
+
+    /**
+     * If a fenced code block begins at {@link #currentLine}, render to {@link #result}, update
+     * {@link #currentLine} to point to the closing fence, and return true.
+     */
+    private boolean formatFencedCodeBlock() {
+      // See https://github.github.com/gfm/#fenced-code-blocks
+      Matcher opening = CODE_BLOCK_OPENING_FENCE.matcher(lines.get(currentLine));
+      if (!opening.matches()) {
+        return false;
+      }
+      Pattern closingFence = Pattern.compile("^ {0,3}" + opening.group("fence") + " *$");
+      for (int closingLine = currentLine + 1; closingLine < lines.size(); closingLine++) {
+        if (closingFence.matcher(lines.get(closingLine)).matches()) {
+          // We found the closing fence: format the block's contents as HTML.
+          String language = opening.group("lang");
+          if (language != null && !language.isEmpty()) {
+            result.append("<pre><code class=\"language-").append(language).append("\">");
+          } else {
+            result.append("<pre><code>");
+          }
+          int firstContentLine = currentLine + 1;
+          for (int i = firstContentLine; i < closingLine; i++) {
+            if (i > firstContentLine) {
+              result.append(newlineEscape("\n"));
+            }
+            result.append(htmlEscape(lines.get(i)));
+          }
+          result.append("</code></pre>");
+          currentLine = closingLine;
+          return true;
+        }
+      }
+      // We did not find the closing fence.
+      return false;
+    }
+
+    /**
+     * If blank lines appear at {@link #currentLine}, render to {@link #result}, update {@link
+     * #currentLine} to point to the last line of the break, and return true.
+     */
+    private boolean formatParagraphBreak() {
+      int numEmptyLines = 0;
+      for (int i = currentLine; i < lines.size(); i++) {
+        if (lines.get(i).isEmpty()) {
+          numEmptyLines++;
+        } else {
+          break;
+        }
+      }
+      if (numEmptyLines > 0) {
+        result.append("<br><br>");
+        currentLine += numEmptyLines - 1;
+        return true;
+      }
+      return false;
+    }
   }
 
   /**
@@ -91,10 +167,15 @@
    *
    * <p>For example: 'Information with <brackets>.' becomes 'Information with &lt;brackets&gt;'.
    */
-  public String htmlEscape(String docString) {
+  public static String htmlEscape(String docString) {
     return docString.replace("<", "&lt;").replace(">", "&gt;");
   }
 
+  /** Returns a string that escapes newlines with HTML entities. */
+  private static String newlineEscape(String docString) {
+    return docString.replace("\n", "&#10;");
+  }
+
   private static final Pattern CONSECUTIVE_BACKTICKS = Pattern.compile("`+");
 
   /**
@@ -164,23 +245,25 @@
   }
 
   /**
-   * Return a string representing the repository rule summary for the given repository rule with the given name.
+   * Return a string representing the repository rule summary for the given repository rule with the
+   * given name.
    *
-   * <p>For example: 'my_repo_rule(foo, bar)'. The summary will contain hyperlinks for each attribute.
+   * <p>For example: 'my_repo_rule(foo, bar)'. The summary will contain hyperlinks for each
+   * attribute.
    */
   @SuppressWarnings("unused") // Used by markdown template.
   public String repositoryRuleSummary(String ruleName, RepositoryRuleInfo ruleInfo) {
     ImmutableList<String> attributeNames =
-        ruleInfo.getAttributeList().stream()
-            .map(AttributeInfo::getName)
-            .collect(toImmutableList());
+        ruleInfo.getAttributeList().stream().map(AttributeInfo::getName).collect(toImmutableList());
     return summary(ruleName, attributeNames);
   }
 
   /**
-   * Return a string representing the module extension summary for the given module extension with the given name.
+   * Return a string representing the module extension summary for the given module extension with
+   * the given name.
    *
    * <p>For example:
+   *
    * <pre>
    * my_ext = use_extension("//some:file.bzl", "my_ext")
    * my_ext.tag1(foo, bar)
@@ -192,13 +275,19 @@
   @SuppressWarnings("unused") // Used by markdown template.
   public String moduleExtensionSummary(String extensionName, ModuleExtensionInfo extensionInfo) {
     StringBuilder summaryBuilder = new StringBuilder();
-    summaryBuilder.append(String.format("%s = use_extension(\"%s\", \"%s\")", extensionName, extensionBzlFile, extensionName));
+    summaryBuilder.append(
+        String.format(
+            "%s = use_extension(\"%s\", \"%s\")", extensionName, extensionBzlFile, extensionName));
     for (ModuleExtensionTagClassInfo tagClass : extensionInfo.getTagClassList()) {
       ImmutableList<String> attributeNames =
-        tagClass.getAttributeList().stream()
-            .map(AttributeInfo::getName)
-            .collect(toImmutableList());
-      summaryBuilder.append("\n").append(summary(String.format("%s.%s", extensionName, tagClass.getTagName()), attributeNames));
+          tagClass.getAttributeList().stream()
+              .map(AttributeInfo::getName)
+              .collect(toImmutableList());
+      summaryBuilder
+          .append("\n")
+          .append(
+              summary(
+                  String.format("%s.%s", extensionName, tagClass.getTagName()), attributeNames));
     }
     return summaryBuilder.toString();
   }
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
index 14d3c2a..4c37a38 100644
--- a/src/test/java/com/google/devtools/build/skydoc/rendering/MarkdownUtilTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/rendering/MarkdownUtilTest.java
@@ -47,4 +47,38 @@
     assertThat(MarkdownUtil.markdownCodeSpan("foo`")).isEqualTo("`` foo` ``");
     assertThat(MarkdownUtil.markdownCodeSpan("foo``")).isEqualTo("``` foo`` ```");
   }
+
+  @Test
+  public void markdownCellFormat_pipes() {
+    assertThat(MarkdownUtil.markdownCellFormat("foo|bar")).isEqualTo("foo\\|bar");
+    assertThat(MarkdownUtil.markdownCellFormat("|\\|foobar||")).isEqualTo("\\|\\\\|foobar\\|\\|");
+  }
+
+  @Test
+  public void markdownCellFormat_newlines() {
+    assertThat(MarkdownUtil.markdownCellFormat("\nfoo\nbar\n\nbaz\r\n\r\n\r\nqux\r\n"))
+        .isEqualTo("foo bar<br><br>baz<br><br>qux");
+    // Newline escapes are not expanded
+    assertThat(MarkdownUtil.markdownCellFormat("hello\\r\\nworld")).isEqualTo("hello\\r\\nworld");
+  }
+
+  @Test
+  public void markdownCellFormat_codeBlocks() {
+    assertThat(MarkdownUtil.markdownCellFormat("```\nhello();\n```"))
+        .isEqualTo("<pre><code>hello();</code></pre>");
+    assertThat(MarkdownUtil.markdownCellFormat("```\nhello();\n```\nor\n~~~\nbye();\n~~~"))
+        .isEqualTo("<pre><code>hello();</code></pre> or <pre><code>bye();</code></pre>");
+    assertThat(MarkdownUtil.markdownCellFormat("```bash\ncat foo.txt | cmd > /dev/null\n```"))
+        .isEqualTo(
+            "<pre><code class=\"language-bash\">cat foo.txt \\| cmd &gt; /dev/null</code></pre>");
+    assertThat(MarkdownUtil.markdownCellFormat("````\n```\n```\n````"))
+        .isEqualTo("<pre><code>```&#10;```</code></pre>");
+  }
+
+  @Test
+  public void markdownCellFormat_inlineMarkup() {
+    assertThat(MarkdownUtil.markdownCellFormat("<b>bold</b> <i>italic</i>"))
+        .isEqualTo("<b>bold</b> <i>italic</i>");
+    assertThat(MarkdownUtil.markdownCellFormat("**bold** _italic_")).isEqualTo("**bold** _italic_");
+  }
 }
diff --git a/test/bzlmod/docs.md.golden b/test/bzlmod/docs.md.golden
index fdc1c47..3b671cc 100644
--- a/test/bzlmod/docs.md.golden
+++ b/test/bzlmod/docs.md.golden
@@ -17,6 +17,6 @@
 
 | Name  | Description | Default Value |
 | :------------- | :------------- | :------------- |
-| <a id="write_host_constraints-name"></a>name |  The name of the target. The output file will be named <code>&lt;name&gt;.txt</code>.   |  none |
+| <a id="write_host_constraints-name"></a>name |  The name of the target. The output file will be named `<name>.txt`.   |  none |
 
 
diff --git a/test/testdata/angle_bracket_test/golden.md b/test/testdata/angle_bracket_test/golden.md
index d487cfd..06496b2 100644
--- a/test/testdata/angle_bracket_test/golden.md
+++ b/test/testdata/angle_bracket_test/golden.md
@@ -12,7 +12,6 @@
 (`\\<` becomes \<), or by using HTML entities (`&lt;` becomes &lt;).
 Angle brackets are also preserved in inline code blocks (`#include <vector>`).
 
-
 <a id="my_anglebrac"></a>
 
 ## my_anglebrac
@@ -29,8 +28,8 @@
 | Name  | Description | Type | Mandatory | Default |
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <a id="my_anglebrac-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="my_anglebrac-also_useless"></a>also_useless |  Args with some formatted tags: <code>&lt;tag&gt;</code>   | String | optional |  `"1<<5"`  |
-| <a id="my_anglebrac-useless"></a>useless |  Args with some tags: &lt;tag1&gt;, &lt;tag2&gt;   | String | optional |  `"Find \\<brackets>"`  |
+| <a id="my_anglebrac-also_useless"></a>also_useless |  Args with some formatted tags: `<tag>` and <pre><code class="language-xml">&lt;tag2&gt;x&lt;/tag2&gt;</code></pre>   | String | optional |  `"1<<5"`  |
+| <a id="my_anglebrac-useless"></a>useless |  Args with some tags: \<tag1>, \<tag2>   | String | optional |  `"Find \\<brackets>"`  |
 
 
 <a id="bracketuse"></a>
@@ -48,7 +47,7 @@
 
 | Name  | Description |
 | :------------- | :------------- |
-| <a id="bracketuse-foo"></a>foo |  A string representing &lt;foo&gt;    |
+| <a id="bracketuse-foo"></a>foo |  A string representing \<foo>    |
 | <a id="bracketuse-bar"></a>bar |  A string representing bar    |
 | <a id="bracketuse-baz"></a>baz |  A string representing baz    |
 
@@ -78,7 +77,7 @@
 
 | Name  | Description | Default Value |
 | :------------- | :------------- | :------------- |
-| <a id="bracket_function-param"></a>param |  an arg with **formatted** docstring, <code>&lt;default&gt;</code> by default.   |  `"<default>"` |
+| <a id="bracket_function-param"></a>param |  an arg with **formatted** docstring, `<default>` by default.   |  `"<default>"` |
 | <a id="bracket_function-md_string"></a>md_string |  A markdown string.   |  ``"foo `1<<10` bar"`` |
 
 **RETURNS**
@@ -106,7 +105,6 @@
 ```
 which includes angle brackets.
 
-
 **ASPECT ATTRIBUTES**
 
 
@@ -121,6 +119,6 @@
 | Name  | Description | Type | Mandatory | Default |
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <a id="bracket_aspect-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="bracket_aspect-brackets"></a>brackets |  Attribute with \&lt;brackets&gt;   | String | optional |  `"<default>"`  |
+| <a id="bracket_aspect-brackets"></a>brackets |  Attribute with \<brackets>   | String | optional |  `"<default>"`  |
 
 
diff --git a/test/testdata/angle_bracket_test/input.bzl b/test/testdata/angle_bracket_test/input.bzl
index 6ef8c40..0915215 100644
--- a/test/testdata/angle_bracket_test/input.bzl
+++ b/test/testdata/angle_bracket_test/input.bzl
@@ -39,7 +39,7 @@
 bracketuse = provider(
     doc = "Information with \\<brackets>",
     fields = {
-        "foo": "A string representing <foo>",
+        "foo": "A string representing \\<foo>",
         "bar": "A string representing bar",
         "baz": "A string representing baz",
     },
@@ -54,11 +54,15 @@
     doc = "Rule with \\<brackets>",
     attrs = {
         "useless": attr.string(
-            doc = "Args with some tags: <tag1>, <tag2>",
+            doc = "Args with some tags: \\<tag1>, \\<tag2>",
             default = "Find \\<brackets>",
         ),
         "also_useless": attr.string(
-            doc = "Args with some formatted tags: `<tag>`",
+            doc = """Args with some formatted tags: `<tag>` and
+```xml
+<tag2>x</tag2>
+```
+""",
             default = "1<<5",
         ),
     },
diff --git a/test/testdata/angle_bracket_test/legacy_golden.md b/test/testdata/angle_bracket_test/legacy_golden.md
index a9ccc14..d6457b2 100644
--- a/test/testdata/angle_bracket_test/legacy_golden.md
+++ b/test/testdata/angle_bracket_test/legacy_golden.md
@@ -29,8 +29,8 @@
 | Name  | Description | Type | Mandatory | Default |
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <a id="my_anglebrac-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="my_anglebrac-also_useless"></a>also_useless |  Args with some formatted tags: <code>&lt;tag&gt;</code>   | String | optional |  `"1<<5"`  |
-| <a id="my_anglebrac-useless"></a>useless |  Args with some tags: &lt;tag1&gt;, &lt;tag2&gt;   | String | optional |  `"Find \<brackets>"`  |
+| <a id="my_anglebrac-also_useless"></a>also_useless |  Args with some formatted tags: `<tag>` and <pre><code class="language-xml">&lt;tag2&gt;x&lt;/tag2&gt;</code></pre>   | String | optional |  `"1<<5"`  |
+| <a id="my_anglebrac-useless"></a>useless |  Args with some tags: \<tag1>, \<tag2>   | String | optional |  `"Find \<brackets>"`  |
 
 
 <a id="bracketuse"></a>
@@ -48,7 +48,7 @@
 
 | Name  | Description |
 | :------------- | :------------- |
-| <a id="bracketuse-foo"></a>foo |  A string representing &lt;foo&gt;    |
+| <a id="bracketuse-foo"></a>foo |  A string representing \<foo>    |
 | <a id="bracketuse-bar"></a>bar |  A string representing bar    |
 | <a id="bracketuse-baz"></a>baz |  A string representing baz    |
 
@@ -78,7 +78,7 @@
 
 | Name  | Description | Default Value |
 | :------------- | :------------- | :------------- |
-| <a id="bracket_function-param"></a>param |  an arg with **formatted** docstring, <code>&lt;default&gt;</code> by default.   |  `"<default>"` |
+| <a id="bracket_function-param"></a>param |  an arg with **formatted** docstring, `<default>` by default.   |  `"<default>"` |
 | <a id="bracket_function-md_string"></a>md_string |  A markdown string.   |  ``"foo `1<<10` bar"`` |
 
 **RETURNS**
@@ -121,6 +121,6 @@
 | Name  | Description | Type | Mandatory | Default |
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <a id="bracket_aspect-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="bracket_aspect-brackets"></a>brackets |  Attribute with \&lt;brackets&gt;   | String | optional |  `"<default>"`  |
+| <a id="bracket_aspect-brackets"></a>brackets |  Attribute with \<brackets>   | String | optional |  `"<default>"`  |
 
 
diff --git a/test/testdata/function_basic_test/golden.md b/test/testdata/function_basic_test/golden.md
index d3db17d..bf758b1 100644
--- a/test/testdata/function_basic_test/golden.md
+++ b/test/testdata/function_basic_test/golden.md
@@ -63,7 +63,7 @@
 
 | 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="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 class="language-json">{&#10;    "key": "value"&#10;}</code></pre>   |  none |
 
 
 <a id="returns_a_thing"></a>
diff --git a/test/testdata/provider_basic_test/golden.md b/test/testdata/provider_basic_test/golden.md
index 4397bed..99cd944 100644
--- a/test/testdata/provider_basic_test/golden.md
+++ b/test/testdata/provider_basic_test/golden.md
@@ -43,11 +43,9 @@
 MyVeryDocumentedInfo(<a href="#MyVeryDocumentedInfo-favorite_food">favorite_food</a>, <a href="#MyVeryDocumentedInfo-favorite_color">favorite_color</a>)
 </pre>
 
-
 A provider with some really neat documentation.
 Look on my works, ye mighty, and despair!
 
-
 **FIELDS**
 
 
diff --git a/test/testdata/pure_markdown_template_test/golden.md b/test/testdata/pure_markdown_template_test/golden.md
index 9b5ecff..62a9327 100644
--- a/test/testdata/pure_markdown_template_test/golden.md
+++ b/test/testdata/pure_markdown_template_test/golden.md
@@ -58,7 +58,7 @@
 | 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>   |  `"bar"` |
+| <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 class="language-json">{&#10;    "key": "value"&#10;}</code></pre>   |  `"bar"` |
 
 
 <a id="example_aspect"></a>
diff --git a/test/testdata/repo_rules_test/golden.md b/test/testdata/repo_rules_test/golden.md
index 75befef..67a1074 100644
--- a/test/testdata/repo_rules_test/golden.md
+++ b/test/testdata/repo_rules_test/golden.md
@@ -18,7 +18,7 @@
 | Name  | Description | Type | Mandatory | Default |
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <a id="my_repo-name"></a>name |  A unique name for this repository.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="my_repo-repo_mapping"></a>repo_mapping |  In <code>WORKSPACE</code> context only: a dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<br><br>For example, an entry <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>).<br><br>This attribute is _not_ supported in <code>MODULE.bazel</code> context (when invoking a repository rule inside a module extension's implementation function).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional |  |
+| <a id="my_repo-repo_mapping"></a>repo_mapping |  In `WORKSPACE` context only: a dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<br><br>For example, an entry `"@foo": "@bar"` declares that, for any time this repository depends on `@foo` (such as a dependency on `@foo//some:target`, it should actually resolve that dependency within globally-declared `@bar` (`@bar//some:target`).<br><br>This attribute is _not_ supported in `MODULE.bazel` context (when invoking a repository rule inside a module extension's implementation function).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional |  |
 | <a id="my_repo-useless"></a>useless |  This argument will be ignored. You don't have to specify it, but you may.   | String | optional |  `"ignoreme"`  |
 
 **ENVIRONMENT VARIABLES**
diff --git a/test/testdata/repo_rules_test/legacy_golden.md b/test/testdata/repo_rules_test/legacy_golden.md
index 639b5c7..c0084fa 100644
--- a/test/testdata/repo_rules_test/legacy_golden.md
+++ b/test/testdata/repo_rules_test/legacy_golden.md
@@ -18,7 +18,7 @@
 | Name  | Description | Type | Mandatory | Default |
 | :------------- | :------------- | :------------- | :------------- | :------------- |
 | <a id="my_repo-name"></a>name |  A unique name for this repository.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
-| <a id="my_repo-repo_mapping"></a>repo_mapping |  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-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.<p>For example, an entry `"@foo": "@bar"` declares that, for any time this repository depends on `@foo` (such as a dependency on `@foo//some:target`, it should actually resolve that dependency within globally-declared `@bar` (`@bar//some:target`).   | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required |  |
 | <a id="my_repo-useless"></a>useless |  This argument will be ignored. You don't have to specify it, but you may.   | String | optional |  `"ignoreme"`  |