8266666: Implementation for snippets

Co-authored-by: Jonathan Gibbons <jjg@openjdk.org>
Co-authored-by: Hannes Wallnöfer <hannesw@openjdk.org>
Reviewed-by: jjg
This commit is contained in:
Pavel Rappo 2021-09-21 15:53:35 +00:00
parent 6d91a3eb7b
commit 0fc47e99d2
43 changed files with 5204 additions and 25 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -184,7 +184,12 @@ public interface DocumentationTool extends Tool, OptionChecker {
/**
* Location to search for taglets.
*/
TAGLET_PATH;
TAGLET_PATH,
/**
* Location to search for snippets.
*/
SNIPPET_PATH;
public String getName() { return name(); }

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -29,7 +29,7 @@ import java.util.List;
import javax.lang.model.element.Name;
/**
* A tree node for an attribute in an HTML element.
* A tree node for an attribute in an HTML element or tag.
*
* @since 1.8
*/

View File

@ -37,7 +37,7 @@ public interface DocTree {
enum Kind {
/**
* Used for instances of {@link AttributeTree}
* representing an HTML attribute.
* representing an attribute in an HTML element or tag.
*/
ATTRIBUTE,
@ -204,6 +204,12 @@ public interface DocTree {
*/
SINCE("since"),
/**
* Used for instances of {@link SnippetTree}
* representing an {@code @snippet} tag.
*/
SNIPPET("snippet"),
/**
* Used for instances of {@link EndElementTree}
* representing the start of an HTML element.

View File

@ -287,6 +287,21 @@ public interface DocTreeVisitor<R,P> {
*/
R visitSince(SinceTree node, P p);
/**
* Visits a {@code SnippetTree} node.
*
* @implSpec Visits the provided {@code SnippetTree} node
* by calling {@code visitOther(node, p)}.
*
* @param node the node being visited
* @param p a parameter value
* @return a result value
* @since 18
*/
default R visitSnippet(SnippetTree node, P p) {
return visitOther(node, p);
}
/**
* Visits a {@code StartElementTree} node.
* @param node the node being visited

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.source.doctree;
import java.util.List;
/**
* A tree node for an {@code @snippet} inline tag.
*
* <pre>
* {&#064;snippet :
* body
* }
*
* {&#064;snippet attributes}
*
* {&#064;snippet attributes :
* body
* }
* </pre>
*
* @since 18
*/
public interface SnippetTree extends InlineTagTree {
/**
* Returns the list of the attributes of the {@code @snippet} tag.
*
* @return the list of the attributes
*/
List<? extends DocTree> getAttributes();
/**
* Returns the body of the {@code @snippet} tag, or {@code null} if there is no body.
*
* @apiNote
* An instance of {@code SnippetTree} with an empty body differs from an
* instance of {@code SnippetTree} with no body.
* If a tag has no body, then calling this method returns {@code null}.
* If a tag has an empty body, then this method returns a {@code TextTree}
* whose {@link TextTree#getBody()} returns an empty string.
*
* @return the body of the tag, or {@code null} if there is no body
*/
TextTree getBody();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -58,6 +58,7 @@ import com.sun.source.doctree.SerialDataTree;
import com.sun.source.doctree.SerialFieldTree;
import com.sun.source.doctree.SerialTree;
import com.sun.source.doctree.SinceTree;
import com.sun.source.doctree.SnippetTree;
import com.sun.source.doctree.StartElementTree;
import com.sun.source.doctree.SummaryTree;
import com.sun.source.doctree.SystemPropertyTree;
@ -79,7 +80,7 @@ import com.sun.source.doctree.VersionTree;
*/
public interface DocTreeFactory {
/**
* Creates a new {@code AttributeTree} object, to represent an HTML attribute in an HTML tag.
* Creates a new {@code AttributeTree} object, to represent an attribute in an HTML element or tag.
* @param name the name of the attribute
* @param vkind the kind of the attribute value
* @param value the value, if any, of the attribute
@ -326,6 +327,15 @@ public interface DocTreeFactory {
*/
SinceTree newSinceTree(List<? extends DocTree> text);
/**
* Creates a new {@code SnippetTree} object, to represent a {@code {@snippet }} tag.
* @param attributes the attributes of the tag
* @param text the body of the tag, or {@code null} if the tag has no body (not to be confused with an empty body)
* @return a {@code SnippetTree} object
* @since 18
*/
SnippetTree newSnippetTree(List<? extends DocTree> attributes, TextTree text);
/**
* Creates a new {@code StartElementTree} object, to represent the start of an HTML element.
* @param name the name of the HTML element

View File

@ -492,6 +492,23 @@ public class DocTreeScanner<R,P> implements DocTreeVisitor<R,P> {
return scan(node.getBody(), p);
}
/**
* {@inheritDoc}
*
* @implSpec This implementation scans the children in left to right order.
*
* @param node {@inheritDoc}
* @param p {@inheritDoc}
* @return the result of scanning
* @since 18
*/
@Override
public R visitSnippet(SnippetTree node, P p) {
R r = scan(node.getAttributes(), p);
r = scanAndReduce(node.getBody(), p, r);
return r;
}
/**
* {@inheritDoc}
*

View File

@ -448,6 +448,21 @@ public class SimpleDocTreeVisitor<R,P> implements DocTreeVisitor<R, P> {
return defaultAction(node, p);
}
/**
* {@inheritDoc}
*
* @implSpec This implementation calls {@code defaultAction}.
*
* @param node {@inheritDoc}
* @param p {@inheritDoc}
* @return the result of {@code defaultAction}
* @since 18
*/
@Override
public R visitSnippet(SnippetTree node, P p) {
return defaultAction(node, p);
}
/**
* {@inheritDoc}
*

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -1062,6 +1062,13 @@ public class DocCommentParser {
return Character.isWhitespace(ch);
}
protected boolean isHorizontalWhitespace(char ch) {
// This parser treats `\f` as a line break (see `nextChar`).
// To be consistent with that behaviour, this method does the same.
// (see JDK-8273809)
return ch == ' ' || ch == '\t';
}
protected void skipWhitespace() {
while (bp < buflen && isWhitespace(ch)) {
nextChar();
@ -1397,6 +1404,93 @@ public class DocCommentParser {
}
},
// {@snippet attributes :
// body}
new TagParser(TagParser.Kind.INLINE, DCTree.Kind.SNIPPET) {
@Override
DCTree parse(int pos) throws ParseException {
skipWhitespace();
List<DCTree> attributes = tagAttrs();
// expect "}" or ":"
if (ch == '}') {
nextChar();
return m.at(pos).newSnippetTree(attributes, null);
} else if (ch == ':') {
newline = false;
// consume ':'
nextChar();
// expect optional whitespace followed by mandatory newline
while (bp < buflen && isHorizontalWhitespace(ch)) {
nextChar();
}
// check that we are looking at newline
if (!newline) {
if (bp >= buf.length - 1) {
throw new ParseException("dc.no.content");
}
throw new ParseException("dc.unexpected.content");
}
// consume newline
nextChar();
DCText text = inlineText(WhitespaceRetentionPolicy.RETAIN_ALL);
nextChar();
return m.at(pos).newSnippetTree(attributes, text);
} else if (bp >= buf.length - 1) {
throw new ParseException("dc.no.content");
} else {
throw new ParseException("dc.unexpected.content");
}
}
/*
* Reads a series of inline snippet tag attributes.
*
* Attributes are terminated by the first of ":" (colon) or
* an unmatched "}" (closing curly).
*/
private List<DCTree> tagAttrs() {
ListBuffer<DCTree> attrs = new ListBuffer<>();
skipWhitespace();
while (bp < buflen && isIdentifierStart(ch)) {
int namePos = bp;
Name name = readAttributeName();
skipWhitespace();
List<DCTree> value = null;
ValueKind vkind = ValueKind.EMPTY;
if (ch == '=') {
ListBuffer<DCTree> v = new ListBuffer<>();
nextChar();
skipWhitespace();
if (ch == '\'' || ch == '"') {
newline = false;
vkind = (ch == '\'') ? ValueKind.SINGLE : ValueKind.DOUBLE;
char quote = ch;
nextChar();
textStart = bp;
while (bp < buflen && ch != quote) {
nextChar();
}
addPendingText(v, bp - 1);
nextChar();
} else {
vkind = ValueKind.UNQUOTED;
textStart = bp;
// Stop on '}' and ':' for them to be re-consumed by non-attribute parts of tag
while (bp < buflen && (ch != '}' && ch != ':' && !isUnquotedAttrValueTerminator(ch))) {
nextChar();
}
addPendingText(v, bp - 1);
}
skipWhitespace();
value = v.toList();
}
DCAttribute attr = m.at(namePos).newAttributeTree(name, vkind, value);
attrs.add(attr);
}
return attrs.toList();
}
},
// {@summary summary-text}
new TagParser(TagParser.Kind.INLINE, DCTree.Kind.SUMMARY) {
@Override

View File

@ -857,6 +857,36 @@ public abstract class DCTree implements DocTree {
}
}
public static class DCSnippet extends DCInlineTag implements SnippetTree {
public final List<? extends DocTree> attributes;
public final DCText body;
public DCSnippet(List<DCTree> attributes, DCText body) {
this.body = body;
this.attributes = attributes;
}
@Override @DefinedBy(Api.COMPILER_TREE)
public Kind getKind() {
return Kind.SNIPPET;
}
@Override @DefinedBy(Api.COMPILER_TREE)
public <R, D> R accept(DocTreeVisitor<R, D> v, D d) {
return v.visitSnippet(this, d);
}
@Override @DefinedBy(Api.COMPILER_TREE)
public List<? extends DocTree> getAttributes() {
return attributes;
}
@Override @DefinedBy(Api.COMPILER_TREE)
public TextTree getBody() {
return body;
}
}
public static class DCStartElement extends DCEndPosTree<DCStartElement> implements StartElementTree {
public final Name name;
public final List<DCTree> attrs;

View File

@ -490,6 +490,27 @@ public class DocPretty implements DocTreeVisitor<Void,Void> {
return null;
}
@Override @DefinedBy(Api.COMPILER_TREE)
public Void visitSnippet(SnippetTree node, Void p) {
try {
print("{");
printTagName(node);
List<? extends DocTree> attrs = node.getAttributes();
if (!attrs.isEmpty()) {
print(" ");
print(attrs, " ");
}
if (node.getBody() != null) {
print(" :\n");
print(node.getBody());
}
print("}");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return null;
}
@Override @DefinedBy(Api.COMPILER_TREE)
public Void visitStartElement(StartElementTree node, Void p) {
try {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -29,7 +29,6 @@ import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
@ -76,6 +75,7 @@ import com.sun.tools.javac.tree.DCTree.DCSerial;
import com.sun.tools.javac.tree.DCTree.DCSerialData;
import com.sun.tools.javac.tree.DCTree.DCSerialField;
import com.sun.tools.javac.tree.DCTree.DCSince;
import com.sun.tools.javac.tree.DCTree.DCSnippet;
import com.sun.tools.javac.tree.DCTree.DCStartElement;
import com.sun.tools.javac.tree.DCTree.DCSummary;
import com.sun.tools.javac.tree.DCTree.DCSystemProperty;
@ -431,6 +431,13 @@ public class DocTreeMaker implements DocTreeFactory {
return tree;
}
@Override @DefinedBy(Api.COMPILER_TREE)
public DCSnippet newSnippetTree(List<? extends DocTree> attributes, TextTree text) {
DCSnippet tree = new DCSnippet(cast(attributes), (DCText) text);
tree.pos = pos;
return tree;
}
@Override @DefinedBy(Api.COMPILER_TREE)
public DCStartElement newStartElementTree(Name name, List<? extends DocTree> attrs, boolean selfClosing) {
DCStartElement tree = new DCStartElement(name, cast(attrs), selfClosing);

View File

@ -1142,6 +1142,133 @@ public class HtmlDocletWriter {
}
}
// TODO: this method and seeTagToContent share much of the code; consider factoring common pieces out
public Content linkToContent(Element referrer, Element target, String targetSignature, String text) {
CommentHelper ch = utils.getCommentHelper(referrer);
boolean isLinkPlain = false; // TODO: for now
Content labelContent = plainOrCode(isLinkPlain, Text.of(text));
TypeElement refClass = ch.getReferencedClass(target);
Element refMem = ch.getReferencedMember(target);
String refMemName = ch.getReferencedMemberName(targetSignature);
if (refMemName == null && refMem != null) {
refMemName = refMem.toString();
}
if (refClass == null) {
ModuleElement refModule = ch.getReferencedModule(target);
if (refModule != null && utils.isIncluded(refModule)) {
return getModuleLink(refModule, labelContent);
}
//@see is not referencing an included class
PackageElement refPackage = ch.getReferencedPackage(target);
if (refPackage != null && utils.isIncluded(refPackage)) {
//@see is referencing an included package
if (labelContent.isEmpty())
labelContent = plainOrCode(isLinkPlain,
Text.of(refPackage.getQualifiedName()));
return getPackageLink(refPackage, labelContent);
} else {
// @see is not referencing an included class, module or package. Check for cross links.
String refModuleName = ch.getReferencedModuleName(targetSignature);
DocLink elementCrossLink = (refPackage != null) ? getCrossPackageLink(refPackage) :
(configuration.extern.isModule(refModuleName))
? getCrossModuleLink(utils.elementUtils.getModuleElement(refModuleName))
: null;
if (elementCrossLink != null) {
// Element cross link found
return links.createExternalLink(elementCrossLink, labelContent);
} else {
// No cross link found so print warning
// TODO:
// messages.warning(ch.getDocTreePath(see),
// "doclet.see.class_or_package_not_found",
// "@" + tagName,
// seeText);
return labelContent;
}
}
} else if (refMemName == null) {
// Must be a class reference since refClass is not null and refMemName is null.
if (labelContent.isEmpty()) {
if (!refClass.getTypeParameters().isEmpty() && targetSignature.contains("<")) {
// If this is a generic type link try to use the TypeMirror representation.
// TODO:
// TypeMirror refType = ch.getReferencedType(target);
TypeMirror refType = target.asType();
if (refType != null) {
return plainOrCode(isLinkPlain, getLink(
new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.DEFAULT, refType)));
}
}
labelContent = plainOrCode(isLinkPlain, Text.of(utils.getSimpleName(refClass)));
}
return getLink(new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.DEFAULT, refClass)
.label(labelContent));
} else if (refMem == null) {
// Must be a member reference since refClass is not null and refMemName is not null.
// However, refMem is null, so this referenced member does not exist.
return labelContent;
} else {
// Must be a member reference since refClass is not null and refMemName is not null.
// refMem is not null, so this @see tag must be referencing a valid member.
TypeElement containing = utils.getEnclosingTypeElement(refMem);
// Find the enclosing type where the method is actually visible
// in the inheritance hierarchy.
ExecutableElement overriddenMethod = null;
if (refMem.getKind() == ElementKind.METHOD) {
VisibleMemberTable vmt = configuration.getVisibleMemberTable(containing);
overriddenMethod = vmt.getOverriddenMethod((ExecutableElement)refMem);
if (overriddenMethod != null)
containing = utils.getEnclosingTypeElement(overriddenMethod);
}
if (targetSignature.trim().startsWith("#") &&
! (utils.isPublic(containing) || utils.isLinkable(containing))) {
// Since the link is relative and the holder is not even being
// documented, this must be an inherited link. Redirect it.
// The current class either overrides the referenced member or
// inherits it automatically.
if (this instanceof ClassWriterImpl writer) {
containing = writer.getTypeElement();
} else if (!utils.isPublic(containing)) {
// TODO:
// messages.warning(
// ch.getDocTreePath(see), "doclet.see.class_or_package_not_accessible",
// tagName, utils.getFullyQualifiedName(containing));
} else {
// TODO:
// messages.warning(
// ch.getDocTreePath(see), "doclet.see.class_or_package_not_found",
// tagName, seeText);
}
}
if (configuration.currentTypeElement != containing) {
refMemName = (utils.isConstructor(refMem))
? refMemName
: utils.getSimpleName(containing) + "." + refMemName;
}
if (utils.isExecutableElement(refMem)) {
if (refMemName.indexOf('(') < 0) {
refMemName += utils.makeSignature((ExecutableElement) refMem, null, true);
}
if (overriddenMethod != null) {
// The method to actually link.
refMem = overriddenMethod;
}
}
return getDocLink(HtmlLinkInfo.Kind.SEE_TAG, containing,
refMem, (labelContent.isEmpty()
? plainOrCode(isLinkPlain, Text.of(text))
: labelContent), null, false);
}
}
private String removeTrailingSlash(String s) {
return s.endsWith("/") ? s.substring(0, s.length() -1) : s;
}

View File

@ -27,6 +27,7 @@ package jdk.javadoc.internal.doclets.formats.html;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -47,8 +48,10 @@ import com.sun.source.doctree.LiteralTree;
import com.sun.source.doctree.ParamTree;
import com.sun.source.doctree.ReturnTree;
import com.sun.source.doctree.SeeTree;
import com.sun.source.doctree.SnippetTree;
import com.sun.source.doctree.SystemPropertyTree;
import com.sun.source.doctree.ThrowsTree;
import com.sun.source.util.DocTreePath;
import jdk.javadoc.internal.doclets.formats.html.markup.ContentBuilder;
import jdk.javadoc.internal.doclets.formats.html.markup.HtmlId;
import jdk.javadoc.internal.doclets.formats.html.markup.HtmlStyle;
@ -63,6 +66,8 @@ import jdk.javadoc.internal.doclets.toolkit.Resources;
import jdk.javadoc.internal.doclets.toolkit.builders.SerializedFormBuilder;
import jdk.javadoc.internal.doclets.toolkit.taglets.ParamTaglet;
import jdk.javadoc.internal.doclets.toolkit.taglets.TagletWriter;
import jdk.javadoc.internal.doclets.toolkit.taglets.snippet.Style;
import jdk.javadoc.internal.doclets.toolkit.taglets.snippet.StyledText;
import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper;
import jdk.javadoc.internal.doclets.toolkit.util.DocLink;
import jdk.javadoc.internal.doclets.toolkit.util.DocPath;
@ -374,6 +379,81 @@ public class TagletWriterImpl extends TagletWriter {
HtmlTree.DD(body));
}
@Override
protected Content snippetTagOutput(Element element, SnippetTree tag, StyledText content) {
HtmlTree result = new HtmlTree(TagName.PRE).setStyle(HtmlStyle.snippet);
result.add(Text.of(utils.normalizeNewlines("\n")));
content.consumeBy((styles, sequence) -> {
CharSequence text = utils.normalizeNewlines(sequence);
if (styles.isEmpty()) {
result.add(text);
} else {
Element e = null;
String t = null;
boolean linkEncountered = false;
Set<String> classes = new HashSet<>();
for (Style s : styles) {
if (s instanceof Style.Name n) {
classes.add(n.name());
} else if (s instanceof Style.Link l) {
assert !linkEncountered; // TODO: do not assert; pick the first link report on subsequent
linkEncountered = true;
t = l.target();
e = getLinkedElement(element, t);
if (e == null) {
// TODO: diagnostic output
}
} else if (s instanceof Style.Markup) {
} else {
// TODO: transform this if...else into an exhaustive
// switch over the sealed Style hierarchy when "Pattern
// Matching for switch" has been implemented (JEP 406
// and friends)
throw new AssertionError(styles);
}
}
Content c;
if (linkEncountered) {
assert e != null;
String line = sequence.toString();
String strippedLine = line.strip();
int idx = line.indexOf(strippedLine);
assert idx >= 0; // because the stripped line is a substring of the line being stripped
Text whitespace = Text.of(line.substring(0, idx));
// If the leading whitespace is not excluded from the link,
// browsers might exhibit unwanted behavior. For example, a
// browser might display hand-click cursor while user hovers
// over that whitespace portion of the line; or use
// underline decoration.
c = new ContentBuilder(whitespace, htmlWriter.linkToContent(element, e, t, strippedLine));
// We don't care about trailing whitespace.
} else {
c = HtmlTree.SPAN(Text.of(sequence));
classes.forEach(((HtmlTree) c)::addStyle);
}
result.add(c);
}
});
return result;
}
/*
* Returns the element that is linked from the context of the referrer using
* the provided signature; returns null if such element could not be found.
*
* This method is to be used when it is the target of the link that is
* important, not the container of the link (e.g. was it an @see,
* @link/@linkplain or @snippet tags, etc.)
*/
public Element getLinkedElement(Element referer, String signature) {
var factory = utils.docTrees.getDocTreeFactory();
var docCommentTree = utils.getDocCommentTree(referer);
var rootPath = new DocTreePath(utils.getTreePath(referer), docCommentTree);
var reference = factory.newReferenceTree(signature);
var fabricatedPath = new DocTreePath(rootPath, reference);
return utils.docTrees.getElement(fabricatedPath);
}
@Override
protected Content systemPropertyTagOutput(Element element, SystemPropertyTree tag) {
String tagText = tag.getPropertyName().toString();

View File

@ -75,6 +75,11 @@ public enum HtmlStyle {
typeNameLabel,
typeNameLink,
/**
* The class of the {@code pre} element presenting a snippet.
*/
snippet,
//<editor-fold desc="navigation bar">
//
// The following constants are used for the main navigation bar that appears in the
@ -803,6 +808,7 @@ public enum HtmlStyle {
* The class of the {@code body} element for the page for the class hierarchy.
*/
treePage,
//</editor-fold>
//<editor-fold desc="help page">

View File

@ -538,6 +538,12 @@ doclet.usage.taglet.description=\
doclet.usage.tagletpath.description=\
The path to Taglets
doclet.usage.snippet-path.parameters=\
<path>
doclet.usage.snippet-path.description=\
The path for external snippets
doclet.usage.charset.parameters=\
<charset>
doclet.usage.charset.description=\

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -26,9 +26,13 @@
package jdk.javadoc.internal.doclets.toolkit;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -50,8 +54,10 @@ import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleElementVisitor14;
import javax.tools.DocumentationTool;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.DocTreePath;
@ -374,6 +380,28 @@ public abstract class BaseConfiguration {
extern.checkPlatformLinks(options.linkPlatformProperties(), reporter);
}
typeElementCatalog = new TypeElementCatalog(includedTypeElements, this);
String snippetPath = options.snippetPath();
if (snippetPath != null) {
Messages messages = getMessages();
JavaFileManager fm = getFileManager();
if (fm instanceof StandardJavaFileManager) {
try {
List<Path> sp = Arrays.stream(snippetPath.split(File.pathSeparator))
.map(Path::of)
.toList();
StandardJavaFileManager sfm = (StandardJavaFileManager) fm;
sfm.setLocationFromPaths(DocumentationTool.Location.SNIPPET_PATH, sp);
} catch (IOException | InvalidPathException e) {
throw new SimpleDocletException(messages.getResources().getText(
"doclet.error_setting_snippet_path", snippetPath, e), e);
}
} else {
throw new SimpleDocletException(messages.getResources().getText(
"doclet.cannot_use_snippet_path", snippetPath));
}
}
initTagletManager(options.customTagStrs());
options.groupPairs().forEach(grp -> {
if (showModules) {

View File

@ -282,6 +282,12 @@ public abstract class BaseOptions {
*/
private String tagletPath = null;
/**
* Argument for command-line option {@code --snippet-path}.
* The path for external snippets.
*/
private String snippetPath = null;
//</editor-fold>
private final BaseConfiguration config;
@ -554,6 +560,14 @@ public abstract class BaseOptions {
}
},
new Option(resources, "--snippet-path", 1) {
@Override
public boolean process(String opt, List<String> args) {
snippetPath = args.get(0);
return true;
}
},
new Option(resources, "-version") {
@Override
public boolean process(String opt, List<String> args) {
@ -962,6 +976,14 @@ public abstract class BaseOptions {
return tagletPath;
}
/**
* Argument for command-line option {@code --snippet-path}.
* The path for external snippets.
*/
public String snippetPath() {
return snippetPath;
}
protected abstract static class Option implements Doclet.Option, Comparable<Option> {
private final String[] names;
private final String parameters;

View File

@ -347,4 +347,54 @@ doclet.search.packages=Packages
doclet.search.classes_and_interfaces=Classes and Interfaces
doclet.search.types=Types
doclet.search.members=Members
doclet.search.search_tags=Search Tags
doclet.search.search_tags=Search Tags
doclet.snippet.contents.none=\
@snippet does not specify contents
doclet.snippet.contents.ambiguity.external=\
@snippet specifies multiple external contents, which is ambiguous
doclet.snippet.region.not_found=\
region not found: "{0}"
doclet.tag.attribute.value.illegal=\
illegal value for attribute "{0}": "{1}"
doclet.tag.attribute.repeated=\
repeated attribute: "{0}"
doclet.snippet.contents.mismatch=\
contents mismatch:\n{0}
doclet.snippet.markup=\
snippet markup: {0}
doclet.snippet.markup.attribute.absent=\
missing attribute "{0}"
doclet.snippet.markup.attribute.simultaneous.use=\
attributes "{0}" and "{1}" used simultaneously
doclet.snippet.markup.attribute.unexpected=\
unexpected attribute
doclet.snippet.markup.attribute.value.invalid=\
invalid attribute value
doclet.snippet.markup.attribute.value.unterminated=\
unterminated attribute value
doclet.snippet.markup.regex.invalid=\
invalid regex
doclet.snippet.markup.region.duplicated=\
duplicated region
doclet.snippet.markup.region.none=\
no region to end
doclet.snippet.markup.region.unpaired=\
unpaired region
doclet.snippet.markup.tag.non.existent.lines=\
tag refers to non-existent lines
# 0: path
doclet.cannot_use_snippet_path=\
Cannot use ''--snippet-path'' option with the given file manager: {0}
# 0: path; 1: exception
doclet.error_setting_snippet_path=\
Error setting snippet path {0}: {1}

View File

@ -863,3 +863,24 @@ table.striped > tbody > tr > th {
padding-right: 12px;
}
}
pre.snippet {
background-color: #ebecee;
padding: 10px;
margin: 12px 0;
overflow: auto;
white-space: pre;
}
pre.snippet .italic {
font-style: italic;
}
pre.snippet .bold {
font-weight: bold;
}
pre.snippet .highlighted {
background-color: #f7c590;
border-radius: 10%;
}

View File

@ -0,0 +1,367 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets;
import java.io.IOException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.tools.Diagnostic;
import javax.tools.DocumentationTool.Location;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import com.sun.source.doctree.AttributeTree;
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.SnippetTree;
import com.sun.source.doctree.TextTree;
import jdk.javadoc.doclet.Taglet;
import jdk.javadoc.internal.doclets.toolkit.Content;
import jdk.javadoc.internal.doclets.toolkit.DocletElement;
import jdk.javadoc.internal.doclets.toolkit.Resources;
import jdk.javadoc.internal.doclets.toolkit.taglets.snippet.Action;
import jdk.javadoc.internal.doclets.toolkit.taglets.snippet.ParseException;
import jdk.javadoc.internal.doclets.toolkit.taglets.snippet.Parser;
import jdk.javadoc.internal.doclets.toolkit.taglets.snippet.StyledText;
import jdk.javadoc.internal.doclets.toolkit.util.Utils;
/**
* A taglet that represents the {@code @snippet} tag.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class SnippetTaglet extends BaseTaglet {
public SnippetTaglet() {
super(DocTree.Kind.SNIPPET, true, EnumSet.allOf(Taglet.Location.class));
}
/*
* A snippet can specify content by value (inline), by reference (external)
* or both (hybrid).
*
* To specify content by value, a snippet uses its body; the body of
* a snippet is the content.
*
* To specify content by reference, a snippet uses either the "class"
* or "file" attribute; the value of that attribute refers to the content.
*
* A snippet can specify the "region" attribute. That attribute refines
* the location of the content. The value of that attribute must match
* one of the named regions in the snippets content.
*/
@Override
public Content getInlineTagOutput(Element holder, DocTree tag, TagletWriter writer) {
SnippetTree snippetTag = (SnippetTree) tag;
// organize snippet attributes in a map, performing basic checks along the way
Map<String, AttributeTree> attributes = new HashMap<>();
for (DocTree d : snippetTag.getAttributes()) {
if (!(d instanceof AttributeTree a)) {
continue; // this might be an ErroneousTree
}
if (attributes.putIfAbsent(a.getName().toString(), a) == null) {
continue;
}
// two like-named attributes found; although we report on the most
// recently encountered of the two, the iteration order might differ
// from the source order (see JDK-8266826)
error(writer, holder, a, "doclet.tag.attribute.repeated",
a.getName().toString());
return badSnippet(writer);
}
final String CLASS = "class";
final String FILE = "file";
final boolean containsClass = attributes.containsKey(CLASS);
final boolean containsFile = attributes.containsKey(FILE);
final boolean containsBody = snippetTag.getBody() != null;
if (containsClass && containsFile) {
error(writer, holder, attributes.get(CLASS),
"doclet.snippet.contents.ambiguity.external");
return badSnippet(writer);
} else if (!containsClass && !containsFile && !containsBody) {
error(writer, holder, tag, "doclet.snippet.contents.none");
return badSnippet(writer);
}
String regionName = null;
AttributeTree region = attributes.get("region");
if (region != null) {
regionName = stringOf(region.getValue());
if (regionName.isBlank()) {
error(writer, holder, region, "doclet.tag.attribute.value.illegal",
"region", region.getValue());
return badSnippet(writer);
}
}
String inlineContent = null, externalContent = null;
if (containsBody) {
inlineContent = snippetTag.getBody().getBody();
}
FileObject fileObject = null;
if (containsFile || containsClass) {
AttributeTree a;
String v = containsFile
? stringOf((a = attributes.get(FILE)).getValue())
: stringOf((a = attributes.get(CLASS)).getValue()).replace(".", "/") + ".java";
if (v.isBlank()) {
error(writer, holder, a, "doclet.tag.attribute.value.illegal",
containsFile ? FILE : CLASS, v);
}
// we didn't create JavaFileManager, so we won't close it; even if an error occurs
var fileManager = writer.configuration().getFileManager();
// first, look in local snippet-files subdirectory
Utils utils = writer.configuration().utils;
PackageElement pkg = getPackageElement(holder, utils);
JavaFileManager.Location l = utils.getLocationForPackage(pkg);
String relativeName = "snippet-files/" + v;
String packageName = packageName(pkg, utils);
try {
fileObject = fileManager.getFileForInput(l, packageName, relativeName);
// if not found in local snippet-files directory, look on snippet path
if (fileObject == null && fileManager.hasLocation(Location.SNIPPET_PATH)) {
fileObject = fileManager.getFileForInput(Location.SNIPPET_PATH, "", v);
}
} catch (IOException | IllegalArgumentException e) {
// JavaFileManager.getFileForInput can throw IllegalArgumentException in certain cases
error(writer, holder, a, "doclet.exception.read.file", v, e.getCause());
return badSnippet(writer);
}
if (fileObject == null) {
// i.e. the file does not exist
error(writer, holder, a, "doclet.File_not_found", v);
return badSnippet(writer);
}
try {
externalContent = fileObject.getCharContent(true).toString();
} catch (IOException e) {
error(writer, holder, a, "doclet.exception.read.file",
fileObject.getName(), e.getCause());
return badSnippet(writer);
}
}
// TODO cache parsed external snippet (WeakHashMap)
StyledText inlineSnippet = null;
StyledText externalSnippet = null;
try {
if (inlineContent != null) {
inlineSnippet = parse(writer.configuration().getDocResources(), inlineContent);
}
} catch (ParseException e) {
var path = writer.configuration().utils.getCommentHelper(holder)
.getDocTreePath(snippetTag.getBody());
// TODO: there should be a method in Messages; that method should mirror Reporter's; use that method instead accessing Reporter.
String msg = writer.configuration().getDocResources()
.getText("doclet.snippet.markup", e.getMessage());
writer.configuration().getReporter().print(Diagnostic.Kind.ERROR,
path, e.getPosition(), e.getPosition(), e.getPosition(), msg);
return badSnippet(writer);
}
try {
if (externalContent != null) {
externalSnippet = parse(writer.configuration().getDocResources(), externalContent);
}
} catch (ParseException e) {
assert fileObject != null;
writer.configuration().getMessages().error(fileObject, e.getPosition(),
e.getPosition(), e.getPosition(), "doclet.snippet.markup", e.getMessage());
return badSnippet(writer);
}
// the region must be matched at least in one content: it can be matched
// in both, but never in none
if (regionName != null) {
StyledText r1 = null;
StyledText r2 = null;
if (inlineSnippet != null) {
r1 = inlineSnippet.getBookmarkedText(regionName);
if (r1 != null) {
inlineSnippet = r1;
}
}
if (externalSnippet != null) {
r2 = externalSnippet.getBookmarkedText(regionName);
if (r2 != null) {
externalSnippet = r2;
}
}
if (r1 == null && r2 == null) {
error(writer, holder, tag, "doclet.snippet.region.not_found", regionName);
return badSnippet(writer);
}
}
if (inlineSnippet != null) {
inlineSnippet = toDisplayForm(inlineSnippet);
}
if (externalSnippet != null) {
externalSnippet = toDisplayForm(externalSnippet);
}
if (inlineSnippet != null && externalSnippet != null) {
String inlineStr = inlineSnippet.asCharSequence().toString();
String externalStr = externalSnippet.asCharSequence().toString();
if (!Objects.equals(inlineStr, externalStr)) {
error(writer, holder, tag, "doclet.snippet.contents.mismatch", diff(inlineStr, externalStr));
// output one above the other
return badSnippet(writer);
}
}
assert inlineSnippet != null || externalSnippet != null;
StyledText text = inlineSnippet != null ? inlineSnippet : externalSnippet;
return writer.snippetTagOutput(holder, snippetTag, text);
}
/*
* Maybe there's a case for implementing a proper (or at least more helpful)
* diff view, but for now simply outputting both sides of a hybrid snippet
* would do. A user could then use a diff tool of their choice to compare
* those sides.
*
* There's a separate issue of mapping discrepancies back to their
* originating source in the doc comment and the external file. Maybe there
* is a value in it, or maybe there isn't. In any case, accurate mapping
* would not be trivial to code.
*/
private static String diff(String inline, String external) {
return """
----------------- inline -------------------
%s
----------------- external -----------------
%s
""".formatted(inline, external);
}
private StyledText parse(Resources resources, String content) throws ParseException {
Parser.Result result = new Parser(resources).parse(content);
result.actions().forEach(Action::perform);
return result.text();
}
private static String stringOf(List<? extends DocTree> value) {
return value.stream()
// value consists of TextTree or ErroneousTree nodes;
// ErroneousTree is a subtype of TextTree
.map(t -> ((TextTree) t).getBody())
.collect(Collectors.joining());
}
private void error(TagletWriter writer, Element holder, DocTree tag, String key, Object... args) {
writer.configuration().getMessages().error(
writer.configuration().utils.getCommentHelper(holder).getDocTreePath(tag), key, args);
}
private Content badSnippet(TagletWriter writer) {
return writer.getOutputInstance().add("bad snippet");
}
private String packageName(PackageElement pkg, Utils utils) {
return utils.getPackageName(pkg);
}
private static PackageElement getPackageElement(Element e, Utils utils) {
if (e instanceof DocletElement de) {
return de.getPackageElement();
} else {
return utils.elementUtils.getPackageOf(e);
}
}
/*
* Returns a version of styled text that can be rendered into HTML or
* compared to another such version. The latter is used to decide if inline
* and external parts of a hybrid snippet match.
*
* Use this method to obtain a final version of text. After all
* transformations on text have been performed, call this method with that
* text and then use the returned result as described above.
*/
private static StyledText toDisplayForm(StyledText source) {
var sourceString = source.asCharSequence().toString();
var result = new StyledText();
var originalLines = sourceString.lines().iterator();
var unindentedLines = sourceString.stripIndent().lines().iterator();
// done; the rest of the method translates the stripIndent
// transformation performed on a character sequence to the styled
// text that this sequence originates from, line by line
int pos = 0;
// overcome a "quirk" of String.lines
boolean endsWithLineFeed = !sourceString.isEmpty() && sourceString.charAt(source.length() - 1) == '\n';
while (originalLines.hasNext() && unindentedLines.hasNext()) { // [^1]
String originalLine = originalLines.next();
String unindentedLine = unindentedLines.next();
// the search MUST succeed
int idx = originalLine.indexOf(unindentedLine);
// assume newlines are always of the \n form
// append the found fragment
result.append(source.subText(pos + idx, pos + idx + unindentedLine.length()));
// append the possibly styled newline, but not if it's the last line
int eol = pos + originalLine.length();
if (originalLines.hasNext() || endsWithLineFeed) {
result.append(source.subText(eol, eol + 1));
}
pos = eol + 1;
}
return result;
// [^1]: Checking hasNext() on both iterators might look unnecessary.
// However, there are strings for which those iterators return different
// number of lines. That is, there exists a string s, such that
//
// s.lines().count() != s.stripIndent().lines().count()
//
// The most trivial example of such a string is " ". In fact, any string
// with a trailing non-empty blank line would do.
}
}

View File

@ -655,6 +655,7 @@ public class TagletManager {
addStandardTaglet(new ValueTaglet());
addStandardTaglet(new LiteralTaglet());
addStandardTaglet(new CodeTaglet());
addStandardTaglet(new SnippetTaglet());
addStandardTaglet(new IndexTaglet());
addStandardTaglet(new SummaryTaglet());
addStandardTaglet(new SystemPropertyTaglet());

View File

@ -39,11 +39,13 @@ import com.sun.source.doctree.LiteralTree;
import com.sun.source.doctree.ParamTree;
import com.sun.source.doctree.ReturnTree;
import com.sun.source.doctree.SeeTree;
import com.sun.source.doctree.SnippetTree;
import com.sun.source.doctree.SystemPropertyTree;
import com.sun.source.doctree.ThrowsTree;
import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration;
import jdk.javadoc.internal.doclets.toolkit.Content;
import jdk.javadoc.internal.doclets.toolkit.taglets.Taglet.UnsupportedTagletOperationException;
import jdk.javadoc.internal.doclets.toolkit.taglets.snippet.StyledText;
import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper;
import jdk.javadoc.internal.doclets.toolkit.util.Utils;
@ -174,6 +176,16 @@ public abstract class TagletWriter {
*/
protected abstract Content simpleBlockTagOutput(Element element, List<? extends DocTree> simpleTags, String header);
/**
* Returns the output for a {@code {@snippet ...}} tag.
*
* @param element The element that owns the doc comment
* @param snippetTag the snippet tag
*
* @return the output
*/
protected abstract Content snippetTagOutput(Element element, SnippetTree snippetTag, StyledText text);
/**
* Returns the output for a {@code {@systemProperty...}} tag.
*

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
/**
* An action described by markup. Such an action is typically an opaque compound
* of primitive operations of {@link StyledText}.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public interface Action {
/**
* Performs this action.
*/
void perform();
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An action that applies an additional style to text.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public final class AddStyle implements Action {
private final Style style;
private final Pattern pattern;
private final StyledText text;
/**
* Constructs an action that applies an additional style to regex finds in
* text.
*
* @param style the style to add (to already existing styles)
* @param pattern the regex used to search the text
* @param text the text to search
*/
public AddStyle(Style style, Pattern pattern, StyledText text) {
this.style = style;
this.pattern = pattern;
this.text = text;
}
@Override
public void perform() {
var singleStyle = Set.of(style);
Matcher matcher = pattern.matcher(text.asCharSequence());
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
text.subText(start, end).addStyle(singleStyle);
}
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.util.Objects;
/*
* 1. The hierarchy of attributes is modelled as
*
* Attribute
* |
* +- Valueless
* |
* +- Valued
*
* not as
*
* Attribute (Valueless)
* |
* +- Valued
*
* because in conjunction with query operations of `Attributes`, `Valued` and
* `Valueless` should be more useful if neither is a subtype of the other.
*
* 2. `Attribute` is abstract because its sole purpose is to be a category.
*
* 3. This attribute abstraction is simpler than that of com.sun.source.doctree.AttributeTree.
* There's no need to have recursive structure similar to that of allowed by AttributeTree.
*/
/**
* A markup attribute.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
// TODO: uncomment /* sealed */ when minimum boot JDK version >= 17
public /* sealed */ abstract class Attribute {
private final String name;
private final int nameStartPosition;
private Attribute(String name, int nameStartPosition) {
this.name = Objects.requireNonNull(name);
this.nameStartPosition = nameStartPosition;
}
String name() {
return name;
}
int nameStartPosition() {
return nameStartPosition;
}
/*
* `Valued` can be later extended by classes such as DoublyQuoted,
* SinglyQuoted or Unquoted to form a (sealed) hierarchy. In that case,
* `Valued` should become abstract similarly to `Attribute`.
*/
final static class Valued extends Attribute {
private final String value;
private final int valueStartPosition;
Valued(String name, String value, int namePosition, int valueStartPosition) {
super(name, namePosition);
this.value = Objects.requireNonNull(value);
this.valueStartPosition = valueStartPosition;
}
String value() {
return value;
}
public int valueStartPosition() {
return valueStartPosition;
}
}
final static class Valueless extends Attribute {
Valueless(String name, int nameStartPosition) {
super(name, nameStartPosition);
}
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Convenient access to attributes.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public final class Attributes {
private final Map<String, List<Attribute>> attributes;
public Attributes(Collection<? extends Attribute> attributes) {
this.attributes = attributes
.stream()
.collect(Collectors.groupingBy(Attribute::name,
Collectors.toList()));
}
/*
* 1. If there are multiple attributes with the same name and type, it is
* unknown which one of these attributes will be returned.
*
* 2. If there are no attributes with this name and type, an empty optional
* will be returned.
*
* 3. If a non-specific (any/or/union/etc.) result is required, query for
* the Attribute.class type.
*/
public <T extends Attribute> Optional<T> get(String name, Class<T> type) {
return attributes.getOrDefault(name, List.of())
.stream()
.filter(type::isInstance)
.map(type::cast)
.findAny();
}
public int size() {
return attributes.values().stream().mapToInt(List::size).sum();
}
public boolean isEmpty() {
return attributes.isEmpty();
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
/**
* An action that associates text with a name.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public final class Bookmark implements Action {
private final String name;
private final StyledText text;
/**
* Constructs an action that associates text with a name.
*
* @param name the string (key) to associate text with
* @param text the text
*/
public Bookmark(String name, StyledText text) {
this.name = name;
this.text = text;
}
@Override
public void perform() {
text.subText(0, text.length()).bookmark(name);
}
}

View File

@ -0,0 +1,236 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.util.ArrayList;
import java.util.List;
import jdk.javadoc.internal.doclets.toolkit.Resources;
//
// markup-comment = { markup-tag } ;
// markup-tag = "@" , tag-name , {attribute} [":"] ;
//
// If optional trailing ":" is present, the tag refers to the next line
// rather than to this line.
//
/**
* A parser of a markup line.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public final class MarkupParser {
private final static int EOI = 0x1A;
private char[] buf;
private int bp;
private int buflen;
private char ch;
private final Resources resources;
public MarkupParser(Resources resources) {
this.resources = resources;
}
public List<Parser.Tag> parse(String input) throws ParseException {
// No vertical whitespace
assert input.codePoints().noneMatch(c -> c == '\n' || c == '\r');
buf = new char[input.length() + 1];
input.getChars(0, input.length(), buf, 0);
buf[buf.length - 1] = EOI;
buflen = buf.length - 1;
bp = -1;
nextChar();
return parse();
}
protected List<Parser.Tag> parse() throws ParseException {
List<Parser.Tag> tags = new ArrayList<>();
// TODO: what to do with leading and trailing unrecognized markup?
while (bp < buflen) {
switch (ch) {
case '@' -> tags.add(readTag());
default -> nextChar();
}
}
return tags;
}
protected Parser.Tag readTag() throws ParseException {
nextChar();
final int nameBp = bp;
String name = readIdentifier();
skipWhitespace();
boolean appliesToNextLine = false;
List<Attribute> attributes = List.of();
if (ch == ':') {
appliesToNextLine = true;
nextChar();
} else {
attributes = attrs();
skipWhitespace();
if (ch == ':') {
appliesToNextLine = true;
nextChar();
}
}
Parser.Tag i = new Parser.Tag();
i.nameLineOffset = nameBp;
i.name = name;
i.attributes = attributes;
i.appliesToNextLine = appliesToNextLine;
return i;
}
protected String readIdentifier() {
int start = bp;
nextChar();
while (bp < buflen && (Character.isUnicodeIdentifierPart(ch) || ch == '-')) {
nextChar();
}
return new String(buf, start, bp - start);
}
protected void skipWhitespace() {
while (bp < buflen && Character.isWhitespace(ch)) {
nextChar();
}
}
void nextChar() {
ch = buf[bp < buflen ? ++bp : buflen];
}
// Parsing machinery is adapted from com.sun.tools.javac.parser.DocCommentParser:
private enum ValueKind {
EMPTY,
UNQUOTED,
SINGLE_QUOTED,
DOUBLE_QUOTED;
}
protected List<Attribute> attrs() throws ParseException {
List<Attribute> attrs = new ArrayList<>();
skipWhitespace();
while (bp < buflen && isIdentifierStart(ch)) {
int nameStartPos = bp;
String name = readAttributeName();
skipWhitespace();
StringBuilder value = new StringBuilder();
var vkind = ValueKind.EMPTY;
int valueStartPos = -1;
if (ch == '=') {
nextChar();
skipWhitespace();
if (ch == '\'' || ch == '"') {
vkind = (ch == '\'') ? ValueKind.SINGLE_QUOTED : ValueKind.DOUBLE_QUOTED;
char quote = ch;
nextChar();
valueStartPos = bp;
while (bp < buflen && ch != quote) {
nextChar();
}
if (bp >= buflen) {
String message = resources.getText("doclet.snippet.markup.attribute.value.unterminated");
throw new ParseException(() -> message, bp - 1);
}
addPendingText(value, valueStartPos, bp - 1);
nextChar();
} else {
vkind = ValueKind.UNQUOTED;
valueStartPos = bp;
while (bp < buflen && !isUnquotedAttrValueTerminator(ch)) {
nextChar();
}
// Unlike the case with a quoted value, there's no need to
// check for unexpected EOL here; an EOL would simply mean
// "end of unquoted value".
addPendingText(value, valueStartPos, bp - 1);
}
skipWhitespace();
}
// material implication:
// if vkind != EMPTY then it must be the case that valueStartPos >=0
assert !(vkind != ValueKind.EMPTY && valueStartPos < 0);
var attribute = vkind == ValueKind.EMPTY ?
new Attribute.Valueless(name, nameStartPos) :
new Attribute.Valued(name, value.toString(), nameStartPos, valueStartPos);
attrs.add(attribute);
}
return attrs;
}
protected boolean isIdentifierStart(char ch) {
return Character.isUnicodeIdentifierStart(ch);
}
protected String readAttributeName() {
int start = bp;
nextChar();
while (bp < buflen && (Character.isUnicodeIdentifierPart(ch) || ch == '-'))
nextChar();
return new String(buf, start, bp - start);
}
// Similar to https://html.spec.whatwg.org/multipage/syntax.html#unquoted
protected boolean isUnquotedAttrValueTerminator(char ch) {
switch (ch) {
case ':': // indicates that the instruction relates to the next line
case ' ': case '\t':
case '"': case '\'': case '`':
case '=': case '<': case '>':
return true;
default:
return false;
}
}
protected void addPendingText(StringBuilder b, int textStart, int textEnd) {
if (textStart != -1) {
if (textStart <= textEnd) {
b.append(buf, textStart, (textEnd - textStart) + 1);
}
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.util.function.Supplier;
/**
* An exception thrown by {@link Parser} and {@link MarkupParser}.
*
* This exception is only used to capture a user-facing error message.
* The message supplier is accepted not to control when to obtain a message,
* but to abstract how to obtain it.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class ParseException extends Exception {
@java.io.Serial
private static final long serialVersionUID = 1;
private final int index;
public ParseException(Supplier<String> messageSupplier, int position) {
super(messageSupplier.get());
if (position < 0) {
throw new IllegalArgumentException(String.valueOf(position));
}
this.index = position;
}
public int getPosition() {
return index;
}
}

View File

@ -0,0 +1,531 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import jdk.javadoc.internal.doclets.toolkit.Resources;
/*
* Semantics of a EOL comment; plus
* 1. This parser treats input as plain text. This may result in markup being
* produced from unexpected places; for example, when parsing Java text blocks:
*
* String text =
* """
* // @start x
* """;
*
* false positives are possible, but false negatives are not.
* To remediate that, perhaps a no-op trailing // @start x @end x might be added.
*
* 2. To allow some preexisting constructs, unknown actions in a leading position are skipped;
* for example, "// @formatter:on" marker in IntelliJ IDEA is ignored.
*
* 3. This match's value can be confused for a trailing markup.
*
* String x; // comment // another comment // @formatter:on // @highlight match="// @"
*
* Do we need escapes?
*
* 4. Rules for EOL are very different among formats: compare Java's // with properties' #/!
*
* 5. A convenience `end` ends all the things started so far.
*/
/**
* A parser of snippet content.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public final class Parser {
// next-line tag behaves as if it were specified on the next line
private String eolMarker;
private Matcher markedUpLine;
private final Resources resources;
private final MarkupParser markupParser;
// Incomplete actions waiting for their complementary @end
private final Regions regions = new Regions();
private final Queue<Tag> tags = new LinkedList<>();
public Parser(Resources resources) {
this.resources = resources;
this.markupParser = new MarkupParser(resources);
}
public Result parse(String source) throws ParseException {
return parse("//", source);
}
/*
* Newline characters in the returned text are of the \n form.
*/
public Result parse(String eolMarker, String source) throws ParseException {
Objects.requireNonNull(eolMarker);
Objects.requireNonNull(source);
if (!Objects.equals(eolMarker, this.eolMarker)) {
if (eolMarker.length() < 1) {
throw new IllegalArgumentException();
}
for (int i = 0; i < eolMarker.length(); i++) {
switch (eolMarker.charAt(i)) {
case '\f', '\n', '\r' -> throw new IllegalArgumentException();
}
}
this.eolMarker = eolMarker;
// capture the rightmost eolMarker (e.g. "//")
// The below Pattern.compile should never throw PatternSyntaxException
Pattern pattern = Pattern.compile("^(.*)(" + Pattern.quote(eolMarker)
+ "(\\s*@\\s*\\w+.+?))$");
this.markedUpLine = pattern.matcher(""); // reusable matcher
}
tags.clear();
regions.clear();
Queue<Action> actions = new LinkedList<>();
StyledText text = new StyledText();
boolean trailingNewline = source.endsWith("\r") || source.endsWith("\n");
int lineStart = 0;
List<Tag> previousLineTags = new ArrayList<>();
List<Tag> thisLineTags = new ArrayList<>();
List<Tag> tempList = new ArrayList<>();
// while lines could be computed lazily, it would yield more complex code
record OffsetAndLine(int offset, String line) { }
var offsetAndLines = new LinkedList<OffsetAndLine>();
forEachLine(source, (off, line) -> offsetAndLines.add(new OffsetAndLine(off, line)));
Iterator<OffsetAndLine> iterator = offsetAndLines.iterator();
while (iterator.hasNext()) {
// There are 3 cases:
// 1. The pattern that describes a marked-up line is not matched
// 2. While the pattern is matched, the markup is not recognized
// 3. Both the pattern is matched and the markup is recognized
OffsetAndLine next = iterator.next();
String rawLine = next.line();
boolean addLineTerminator = iterator.hasNext() || trailingNewline;
String line;
markedUpLine.reset(rawLine);
if (!markedUpLine.matches()) { // (1)
line = rawLine + (addLineTerminator ? "\n" : "");
} else {
String maybeMarkup = markedUpLine.group(3);
List<Tag> parsedTags = null;
try {
parsedTags = markupParser.parse(maybeMarkup);
} catch (ParseException e) {
// adjust index
throw new ParseException(e::getMessage, markedUpLine.start(3) + e.getPosition());
}
for (Tag t : parsedTags) {
t.lineSourceOffset = next.offset;
t.markupLineOffset = markedUpLine.start(3);
}
thisLineTags.addAll(parsedTags);
for (var tagIterator = thisLineTags.iterator(); tagIterator.hasNext(); ) {
Tag t = tagIterator.next();
if (t.appliesToNextLine) {
tagIterator.remove();
t.appliesToNextLine = false; // clear the flag
tempList.add(t);
}
}
if (parsedTags.isEmpty()) { // (2)
// TODO: log this with NOTICE;
line = rawLine + (addLineTerminator ? "\n" : "");
} else { // (3)
String payload = markedUpLine.group(1);
line = payload + (addLineTerminator ? "\n" : "");
}
}
thisLineTags.addAll(0, previousLineTags); // prepend!
previousLineTags.clear();
for (Tag t : thisLineTags) {
t.start = lineStart;
t.end = lineStart + line.length(); // this includes line terminator, if any
processTag(t);
}
previousLineTags.addAll(tempList);
tempList.clear();
thisLineTags.clear();
append(text, Set.of(), line);
// TODO: mark up trailing whitespace!
lineStart += line.length();
}
if (!previousLineTags.isEmpty()) {
Tag t = previousLineTags.iterator().next();
String message = resources.getText("doclet.snippet.markup.tag.non.existent.lines");
throw new ParseException(() -> message, t.lineSourceOffset
+ t.markupLineOffset + t.nameLineOffset);
}
for (var t : tags) {
// Translate a list of attributes into a more convenient form
Attributes attributes = new Attributes(t.attributes());
final var substring = attributes.get("substring", Attribute.Valued.class);
final var regex = attributes.get("regex", Attribute.Valued.class);
if (!t.name().equals("start") && substring.isPresent() && regex.isPresent()) {
throw newParseException(t.lineSourceOffset + t.markupLineOffset
+ substring.get().nameStartPosition(),
"doclet.snippet.markup.attribute.simultaneous.use",
"substring", "regex");
}
switch (t.name()) {
case "link" -> {
var target = attributes.get("target", Attribute.Valued.class)
.orElseThrow(() -> newParseException(t.lineSourceOffset
+ t.markupLineOffset + t.nameLineOffset,
"doclet.snippet.markup.attribute.absent", "target"));
// "type" is what HTML calls an enumerated attribute
var type = attributes.get("type", Attribute.Valued.class);
String typeValue = type.isPresent() ? type.get().value() : "link";
if (!typeValue.equals("link") && !typeValue.equals("linkplain")) {
throw newParseException(t.lineSourceOffset + t.markupLineOffset
+ type.get().valueStartPosition(),
"doclet.snippet.markup.attribute.value.invalid", typeValue);
}
AddStyle a = new AddStyle(new Style.Link(target.value()),
// the default regex is different so as not to include newline
createRegexPattern(substring, regex, ".+",
t.lineSourceOffset + t.markupLineOffset),
text.subText(t.start(), t.end()));
actions.add(a);
}
case "replace" -> {
var replacement = attributes.get("replacement", Attribute.Valued.class)
.orElseThrow(() -> newParseException(t.lineSourceOffset
+ t.markupLineOffset + t.nameLineOffset,
"doclet.snippet.markup.attribute.absent", "replacement"));
Replace a = new Replace(replacement.value(),
createRegexPattern(substring, regex,
t.lineSourceOffset + t.markupLineOffset),
text.subText(t.start(), t.end()));
actions.add(a);
}
case "highlight" -> {
var type = attributes.get("type", Attribute.Valued.class);
String typeValue = type.isPresent() ? type.get().value() : "bold";
AddStyle a = new AddStyle(new Style.Name(typeValue),
createRegexPattern(substring, regex,
t.lineSourceOffset + t.markupLineOffset),
text.subText(t.start(), t.end()));
actions.add(a);
}
case "start" -> {
var region = attributes.get("region", Attribute.Valued.class)
.orElseThrow(() -> newParseException(t.lineSourceOffset
+ t.markupLineOffset + t.nameLineOffset,
"doclet.snippet.markup.attribute.absent", "region"));
String regionValue = region.value();
if (regionValue.isBlank()) {
throw newParseException(t.lineSourceOffset + t.markupLineOffset
+ region.valueStartPosition(), "doclet.snippet.markup.attribute.value.invalid");
}
for (Attribute a : t.attributes) {
if (!a.name().equals("region")) {
throw newParseException(t.lineSourceOffset +
t.markupLineOffset + a.nameStartPosition(),
"doclet.snippet.markup.attribute.unexpected");
}
}
actions.add(new Bookmark(region.value(), text.subText(t.start(), t.end() - 1)));
}
}
}
// also report on unpaired with corresponding `end` or unknown tags
if (!regions.isEmpty()) {
Optional<Tag> tag = regions.removeLast(); // any of these tags would do
Tag t = tag.get();
String message = resources.getText("doclet.snippet.markup.region.unpaired");
throw new ParseException(() -> message, t.lineSourceOffset
+ t.markupLineOffset + t.nameLineOffset);
}
return new Result(text, actions);
}
private ParseException newParseException(int pos, String key, Object... args) {
String message = resources.getText(key, args);
return new ParseException(() -> message, pos);
}
private Pattern createRegexPattern(Optional<Attribute.Valued> substring,
Optional<Attribute.Valued> regex,
int offset) throws ParseException {
return createRegexPattern(substring, regex, "(?s).+", offset);
}
private Pattern createRegexPattern(Optional<Attribute.Valued> substring,
Optional<Attribute.Valued> regex,
String defaultRegex,
int offset) throws ParseException {
Pattern pattern;
if (substring.isPresent()) {
// this Pattern.compile *cannot* throw an exception
pattern = Pattern.compile(Pattern.quote(substring.get().value()));
} else if (regex.isEmpty()) {
// this Pattern.compile *should not* throw an exception
pattern = Pattern.compile(defaultRegex);
} else {
final String value = regex.get().value();
try {
pattern = Pattern.compile(value);
} catch (PatternSyntaxException e) {
// Unlike string literals in Java source, attribute values in
// snippet markup do not use escape sequences. This is why
// indices of characters in the regex pattern directly map to
// their corresponding positions in snippet source. Refine
// position using e.getIndex() only if that index is relevant to
// the regex in the attribute value. Index might be irrelevant
// because it refers to an internal representation of regex,
// e.getPattern(), which might be a normalized or partial view
// of the original pattern.
int pos = offset + regex.get().valueStartPosition();
if (e.getIndex() > -1 && value.equals(e.getPattern())) {
pos += e.getIndex();
}
// getLocalized cannot be used because it provides a localized
// version of getMessage(), which in the case of this particular
// exception is multi-line with the caret. If we used that,
// it would duplicate the information we're trying to provide.
String message = resources.getText("doclet.snippet.markup.regex.invalid");
throw new ParseException(() -> message, pos);
}
}
return pattern;
}
private void processTag(Tag t) throws ParseException {
Attributes attributes = new Attributes(t.attributes()); // TODO: avoid creating attributes twice
Optional<Attribute> region = attributes.get("region", Attribute.class);
if (!t.name().equals("end")) {
tags.add(t);
if (region.isPresent()) {
if (region.get() instanceof Attribute.Valued v) {
String name = v.value();
if (!regions.addNamed(name, t)) {
throw newParseException(t.lineSourceOffset + t.markupLineOffset
+ v.valueStartPosition(), "doclet.snippet.markup.region.duplicated", name);
}
} else {
// TODO: change to exhaustive switch after "Pattern Matching for switch" is implemented
assert region.get() instanceof Attribute.Valueless;
regions.addAnonymous(t);
}
}
} else {
if (region.isEmpty() || region.get() instanceof Attribute.Valueless) {
Optional<Tag> tag = regions.removeLast();
if (tag.isEmpty()) {
throw newParseException(t.lineSourceOffset + t.markupLineOffset
+ t.nameLineOffset, "doclet.snippet.markup.region.none");
}
completeTag(tag.get(), t);
} else {
assert region.get() instanceof Attribute.Valued;
String name = ((Attribute.Valued) region.get()).value();
Optional<Tag> tag = regions.removeNamed(name);
if (tag.isEmpty()) {
throw newParseException(t.lineSourceOffset + t.markupLineOffset
+ region.get().nameStartPosition(), "doclet.snippet.markup.region.unpaired", name);
}
completeTag(tag.get(), t);
}
}
}
static final class Tag {
String name;
int lineSourceOffset;
int markupLineOffset;
int nameLineOffset;
int start;
int end;
List<Attribute> attributes;
boolean appliesToNextLine;
String name() {
return name;
}
List<Attribute> attributes() {
return attributes;
}
int start() {
return start;
}
int end() {
return end;
}
@Override
public String toString() {
return "Tag{" +
"name='" + name + '\'' +
", start=" + start +
", end=" + end +
", attributes=" + attributes +
'}';
}
}
private void completeTag(Tag start, Tag end) {
assert !start.name().equals("end") : start;
assert end.name().equals("end") : end;
start.end = end.end();
}
private void append(StyledText text, Set<Style> style, CharSequence s) {
text.subText(text.length(), text.length()).replace(style, s.toString());
}
public record Result(StyledText text, Queue<Action> actions) { }
/*
* Encapsulates the data structure used to manage regions.
*
* boolean-returning commands return true if succeed and false if fail.
*/
public static final class Regions {
/*
* LinkedHashMap does not fit here because of both the need for unique
* keys for anonymous regions and inability to easily access the most
* recently put entry.
*
* Since we expect only a few regions, a list will do.
*/
private final ArrayList<Map.Entry<Optional<String>, Tag>> tags = new ArrayList<>();
void addAnonymous(Tag i) {
tags.add(Map.entry(Optional.empty(), i));
}
boolean addNamed(String name, Tag i) {
boolean matches = tags.stream()
.anyMatch(entry -> entry.getKey().isPresent() && entry.getKey().get().equals(name));
if (matches) {
return false; // won't add a duplicate
}
tags.add(Map.entry(Optional.of(name), i));
return true;
}
Optional<Tag> removeNamed(String name) {
for (var iterator = tags.iterator(); iterator.hasNext(); ) {
var entry = iterator.next();
if (entry.getKey().isPresent() && entry.getKey().get().equals(name)) {
iterator.remove();
return Optional.of(entry.getValue());
}
}
return Optional.empty();
}
Optional<Tag> removeLast() {
if (tags.isEmpty()) {
return Optional.empty();
}
Map.Entry<Optional<String>, Tag> e = tags.remove(tags.size() - 1);
return Optional.of(e.getValue());
}
void clear() {
tags.clear();
}
boolean isEmpty() {
return tags.isEmpty();
}
}
/*
* The reason that the lines are split using a custom method as opposed to
* String.split(String) or String.lines() is that along with the lines
* themselves we also need their offsets in the originating input to supply
* to diagnostic exceptions should they arise.
*
* The reason that "\n|(\r\n)|\r" is used instead of "\\R" is that the
* latter is UNICODE-aware, which we must be not.
*/
static void forEachLine(String s, LineConsumer consumer) {
// the fact that the regex alternation is *ordered* is used here to try
// to match \r\n before \r
final Pattern NEWLINE = Pattern.compile("\n|(\r\n)|\r");
Matcher matcher = NEWLINE.matcher(s);
int pos = 0;
while (matcher.find()) {
consumer.accept(pos, s.substring(pos, matcher.start()));
pos = matcher.end();
}
if (pos < s.length())
consumer.accept(pos, s.substring(pos));
}
/*
* This interface is introduced to encapsulate the matching mechanics so
* that it wouldn't be obtrusive to the client code.
*/
@FunctionalInterface
interface LineConsumer {
void accept(int offset, String line);
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.util.ArrayList;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An action that replaces characters in text.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public final class Replace implements Action {
private final Pattern pattern;
private final String replacement;
private final StyledText text;
/**
* Constructs an action that replaces regex finds in text.
*
* @param replacement the replacement string
* @param pattern the regex used to search the text
* @param text the text
*/
public Replace(String replacement, Pattern pattern, StyledText text) {
this.replacement = replacement;
this.pattern = pattern;
this.text = text;
}
@Override
public void perform() {
record Replacement(int start, int end, String value) { }
// until JDK-8261619 is resolved, translating replacements requires some
// amount of waste and careful index manipulation
String textString = text.asCharSequence().toString();
Matcher matcher = pattern.matcher(textString);
var replacements = new ArrayList<Replacement>();
StringBuilder b = new StringBuilder();
int off = 0; // offset because of the replacements (can be negative)
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
// replacements are computed as they may have special symbols
matcher.appendReplacement(b, replacement);
String s = b.substring(start + off);
off = b.length() - end;
replacements.add(new Replacement(start, end, s));
}
// there's no need to call matcher.appendTail(b)
for (int i = replacements.size() - 1; i >= 0; i--) {
Replacement r = replacements.get(i);
text.subText(r.start, r.end).replace(Set.of(), r.value);
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
/**
* A style of a snippet text character.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
// TODO: uncomment /* sealed */ when minimum boot JDK version >= 17
public /* sealed */ interface Style {
/**
* A style that describes a link. Characters of this style are typically
* processed by wrapping into an HTML {@code A} element pointing to the
* provided target.
*/
record Link(String target) implements Style { }
/**
* A named style. Characters of this style are typically processed by
* wrapping into an HTML {@code SPAN} element with the {@code class}
* attribute which is obtained from the provided name.
*/
record Name(String name) implements Style { }
/**
* A marker of belonging to markup. Characters of this style are typically
* processed by omitting from the output.
*/
record Markup() implements Style { }
}

View File

@ -0,0 +1,343 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.toolkit.taglets.snippet;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* A mutable sequence of individually styleable characters.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class StyledText {
private Map<String, StyledText> bookmarks;
private StringBuilder chars;
private Styles styles;
private List<WeakReference<SubText>> subtexts;
public StyledText() {
init();
}
/*
* This method should be overridden to be no-op by a subclass that wants to
* inherit the interface but not the implementation, which includes
* unnecessary internal objects. If this is done, then all public methods
* should be overridden too, otherwise they will not work.
*
* An alternative design would be to provide an interface for styled text;
* but I ruled that out as unnecessarily heavyweight.
*/
protected void init() {
this.bookmarks = new HashMap<>();
this.chars = new StringBuilder();
this.styles = new Styles();
this.subtexts = new ArrayList<>();
}
/*
* For each character of this text adds the provided objects to a set of
* objects associated with that character.
*/
public void addStyle(Set<? extends Style> additionalStyles) {
styles.add(0, length(), additionalStyles);
}
public int length() {
return chars.length();
}
/*
* Replaces all characters of this text with the provided sequence of
* characters, each of which is associated with all the provided objects.
*/
public void replace(Set<? extends Style> styles, CharSequence plaintext) {
replace(0, length(), styles, plaintext);
}
/*
* A multi-purpose operation that can be used to replace, insert or delete
* text. The effect on a text is as if [start, end) were deleted and
* then plaintext inserted at start.
*/
private void replace(int start, int end, Set<? extends Style> styles, CharSequence plaintext) {
chars.replace(start, end, plaintext.toString());
this.styles.delete(start, end);
this.styles.insert(start, plaintext.length(), styles);
// The number of subtexts is not expected to be big; hence no
// optimizations are applied
var iterator = subtexts.iterator();
while (iterator.hasNext()) {
WeakReference<SubText> ref = iterator.next();
SubText txt = ref.get();
if (txt == null) {
iterator.remove(); // a stale ref
} else {
update(start, end, plaintext.length(), txt);
}
}
}
/*
* Updates the text given the scope of the change to reflect text continuity.
*/
private void update(int start, int end, int newLength, SubText text) {
assert start <= end;
assert text.start <= text.end;
assert newLength >= 0;
if (text.start == text.end && start == text.start) {
// insertion into empty text; special-cased for simplicity
text.end += newLength;
return;
}
if (end <= text.start) { // the change is on the left-hand side of the text
int diff = newLength - (end - start);
text.start += diff;
text.end += diff;
} else if (text.end <= start) { // the change is on the right-hand side of the text
// no-op; explicit "if" for clarity
} else { // the change intersects with the text
if (text.start <= start && end <= text.end) { // the change is within the text
text.end += newLength - (end - start);
} else {
int intersectionLen = min(end, text.end) - max(start, text.start);
int oldLen = text.end - text.start;
if (start <= text.start) {
text.start = start + newLength;
}
text.end = text.start + oldLen - intersectionLen;
}
}
}
private void addStyle(int start, int end, Set<? extends Style> additionalStyles) {
styles.add(start, end, additionalStyles);
}
public StyledText getBookmarkedText(String bookmark) {
return bookmarks.get(Objects.requireNonNull(bookmark));
}
/*
* Maps the provided name to this text, using a flat namespace. A flat
* namespace means that this text (t), as well as any subtext derived from
* either t or t's subtext, share the naming map.
*/
public void bookmark(String name) {
bookmark(name, 0, length());
}
private void bookmark(String name, int start, int end) {
bookmarks.put(Objects.requireNonNull(name), subText(start, end));
}
/*
* Selects a view of the portion of this text starting from start
* (inclusive) to end (exclusive).
*
* In contrast with java.util.List.subList, returned views provide extra
* consistency: they reflect structural changes happening to the underlying
* text and other views thereof.
*/
public StyledText subText(int start, int end) {
Objects.checkFromToIndex(start, end, length());
var s = new SubText(start, end);
subtexts.add(new WeakReference<>(s));
return s;
}
/*
* Returns plaintext version of this text. This method is to be used for
* algorithms that accept String or CharSequence to map the result back to
* this text.
*
* There are no extensible "mutable string" interface. java.lang.Appendable
* does not support replacements and insertions. StringBuilder/StringBuffer
* is not extensible. Even if it were extensible, not many general-purpose
* string algorithms accept it.
*/
public CharSequence asCharSequence() {
return chars;
}
/*
* Provides text to the consumer efficiently. The text always calls the
* consumer at least once; even if the text is empty.
*/
public void consumeBy(StyledText.Consumer consumer) {
consumeBy(consumer, 0, length());
}
private void consumeBy(StyledText.Consumer consumer, int start, int end) {
Objects.checkFromToIndex(start, end, length());
styles.consumeBy(consumer, chars, start, end);
}
public StyledText append(Set<? extends Style> styles, CharSequence sequence) {
subText(length(), length()).replace(styles, sequence);
return this;
}
public StyledText append(StyledText fragment) {
fragment.consumeBy((style, sequence) -> subText(length(), length()).replace(style, sequence));
return this;
}
@FunctionalInterface
public interface Consumer {
void consume(Set<? extends Style> style, CharSequence sequence);
}
/*
* A structure that stores character styles.
*/
private static final class Styles {
// Although this structure optimizes neither memory use nor object
// allocation, it is simple both to implement and reason about.
// list is a reference to ArrayList because this class accesses list by
// index, so this is important that the list is RandomAccess, which
// ArrayList is
private final ArrayList<Set<Style>> list = new ArrayList<>();
private void delete(int fromIndex, int toIndex) {
list.subList(fromIndex, toIndex).clear();
}
private void insert(int fromIndex, int length, Set<? extends Style> s) {
list.addAll(fromIndex, Collections.nCopies(length, Set.copyOf(s)));
}
private void add(int fromIndex, int toIndex, Set<? extends Style> additional) {
Set<Style> copyOfAdditional = Set.copyOf(additional);
list.subList(fromIndex, toIndex).replaceAll(current -> sum(current, copyOfAdditional));
}
private Set<Style> sum(Set<? extends Style> a, Set<Style> b) {
// assumption: until there are complex texts, the most common
// scenario is the one where `a` is empty while `b` is not
if (a.isEmpty()) {
return b;
} else {
Set<Style> c = new HashSet<>(a);
c.addAll(b);
return Set.copyOf(c);
}
}
private void consumeBy(StyledText.Consumer consumer, CharSequence seq, int start, int end) {
if (start == end) {
// an empty region doesn't have an associated set; special-cased
// for simplicity to avoid more complicated implementation of
// this method using a do-while loop
consumer.consume(Set.of(), "");
} else {
for (int i = start, j = i + 1; i < end; i = j) {
var ith = list.get(i);
while (j < end && ith.equals(list.get(j))) {
j++;
}
consumer.consume(ith, seq.subSequence(i, j));
}
}
}
}
final class SubText extends StyledText {
int start, end;
private SubText(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void init() {
// no-op
}
@Override
public void addStyle(Set<? extends Style> additionalStyles) {
StyledText.this.addStyle(start, end, additionalStyles);
}
@Override
public int length() {
return end - start;
}
@Override
public void replace(Set<? extends Style> styles, CharSequence plaintext) {
// If the "replace" operation affects this text's size, which it
// can, then that size will be updated along with all other sizes
// during the bulk "update" operation in tracking text instance.
StyledText.this.replace(start, end, styles, plaintext);
}
@Override
public StyledText getBookmarkedText(String bookmark) {
return StyledText.this.getBookmarkedText(bookmark);
}
@Override
public void bookmark(String name) {
StyledText.this.bookmark(name, start, end);
}
@Override
public StyledText subText(int start, int end) {
return StyledText.this.subText(this.start + start, this.start + end);
}
@Override
public CharSequence asCharSequence() {
return StyledText.this.asCharSequence().subSequence(start, end);
}
@Override
public void consumeBy(StyledText.Consumer consumer) {
StyledText.this.consumeBy(consumer, start, end);
}
}
}

View File

@ -340,8 +340,12 @@ public class CommentHelper {
}
public TypeElement getReferencedClass(DocTree dtree) {
Utils utils = configuration.utils;
Element e = getReferencedElement(dtree);
return getReferencedClass(e);
}
public TypeElement getReferencedClass(Element e) {
Utils utils = configuration.utils;
if (e == null) {
return null;
} else if (utils.isTypeElement(e)) {
@ -354,16 +358,24 @@ public class CommentHelper {
public String getReferencedModuleName(DocTree dtree) {
String s = getReferencedSignature(dtree);
if (s == null || s.contains("#") || s.contains("(")) {
return getReferencedModuleName(s);
}
public String getReferencedModuleName(String signature) {
if (signature == null || signature.contains("#") || signature.contains("(")) {
return null;
}
int n = s.indexOf("/");
return (n == -1) ? s : s.substring(0, n);
int n = signature.indexOf("/");
return (n == -1) ? signature : signature.substring(0, n);
}
public Element getReferencedMember(DocTree dtree) {
Utils utils = configuration.utils;
Element e = getReferencedElement(dtree);
return getReferencedMember(e);
}
public Element getReferencedMember(Element e) {
Utils utils = configuration.utils;
if (e == null) {
return null;
}
@ -372,15 +384,23 @@ public class CommentHelper {
public String getReferencedMemberName(DocTree dtree) {
String s = getReferencedSignature(dtree);
if (s == null) {
return getReferencedMemberName(s);
}
public String getReferencedMemberName(String signature) {
if (signature == null) {
return null;
}
int n = s.indexOf("#");
return (n == -1) ? null : s.substring(n + 1);
int n = signature.indexOf("#");
return (n == -1) ? null : signature.substring(n + 1);
}
public PackageElement getReferencedPackage(DocTree dtree) {
Element e = getReferencedElement(dtree);
return getReferencedPackage(e);
}
public PackageElement getReferencedPackage(Element e) {
if (e != null) {
Utils utils = configuration.utils;
return utils.containingPackage(e);
@ -390,13 +410,16 @@ public class CommentHelper {
public ModuleElement getReferencedModule(DocTree dtree) {
Element e = getReferencedElement(dtree);
return getReferencedModule(e);
}
public ModuleElement getReferencedModule(Element e) {
if (e != null && configuration.utils.isModule(e)) {
return (ModuleElement) e;
}
return null;
}
public List<? extends DocTree> getFirstSentenceTrees(List<? extends DocTree> body) {
return configuration.docEnv.getDocTrees().getFirstSentence(body);
}

View File

@ -629,6 +629,12 @@ public class Checker extends DocTreePathScanner<Void, Void> {
@Override @DefinedBy(Api.COMPILER_TREE) @SuppressWarnings("fallthrough")
public Void visitAttribute(AttributeTree tree, Void ignore) {
// for now, ensure we're in an HTML StartElementTree;
// in time, we might check uses of attributes in other tree nodes
if (getParentKind() != DocTree.Kind.START_ELEMENT) {
return null;
}
HtmlTag currTag = tagStack.peek().tag;
if (currTag != null && currTag.elemKind != ElemKind.HTML4) {
Name name = tree.getName();
@ -1156,6 +1162,10 @@ public class Checker extends DocTreePathScanner<Void, Void> {
// <editor-fold defaultstate="collapsed" desc="Utility methods">
private DocTree.Kind getParentKind() {
return getCurrentPath().getParentPath().getLeaf().getKind();
}
private boolean isCheckedException(TypeMirror t) {
return !(env.types.isAssignable(t, env.java_lang_Error)
|| env.types.isAssignable(t, env.java_lang_RuntimeException));

View File

@ -140,6 +140,9 @@ public class CheckStylesheetClasses {
"ui-autocomplete", "ui-autocomplete-category",
"watermark");
// snippet-related
removeAll(styleSheetNames, "bold", "highlighted", "italic");
// very JDK specific
styleSheetNames.remove("module-graph");

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
@serialData: block ........ ...... ....... .... ........... ...... ..... ...... ........
@serialField: block ........ ...... ....... .... ........... ...... field ...... ........
@since: block overview module package type constructor method field ...... ........
{@snippet}: ..... overview module package type constructor method field inline ........
{@summary}: ..... overview module package type constructor method field inline ........
{@systemProperty}: ..... ........ module package type constructor method field inline ........
@throws: block ........ ...... ....... .... constructor method ..... ...... ........

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -62,7 +62,7 @@ public class DocumentationToolLocationTest extends APITest {
@Test
public void testEnumMethods() throws Exception {
DocumentationTool.Location[] values = DocumentationTool.Location.values();
if (values.length != 3)
if (values.length != 4)
throw new Exception("unexpected number of values returned");
for (DocumentationTool.Location dl: values) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -105,6 +105,13 @@ public class EmptyHtmlTest extends TestRunner {
case "ReturnTree" ->
test(d, type, "{@return abc}");
case "SnippetTree" ->
test(d, type, """
{@snippet :
abc
}
""");
case "SummaryTree" ->
test(d, type, "{@summary First sentence.}");

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -587,6 +587,18 @@ public class DocCommentTester {
return null;
}
@Override
public Void visitSnippet(SnippetTree node, Void p) {
header(node);
indent(+1);
print("attributes", node.getAttributes());
print("body", node.getBody());
indent(-1);
indent();
out.println("]");
return null;
}
public Void visitStartElement(StartElementTree node, Void p) {
header(node);
indent(+1);

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8266666
* @summary Implementation for snippets
* @modules jdk.compiler/com.sun.tools.javac.api
* jdk.compiler/com.sun.tools.javac.file
* jdk.compiler/com.sun.tools.javac.tree
* jdk.compiler/com.sun.tools.javac.util
* @build DocCommentTester
* @run main DocCommentTester SnippetTest.java
*/
class SnippetTest {
/**
* {@snippet attr1="val1" :
* Hello, Snippet!
* }
*/
void inline() { }
/*
DocComment[DOC_COMMENT, pos:1
firstSentence: 1
Snippet[SNIPPET, pos:1
attributes: 1
Attribute[ATTRIBUTE, pos:11
name: attr1
vkind: DOUBLE
value: 1
Text[TEXT, pos:18, val1]
]
body:
Text[TEXT, pos:26, _____Hello,_Snippet!|_]
]
body: empty
block tags: empty
]
*/
}