All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dotty.tools.scaladoc.tasty.comments.Comments.scala Maven / Gradle / Ivy

The newest version!
package dotty.tools.scaladoc
package tasty.comments

import scala.collection.immutable.SortedMap
import scala.util.Try

import com.vladsch.flexmark.util.{ast => mdu, sequence}
import com.vladsch.flexmark.{ast => mda}
import com.vladsch.flexmark.formatter.Formatter
import com.vladsch.flexmark.util.sequence.BasedSequence

import scala.quoted._
import dotty.tools.scaladoc.tasty.comments.markdown.ExtendedFencedCodeBlock
import dotty.tools.scaladoc.tasty.comments.wiki.Paragraph
import dotty.tools.scaladoc.DocPart
import dotty.tools.scaladoc.tasty.{ SymOpsWithLinkCache, SymOps }
import scala.jdk.CollectionConverters._
import dotty.tools.scaladoc.snippets._

class Repr(val qctx: Quotes)(val sym: qctx.reflect.Symbol)

case class Comment (
  body:                    DocPart,
  short:                   Option[DocPart],
  authors:                 List[DocPart],
  see:                     List[DocPart],
  result:                  Option[DocPart],
  throws:                  List[DocPart],
  valueParams:             SortedMap[String, DocPart],
  typeParams:              SortedMap[String, DocPart],
  version:                 Option[DocPart],
  since:                   Option[DocPart],
  todo:                    List[DocPart],
  deprecated:              Option[DocPart],
  note:                    List[DocPart],
  example:                 List[DocPart],
  constructor:             Option[DocPart],
  group:                   Option[String],
  // see comment in PreparsedComment below regarding these
  groupDesc:               SortedMap[String, DocPart],
  groupNames:              SortedMap[String, DocPart],
  groupPrio:               SortedMap[String, Int],
  /** List of conversions to hide - containing e.g: `scala.Predef.FloatArrayOps` */
  hideImplicitConversions: List[DocPart]
)

case class PreparsedComment(
  body:                    String,
  authors:                 List[String],
  see:                     List[String],
  result:                  List[String],
  throws:                  SortedMap[String, String],
  valueParams:             SortedMap[String, String],
  typeParams:              SortedMap[String, String],
  version:                 List[String],
  since:                   List[String],
  todo:                    List[String],
  deprecated:              List[String],
  note:                    List[String],
  example:                 List[String],
  constructor:             List[String],
  group:                   List[String],
  // NOTE these don't need to be sorted in principle, but code is nicer if they are
  groupDesc:               SortedMap[String, String],
  groupNames:              SortedMap[String, String],
  groupPrio:               SortedMap[String, Int],
  hideImplicitConversions: List[String],
  shortDescription:        List[String],
  syntax:                  List[String],
  strippedLinesBeforeNo:   Int,
)

case class DokkaCommentBody(summary: Option[DocPart], body: DocPart)

abstract class MarkupConversion[T](val repr: Repr)(using dctx: DocContext) {
  protected def stringToMarkup(str: String): T
  protected def markupToDokka(t: T): DocPart
  protected def markupToString(t: T): String
  protected def markupToDokkaCommentBody(t: T): DokkaCommentBody
  protected def filterEmpty(xs: List[String]): List[T]
  protected def filterEmpty(xs: SortedMap[String, String]): SortedMap[String, T]
  protected def processSnippets(t: T, preparsed: PreparsedComment): T

  lazy val snippetChecker = dctx.snippetChecker

  val qctx: repr.qctx.type = if repr == null then null else repr.qctx // TODO why we do need null?
  val owner: qctx.reflect.Symbol =
    if repr == null then null.asInstanceOf[qctx.reflect.Symbol] else repr.sym
  private given qctx.type = qctx

  lazy val srcPos = if owner == qctx.reflect.defn.RootClass then {
    val sourceFile = dctx.args.rootDocPath.map(p => dotty.tools.dotc.util.SourceFile(dotty.tools.io.AbstractFile.getFile(p), scala.io.Codec.UTF8))
    sourceFile.fold(dotty.tools.dotc.util.NoSourcePosition)(sf => dotty.tools.dotc.util.SourcePosition(sf, dotty.tools.dotc.util.Spans.NoSpan))
  } else owner.pos.get.asInstanceOf[dotty.tools.dotc.util.SrcPos]

  object SymOpsWithLinkCache extends SymOpsWithLinkCache
  export SymOpsWithLinkCache._
  import SymOps._

