| // 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 static com.google.common.collect.ImmutableList.toImmutableList; |
| import static java.util.Comparator.naturalOrder; |
| import static java.util.stream.Collectors.joining; |
| |
| 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.ModuleExtensionInfo; |
| import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleExtensionTagClassInfo; |
| 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.RepositoryRuleInfo; |
| 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.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** Contains a number of utility methods for markdown rendering. */ |
| public final class MarkdownUtil { |
| private final String extensionBzlFile; |
| |
| private static final int MAX_LINE_LENGTH = 100; |
| |
| public MarkdownUtil(String extensionBzlFile) { |
| this.extensionBzlFile = extensionBzlFile; |
| } |
| |
| /** |
| * Formats the input string so that it is displayable in a Markdown table cell. This performs the |
| * following operations: |
| * |
| * <ul> |
| * <li>Trims the string of leading/trailing whitespace. |
| * <li>Escapes pipe characters ({@code |}) as {@code \|}. |
| * <li>Transforms Markdown code blocks ({@code ```}) into HTML preformatted code blocks, and |
| * transforms newlines within those code blocks into character entities |
| * <li>Transforms remaining 'new paragraph' patterns (two or more sequential newline characters) |
| * into line break HTML tags. |
| * <li>Turns remaining newlines into spaces (as they generally indicate intended line wrap). |
| * </ul> |
| * |
| * TODO(https://github.com/bazelbuild/stardoc/issues/118): also format Markdown lists as HTML. |
| */ |
| public static String markdownCellFormat(String docString) { |
| return new MarkdownCellFormatter(docString).format(); |
| } |
| |
| // See https://github.github.com/gfm |
| private static final class MarkdownCellFormatter { |
| // Lines of the input docstring, without newline terminators. |
| private final ImmutableList<String> lines; |
| // Index of the current line in lines, 0-based. |
| int currentLine; |
| // Formatted result. |
| StringBuilder result; |
| |
| private static final Pattern CODE_BLOCK_OPENING_FENCE = |
| Pattern.compile("^ {0,3}(?<fence>```+|~~~+) *(?<lang>\\w*)[^`~]*$"); |
| |
| MarkdownCellFormatter(String docString) { |
| lines = docString.trim().replace("|", "\\|").lines().collect(toImmutableList()); |
| currentLine = 0; |
| result = new StringBuilder(); |
| } |
| |
| /** Consumes the input and yields the formatted result. */ |
| String format() { |
| boolean prefixContentWithSpace = false; |
| for (; currentLine < lines.size(); currentLine++) { |
| if (formatParagraphBreak()) { |
| prefixContentWithSpace = false; |
| continue; |
| } |
| if (prefixContentWithSpace) { |
| result.append(" "); |
| } |
| prefixContentWithSpace = true; |
| if (formatFencedCodeBlock()) { |
| continue; |
| } |
| result.append(lines.get(currentLine)); |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * If a fenced code block begins at {@link #currentLine}, render to {@link #result}, update |
| * {@link #currentLine} to point to the closing fence, and return true. |
| */ |
| private boolean formatFencedCodeBlock() { |
| // See https://github.github.com/gfm/#fenced-code-blocks |
| Matcher opening = CODE_BLOCK_OPENING_FENCE.matcher(lines.get(currentLine)); |
| if (!opening.matches()) { |
| return false; |
| } |
| Pattern closingFence = Pattern.compile("^ {0,3}" + opening.group("fence") + " *$"); |
| for (int closingLine = currentLine + 1; closingLine < lines.size(); closingLine++) { |
| if (closingFence.matcher(lines.get(closingLine)).matches()) { |
| // We found the closing fence: format the block's contents as HTML. |
| String language = opening.group("lang"); |
| if (language != null && !language.isEmpty()) { |
| result.append("<pre><code class=\"language-").append(language).append("\">"); |
| } else { |
| result.append("<pre><code>"); |
| } |
| int firstContentLine = currentLine + 1; |
| for (int i = firstContentLine; i < closingLine; i++) { |
| if (i > firstContentLine) { |
| result.append(newlineEscape("\n")); |
| } |
| result.append(htmlEscape(lines.get(i))); |
| } |
| result.append("</code></pre>"); |
| currentLine = closingLine; |
| return true; |
| } |
| } |
| // We did not find the closing fence. |
| return false; |
| } |
| |
| /** |
| * If blank lines appear at {@link #currentLine}, render to {@link #result}, update {@link |
| * #currentLine} to point to the last line of the break, and return true. |
| */ |
| private boolean formatParagraphBreak() { |
| int numEmptyLines = 0; |
| for (int i = currentLine; i < lines.size(); i++) { |
| if (lines.get(i).isEmpty()) { |
| numEmptyLines++; |
| } else { |
| break; |
| } |
| } |
| if (numEmptyLines > 0) { |
| result.append("<br><br>"); |
| currentLine += numEmptyLines - 1; |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Return a string that escapes angle brackets for HTML. |
| * |
| * <p>For example: 'Information with <brackets>.' becomes 'Information with <brackets>'. |
| */ |
| public static String htmlEscape(String docString) { |
| return docString.replace("<", "<").replace(">", ">"); |
| } |
| |
| /** Returns a string that escapes newlines with HTML entities. */ |
| private static String newlineEscape(String docString) { |
| return docString.replace("\n", " "); |
| } |
| |
| private static final Pattern CONSECUTIVE_BACKTICKS = Pattern.compile("`+"); |
| |
| /** |
| * Returns a Markdown code span (e.g. {@code `return foo;`}) that contains the given literal text, |
| * which may itself contain backticks. |
| * |
| * <p>For example: |
| * |
| * <ul> |
| * <li>{@code markdownCodeSpan("foo")} returns {@code "`foo`"} |
| * <li>{@code markdownCodeSpan("fo`o")} returns {@code "``fo`o``"} |
| * <li>{@code markdownCodeSpan("`foo`")} returns {@code "`` foo` ``""} |
| * </ul> |
| */ |
| public static String markdownCodeSpan(String code) { |
| // https://github.github.com/gfm/#code-span |
| int numConsecutiveBackticks = |
| CONSECUTIVE_BACKTICKS |
| .matcher(code) |
| .results() |
| .map(match -> match.end() - match.start()) |
| .max(naturalOrder()) |
| .orElse(0); |
| String padding = code.startsWith("`") || code.endsWith("`") ? " " : ""; |
| return String.format( |
| "%1$s%2$s%3$s%2$s%1$s", "`".repeat(numConsecutiveBackticks + 1), padding, code); |
| } |
| |
| /** |
| * Return a string representing the rule summary for the given rule with the given name. |
| * |
| * <p>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) { |
| ImmutableList<String> attributeNames = |
| ruleInfo.getAttributeList().stream().map(AttributeInfo::getName).collect(toImmutableList()); |
| return summary(ruleName, attributeNames); |
| } |
| |
| /** |
| * Return a string representing the summary for the given provider with the given name. |
| * |
| * <p>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) { |
| ImmutableList<String> fieldNames = |
| providerInfo.getFieldInfoList().stream() |
| .map(field -> field.getName()) |
| .collect(toImmutableList()); |
| 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) { |
| ImmutableList<String> attributeNames = |
| aspectInfo.getAttributeList().stream() |
| .map(AttributeInfo::getName) |
| .collect(toImmutableList()); |
| return summary(aspectName, attributeNames); |
| } |
| |
| /** |
| * Return a string representing the repository rule summary for the given repository rule with the |
| * given name. |
| * |
| * <p>For example: 'my_repo_rule(foo, bar)'. The summary will contain hyperlinks for each |
| * attribute. |
| */ |
| @SuppressWarnings("unused") // Used by markdown template. |
| public String repositoryRuleSummary(String ruleName, RepositoryRuleInfo ruleInfo) { |
| ImmutableList<String> attributeNames = |
| ruleInfo.getAttributeList().stream().map(AttributeInfo::getName).collect(toImmutableList()); |
| return summary(ruleName, attributeNames); |
| } |
| |
| /** |
| * Return a string representing the module extension summary for the given module extension with |
| * the given name. |
| * |
| * <p>For example: |
| * |
| * <pre> |
| * my_ext = use_extension("//some:file.bzl", "my_ext") |
| * my_ext.tag1(foo, bar) |
| * my_ext.tag2(baz) |
| * </pre> |
| * |
| * <p>The summary will contain hyperlinks for each attribute. |
| */ |
| @SuppressWarnings("unused") // Used by markdown template. |
| public String moduleExtensionSummary(String extensionName, ModuleExtensionInfo extensionInfo) { |
| StringBuilder summaryBuilder = new StringBuilder(); |
| summaryBuilder.append( |
| String.format( |
| "%s = use_extension(\"%s\", \"%s\")", extensionName, extensionBzlFile, extensionName)); |
| for (ModuleExtensionTagClassInfo tagClass : extensionInfo.getTagClassList()) { |
| ImmutableList<String> attributeNames = |
| tagClass.getAttributeList().stream() |
| .map(AttributeInfo::getName) |
| .collect(toImmutableList()); |
| summaryBuilder |
| .append("\n") |
| .append( |
| summary( |
| String.format("%s.%s", extensionName, tagClass.getTagName()), attributeNames)); |
| } |
| return summaryBuilder.toString(); |
| } |
| |
| /** |
| * 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) { |
| ImmutableList<String> paramNames = |
| funcInfo.getParameterList().stream() |
| .map(FunctionParamInfo::getName) |
| .collect(toImmutableList()); |
| return summary(funcInfo.getFunctionName(), paramNames); |
| } |
| |
| private static String summary(String functionName, ImmutableList<String> paramNames) { |
| ImmutableList<ImmutableList<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(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 ImmutableList<ImmutableList<String>> wrap( |
| String functionName, ImmutableList<String> paramNames, int maxLineLength) { |
| ImmutableList.Builder<ImmutableList<String>> paramLines = ImmutableList.builder(); |
| 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.build(); |
| } |
| |
| /** |
| * 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); |
| } |
| } |