| // Copyright 2025 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.common.options; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| |
| import com.google.common.base.Ascii; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.escape.Escaper; |
| import com.google.devtools.build.lib.util.Markdown; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| /** Utilities related to HTML generation. */ |
| public class HtmlUtils { |
| private HtmlUtils() {} |
| |
| /** |
| * Returns a description of all the options the given parser can digest. In addition to {@link |
| * Option} annotations, this method also interprets {@link OptionsUsage} annotations which give an |
| * intuitive short description for the options. |
| */ |
| public static String describeOptionsHtml( |
| OptionsParser parser, Escaper escaper, List<String> optionsToIgnore, String commandName) { |
| StringBuilder desc = new StringBuilder(); |
| ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions = |
| OptionFilterDescriptions.getOptionCategoriesEnumDescription(); |
| |
| for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e : |
| parser.getOptionsSortedByCategory().entrySet()) { |
| List<OptionDefinition> categorizedOptionsList = e.getValue(); |
| categorizedOptionsList = |
| categorizedOptionsList.stream() |
| .filter( |
| optionDef -> |
| Arrays.stream(optionDef.getOptionEffectTags()) |
| .noneMatch(effectTag -> effectTag.equals(OptionEffectTag.NO_OP))) |
| .filter(optionDef -> !optionsToIgnore.contains(optionDef.getOptionName())) |
| .collect(toImmutableList()); |
| if (categorizedOptionsList.isEmpty()) { |
| continue; |
| } |
| String categoryDescription = optionCategoryDescriptions.get(e.getKey()); |
| |
| desc.append("<dl>").append(escaper.escape(categoryDescription)).append(":\n"); |
| for (OptionDefinition optionDef : categorizedOptionsList) { |
| getUsageHtml( |
| optionDef, |
| desc, |
| escaper, |
| OptionsParser.getOptionsDataInternal(optionDef.getDeclaringClass(OptionsBase.class)), |
| true, |
| commandName); |
| } |
| desc.append("</dl>\n"); |
| } |
| return desc.toString(); |
| } |
| |
| /** Append the usage message for a single option-field message to 'usage'. */ |
| static void getUsageHtml( |
| OptionDefinition optionDefinition, |
| StringBuilder usage, |
| Escaper escaper, |
| OptionsData optionsData, |
| boolean includeTags, |
| String commandName) { |
| String plainFlagName = optionDefinition.getOptionName(); |
| String flagName = OptionsUsage.getFlagName(optionDefinition); |
| String valueDescription = optionDefinition.getValueTypeHelpText(); |
| String typeDescription = OptionsUsage.getTypeDescription(optionDefinition); |
| |
| StringBuilder anchorId = new StringBuilder(); |
| if (commandName != null) { |
| anchorId.append(commandName).append("-"); |
| } |
| anchorId.append("flag--").append(plainFlagName); |
| |
| // String.format is a lot slower, sometimes up to 10x. |
| // https://stackoverflow.com/questions/925423/is-it-better-practice-to-use-string-format-over-string-concatenation-in-java |
| // |
| // Considering that this runs for every flag in the CLI reference, it's better to use regular |
| // appends here. |
| usage |
| .append("<dt id=\"") |
| // Add the id of the flag to point anchor hrefs to it |
| .append(anchorId) |
| .append("\">") |
| // Add the href to the id hash |
| .append("<code"); |
| // Historically, we used `flag--${plainFlagName}` as the anchor id, but this is not unique |
| // across commands. We now use the per-command `anchorId` defined above, but we moved the old |
| // `flag--${plainFlagName}` to be an id on the code block for backwards compatibility with |
| // old links. |
| if (commandName != null) { |
| usage.append(" id=\"").append(plainFlagName).append("\""); |
| } |
| usage |
| .append(">") |
| .append("<a href=\"#") |
| .append(anchorId) |
| .append("\">") |
| .append("--") |
| .append(flagName) |
| .append("</a>"); |
| |
| if (!optionDefinition.requiresValue()) { |
| // Nothing for boolean, tristate, boolean_or_enum, or void options. |
| } else if (!valueDescription.isEmpty()) { |
| usage.append("=").append(escaper.escape(valueDescription)); |
| } else if (!typeDescription.isEmpty()) { |
| // Generic fallback, which isn't very good. |
| usage.append("=<").append(escaper.escape(typeDescription)).append(">"); |
| } |
| usage.append("</code>"); |
| if (optionDefinition.getAbbreviation() != '\0') { |
| usage.append(" [<code>-").append(optionDefinition.getAbbreviation()).append("</code>]"); |
| } |
| if (optionDefinition.allowsMultiple()) { |
| // Allow-multiple options can't have a default value. |
| usage.append(" multiple uses are accumulated"); |
| } else { |
| // Don't call the annotation directly (we must allow overrides to certain defaults). |
| String defaultValueString = optionDefinition.getUnparsedDefaultValue(); |
| if (optionDefinition.isVoidField()) { |
| // Void options don't have a default. |
| } else if (optionDefinition.isSpecialNullDefault()) { |
| usage.append(" default: see description"); |
| } else { |
| usage.append(" default: \"").append(escaper.escape(defaultValueString)).append("\""); |
| } |
| } |
| usage.append("</dt>\n"); |
| usage.append("<dd>\n"); |
| if (!optionDefinition.getHelpText().isEmpty()) { |
| usage.append(Markdown.renderToHtml(escaper.escape(optionDefinition.getHelpText()))); |
| usage.append('\n'); |
| } |
| |
| ImmutableList<String> expansion = optionsData.getEvaluatedExpansion(optionDefinition); |
| if (!expansion.isEmpty()) { |
| // If this is an expansion option, list the expansion if known, or at least specify that we |
| // don't know. |
| usage.append("<p>Expands to:\n"); |
| for (String exp : expansion) { |
| // TODO(jingwen): We link to the expanded flags here, but unfortunately we don't |
| // currently guarantee that all flags are only printed once. A flag in an OptionBase that |
| // is included by 2 different commands, but not inherited through a parent command, will |
| // be printed multiple times. Clicking on the flag will bring the user to its first |
| // definition. |
| usage |
| .append("<br/> ") |
| .append("<code><a href=\"#flag") |
| // Link to the '#flag--flag_name' hash. |
| // Some expansions are in the form of '--flag_name=value', so we drop everything from |
| // '=' onwards. |
| .append(Iterables.get(Splitter.on('=').split(escaper.escape(exp)), 0)) |
| .append("\">") |
| .append(escaper.escape(exp)) |
| .append("</a></code>\n"); |
| } |
| usage.append("</p>"); |
| } |
| |
| // Add effect tags, if not UNKNOWN, and metadata tags, if not empty. |
| if (includeTags) { |
| Stream<OptionEffectTag> effectTagStream = |
| Arrays.stream(optionDefinition.getOptionEffectTags()) |
| .filter(OptionsUsage::shouldEffectTagBeListed); |
| Stream<OptionMetadataTag> metadataTagStream = |
| Arrays.stream(optionDefinition.getOptionMetadataTags()) |
| .filter(OptionsUsage::shouldMetadataTagBeListed); |
| String tagList = |
| Stream.concat( |
| effectTagStream.map( |
| tag -> |
| String.format( |
| "<a href=\"#effect_tag_%s\"><code>%s</code></a>", |
| tag, Ascii.toLowerCase(tag.name()))), |
| metadataTagStream.map( |
| tag -> |
| String.format( |
| "<a href=\"#metadata_tag_%s\"><code>%s</code></a>", |
| tag, Ascii.toLowerCase(tag.name())))) |
| .collect(Collectors.joining(", ")); |
| if (!tagList.isEmpty()) { |
| usage.append("<p>Tags:\n").append(tagList).append("\n</p>"); |
| } |
| } |
| |
| usage.append("</dd>\n"); |
| } |
| } |