diff --git a/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala b/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala
index 8b6cd6ea5a0d..d3c334599666 100644
--- a/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala
+++ b/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala
@@ -37,7 +37,7 @@ class StoreReporter(outer: Reporter = Reporter.NoReporter) extends Reporter {
if (infos != null) try infos.toList finally infos = null
else Nil
- override def pendingMessages(using Context): List[Diagnostic] = infos.toList
+ override def pendingMessages(using Context): List[Diagnostic] = if (infos != null) infos.toList else Nil
override def errorsReported: Boolean = hasErrors || (outer != null && outer.errorsReported)
}
diff --git a/dist/bin/scaladoc b/dist/bin/scaladoc
index e92e21d6e49a..f09bf27812c0 100755
--- a/dist/bin/scaladoc
+++ b/dist/bin/scaladoc
@@ -127,6 +127,7 @@ eval "\"$JAVACMD\"" \
${JAVA_OPTS:-$default_java_opts} \
"${java_args[@]}" \
"${jvm_cp_args-}" \
+ -Dscala.usejavacp=true \
"dotty.tools.scaladoc.Main" \
"${scala_args[@]}" \
"${residual_args[@]}" \
diff --git a/docs/css/dottydoc.css b/docs/css/dottydoc.css
index caeb967d6fd1..d2520888120a 100644
--- a/docs/css/dottydoc.css
+++ b/docs/css/dottydoc.css
@@ -180,18 +180,6 @@ h5:hover a.anchor:hover {
font-family: var(--font-family-monospace);
}
-/* code */
-pre, code {
- font-variant-ligatures: none;
-}
-pre {
- padding: 0;
- font-size: 13px;
- background: var(--pre-bg);
- border-radius: 2px;
- border: 1px solid rgba(0, 0, 0, 0.1);
-}
-
/* admonitions */
blockquote {
padding: 0 1em;
diff --git a/project/Build.scala b/project/Build.scala
index c0f25d09694c..52ff8f8979e5 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -1243,16 +1243,19 @@ object Build {
libraryDependencies += ("org.scala-js" %%% "scalajs-dom" % "1.1.0").cross(CrossVersion.for3Use2_13)
)
- def generateDocumentation(targets: Seq[String], name: String, outDir: String, ref: String, params: Seq[String] = Nil) =
+ def generateDocumentation(targets: Seq[String], name: String, outDir: String, ref: String, params: Seq[String] = Nil, usingScript: Boolean = true) =
Def.taskDyn {
val distLocation = (dist / pack).value
val projectVersion = version.value
IO.createDirectory(file(outDir))
val stdLibVersion = stdlibVersion(Bootstrapped)
+ val scalaLib = findArtifactPath(externalCompilerClasspathTask.value, "scala-library")
+ val dottyLib = (`scala3-library` / Compile / classDirectory).value
// TODO add versions etc.
def srcManaged(v: String, s: String) = s"out/bootstrap/stdlib-bootstrapped/scala-$v/src_managed/main/$s-library-src"
def scalaSrcLink(v: String, s: String) = s"-source-links:$s=github://scala/scala/v$v#src/library"
def dottySrcLink(v: String, s: String) = s"-source-links:$s=github://lampepfl/dotty/$v#library/src"
+
val revision = Seq("-revision", ref, "-project-version", projectVersion)
val cmd = Seq(
"-d",
@@ -1264,7 +1267,20 @@ object Build {
s"-source-links:github://lampepfl/dotty/$referenceVersion",
) ++ scalacOptionsDocSettings ++ revision ++ params ++ targets
import _root_.scala.sys.process._
- Def.task((s"$distLocation/bin/scaladoc" +: cmd).!)
+ if (usingScript)
+ Def.task((s"$distLocation/bin/scaladoc" +: cmd).!)
+ else {
+ val escapedCmd = cmd.map(arg => if(arg.contains(" ")) s""""$arg"""" else arg)
+ Def.task {
+ try {
+ (Compile / run).toTask(escapedCmd.mkString(" ", " ", "")).value
+ 0
+ } catch {
+ case _ : Throwable => 1
+ }
+ }
+ }
+
}
val SourceLinksIntegrationTest = config("sourceLinksIntegrationTest") extend Test
@@ -1311,7 +1327,7 @@ object Build {
generateSelfDocumentation := Def.taskDyn {
generateDocumentation(
(Compile / classDirectory).value.getAbsolutePath :: Nil,
- "scaladoc", "scaladoc/output/self", VersionUtil.gitHash,
+ "scaladoc", "scaladoc/output/self", VersionUtil.gitHash
)
}.value,
generateScalaDocumentation := Def.inputTaskDyn {
@@ -1352,7 +1368,7 @@ object Build {
s"-source-links:docs=github://lampepfl/dotty/master#docs",
"-doc-root-content", docRootFile.toString,
"-Ydocument-synthetic-types"
- )
+ ), usingScript = false
))
}.evaluated,
@@ -1361,7 +1377,8 @@ object Build {
(Test / Build.testcasesOutputDir).value,
"scaladoc testcases",
"scaladoc/output/testcases",
- "master")
+ "master"
+ )
}.value,
Test / buildInfoKeys := Seq[BuildInfoKey](
diff --git a/scaladoc-js/resources/scaladoc-searchbar.css b/scaladoc-js/resources/scaladoc-searchbar.css
index 035adbee6555..45f3b42080c8 100644
--- a/scaladoc-js/resources/scaladoc-searchbar.css
+++ b/scaladoc-js/resources/scaladoc-searchbar.css
@@ -100,5 +100,38 @@
.pull-right {
float: right;
- margin-left: auto
+ margin-left: auto;
}
+
+.snippet-comment-button {
+ position: absolute;
+ display: inline-block;
+ left: 50%;
+ width: 24px;
+ height: 24px;
+ background:
+ linear-gradient(#fff, #fff),
+ linear-gradient(#fff, #fff),
+ #aaa;
+ background-position: center;
+ background-size: 50% 2px, 2px 50%;
+ background-repeat: no-repeat;
+ border-radius: 12px;
+ box-shadow: 0 0 2px black;
+}
+
+.snippet-comment-button:hover {
+ background:
+ linear-gradient(#444, #444),
+ linear-gradient(#444, #444),
+ #ddd;
+ background-position: center;
+ background-size: 50% 2px, 2px 50%;
+ background-repeat: no-repeat;
+}
+
+.hide-snippet-comments-button {
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+
diff --git a/scaladoc-js/src/Main.scala b/scaladoc-js/src/Main.scala
index 7b32aed9a1ea..c31ad58568fa 100644
--- a/scaladoc-js/src/Main.scala
+++ b/scaladoc-js/src/Main.scala
@@ -3,4 +3,5 @@ package dotty.tools.scaladoc
object Main extends App {
Searchbar()
SocialLinks()
+ CodeSnippets()
}
diff --git a/scaladoc-js/src/searchbar/code-snippets/CodeSnippets.scala b/scaladoc-js/src/searchbar/code-snippets/CodeSnippets.scala
new file mode 100644
index 000000000000..50f0ad2d5df7
--- /dev/null
+++ b/scaladoc-js/src/searchbar/code-snippets/CodeSnippets.scala
@@ -0,0 +1,27 @@
+package dotty.tools.scaladoc
+
+import org.scalajs.dom._
+import org.scalajs.dom.ext._
+
+class CodeSnippets:
+ def toggleHide(e: html.Element | html.Document) = e.querySelectorAll("code span.hideable").foreach {
+ case e: html.Element if e.style.getPropertyValue("display").isEmpty => e.style.setProperty("display", "none")
+ case e: html.Element => e.style.removeProperty("display")
+ }
+
+ toggleHide(document)
+
+ document.querySelectorAll("pre").foreach {
+ case e: html.Element if e.querySelectorAll("code span.hideable").nonEmpty =>
+ val a = document.createElement("a")
+ a.addEventListener("click", { (_: MouseEvent) =>
+ if(a.classList.contains("hide-snippet-comments-button")) {
+ a.classList.remove("hide-snippet-comments-button")
+ } else {
+ a.classList.add("hide-snippet-comments-button")
+ }
+ toggleHide(e)
+ })
+ a.classList.add("snippet-comment-button")
+ e.insertBefore(a, e.firstChild)
+ }
diff --git a/scaladoc-testcases/docs/docs/index.md b/scaladoc-testcases/docs/docs/index.md
new file mode 100644
index 000000000000..69036cd254a4
--- /dev/null
+++ b/scaladoc-testcases/docs/docs/index.md
@@ -0,0 +1,13 @@
+---
+
+
+---
+
+```scala sc:compile
+2 + List(0)
+```
+
+```scala sc:compile
+new snippetCompiler.Snippet0 { }
+```
+
diff --git a/scaladoc-testcases/src/tests/snippetComments.scala b/scaladoc-testcases/src/tests/snippetComments.scala
new file mode 100644
index 000000000000..39b15648103e
--- /dev/null
+++ b/scaladoc-testcases/src/tests/snippetComments.scala
@@ -0,0 +1,31 @@
+package tests.snippetComments
+
+
+/**
+ * This is my codeblock
+ *
+ * ```
+ * //{{
+ * import xd
+ * import xd2
+ * //}}
+ *
+ *
+ * val x = 1 // This is my valid comment
+ *
+ * /*
+ * multi line comment
+ * */
+ * val reallyFLongDeclaration = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ * val y = 2 // comment in the same line
+ * // comment in new line
+ * val z = 3
+ *
+ * //{{
+ * val hideMe = 7
+ * //}}
+ * ```
+ *
+ * The end of my codeblock
+ */
+class A
diff --git a/scaladoc-testcases/src/tests/snippetCompilerTests.scala b/scaladoc-testcases/src/tests/snippetCompilerTests.scala
new file mode 100644
index 000000000000..85d4db4ee0a2
--- /dev/null
+++ b/scaladoc-testcases/src/tests/snippetCompilerTests.scala
@@ -0,0 +1,68 @@
+package tests
+package snippetCompilerTests
+
+/**
+ * ```scala sc:compile
+ * //{
+ * import scala.collection.Seq
+ * //}
+ *
+ * def a = 2
+ * val x = 1 + List()
+ * a
+ *
+ * try {
+ * 2+3
+ * }
+ *
+ * /*
+ * Huge comment
+ * */
+ * val xd: String = 42
+ *
+ * def a(i: Int): Boolean = i match // This is a function
+ * case 1 => true
+ *
+ * val b: Int = 2.3 /* Also quite a big comment */
+ *
+ * val d: Long = "asd"
+ * ```
+ *
+ * ```scala sc:fail
+ * def a = 2
+ * val x = 1 + List()
+ * a
+ * ```
+ *
+ * ```scala sc:fail
+ * def a = 2
+ * ```
+ *
+ * ```scala sc:nocompile
+ * def a = 3
+ * a()
+ * ```
+ */
+class A {
+ trait B
+}
+
+/**
+ * ```scala sc:compile
+ * val c: Int = 4.5
+ * ```
+ */
+class B { }
+
+trait Quotes {
+ val reflect: reflectModule = ???
+ trait reflectModule { self: reflect.type =>
+ /**
+ * ```scala sc:compile
+ * 2 + List()
+ * ```
+ *
+ */
+ def a = 3
+ }
+}
\ No newline at end of file
diff --git a/scaladoc-testcases/src/tests/snippetTestcase1.scala b/scaladoc-testcases/src/tests/snippetTestcase1.scala
new file mode 100644
index 000000000000..b0907f983946
--- /dev/null
+++ b/scaladoc-testcases/src/tests/snippetTestcase1.scala
@@ -0,0 +1,12 @@
+package tests.snippetTestcase1
+
+class SnippetTestcase1:
+ /**
+ * SNIPPET(OUTERLINEOFFSET:8,OUTERCOLUMNOFFSET:6,INNERLINEOFFSET:3,INNERCOLUMNOFFSET:2)
+ * ERROR(LINE:8,COLUMN:8)
+ * ```scala sc:compile
+ * 2 + List()
+ * ```
+ *
+ */
+ def a = 3
\ No newline at end of file
diff --git a/scaladoc-testcases/src/tests/snippetTestcase2.scala b/scaladoc-testcases/src/tests/snippetTestcase2.scala
new file mode 100644
index 000000000000..8e3fd0f33744
--- /dev/null
+++ b/scaladoc-testcases/src/tests/snippetTestcase2.scala
@@ -0,0 +1,58 @@
+package tests
+package snippetTestcase2
+
+trait Quotes2[A] {
+ val r1: r1Module[_] = ???
+ trait r1Module[A] {
+ type X
+ object Y {
+ /**
+ * SNIPPET(OUTERLINEOFFSET:13,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:6,INNERCOLUMNOFFSET:6)
+ * ERROR(LINE:13,COLUMN:12)
+ * ```scala sc:compile
+ * 2 + List()
+ * ```
+ *
+ */
+ type YY
+ }
+ val z: zModule = ???
+ trait zModule {
+ /**
+ * SNIPPET(OUTERLINEOFFSET:25,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:7,INNERCOLUMNOFFSET:6)
+ * ERROR(LINE:25,COLUMN:12)
+ * ```scala sc:compile
+ * 2 + List()
+ * ```
+ *
+ */
+ type ZZ
+ }
+ }
+ object r2 {
+ type X
+ object Y {
+ /**
+ * SNIPPET(OUTERLINEOFFSET:39,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:5,INNERCOLUMNOFFSET:6)
+ * ERROR(LINE:39,COLUMN:12)
+ * ```scala sc:compile
+ * 2 + List()
+ * ```
+ *
+ */
+ type YY
+ }
+ val z: zModule = ???
+ trait zModule {
+ /**
+ * SNIPPET(OUTERLINEOFFSET:51,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:6,INNERCOLUMNOFFSET:6)
+ * ERROR(LINE:51,COLUMN:12)
+ * ```scala sc:compile
+ * 2 + List()
+ * ```
+ *
+ */
+ type ZZ
+ }
+ }
+}
\ No newline at end of file
diff --git a/scaladoc/resources/dotty_res/styles/scalastyle.css b/scaladoc/resources/dotty_res/styles/scalastyle.css
index dda4e5f08fd1..c32f351bb82d 100644
--- a/scaladoc/resources/dotty_res/styles/scalastyle.css
+++ b/scaladoc/resources/dotty_res/styles/scalastyle.css
@@ -5,7 +5,9 @@
--border-light: #DADFE6;
--border-medium: #abc;
+ --body-bg: #f0f3f6;
--code-bg: #F4F5FA;
+ --documentable-bg: #FFFFFF;
--symbol-fg: #333;
--link-fg: #00607D;
--link-hover-fg: #00A0D0;
@@ -45,6 +47,7 @@ body {
padding: 0;
font-family: "Lato", sans-serif;
font-size: 16px;
+ background-color: var(--body-bg);
}
/* Page layout */
@@ -75,7 +78,12 @@ h1, h2, h3 {
font-family: var(--title-font);
color: var(--title-fg);
}
-pre, code, .monospace, .hljs {
+.monospace {
+ font-family: var(--mono-font);
+ background: var(--documentable-bg);
+ font-variant-ligatures: none;
+}
+pre, code, .hljs {
font-family: var(--mono-font);
background: var(--code-bg);
font-variant-ligatures: none;
@@ -84,12 +92,49 @@ code {
font-size: .8em;
padding: 0 .3em;
}
+pre {
+ overflow: visible;
+ scrollbar-width: thin;
+ margin: 0px;
+}
pre code, pre code.hljs {
font-size: 1em;
padding: 0;
}
+.snippet {
+ padding: 12px 8px 10px 12px;
+ background: var(--code-bg);
+ margin: 1em 0px;
+ border-radius: 2px;
+ box-shadow: 0 0 2px #888;
+}
+.snippet-error {
+ border-bottom: 2px dotted red;
+}
+.snippet-warn {
+ border-bottom: 2px dotted orange;
+}
+.snippet-info {
+ border-bottom: 2px dotted teal;
+}
+.snippet-debug {
+ border-bottom: 2px dotted pink;
+}
+.tooltip {
+ position: relative;
+}
+.tooltip:hover:after {
+ content: attr(label);
+ padding: 4px 8px;
+ color: white;
+ background-color:black;
+ position: absolute;
+ left: 0;
+ z-index:10;
+ box-shadow:0 0 3px #444;
+ opacity: 0.8;
+}
pre, .symbol.monospace {
- padding: 10px 8px 10px 12px;
font-weight: 500;
font-size: 12px;
}
@@ -97,6 +142,9 @@ pre .hljs-comment {
/* Fold comments in snippets */
white-space: normal;
}
+.symbol.monospace {
+ padding: 12px 8px 10px 12px;
+}
a, a:visited, span[data-unresolved-link] {
text-decoration: none;
color: var(--link-fg);
@@ -472,12 +520,18 @@ footer .pull-right {
padding-right: 0.5em;
min-width: 10em;
max-width: 10em;
+ width: 10em;
overflow: hidden;
direction: rtl;
white-space: nowrap;
text-indent: 0em;
}
+.documentableElement .docs {
+ width: 100%;
+ table-layout: fixed;
+}
+
.documentableElement .modifiers .other-modifiers {
color: gray;
}
@@ -518,8 +572,8 @@ footer .pull-right {
padding: 5px 4px 5px 4px;
font-weight: 500;
font-size: 12px;
- background: var(--code-bg);
- border: 0.25em solid white;
+ background: var(--documentable-bg);
+ border: 0.25em solid var(--body-bg);
}
.documentableElement>div {
@@ -528,12 +582,11 @@ footer .pull-right {
.expand.documentableElement>div {
display: block;
- padding-left: 3em;
}
.expand.documentableElement>div.header {
display: block;
- padding-left: 7.5em;
+ padding-left: 4.5em;
text-indent: -4.5em;
}
diff --git a/scaladoc/src/dotty/tools/scaladoc/DocContext.scala b/scaladoc/src/dotty/tools/scaladoc/DocContext.scala
index 9f44069fece1..ee1444effc4a 100644
--- a/scaladoc/src/dotty/tools/scaladoc/DocContext.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/DocContext.scala
@@ -73,10 +73,18 @@ case class NavigationNode(name: String, dri: DRI, nested: Seq[NavigationNode])
case class DocContext(args: Scaladoc.Args, compilerContext: CompilerContext):
lazy val sourceLinks = SourceLinks.load(args.sourceLinks, args.revision)(using compilerContext)
+
+ lazy val snippetCompilerArgs = snippets.SnippetCompilerArgs.load(args.snippetCompiler, args.snippetCompilerDebug)(using compilerContext)
+
+ lazy val snippetChecker = snippets.SnippetChecker(args)(using compilerContext)
+
lazy val staticSiteContext = args.docsRoot.map(path => StaticSiteContext(
File(path).getAbsoluteFile(),
args,
- sourceLinks
+ sourceLinks,
+ snippetCompilerArgs,
+ snippetChecker
)(using compilerContext))
+
val externalDocumentationLinks = args.externalMappings
diff --git a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala
index 3b9c8e9b9624..44dcb15b65b2 100644
--- a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala
@@ -31,6 +31,7 @@ object Scaladoc:
tastyDirs: Seq[File] = Nil,
tastyFiles: Seq[File] = Nil,
classpath: String = "",
+ bootclasspath: String = "",
output: File,
docsRoot: Option[String] = None,
projectVersion: Option[String] = None,
@@ -49,6 +50,8 @@ object Scaladoc:
includePrivateAPI: Boolean = false,
docCanonicalBaseUrl: String = "",
documentSyntheticTypes: Boolean = false,
+ snippetCompiler: List[String] = Nil,
+ snippetCompilerDebug: Boolean = false
)
def run(args: Array[String], rootContext: CompilerContext): Reporter =
@@ -65,7 +68,7 @@ object Scaladoc:
val tastyFiles = parsedArgs.tastyFiles ++ parsedArgs.tastyDirs.flatMap(listTastyFiles)
if !ctx.reporter.hasErrors then
- val updatedArgs = parsedArgs.copy(tastyDirs = Nil, tastyFiles = tastyFiles)
+ val updatedArgs = parsedArgs.copy(tastyDirs = parsedArgs.tastyDirs, tastyFiles = tastyFiles)
if (parsedArgs.output.exists()) util.IO.delete(parsedArgs.output)
@@ -172,6 +175,7 @@ object Scaladoc:
dirs,
validFiles,
classpath.get,
+ bootclasspath.get,
destFile,
siteRoot.nonDefault,
projectVersion.nonDefault,
@@ -189,7 +193,9 @@ object Scaladoc:
groups.get,
visibilityPrivate.get,
docCanonicalBaseUrl.get,
- YdocumentSyntheticTypes.get
+ YdocumentSyntheticTypes.get,
+ snippetCompiler.get,
+ snippetCompilerDebug.get
)
(Some(docArgs), newContext)
}
diff --git a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala
index 8320a5d6c2f7..ec326bf57178 100644
--- a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala
@@ -19,13 +19,14 @@ import dotty.tools.dotc.core.Contexts._
class ScaladocSettings extends SettingGroup with AllScalaSettings:
val unsupportedSettings = Seq(
// Options that we like to support
- bootclasspath, extdirs, javabootclasspath, encoding, usejavacp,
+ extdirs, javabootclasspath, encoding,
// Needed for plugin architecture
plugin,disable,require, pluginsDir, pluginOptions,
// we need support for sourcepath and sourceroot
sourcepath, sourceroot
)
+
val projectName: Setting[String] =
StringSetting("-project", "project title", "The name of the project.", "", aliases = List("-doc-title"))
@@ -94,3 +95,12 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings:
val YdocumentSyntheticTypes: Setting[Boolean] =
BooleanSetting("-Ydocument-synthetic-types", "Documents intrinsic types e. g. Any, Nothing. Setting is useful only for stdlib", false)
+
+ val snippetCompiler: Setting[List[String]] =
+ MultiStringSetting("-snippet-compiler", "snippet-compiler", snippets.SnippetCompilerArgs.usage)
+
+ val snippetCompilerDebug: Setting[Boolean] =
+ BooleanSetting("-Ysnippet-compiler-debug", snippets.SnippetCompilerArgs.debugUsage, false)
+
+ def scaladocSpecificSettings: Set[Setting[_]] =
+ Set(sourceLinks, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, snippetCompilerDebug)
diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala
index 17e1207324ea..91f0eb3cc5b4 100644
--- a/scaladoc/src/dotty/tools/scaladoc/api.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/api.scala
@@ -232,4 +232,15 @@ extension (s: Signature)
case l: Link => l.name
}.mkString
-case class TastyMemberSource(val path: java.nio.file.Path, val lineNumber: Int)
+case class TastyMemberSource(path: java.nio.file.Path, lineNumber: Int)
+
+object SnippetCompilerData:
+ case class Position(line: Int, column: Int)
+ case class ClassInfo(tpe: Option[String], names: Seq[String], generics: Option[String])
+
+case class SnippetCompilerData(
+ packageName: String,
+ classInfos: Seq[SnippetCompilerData.ClassInfo],
+ imports: List[String],
+ position: SnippetCompilerData.Position
+)
diff --git a/scaladoc/src/dotty/tools/scaladoc/site/LoadedTemplate.scala b/scaladoc/src/dotty/tools/scaladoc/site/LoadedTemplate.scala
index beda1eb14676..590a016a25d3 100644
--- a/scaladoc/src/dotty/tools/scaladoc/site/LoadedTemplate.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/site/LoadedTemplate.scala
@@ -51,4 +51,4 @@ case class LoadedTemplate(
("site" -> (getMap("site") + ("posts" -> posts))) + ("urls" -> sourceLinks.toMap) +
("page" -> (getMap("page") + ("title" -> templateFile.title)))
- templateFile.resolveInner(RenderingContext(updatedSettings, ctx.layouts))
+ templateFile.resolveInner(RenderingContext(updatedSettings, ctx.layouts))(using ctx)
diff --git a/scaladoc/src/dotty/tools/scaladoc/site/StaticSiteContext.scala b/scaladoc/src/dotty/tools/scaladoc/site/StaticSiteContext.scala
index 8a2d60036e6c..010ecfeec052 100644
--- a/scaladoc/src/dotty/tools/scaladoc/site/StaticSiteContext.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/site/StaticSiteContext.scala
@@ -13,7 +13,9 @@ import collection.JavaConverters._
class StaticSiteContext(
val root: File,
val args: Scaladoc.Args,
- val sourceLinks: SourceLinks)(using val outerCtx: CompilerContext):
+ val sourceLinks: SourceLinks,
+ val snippetCompilerArgs: snippets.SnippetCompilerArgs,
+ val snippetChecker: snippets.SnippetChecker)(using val outerCtx: CompilerContext):
var memberLinkResolver: String => Option[DRI] = _ => None
diff --git a/scaladoc/src/dotty/tools/scaladoc/site/common.scala b/scaladoc/src/dotty/tools/scaladoc/site/common.scala
index 8cc576ad82d4..49c4621a90dc 100644
--- a/scaladoc/src/dotty/tools/scaladoc/site/common.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/site/common.scala
@@ -36,7 +36,8 @@ val defaultMarkdownOptions: DataHolder =
EmojiExtension.create(),
YamlFrontMatterExtension.create(),
StrikethroughExtension.create(),
- WikiLinkExtension.create()
+ WikiLinkExtension.create(),
+ tasty.comments.markdown.SnippetRenderingExtension
))
def emptyTemplate(file: File, title: String): TemplateFile = TemplateFile(
@@ -48,7 +49,8 @@ def emptyTemplate(file: File, title: String): TemplateFile = TemplateFile(
title = title,
hasFrame = true,
resources = List.empty,
- layout = None
+ layout = None,
+ configOffset = 0
)
final val ConfigSeparator = "---"
@@ -106,6 +108,7 @@ def loadTemplateFile(file: File): TemplateFile = {
title = stringSetting(allSettings, "title").getOrElse(name),
hasFrame = !stringSetting(allSettings, "hasFrame").contains("false"),
resources = (listSetting(allSettings, "extraCSS") ++ listSetting(allSettings, "extraJS")).flatten.toList,
- layout = stringSetting(allSettings, "layout")
+ layout = stringSetting(allSettings, "layout"),
+ configOffset = config.size
)
}
diff --git a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala
index 5ff9fd161746..d832659ab4bd 100644
--- a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala
@@ -2,7 +2,7 @@ package dotty.tools.scaladoc
package site
import java.io.File
-import java.nio.file.Files
+import java.nio.file.{Files, Paths}
import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
@@ -18,6 +18,7 @@ import liqp.Template
import scala.collection.JavaConverters._
import scala.io.Source
+import dotty.tools.scaladoc.snippets._
case class RenderingContext(
properties: Map[String, Object],
@@ -52,10 +53,32 @@ case class TemplateFile(
hasFrame: Boolean,
resources: List[String],
layout: Option[String],
+ configOffset: Int
):
def isIndexPage() = file.isFile && (file.getName == "index.md" || file.getName == "index.html")
- private[site] def resolveInner(ctx: RenderingContext): ResolvedPage =
+ private[site] def resolveInner(ctx: RenderingContext)(using ssctx: StaticSiteContext): ResolvedPage =
+
+ lazy val snippetCheckingFunc: SnippetChecker.SnippetCheckingFunc =
+ val path = Some(Paths.get(file.getAbsolutePath))
+ val pathBasedArg = ssctx.snippetCompilerArgs.get(path)
+ (str: String, lineOffset: SnippetChecker.LineOffset, argOverride: Option[SCFlags]) => {
+ val arg = argOverride.fold(pathBasedArg)(pathBasedArg.overrideFlag(_))
+ val compilerData = SnippetCompilerData(
+ "staticsitesnippet",
+ Seq(SnippetCompilerData.ClassInfo(None, Nil, None)),
+ Nil,
+ SnippetCompilerData.Position(configOffset - 1, 0)
+ )
+ ssctx.snippetChecker.checkSnippet(str, Some(compilerData), arg, lineOffset).collect {
+ case r: SnippetCompilationResult if !r.isSuccessful =>
+ val msg = s"In static site (${file.getAbsolutePath}):\n${r.getSummary}"
+ report.error(msg)(using ssctx.outerCtx)
+ r
+ case r => r
+ }
+ }
+
if (ctx.resolving.contains(file.getAbsolutePath))
throw new RuntimeException(s"Cycle in templates involving $file: ${ctx.resolving}")
@@ -74,9 +97,13 @@ case class TemplateFile(
val rendered = Template.parse(this.rawCode).render(mutableProperties)
// We want to render markdown only if next template is html
val code = if (isHtml || layoutTemplate.exists(!_.isHtml)) rendered else
+ // Snippet compiler currently supports markdown only
val parser: Parser = Parser.builder(defaultMarkdownOptions).build()
- HtmlRenderer.builder(defaultMarkdownOptions).build().render(parser.parse(rendered))
+ val parsedMd = parser.parse(rendered)
+ val processed = FlexmarkSnippetProcessor.processSnippets(parsedMd, ssctx.snippetCompilerArgs.debug, snippetCheckingFunc)(using ssctx.outerCtx)
+ HtmlRenderer.builder(defaultMarkdownOptions).build().render(processed)
+
layoutTemplate match
case None => ResolvedPage(code, resources ++ ctx.resources)
case Some(layoutTemplate) =>
- layoutTemplate.resolveInner(ctx.nest(code, file, resources))
+ layoutTemplate.resolveInner(ctx.nest(code, file, resources))
\ No newline at end of file
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala
new file mode 100644
index 000000000000..eb67e2e1fc2a
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala
@@ -0,0 +1,57 @@
+package dotty.tools.scaladoc
+package snippets
+
+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.options.MutableDataSet
+import collection.JavaConverters._
+
+import dotty.tools.scaladoc.tasty.comments.markdown.ExtendedFencedCodeBlock
+
+object FlexmarkSnippetProcessor:
+ def processSnippets(root: mdu.Node, debug: Boolean, checkingFunc: => SnippetChecker.SnippetCheckingFunc)(using CompilerContext): mdu.Node = {
+ lazy val cf: SnippetChecker.SnippetCheckingFunc = checkingFunc
+
+ val nodes = root.getDescendants().asScala.collect {
+ case fcb: mda.FencedCodeBlock => fcb
+ }.toList
+
+ nodes.foreach { node =>
+ val snippet = node.getContentChars.toString
+ val lineOffset = node.getStartLineNumber
+ val info = node.getInfo.toString.split(" ")
+ if info.contains("scala") then {
+ val argOverride =
+ info
+ .find(_.startsWith("sc:"))
+ .map(_.stripPrefix("sc:"))
+ .map(SCFlagsParser.parse)
+ .flatMap(_ match {
+ case Right(flags) => Some(flags)
+ case Left(error) =>
+ report.warning(
+ s"""|Error occured during parsing flags in snippet:
+ |$error""".stripMargin
+ )
+ None
+ })
+ val snippetCompilationResult = cf(snippet, lineOffset, argOverride) match {
+ case result@Some(SnippetCompilationResult(wrapped, _, _, _)) if debug =>
+ val s = sequence.BasedSequence.EmptyBasedSequence()
+ .append(wrapped.snippet)
+ .append(sequence.BasedSequence.EOL)
+ val content = mdu.BlockContent()
+ content.add(s, 0)
+ node.setContent(content)
+ result
+ case result => result
+ }
+
+ node.insertBefore(new ExtendedFencedCodeBlock(node, snippetCompilationResult))
+ node.unlink()
+ }
+ }
+
+ root
+ }
\ No newline at end of file
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SelfTypePrinter.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SelfTypePrinter.scala
new file mode 100644
index 000000000000..1f51a29243a2
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SelfTypePrinter.scala
@@ -0,0 +1,42 @@
+package dotty.tools.scaladoc
+package snippets
+
+import dotty.tools.dotc.printing.RefinedPrinter
+import dotty.tools.dotc.core._
+import dotty.tools.dotc.printing.Texts._
+import dotty.tools.dotc.core.Types._
+import dotty.tools.dotc.core.Flags._
+import dotty.tools.dotc.core.Names._
+import dotty.tools.dotc.core.Symbols._
+import dotty.tools.dotc.core.NameOps._
+import dotty.tools.dotc.core.TypeErasure.ErasedValueType
+import dotty.tools.dotc.core.Contexts._
+import dotty.tools.dotc.core.Annotations.Annotation
+import dotty.tools.dotc.core.Denotations._
+import dotty.tools.dotc.core.SymDenotations._
+import dotty.tools.dotc.core.StdNames.{nme, tpnme}
+import dotty.tools.dotc.ast.{Trees, untpd}
+import dotty.tools.dotc.typer.{Implicits, Namer, Applications}
+import dotty.tools.dotc.typer.ProtoTypes._
+import dotty.tools.dotc.ast.Trees._
+import dotty.tools.dotc.core.TypeApplications._
+import dotty.tools.dotc.core.Decorators._
+import dotty.tools.dotc.util.Chars.isOperatorPart
+import dotty.tools.dotc.transform.TypeUtils._
+import dotty.tools.dotc.transform.SymUtils._
+
+import language.implicitConversions
+import dotty.tools.dotc.util.{NameTransformer, SourcePosition}
+import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef}
+
+class SelfTypePrinter(using _ctx: Context) extends RefinedPrinter(_ctx):
+
+ override def toTextSingleton(tp: SingletonType): Text =
+ tp match
+ case ConstantType(value) =>
+ if value.tag == Constants.ByteTag || value.tag == Constants.ShortTag then
+ toText(value) ~ s" /*${value.tpe.show}*/"
+ else
+ toText(value)
+ case _: TermRef => toTextRef(tp) ~ ".type /*" ~ toTextGlobal(tp.underlying) ~ "*/"
+ case _ => "(" ~ toTextRef(tp) ~ ": " ~ toTextGlobal(tp.underlying) ~ ")"
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala
new file mode 100644
index 000000000000..763821d0de59
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetChecker.scala
@@ -0,0 +1,60 @@
+package dotty.tools.scaladoc
+package snippets
+
+import dotty.tools.scaladoc.DocContext
+import java.nio.file.Paths
+import java.io.File
+
+import dotty.tools.io.AbstractFile
+import dotty.tools.dotc.fromtasty.TastyFileUtil
+import dotty.tools.dotc.config.Settings._
+import dotty.tools.dotc.config.ScalaSettings
+
+class SnippetChecker(val args: Scaladoc.Args)(using cctx: CompilerContext):
+
+// (val classpath: String, val bootclasspath: String, val tastyFiles: Seq[File], isScalajs: Boolean, useJavaCp: Boolean):
+ private val sep = System.getProperty("path.separator")
+
+ private val fullClasspath = List(
+ args.tastyFiles
+ .map(_.getAbsolutePath())
+ .map(AbstractFile.getFile(_))
+ .flatMap(t => try { TastyFileUtil.getClassPath(t) } catch { case e: AssertionError => Seq() })
+ .distinct.mkString(sep),
+ args.classpath
+ ).mkString(sep)
+
+ private val snippetCompilerSettings: Seq[SnippetCompilerSetting[_]] = cctx.settings.userSetSettings(cctx.settingsState).filter(_ != cctx.settings.classpath).map( s =>
+ SnippetCompilerSetting(s, s.valueIn(cctx.settingsState))
+ ) :+ SnippetCompilerSetting(cctx.settings.classpath, fullClasspath)
+
+ private val compiler: SnippetCompiler = SnippetCompiler(snippetCompilerSettings = snippetCompilerSettings)
+
+ // These constants were found empirically to make snippet compiler
+ // report errors in the same position as main compiler.
+ private val constantLineOffset = 3
+ private val constantColumnOffset = 4
+
+ def checkSnippet(
+ snippet: String,
+ data: Option[SnippetCompilerData],
+ arg: SnippetCompilerArg,
+ lineOffset: SnippetChecker.LineOffset
+ ): Option[SnippetCompilationResult] = {
+ if arg.flag != SCFlags.NoCompile then
+ val wrapped = WrappedSnippet(
+ snippet,
+ data.map(_.packageName),
+ data.fold(Nil)(_.classInfos),
+ data.map(_.imports).getOrElse(Nil),
+ lineOffset + data.fold(0)(_.position.line) + constantLineOffset,
+ data.fold(0)(_.position.column) + constantColumnOffset
+ )
+ val res = compiler.compile(wrapped, arg)
+ Some(res)
+ else None
+ }
+
+object SnippetChecker:
+ type LineOffset = Int
+ type SnippetCheckingFunc = (String, LineOffset, Option[SCFlags]) => Option[SnippetCompilationResult]
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilationResult.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilationResult.scala
new file mode 100644
index 000000000000..f4fe2f8223fb
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilationResult.scala
@@ -0,0 +1,27 @@
+package dotty.tools.scaladoc
+package snippets
+
+import dotty.tools.io.{ AbstractFile }
+
+case class Position(line: Int, column: Int, sourceLine: String, relativeLine: Int)
+
+case class SnippetCompilerMessage(position: Option[Position], message: String, level: MessageLevel):
+ def getSummary: String =
+ position.fold(s"${level.text}: ${message}") { pos =>
+ s"At ${pos.line}:${pos.column}:\n${pos.sourceLine}${level.text}: ${message}"
+ }
+
+case class SnippetCompilationResult(
+ wrappedSnippet: WrappedSnippet,
+ isSuccessful: Boolean,
+ result: Option[AbstractFile],
+ messages: Seq[SnippetCompilerMessage]
+):
+ def getSummary: String = messages.map(_.getSummary).mkString("\n")
+
+enum MessageLevel(val text: String):
+ case Info extends MessageLevel("Info")
+ case Warning extends MessageLevel("Warning")
+ case Error extends MessageLevel("Error")
+ case Debug extends MessageLevel("Debug")
+
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala
new file mode 100644
index 000000000000..9fdcfc8e46cf
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala
@@ -0,0 +1,108 @@
+package dotty.tools.scaladoc
+package snippets
+
+import dotty.tools.io.{AbstractFile, VirtualDirectory}
+import dotty.tools.dotc.Driver
+import dotty.tools.dotc.core.Contexts.Context
+import dotty.tools.dotc.core.Mode
+import dotty.tools.dotc.config.Settings.Setting._
+import dotty.tools.dotc.interfaces.SourcePosition
+import dotty.tools.dotc.ast.Trees.Tree
+import dotty.tools.dotc.interfaces.{SourceFile => ISourceFile}
+import dotty.tools.dotc.reporting.{ Diagnostic, StoreReporter }
+import dotty.tools.dotc.parsing.Parsers.Parser
+import dotty.tools.dotc.{ Compiler, Run }
+import dotty.tools.io.{AbstractFile, VirtualDirectory}
+import dotty.tools.repl.AbstractFileClassLoader
+import dotty.tools.dotc.util.SourceFile
+import dotty.tools.dotc.interfaces.Diagnostic._
+
+import scala.util.{ Try, Success, Failure }
+
+class SnippetCompiler(
+ val snippetCompilerSettings: Seq[SnippetCompilerSetting[_]],
+ target: AbstractFile = new VirtualDirectory("(memory)")
+):
+ object SnippetDriver extends Driver:
+ val currentCtx =
+ val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive)
+ rootCtx.setSetting(rootCtx.settings.YretainTrees, true)
+ rootCtx.setSetting(rootCtx.settings.YcookComments, true)
+ rootCtx.setSetting(rootCtx.settings.YreadComments, true)
+ rootCtx.setSetting(rootCtx.settings.color, "never")
+ rootCtx.setSetting(rootCtx.settings.XimportSuggestionTimeout, 0)
+
+ val ctx = setup(Array(""), rootCtx) match
+ case Some((_, ctx)) =>
+ ctx
+ case None => rootCtx
+ val res = snippetCompilerSettings.foldLeft(ctx.fresh) { (ctx, setting) =>
+ ctx.setSetting(setting.setting, setting.value)
+ }
+ res.initialize()(using res)
+ res
+
+ private val scala3Compiler = new Compiler
+
+ private def newRun(using ctx: Context): Run = scala3Compiler.newRun
+
+ private def nullableMessage(msgOrNull: String): String =
+ if (msgOrNull == null) "" else msgOrNull
+
+ private def createReportMessage(wrappedSnippet: WrappedSnippet, arg: SnippetCompilerArg, diagnostics: Seq[Diagnostic]): Seq[SnippetCompilerMessage] = {
+ val line = wrappedSnippet.outerLineOffset
+ val column = wrappedSnippet.outerColumnOffset
+ val innerLineOffset = wrappedSnippet.innerLineOffset
+ val innerColumnOffset = wrappedSnippet.innerColumnOffset
+ val infos = diagnostics.toSeq.sortBy(_.pos.source.path)
+ val errorMessages = infos.map {
+ case diagnostic if diagnostic.position.isPresent =>
+ val diagPos = diagnostic.position.get
+ val pos = Some(
+ Position(diagPos.line + line - innerLineOffset, diagPos.column + column - innerColumnOffset, diagPos.lineContent, if arg.debug then diagPos.line else diagPos.line - innerLineOffset)
+ )
+ val dmsg = Try(diagnostic.message) match {
+ case Success(msg) => msg
+ case Failure(ex) => ex.getMessage
+ }
+ val msg = nullableMessage(dmsg)
+ val level = MessageLevel.fromOrdinal(diagnostic.level)
+ SnippetCompilerMessage(pos, msg, level)
+ case d =>
+ val level = MessageLevel.fromOrdinal(d.level)
+ SnippetCompilerMessage(None, nullableMessage(d.message), level)
+ }
+ errorMessages
+ }
+
+ private def additionalMessages(wrappedSnippet: WrappedSnippet, arg: SnippetCompilerArg, context: Context): Seq[SnippetCompilerMessage] = {
+ Option.when(arg.flag == SCFlags.Fail && !context.reporter.hasErrors)(
+ SnippetCompilerMessage(None, "Snippet should not compile but compiled succesfully", MessageLevel.Error)
+ ).toList
+ }
+
+ private def isSuccessful(arg: SnippetCompilerArg, context: Context): Boolean = {
+ if arg.flag == SCFlags.Fail then context.reporter.hasErrors
+ else !context.reporter.hasErrors
+ }
+
+ def compile(
+ wrappedSnippet: WrappedSnippet,
+ arg: SnippetCompilerArg
+ ): SnippetCompilationResult = {
+ val context = SnippetDriver.currentCtx.fresh
+ .setSetting(
+ SnippetDriver.currentCtx.settings.outputDir,
+ target
+ )
+ .setReporter(new StoreReporter)
+ val run = newRun(using context)
+ run.compileFromStrings(List(wrappedSnippet.snippet))
+
+ val messages =
+ createReportMessage(wrappedSnippet, arg, context.reporter.pendingMessages(using context)) ++
+ additionalMessages(wrappedSnippet, arg, context)
+
+ val t = Option.when(!context.reporter.hasErrors)(target)
+ SnippetCompilationResult(wrappedSnippet, isSuccessful(arg, context), t, messages)
+ }
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala
new file mode 100644
index 000000000000..dcba74b12533
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala
@@ -0,0 +1,72 @@
+package dotty.tools.scaladoc
+package snippets
+
+import java.nio.file.Path
+
+case class SnippetCompilerArg(flag: SCFlags, debug: Boolean):
+ def overrideFlag(f: SCFlags): SnippetCompilerArg = copy(flag = f)
+
+sealed trait SCFlags(val flagName: String)
+
+object SCFlags:
+ case object Compile extends SCFlags("compile")
+ case object NoCompile extends SCFlags("nocompile")
+ case object Fail extends SCFlags("fail")
+
+ def values: Seq[SCFlags] = Seq(Compile, NoCompile, Fail)
+
+case class SnippetCompilerArgs(scFlags: PathBased[SCFlags], val debug: Boolean, defaultFlag: SCFlags):
+ def get(member: Member): SnippetCompilerArg =
+ member.sources
+ .flatMap(s => scFlags.get(s.path).map(_.elem))
+ .fold(SnippetCompilerArg(defaultFlag, debug))(SnippetCompilerArg(_, debug))
+
+ def get(path: Option[Path]): SnippetCompilerArg =
+ path
+ .flatMap(p => scFlags.get(p).map(_.elem))
+ .fold(SnippetCompilerArg(defaultFlag, debug))(SnippetCompilerArg(_, debug))
+
+
+object SnippetCompilerArgs:
+ val usage =
+ """
+ |Snippet compiler arguments provide a way to configure snippet checking.
+ |
+ |This setting accept list of arguments in format:
+ |args := arg{,arg}
+ |arg := [path=]flag
+ |where path is a prefix of source paths to members to which argument should be set.
+ |
+ |If path is not present, argument will be used as default.
+ |
+ |Available flags:
+ |compile - Enables snippet checking.
+ |nocompile - Disables snippet checking.
+ |fail - Enables snippet checking, asserts that snippet doesn't compile.
+ |
+ """.stripMargin
+
+ val debugUsage = """
+ |Setting this option causes snippet compiler to print snippet as it is compiled (after wrapping).
+ """.stripMargin
+
+ def load(args: List[String], debug: Boolean, defaultFlag: SCFlags = SCFlags.NoCompile)(using CompilerContext): SnippetCompilerArgs = {
+ PathBased.parse[SCFlags](args)(using SCFlagsParser) match {
+ case PathBased.ParsingResult(errors, res) =>
+ if errors.nonEmpty then report.warning(s"""
+ |Got following errors during snippet compiler args parsing:
+ |$errors
+ |
+ |$usage
+ |""".stripMargin
+ )
+ SnippetCompilerArgs(res, debug, defaultFlag)
+ }
+ }
+
+object SCFlagsParser extends ArgParser[SCFlags]:
+ def parse(s: String): Either[String, SCFlags] = {
+ SCFlags.values
+ .find(_.flagName == s)
+ .fold(Left(s"$s: No such flag found."))(Right(_))
+ }
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerDataCollector.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerDataCollector.scala
new file mode 100644
index 000000000000..475d38fef895
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerDataCollector.scala
@@ -0,0 +1,82 @@
+package dotty.tools.scaladoc
+package snippets
+
+import scala.quoted._
+import dotty.tools.scaladoc.tasty.SymOps._
+import dotty.tools.dotc.core._
+
+class SnippetCompilerDataCollector[Q <: Quotes](val qctx: Q):
+ import qctx.reflect._
+ given qctx.type = qctx
+
+ def getSnippetCompilerData(sym: Symbol, originalSym: Symbol): SnippetCompilerData =
+ val packageName = sym.packageName
+ if !sym.isPackageDef then sym.tree match {
+ case c: qctx.reflect.ClassDef =>
+ import dotty.tools.dotc
+ import dotty.tools.dotc.core.Decorators._
+ given dotc.core.Contexts.Context = qctx.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
+ val printer: dotc.printing.Printer = SelfTypePrinter()
+ val classSym = c.symbol.asInstanceOf[Symbols.ClassSymbol]
+
+ def getOwners(sym: Symbols.ClassSymbol): Seq[Symbols.ClassSymbol] = Seq(sym) ++
+ (if sym.owner.isClass && !sym.owner.is(dotc.core.Flags.Package) then getOwners(sym.owner.asInstanceOf[Symbols.ClassSymbol]) else Nil)
+
+ def collectNames(tp: Types.Type): Seq[String] = tp match {
+ case Types.AndType(t1, t2) => collectNames(t1) ++ collectNames(t2)
+ case Types.AppliedType(tpe, _) => collectNames(tpe)
+ case Types.AnnotatedType(tpe, _) => collectNames(tpe)
+ case t: Types.NamedType => if t.symbol.is(dotc.core.Flags.Module) then Seq() else Seq(t.symbol.name.show)
+ case t: Types.ThisType => Seq(t.cls.name.show)
+ case _ => Seq()
+ }
+
+ val allSyms = getOwners(classSym)
+
+ val allProcessed = allSyms.map { cSym =>
+ def createTypeConstructor(tpe: Types.Type, topLevel: Boolean = true): String = tpe match {
+ case t @ Types.TypeBounds(upper, lower) => lower match {
+ case l: Types.HKTypeLambda =>
+ (if topLevel then "" else "?") + l.paramInfos.map(p => createTypeConstructor(p, false)).mkString("[",", ","]")
+ case _ => (if topLevel then "" else "_")
+ }
+ }
+ val classType =
+ val ct = cSym.classInfo.selfType.toText(printer).show.replace(".this","").replace("\n", " ").stripPrefix(s"$packageName.")
+ Some(ct)
+ val classNames = collectNames(cSym.classInfo.selfType)
+ val classGenerics = Option.when(
+ !cSym.typeParams.isEmpty
+ )(
+ cSym.typeParams.map(_.typeRef).map(t =>
+ t.show +
+ createTypeConstructor(t.asInstanceOf[Types.TypeRef].underlying)
+ ).mkString("[",", ","]")
+ )
+ SnippetCompilerData.ClassInfo(classType, classNames, classGenerics)
+ }
+ val firstProcessed = allProcessed.head
+ SnippetCompilerData(packageName, allProcessed.reverse, Nil, position(hackGetPositionOfDocstring(using qctx)(originalSym)))
+ case _ => getSnippetCompilerData(sym.maybeOwner, originalSym)
+ } else SnippetCompilerData(packageName, Nil, Nil, position(hackGetPositionOfDocstring(using qctx)(originalSym)))
+
+ private def position(p: Option[qctx.reflect.Position]): SnippetCompilerData.Position =
+ p.fold(SnippetCompilerData.Position(0, 0))(p => SnippetCompilerData.Position(p.startLine, p.startColumn))
+
+ private def hackGetPositionOfDocstring(using Quotes)(s: qctx.reflect.Symbol): Option[qctx.reflect.Position] =
+ import dotty.tools.dotc.core.Comments.CommentsContext
+ import dotty.tools.dotc
+ given ctx: Contexts.Context = qctx.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
+ val docCtx = ctx.docCtx.getOrElse {
+ throw new RuntimeException(
+ "DocCtx could not be found and documentations are unavailable. This is a compiler-internal error."
+ )
+ }
+ s.pos.flatMap { pos =>
+ docCtx.docstring(s.asInstanceOf[Symbols.Symbol]).map { docstring =>
+ dotty.tools.dotc.util.SourcePosition(
+ pos.sourceFile.asInstanceOf[dotty.tools.dotc.util.SourceFile],
+ docstring.span
+ ).asInstanceOf[qctx.reflect.Position]
+ }
+ }
\ No newline at end of file
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerSetting.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerSetting.scala
new file mode 100644
index 000000000000..4c5ef500c83a
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerSetting.scala
@@ -0,0 +1,8 @@
+package dotty.tools.scaladoc
+package snippets
+
+import dotty.tools.scaladoc.DocContext
+import dotty.tools.dotc.config.Settings._
+import dotty.tools.dotc.config.ScalaSettings
+
+case class SnippetCompilerSetting[T](setting: Setting[T], value: T)
\ No newline at end of file
diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala
new file mode 100644
index 000000000000..e12078217c7e
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala
@@ -0,0 +1,54 @@
+package dotty.tools.scaladoc
+package snippets
+
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+
+case class WrappedSnippet(snippet: String, outerLineOffset: Int, outerColumnOffset: Int, innerLineOffset: Int, innerColumnOffset: Int)
+
+object WrappedSnippet:
+
+ val indent: Int = 2
+
+ def apply(str: String): WrappedSnippet =
+ val baos = new ByteArrayOutputStream()
+ val ps = new PrintStream(baos)
+ ps.println("package snippets")
+ ps.println("object Snippet {")
+ str.split('\n').foreach(ps.printlnWithIndent(indent, _))
+ ps.println("}")
+ WrappedSnippet(baos.toString, 0, 0, indent, indent)
+
+ def apply(
+ str: String,
+ packageName: Option[String],
+ classInfos: Seq[SnippetCompilerData.ClassInfo],
+ imports: List[String],
+ outerLineOffset: Int,
+ outerColumnOffset: Int
+ ): WrappedSnippet =
+ val baos = new ByteArrayOutputStream()
+ val ps = new PrintStream(baos)
+ ps.println(s"package ${packageName.getOrElse("snippets")}")
+ imports.foreach(i => ps.println(s"import $i"))
+ val notEmptyClassInfos = if classInfos.isEmpty then Seq(SnippetCompilerData.ClassInfo(None, Nil, None)) else classInfos
+ notEmptyClassInfos.zipWithIndex.foreach { (info, i) =>
+ ps.printlnWithIndent(indent * i, s"trait Snippet$i${info.generics.getOrElse("")} { ${info.tpe.fold("")(cn => s"self: $cn =>")}")
+ info.names.foreach{ name =>
+ ps.printlnWithIndent(indent * i + indent, s"val $name = self")
+ }
+ }
+ str.split('\n').foreach(ps.printlnWithIndent(notEmptyClassInfos.size * indent, _))
+ (0 to notEmptyClassInfos.size -1).reverse.foreach( i => ps.printlnWithIndent(i * indent, "}"))
+ WrappedSnippet(
+ baos.toString,
+ outerLineOffset,
+ outerColumnOffset,
+ notEmptyClassInfos.size + notEmptyClassInfos.flatMap(_.names).size + packageName.size,
+ notEmptyClassInfos.size * indent
+ )
+
+ extension (ps: PrintStream) private def printlnWithIndent(indent: Int, str: String) =
+ ps.println((" " * indent) + str)
+
+
diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala
index 77cf553c82c9..bbfe50f421ec 100644
--- a/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/tasty/BasicSupport.scala
@@ -39,10 +39,6 @@ trait BasicSupport:
extension (using Quotes)(sym: reflect.Symbol)
def documentation = sym.docstring.map(parseComment(_, sym.tree))
- def source =
- val path = sym.pos.map(_.sourceFile.jpath).filter(_ != null).map(_.toAbsolutePath)
- path.map(TastyMemberSource(_, sym.pos.get.startLine))
-
def getAnnotations(): List[Annotation] =
sym.annotations.filterNot(_.symbol.packageName.startsWith("scala.annotation.internal")).map(parseAnnotation).reverse
diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
index cdb1e889b29c..72c8a8073d8a 100644
--- a/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
@@ -38,6 +38,11 @@ object SymOps:
Some(s"${sym.name}-$hash")
}
else None
+
+ def source =
+ val path = sym.pos.map(_.sourceFile.jpath).filter(_ != null).map(_.toAbsolutePath)
+ path.map(TastyMemberSource(_, sym.pos.get.startLine))
+
//TODO: Retrieve string that will match scaladoc anchors
diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala
index d9199864a407..066b7b5173c4 100644
--- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala
@@ -4,17 +4,18 @@ package tasty.comments
import scala.collection.immutable.SortedMap
import scala.util.Try
-import com.vladsch.flexmark.util.{ast => mdu}
+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.options.MutableDataSet
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
-import collection.JavaConverters._
+import dotty.tools.scaladoc.tasty.{ SymOpsWithLinkCache, SymOps }
import collection.JavaConverters._
+import dotty.tools.scaladoc.snippets._
class Repr(val qctx: Quotes)(val sym: qctx.reflect.Symbol)
@@ -70,13 +71,16 @@ case class PreparsedComment(
case class DokkaCommentBody(summary: Option[DocPart], body: DocPart)
-abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {
+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): 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 =
@@ -84,8 +88,8 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {
private given qctx.type = qctx
object SymOpsWithLinkCache extends SymOpsWithLinkCache
- export SymOpsWithLinkCache.dri
- export SymOpsWithLinkCache.driInContextOfInheritingParent
+ export SymOpsWithLinkCache._
+ import SymOps._
def resolveLink(queryStr: String): DocLink =
if SchemeUri.matches(queryStr) then DocLink.ToURL(queryStr)
@@ -122,8 +126,27 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {
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 data = SnippetCompilerDataCollector[qctx.type](qctx).getSnippetCompilerData(s, s)
+ (str: String, lineOffset: SnippetChecker.LineOffset, argOverride: Option[SCFlags]) => {
+ val arg = argOverride.fold(pathBasedArg)(pathBasedArg.overrideFlag(_))
+
+ snippetChecker.checkSnippet(str, Some(data), arg, lineOffset).collect {
+ case r: SnippetCompilationResult if !r.isSuccessful =>
+ val msg = s"In member ${s.name} (${s.dri.location}):\n${r.getSummary}"
+ report.error(msg)(using dctx.compilerContext)
+ r
+ case r => r
+ }
+ }
+ }
+
final def parse(preparsed: PreparsedComment): Comment =
- val body = markupToDokkaCommentBody(stringToMarkup(preparsed.body))
+ val markup = stringToMarkup(preparsed.body)
+ val body = markupToDokkaCommentBody(processSnippets(markup))
Comment(
body = body.body,
short = body.summary,
@@ -148,7 +171,7 @@ abstract class MarkupConversion[T](val repr: Repr)(using DocContext) {
)
}
-class MarkdownCommentParser(repr: Repr)(using DocContext)
+class MarkdownCommentParser(repr: Repr)(using dctx: DocContext)
extends MarkupConversion[mdu.Node](repr) {
def stringToMarkup(str: String) =
@@ -174,6 +197,9 @@ class MarkdownCommentParser(repr: Repr)(using DocContext)
xs.view.mapValues(_.trim)
.filterNot { case (_, v) => v.isEmpty }
.mapValues(stringToMarkup).to(SortedMap)
+
+ def processSnippets(root: mdu.Node): mdu.Node =
+ FlexmarkSnippetProcessor.processSnippets(root, dctx.snippetCompilerArgs.debug, snippetCheckingFunc(owner))
}
class WikiCommentParser(repr: Repr)(using DocContext)
@@ -227,3 +253,7 @@ class WikiCommentParser(repr: Repr)(using DocContext)
def filterEmpty(xs: SortedMap[String,String]) =
xs.view.mapValues(stringToMarkup).to(SortedMap)
.filterNot { case (_, v) => v.blocks.isEmpty }
+
+ def processSnippets(root: wiki.Body): wiki.Body =
+ // Currently not supported
+ root
diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/MemberLookup.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/MemberLookup.scala
index 14f14cbedbd1..a6da58ab3e71 100644
--- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/MemberLookup.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/MemberLookup.scala
@@ -151,10 +151,7 @@ trait MemberLookup {
}
if owner.isPackageDef then
- findMatch(hackMembersOf(owner).flatMap {
- s =>
- (if s.name.endsWith("package$") then hackMembersOf(s) else Iterator.empty) ++ Iterator(s)
- })
+ findMatch(hackMembersOf(owner))
else
owner.tree match {
case tree: TypeDef =>
diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala
index ad5e4ac20e2d..2868b901f191 100644
--- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala
@@ -10,13 +10,21 @@ import com.vladsch.flexmark.ext.wikilink.internal.WikiLinkLinkRefProcessor
import com.vladsch.flexmark.util.ast._
import com.vladsch.flexmark.util.options._
import com.vladsch.flexmark.util.sequence.BasedSequence
+import com.vladsch.flexmark._
+import dotty.tools.scaladoc.snippets._
+import scala.collection.JavaConverters._
class DocLinkNode(
val target: DocLink,
val body: String,
seq: BasedSequence
- ) extends WikiNode(seq, false, false, false, false)
+) extends WikiNode(seq, false, false, false, false)
+
+case class ExtendedFencedCodeBlock(
+ codeBlock: ast.FencedCodeBlock,
+ compilationResult: Option[SnippetCompilationResult]
+) extends WikiNode(codeBlock.getChars, false, false, false, false)
class DocFlexmarkParser(resolveLink: String => DocLink) extends Parser.ParserExtension:
@@ -55,7 +63,9 @@ case class DocFlexmarkRenderer(renderLink: (DocLink, String) => String)
object Render extends NodeRenderer:
override def getNodeRenderingHandlers: JSet[NodeRenderingHandler[_]] =
- JSet(new NodeRenderingHandler(classOf[DocLinkNode], Handler))
+ JSet(
+ new NodeRenderingHandler(classOf[DocLinkNode], Handler),
+ )
object Factory extends NodeRendererFactory:
override def create(options: DataHolder): NodeRenderer = Render
@@ -65,5 +75,10 @@ case class DocFlexmarkRenderer(renderLink: (DocLink, String) => String)
object DocFlexmarkRenderer:
def render(node: Node)(renderLink: (DocLink, String) => String) =
- val opts = MarkdownParser.mkMarkdownOptions(Seq(DocFlexmarkRenderer(renderLink)))
- HtmlRenderer.builder(opts).escapeHtml(true).build().render(node)
+ val opts = MarkdownParser.mkMarkdownOptions(
+ Seq(
+ DocFlexmarkRenderer(renderLink),
+ SnippetRenderingExtension
+ )
+ )
+ HtmlRenderer.builder(opts).build().render(node)
\ No newline at end of file
diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderer.scala
new file mode 100644
index 000000000000..0c480fe5ced9
--- /dev/null
+++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderer.scala
@@ -0,0 +1,127 @@
+package dotty.tools.scaladoc.tasty.comments.markdown
+
+import com.vladsch.flexmark.html._
+
+import dotty.tools.scaladoc.snippets._
+
+case class SnippetLine(content: String, lineNo: Int, classes: Set[String] = Set.empty, messages: Seq[String] = Seq.empty):
+ def withClass(cls: String) = this.copy(classes = classes + cls)
+ private def escapeQuotes(msg: String): String = msg.replace("\"", """)
+ def toHTML =
+ val label = if messages.nonEmpty then s"""label="${messages.map(escapeQuotes).mkString("\n")}"""" else ""
+ s"""$content"""
+
+object SnippetRenderer:
+ 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 wrapHiddenSymbols(snippetLines: Seq[SnippetLine]): Seq[SnippetLine] =
+ val mRes = cutBetweenSymbols("//{", "//}", snippetLines) {
+ case (begin, mid, end) =>
+ begin ++ mid.drop(1).dropRight(1).map(_.withClass("hideable")) ++ wrapHiddenSymbols(end)
+ }
+ mRes.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 wrapSingleLineComments(snippetLines: Seq[SnippetLine]): Seq[SnippetLine] =
+ snippetLines.map { line =>
+ line.content.indexOf("//") match
+ case -1 => line
+ case idx =>
+ wrapLineInBetween(Some("//"), None, line)
+ }
+
+ private def wrapMultiLineComments(snippetLines: Seq[SnippetLine]): Seq[SnippetLine] =
+ val mRes = cutBetweenSymbols("/*", "*/", snippetLines) {
+ case (begin, mid, end) if mid.size == 1 =>
+ val midRedacted = mid.map(wrapLineInBetween(Some("/*"), Some("*/"), _))
+ begin ++ midRedacted ++ end
+ case (begin, mid, end) =>
+ val midRedacted =
+ mid.take(1).map(wrapLineInBetween(Some("/*"), None, _))
+ ++ mid.drop(1).dropRight(1).map(_.withClass("hideable"))
+ ++ mid.takeRight(1).map(wrapLineInBetween(None, Some("*/"), _))
+ begin ++ midRedacted ++ wrapMultiLineComments(end)
+ }
+ mRes.getOrElse(snippetLines)
+
+ private def wrapCodeLines(codeLines: Seq[String]): Seq[SnippetLine] =
+ val snippetLines = codeLines.zipWithIndex.map {
+ case (content, idx) => SnippetLine(content, idx)
+ }
+ wrapHiddenSymbols
+ .andThen(wrapSingleLineComments)
+ .andThen(wrapMultiLineComments)
+ .apply(snippetLines)
+
+ private def addCompileMessages(messages: Seq[SnippetCompilerMessage])(codeLines: Seq[SnippetLine]): Seq[SnippetLine] = //TODO add tooltips and stuff
+ 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.getSummary}"""
+ }
+ .mkString("
")
+ s"""
${transformedLines.mkString("")}
"""
+ s"""$codeHTML