8288699: cleanup HTML tree in HtmlDocletWriter.commentTagsToContent

Reviewed-by: hannesw
This commit is contained in:
Jonathan Gibbons 2022-07-08 19:33:03 +00:00
parent 6aaf141f61
commit 54b4576f78
18 changed files with 348 additions and 173 deletions

View File

@ -877,6 +877,32 @@ public class DocCommentParser {
} }
nextChar(); nextChar();
} }
} else {
String CDATA = "[CDATA["; // full prefix is <![CDATA[
for (int i = 0; i < CDATA.length(); i++) {
if (ch == CDATA.charAt(i)) {
nextChar();
} else {
return erroneous("dc.invalid.html", p);
}
}
// suffix is ]]>
while (bp < buflen) {
if (ch == ']') {
int n = 0;
while (bp < buflen && ch == ']') {
n++;
nextChar();
}
if (n >= 2 && ch == '>') {
nextChar();
return m.at(p).newTextTree(newString(p, bp));
}
} else {
nextChar();
}
}
return erroneous("dc.invalid.html", p);
} }
} }

View File

@ -3339,6 +3339,9 @@ compiler.err.dc.bad.inline.tag=\
compiler.err.dc.identifier.expected=\ compiler.err.dc.identifier.expected=\
identifier expected identifier expected
compiler.err.dc.invalid.html=\
invalid HTML
compiler.err.dc.malformed.html=\ compiler.err.dc.malformed.html=\
malformed HTML malformed HTML

View File

