|
| 1 | +package dotty.tools.dotc.util |
| 2 | + |
| 3 | +import dotty.tools.dotc.core.Comments.{Comment, CommentsContext} |
| 4 | +import dotty.tools.dotc.core.Contexts.Context |
| 5 | +import dotty.tools.dotc.core.Names.TermName |
| 6 | +import dotty.tools.dotc.core.Symbols._ |
| 7 | +import dotty.tools.dotc.printing.SyntaxHighlighting |
| 8 | + |
| 9 | +import scala.Console.{BOLD, RESET, UNDERLINED} |
| 10 | +import scala.collection.immutable.ListMap |
| 11 | +import scala.util.matching.Regex |
| 12 | + |
| 13 | +/** |
| 14 | + * A parsed doc comment. |
| 15 | + * |
| 16 | + * @param comment The doc comment to parse |
| 17 | + */ |
| 18 | +class ParsedComment(val comment: Comment) { |
| 19 | + |
| 20 | + /** |
| 21 | + * The bounds of a section that represents the [start; end[ char offset |
| 22 | + * of the section within this comment's `content`. |
| 23 | + */ |
| 24 | + private type Bounds = (Int, Int) |
| 25 | + |
| 26 | + /** The content of this comment, after expansion if possible. */ |
| 27 | + val content: String = comment.expandedBody.getOrElse(comment.raw) |
| 28 | + |
| 29 | + /** An index that marks all sections boundaries */ |
| 30 | + private lazy val tagIndex: List[Bounds] = CommentParsing.tagIndex(content) |
| 31 | + |
| 32 | + /** |
| 33 | + * Maps a parameter name to the bounds of its doc |
| 34 | + * |
| 35 | + * @see paramDoc |
| 36 | + */ |
| 37 | + private lazy val paramDocs: Map[String, Bounds] = CommentParsing.paramDocs(content, "@param", tagIndex) |
| 38 | + |
| 39 | + /** |
| 40 | + * The "main" documentation for this comment. That is, the comment before any section starts. |
| 41 | + */ |
| 42 | + lazy val mainDoc: String = { |
| 43 | + val doc = tagIndex match { |
| 44 | + case Nil => content.stripSuffix("*/") |
| 45 | + case (start, _) :: _ => content.slice(0, start) |
| 46 | + } |
| 47 | + clean(doc.stripPrefix("/**")) |
| 48 | + } |
| 49 | + |
| 50 | + /** |
| 51 | + * Renders this comment as markdown. |
| 52 | + * |
| 53 | + * The different sections are formatted according to the mapping in `knownTags`. |
| 54 | + */ |
| 55 | + def renderAsMarkdown(implicit ctx: Context): String = { |
| 56 | + val buf = new StringBuilder |
| 57 | + buf.append(mainDoc + System.lineSeparator + System.lineSeparator) |
| 58 | + val groupedSections = CommentParsing.groupedSections(content, tagIndex) |
| 59 | + |
| 60 | + for { |
| 61 | + (tag, formatter) <- ParsedComment.knownTags |
| 62 | + boundss <- groupedSections.get(tag) |
| 63 | + texts = boundss.map { case (start, end) => clean(content.slice(start, end)) } |
| 64 | + formatted <- formatter(texts) |
| 65 | + } { |
| 66 | + buf.append(formatted) |
| 67 | + buf.append(System.lineSeparator) |
| 68 | + } |
| 69 | + |
| 70 | + buf.toString |
| 71 | + } |
| 72 | + |
| 73 | + /** |
| 74 | + * The `@param` section corresponding to `name`. |
| 75 | + * |
| 76 | + * @param name The parameter name whose documentation to extract. |
| 77 | + * @return The formatted documentation corresponding to `name`. |
| 78 | + */ |
| 79 | + def paramDoc(name: TermName): Option[String] = paramDocs.get(name.toString).map { case (start, end) => |
| 80 | + val rawContent = content.slice(start, end) |
| 81 | + val docContent = ParsedComment.prefixRegex.replaceFirstIn(rawContent, "") |
| 82 | + clean(docContent) |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Cleans `str`: remove prefixing `*` and trim the string. |
| 87 | + * |
| 88 | + * @param str The string to clean |
| 89 | + * @return The cleaned string. |
| 90 | + */ |
| 91 | + private def clean(str: String): String = str.stripMargin('*').trim |
| 92 | +} |
| 93 | + |
| 94 | +object ParsedComment { |
| 95 | + |
| 96 | + /** |
| 97 | + * Return the `ParsedComment` associated with `symbol`, if it exists. |
| 98 | + * |
| 99 | + * @param symbol The symbol for which to retrieve the documentation |
| 100 | + * @return If it exists, the `ParsedComment` for `symbol`. |
| 101 | + */ |
| 102 | + def docOf(symbol: Symbol)(implicit ctx: Context): Option[ParsedComment] = { |
| 103 | + val documentedSymbol = if (symbol.isPrimaryConstructor) symbol.owner else symbol |
| 104 | + for { docCtx <- ctx.docCtx |
| 105 | + comment <- docCtx.docstring(documentedSymbol) |
| 106 | + } yield new ParsedComment(comment) |
| 107 | + } |
| 108 | + |
| 109 | + @scala.annotation.internal.sharable |
| 110 | + private val prefixRegex = """@param\s+\w+\s+""".r |
| 111 | + |
| 112 | + /** A mapping from tag name to `TagFormatter` */ |
| 113 | + private val knownTags: ListMap[String, TagFormatter] = ListMap( |
| 114 | + "@tparam" -> TagFormatter("Type Parameters", toDescriptionList), |
| 115 | + "@param" -> TagFormatter("Parameters", toDescriptionList), |
| 116 | + "@return" -> TagFormatter("Returns", toMarkdownList), |
| 117 | + "@throws" -> TagFormatter("Throws", toDescriptionList), |
| 118 | + "@see" -> TagFormatter("See Also", toMarkdownList), |
| 119 | + "@example" -> TagFormatter("Examples", toCodeFences("scala")), |
| 120 | + "@usecase" -> TagFormatter("Usecases", toCodeFences("scala")), |
| 121 | + "@note" -> TagFormatter("Note", toMarkdownList), |
| 122 | + "@author" -> TagFormatter("Authors", toMarkdownList), |
| 123 | + "@since" -> TagFormatter("Since", toMarkdownList), |
| 124 | + "@version" -> TagFormatter("Version", toMarkdownList) |
| 125 | + ) |
| 126 | + |
| 127 | + /** |
| 128 | + * Formats a list of items into a list describing them. |
| 129 | + * |
| 130 | + * Each element is assumed to consist of a first word, which is the item being described. The rest |
| 131 | + * is the description of the item. |
| 132 | + * |
| 133 | + * @param items The items to format into a list. |
| 134 | + * @return A markdown list of descriptions. |
| 135 | + */ |
| 136 | + private def toDescriptionList(ctx: Context, items: List[String]): String = { |
| 137 | + val formattedItems = items.map { p => |
| 138 | + val name :: rest = p.split(" ", 2).toList |
| 139 | + s"${bold(name)(ctx)} ${rest.mkString("").trim}" |
| 140 | + } |
| 141 | + toMarkdownList(ctx, formattedItems) |
| 142 | + } |
| 143 | + |
| 144 | + /** |
| 145 | + * Formats a list of items into a markdown list. |
| 146 | + * |
| 147 | + * @param items The items to put in a list. |
| 148 | + * @return The list of items, in markdown. |
| 149 | + */ |
| 150 | + private def toMarkdownList(ctx: Context, items: List[String]): String = { |
| 151 | + val formattedItems = items.map(_.lines.mkString(System.lineSeparator + " ")) |
| 152 | + formattedItems.mkString(" - ", System.lineSeparator + " - ", "") |
| 153 | + } |
| 154 | + |
| 155 | + /** |
| 156 | + * If the color is enabled, add syntax highlighting to each of `snippets`, otherwise wrap each |
| 157 | + * of them in a markdown code fence. |
| 158 | + * The results are put into a markdown list. |
| 159 | + * |
| 160 | + * @param language The language to use for the code fences |
| 161 | + * @param snippets The list of snippets to format. |
| 162 | + * @return A markdown list of code fences. |
| 163 | + * @see toCodeFence |
| 164 | + */ |
| 165 | + private def toCodeFences(language: String)(ctx: Context, snippets: List[String]): String = |
| 166 | + toMarkdownList(ctx, snippets.map(toCodeFence(language)(ctx, _))) |
| 167 | + |
| 168 | + /** |
| 169 | + * Formats `snippet` for display. If the color is enabled, the syntax is highlighted, |
| 170 | + * otherwise the snippet is wrapped in a markdown code fence. |
| 171 | + * |
| 172 | + * @param language The language to use. |
| 173 | + * @param snippet The code snippet |
| 174 | + * @return `snippet`, wrapped in a code fence. |
| 175 | + */ |
| 176 | + private def toCodeFence(language: String)(ctx: Context, snippet: String): String = { |
| 177 | + if (colorEnabled(ctx)) { |
| 178 | + SyntaxHighlighting.highlight(snippet)(ctx) |
| 179 | + } else { |
| 180 | + s"""```$language |
| 181 | + |$snippet |
| 182 | + |```""".stripMargin |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + /** |
| 187 | + * Format the elements of documentation associated with a given tag using `fn`, and starts the |
| 188 | + * section with `title`. |
| 189 | + * |
| 190 | + * @param title The title to give to the formatted items. |
| 191 | + * @param fn The formatting function to use. |
| 192 | + */ |
| 193 | + private case class TagFormatter(title: String, fn: (Context, List[String]) => String) { |
| 194 | + |
| 195 | + /** |
| 196 | + * Format `item` using `fn` if `items` is not empty. |
| 197 | + * |
| 198 | + * @param items The items to format |
| 199 | + * @return If items is not empty, the items formatted using `fn`. |
| 200 | + */ |
| 201 | + def apply(items: List[String])(implicit ctx: Context): Option[String] = items match { |
| 202 | + case Nil => |
| 203 | + None |
| 204 | + case items => |
| 205 | + Some(s"""${bold(title)} |
| 206 | + |${fn(ctx, items)} |
| 207 | + |""".stripMargin) |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + /** Is the color enabled in the context? */ |
| 212 | + private def colorEnabled(implicit ctx: Context): Boolean = |
| 213 | + ctx.settings.color.value != "never" |
| 214 | + |
| 215 | + /** Show `str` in bold */ |
| 216 | + private def bold(str: String)(implicit ctx: Context): String = { |
| 217 | + if (colorEnabled) s"$BOLD$str$RESET" |
| 218 | + else s"**$str**" |
| 219 | + } |
| 220 | + |
| 221 | +} |
0 commit comments