Add rule_or_page#heading syntax to RuleLinkExpander and support for expanding
rule_name_implicit_outputs.

This change adds support for new syntax for referencing a heading for a rule or
static BE page.

--
MOS_MIGRATED_REVID=122509323
diff --git a/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java b/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
index da7a1da..f1f7109 100644
--- a/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
+++ b/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
@@ -102,6 +102,9 @@
   public static final Pattern BLAZE_RULE_LINK = Pattern.compile(
       "\\$\\{link (([a-zA-Z_-]+)(\\.([a-zA-Z_\\.-]+))?)\\}");
 
+  public static final Pattern BLAZE_RULE_HEADING_LINK = Pattern.compile(
+      "\\$\\{link (([a-zA-Z_-]+)\\#([a-zA-Z_\\.-]+))\\}");
+
   /**
    * i.e. <!-- #BLAZE_RULE(NAME = RULE_NAME, TYPE = RULE_TYPE, FAMILY = RULE_FAMILY) -->
    * i.e. <!-- #BLAZE_RULE(...)[DEPRECATED] -->
diff --git a/src/main/java/com/google/devtools/build/docgen/RuleLinkExpander.java b/src/main/java/com/google/devtools/build/docgen/RuleLinkExpander.java
index 65147d0..e7d8b38 100644
--- a/src/main/java/com/google/devtools/build/docgen/RuleLinkExpander.java
+++ b/src/main/java/com/google/devtools/build/docgen/RuleLinkExpander.java
@@ -30,6 +30,7 @@
 class RuleLinkExpander {
   private static final String EXAMPLES_SUFFIX = "_examples";
   private static final String ARGS_SUFFIX = "_args";
+  private static final String IMPLICIT_OUTPUTS_SUFFIX = "_implicit_outputs";
   private static final String FUNCTIONS_PAGE = "functions";
 
   private static final Set<String> STATIC_PAGES = ImmutableSet.<String>of(
@@ -64,13 +65,10 @@
     matcher.appendReplacement(sb, Matcher.quoteReplacement(link));
   }
 
-  /**
-   * Expands all rule references in the input HTML documentation.
-   *
-   * @param htmlDoc The input HTML documentation with ${link foo.bar} references.
-   * @return The HTML documentation with all link references expanded.
+  /*
+   * Match and replace all ${link rule.attribute} references.
    */
-  public String expand(String htmlDoc) throws IllegalArgumentException {
+  private String expandRuleLinks(String htmlDoc) throws IllegalArgumentException {
     Matcher matcher = DocgenConsts.BLAZE_RULE_LINK.matcher(htmlDoc);
     StringBuffer sb = new StringBuffer(htmlDoc.length());
     while (matcher.find()) {
@@ -87,11 +85,21 @@
         continue;
       }
 
-      // The name is referencing the examples or arguments of a rule (e.g. "cc_library_args" or
-      // "cc_library_examples"). Strip the suffix and then try matching the name to a rule family.
-      if (name.endsWith(EXAMPLES_SUFFIX) || name.endsWith(ARGS_SUFFIX)) {
-        String ruleName = name.substring(
-            0, name.indexOf(name.endsWith(EXAMPLES_SUFFIX) ? EXAMPLES_SUFFIX : ARGS_SUFFIX));
+      // The name is referencing the examples, arguments, or implicit outputs of a rule (e.g.
+      // "cc_library_args", "cc_library_examples", or "java_binary_implicit_outputs"). Strip the
+      // suffix and then try matching the name to a rule family.
+      if (name.endsWith(EXAMPLES_SUFFIX)
+          || name.endsWith(ARGS_SUFFIX)
+          || name.endsWith(IMPLICIT_OUTPUTS_SUFFIX)) {
+        int endIndex;
+        if (name.endsWith(EXAMPLES_SUFFIX)) {
+          endIndex = name.indexOf(EXAMPLES_SUFFIX);
+        } else if (name.endsWith(ARGS_SUFFIX)) {
+          endIndex = name.indexOf(ARGS_SUFFIX);
+        } else {
+          endIndex = name.indexOf(IMPLICIT_OUTPUTS_SUFFIX);
+        }
+        String ruleName = name.substring(0, endIndex);
         if (ruleIndex.containsKey(ruleName)) {
           appendRuleLink(matcher, sb, ruleName, ref);
           continue;
@@ -102,6 +110,7 @@
       // common-definitions. Generate a link to that page, and append the page heading if
       // specified. For example, ${link common-definitions.label-expansion} expands to
       // common-definitions.html#label-expansion.
+      // TODO(dzc): Deprecate this in favor of the ${link static-page#heading} syntax below.
       if (STATIC_PAGES.contains(name)) {
         String link = name + ".html";
         // The fourth capture group matches the attribute name, or page heading, e.g.
@@ -116,9 +125,63 @@
 
       // If the reference does not match any rule or static page, throw an exception.
       throw new IllegalArgumentException(
-          "link tag does not match any rule or BE page: " + matcher.group());
+          "Rule family " + name + " in link tag does not match any rule or BE page: "
+          + matcher.group());
     }
     matcher.appendTail(sb);
     return sb.toString();
   }
+
+  /*
+   * Match and replace all ${link rule#heading} references.
+   */
+  private String expandRuleHeadingLinks(String htmlDoc) throws IllegalArgumentException {
+    Matcher matcher = DocgenConsts.BLAZE_RULE_HEADING_LINK.matcher(htmlDoc);
+    StringBuffer sb = new StringBuffer(htmlDoc.length());
+    while (matcher.find()) {
+      // The second capture group only matches the rule name, e.g. "cc_library" in
+      // "cc_library#some_heading"
+      String name = matcher.group(2);
+      // The third capture group only matches the heading, e.g. "some_heading" in
+      // "cc_library#some_heading"
+      String heading = matcher.group(3);
+
+      // The name in the reference is the name of a rule. Get the rule family for the rule and
+      // replace the reference with the link in the form of rule-family.html#heading. Examples of
+      // this include custom <a name="heading"> tags in the description or examples for the rule.
+      if (ruleIndex.containsKey(name)) {
+        String ruleFamily = ruleIndex.get(name);
+        String link = ruleFamily + ".html#" + heading;
+        matcher.appendReplacement(sb, Matcher.quoteReplacement(link));
+        continue;
+      }
+
+      // The name is of a static page, such as common.definitions. Generate a link to that page, and
+      // append the page heading. For example, ${link common-definitions#label-expansion} expands to
+      // common-definitions.html#label-expansion.
+      if (STATIC_PAGES.contains(name)) {
+        String link = name + ".html#" + heading;
+        matcher.appendReplacement(sb, Matcher.quoteReplacement(link));
+        continue;
+      }
+
+      // If the reference does not match any rule or static page, throw an exception.
+      throw new IllegalArgumentException(
+          "Rule family " + name + " in link tag does not match any rule or BE page: "
+          + matcher.group());
+    }
+    matcher.appendTail(sb);
+    return sb.toString();
+  }
+
+  /**
+   * Expands all rule references in the input HTML documentation.
+   *
+   * @param htmlDoc The input HTML documentation with ${link foo.bar} references.
+   * @return The HTML documentation with all link references expanded.
+   */
+  public String expand(String htmlDoc) throws IllegalArgumentException {
+    String expanded = expandRuleLinks(htmlDoc);
+    return expandRuleHeadingLinks(expanded);
+  }
 }