  def resolveLink(queryStr: String): DocLink =
    if SchemeUri.matches(queryStr) then DocLink.ToURL(queryStr)
    else QueryParser(queryStr).tryReadQuery() match
      case Left(err) =>
        report.warning(s"Unable to parse query `$queryStr`: ${err.getMessage}", srcPos)
        val msg = s"Unable to parse query: ${err.getMessage}"
        DocLink.UnresolvedDRI(queryStr, msg)
      case Right(query) =>
        MemberLookup.lookup(using qctx)(query, owner) match
          case Some((sym, targetText, inheritingParent)) =>
            var dri = inheritingParent match
              case Some(parent) => sym.driInContextOfInheritingParent(parent)
              case None => sym.dri
            DocLink.ToDRI(dri, targetText)
          case None =>
            val txt = s"No DRI found for query"
            val msg = s"$txt: $queryStr"

            if (!summon[DocContext].args.noLinkWarnings) then

              report.warning(msg, srcPos)

            DocLink.UnresolvedDRI(queryStr, txt)

  private val SchemeUri = """[a-z]+:.*""".r

  private def single(annot: String, xs: List[String], filter: Boolean = true): Option[T] =
    (if (filter) filterEmpty(xs) else xs.map(stringToMarkup)) match {
      case x :: xs =>
        Some(x)
      case _ => None
    }

  def snippetCheckingFunc: qctx.reflect.Symbol => SnippetChecker.SnippetCheckingFunc =
    (s: qctx.reflect.Symbol) => {
      val path = s.source.map(_.path)
      val pathBasedArg = dctx.snippetCompilerArgs.get(path)
      val scDataCollector = SnippetCompilerDataCollector[qctx.type](qctx)
      val data = scDataCollector.getSnippetCompilerData(s, s)
      val sourceFile = scDataCollector.getSourceFile(s)
      (str: String, lineOffset: SnippetChecker.LineOffset, argOverride: Option[SCFlags]) => {
          val arg = argOverride.fold(pathBasedArg)(pathBasedArg.overrideFlag(_))
          val res = snippetChecker.checkSnippet(str, Some(data), arg, lineOffset, sourceFile)
          res.filter(r => !r.isSuccessful).foreach(_.reportMessages()(using compilerContext))
          res
      }
    }

  def linkedExceptions(exceptions: SortedMap[String, T]): List[DocPart] = {
    exceptions.map { (name, body) =>
      val link: DocLink = resolveLink(name)
      val merged = mergeLinkWithBody(link, body)
      markupToDokka(merged)
    }.toList
  }

  def mergeLinkWithBody(link: DocLink, body: T): T

  final def parse(preparsed: PreparsedComment): Comment =
    val markup = stringToMarkup(preparsed.body)
    val body = markupToDokkaCommentBody(processSnippets(markup, preparsed))
    Comment(
      body                    = body.body,
      short                   = body.summary,
      authors                 = filterEmpty(preparsed.authors).map(markupToDokka),
      see                     = filterEmpty(preparsed.see).map(markupToDokka),
      result                  = single("@result", preparsed.result).map(markupToDokka),
      throws                  = linkedExceptions(filterEmpty(preparsed.throws)),
      valueParams             = filterEmpty(preparsed.valueParams).view.mapValues(markupToDokka).to(SortedMap),
      typeParams              = filterEmpty(preparsed.typeParams).view.mapValues(markupToDokka).to(SortedMap),
      version                 = single("@version", preparsed.version).map(markupToDokka),
      since                   = single("@since", preparsed.since).map(markupToDokka),
      todo                    = filterEmpty(preparsed.todo).map(markupToDokka),
      deprecated              = single("@deprecated", preparsed.deprecated, filter = false).map(markupToDokka),
      note                    = filterEmpty(preparsed.note).map(markupToDokka),
      example                 = filterEmpty(preparsed.example).map(markupToDokka),
      constructor             = single("@constructor", preparsed.constructor).map(markupToDokka),
      group                   = single("@group", preparsed.group).map(markupToString),
      groupDesc               = filterEmpty(preparsed.groupDesc).view.mapValues(markupToDokka).to(SortedMap),
      groupNames              = filterEmpty(preparsed.groupNames).view.mapValues(markupToDokka).to(SortedMap),
      groupPrio               = preparsed.groupPrio,
      hideImplicitConversions = filterEmpty(preparsed.hideImplicitConversions).map(markupToDokka)
    )
}

