| // Copyright 2018 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.skydoc.rendering; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo; | |
| 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.StarlarkFunctionInfo; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.stream.Collectors; | |
| /** | |
| * Contains a number of utility methods for markdown rendering. | |
| */ | |
| public final class MarkdownUtil { | |
| private static final int MAX_LINE_LENGTH = 100; | |
| /** | |
| * Return a string that formats the input string so 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. | |
| * </ul> | |
| */ | |
| 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', ' '); | |
| } | |
| private static String replaceWithTag( | |
| String wholeString, String stringToReplace, String openTag, String closeTag) { | |
| String remainingString = wholeString; | |
| StringBuilder resultString = new StringBuilder(); | |
| 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); | |
| } | |
| resultString.append(remainingString); | |
| return resultString.toString(); | |
| } | |
| /** | |
| * Return a string that escapes angle brackets for HTML. | |
| * | |
| * <p>For example: 'Information with <brackets>.' becomes 'Information with <brackets>'. | |
| */ | |
| public String htmlEscape(String docString) { | |
| return docString.replace("<", "<").replace(">", ">"); | |
| } | |
| /** | |
| * Return a string representing the rule summary for the given rule with the given name. | |
| * | |
| * For example: 'my_rule(foo, bar)'. | |
| * The summary will contain hyperlinks for each attribute. | |
| */ | |
| @SuppressWarnings("unused") // Used by markdown template. | |
| public String ruleSummary(String ruleName, RuleInfo ruleInfo) { | |
| List<String> attributeNames = | |
| ruleInfo.getAttributeList().stream() | |
| .map(AttributeInfo::getName) | |
| .collect(Collectors.toList()); | |
| return summary(ruleName, attributeNames); | |
| } | |
| /** | |
| * Return a string representing the summary for the given provider with the given name. | |
| * | |
| * For example: 'MyInfo(foo, bar)'. | |
| * The summary will contain hyperlinks for each field. | |
| */ | |
| @SuppressWarnings("unused") // Used by markdown template. | |
| public String providerSummary(String providerName, ProviderInfo providerInfo) { | |
| List<String> fieldNames = | |
| providerInfo.getFieldInfoList().stream() | |
| .map(field -> field.getName()) | |
| .collect(Collectors.toList()); | |
| return summary(providerName, fieldNames); | |
| } | |
| /** | |
| * Return a string representing the aspect summary for the given aspect with the given name. | |
| * | |
| * <p>For example: 'my_aspect(foo, bar)'. The summary will contain hyperlinks for each attribute. | |
| */ | |
| @SuppressWarnings("unused") // Used by markdown template. | |
| public String aspectSummary(String aspectName, AspectInfo aspectInfo) { | |
| List<String> attributeNames = | |
| aspectInfo.getAttributeList().stream() | |
| .map(AttributeInfo::getName) | |
| .collect(Collectors.toList()); | |
| return summary(aspectName, attributeNames); | |
| } | |
| /** | |
| * Return a string representing the summary for the given user-defined function. | |
| * | |
| * <p>For example: 'my_func(foo, bar)'. The summary will contain hyperlinks for each parameter. | |
| */ | |
| @SuppressWarnings("unused") // Used by markdown template. | |
| public String funcSummary(StarlarkFunctionInfo funcInfo) { | |
| List<String> paramNames = | |
| funcInfo.getParameterList().stream() | |
| .map(FunctionParamInfo::getName) | |
| .collect(Collectors.toList()); | |
| return summary(funcInfo.getFunctionName(), paramNames); | |
| } | |
| private static String summary(String functionName, List<String> paramNames) { | |
| List<List<String>> paramLines = wrap(functionName, paramNames, MAX_LINE_LENGTH); | |
| List<String> paramLinksLines = new ArrayList<>(); | |
| for (List<String> params : paramLines) { | |
| String paramLinksLine = | |
| params.stream() | |
| .map(param -> String.format("<a href=\"#%s-%s\">%s</a>", functionName, param, param)) | |
| .collect(Collectors.joining(", ")); | |
| paramLinksLines.add(paramLinksLine); | |
| } | |
| String paramList = | |
| Joiner.on(",\n" + " ".repeat(functionName.length() + 1)).join(paramLinksLines); | |
| return String.format("%s(%s)", functionName, paramList); | |
| } | |
| /** | |
| * Wraps the given function parameter names to be able to construct a function summary that stays | |
| * within the provided line length limit. | |
| * | |
| * @param functionName the function name. | |
| * @param paramNames the function parameter names. | |
| * @param maxLineLength the maximal line length. | |
| * @return the lines with the wrapped parameter names. | |
| */ | |
| private static List<List<String>> wrap( | |
| String functionName, List<String> paramNames, int maxLineLength) { | |
| List<List<String>> paramLines = new ArrayList<>(); | |
| ImmutableList.Builder<String> linesBuilder = new ImmutableList.Builder<>(); | |
| int leading = functionName.length(); | |
| int length = leading; | |
| int punctuation = 2; // cater for left parenthesis/space before and comma after parameter | |
| for (String paramName : paramNames) { | |
| length += paramName.length() + punctuation; | |
| if (length > maxLineLength) { | |
| paramLines.add(linesBuilder.build()); | |
| length = leading + paramName.length(); | |
| linesBuilder = new ImmutableList.Builder<>(); | |
| } | |
| linesBuilder.add(paramName); | |
| } | |
| paramLines.add(linesBuilder.build()); | |
| return paramLines; | |
| } | |
| /** | |
| * Returns a string describing the given attribute's type. The description consists of a hyperlink | |
| * if there is a relevant hyperlink to Bazel documentation available. | |
| */ | |
| public String attributeTypeString(AttributeInfo attrInfo) { | |
| String typeLink; | |
| switch (attrInfo.getType()) { | |
| case LABEL: | |
| case LABEL_LIST: | |
| case OUTPUT: | |
| typeLink = "https://bazel.build/concepts/labels"; | |
| break; | |
| case NAME: | |
| typeLink = "https://bazel.build/concepts/labels#target-names"; | |
| break; | |
| case STRING_DICT: | |
| case STRING_LIST_DICT: | |
| case LABEL_STRING_DICT: | |
| typeLink = "https://bazel.build/rules/lib/dict"; | |
| break; | |
| default: | |
| typeLink = null; | |
| break; | |
| } | |
| if (typeLink == null) { | |
| return attributeTypeDescription(attrInfo.getType()); | |
| } else { | |
| return String.format( | |
| "<a href=\"%s\">%s</a>", typeLink, attributeTypeDescription(attrInfo.getType())); | |
| } | |
| } | |
| public String mandatoryString(AttributeInfo attrInfo) { | |
| return attrInfo.getMandatory() ? "required" : "optional"; | |
| } | |
| /** | |
| * Returns "required" if providing a value for this parameter is mandatory. Otherwise, returns | |
| * "optional". | |
| */ | |
| public String mandatoryString(FunctionParamInfo paramInfo) { | |
| return paramInfo.getMandatory() ? "required" : "optional"; | |
| } | |
| /** | |
| * Return a string explaining what providers an attribute requires. Adds hyperlinks to providers. | |
| */ | |
| public String attributeProviders(AttributeInfo attributeInfo) { | |
| List<ProviderNameGroup> providerNames = attributeInfo.getProviderNameGroupList(); | |
| List<String> finalProviderNames = new ArrayList<>(); | |
| for (ProviderNameGroup providerNameList : providerNames) { | |
| List<String> providers = providerNameList.getProviderNameList(); | |
| finalProviderNames.add(Joiner.on(", ").join(providers)); | |
| } | |
| return Joiner.on("; or ").join(finalProviderNames); | |
| } | |
| private static String attributeTypeDescription(AttributeType attributeType) { | |
| switch (attributeType) { | |
| case NAME: | |
| return "Name"; | |
| case INT: | |
| return "Integer"; | |
| case STRING: | |
| return "String"; | |
| case STRING_LIST: | |
| return "List of strings"; | |
| case INT_LIST: | |
| return "List of integers"; | |
| case BOOLEAN: | |
| return "Boolean"; | |
| case LABEL_STRING_DICT: | |
| return "Dictionary: Label -> String"; | |
| case STRING_DICT: | |
| return "Dictionary: String -> String"; | |
| case STRING_LIST_DICT: | |
| return "Dictionary: String -> List of strings"; | |
| case LABEL: | |
| case OUTPUT: | |
| return "Label"; | |
| case LABEL_LIST: | |
| case OUTPUT_LIST: | |
| return "List of labels"; | |
| case UNKNOWN: | |
| case UNRECOGNIZED: | |
| throw new IllegalArgumentException("Unhandled type " + attributeType); | |
| } | |
| throw new IllegalArgumentException("Unhandled type " + attributeType); | |
| } | |
| } |