|  | // Copyright 2014 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.docgen; | 
|  |  | 
|  | import com.google.common.collect.ImmutableMap; | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | import java.nio.file.Paths; | 
|  | import java.util.HashMap; | 
|  | import java.util.Map; | 
|  | import java.util.regex.Matcher; | 
|  |  | 
|  | /** | 
|  | * Helper class used for expanding link references in rule and attribute documentation. | 
|  | * | 
|  | * <p>See {@link com.google.devtools.build.docgen.DocgenConsts.BLAZE_RULE_LINK} for the regex used | 
|  | * to match link references. | 
|  | */ | 
|  | // TODO(fwe): rename to LinkExpander | 
|  | // TODO(fwe): prefix rule links with BE root? | 
|  | public 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 ImmutableMap<String, String> FUNCTIONS = | 
|  | ImmutableMap.<String, String>builder() | 
|  | .put("load", FUNCTIONS_PAGE) | 
|  | .put("package", FUNCTIONS_PAGE) | 
|  | .put("package_group", FUNCTIONS_PAGE) | 
|  | .put("description", FUNCTIONS_PAGE) | 
|  | .put("distribs", FUNCTIONS_PAGE) | 
|  | .put("licenses", FUNCTIONS_PAGE) | 
|  | .put("exports_files", FUNCTIONS_PAGE) | 
|  | .put("glob", FUNCTIONS_PAGE) | 
|  | .put("select", FUNCTIONS_PAGE) | 
|  | .buildOrThrow(); | 
|  |  | 
|  | // These static pages only exist in the multi-page BE. In the single-page BE | 
|  | // their content is part of the BE. | 
|  | private static final ImmutableSet<String> STATIC_PAGES_REPLACED_BY_SINGLE_PAGE_BE = | 
|  | ImmutableSet.<String>of("common-definitions", "make-variables"); | 
|  |  | 
|  | private final DocLinkMap linkMap; | 
|  | private final Map<String, String> ruleIndex = new HashMap<>(); | 
|  | private final boolean singlePage; | 
|  |  | 
|  | RuleLinkExpander(Map<String, String> ruleIndex, boolean singlePage, DocLinkMap linkMap) { | 
|  | this.ruleIndex.putAll(ruleIndex); | 
|  | this.ruleIndex.putAll(FUNCTIONS); | 
|  | this.singlePage = singlePage; | 
|  | this.linkMap = linkMap; | 
|  | } | 
|  |  | 
|  | RuleLinkExpander(boolean singlePage, DocLinkMap linkMap) { | 
|  | this.ruleIndex.putAll(FUNCTIONS); | 
|  | this.singlePage = singlePage; | 
|  | this.linkMap = linkMap; | 
|  | } | 
|  |  | 
|  | public String beRoot() { | 
|  | return linkMap.beRoot; | 
|  | } | 
|  |  | 
|  | public void addIndex(Map<String, String> ruleIndex) { | 
|  | this.ruleIndex.putAll(ruleIndex); | 
|  | } | 
|  |  | 
|  | private void appendRuleLink(Matcher matcher, StringBuffer sb, String ruleName, String ref) { | 
|  | String ruleFamily = ruleIndex.get(ruleName); | 
|  | String link = | 
|  | singlePage | 
|  | ? "#" + ref | 
|  | : Paths.get(linkMap.beRoot, String.format("%s.html#%s", ruleFamily, ref)).toString(); | 
|  | matcher.appendReplacement(sb, Matcher.quoteReplacement(link)); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Match and replace all ${link rule.attribute} references. | 
|  | */ | 
|  | private String expandRuleLinks(String htmlDoc) throws IllegalArgumentException { | 
|  | Matcher matcher = DocgenConsts.BLAZE_RULE_LINK.matcher(htmlDoc); | 
|  | StringBuffer sb = new StringBuffer(htmlDoc.length()); | 
|  | while (matcher.find()) { | 
|  | // The first capture group matches the entire reference, e.g. "cc_binary.deps". | 
|  | String ref = matcher.group(1); | 
|  | // The second capture group only matches the rule name, e.g. "cc_binary" in "cc_binary.deps". | 
|  | String name = matcher.group(2); | 
|  |  | 
|  | // The name in the reference is the name of a rule. Get the rule family for the rule and | 
|  | // replace the reference with a link with the form of rule-family.html#rule.attribute. For | 
|  | // example, ${link cc_library.deps} expands to c-cpp.html#cc_library.deps. | 
|  | if (ruleIndex.containsKey(name)) { | 
|  | appendRuleLink(matcher, sb, name, ref); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // 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; | 
|  | } | 
|  | } | 
|  |  | 
|  | // The name is not the name of a rule but is the name of a static page, such as | 
|  | // common-definitions. Generate a link to that page. | 
|  | String mapping = linkMap.values.get(name); | 
|  | if (mapping != null) { | 
|  | String link = | 
|  | singlePage && STATIC_PAGES_REPLACED_BY_SINGLE_PAGE_BE.contains(name) | 
|  | ? "#" + name | 
|  | : mapping; | 
|  | // For referencing headings on a static page, use the following syntax: | 
|  | // ${link static_page_name#heading_name}, example: ${link make-variables#gendir} | 
|  | String pageHeading = matcher.group(4); | 
|  | if (pageHeading != null) { | 
|  | throw new IllegalArgumentException( | 
|  | "Invalid link syntax for BE page: " + matcher.group() | 
|  | + "\nUse ${link static-page#heading} syntax instead."); | 
|  | } | 
|  | 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(); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * 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 first capture group matches the entire reference, e.g. "cc_library#some_heading". | 
|  | String ref = matcher.group(1); | 
|  | // 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 = singlePage | 
|  | ? "#" + heading | 
|  | : 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. | 
|  |  | 
|  | // We need to search for the entire match first since some documentation files have a 1:n | 
|  | // relation between Blaze and Bazel. Example: build-ref#labels points to build-ref.html#labels | 
|  | // for Blaze, but to /concepts/labels for Bazel. However, we have to consider whether a single | 
|  | // heading or the entire page has to be redirected. | 
|  |  | 
|  | // Not-null if page#heading has a mapping (other headings on the page are unaffected): | 
|  | String headingMapping = linkMap.values.get(ref); | 
|  | // Not-null if the entire page has a mapping, i.e. all headings should be redirected: | 
|  | String pageMapping = linkMap.values.get(name); | 
|  |  | 
|  | if (headingMapping != null || pageMapping != null) { | 
|  | String link; | 
|  | if (singlePage && STATIC_PAGES_REPLACED_BY_SINGLE_PAGE_BE.contains(name)) { | 
|  | // Special case: Some of the stand-alone files in the multi-page BE are made redundant | 
|  | // by the BE in the single-page case. Consequently, we ignore the value of the mapping | 
|  | // in this case (we only need to know that the mapping exists, since this means it is | 
|  | // a legitimate reference). | 
|  | link = "#" + heading; | 
|  | } else if (headingMapping != null) { | 
|  | // Multi-page BE where page#heading has to be redirected. | 
|  | link = headingMapping; | 
|  | } else { // pageMapping != null | 
|  | // Multi-page BE where the entire page has to be forwarded (but the new page has | 
|  | // identical headings). | 
|  | link = String.format("%s#%s", pageMapping, 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); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Expands the rule reference. | 
|  | * | 
|  | * <p>This method is used to expand references in the BE velocity templates. | 
|  | * | 
|  | * @param ref The rule reference to expand. | 
|  | * @return The expanded rule reference. | 
|  | */ | 
|  | public String expandRef(String ref) throws IllegalArgumentException { | 
|  | return expand("${link " + ref + "}"); | 
|  | } | 
|  | } |