class MarkdownCommentParser(repr: Repr)(using dctx: DocContext)
    extends MarkupConversion[mdu.Node](repr) {

  def stringToMarkup(str: String) =
    MarkdownParser.parseToMarkdown(str, markdown.DocFlexmarkParser(resolveLink))

  def markupToString(t: mdu.Node): String = t.getChars().toString()

  def markupToDokka(md: mdu.Node): DocPart = md

  def markupToDokkaCommentBody(md: mdu.Node) =
    val summary =
      md.getChildIterator.asScala.collectFirst { case p: mda.Paragraph => p }

    DokkaCommentBody(summary, md)

  def filterEmpty(xs: List[String]) = {
    xs.map(_.trim)
      .filterNot(_.isEmpty)
      .map(stringToMarkup)
  }

  def filterEmpty(xs: SortedMap[String,String]) =
    xs.view.mapValues(_.trim)
      .filterNot { case (_, v) => v.isEmpty }
      .mapValues(stringToMarkup).to(SortedMap)

  def processSnippets(root: mdu.Node, preparsed: PreparsedComment): mdu.Node =
    FlexmarkSnippetProcessor.processSnippets(root, Some(preparsed), snippetCheckingFunc(owner))

  def mergeLinkWithBody(link: DocLink, body: mdu.Node): mdu.Node = {
    import dotty.tools.scaladoc.tasty.comments.markdown._
    val str = link match
      case DocLink.ToURL(url) => url
      case DocLink.ToDRI(dri, name) => name
      case DocLink.UnresolvedDRI(query, msg) => query
    val sequence = BasedSequence.EmptyBasedSequence().append(str)
    val node = new DocLinkNode(link, "", sequence)
    body.prependChild(node)
    body
  }
}

class WikiCommentParser(repr: Repr)(using DocContext)
    extends MarkupConversion[wiki.Body](repr):

  def stringToMarkup(str: String) = wiki.Parser(str, resolverLink).document()

  def resolverLink(queryStr: String, bodyOpt: Option[wiki.Inline]): wiki.Inline =
    val link = resolveLink(queryStr)
    wiki.Link(link, bodyOpt)

  // Do we need those?
  private def flatten(b: wiki.Inline): String = b match
    case wiki.Text(t) => t
    case wiki.Italic(t) => flatten(t)
    case wiki.Bold(t) => flatten(t)
    case wiki.Underline(t) => flatten(t)
    case wiki.Superscript(t) => flatten(t)
    case wiki.Subscript(t) => flatten(t)
    case wiki.Link(_, t) => t.fold("")(flatten)
    case wiki.Monospace(t) => flatten(t)
    case wiki.RepresentationLink(t, _) => flatten(t)
    case wiki.Chain(elems) => elems.headOption.fold("")(flatten)
    case wiki.HtmlTag(t) => t
    case wiki.Summary(t) => flatten(t)

  private def flatten(b: wiki.Block): String = b match
    case wiki.Paragraph(text) => flatten(text)
    case wiki.Title(text, _) => flatten(text)
    case wiki.Code(text) => text
    case wiki.UnorderedList(elems) => elems.headOption.fold("")(flatten)
    case wiki.OrderedList(elems, _) => elems.headOption.fold("")(flatten)
    case wiki.DefinitionList(items) => items.headOption.fold("")(e => flatten(e._1))
    case wiki.HorizontalRule => ""
    case wiki.Table(header, columns, rows) => (header +: rows).flatMap(_.cells).flatMap(_.blocks).map(flatten).mkString

  def markupToString(str: wiki.Body) = str.blocks.headOption.fold("")(flatten)

  def markupToDokka(body: wiki.Body) = parseBlocks(body.blocks) // TODO

  def parseBlocks(blocks: Seq[wiki.WikiDocElement]) = blocks

  def markupToDokkaCommentBody(body: wiki.Body) =
     DokkaCommentBody(
      summary = body.summary.map(s => parseBlocks(s.blocks)),
      body = parseBlocks(body.blocks),
    )

  def filterEmpty(xs: List[String]) =
    xs.map(stringToMarkup)

  def filterEmpty(xs: SortedMap[String,String]) =
    xs.view.mapValues(stringToMarkup).to(SortedMap)
      .filterNot { case (_, v) => v.blocks.isEmpty }

  def processSnippets(root: wiki.Body, preparsed: PreparsedComment): wiki.Body =
    // Currently not supported
    root

  def mergeLinkWithBody(link: DocLink, body: wiki.Body): wiki.Body =
    val linkNode = wiki.Link(link, None)
    val newBody = body match {
      case wiki.Body(List(wiki.Paragraph(wiki.Chain(content)))) =>
        val descr = wiki.Text(" ") +: content
        wiki.Body(List(wiki.Paragraph(wiki.Chain(linkNode +: descr))))
      case _ => body
    }
    newBody




© 2015 - 2024 Weber Informatics LLC | Privacy Policy