跳到主要内容

JEP 467:Markdown 文档注释

概括

允许以 Markdown 格式编写 JavaDoc 文档注释,而不是仅以 HTML 和 JavaDoc@标签的混合格式编写。

目标

  • 通过引入在文档注释中使用 Markdown 语法以及 HTML 元素和 JavaDoc 标记的功能,使 API 文档注释以源代码形式更易于编写和阅读。

  • 不要对现有文档注释的解释产生不利影响。

  • 扩展编译器树 API,以启用其他分析文档注释的工具来处理这些注释中的 Markdown 内容。

非目标

  • 实现现有文档注释自动转换为 Markdown 语法并不是我们的目标。

动机

文档注释是出现在源代码中的程式化注释,靠近它们用于记录的声明。 Java 源代码中的文档注释使用 HTML 和自定义 JavaDoc标记的组合来标记文本。

1995 年选择 HTML 作为标记语言是合理的。HTML 功能强大、标准化,并且在当时非常流行。但是,尽管如今作为 Web 浏览器使用的标记语言,HTML 的受欢迎程度丝毫不减,但自 1995 年以来,HTML 作为人类手动生成的标记已经变得不那么受欢迎了,因为它编写起来乏味且难以阅读。如今,它更常见的是从其他一些更适合人类的标记语言生成的。因为 HTML 写起来很乏味,所以格式良好的文档注释写起来也很乏味,甚至更乏味,因为许多新开发人员由于 HTML 作为人类生成的格式的衰落而不能流利地使用 HTML。

内联JavaDoc标签,例如{@link}{@code},也很麻烦,开发人员更不熟悉,常常需要作者查阅文档来了解它们的用法。最近对 JDK 源代码中的文档注释的分析表明,超过 95% 的内联标记用于代码片段和文档中其他位置的链接,这表明这些结构的更简单形式将受到欢迎。

Markdown是一种流行的简单文档标记语言,易于阅读、易于编写并且易于转换为 HTML。文档注释通常不是复杂的结构化文档,对于文档注释中通常出现的结构(例如段落、列表、样式文本和链接),Markdown 提供了比 HTML 更简单的形式。对于 Markdown 不直接支持的那些结构,Markdown 也允许使用 HTML。

引入在文档注释中使用 Markdown 的功能将结合两个世界的优点。它将允许最常见结构的简洁语法,并减少对 HTML 标记和 JavaDoc 标签的需求,同时保留使用专用标签来实现 Markdown 中不可用的功能的能力。它将使源代码中的文档注释更容易编写和阅读,同时保留生成与以前相同类型的生成 API 文档的能力。

描述

作为在文档注释中使用 Markdown 的示例,请考虑java.lang.Object.hashCode的注释:

/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@link
* #equals(Object) equals} method, then calling the {@code
* hashCode} method on each of the two objects must produce the
* same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link #equals(Object) equals} method, then
* calling the {@code hashCode} method on each of the two objects
* must produce distinct integer results. However, the programmer
* should be aware that producing distinct integer results for
* unequal objects may improve the performance of hash tables.
* </ul>
*
* @implSpec
* As far as is reasonably practical, the {@code hashCode} method defined
* by class {@code Object} returns distinct integers for distinct objects.
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/

可以通过在 Markdown 中表达其结构和样式来编写相同的注释,而不使用 HTML,仅使用一些 JavaDoc 内联标记:

/// Returns a hash code value for the object. This method is
/// supported for the benefit of hash tables such as those provided by
/// [java.util.HashMap].
///
/// The general contract of `hashCode` is:
///
/// - Whenever it is invoked on the same object more than once during
/// an execution of a Java application, the `hashCode` method
/// must consistently return the same integer, provided no information
/// used in `equals` comparisons on the object is modified.
/// This integer need not remain consistent from one execution of an
/// application to another execution of the same application.
/// - If two objects are equal according to the
/// [equals][#equals(Object)] method, then calling the
/// `hashCode` method on each of the two objects must produce the
/// same integer result.
/// - It is _not_ required that if two objects are unequal
/// according to the [equals][#equals(Object)] method, then
/// calling the `hashCode` method on each of the two objects
/// must produce distinct integer results. However, the programmer
/// should be aware that producing distinct integer results for
/// unequal objects may improve the performance of hash tables.
///
/// @implSpec
/// As far as is reasonably practical, the `hashCode` method defined
/// by class `Object` returns distinct integers for distinct objects.
///
/// @return a hash code value for this object.
/// @see java.lang.Object#equals(java.lang.Object)
/// @see java.lang.System#identityHashCode

(出于本示例的目的,故意避免了诸如重排文本之类的外观更改,以帮助进行前后比较。)

