// Copyright 2017 The Nomulus 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 google.registry.documentation; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.re2j.Matcher; import com.google.re2j.Pattern; import google.registry.documentation.FlowDocumentation.ErrorCase; import java.util.ArrayList; import java.util.List; /** * Formatter that converts flow documentation into Markdown. */ public final class MarkdownDocumentationFormatter { /** Header for flow documentation HTML output. */ private static final String MARKDOWN_HEADER = "# Nomulus EPP Command API Documentation\n\n"; /** Pattern that naively matches HTML tags and entity references. */ private static final Pattern HTML_TAG_AND_ENTITY_PATTERN = Pattern.compile("<[^>]*>|&[^;]*;"); /** 8 character indentation. */ private static final String INDENT8 = Strings.repeat(" ", 8); /** Max linewidth for our markdown docs. */ private static final int LINE_WIDTH = 80; /** * Returns the string with all HTML tags stripped. Also, removes a single space after any * newlines that have one (we get a single space indent for all lines but the first because of * the way that javadocs are written in comments). */ @VisibleForTesting static String fixHtml(String value) { Matcher matcher = HTML_TAG_AND_ENTITY_PATTERN.matcher(value); int pos = 0; StringBuilder result = new StringBuilder(); while (matcher.find(pos)) { result.append(value, pos, matcher.start()); switch (matcher.group(0)) { case "

": //

is simply removed. break; case "&": result.append("&"); break; case "<": result.append("<"); break; case ">": result.append(">"); break; case "&squot;": result.append("'"); break; case """: result.append("\""); break; default: throw new IllegalArgumentException("Unrecognized HTML sequence: " + matcher.group(0)); } pos = matcher.end(); } // Add the string after the last HTML sequence. result.append(value.substring(pos)); return result.toString().replace("\n ", "\n"); } /** * Formats a list of words into a paragraph with less than maxWidth characters per line. */ @VisibleForTesting static String formatParagraph(List words, int maxWidth) { int lineLength = 0; StringBuilder output = new StringBuilder(); for (String word : words) { // This check ensures that 1) don't add a space before the word and 2) always have at least // one word per line, so that we don't mishandle a very long word at the end of a line by // adding a blank line before the word. if (lineLength > 0) { // Do we have enough room for another word? if (lineLength + 1 + word.length() > maxWidth) { // No. End the line. output.append('\n'); lineLength = 0; } else { // Yes: Insert a space before the word. output.append(' '); ++lineLength; } } output.append(word); lineLength += word.length(); } output.append('\n'); return output.toString(); } /** * Returns 'value' with words reflowed to maxWidth characters. */ @VisibleForTesting static String reflow(String text, int maxWidth) { // A list of words that will be constructed into the list of words in a paragraph. ArrayList words = new ArrayList<>(); // Read through the lines, process a paragraph every time we get a blank line. StringBuilder resultBuilder = new StringBuilder(); for (String line : Splitter.on('\n').trimResults().split(text)) { // If we got a blank line, format our current paragraph and start fresh. if (line.trim().isEmpty()) { resultBuilder.append(formatParagraph(words, maxWidth)); resultBuilder.append('\n'); words.clear(); continue; } // Split the line into words and add them to the current paragraph. words.addAll(Splitter.on( CharMatcher.breakingWhitespace()).omitEmptyStrings().splitToList(line)); } // Format the last paragraph, if any. if (!words.isEmpty()) { resultBuilder.append(formatParagraph(words, maxWidth)); } return resultBuilder.toString(); } /** Returns a string of HTML representing the provided flow documentation objects. */ public static String generateMarkdownOutput(Iterable flowDocs) { StringBuilder output = new StringBuilder(); output.append(MARKDOWN_HEADER); for (FlowDocumentation flowDoc : flowDocs) { output.append(String.format("## %s\n\n", flowDoc.getName())); output.append("### Description\n\n"); output.append(String.format("%s\n\n", reflow(fixHtml(flowDoc.getClassDocs()), LINE_WIDTH))); output.append("### Errors\n\n"); for (Long code : flowDoc.getErrorsByCode().keySet()) { output.append(String.format("* %d\n", code)); for (ErrorCase error : flowDoc.getErrorsByCode().get(code)) { output.append(" * "); String wrappedReason = reflow(fixHtml(error.getReason()), LINE_WIDTH - 8); // Replace internal newlines with indentation and strip the final newline. output.append(wrappedReason.trim().replace("\n", "\n" + INDENT8)); output.append('\n'); } } output.append('\n'); } return output.toString(); } private MarkdownDocumentationFormatter() {} }