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

dotty.tools.scaladoc.tasty.comments.markdown.SnippetRenderer.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 tasty.comments.markdown

import com.vladsch.flexmark.html._
import util.HTML._

import dotty.tools.scaladoc.snippets._

case class SnippetLine(content: String, lineNo: Int, classes: Set[String] = Set.empty, messages: Seq[String] = Seq.empty, attributes: Map[String, String] = Map.empty):
  def withClass(cls: String) = this.copy(classes = classes + cls)
  def withAttribute(name: String, value: String) = this.copy(attributes = attributes.updated(name, value))
  def toHTML =
    val label = if messages.nonEmpty then s"""label="${messages.map(_.escapeReservedTokens).mkString("\n")}"""" else ""
    s"""$content"""

object SnippetRenderer:
  val hiddenStartSymbol = "//{"
  val hiddenEndSymbol = "//}"

  val importedStartSymbol = "//{i"
  val importedEndSymbol = "//i}"
  val importedRegex = """\/\/\{i:(.*)""".r

  private def compileMessageCSSClass(msg: SnippetCompilerMessage) = msg.level match
    case MessageLevel.Info => "snippet-info"
    case MessageLevel.Warning => "snippet-warn"
    case MessageLevel.Error => "snippet-error"
    case MessageLevel.Debug => "snippet-debug"

  private def cutBetweenSymbols[A](
    startSymbol: String,
    endSymbol: String,
    snippetLines: Seq[SnippetLine]
  )(
    f: (Seq[SnippetLine], Seq[SnippetLine], Seq[SnippetLine]) => A
  ): Option[A] =
    for {
      startIdx <- snippetLines.zipWithIndex.find(_._1.content.contains(startSymbol)).map(_._2)
      endIdx <- snippetLines.zipWithIndex.find(_._1.content.contains(endSymbol)).map(_._2)
      (tmp, end) = snippetLines.splitAt(endIdx+1)
      (begin, mid) = tmp.splitAt(startIdx)
    } yield f(begin, mid, end)

  private def wrapImportedSection(snippetLines: Seq[SnippetLine]): Seq[SnippetLine] =
    val mRes = cutBetweenSymbols(importedStartSymbol, importedEndSymbol, snippetLines) {
      case (begin, mid, end) =>
        val name = importedRegex.findFirstMatchIn(mid.head.content).fold("")(_.group(1))
        begin ++ mid.drop(1).dropRight(1).map(_.withClass("hideable").withClass("include").withAttribute("name", name)) ++ wrapImportedSection(end)
    }
    mRes.getOrElse(snippetLines)

  private def wrapHiddenSymbols(snippetLines: Seq[SnippetLine]): Seq[SnippetLine] =
    val mRes = cutBetweenSymbols(hiddenStartSymbol, hiddenEndSymbol, snippetLines) {
      case (begin, mid, end) =>
        begin ++ mid.drop(1).dropRight(1).map(_.withClass("hideable")) ++ wrapHiddenSymbols(end)
    }
    mRes.getOrElse(snippetLines)

  private def wrapCommonIndent(snippetLines: Seq[SnippetLine]): Seq[SnippetLine] =
    val nonHiddenSnippetLines = snippetLines.filter(l => !l.classes.contains("hideable"))
    nonHiddenSnippetLines.headOption.map(_.content.takeWhile(_ == ' ')).map { prefix =>
      val maxCommonIndent = nonHiddenSnippetLines.foldLeft(prefix) { (currPrefix, elem) =>
        if elem.content.startsWith(currPrefix) then currPrefix else elem.content.takeWhile(_ == ' ')
      }
      snippetLines.map { line =>
        if line.classes.contains("hideable") || maxCommonIndent.size == 0 then line
        else line.copy(content = span(cls := "hideable")(maxCommonIndent).toString + line.content.stripPrefix(maxCommonIndent))
      }
    }.getOrElse(snippetLines)

  private def wrapLineInBetween(startSymbol: Option[String], endSymbol: Option[String], line: SnippetLine): SnippetLine =
    val startIdx = startSymbol.map(s => line.content.indexOf(s))
    val endIdx = endSymbol.map(s => line.content.indexOf(s))
    (startIdx, endIdx) match
      case (Some(idx), None) =>
        val (code, comment) = line.content.splitAt(idx)
        comment match
          case _ if code.forall(_.isWhitespace) =>
            line.withClass("hideable")
          case _ if comment.last == '\n' =>
            line.copy(content = code + s"""${comment.dropRight(1)}${"\n"}""")
          case _  =>
            line.copy(content = code + s"""$comment""")
      case (None, Some(idx)) =>
        val (comment, code) = line.content.splitAt(idx+endSymbol.get.size)
        comment match
          case _ if code.forall(_.isWhitespace) =>
            line.withClass("hideable")
          case _ =>
            line.copy(content = s"""$comment""" + code)
      case (Some(startIdx), Some(endIdx)) =>
        val (tmp, end) = line.content.splitAt(endIdx+endSymbol.get.size)
        val (begin, comment) = tmp.splitAt(startIdx)
        line.copy(content = begin + s"""$comment""" + end)
      case _ => line

  private def reindexLines(lines: Seq[SnippetLine]) =
    lines.zipWithIndex.map {
      case (line, newIdx) => line.copy(lineNo = newIdx)
    }

  private def wrapCodeLines(codeLines: Seq[String]): Seq[SnippetLine] =
    val snippetLines = codeLines.zipWithIndex.map {
      case (content, idx) => SnippetLine(content.escapeReservedTokens, idx)
    }
    wrapImportedSection
      .andThen(wrapHiddenSymbols)
      .andThen(wrapCommonIndent)
      .apply(snippetLines)

  private def addCompileMessages(messages: Seq[SnippetCompilerMessage])(codeLines: Seq[SnippetLine]): Seq[SnippetLine] =
    val messagesDict = messages.filter(_.position.nonEmpty).groupBy(_.position.get.relativeLine).toMap[Int, Seq[SnippetCompilerMessage]]
    codeLines.map { line =>
      messagesDict.get(line.lineNo) match
        case None => line
        case Some(messages) =>
          val classes = List(
            messages.find(_.level == MessageLevel.Error).map(compileMessageCSSClass),
            messages.find(_.level == MessageLevel.Warning).map(compileMessageCSSClass),
            messages.find(_.level == MessageLevel.Info).map(compileMessageCSSClass)
          ).flatten
          line.copy(classes = line.classes ++ classes.toSet ++ Set("tooltip"), messages = messages.map(_.message))
    }

  private def messagesHTML(messages: Seq[SnippetCompilerMessage]): String =
    if messages.isEmpty
      then ""
      else
        val content = messages
          .map { msg =>
            s"""${msg.message}"""
          }
          .mkString("
") s"""
$content""" private def snippetLabel(name: String): String = div(cls := "snippet-meta")( div(cls := "snippet-label")(name) ).toString def renderSnippetWithMessages(snippetName: Option[String], codeLines: Seq[String], messages: Seq[SnippetCompilerMessage], success: Boolean): String = val transformedLines = wrapCodeLines.andThen(addCompileMessages(messages)).andThen(reindexLines).apply(codeLines).map(_.toHTML) val codeHTML = s"""${transformedLines.mkString("")}""" val isRunnable = success val attrs = Seq( Option.when(isRunnable)(Attr("runnable") := "") ).flatten div(cls := "snippet mono-small-block", Attr("scala-snippet") := "", attrs)( pre( raw(codeHTML) ), raw(snippetName.fold("")(snippetLabel(_))), div(cls := "buttons")() ).toString def renderSnippetWithMessages(node: ExtendedFencedCodeBlock): String = renderSnippetWithMessages( node.name, node.codeBlock.getContentChars.toString.split("\n").map(_ + "\n").toSeq, node.compilationResult.toSeq.flatMap(_.messages), node.compilationResult.fold(false)(_.isSuccessful) ) def renderSnippet(content: String, language: Option[String] = None): String = val codeLines = content.split("\n").map(_ + "\n").toSeq div(cls := "snippet mono-small-block")( pre( code(language.fold(Nil)(l => Seq(cls := s"language-$l")))( raw(wrapCodeLines(codeLines).map(_.toHTML).mkString) ) ), div(cls := "buttons")() ).toString




© 2015 - 2024 Weber Informatics LLC | Privacy Policy