@ -125,7 +125,7 @@ public abstract class AbstractOverviewIndexWriter extends HtmlDocletWriter {
protected void addConfigurationTitle(Content target) { protected void addConfigurationTitle(Content target) {
String doctitle = configuration.getOptions().docTitle(); String doctitle = configuration.getOptions().docTitle();
if (!doctitle.isEmpty()) { if (!doctitle.isEmpty()) {
var title = new RawHtml(doctitle); var title = RawHtml.of(doctitle);
var heading = HtmlTree.HEADING(Headings.PAGE_TITLE_HEADING, var heading = HtmlTree.HEADING(Headings.PAGE_TITLE_HEADING,
HtmlStyle.title, title); HtmlStyle.title, title);
var div = HtmlTree.DIV(HtmlStyle.header, heading); var div = HtmlTree.DIV(HtmlStyle.header, heading);

View File

@ -500,7 +500,7 @@ public class HtmlDocletWriter {
*/ */
protected HtmlTree getHeader(Navigation.PageMode pageMode, Element element) { protected HtmlTree getHeader(Navigation.PageMode pageMode, Element element) {
return HtmlTree.HEADER() return HtmlTree.HEADER()
.add(new RawHtml(replaceDocRootDir(options.top()))) .add(RawHtml.of(replaceDocRootDir(options.top())))
.add(getNavBar(pageMode, element).getContent()); .add(getNavBar(pageMode, element).getContent());
} }
@ -515,7 +515,7 @@ public class HtmlDocletWriter {
*/ */
protected Navigation getNavBar(Navigation.PageMode pageMode, Element element) { protected Navigation getNavBar(Navigation.PageMode pageMode, Element element) {
return new Navigation(element, configuration, pageMode, path) return new Navigation(element, configuration, pageMode, path)
.setUserHeader(new RawHtml(replaceDocRootDir(options.header()))); .setUserHeader(RawHtml.of(replaceDocRootDir(options.header())));
} }
/** /**
@ -532,7 +532,7 @@ public class HtmlDocletWriter {
.add(new HtmlTree(TagName.HR)) .add(new HtmlTree(TagName.HR))
.add(HtmlTree.P(HtmlStyle.legalCopy, .add(HtmlTree.P(HtmlStyle.legalCopy,
HtmlTree.SMALL( HtmlTree.SMALL(
new RawHtml(replaceDocRootDir(bottom))))); RawHtml.of(replaceDocRootDir(bottom)))));
} }
/** /**
@ -1004,7 +1004,7 @@ public class HtmlDocletWriter {
} }
case START_ELEMENT -> { case START_ELEMENT -> {
// @see <a href="...">...</a> // @see <a href="...">...</a>
return new RawHtml(replaceDocRootDir(removeTrailingSlash(seeText))); return RawHtml.of(replaceDocRootDir(removeTrailingSlash(seeText)));
} }
case REFERENCE -> { case REFERENCE -> {
// @see reference label... // @see reference label...
@ -1484,7 +1484,6 @@ public class HtmlDocletWriter {
} }
}; };
CommentHelper ch = utils.getCommentHelper(element); CommentHelper ch = utils.getCommentHelper(element);
// Array of all possible inline tags for this javadoc run
configuration.tagletManager.checkTags(element, trees, true); configuration.tagletManager.checkTags(element, trees, true);
commentRemoved = false; commentRemoved = false;
@ -1511,103 +1510,108 @@ public class HtmlDocletWriter {
} }
} }
boolean allDone = new SimpleDocTreeVisitor<Boolean, Content>() { var docTreeVisitor = new SimpleDocTreeVisitor<Boolean, Content>() {
private boolean inAnAtag() { private boolean inAnAtag() {
if (utils.isStartElement(tag)) { return (tag instanceof StartElementTree st) && equalsIgnoreCase(st.getName(), "a");
StartElementTree st = (StartElementTree)tag; }
Name name = st.getName();
if (name != null) { private boolean equalsIgnoreCase(Name name, String s) {
HtmlTag htag = HtmlTag.get(name); return name != null && name.toString().equalsIgnoreCase(s);
return htag != null && htag.equals(HtmlTag.A);
}
}
return false;
} }
@Override @Override
public Boolean visitAttribute(AttributeTree node, Content c) { public Boolean visitAttribute(AttributeTree node, Content content) {
StringBuilder sb = new StringBuilder(SPACER).append(node.getName().toString()); if (!content.isEmpty()) {
content.add(" ");
}
content.add(node.getName());
if (node.getValueKind() == ValueKind.EMPTY) { if (node.getValueKind() == ValueKind.EMPTY) {
result.add(sb);
return false; return false;
} }
sb.append("="); content.add("=");
String quote = switch (node.getValueKind()) { String quote = switch (node.getValueKind()) {
case DOUBLE -> "\""; case DOUBLE -> "\"";
case SINGLE -> "'"; case SINGLE -> "'";
default -> ""; default -> "";
}; };
sb.append(quote); content.add(quote);
result.add(sb);
Content docRootContent = new ContentBuilder();
boolean isHRef = inAnAtag() && node.getName().toString().equalsIgnoreCase("href"); /* In the following code for an attribute value:
* 1. {@docRoot} followed by text beginning "/.." is replaced by the value
* of the docrootParent option, followed by the remainder of the text
* 2. in the value of an "href" attribute in a <a> tag, an initial text
* value will have a relative link redirected.
* Note that, realistically, it only makes sense to ever use {@docRoot}
* at the beginning of a URL in an attribute value, but this is not
* required or enforced.
*/
boolean isHRef = inAnAtag() && equalsIgnoreCase(node.getName(), "href");
boolean first = true;
DocRootTree pendingDocRoot = null;
for (DocTree dt : node.getValue()) { for (DocTree dt : node.getValue()) {
if (utils.isText(dt) && isHRef) { if (pendingDocRoot != null) {
String text = ((TextTree) dt).getBody(); if (dt instanceof TextTree tt) {
if (text.startsWith("/..") && !options.docrootParent().isEmpty()) { String text = tt.getBody();
result.add(options.docrootParent()); if (text.startsWith("/..") && !options.docrootParent().isEmpty()) {
docRootContent = new ContentBuilder(); content.add(options.docrootParent());
result.add(textCleanup(text.substring(3), isLastNode)); content.add(textCleanup(text.substring(3), isLastNode));
} else { pendingDocRoot = null;
if (!docRootContent.isEmpty()) { continue;
docRootContent = copyDocRootContent(docRootContent);
} else {
text = redirectRelativeLinks(element, (TextTree) dt);
} }
result.add(textCleanup(text, isLastNode));
} }
} else { pendingDocRoot.accept(this, content);
docRootContent = copyDocRootContent(docRootContent); pendingDocRoot = null;
dt.accept(this, docRootContent);
} }
if (dt instanceof TextTree tt) {
String text = tt.getBody();
if (first && isHRef) {
text = redirectRelativeLinks(element, tt);
}
content.add(textCleanup(text, isLastNode));
} else if (dt instanceof DocRootTree drt) {
// defer until we see what, if anything, follows this node
pendingDocRoot = drt;
} else {
dt.accept(this, content);
}
first = false;
} }
copyDocRootContent(docRootContent); if (pendingDocRoot != null) {
result.add(quote); pendingDocRoot.accept(this, content);
return false;
}
@Override
public Boolean visitComment(CommentTree node, Content c) {
result.add(new RawHtml(node.getBody()));
return false;
}
private Content copyDocRootContent(Content content) {
if (!content.isEmpty()) {
result.add(content);
return new ContentBuilder();
} }
return content;
}
@Override content.add(quote);
public Boolean visitDocRoot(DocRootTree node, Content c) {
Content docRootContent = getInlineTagOutput(element, node, context);
if (c != null) {
c.add(docRootContent);
} else {
result.add(docRootContent);
}
return false; return false;
} }
@Override @Override
public Boolean visitEndElement(EndElementTree node, Content c) { public Boolean visitComment(CommentTree node, Content content) {
RawHtml rawHtml = new RawHtml("</" + node.getName() + ">"); content.add(RawHtml.comment(node.getBody()));
result.add(rawHtml);
return false; return false;
} }
@Override @Override
public Boolean visitEntity(EntityTree node, Content c) { public Boolean visitDocRoot(DocRootTree node, Content content) {
result.add(new RawHtml(node.toString())); content.add(getInlineTagOutput(element, node, context));
return false; return false;
} }
@Override @Override
public Boolean visitErroneous(ErroneousTree node, Content c) { public Boolean visitEndElement(EndElementTree node, Content content) {
content.add(RawHtml.endElement(node.getName()));
return false;
}
@Override
public Boolean visitEntity(EntityTree node, Content content) {
content.add(Entity.of(node.getName()));
return false;
}
@Override
public Boolean visitErroneous(ErroneousTree node, Content content) {
DocTreePath dtp = ch.getDocTreePath(node); DocTreePath dtp = ch.getDocTreePath(node);
if (dtp != null) { if (dtp != null) {
String body = node.getBody(); String body = node.getBody();
@ -1617,11 +1621,11 @@ public class HtmlDocletWriter {
if (!configuration.isDocLintSyntaxGroupEnabled()) { if (!configuration.isDocLintSyntaxGroupEnabled()) {
messages.warning(dtp, "doclet.tag.invalid_input", body); messages.warning(dtp, "doclet.tag.invalid_input", body);
} }
result.add(invalidTagOutput(resources.getText("doclet.tag.invalid_input", body), content.add(invalidTagOutput(resources.getText("doclet.tag.invalid_input", body),
Optional.empty())); Optional.empty()));
} else { } else {
messages.warning(dtp, "doclet.tag.invalid_usage", body); messages.warning(dtp, "doclet.tag.invalid_usage", body);
result.add(invalidTagOutput(resources.getText("doclet.tag.invalid", tagName), content.add(invalidTagOutput(resources.getText("doclet.tag.invalid", tagName),
Optional.of(Text.of(body)))); Optional.of(Text.of(body))));
} }
} }
@ -1629,24 +1633,24 @@ public class HtmlDocletWriter {
} }
@Override @Override
public Boolean visitInheritDoc(InheritDocTree node, Content c) { public Boolean visitInheritDoc(InheritDocTree node, Content content) {
Content output = getInlineTagOutput(element, node, context); Content output = getInlineTagOutput(element, node, context);
result.add(output); content.add(output);
// if we obtained the first sentence successfully, nothing more to do // if we obtained the first sentence successfully, nothing more to do
return (context.isFirstSentence && !output.isEmpty()); return (context.isFirstSentence && !output.isEmpty());
} }
@Override @Override
public Boolean visitIndex(IndexTree node, Content p) { public Boolean visitIndex(IndexTree node, Content content) {
Content output = getInlineTagOutput(element, node, context); Content output = getInlineTagOutput(element, node, context);
if (output != null) { if (output != null) {
result.add(output); content.add(output);
} }
return false; return false;
} }
@Override @Override
public Boolean visitLink(LinkTree node, Content c) { public Boolean visitLink(LinkTree node, Content content) {
var inTags = context.inTags; var inTags = context.inTags;
if (inTags.contains(LINK) || inTags.contains(LINK_PLAIN) || inTags.contains(SEE)) { if (inTags.contains(LINK) || inTags.contains(LINK_PLAIN) || inTags.contains(SEE)) {
DocTreePath dtp = ch.getDocTreePath(node); DocTreePath dtp = ch.getDocTreePath(node);
@ -1657,55 +1661,50 @@ public class HtmlDocletWriter {
if (label.isEmpty()) { if (label.isEmpty()) {
label = Text.of(node.getReference().getSignature()); label = Text.of(node.getReference().getSignature());
} }
result.add(label); content.add(label);
} else { } else {
Content content = seeTagToContent(element, node, context.within(node)); Content c = seeTagToContent(element, node, context.within(node));
result.add(content); content.add(c);
} }
return false; return false;
} }
@Override @Override
public Boolean visitLiteral(LiteralTree node, Content c) { public Boolean visitLiteral(LiteralTree node, Content content) {
String s = node.getBody().getBody(); String s = node.getBody().getBody();
Content content = Text.of(utils.normalizeNewlines(s)); Content t = Text.of(utils.normalizeNewlines(s));
if (node.getKind() == CODE) content.add(node.getKind() == CODE ? HtmlTree.CODE(t) : t);
content = HtmlTree.CODE(content);
result.add(content);
return false; return false;
} }
@Override @Override
public Boolean visitSee(SeeTree node, Content c) { public Boolean visitSee(SeeTree node, Content content) {
result.add(seeTagToContent(element, node, context)); content.add(seeTagToContent(element, node, context));
return false; return false;
} }
@Override @Override
public Boolean visitStartElement(StartElementTree node, Content c) { public Boolean visitStartElement(StartElementTree node, Content content) {
String text = "<" + node.getName(); Content attrs = new ContentBuilder();
RawHtml rawHtml = new RawHtml(utils.normalizeNewlines(text));
result.add(rawHtml);
for (DocTree dt : node.getAttributes()) { for (DocTree dt : node.getAttributes()) {
dt.accept(this, null); dt.accept(this, attrs);
} }
result.add(new RawHtml(node.isSelfClosing() ? "/>" : ">")); content.add(RawHtml.startElement(node.getName(), attrs, node.isSelfClosing()));
return false; return false;
} }
@Override @Override
public Boolean visitSummary(SummaryTree node, Content c) { public Boolean visitSummary(SummaryTree node, Content content) {
Content output = getInlineTagOutput(element, node, context); Content output = getInlineTagOutput(element, node, context);
result.add(output); content.add(output);
return false; return false;
} }
@Override @Override
public Boolean visitSystemProperty(SystemPropertyTree node, Content p) { public Boolean visitSystemProperty(SystemPropertyTree node, Content content) {
Content output = getInlineTagOutput(element, node, context); Content output = getInlineTagOutput(element, node, context);
if (output != null) { if (output != null) {
result.add(output); content.add(output);
} }
return false; return false;
} }
@ -1728,23 +1727,28 @@ public class HtmlDocletWriter {
} }
@Override @Override
public Boolean visitText(TextTree node, Content c) { public Boolean visitText(TextTree node, Content content) {
String text = node.getBody(); String text = node.getBody();
result.add(new RawHtml(textCleanup(text, isLastNode, commentRemoved))); result.add(text.startsWith("<![CDATA[")
? RawHtml.cdata(text)
: Text.of(textCleanup(text, isLastNode, commentRemoved)));
return false; return false;
} }
@Override @Override
protected Boolean defaultAction(DocTree node, Content c) { protected Boolean defaultAction(DocTree node, Content content) {
Content output = getInlineTagOutput(element, node, context); Content output = getInlineTagOutput(element, node, context);
if (output != null) { if (output != null) {
result.add(output); content.add(output);
} }
return false; return false;
} }
}.visit(tag, null); };
boolean allDone = docTreeVisitor.visit(tag, result);
commentRemoved = false; commentRemoved = false;
if (allDone) if (allDone)
break; break;
} }
@ -1897,15 +1901,6 @@ public class HtmlDocletWriter {
return text; return text;
} }
/**
* According to
* <cite>The Java Language Specification</cite>,
* all the outer classes and static nested classes are core classes.
*/
public boolean isCoreClass(TypeElement typeElement) {
return utils.getEnclosingTypeElement(typeElement) == null || utils.isStatic(typeElement);
}
/** /**
* {@return the annotation types info for the given element} * {@return the annotation types info for the given element}
* *
@ -2166,6 +2161,7 @@ public class HtmlDocletWriter {
} }
}.visit(t); }.visit(t);
} }
@Override @Override
public Content visitAnnotation(AnnotationMirror a, Void p) { public Content visitAnnotation(AnnotationMirror a, Void p) {
List<Content> list = getAnnotations(List.of(a), false); List<Content> list = getAnnotations(List.of(a), false);
@ -2175,10 +2171,12 @@ public class HtmlDocletWriter {
} }
return buf; return buf;
} }
@Override @Override
public Content visitEnumConstant(VariableElement c, Void p) { public Content visitEnumConstant(VariableElement c, Void p) {
return getDocLink(HtmlLinkInfo.Kind.ANNOTATION, c, c.getSimpleName()); return getDocLink(HtmlLinkInfo.Kind.ANNOTATION, c, c.getSimpleName());
} }
@Override @Override
public Content visitArray(List<? extends AnnotationValue> vals, Void p) { public Content visitArray(List<? extends AnnotationValue> vals, Void p) {
ContentBuilder buf = new ContentBuilder(); ContentBuilder buf = new ContentBuilder();
@ -2190,6 +2188,7 @@ public class HtmlDocletWriter {
} }
return buf; return buf;
} }
@Override @Override
protected Content defaultAction(Object o, Void p) { protected Content defaultAction(Object o, Void p) {
return Text.of(annotationValue.toString()); return Text.of(annotationValue.toString());
@ -2274,10 +2273,6 @@ public class HtmlDocletWriter {
return HtmlStyle.valueOf(page); return HtmlStyle.valueOf(page);
} }
Script getMainBodyScript() {
return mainBodyScript;
}
/** /**
* Returns the path of module/package specific stylesheets for the element. * Returns the path of module/package specific stylesheets for the element.
* @param element module/Package element * @param element module/Package element

View File

@ -134,7 +134,7 @@ public class HtmlSerialFieldWriter extends FieldWriterImpl
CommentHelper ch = utils.getCommentHelper(field); CommentHelper ch = utils.getCommentHelper(field);
List<? extends DocTree> description = ch.getDescription(serialFieldTag); List<? extends DocTree> description = ch.getDescription(serialFieldTag);
if (!description.isEmpty()) { if (!description.isEmpty()) {
Content serialFieldContent = new RawHtml(ch.getText(description)); Content serialFieldContent = RawHtml.of(ch.getText(description)); // should interpret tags
var div = HtmlTree.DIV(HtmlStyle.block, serialFieldContent); var div = HtmlTree.DIV(HtmlStyle.block, serialFieldContent);
content.add(div); content.add(div);
} }

View File

@ -530,7 +530,7 @@ public class Signatures {
*/ */
private int appendTypeParameters(Content target, int lastLineSeparator) { private int appendTypeParameters(Content target, int lastLineSeparator) {
// Apply different wrapping strategies for type parameters // Apply different wrapping strategies for type parameters
// depending of combined length of type parameters and return type. // depending on the combined length of type parameters and return type.
int typeParamLength = typeParameters.charCount(); int typeParamLength = typeParameters.charCount();
if (typeParamLength >= TYPE_PARAMS_MAX_INLINE_LENGTH) { if (typeParamLength >= TYPE_PARAMS_MAX_INLINE_LENGTH) {

View File

@ -52,6 +52,7 @@ import com.sun.source.doctree.ReturnTree;
import com.sun.source.doctree.SeeTree; import com.sun.source.doctree.SeeTree;
import com.sun.source.doctree.SnippetTree; import com.sun.source.doctree.SnippetTree;
import com.sun.source.doctree.SystemPropertyTree; import com.sun.source.doctree.SystemPropertyTree;
import com.sun.source.doctree.TextTree;
import com.sun.source.doctree.ThrowsTree; import com.sun.source.doctree.ThrowsTree;
import com.sun.source.util.DocTreePath; import com.sun.source.util.DocTreePath;
import jdk.javadoc.internal.doclets.formats.html.markup.ContentBuilder; import jdk.javadoc.internal.doclets.formats.html.markup.ContentBuilder;
@ -307,7 +308,7 @@ public class TagletWriterImpl extends TagletWriter {
public Content returnTagOutput(Element element, ReturnTree returnTag, boolean inline) { public Content returnTagOutput(Element element, ReturnTree returnTag, boolean inline) {
CommentHelper ch = utils.getCommentHelper(element); CommentHelper ch = utils.getCommentHelper(element);
List<? extends DocTree> desc = ch.getDescription(returnTag); List<? extends DocTree> desc = ch.getDescription(returnTag);
Content content = htmlWriter.commentTagsToContent(element, desc , context.within(returnTag)); Content content = htmlWriter.commentTagsToContent(element, desc, context.within(returnTag));
return inline return inline
? new ContentBuilder(contents.getContent("doclet.Returns_0", content)) ? new ContentBuilder(contents.getContent("doclet.Returns_0", content))
: new ContentBuilder(HtmlTree.DT(contents.returns), HtmlTree.DD(content)); : new ContentBuilder(HtmlTree.DT(contents.returns), HtmlTree.DD(content));
@ -371,7 +372,7 @@ public class TagletWriterImpl extends TagletWriter {
many = true; many = true;
} }
return new ContentBuilder( return new ContentBuilder(
HtmlTree.DT(new RawHtml(header)), HtmlTree.DT(RawHtml.of(header)),
HtmlTree.DD(body)); HtmlTree.DD(body));
} }
@ -504,9 +505,9 @@ public class TagletWriterImpl extends TagletWriter {
excName = htmlWriter.getLink(new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER, excName = htmlWriter.getLink(new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER,
substituteType)); substituteType));
} else if (exception == null) { } else if (exception == null) {
excName = new RawHtml(throwsTag.getExceptionName().toString()); excName = RawHtml.of(throwsTag.getExceptionName().toString());
} else if (exception.asType() == null) { } else if (exception.asType() == null) {
excName = new RawHtml(utils.getFullyQualifiedName(exception)); excName = Text.of(utils.getFullyQualifiedName(exception));
} else { } else {
HtmlLinkInfo link = new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER, HtmlLinkInfo link = new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER,
exception.asType()); exception.asType());

View File

@ -41,6 +41,16 @@ public class Entity extends Content {
public final String text; public final String text;
/**
* Creates an entity with a given name or numeric value.
*
* @param name the name, or numeric value
* @return the entity
*/
public static Entity of(CharSequence name) {
return new Entity("&" + name + ";");
}
private Entity(String text) { private Entity(String text) {
this.text = text; this.text = text;
} }

View File

@ -36,14 +36,91 @@ import jdk.javadoc.internal.doclets.toolkit.util.DocletConstants;
*/ */
public class RawHtml extends Content { public class RawHtml extends Content {
private final String rawHtmlContent; protected final String rawHtmlContent;
/**
* Creates HTML for an arbitrary string of HTML.
* The string is accepted as-is and is not validated in any way.
* It should be syntactically well-formed and contain matching {@code <} and {@code >},
* and matching quotes for attributes.
*
* @param rawHtml the string
* @return the HTML
*/
public static RawHtml of(CharSequence rawHtml) {
return new RawHtml(rawHtml) {
@Override
public int charCount() {
return charCount(rawHtmlContent);
}
};
}
/**
* Creates HTML for the start of an element.
*
* @param name the name of the element
* @param attrs content containing any attributes
* @param selfClosing whether this is a self-closing element.
* @return the HTML
*/
public static RawHtml startElement(CharSequence name, Content attrs, boolean selfClosing) {
StringBuilder sb = new StringBuilder("<" + name);
if (!attrs.isEmpty()) {
sb.append(" ");
sb.append(attrs);
}
sb.append(selfClosing ? "/>" : ">");
return new RawHtml(sb);
}
/**
* Creates HTML for the end of an element.
*
* @param name the name of the element
* @return the HTML
*/
public static RawHtml endElement(CharSequence name) {
return new RawHtml("</" + name + ">");
}
/**
* Creates HTML for an HTML comment.
*
* The body will be enclosed in {@code <!--} and {@code -->} if it does not
* already begin and end with those sequences.
*
* @param body the body of the comment
*
* @return the HTML
*/
public static RawHtml comment(String body) {
return section("<!--", body, "-->");
}
/**
* Creates HTML for an HTML CDATA section.
*
* The body will be enclosed in {@code <![CDATA]} and {@code ]]>} if it does not
* already begin and end with those sequences.
*
* @param body the body of the CDATA section
*
* @return the HTML
*/
public static RawHtml cdata(String body) {
return section("<![CDATA[", body, "]]>");
}
private static RawHtml section(String prefix, String body, String suffix) {
return new RawHtml(body.startsWith(prefix) && body.endsWith(suffix) ? body : prefix + body + suffix);
}
/** /**
* Constructor to construct a RawHtml object. * Constructor to construct a RawHtml object.
* *
* @param rawHtml raw HTML text to be added * @param rawHtml raw HTML text to be added
*/ */
public RawHtml(CharSequence rawHtml) { private RawHtml(CharSequence rawHtml) {
rawHtmlContent = rawHtml.toString(); rawHtmlContent = rawHtml.toString();
} }
@ -59,12 +136,7 @@ public class RawHtml extends Content {
private enum State { TEXT, ENTITY, TAG, STRING } private enum State { TEXT, ENTITY, TAG, STRING }
@Override protected static int charCount(CharSequence htmlText) {
public int charCount() {
return charCount(rawHtmlContent);
}
static int charCount(CharSequence htmlText) {
State state = State.TEXT; State state = State.TEXT;
int count = 0; int count = 0;
for (int i = 0; i < htmlText.length(); i++) { for (int i = 0; i < htmlText.length(); i++) {
@ -80,9 +152,12 @@ public class RawHtml extends Content {
count++; count++;
break; break;
case '\r': case '\r':
case '\n':
// Windows uses "\r\n" as line separator while UNIX uses "\n". // Windows uses "\r\n" as line separator while UNIX uses "\n".
// Ignore line separators to get consistent results across platforms. // Skip the "\r" to get consistent results across platforms.
if (i + 1 < htmlText.length() && htmlText.charAt(i + 1) == '\n') {
i++;
}
count++;
break; break;
default: default:
count++; count++;

View File

@ -33,6 +33,7 @@ import jdk.javadoc.internal.doclets.toolkit.util.DocletConstants;
/** /**
* Class for containing immutable string content for HTML tags of javadoc output. * Class for containing immutable string content for HTML tags of javadoc output.
* Any special HTML characters will be escaped if and when the content is written out.
*/ */
public class Text extends Content { public class Text extends Content {
private final String string; private final String string;
@ -55,7 +56,7 @@ public class Text extends Content {
* @param content content for the object * @param content content for the object
*/ */
private Text(CharSequence content) { private Text(CharSequence content) {
string = Entity.escapeHtmlChars(content); string = content.toString();
} }
@Override @Override
@ -65,7 +66,20 @@ public class Text extends Content {
@Override @Override
public int charCount() { public int charCount() {
return RawHtml.charCount(string); return charCount(string);
}
static int charCount(CharSequence cs) {
int count = 0;
for (int i = 0; i < cs.length(); i++) {
// Windows uses "\r\n" as line separator while UNIX uses "\n".
// Skip the "\r" to get consistent results across platforms.
if (cs.charAt(i) == '\r' && (i + 1 < cs.length()) && cs.charAt(i + 1) == '\n') {
i++;
}
count++;
}
return count;
} }
@Override @Override
@ -75,7 +89,7 @@ public class Text extends Content {
@Override @Override
public boolean write(Writer out, boolean atNewline) throws IOException { public boolean write(Writer out, boolean atNewline) throws IOException {
out.write(string); out.write(Entity.escapeHtmlChars(string));
return string.endsWith(DocletConstants.NL); return string.endsWith(DocletConstants.NL);
} }

View File

@ -34,6 +34,7 @@ import jdk.javadoc.internal.doclets.toolkit.util.DocletConstants;
/** /**
* Class for generating string content for HTML tags of javadoc output. * Class for generating string content for HTML tags of javadoc output.
* The content is mutable to the extent that additional content may be added. * The content is mutable to the extent that additional content may be added.
* Any special HTML characters will be escaped if and when the content is written out.
*/ */
public class TextBuilder extends Content { public class TextBuilder extends Content {
@ -52,19 +53,17 @@ public class TextBuilder extends Content {
* @param initialContent initial content for the object * @param initialContent initial content for the object
*/ */
public TextBuilder(CharSequence initialContent) { public TextBuilder(CharSequence initialContent) {
stringBuilder = new StringBuilder(); stringBuilder = new StringBuilder(initialContent);
Entity.escapeHtmlChars(initialContent, stringBuilder);
} }
/** /**
* Adds content for the StringContent object. The method escapes * Adds content for the StringContent object.
* HTML characters for the string content that is added.
* *
* @param strContent string content to be added * @param strContent string content to be added
*/ */
@Override @Override
public TextBuilder add(CharSequence strContent) { public TextBuilder add(CharSequence strContent) {
Entity.escapeHtmlChars(strContent, stringBuilder); stringBuilder.append(strContent);
return this; return this;
} }
@ -75,7 +74,7 @@ public class TextBuilder extends Content {
@Override @Override
public int charCount() { public int charCount() {
return RawHtml.charCount(stringBuilder.toString()); return Text.charCount(stringBuilder);
} }
@Override @Override
@ -85,7 +84,7 @@ public class TextBuilder extends Content {
@Override @Override
public boolean write(Writer out, boolean atNewline) throws IOException { public boolean write(Writer out, boolean atNewline) throws IOException {
String s = stringBuilder.toString(); String s = Entity.escapeHtmlChars(stringBuilder);
out.write(s); out.write(s);
return s.endsWith(DocletConstants.NL); return s.endsWith(DocletConstants.NL);
} }

View File

@ -128,6 +128,40 @@ public class CommentUtils {
return treeFactory.newTextTree(resources.getText(key)); return treeFactory.newTextTree(resources.getText(key));
} }
/**
* Parses a string, looking for simple embedded HTML.
* @param s the string
* @return the list of parsed {@code DocTree} nodes
*/
private List<DocTree> parse(String s) {
List<DocTree> list = null;
Pattern p = Pattern.compile("(?i)<(/)?([a-z0-9]+)(/)?>");
Matcher m = p.matcher(s);
int start = 0;
while (m.find()) {
if (list == null) {
list = new ArrayList<>();
}
if (m.start() > 0) {
list.add(treeFactory.newTextTree(s.substring(start, m.start())));
}
Name name = elementUtils.getName(m.group(2));
list.add(m.group(1) == null
? treeFactory.newStartElementTree(name, List.of(), m.group(3) != null)
: treeFactory.newEndElementTree(name));
start = m.end();
}
if (list == null) {
return List.of(treeFactory.newTextTree(s));
} else {
if (start < s.length()) {
list.add(treeFactory.newTextTree(s.substring(start, s.length())));
}
return list;
}
}
public void setEnumValuesTree(ExecutableElement ee) { public void setEnumValuesTree(ExecutableElement ee) {
List<DocTree> fullBody = new ArrayList<>(); List<DocTree> fullBody = new ArrayList<>();
fullBody.add(treeFactory.newTextTree(resources.getText("doclet.enum_values_doc.fullbody"))); fullBody.add(treeFactory.newTextTree(resources.getText("doclet.enum_values_doc.fullbody")));
@ -142,8 +176,7 @@ public class CommentUtils {
} }
public void setEnumValueOfTree(ExecutableElement ee) { public void setEnumValueOfTree(ExecutableElement ee) {
List<DocTree> fullBody = new ArrayList<>(); List<DocTree> fullBody = parse(resources.getText("doclet.enum_valueof_doc.fullbody"));
fullBody.add(treeFactory.newTextTree(resources.getText("doclet.enum_valueof_doc.fullbody")));
List<DocTree> tags = new ArrayList<>(); List<DocTree> tags = new ArrayList<>();
@ -242,15 +275,15 @@ public class CommentUtils {
int start = 0; int start = 0;
while (m.find(start)) { while (m.find(start)) {
if (m.start() > start) { if (m.start() > start) {
contents.add(treeFactory.newTextTree(body.substring(start, m.start()))); contents.addAll(parse(body.substring(start, m.start())));
} }
ReferenceTree refTree = treeFactory.newReferenceTree(m.group(1)); ReferenceTree refTree = treeFactory.newReferenceTree(m.group(1));
List<DocTree> descr = List.of(treeFactory.newTextTree(m.group(2).trim())) ; List<DocTree> descr = parse(m.group(2).trim());
contents.add(treeFactory.newLinkTree(refTree, descr)); contents.add(treeFactory.newLinkTree(refTree, descr));
start = m.end(); start = m.end();
} }
if (start < body.length()) { if (start < body.length()) {
contents.add(treeFactory.newTextTree(body.substring(start))); contents.addAll(parse(body.substring(start)));
} }
} }
@ -485,16 +518,16 @@ public class CommentUtils {
String text = resources.getText(key); String text = resources.getText(key);
int index = text.indexOf("{0}"); int index = text.indexOf("{0}");
if (index == -1) { if (index == -1) {
return List.of(treeFactory.newTextTree(text)); return parse(text);
} else { } else {
Name CODE = elementUtils.getName("code"); Name CODE = elementUtils.getName("code");
return List.of( var list = new ArrayList<DocTree>();
treeFactory.newTextTree(text.substring(0, index)), list.addAll(parse(text.substring(0, index)));
treeFactory.newStartElementTree(CODE, List.of(), false), list.add(treeFactory.newStartElementTree(CODE, List.of(), false));
treeFactory.newTextTree(name.toString()), list.add(treeFactory.newTextTree(name.toString())) ;
treeFactory.newEndElementTree(CODE), list.add(treeFactory.newEndElementTree(CODE));
treeFactory.newTextTree(text.substring(index + 3)) list.addAll(parse(text.substring(index + 3)));
); return list;
} }
} }

View File

@ -133,9 +133,7 @@ public abstract class Content {
} }
/** /**
* Return the number of characters of plain text content in this object * {@return the number of characters of plain text content in this object}
* (optional operation.)
* @return the number of characters of plain text content in this
*/ */
public int charCount() { public int charCount() {
return 0; return 0;

View File

@ -107,7 +107,7 @@ public final class UserTaglet implements Taglet {
@Override @Override
public Content getInlineTagOutput(Element element, DocTree tag, TagletWriter writer) { public Content getInlineTagOutput(Element element, DocTree tag, TagletWriter writer) {
Content output = writer.getOutputInstance(); Content output = writer.getOutputInstance();
output.add(new RawHtml(userTaglet.toString(List.of(tag), element))); output.add(RawHtml.of(userTaglet.toString(List.of(tag), element)));
return output; return output;
} }
@ -119,7 +119,7 @@ public final class UserTaglet implements Taglet {
if (!tags.isEmpty()) { if (!tags.isEmpty()) {
String tagString = userTaglet.toString(tags, holder); String tagString = userTaglet.toString(tags, holder);
if (tagString != null) { if (tagString != null) {
output.add(new RawHtml(tagString)); output.add(RawHtml.of(tagString));
} }
} }
return output; return output;

View File

@ -314,7 +314,7 @@ public class CommentHelper {
public String visitSee(SeeTree node, Void p) { public String visitSee(SeeTree node, Void p) {
Utils utils = configuration.utils; Utils utils = configuration.utils;
return node.getReference().stream() return node.getReference().stream()
.filter(utils::isText) .filter(dt -> dt.getKind() == DocTree.Kind.TEXT)
.map(dt -> ((TextTree) dt).getBody()) .map(dt -> ((TextTree) dt).getBody())
.collect(Collectors.joining()); .collect(Collectors.joining());
} }

View File

@ -2176,18 +2176,6 @@ public class Utils {
return mdle.getQualifiedName().toString(); return mdle.getQualifiedName().toString();
} }
public boolean isStartElement(DocTree doctree) {
return isKind(doctree, START_ELEMENT);
}
public boolean isText(DocTree doctree) {
return isKind(doctree, TEXT);
}
private boolean isKind(DocTree doctree, DocTree.Kind match) {
return doctree.getKind() == match;
}
private final CommentHelperCache commentHelperCache = new CommentHelperCache(this); private final CommentHelperCache commentHelperCache = new CommentHelperCache(this);
public CommentHelper getCommentHelper(Element element) { public CommentHelper getCommentHelper(Element element) {

View File

@ -44,6 +44,7 @@ public class TestTypeAnnotations extends JavadocTester {
@Test @Test
public void test() { public void test() {
javadoc("-d", "out", javadoc("-d", "out",
"-Xdoclint:none",
"--no-platform-links", "--no-platform-links",
"-sourcepath", testSrc, "-sourcepath", testSrc,
"-private", "-private",

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2012, 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.
*/
// key: compiler.err.dc.invalid.html
// key: compiler.note.note
// key: compiler.note.proc.messager
// run: backdoor
// options: -processor DocCommentProcessor -proc:only
/** <![CDATA[ */
class InvalidHtml { }