|  | 
|  | 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