// 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.util.HashMap; import java.util.Locale; 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. */ 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 ImmutableSet<String> STATIC_PAGES = ImmutableSet.<String>of( "common-definitions", "make-variables", "predefined-python-variables"); 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) .put("workspace", FUNCTIONS_PAGE) .build(); private final String productName; private final Map<String, String> ruleIndex = new HashMap<>(); private final boolean singlePage; RuleLinkExpander(String productName, Map<String, String> ruleIndex, boolean singlePage) { this.productName = productName; this.ruleIndex.putAll(ruleIndex); this.ruleIndex.putAll(FUNCTIONS); this.singlePage = singlePage; } RuleLinkExpander(String productName, boolean singlePage) { this.productName = productName; this.ruleIndex.putAll(FUNCTIONS); this.singlePage = singlePage; } 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 : ruleFamily + ".html#" + ref; 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. if (STATIC_PAGES.contains(name)) { String link = singlePage ? "#" + name : name + ".html"; // 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 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. if (STATIC_PAGES.contains(name)) { String link = singlePage ? "#" + heading : name + ".html#" + heading; matcher.appendReplacement(sb, Matcher.quoteReplacement(link)); continue; } // Links to the user manual are handled specially. Meh. if ("user-manual".equals(name)) { String link = productName.toLowerCase(Locale.US) + "-" + 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); } /** * 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 + "}"); } }