需要观察的主要差异:

  • Markdown 的使用是通过一种新形式的文档注释来表示的,其中每行都以 开头,///而不是传统的/** ... */语法。

  • HTML<p>元素不是必需的;空行表示段落分隔符。

  • HTML<ul><li>元素被 Markdown 项目符号列表标记替换,用于-指示列表中每个项目的开头。

  • HTML<em>元素被替换为使用下划线 ( _) 来指示字体更改。

  • 标记的实例{@code ...}由反引号 ( ) 替换`...`以指示等宽字体。

  • {@link ...}链接到其他程序元素的实例被 Markdown参考链接的扩展形式所取代。

  • 块标签的实例,例如@implSpec@return@see,通常不受影响,只是这些标签的内容现在也在 Markdown 中,例如这里在标签内容的反引号中@implSpec

下面的屏幕截图并排突出显示了两个版本之间的差异:

差异截图

用于///Markdown 文档注释

我们使用///Markdown 注释是为了克服传统/**注释的两个问题。

  • 以 开头的块注释/*不能包含字符序列*/( JLS §3.7 )。将代码示例放在文档注释中变得越来越普遍。此限制排除了包含嵌入/*...*/注释的示例或包含字符 的表达式*/,而无需使用破坏性的解决方法。

    //注释中,对该行其余部分可能出现的字符没有限制。

  • 在传统的文档注释中,从 开始,每行是否/**可以使用前导空格并后跟一个或多个星号。当注释行中省略此类星号时,本身以星号开头的 Markdown 结构就会产生歧义,例如强调、列表项和主题分隔。

    ///评论中,从来没有任何这样的歧义。

无法通过更改 Java 语言的语法来允许新的注释形式。因此,任何新样式的文档注释都必须采用传统的/* ... */块注释或一系列//行尾注释的形式。

