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:
parent
6d91a3eb7b
commit
0fc47e99d2
@ -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(); }
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
* {@snippet :
|
||||
* body
|
||||
* }
|
||||
*
|
||||
* {@snippet attributes}
|
||||
*
|
||||
* {@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();
|
||||
}
|
@ -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
|
||||
|
@ -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}
|
||||
*
|
||||
|
@ -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}
|
||||
*
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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">
|
||||
|
@ -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=\
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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%;
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 { }
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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");
|
||||
|
||||
|
2380
test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java
Normal file
2380
test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 ..... ...... ........
|
||||
|
@ -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) {
|
||||
|
@ -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.}");
|
||||
|
||||
|
@ -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);
|
||||
|
61
test/langtools/tools/javac/doctree/SnippetTest.java
Normal file
61
test/langtools/tools/javac/doctree/SnippetTest.java
Normal 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
|
||||
]
|
||||
*/
|
||||
}
|
Loading…
Reference in New Issue
Block a user