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

dotty.tools.scaladoc.renderers.MemberRenderer.scala Maven / Gradle / Ivy

There is a newer version: 3.6.0-RC1-bin-20240903-21a3d39-NIGHTLY
Show newest version
package dotty.tools.scaladoc
package renderers

import scala.collection.immutable.SortedMap
import scala.util.chaining.*
import util.HTML.{div, *}

import scala.jdk.CollectionConverters.*
import dotty.tools.scaladoc.translators.FilterAttributes
import org.jsoup.Jsoup
import translators.*

class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) extends DocRender(signatureRenderer):
  import signatureRenderer._

  val signatureProvider: ScalaSignatureProvider = ScalaSignatureProvider()

  def doc(m: Member): Seq[AppliedTag] =  m.docs.fold(Nil)(d => Seq(renderDocPart(d.body)))

  def tableRow(name: String, content: TagArg*) = Seq(dt(cls := "body-small")(name), dd(cls := "body-medium")(content*))

  def defintionClasses(m: Member) = m.origin match
    case Origin.Overrides(defs) =>
      def renderDef(d: Overridden): Seq[TagArg] =
        Seq(" -> ", signatureRenderer.renderLink(d.name, d.dri))
      val headNode = m.inheritedFrom.map(form => signatureRenderer.renderLink(form.name, form.dri))
      val tailNodes = defs.flatMap(renderDef)
      val nodes = headNode.fold(tailNodes.drop(1))(_ +: tailNodes)
      tableRow("Definition Classes", div(nodes*))

    case _ => Nil

  def inheritedFrom(m: Member) = m.inheritedFrom match
    case Some(InheritedFrom(name, dri, isSourceSuperclassHidden)) =>
      val hiddenNameSuffix = if isSourceSuperclassHidden then " (hidden)" else ""
      tableRow("Inherited from:", signatureRenderer.renderLink(name + hiddenNameSuffix, dri))
    case _ => Nil

  def flattenedDocPart(on: SortedMap[String, DocPart]): Seq[AppliedTag] =
    on.flatMap { case (name, value) => tableRow(name, renderDocPart(value)) }.toSeq

  def docAttributes(m: Member): Seq[AppliedTag] =

    def list(name: String, on: List[DocPart]): Seq[AppliedTag] =
      if on.isEmpty then Nil else tableRow(name, div(on.map(e => div(renderDocPart(e)))))

    def opt(name: String, on: Option[DocPart]): Seq[AppliedTag] =
      if on.isEmpty then Nil else tableRow(name, renderDocPart(on.get))

    def authors(authors: List[DocPart]) = if summon[DocContext].args.includeAuthors then list("Authors:", authors) else Nil

    m.docs.fold(Nil)(d =>
      opt("Returns", d.result) ++
      list("Throws", d.throws) ++
      opt("Constructor", d.constructor) ++
      authors(d.authors) ++
      list("See also", d.see) ++
      opt("Version", d.version) ++
      opt("Since", d.since) ++
      list("Todo", d.todo) ++
      list("Note", d.note) ++
      list("Example", d.example)
    )

  def companion(m: Member): Seq[AppliedTag] = m.companion.fold(Nil){ (kind, dri) =>
    val kindName = kind.name
    tableRow("Companion", signatureRenderer.renderLink(kindName, dri))
  }

  def source(m: Member): Seq[AppliedTag] =
    summon[DocContext].sourceLinks.pathTo(m).fold(Nil){ link =>
      tableRow("Source", a(href := link, target := "_blank")(m.sources.fold("(source)")(_.path.getFileName().toString())))
    }

  def deprecation(m: Member): Seq[AppliedTag] = m.deprecated.fold(Nil){ a =>
    def stripQuotes(s: String) = s.stripPrefix("\"").stripSuffix("\"")
    def parameter(p: Annotation.AnnotationParameter): TagArg = p match
      case Annotation.PrimitiveParameter(_, value) => stripQuotes(value)
      case Annotation.LinkParameter(_, dri, text) =>
        signatureRenderer.renderLink(stripQuotes(text), dri)
      case Annotation.UnresolvedParameter(_, value) => stripQuotes(value)

    val (named, unnamed) = a.params.partition(_.name.nonEmpty)
    val message = named.find(_.name.get == "message")
    val since = named.find(_.name.get == "since")

    val content = (
      Seq(
        since.map(s => code("[Since version ", parameter(s), "] ")),
        message.map(m => parameter(m)),
      ) ++ m.docs.map(_.deprecated.toSeq.map(renderDocPart))
      ).flatten.pipe { c =>
        if c.isEmpty then Seq("true") else c
      }
    tableRow("Deprecated", content*)
  }

  def experimental(m: Member) = m.experimental.fold(Nil)(_ => tableRow("Experimental", Seq("true")))

  def typeParams(m: Member): Seq[AppliedTag] = m.docs.fold(Nil)(d => flattenedDocPart(d.typeParams))
  def valueParams(m: Member): Seq[AppliedTag] = m.docs.fold(Nil)(d => flattenedDocPart(d.valueParams))

  def memberInfo(m: Member, withBrief: Boolean = false, full: Boolean = false): Seq[AppliedTag] =
    val comment = m.docs
    val bodyContents = m.docs.fold(Nil)(e => renderDocPart(e.body) :: Nil)

    val classLikeInfo: TagArg = classLikeParts(m, full)

    val memberTypeParams = typeParams(m)
    val memberValueParams = valueParams(m)
    val attributes = Seq(
      docAttributes(m),
      companion(m),
      deprecation(m),
      experimental(m),
      defintionClasses(m),
      inheritedFrom(m),
      source(m),
      classLikeInfo
    ).filter {
      case Nil => false
      case _ => true
    }

    Seq(
      Option.when(withBrief && comment.flatMap(_.short).nonEmpty)(div(cls := "documentableBrief doc")(comment.flatMap(_.short).fold("")(renderDocPart))),
      Option.when(bodyContents.nonEmpty || attributes.nonEmpty)(
        div(cls := "cover")(
          div(cls := "doc")(bodyContents),
          Option.when(full)(
            section(id := "attributes")(
              Option.when(memberTypeParams.nonEmpty)(Seq(
                h2(cls := "h500")("Type parameters"),
                dl(cls := "attributes")(memberTypeParams*)
              )).toSeq.flatten,
              Option.when(memberValueParams.nonEmpty)(Seq(
                h2(cls := "h500")("Value parameters"),
                dl(cls := "attributes")(memberValueParams*)
              )).toSeq.flatten,
              h2(cls := "h500")("Attributes"),
              dl(cls := "attributes")(attributes*)
            )
          ).getOrElse(
            Option.when(memberTypeParams.nonEmpty)(Seq(
              h2(cls := "h200")("Type parameters"),
              dl(cls := "attributes attributes-small")(memberTypeParams *)
            )).toSeq.flatten ++
            Option.when(memberValueParams.nonEmpty)(Seq(
              h2(cls := "h200")("Value parameters"),
              dl(cls := "attributes attributes-small")(memberValueParams *)
            )).toSeq.flatten :+
            h2(cls := "h200")("Attributes") :+
            dl(cls := "attributes attributes-small")(attributes *)
          )
        )
      )
    ).flatten

  private def originInfo(m: Member): Seq[TagArg] = m.origin match {
    case Origin.ImplicitlyAddedBy(name, dri) =>
      Seq("Implicitly added by ", renderLink(name, dri))
    case Origin.ExtensionFrom(name, dri) =>
      Seq("Extension method from ", renderLink(name, dri))
    case Origin.ExportedFrom(Some(link)) =>
      val signatureName: TagArg = link match
        case Link(name, dri) => renderLink(name, dri)
      Seq("Exported from ", signatureName)
    case _ => Nil
  }

  def memberSignature(member: Member) =
    val depStyle = if member.deprecated.isEmpty then "" else "deprecated"
    val nameClasses = Seq(
      cls := s"documentableName $depStyle",
    )

    val signature: MemberSignature = signatureProvider.rawSignature(member)()
    val isSubtype = signature.suffix.exists {
      case Keyword(keyword) => keyword.contains("extends")
      case _ => false
    }
    if !isSubtype then
      Seq(
        div(cls := "signature")(
          (Seq[TagArg](
            span(cls := "modifiers")(signature.prefix.map(renderElement(_))),
            span(cls := "kind")(signature.kind.map(renderElement(_))),
            signature.name.map(renderElement(_, nameClasses*))
          ) ++ signature.suffix.map(renderElement(_)))*
        ),
      )
    else
      val (beforeExtends, afterExtends) = signature.suffix.splitAt(signature.suffix.indexOf(Keyword("extends")))
      val (shortSuffix, longSuffix) = splitTypeSuffixSignature(beforeExtends, afterExtends)
      Seq(
        div(cls := "signature")(
          span(cls := "signature-short")(
            (Seq[TagArg](
              span(cls := "modifiers")(signature.prefix.map(renderElement(_))),
              span(cls := "kind")(signature.kind.map(renderElement(_))),
              signature.name.map(renderElement(_, nameClasses *))
            ) ++ shortSuffix.map(renderElement(_)))*
          ),
          span(cls := "signature-long")(
            longSuffix.map(renderElement(_))*
          )
        ),
      )
  end memberSignature

  def splitTypeSuffixSignature(shortAcc: List[SignaturePart], tail: List[SignaturePart], nestedTypeLevel: Int = 0): (List[SignaturePart], List[SignaturePart]) =
    tail match
      case Nil =>
        (shortAcc, Nil)
      case (head @ Plain("[")) :: rest =>
        splitTypeSuffixSignature(shortAcc :+ head, rest, nestedTypeLevel + 1)
      case (head @ Plain("]")) :: rest =>
        splitTypeSuffixSignature(shortAcc :+ head, rest, nestedTypeLevel - 1)
      case (head @ Keyword(", ")) :: rest if nestedTypeLevel == 0 =>
        (shortAcc :+ head, rest)
      case head :: rest =>
        splitTypeSuffixSignature(shortAcc :+ head, rest, nestedTypeLevel)


  def memberIcon(member: Member) = member.kind match {
    case _ =>

      // val iconSpan = span(cls := s"micon ${member.kind.name.take(2)}$withCompanion")()
      val iconSpan = span(cls := "icon")(
        span(cls := s"micon ${member.kind.name.take(2)}"),
        member.companion.map((kind, _) => span(cls := s"micon companion ${kind.name.take(2)}")).toList
      )
      Seq(member.companion.flatMap( (_, dri) => link(dri)).fold(iconSpan)(link => a(href := link)(iconSpan)))
  }

  def annotations(member: Member): Option[TagArg] =
   val rawBuilder = SignatureBuilder().annotationsBlock(member)
   val signatures = rawBuilder.content
   val rendered = signatures.map(renderElement(_))
   Option.when(rendered.nonEmpty)(span(cls := "annotations monospace")(rendered))

  def member(member: Member) =
    val filterAttributes = FilterAttributes.attributesFor(member)
    val anchor = if member.dri.anchor.isEmpty then Nil else Seq(id := member.dri.anchor)
    def topLevelAttr = Seq(cls := "documentableElement")
      ++ anchor
      ++ filterAttributes.map{ case (n, v) => Attr(s"data-f-$n") := v }

    val originInf = originInfo(member)
    val memberInf = memberInfo(member, withBrief = true)
    val annots = annotations(member)

    div(topLevelAttr*)(
      div(cls := "documentableElement-expander")(
        Option.when(annots.nonEmpty || originInf.nonEmpty || memberInf.nonEmpty)(button(cls := "icon-button ar show-content")).toList,
        annots.map(div(_)).toList,
        div(cls := "header monospace mono-medium")(memberSignature(member)),
      ),
      Option.when(originInf.nonEmpty || memberInf.nonEmpty)(
        div(cls := "docs")(
          span(cls := "modifiers"), // just to have padding on left
          div(
            div(cls := "originInfo")(originInf*),
            div(cls := "memberDocumentation")(memberInf*),
          )
        )
      ).toList
    )

  private case class MGroup(header: AppliedTag, members: Seq[Member], groupName: String)

  private def makeSubgroupHeader(name: String): AppliedTag =
    h4(cls := "groupHeader h200")(name)

  private def actualGroup(name: String, members: Seq[Member | MGroup], headerConstructor: String => AppliedTag = makeSubgroupHeader, wrapInSection: Boolean = true): Seq[AppliedTag] =
    if members.isEmpty then Nil else
      val groupBody = div(cls := "documentableList expand")(
        div(cls := "documentableList-expander")(
          button(cls := "icon-button show-content expand"),
          headerConstructor(name)
        ),
        members.sortBy {
          case m: Member => m.name
          case MGroup(_, _, name) => name
        }.map {
          case element: Member =>
            member(element)
          case MGroup(header, members, _) =>
            div(
              header,
              members.map(member)
            )
        }
      )
      if wrapInSection then
        section(id := name.replace(' ', '-'))(
          groupBody
        ) :: Nil
      else
        groupBody :: Nil


  private def isDeprecated(m: Member | MGroup): Boolean = m match
    case m: Member => m.deprecated.nonEmpty
    case g: MGroup => g.members.exists(isDeprecated)

  private def isExperimental(m: Member | MGroup): Boolean = m match
    case m: Member => m.experimental.nonEmpty
    case g: MGroup => g.members.exists(isExperimental)

  private def isInherited(m: Member | MGroup): Boolean = m match
    case m: Member => m.inheritedFrom.nonEmpty
    case g: MGroup => g.members.exists(isInherited)

  private def isAbstract(m: Member | MGroup): Boolean = m match
    case m: Member => m.modifiers.exists(Set(Modifier.Abstract, Modifier.Deferred).contains)
    case g: MGroup => g.members.exists(isAbstract)

  private type SubGroup = (String, Seq[Member | MGroup])
  private def buildGroup(name: String, subgroups: Seq[SubGroup]): Tab =
    val all = subgroups.map { case (name, members) =>
      val (experimental, nonExperimental) = members.partition(isExperimental)
      val (allInherited, allDefined) = nonExperimental.partition(isInherited)
      val (depDefined, defined) = allDefined.partition(isDeprecated)
      val (depInherited, inherited) = allInherited.partition(isDeprecated)
      val (abstractInherited, concreteInherited) = inherited.partition(isAbstract)
      val normalizedName = name.toLowerCase
      val definedWithGroup = if Set("methods", "fields").contains(normalizedName) then
          val (abstr, concr) = defined.partition(isAbstract)
          Seq(
            actualGroup(s"Abstract ${normalizedName}", abstr),
            actualGroup(s"Concrete ${normalizedName}", concr)
          )
        else
          Seq(actualGroup(name, defined))

      definedWithGroup ++ List(
        actualGroup(s"Deprecated ${normalizedName}", depDefined),
        actualGroup(s"Inherited ${normalizedName}", concreteInherited),
        actualGroup(s"Inherited and Abstract ${normalizedName}", abstractInherited),
        actualGroup(s"Deprecated and Inherited ${normalizedName}", depInherited),
        actualGroup(name = s"Experimental ${normalizedName}", experimental)
      )
    }

    val children = all.flatten.flatten
    if children.isEmpty then emptyTab
    else Tab(
      name,
      name,
      Seq(
        div(cls := "member-group-header")(
          h3(tabAttr(name), cls := "h400")(name)
        )
      ) ++ children,
      "expand"
    )

  case class ExpandedGroup(name: AppliedTag, description: AppliedTag, prio: Int)

  val emptyTab = Tab("", "", Nil)

  def grouppedMember(m: Member, membersInGroups: Seq[Member]): Tab =
    if membersInGroups.isEmpty then emptyTab else
      val descriptions = m.docs.map(_.groupDesc).getOrElse(Map.empty)
      val names = m.docs.map(_.groupNames).getOrElse(Map.empty)
      val prios = m.docs.map(_.groupPrio).getOrElse(Map.empty)

      val rawGroups = membersInGroups.groupBy(_.docs.flatMap(_.group)).collect {
          case (Some(groupName), members) =>
            ExpandedGroup(
              names.get(groupName).fold(raw(groupName))(renderDocPart),
              descriptions.get(groupName).fold(raw(""))(renderDocPart),
              prios.getOrElse(groupName, 1000)
            ) -> members
        }
      val content = rawGroups.toSeq.sortBy(_._1.prio).flatMap {
        case (group, members) =>
          Seq(div(cls := "documentableList expand")(
            button(cls := "icon-button show-content expand"),
            h3(cls := "h200")(group.name),
            group.description,
            members.map(member)
          ))
      }
      Tab("Grouped members", "grouped_members",
        Seq(
          div(cls := "member-group-header")(
            h3(tabAttr("grouped_members"), cls := "h400")("Grouped members")
          )
        ) ++ content,
        "expand")

  def buildMembers(s: Member): AppliedTag =
    def partitionIntoGroups(members: Seq[Member]) =
      if summon[DocContext].args.includeGroups then
        members.partition(_.docs.exists(_.group.nonEmpty))
      else
        (Nil, members)

    val (membersInGroups, rest) = partitionIntoGroups(s.members)

    val extensions =
      rest.groupBy{ _.kind match
        case Kind.Extension(on, _) => Some(on)
        case _ => None
      }.collect {
        case (Some(on), members) =>
          val typeSig = SignatureBuilder()
            .keyword("extension ")
            .typeParamList(on.typeParams)
            .content
          val argsSig = SignatureBuilder()
            .functionTermParameters(on.argsLists)
            .content
          val sig = typeSig ++ argsSig
          MGroup(span(cls := "groupHeader")(sig.map(renderElement(_))), members.sortBy(_.name).toSeq, on.name) -> on.position
      }.toSeq.sortBy(_._2).map(_._1)

    div(cls := "membersList expand")(
    renderTabs(
      singleSelection = false,
      Tab("Packages", "packages",
        actualGroup("Packages", rest.filter(m => m.kind == Kind.Package), name => h3(cls := "groupHeader h400")(name), false),
        "expand"),
      grouppedMember(s, membersInGroups),
      buildGroup("Type members", Seq(
        ("Classlikes", rest.filter(m => m.kind.isInstanceOf[Classlike])),
        ("Types", rest.filter(_.kind.isInstanceOf[Kind.Type])),
        ("Enum entries", rest.filter(_.kind.isInstanceOf[Kind.EnumCase])),
      )),
      buildGroup("Value members", Seq(
        ("Constructors", rest.filter(_.kind.isInstanceOf[Kind.Constructor])),
        ("Methods", rest.filter(_.kind.isInstanceOf[Kind.Def])),
        ("Fields", rest.filter(m => m.kind == Kind.Val || m.kind == Kind.Var)),
      )),
      buildGroup("Givens", Seq(
        ("Givens", rest.filter(_.kind.isInstanceOf[Kind.Given])),
      )),
      buildGroup("Extensions", Seq(
        ("Extensions", extensions),
      )),
      buildGroup("Implicits", Seq(
        ("Implicits", rest.filter(_.kind.isInstanceOf[Kind.Implicit])),
      )),
      buildGroup("Exports", Seq(
        ("Defined exports", rest.filter(_.kind.isInstanceOf[Kind.Exported])),
      ))
    ))

  case class Tab(name: String, id: String, content: Seq[AppliedTag], cls: String = "")

  def tabAttr(id: String) = Attr("data-togglable") := id

  private def renderTabs(singleSelection: Boolean, allTabs: Tab*): Seq[AppliedTag] =
    val tabs = allTabs.filter(_.content.nonEmpty)
      if tabs.isEmpty then Nil else
        Seq(div(cls := (if singleSelection then "tabs single" else "tabs"))(
            div(cls := "contents")(tabs.map(t =>
              section(id := t.name.replace(' ', '-'))(
                div(tabAttr(t.id), cls := s"tab ${t.cls}")(t.content)
              )
            ))
        ))

  def classLikeParts(m: Member, full: Boolean = true): TagArg =
    if !m.kind.isInstanceOf[Classlike] then Nil else
      val graphHtml = m.graph match
        case graph if graph.edges.nonEmpty && full =>
          Seq(div( id := "inheritance-diagram", cls := "diagram-class showGraph")(
            button(`type` := "button", cls := "label-only-button", onclick := "zoomOut()")("Reset zoom"),
            button(`type` := "button", cls := "label-only-button", onclick := "hideGraph()")("Hide graph"),
            button(`type` := "button", cls := "label-only-button", onclick := "showGraph()")("Show graph"),
            svg(id := "graph"),
            script(`type` := "text/dot", id := "dot")(
              raw(DotDiagramBuilder.build(graph, signatureRenderer))
            ),
          ))
        case _ => Nil

      def signatureList(list: Seq[LinkToType], className: String = "", expandable: Boolean): Seq[AppliedTag] =
        if list.isEmpty then Nil
         else Seq(div(cls := s"mono-small-block $className")(
         list.map(link =>
          div(link.kind.name," ", link.signature.map(renderElement(_)))),
          if(expandable) then span(cls := "show-all-code show-content text-button h100")("Show all") else span()
        ))

      def selfTypeList(list: List[LinkToType]): Seq[AppliedTag] =
        if list.isEmpty then Nil
        else Seq(
          div(cls := "mono-small-block supertypes")(
            span(),
            list.map { link =>
              div(link.signature.map(renderElement(_)))
            }
        ))

      val supertypes = signatureList(m.parents, "supertypes", m.parents.length > 5)
      val subtypes = signatureList(m.knownChildren, "subtypes", m.knownChildren.length > 5)
      val selfType = selfTypeList(m.selfType.toList)

      Seq(
        "Graph" -> graphHtml,
        "Supertypes" -> supertypes,
        "Known subtypes" -> subtypes,
        "Self type" -> selfType,
      ).filterNot(_._2.isEmpty).flatMap(tableRow(_, _))

  private def renderTable(keyValues: (TagArg, TagArg)*): TagArg =
    table(
      keyValues.map((key, value) => tr(td(key), td(value)))
    )

  private def buildDocumentableFilter = div(cls := "documentableFilter")(
    div(cls := "filtersContainer")(),
    input(cls := "filterableInput", placeholder := "Filter by any phrase", testId := "filterBarInput", `type` := "search"),
    button(cls := "clearButton label-only-button",  testId := "filterBarClearButton")("Clear all"),
  )

  private def companionBadge(m: Member): Seq[AppliedTag] = m.companion.fold(Nil) { companion =>
    Seq(div(cls := "companion-badge body-small")(
      span(
        "See the",
        span(cls := s"micon ${companion._1.name.take(2)}"),
        a(href := link(companion._2).getOrElse(""))(m.name),
        " companion ",
        companion._1.name
      )
    ))
  }

  def fullMember(m: Member): PageContent =
    val wideClass = m.companion.map(_ => "multi").getOrElse("single")
    val intro = m.kind match
      case Kind.RootPackage =>Seq(h1(cls := s"h600")(summon[DocContext].args.name))
      case _ =>
        Seq(
          div(cls := "cover-header")(
            memberIcon(m),
            h1(cls := s"h600 $wideClass")(m.name)
          ),
          div(cls := "fqname body-large")(
            span(m.fullName)
          )
        ) ++ companionBadge(m) ++
        Seq(
          div(cls := "main-signature mono-small-block")(
            annotations(m).getOrElse(Nil),
            memberSignature(m)
          )
        )

    val memberContent = div(
      intro,
      memberInfo(m, full = true),
      if m.members.length > 0 then
        Seq(section(id := "members-list")(
          h2(cls := "h500")("Members list"),
          buildDocumentableFilter,
          buildMembers(m)
        ))
      else Nil
    )

    val memberDocument = Jsoup.parse(memberContent.toString)

    val toc = memberDocument.select("section[id]").asScala.toSeq
      .flatMap { elem =>
        val header = elem.selectFirst("h1, h2, h3, h4, h5, h6")
        Option(header).map { h =>
          TocEntry(h.tag().getName, h.text(), s"#${elem.id()}")
        }
      }

    PageContent(
      memberContent,
      toc
    )




© 2015 - 2024 Weber Informatics LLC | Privacy Policy