上述几点证明了使用行尾注释而不是传统注释的合理性,但问题仍然是如何区分文档注释和其他行尾注释。我们使用additional ,这与传统文档注释开头的/additional 的使用相呼应。*此外,虽然不是主要考虑因素,但支持行尾文档注释的其他语言(例如C#DartRust)现在已经成功用于///文档注释一段时间了。

句法

Markdown 文档注释是用Markdown 的CommonMark变体编写的。链接的增强功能可以方便地链接到其他程序元素。支持简单的GFM 管道表,以及所有 JavaDoc 标记。

您可以使用 Markdown参考链接的扩展形式创建指向在 API 中其他地方声明的元素的链接,其中引用的标签源自对元素本身的标准 JavaDoc引用。

要创建一个其文本源自元素标识的简单链接,只需将对元素的引用括在方括号中即可。例如,要链接到java.util.List,您可以编写[java.util.List],或者只要代码中[List]importfor 语句即可。java.util.List链接的文本将以等宽字体显示。该链接相当于使用标准 JavaDoc{@link ...}标记。

您可以链接到任何类型的程序元素:

/// - a module [java.base/]
/// - a package [java.util]
/// - a class [String]
/// - a field [String#CASE_INSENSITIVE_ORDER]
/// - a method [String#chars()]

要创建带有替代文本的链接,请使用表单[text][element]。例如,要创建java.util.List指向文本 的链接a list,您可以编写[a list][List]。尽管您可以在文本中使用格式标记,但链接将以当前字体显示。该链接相当于使用标准 JavaDoc{@linkplain ...}标记。

例如:

/// - [the `java.base` module][java.base/]
/// - [the `java.util` package][java.util]
/// - [a class][String]
/// - [a field][String#CASE_INSENSITIVE_ORDER]
/// - [a method][String#chars()]

在参考链接中,您必须避免使用方括号。这可能发生在对带有数组参数的方法的引用中;例如,您可以编写一个指向String.copyValueOf(char[])as的链接[String#copyValueOf(char\[\])]

您可以使用所有其他形式的 Markdown 链接,包括 URL 链接,但其他程序元素的链接可能是最常见的。

表格

支持简单的表格,使用GitHub Flavored Markdown的语法。例如:

/// | Latin | Greek |
/// |-------|-------|
/// | a | alpha |
/// | b | beta |
/// | c | gamma |

不支持辅助功能可能需要的字幕和其他功能。在这种情况下,仍然建议使用 HTML 表格。

JavaDoc 标签

JavaDoc 标签,包括内联标签(如 ){@inheritDoc}块标签(如@param和 )@return,都可以在 Markdown 文档注释中使用:

/// {@inheritDoc}
/// In addition, this methods calls [#wait()].
///
/// @param i the index
public void m(int i) ...

JavaDoc 标签不得在文字文本中使用,例如代码跨度( `...`) 或代码块,即缩进或包含在栅栏(例如```或 )内的文本块~~~。换句话说,字符序列@...{@...}在代码范围和代码块中没有特殊含义:

/// The following code span contains literal text, and not a JavaDoc tag:
/// `{@inheritDoc}`
///
/// In the following indented code block, `@Override` is an annotation,
/// and not a JavaDoc tag:
///
/// @Override
/// public void m() ...
///
/// Likewise, in the following fenced code block, `@Override` is an annotation,
/// and not a JavaDoc tag:
///
/// ```
/// @Override
/// public void m() ...
/// ```

对于那些可能包含带有标记的文本的标签,在 Markdown 文档注释中,标记也在 Markdown 中:

/// @param l   the list, or `null` if no list is available

{@inheritDoc}标签包含一个或多个超类型的方法的文档。包含标签的注释的格式不需要与包含要继承的文档的注释的格式相同:

interface Base {
/** A method. */
void m()
}

class Derived implements Base {
/// {@inheritDoc}
public void m() { }
}

用户定义的 JavaDoc 标签可以在 Markdown 文档注释中使用。例如,在 JDK 文档中,我们定义并使用{@jls ...}Java 语言规范链接的缩写形式,以及诸如@implSpec和 之类的块标签@implNote来介绍特定信息的部分:

/// For more information on comments, see {@jls 3.7 Comments}.
///
/// @implSpec
/// This implementation does nothing.
public void doSomething() { }

独立 Markdown 文件

子目录中的 Markdown 文件doc-files会得到适当的处理,处理方式与此类目录中的 HTML 文件类似。处理此类文件中的 JavaDoc 标记。页面标题是从第一个标题推断出来的。不支持YAML 元数据,例如Pandoc Markdown 处理器支持的元数据。

包含生成的顶级概述页面的内容的文件也可以是 Markdown 文件。

语法突出显示和嵌入式语言

围栏代码块中的开放围栏后面可能跟着一个信息字符串。信息字符串的第一个单词用于派生相应生成的 HTML 中的 CSS 类名称,JavaScript 库也可以使用它来启用语法突出显示(例如使用Prism)和渲染图表(例如使用Mermaid)。

例如,结合适当的库,这将显示带有语法突出显示的 CSS 代码片段:

/// ```css
/// p { color: red }
/// ```

您可以使用该javadoc --add-script选项将 JavaScript 库添加到您的文档中。

语法细节

由于 Markdown 文本每行开头和结尾的水平空白可能很重要,因此 Markdown 文档注释的内容确定如下:

  • 任何前导空格和三个开头/字符都将从每行中删除。

  • 通过删除前导空白字符,将行向左移动,直到具有最少前导空白的非空白行没有剩余的前导空白。

  • 每行中的附加前导空格和任何尾随空格都会被保留,因为它们可能很重要。例如,行首的空格可能表示缩进的代码块或列表项的延续,行尾的空格可能表示硬换行符

(删除前导附带空格的策略与String.stripIndent()的策略类似,只是不需要处理尾随空白行。)

///注释每行后面可以出现的字符没有限制。特别是,注释可能包含代码示例,这些代码示例可能包含自己的注释:

/// Here is an example:
///
/// ```
/// /** Hello World! */
/// public class HelloWorld {
/// public static void main(String... args) {
/// System.out.println("Hello World!"); // the traditional example
/// }
/// }
/// ```

除了用于在视觉上区分新类型的文档注释之外,行尾 ( //) 注释的使用还消除了使用传统 ( /* ... */) 注释所固有的对注释内容的限制。特别是,不可能*/在传统注释(JLS §3.7)中使用字符序列,尽管在编写包含传统注释的示例代码、包含全局表达式的字符串以及包含正则表达式的字符串时可能需要这样做。

对于要在注释中包含的空行,它必须以任何可选的空格开头,然后///

/// This is an example ...
///
/// ... of a 3-line comment containing a blank line.

完全空白的行将导致任何前面和后面的注释被视为单独的注释。在这种情况下,除了最后一个注释之外的所有注释都将被丢弃,并且只有最后一个注释将被视为后续可能声明的文档注释:

/// This comment will be treated as a "dangling comment" and will be ignored.

/// This is the comment for the following declaration.
public void m() { }

///对于两个注释之间可能出现的不以 开头的任何其他注释也是如此///

API及实现

已解析的文档注释由编译器树 APIcom.sun.source.doctree中的包元素表示。

我们引入了一种新类型的树节点 ,RawTextTree它包含未解释的文本,以及一种新的树节点类型 ,DocTree.Kind.MARKDOWN它指示 中的 Markdown 内容RawTextTree。我们向及其子类型添加相应的新visitRawText方法,并且。DocTreeVisitor``DocTreeScanner``DocTreePathScanner

RawTextTree具有 类型的节点MARKDOWN表示 Markdown 内容,包括 HTML 结构,但不包括任何 JavaDoc 标签,例如{@inheritDoc}@param

Markdown 文本分两个阶段处理:

  1. 解析——Markdown 注释被解析为一系列RawTextTree节点,每个节点都有一种DocTree.Kind.MARKDOWN并包含 Markdown 内容,散布着DocTree内联和块标签的标准节点。内联标签和块标签的解析方式与传统文档注释相同,只是标签内容也解析为 Markdown。节点序列DocCommentTree以正常方式存储在节点中。

    与传统的文档注释不同,HTML 构造不会解析为相应的DocTree节点,因为需要考虑太多周围的上下文。

    DocCommentTree然后检查初始解析结果中的 Markdown 内容是否有任何没有关联链接引用定义的引用链接,并且链接标签在语法上与对程序元素的引用相匹配。任何此类链接都由表示或 的等效节点替换。{@link ...}``{@linkplain ...}

  2. 渲染DocCommentTree由工具将其渲染javadoc为适合包含在正在生成的页面中的 HTML。

    节点和其他节点的任何序列RawTextTree都会转换为包含节点文本的单个字符串,RawTextTree并使用 Unicode对象替换字符( U+FFFC) 代替非 Markdown 内容。结果字符串由 Markdown 处理器渲染,然后 U+FFFC 字符在结果输出中被非 Markdown 内容节点的渲染形式替换。

    虽然大部分渲染都很简单,但要特别注意 Markdown 标题:

    • 标题级别根据周围的上下文进行调整。无论标题最初是作为ATX 样式标题(使用字符前缀#来指示级别)还是作为Setext 样式标题(使用带有=或 的下划线-来指示级别),这都适用。

      例如,模块、包或类的文档注释中的 1 级标题在生成的页面中呈现为 2 级标题,而字段、构造函数或方法的文档注释中的 1 级标题则呈现为作为生成页面中的 4 级标题。

      此调整仅适用于 Markdown 标题,不适用于任何直接使用 HTML 标题。

    • 呈现的 HTML 中包含标识符属性id,以便可以轻松地从其他地方引用标题。该标识符是根据标题内容生成的,与 生成其他标识符的方式相同javadoc。 (在浏览器中查看标题时,您可以通过单击弹出链接图标轻松获取该标题的链接。)

    • 标题文本将添加到生成的文档的主搜索索引中。

该实现利用了著名的commonmark-java库的内部副本。根据设计,该库的使用不会在任何公共支持的 JDK API 中公开。

这里描述的大多数功能都是 JDK工具和jdk.javadocjavadoc模块中的编译器树 API的一部分。然而,在标准 Java API 中的一个地方,可以观察到对文档注释的新样式的使用: java.compiler模块中的javax.lang.model.util.Elements.getDocComment方法,该方法返回以下规范化文本:声明的文档注释(如果有)。我们将更新此方法以包含评论。此外,由于注释的类型会影响其解释,因此我们将提供一种新方法来确定声明的文档注释是使用传统的块注释形式还是新的行尾注释形式。///``/** ...*/``///

未来的工作

可以检测标题的一些风格化使用,后跟适当的内容,并将它们转换为等效的 JavaDoc 标记。

例如,标题Parameters后跟参数名称及其描述列表可以转换为等效@param标签:

  • 评论

    # Parameters

    * x the x coordinate
    * y the y coordinate
  • 翻译

    @param x   the x coordinate
    @param y the y coordinate

对于方法可能抛出的异常列表,可以采用类似的策略:

  • 评论

    # Throws

    * NullPointerException if the first parameter is `null`
    * NullPointerException if the second parameter is `null`
    * IllegalArgumentException if an argument is not accepted
  • 翻译

    @throws NullPointerException      if the first parameter is `null`
    @throws NullPointerException if the second parameter is `null`
    @throws IllegalArgumentException if an argument is not accepted

方法的返回值应该只有一个描述,因此在这种情况下不需要使用列表:

  • 评论

    # Returns

    the square root of the argument
  • 翻译

    @return the square root of the argument

提议的表单确实看起来像普通的 Markdown,但它们也占用了更多的垂直空间。开发人员可能更喜欢使用更简洁的形式,使用旧式的 JavaDoc 标签。

将此策略扩展到所有块标签(包括用户指定的标签)可能很困难,但在 JDK 代码库中,仅五个标签(@param@return@see@throws@since)就占块标签所有使用量的 90% 以上。

备择方案

可插拔实现

我们可以支持使用其他用户指定的 Markdown 处理器,从而提供不同风格的 Markdown,而不是利用特定的 Markdown 解析器实现。然而,当生成跨不同库的文档时,这种方法可能会导致不一致,而几乎没有什么明显的好处。

将更多 Markdown 翻译为 HTML

我们可以将额外的 Markdown 结构转换为等效的DocTree节点,表示纯文本、HTML 和 JavaDoc 标签。虽然这种方法的优点是 API 客户端可能不需要知道评论的原始来源是 Markdown,但也有许多缺点:

  • 表示从原始语法树中删除得越多,就越难提供准确且相关的诊断(如果有必要的话)。例如,<table>如果原始注释中没有明确提及合成元素,则有关合成元素的消息可能会令人困惑。

  • DocTree为从 Markdown 结构派生的 HTML 元素合成节点时,很难给出将节点与其在原始注释中的位置相关联的准确位置信息,因为该节点在原始注释中没有表示形式。最多只能给出附近的位置。javac在为合成元素(例如默认的无参数构造函数)或桥接方法分配位置时,Java 编译器中存在类似问题。

  • 通用解决方案很困难,因为它需要了解可能涉及的任何和所有 JavaDoc 标签,因为许多标签允许丰富的内容(例如 Markdown 或 HTML)作为其内容的一部分而不是全部。

    例如,@param标签后面是描述之前的参数名称,<...>如果该名称是类型参数的名称,则可以将该名称括起来。将该名称解释为 HTML 片段是错误的。同样,@serialField标签后面是名称,描述之前是类型。虽然这些是标准 doclet 已知的标准标签,但 doclet 还允许使用用户定义的标签。

内联标签

虽然大多数块标签的使用可以被标题和后续内容的风格化使用所取代,但对于大多数不太常见的内联标签来说,没有这样的等价物。其中,{@inheritDoc}是最常见的,并且在 Markdown 中没有明显的类似物。与其为此而发明替代语法,不如继续使用现有的内联标记语法似乎更好。

/**...*/评论中降价

如上所述///使用文档注释有很多优点。抛开这些原因,如果我们想要解析嵌入在传统/**...*/注释中的 Markdown,而不是引入注释,或者除了引入///注释之外,那么有两种可能性:要么将所有现有/**注释视为 Markdown 注释,要么在每个/**注释中编码一种方式区分 Markdown 注释和传统注释。

将现有注释视为 Markdown 是站不住脚的,因为 Markdown 和 HTML 是不同的语言,具有不同的语法规则。在 HTML 中,空格仅在元素中作为文字文本才有意义<pre>。相比之下,在 Markdown 中,垂直空格可能表示段落分隔符,前导水平空格可能表示缩进的代码块或嵌套列表,尾随空格可能表示硬换行符,相当于<br>HTML 中的情况。此外,在 Markdown 文档中使用 HTML 的规则有些复杂且不直观。最后,JDK 代码中有大量在叙述文本中使用方括号的示例,这可能会被解释为程序元素的链接;例如,The information is returned as a two-dimensional array (array[x][y])

在每个注释中编码文档注释的类型/**是可能的,但没有吸引力。例如,我们可以在首字母后面立即放置一个短字符串,/**以指示后续文本何时应被视为 Markdown:

/**md
* Hello _World!_
*/

当我们对这种方法进行原型设计时,它通常不受欢迎,因为在小评论中被认为过于侵入,而在大评论中则被认为过于无足轻重。

可配置的评论样式

我们可以构建一个可配置的系统,接受一些/** ... */Markdown 格式的文档注释和 HTML 格式的其他文档注释。然而,目前尚不清楚这种机制是否比///在 Markdown 中更公开地使用注释和/** ... */在 HTML 中继续使用注释具有任何显着优势。

风险和假设

  • 该实现使用第三方库commonmark-java将 Markdown 转换为 HTML。如果该库不再维护,那么我们将不得不维护该库的一个分支以便在 JDK 中使用,或者找到等效的替代方案。

  • 由于检查错误代码的能力降低,而且作者有时会忘记检查其文档的生成形式,因此生成的 API 文档存在出现更多错误的风险。

    例如,在传统的文档注释中,包含未终止code标记的段落{@code abc将导致在调用 JavaDoc 时发出诊断消息,并将在生成的文档中显示为▶ invalid @code。在 Markdown 中,等效的未封闭代码范围`abc被指定为文字文本,并且将按原样显示,没有相应的诊断消息。