diff --git a/scaladoc-js/resources/scaladoc-searchbar.css b/scaladoc-js/resources/scaladoc-searchbar.css index 7c81c3fa75b6..66c847d5778a 100644 --- a/scaladoc-js/resources/scaladoc-searchbar.css +++ b/scaladoc-js/resources/scaladoc-searchbar.css @@ -124,34 +124,206 @@ margin-left: auto; } -.snippet-comment-button { - position: absolute; +/* Snippets */ + +.snippet { + cursor: default; +} + +.snippet .snippet-meta { + border-top: 2px solid var(--inactive-bg); + color: var(--inactive-fg); + margin-top: 10px; + padding-top: 10px; + font-size: 0.75em; +} + +.snippet-meta .snippet-label { + font-weight: bold; +} + +.snippet .buttons { + --icon-size: 16px; +} + +.snippet-showhide { + display: flex; + flex-direction: row; + align-items: center; + --slider-width: 40px; + --slider-height: 16px; + --slider-diameter: calc(var(--slider-height) - 4px); +} + +.snippet-showhide p { + margin-left: 4px; + margin-bottom: 0; + margin-top: 0; + color: var(--inactive-fg); +} + +.snippet-showhide-button { 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 var(--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); + position: relative; + width: var(--slider-width); + height: var(--slider-height); + margin-bottom: 0; +} + +.snippet-showhide-button input { + opacity: 0; + width: 0; + height: 0; +} + +.snippet-showhide-button .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--inactive-bg); + -webkit-transition: .4s; + transition: .4s; + border-radius: var(--slider-height); +} + +.snippet-showhide-button .slider:before { + position: absolute; + content: ""; + height: var(--slider-diameter); + width: var(--slider-diameter); + left: 2px; + bottom: 2px; + background-color: var(--inactive-fg); + -webkit-transition: .4s; + transition: .4s; + border-radius: 50%; +} + +.snippet-showhide-button .slider:hover::before { + background-color: var(--active-fg); +} + +input:checked + .slider { + background-color: var(--active-bg); +} + +input:focus + .slider { + box-shadow: 0 0 1px var(--active-bg-shadow); +} + +input:checked + .slider:before { + --translation-size: calc(var(--slider-width) - var(--slider-diameter) - 4px); + -webkit-transform: translateX(var(--translation-size)); + -ms-transform: translateX(var(--translation-size)); + transform: translateX(var(--translation-size)); +} + +.snippet .buttons .tooltip::after { + top: 32px; +} + +.snippet .buttons { + display: flex; + flex-direction: row-reverse; + justify-content: flex-start; +} + +.snippet .buttons button { + outline: none; + background: none; + border: none; + font-size: var(--icon-size); + color: var(--inactive-fg); + cursor: pointer; } + +.snippet .buttons button:hover:not(:disabled) { + color: var(--inactive-fg-shadow) +} + +.snippet .buttons button:active:not(:disabled) { + transform: translateY(2px); + color: var(--active-fg) +} + +.snippet .buttons button:disabled { + color: var(--inactive-bg) +} + + +.snippet .buttons>:not(:last-child) { + border-left: 2px solid var(--inactive-bg); +} + +.snippet .buttons>* { + padding-left: 5px; + padding-right: 5px; +} + +.unselectable { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.included-section { + display: flex; + flex-direction: column; +} + +.included-section a { + color: var(--inactive-fg) !important; + font-size: 0.75em; +} + +.included-section b { + font-weight: bold; +} + +.hideable.hidden { + display: none; +} + +@media(max-width: 576px) { + .snippet-showhide { + --slider-width: 32px; + --slider-height: 16px; + } + + .snippet .buttons { + --icon-size: 16px; + font-size: 16px; + } + +} + +@media(max-width: 360px) { + .snippet-showhide { + --slider-width: 32px; + --slider-height: 16px; + } + + .snippet .buttons { + --icon-size: 16px; + font-size: 0px; + } + +} + +@media(max-width: 240px) { + .snippet-showhide { + --slider-width: 24px; + --slider-height: 10px; + } + + .snippet .buttons { + --icon-size: 16px; + font-size: 0px; + } + +} + diff --git a/scaladoc-js/src/code-snippets/CodeSnippets.scala b/scaladoc-js/src/code-snippets/CodeSnippets.scala new file mode 100644 index 000000000000..248400ea49c6 --- /dev/null +++ b/scaladoc-js/src/code-snippets/CodeSnippets.scala @@ -0,0 +1,130 @@ +package dotty.tools.scaladoc + +import org.scalajs.dom._ +import org.scalajs.dom.ext._ + +class CodeSnippets: + + private def getButtonsSection(snippet: html.Element): Option[html.Div] = snippet.querySelector("div.buttons") match { + case div: html.Div => Some(div) + case _ => None + } + + def enrichSnippets() = document.querySelectorAll("div.snippet").foreach { + case snippet: html.Element => + snippet.addEventListener("click", e => e.stopPropagation()) + snippetAnchor(snippet) + handleHideableCode(snippet) + handleImportedCode(snippet) + copyRunButtons(snippet) + } + + private def handleHideableCode(snippet: html.Element): Unit = { + def toggleHide(e: html.Element | html.Document) = e.querySelectorAll(".hideable").foreach { + case e: html.Element => e.classList.toggle("hidden") + case _ => + } + def createShowHideButton(toggleRoot: html.Element) = { + val div = document.createElement("div") + div.classList.add("snippet-showhide") + val p = document.createElement("p") + p.textContent = "Show collapsed lines" + val showHideButton = document.createElement("label") + showHideButton.classList.add("snippet-showhide-button") + val checkbox = document.createElement("input").asInstanceOf[html.Input] + checkbox.`type` = "checkbox" + val slider = document.createElement("span") + slider.classList.add("slider") + showHideButton.appendChild(checkbox) + showHideButton.appendChild(slider) + checkbox.addEventListener("change", _ => toggleHide(toggleRoot)) + div.appendChild(showHideButton) + div.appendChild(p) + div + } + + toggleHide(snippet) + val buttonsSection = getButtonsSection(snippet) + val hideables = snippet.querySelectorAll(".hideable") + if hideables != null && hideables.nonEmpty then { + val showHideButton = createShowHideButton(snippet) + buttonsSection.foreach(_.appendChild(showHideButton)) + } + } + + private def snippetAnchor(snippet: html.Element): Unit = snippet.querySelector(".snippet-meta .snippet-label") match { + case e: html.Element => + val name = e.textContent.trim + val anchor = document.createElement("a").asInstanceOf[html.Anchor] + anchor.id = s"snippet-$name" + snippet.insertBefore(anchor, snippet.firstChild) + case _ => + } + + private def handleImportedCode(snippet: html.Element): Unit = { + val included = snippet.querySelectorAll("code span.include") + val pre = snippet.querySelector("pre") + if included != null && included.nonEmpty && pre != null then { + val includesDiv = document.createElement("div") + includesDiv.classList.add("included-section") + includesDiv.classList.add("hideable") + included + .collect { case e: html.Element => e } + .toList + .filter(_.hasAttribute("name")) + .map(_.getAttribute("name")) + .distinct + .map { name => + val a = document.createElement("a").asInstanceOf[html.Anchor] + a.classList.add("unselectable") + a.href = s"#snippet-$name" + a.innerHTML = s"included $name" + a + } + .foreach(a => includesDiv.appendChild(a)) + + snippet.insertBefore(includesDiv, pre) + } + } + + private def copyRunButtons(snippet: html.Element) = { + def copyButton = { + val div = document.createElement("div") + val button = document.createElement("button") + val icon = document.createElement("i") + icon.classList.add("far") + icon.classList.add("fa-clone") + button.appendChild(icon) + button.classList.add("copy-button") + button.addEventListener("click", _ => { + val code = snippet.querySelectorAll("code>span:not(.hidden)") + .map(_.textContent) + .mkString + window.navigator.clipboard.writeText(code) + }) + div.appendChild(button) + div + } + def runButton = { + val div = document.createElement("div") + val button = document.createElement("button").asInstanceOf[html.Button] + val icon = document.createElement("i") + icon.classList.add("fas") + icon.classList.add("fa-play") + button.appendChild(icon) + button.classList.add("run-button") + button.addEventListener("click", _ => {}) // TODO: Run button #13065 + button.disabled = true + div.appendChild(button) + div + } + val buttonsSection = getButtonsSection(snippet) + buttonsSection.foreach(s => + s.appendChild(copyButton) + // Temporarily disabled + // s.appendChild(runButton) + ) + } + + enrichSnippets() + diff --git a/scaladoc-js/src/searchbar/code-snippets/CodeSnippets.scala b/scaladoc-js/src/searchbar/code-snippets/CodeSnippets.scala deleted file mode 100644 index c8121b31860d..000000000000 --- a/scaladoc-js/src/searchbar/code-snippets/CodeSnippets.scala +++ /dev/null @@ -1,28 +0,0 @@ -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) - case e => // skip - } diff --git a/scaladoc-testcases/src/tests/snippetTestcase1.scala b/scaladoc-testcases/src/tests/snippetTestcase1.scala index 4a2960092c95..dccb92368041 100644 --- a/scaladoc-testcases/src/tests/snippetTestcase1.scala +++ b/scaladoc-testcases/src/tests/snippetTestcase1.scala @@ -2,11 +2,28 @@ package tests.snippetTestcase1 class SnippetTestcase1: /** - * SNIPPET(OUTERLINEOFFSET:8,OUTERCOLUMNOFFSET:6,INNERLINEOFFSET:3,INNERCOLUMNOFFSET:2) + * SNIPPET(OUTERLINEOFFSET:8,OUTERCOLUMNOFFSET:6,INNERLINEOFFSET:5,INNERCOLUMNOFFSET:2) * ERROR(LINE:8,COLUMN:8) * ```scala sc:fail * 2 + List() * ``` * */ - def a = 3 \ No newline at end of file + def a = 3 + /** + * SNIPPET(OUTERLINEOFFSET:16,OUTERCOLUMNOFFSET:6,INNERLINEOFFSET:5,INNERCOLUMNOFFSET:2) + * ```scala sc:compile sc-name:1 + * val xs: List[Int] = List() + * ``` + * + * SNIPPET(OUTERLINEOFFSET:21,OUTERCOLUMNOFFSET:6,INNERLINEOFFSET:5,INNERCOLUMNOFFSET:2) + * ```scala sc:compile sc-compile-with:1 sc-name:2 + * val ys = xs.map(x => x * 2) + * ``` + * + * SNIPPET(OUTERLINEOFFSET:26,OUTERCOLUMNOFFSET:6,INNERLINEOFFSET:5,INNERCOLUMNOFFSET:2) + * ```scala sc:compile sc-compile-with:2 + * xs ++ ys + * ``` + */ + def b = 3 \ No newline at end of file diff --git a/scaladoc-testcases/src/tests/snippetTestcase2.scala b/scaladoc-testcases/src/tests/snippetTestcase2.scala index e68a339d77e8..3f99b316592f 100644 --- a/scaladoc-testcases/src/tests/snippetTestcase2.scala +++ b/scaladoc-testcases/src/tests/snippetTestcase2.scala @@ -7,7 +7,7 @@ trait Quotes2[A] { type X object Y { /** - * SNIPPET(OUTERLINEOFFSET:13,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:6,INNERCOLUMNOFFSET:6) + * SNIPPET(OUTERLINEOFFSET:13,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:8,INNERCOLUMNOFFSET:6) * ERROR(LINE:13,COLUMN:12) * ```scala sc:fail * 2 + List() @@ -19,7 +19,7 @@ trait Quotes2[A] { val z: zModule = ??? trait zModule { /** - * SNIPPET(OUTERLINEOFFSET:25,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:7,INNERCOLUMNOFFSET:6) + * SNIPPET(OUTERLINEOFFSET:25,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:9,INNERCOLUMNOFFSET:6) * ERROR(LINE:25,COLUMN:12) * ```scala sc:fail * 2 + List() @@ -33,7 +33,7 @@ trait Quotes2[A] { type X object Y { /** - * SNIPPET(OUTERLINEOFFSET:39,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:5,INNERCOLUMNOFFSET:6) + * SNIPPET(OUTERLINEOFFSET:39,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:7,INNERCOLUMNOFFSET:6) * ERROR(LINE:39,COLUMN:12) * ```scala sc:fail * 2 + List() @@ -45,7 +45,7 @@ trait Quotes2[A] { val z: zModule = ??? trait zModule { /** - * SNIPPET(OUTERLINEOFFSET:51,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:6,INNERCOLUMNOFFSET:6) + * SNIPPET(OUTERLINEOFFSET:51,OUTERCOLUMNOFFSET:10,INNERLINEOFFSET:8,INNERCOLUMNOFFSET:6) * ERROR(LINE:51,COLUMN:12) * ```scala sc:fail * 2 + List() diff --git a/scaladoc/src/dotty/tools/scaladoc/DocContext.scala b/scaladoc/src/dotty/tools/scaladoc/DocContext.scala index ee1444effc4a..a10be62644f3 100644 --- a/scaladoc/src/dotty/tools/scaladoc/DocContext.scala +++ b/scaladoc/src/dotty/tools/scaladoc/DocContext.scala @@ -74,7 +74,7 @@ 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 snippetCompilerArgs = snippets.SnippetCompilerArgs.load(args.snippetCompiler)(using compilerContext) lazy val snippetChecker = snippets.SnippetChecker(args)(using compilerContext) diff --git a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala index 20dc4f288d44..5314c54e5bb3 100644 --- a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala +++ b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala @@ -55,7 +55,6 @@ object Scaladoc: docCanonicalBaseUrl: String = "", documentSyntheticTypes: Boolean = false, snippetCompiler: List[String] = Nil, - snippetCompilerDebug: Boolean = false, noLinkWarnings: Boolean = false, versionsDictionaryUrl: Option[String] = None, generateInkuire : Boolean = false, @@ -222,7 +221,6 @@ object Scaladoc: YdocumentSyntheticTypes.get, snippetCompiler.get, noLinkWarnings.get, - snippetCompilerDebug.get, versionsDictionaryUrl.nonDefault, generateInkuire.get, apiSubdirectory.get, diff --git a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala index 390bc653f246..4641555fb9b8 100644 --- a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala +++ b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala @@ -114,9 +114,6 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings: 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) - val generateInkuire: Setting[Boolean] = BooleanSetting("-Ygenerate-inkuire", "Generates InkuireDB and enables Hoogle-like searches", false) @@ -124,4 +121,4 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings: BooleanSetting("-Yapi-subdirectory", "Put the API documentation pages inside a directory `api/`", false) def scaladocSpecificSettings: Set[Setting[_]] = - Set(sourceLinks, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, snippetCompilerDebug, generateInkuire) + Set(sourceLinks, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, generateInkuire) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala index fdc29d836880..04fa68fafc42 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala @@ -84,6 +84,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer: val urls = List( "https://code.jquery.com/jquery-3.5.1.min.js", + "https://use.fontawesome.com/releases/v5.15.3/js/all.js", "https://d3js.org/d3.v6.min.js", "https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.2/dist/graphlib-dot.min.js", "https://cdnjs.cloudflare.com/ajax/libs/dagre-d3/0.6.1/dagre-d3.min.js", diff --git a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala index d14970423e43..fe17973ecda8 100644 --- a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala +++ b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala @@ -106,7 +106,7 @@ case class TemplateFile( // Snippet compiler currently supports markdown only val parser: Parser = Parser.builder(defaultMarkdownOptions).build() val parsedMd = parser.parse(rendered) - val processed = FlexmarkSnippetProcessor.processSnippets(parsedMd, ssctx.snippetCompilerArgs.debug, snippetCheckingFunc)(using ssctx.outerCtx) + val processed = FlexmarkSnippetProcessor.processSnippets(parsedMd, snippetCheckingFunc)(using ssctx.outerCtx) HtmlRenderer.builder(defaultMarkdownOptions).build().render(processed) layoutTemplate match diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala index 0b1bea5df2e9..f6ea9dcad749 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala @@ -10,47 +10,81 @@ 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 = { + def processSnippets(root: mdu.Node, 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 + nodes.foldLeft[Map[String, String]](Map()) { (snippetMap, node) => 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 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 id = info + .find(_.startsWith("sc-name:")) + .map(_.stripPrefix("sc-name:")) + + val snippetImports = info + .find(_.startsWith("sc-compile-with:")) + .toList + .map(_.stripPrefix("sc-compile-with:")) + .flatMap(_.split(",")) + .flatMap { id => + val snippet = snippetMap.get(id) + if snippet.isEmpty then + report.warning( + s"""|Error occured during parsing compile-with in snippet: + |Snippet with id: $id not found. + |Remember that you cannot use forward reference to snippets""".stripMargin + ) + snippet + }.mkString("\n") + + val snippet = node.getContentChars.toString + + extension (n: mdu.Node) + def setContentString(str: String): Unit = val s = sequence.BasedSequence.EmptyBasedSequence() - .append(wrapped.snippet) + .append(str) .append(sequence.BasedSequence.EOL) val content = mdu.BlockContent() content.add(s, 0) node.setContent(content) + + val fullSnippet = Seq(snippetImports, snippet).mkString("\n").trim + val snippetCompilationResult = cf(fullSnippet, lineOffset, argOverride) match { + case result@Some(SnippetCompilationResult(wrapped, _, _, _)) => + node.setContentString(wrapped.snippet) + result + case result => + node.setContentString(fullSnippet) result - case result => result } - node.insertBefore(ExtendedFencedCodeBlock(node, snippetCompilationResult)) + node.insertBefore(ExtendedFencedCodeBlock(id, node, snippetCompilationResult)) node.unlink() - } + id.fold(snippetMap)(id => + val snippetAsImport = s"""|//{i:$id + |$snippet + |//i}""".stripMargin + val entry = (id, Seq(snippetImports, snippetAsImport).mkString("\n")) + snippetMap + entry + ) + } else snippetMap } root diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala index 9fdcfc8e46cf..00f89b94c626 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala @@ -59,7 +59,7 @@ class SnippetCompiler( 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) + Position(diagPos.line + line - innerLineOffset, diagPos.column + column - innerColumnOffset, diagPos.lineContent, diagPos.line) ) val dmsg = Try(diagnostic.message) match { case Success(msg) => msg diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala index dcba74b12533..72bbd291b6da 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompilerArgs.scala @@ -3,7 +3,7 @@ package snippets import java.nio.file.Path -case class SnippetCompilerArg(flag: SCFlags, debug: Boolean): +case class SnippetCompilerArg(flag: SCFlags): def overrideFlag(f: SCFlags): SnippetCompilerArg = copy(flag = f) sealed trait SCFlags(val flagName: String) @@ -15,16 +15,16 @@ object SCFlags: def values: Seq[SCFlags] = Seq(Compile, NoCompile, Fail) -case class SnippetCompilerArgs(scFlags: PathBased[SCFlags], val debug: Boolean, defaultFlag: SCFlags): +case class SnippetCompilerArgs(scFlags: PathBased[SCFlags], defaultFlag: SCFlags): def get(member: Member): SnippetCompilerArg = member.sources .flatMap(s => scFlags.get(s.path).map(_.elem)) - .fold(SnippetCompilerArg(defaultFlag, debug))(SnippetCompilerArg(_, debug)) + .fold(SnippetCompilerArg(defaultFlag))(SnippetCompilerArg(_)) def get(path: Option[Path]): SnippetCompilerArg = path .flatMap(p => scFlags.get(p).map(_.elem)) - .fold(SnippetCompilerArg(defaultFlag, debug))(SnippetCompilerArg(_, debug)) + .fold(SnippetCompilerArg(defaultFlag))(SnippetCompilerArg(_)) object SnippetCompilerArgs: @@ -46,11 +46,7 @@ object SnippetCompilerArgs: | """.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 = { + def load(args: List[String], 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""" @@ -60,7 +56,7 @@ object SnippetCompilerArgs: |$usage |""".stripMargin ) - SnippetCompilerArgs(res, debug, defaultFlag) + SnippetCompilerArgs(res, defaultFlag) } } diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala index e12078217c7e..d3ad8db8f9d3 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/WrappedSnippet.scala @@ -13,11 +13,15 @@ object WrappedSnippet: def apply(str: String): WrappedSnippet = val baos = new ByteArrayOutputStream() val ps = new PrintStream(baos) + ps.startHide() ps.println("package snippets") ps.println("object Snippet {") + ps.endHide() str.split('\n').foreach(ps.printlnWithIndent(indent, _)) + ps.startHide() ps.println("}") - WrappedSnippet(baos.toString, 0, 0, indent, indent) + ps.endHide() + WrappedSnippet(baos.toString, 0, 0, indent + 2 /*Hide tokens*/, indent) def apply( str: String, @@ -29,6 +33,7 @@ object WrappedSnippet: ): WrappedSnippet = val baos = new ByteArrayOutputStream() val ps = new PrintStream(baos) + ps.startHide() 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 @@ -38,17 +43,23 @@ object WrappedSnippet: ps.printlnWithIndent(indent * i + indent, s"val $name = self") } } + ps.endHide() str.split('\n').foreach(ps.printlnWithIndent(notEmptyClassInfos.size * indent, _)) + ps.startHide() (0 to notEmptyClassInfos.size -1).reverse.foreach( i => ps.printlnWithIndent(i * indent, "}")) + ps.endHide() WrappedSnippet( baos.toString, outerLineOffset, outerColumnOffset, - notEmptyClassInfos.size + notEmptyClassInfos.flatMap(_.names).size + packageName.size, + notEmptyClassInfos.size + notEmptyClassInfos.flatMap(_.names).size + packageName.size + 2 /*Hide tokens*/, notEmptyClassInfos.size * indent ) - extension (ps: PrintStream) private def printlnWithIndent(indent: Int, str: String) = - ps.println((" " * indent) + str) + extension (ps: PrintStream) + private def printlnWithIndent(indent: Int, str: String) = + ps.println((" " * indent) + str) + private def startHide() = ps.println(raw"//{") + private def endHide() = ps.println(raw"//}") diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala index 345b2399abe8..bcd7dfcb438d 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/Comments.scala @@ -197,7 +197,7 @@ class MarkdownCommentParser(repr: Repr)(using dctx: DocContext) .mapValues(stringToMarkup).to(SortedMap) def processSnippets(root: mdu.Node): mdu.Node = - FlexmarkSnippetProcessor.processSnippets(root, dctx.snippetCompilerArgs.debug, snippetCheckingFunc(owner)) + FlexmarkSnippetProcessor.processSnippets(root, snippetCheckingFunc(owner)) } class WikiCommentParser(repr: Repr)(using DocContext) 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 2868b901f191..14ea61b5c9da 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala @@ -22,9 +22,10 @@ class DocLinkNode( ) extends WikiNode(seq, false, false, false, false) case class ExtendedFencedCodeBlock( + name: Option[String], codeBlock: ast.FencedCodeBlock, compilationResult: Option[SnippetCompilationResult] -) extends WikiNode(codeBlock.getChars, false, false, false, false) +) extends BlankLine(codeBlock.getContentChars()) class DocFlexmarkParser(resolveLink: String => DocLink) extends Parser.ParserExtension: diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderer.scala index 76403af24f7c..dd87527625d5 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderer.scala @@ -1,17 +1,28 @@ -package dotty.tools.scaladoc.tasty.comments.markdown +package dotty.tools.scaladoc +package tasty.comments.markdown import com.vladsch.flexmark.html._ +import util.HTML._ import dotty.tools.scaladoc.snippets._ import dotty.tools.scaladoc.util.HTML._ -case class SnippetLine(content: String, lineNo: Int, classes: Set[String] = Set.empty, messages: Seq[String] = Seq.empty): +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)) + private def attributesToString: String = attributes.updated("id", lineNo).map((key, value) => s"""$key="$value"""").mkString(" ") def toHTML = val label = if messages.nonEmpty then s"""label="${messages.map(_.escapeReservedTokens).mkString("\n")}"""" else "" - s"""$content""" + 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" @@ -32,13 +43,33 @@ object SnippetRenderer: (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("//{", "//}", snippetLines) { + 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") 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)) @@ -65,35 +96,13 @@ object SnippetRenderer: 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.escapeReservedTokens, idx) } - wrapHiddenSymbols - .andThen(wrapSingleLineComments) - .andThen(wrapMultiLineComments) + wrapImportedSection + .andThen(wrapHiddenSymbols) + .andThen(wrapCommonIndent) .apply(snippetLines) private def addCompileMessages(messages: Seq[SnippetCompilerMessage])(codeLines: Seq[SnippetLine]): Seq[SnippetLine] = @@ -121,7 +130,11 @@ object SnippetRenderer: .mkString("
") s"""
$content""" - def renderSnippetWithMessages(codeLines: Seq[String], messages: Seq[SnippetCompilerMessage]): String = + 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]): String = val transformedLines = wrapCodeLines.andThen(addCompileMessages(messages)).apply(codeLines).map(_.toHTML) - val codeHTML = s"""${transformedLines.mkString("")}
""" - s"""
$codeHTML
""" \ No newline at end of file + val codeHTML = s"""${transformedLines.mkString("")}""" + s"""
$codeHTML
${snippetName.fold("")(snippetLabel(_))}
""" \ No newline at end of file diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderingExtension.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderingExtension.scala index d798d21c4e9f..5f3659383ec0 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderingExtension.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SnippetRenderingExtension.scala @@ -19,6 +19,7 @@ object SnippetRenderingExtension extends HtmlRenderer.HtmlRendererExtension: override def render(node: ExtendedFencedCodeBlock, c: NodeRendererContext, html: HtmlWriter): Unit = html.raw( SnippetRenderer.renderSnippetWithMessages( + node.name, node.codeBlock.getContentChars.toString.split("\n").map(_ + "\n").toSeq, node.compilationResult.toSeq.flatMap(_.messages) ) diff --git a/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetCompilerTest.scala b/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetCompilerTest.scala index 86a630a9d42f..5bfac840091b 100644 --- a/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetCompilerTest.scala +++ b/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetCompilerTest.scala @@ -18,7 +18,7 @@ class SnippetCompilerTest { 0 ) - def runTest(str: String) = compiler.compile(wrapFn(str), SnippetCompilerArg(SCFlags.Compile, false)) + def runTest(str: String) = compiler.compile(wrapFn(str), SnippetCompilerArg(SCFlags.Compile)) private def assertSuccessfulCompilation(res: SnippetCompilationResult): Unit = res match { case r @ SnippetCompilationResult(_, isSuccessful, _, messages) => assert(isSuccessful, r.getSummary) diff --git a/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTest.scala b/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTest.scala index f80b709f3ce9..46cb90152ba9 100644 --- a/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTest.scala +++ b/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTest.scala @@ -18,7 +18,7 @@ import collection.JavaConverters._ import dotty.tools.scaladoc.tasty.comments.markdown.ExtendedFencedCodeBlock -abstract class SnippetsE2eTest(testName: String, flag: SCFlags, debug: Boolean) extends ScaladocTest(testName): +abstract class SnippetsE2eTest(testName: String, flag: SCFlags) extends ScaladocTest(testName): import SnippetsE2eTest._ @@ -29,16 +29,13 @@ abstract class SnippetsE2eTest(testName: String, flag: SCFlags, debug: Boolean) def report(str: String) = s"""|In test $testName: |$str""".stripMargin - println(BuildInfo.test_testcasesOutputDir.map(_ + s"/tests/$testName")) - override def args = Scaladoc.Args( name = "test", tastyDirs = BuildInfo.test_testcasesOutputDir.map(java.io.File(_)).toSeq, tastyFiles = tastyFiles(testName), output = getTempDir().getRoot, projectVersion = Some("1.0"), - snippetCompiler = List(s"${BuildInfo.test_testcasesSourceRoot}/tests=${flag.flagName}"), - snippetCompilerDebug = debug + snippetCompiler = List(s"${BuildInfo.test_testcasesSourceRoot}/tests=${flag.flagName}") ) override def withModule(op: DocContext ?=> Module => Unit) = @@ -85,19 +82,18 @@ abstract class SnippetsE2eTest(testName: String, flag: SCFlags, debug: Boolean) def checkRelativeLines(msg: Message, cmsg: SnippetCompilerMessage): Seq[String] = val pos = cmsg.position.get - if debug then { - if !(pos.relativeLine == pos.line - ws.outerLineOffset + ws.innerLineOffset) then Seq( - s"Expected ${msg.level.text} message at relative line: ${pos.line - ws.outerLineOffset + ws.innerLineOffset} " + - s"but found at ${pos.relativeLine}" - ) else Nil - } else { - if !(pos.relativeLine == pos.line - ws.outerLineOffset) then Seq( - s"Expected ${msg.level.text} message at relative line: ${pos.line - ws.outerLineOffset} " + - s"but found at ${pos.relativeLine}" - ) else Nil - } + if !(pos.relativeLine == pos.line - ws.outerLineOffset + ws.innerLineOffset) then Seq( + s"Expected ${msg.level.text} message at relative line: ${pos.line - ws.outerLineOffset + ws.innerLineOffset} " + + s"but found at ${pos.relativeLine}" + ) else Nil + + val mResult = compilationMessagesWithPos.flatMap { cmsg => + messages + .find(msg => isSamePosition(msg, cmsg)) + .fold(Seq(s"Unexpected compilation message: ${cmsg.message} at relative line: ${cmsg.position.fold(-1)(_.line)}"))(_ => Seq()) + } - val result = messages.flatMap { msg => + val result = mResult ++ messages.flatMap { msg => compilationMessagesWithPos .find(cmsg => isSamePosition(msg, cmsg)) .fold(Seq(s"Expected ${msg.level.text} message at ${msg.offset.line}:${msg.offset.column}.")) { resp => diff --git a/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTestcases.scala b/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTestcases.scala index f1d588da81b5..5d97e049e2df 100644 --- a/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTestcases.scala +++ b/scaladoc/test/dotty/tools/scaladoc/snippets/SnippetsE2eTestcases.scala @@ -1,10 +1,8 @@ package dotty.tools.scaladoc package snippets -class SnippetE2eTestcase1 extends SnippetsE2eTest("snippetTestcase1", SCFlags.Compile, false) +class SnippetE2eTestcase1 extends SnippetsE2eTest("snippetTestcase1", SCFlags.Compile) -class SnippetE2eTestcase1Debug extends SnippetsE2eTest("snippetTestcase1", SCFlags.Compile, true) -class SnippetE2eTestcase2 extends SnippetsE2eTest("snippetTestcase2", SCFlags.Compile, false) +class SnippetE2eTestcase2 extends SnippetsE2eTest("snippetTestcase2", SCFlags.Compile) -class SnippetE2eTestcase2Debug extends SnippetsE2eTest("snippetTestcase2", SCFlags.Compile, true)