/* * 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.j2objc.gen; import com.google.devtools.j2objc.ast.CompilationUnit; import com.google.devtools.j2objc.ast.Javadoc; import com.google.devtools.j2objc.ast.Name; import com.google.devtools.j2objc.ast.SimpleName; import com.google.devtools.j2objc.ast.TagElement; import com.google.devtools.j2objc.ast.TagElement.TagKind; import com.google.devtools.j2objc.ast.TextElement; import com.google.devtools.j2objc.ast.TreeNode; import com.google.devtools.j2objc.util.ElementUtil; import com.google.devtools.j2objc.util.NameTable; import java.text.BreakIterator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.lang.model.element.Element; import javax.lang.model.element.VariableElement; /** * Generates Javadoc comments. * * @author Tom Ball, Keith Stanger */ public class JavadocGenerator extends AbstractSourceGenerator { // True when a <pre> tag is in a Javadoc comment, but not the closing </pre>. boolean spanningPreTag = false; // True with a <style> tag is in a Javadoc comment, but not the closing </style>. boolean spanningStyleTag = false; // All escapes are defined at "http://dev.w3.org/html5/html-author/charref". private static final Map<Character, String> htmlEntities = new HashMap<>(); static { htmlEntities.put('"', """); htmlEntities.put('\'', "'"); htmlEntities.put('<', "<"); htmlEntities.put('>', ">"); htmlEntities.put('&', "&"); htmlEntities.put('@', "@"); } private JavadocGenerator(SourceBuilder builder) { super(builder); } public static void printDocComment(SourceBuilder builder, Javadoc javadoc) { new JavadocGenerator(builder).printDocComment(javadoc); } public static String toString(Javadoc javadoc) { SourceBuilder builder = new SourceBuilder(false); printDocComment(builder, javadoc); return builder.toString(); } public static String toString(TagElement tag) { SourceBuilder builder = new SourceBuilder(false); return new JavadocGenerator(builder).printTag(tag); } private void printDocComment(Javadoc javadoc) { if (javadoc != null) { printIndent(); // Use HeaderDoc doc-comment start, which is compatible with Xcode Quick Help and Doxygen. println("/*!"); List<TagElement> tags = javadoc.getTags(); for (TagElement tag : tags) { if (tag.getTagKind() == TagKind.DESCRIPTION) { String description = printTagFragments(tag.getFragments()); // Extract first sentence from description. BreakIterator iterator = BreakIterator.getSentenceInstance(Locale.US); iterator.setText(description.toString()); int start = iterator.first(); int end = iterator.next(); if (end != BreakIterator.DONE) { // Print brief tag first, since Quick Help shows it first. This makes the // generated source easier to review. printDocLine(String.format("@brief %s", description.substring(start, end)).trim()); String remainder = description.substring(end); if (!remainder.isEmpty()) { printDocLine(remainder); } } else { printDocLine(description); } } else { String doc = printTag(tag); if (!doc.isEmpty()) { printDocLine(doc); } } } printIndent(); println(" */"); } } private void printDocLine(String line) { if (!spanningPreTag) { printIndent(); print(' '); } println(line); } private String printTag(TagElement tag) { TagKind kind = tag.getTagKind(); // Remove @param tags for parameterized types, such as "@param <T> the type". // TODO(tball): update when (if) Xcode supports Objective C type parameter documenting. if (kind == TagKind.PARAM && hasTypeParam(tag.getFragments())) { return ""; } // Xcode 7 compatible tags. if (kind == TagKind.AUTHOR || kind == TagKind.EXCEPTION || kind == TagKind.PARAM || kind == TagKind.RETURN || kind == TagKind.SINCE || kind == TagKind.THROWS || kind == TagKind.VERSION) { // Skip String comment = printTagFragments(tag.getFragments()).trim(); return comment.isEmpty() ? "" : String.format("%s %s", kind, comment); } if (kind == TagKind.DEPRECATED) { // Deprecated annotation translated instead. return ""; } if (kind == TagKind.SEE) { String comment = printTagFragments(tag.getFragments()).trim(); return comment.isEmpty() ? "" : "- seealso: " + comment; } if (kind == TagKind.CODE) { String text = printTagFragments(tag.getFragments()); if (spanningPreTag) { return text; } return String.format("<code>%s</code>", text.trim()); } if (kind == TagKind.LINK) { return formatLinkTag(tag, "<code>%s</code>"); } if (kind == TagKind.LINKPLAIN) { return formatLinkTag(tag, "%s"); } if (kind == TagKind.LITERAL) { String text = printTagFragments(tag.getFragments()).trim(); if (spanningPreTag) { return text; } return escapeHtmlText(text); } if (kind == TagKind.UNKNOWN) { // Skip unknown tags. If --doc-comment-warnings was specified, a warning was // already created. return ""; } return printTagFragments(tag.getFragments()); } public String formatLinkTag(TagElement tag, String template) { String text = printTagFragments(tag.getFragments()).trim(); int iLabel = text.indexOf(' '); if (iLabel > 0) { return String.format(template, text.substring(iLabel).trim()); } // Delete leading '#' characters (method links), and change embedded ones // (such as "class#method") to '.'. if (text.indexOf('#') == 0) { text = text.substring(1); } text = text.replace('#', '.'); return String.format(template, text); } private boolean hasTypeParam(List<TreeNode> fragments) { // The format for a @param tag with a type parameter is: // [ "<", Name, ">", comment ]. return fragments.size() >= 3 && "<".equals(fragments.get(0).toString()) && (fragments.get(1) instanceof SimpleName) && ">".equals(fragments.get(2).toString()); } private String printTagFragments(List<TreeNode> fragments) { if (fragments.isEmpty()) { return ""; } StringBuilder sb = new StringBuilder(); int lineNo = fragments.get(0).getLineNumber(); for (TreeNode fragment : fragments) { if (fragment.getLineNumber() > lineNo) { sb.append("\n "); lineNo = fragment.getLineNumber(); } if (fragment instanceof TextElement) { if (spanningPreTag) { sb.append(getSourceIndent(fragment)); } String text = escapeDocText(((TextElement) fragment).getText()); sb.append(text); } else if (fragment instanceof TagElement) { sb.append(printTag((TagElement) fragment)); } else if (fragment instanceof SimpleName) { Element element = ((Name) fragment).getElement(); if (element != null && ElementUtil.isVariable(element)) { sb.append(NameTable.getDocCommentVariableName(((VariableElement) element))); } else { sb.append(fragment.toString()); } } else { sb.append(fragment.toString().trim()); } } return sb.toString(); } /** * If a string has a <pre> tag, or is continuing one from another * tag, convert to @code/@endcode format. */ private String escapeCodeText(String text) { String lowerText = text.toLowerCase(); int preStart = lowerText.indexOf("<pre>"); int preEnd = lowerText.indexOf("</pre>"); if (preStart == -1 && preEnd == -1) { return text; } if (preStart >= 0 && preEnd >= 0 && preEnd < preStart) { // Bad code formatting, don't try to escape. return text; } // Separately test begin and end tags, to support a span with multiple Javadoc tags. StringBuffer sb = new StringBuffer(); if (preStart > -1 && preEnd > -1) { // Both <pre> and </pre> are in the same text segment. sb.append(text.substring(0, preStart)); if (preStart > 0) { sb.append('\n'); } sb.append("@code\n"); sb.append(text.substring(preStart + "<pre>".length(), preEnd)); sb.append("\n@endcode"); sb.append(text.substring(preEnd + "</pre>".length())); } else if (preStart > -1) { // The text has <pre> but not the </pre> should be in a following Javadoc tag. sb.append(text.substring(0, preStart)); if (preStart > 0) { sb.append('\n'); } sb.append("@code\n"); sb.append(text.substring(preStart + "<pre>".length())); spanningPreTag = true; } else { // The text just has a </pre>. sb.append("\n@endcode"); sb.append(text.substring(preEnd + "</pre>".length())); spanningPreTag = false; } return escapeCodeText(sb.toString()); // Allow for multiple <pre> spans in single text element. } private String escapeDocText(String text) { return skipStyleTag(escapeCodeText(text.replace("@", "@@").replace("/*", "/\\*"))); } private String escapeHtmlText(String text) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { Character c = text.charAt(i); if (htmlEntities.containsKey(c)) { sb.append(htmlEntities.get(c)); } else { sb.append(c); } } return sb.toString(); } /** * Remove <style> tags and their content, as Quick Help displays them. */ private String skipStyleTag(String text) { int start = text.indexOf("<style"); // Leave open as it has attributes. int end = text.indexOf("</style>"); if (start == -1 && end == -1) { return spanningStyleTag ? "" : text; } if (start > -1 && end == -1) { spanningStyleTag = true; return text.substring(0, start); } if (start == -1 && end > -1) { spanningStyleTag = false; return text.substring(end + 8); // "</style>".length } return text.substring(0, start) + text.substring(end + 8); } /** * Fetch the leading whitespace from the comment line. Since the JDT * strips leading and trailing whitespace from lines, the original * source is fetched and is walked backwards from the fragment's start * until the previous new line, then moved forward if there is a leading * "* ". */ private String getSourceIndent(TreeNode fragment) { int index = fragment.getStartPosition(); if (index < 1) { return ""; } TreeNode node = fragment.getParent(); while (node != null && node.getKind() != TreeNode.Kind.COMPILATION_UNIT) { node = node.getParent(); } if (node instanceof CompilationUnit) { String source = ((CompilationUnit) node).getSource(); int i = index - 1; char c; while (i >= 0 && (c = source.charAt(i)) != '\n') { if (c != '*' && !Character.isWhitespace(c)) { // Pre tag embedded in other text, so no indent. return ""; } --i; } String lineStart = source.substring(i + 1, index); i = lineStart.indexOf('*'); if (i == -1) { return lineStart; } // Indent could end with '*' instead of "* ", if there's no text after it. return (++i + 1) < lineStart.length() ? lineStart.substring(i + 1) : lineStart.substring(i); } return ""; } }