-
Notifications
You must be signed in to change notification settings - Fork 5.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
8298405: Implement JEP 467: Markdown Documentation Comments #16388
Changes from 1 commit
128363b
ea9c161
ec7445d
843cc27
39cdd2b
9f46a9e
50bb105
a192f10
30eb1d1
4d22a10
4dd2da4
5e3f03c
50c3a40
f5175a8
0da7ba5
71ce4b1
586dadd
66bd836
56347b2
30f447f
54d40b2
b02d467
f086aaa
203532b
2a20c74
7c63168
5710c28
63dd8bf
92b5e7a
5fc415b
c891fe9
cb07045
3b1350b
91588bc
d22668d
3f8aa6b
6907266
184343f
9eaf84e
1c64a6e
9745cc5
396512d
937035e
2eb7c63
2801c2e
f6d08db
dd4059a
393d3a9
94776a8
eda4036
da8752c
2d905d3
53afd80
25921fd
8616ee1
5fc9c84
7242041
27bc0b9
4061686
d460ee3
398f93f
0b4d9b3
8e64dc2
292ff0f
0c497c9
fc76e8c
8dfd9e0
81279a7
76eccbf
635af77
7d4810d
03652d2
5e08eac
21d9ac5
477fb15
a8ea1c7
3764628
21f5b00
d4c2c73
fbcb4c3
c4709cf
a884ae3
fadc130
74c86f5
20a384b
6576d02
fe94ab8
e42632a
9ad5c39
e48ebb5
cc12140
f466bad
4354610
bfa35bd
a0df7a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,7 @@ | |
import javax.lang.model.element.AnnotationMirror; | ||
import javax.lang.model.element.AnnotationValue; | ||
import javax.lang.model.element.Element; | ||
import javax.lang.model.element.ElementKind; | ||
import javax.lang.model.element.ExecutableElement; | ||
import javax.lang.model.element.ModuleElement; | ||
import javax.lang.model.element.Name; | ||
|
@@ -82,9 +83,17 @@ | |
|
||
import jdk.internal.org.commonmark.Extension; | ||
import jdk.internal.org.commonmark.ext.gfm.tables.TablesExtension; | ||
import jdk.internal.org.commonmark.node.AbstractVisitor; | ||
import jdk.internal.org.commonmark.node.Code; | ||
import jdk.internal.org.commonmark.node.Heading; | ||
import jdk.internal.org.commonmark.node.Node; | ||
import jdk.internal.org.commonmark.parser.Parser; | ||
import jdk.internal.org.commonmark.renderer.NodeRenderer; | ||
import jdk.internal.org.commonmark.renderer.html.HtmlNodeRendererContext; | ||
import jdk.internal.org.commonmark.renderer.html.HtmlNodeRendererFactory; | ||
import jdk.internal.org.commonmark.renderer.html.HtmlRenderer; | ||
import jdk.internal.org.commonmark.renderer.html.HtmlWriter; | ||
|
||
import jdk.javadoc.internal.doclets.formats.html.markup.ContentBuilder; | ||
import jdk.javadoc.internal.doclets.formats.html.markup.Entity; | ||
import jdk.javadoc.internal.doclets.formats.html.markup.Head; | ||
|
@@ -1341,7 +1350,7 @@ public ContentBuilder add(CharSequence text) { | |
commentRemoved = false; | ||
|
||
var useMarkdown = trees.stream().anyMatch(t -> t.getKind() == Kind.MARKDOWN); | ||
var markdownHandler = useMarkdown ? new MarkdownHandler() : null; | ||
var markdownHandler = useMarkdown ? new MarkdownHandler(element) : null; | ||
|
||
for (ListIterator<? extends DocTree> iterator = trees.listIterator(); iterator.hasNext(); ) { | ||
boolean isFirstNode = !iterator.hasPrevious(); | ||
|
@@ -1367,7 +1376,6 @@ public ContentBuilder add(CharSequence text) { | |
} | ||
|
||
var docTreeVisitor = new InlineVisitor(element, tag, isLastNode, context, ch, trees); | ||
|
||
boolean allDone = useMarkdown | ||
? markdownHandler.handle(tag, docTreeVisitor) | ||
: docTreeVisitor.visit(tag, result); | ||
|
@@ -1386,20 +1394,26 @@ public ContentBuilder add(CharSequence text) { | |
|
||
private class MarkdownHandler { | ||
private static final char PLACEHOLDER = '\uFFFC'; // Unicode Object Replacement Character | ||
StringBuilder markdownInput = new StringBuilder() ; | ||
ArrayList<Content> fffcObjects = new ArrayList<>(); | ||
private final StringBuilder markdownInput = new StringBuilder() ; | ||
private final ArrayList<Content> fffcObjects = new ArrayList<>(); | ||
|
||
List<Extension> extns = List.of( | ||
TablesExtension.create() | ||
); | ||
private final Extension tablesExtn = TablesExtension.create(); | ||
private final HtmlNodeRendererFactory headingRendererFactory = HeadingNodeRenderer::new; | ||
|
||
Parser parser = Parser.builder() | ||
.extensions(extns) | ||
private final Element element; | ||
|
||
private final Parser parser = Parser.builder() | ||
.extensions(List.of(tablesExtn)) | ||
.build(); | ||
HtmlRenderer renderer = HtmlRenderer.builder() | ||
.extensions(extns) | ||
private final HtmlRenderer renderer = HtmlRenderer.builder() | ||
.nodeRendererFactory(headingRendererFactory) | ||
.extensions(List.of(tablesExtn/*, headingIdsExtn*/)) | ||
jonathan-gibbons marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.build(); | ||
|
||
MarkdownHandler(Element element) { | ||
this.element = element; | ||
} | ||
|
||
boolean handle(DocTree tree, InlineVisitor visitor) { | ||
boolean allDone; | ||
if (tree instanceof RawTextTree t) { | ||
|
@@ -1464,6 +1478,80 @@ private static String unwrap(String s) { | |
} | ||
return s; | ||
} | ||
|
||
/** | ||
* A renderer for Markdown {@code Heading} nodes, which represent | ||
* both ATX headings (using {@code ####}) and Setext (using underlines). | ||
* The mapping to HTML takes into account the context within the overall | ||
* generated page, and automatically includes an id, to allow the heading | ||
* to be referenced from elsewhere. | ||
*/ | ||
private class HeadingNodeRenderer extends AbstractVisitor implements NodeRenderer { | ||
private final HtmlWriter htmlWriter; | ||
private final HtmlNodeRendererContext context; | ||
|
||
HeadingNodeRenderer(HtmlNodeRendererContext context) { | ||
this.htmlWriter = context.getWriter(); | ||
this.context = context; | ||
} | ||
|
||
@Override | ||
public Set<Class<? extends Node>> getNodeTypes() { | ||
return Set.of(Heading.class); | ||
} | ||
|
||
@Override | ||
public void render(Node node) { | ||
node.accept(this); | ||
} | ||
|
||
@Override | ||
public void visit(Heading heading) { | ||
var htag = getTag(heading); | ||
var id = getId(heading); | ||
|
||
htmlWriter.line(); | ||
htmlWriter.tag(htag, Map.of("id", id.name())); | ||
visitChildren(heading); | ||
htmlWriter.tag('/' + htag); | ||
htmlWriter.line(); | ||
} | ||
|
||
@Override | ||
protected void visitChildren(Node parent) { | ||
Node node = parent.getFirstChild(); | ||
while (node != null) { | ||
Node next = node.getNext(); | ||
context.render(node); | ||
node = next; | ||
} | ||
} | ||
|
||
private String getTag(Heading heading) { | ||
// offset the heading level to allow for its position in the overall page | ||
var eKind = element.getKind(); | ||
var offset = eKind.isField() || eKind.isExecutable() ? 3 // members | ||
: eKind != ElementKind.OTHER ? 1 // module, package, class, interface | ||
: 0; // doc file | ||
return "h" + Math.min(heading.getLevel() + offset, 6); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like that we adapt the heading level to the current context, but I notice that the code kind of expects There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setext headings only come in "level 1" and "level 2" flavors. I also think it is better to have a simple rule than an "adaptive" rule. If you start doing a more complex rule, you have to consider the effect on subheadings as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspected it was about the limited range of Setext headings. Yesterday my proposal was intentionally vague, but thinking about this again I think we should actually do the simplest and least intrusive thing possible:
This arguably generates the correct heading for most simple use cases. What it doesn't do is to translate whole hierarchies of headings, but I would argue that few people people need this and those who do should figure out the rules and use the correct ATX headings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally, I disagree with the policy here. In particular, this suggestion "squashes"/"merges" heading levels at the more significant end of the range (i.e. h1, h2) and not at the least significant end of the range (i.e. h5, h6). And, while we do specify the required heading levels in "traditional" doc comments, that seems less than optimal. (I note in times past we had to modify the headings in doc comments as a result of the policy). Given all that, it seems better/simpler to specify that an author should start the headings in any comment at level 1 and have the tool adjust the level as needed. |
||
} | ||
|
||
private HtmlId getId(Heading heading) { | ||
var list = new ArrayList<String>(); | ||
heading.accept(new AbstractVisitor() { | ||
@Override | ||
public void visit(jdk.internal.org.commonmark.node.Text text) { | ||
list.add(text.getLiteral()); | ||
} | ||
|
||
@Override | ||
public void visit(Code code) { | ||
list.add(code.getLiteral()); | ||
} | ||
}); | ||
return htmlIds.forHeading(String.join(" ", list), headingIds); | ||
} | ||
} | ||
} | ||
|
||
/* | ||
|
@@ -1706,6 +1794,8 @@ private void createSectionIdAndIndex(StartElementTree node, List<? extends DocTr | |
for (DocTree docTree : trees.subList(trees.indexOf(node) + 1, trees.size())) { | ||
if (docTree instanceof TextTree text) { | ||
sb.append(text.getBody()); | ||
} else if (docTree instanceof RawTextTree raw) { | ||
sb.append(raw.getContent().replaceAll("[^A-Za-z0-9]+", " ")); | ||
} else if (docTree instanceof LiteralTree literal) { | ||
sb.append(literal.getBody().getBody()); | ||
} else if (docTree instanceof LinkTree link) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
MarkdownHandler
andHeadingNodeRenderer
classes must become aware of the currentTagletWriter.Context
. That's because headings and other block tags should only be generated ifTagletWriter.Context.isFirstSentence
isfalse
.