diff --git a/docs/_layouts/doc-page.html b/docs/_layouts/doc-page.html index 57980aac0d3d..fb813ae4d3d4 100644 --- a/docs/_layouts/doc-page.html +++ b/docs/_layouts/doc-page.html @@ -14,9 +14,5 @@

{{ page.title }}

{% endif %} {{ content }} - diff --git a/docs/_layouts/static-site-main.html b/docs/_layouts/static-site-main.html index b3e635a09bfb..ab1559190576 100644 --- a/docs/_layouts/static-site-main.html +++ b/docs/_layouts/static-site-main.html @@ -36,14 +36,20 @@ {{ content }} + diff --git a/project/resources/referenceReplacements/_layouts/static-site-main.html b/project/resources/referenceReplacements/_layouts/static-site-main.html index d47907819956..7d63a3ad59c7 100644 --- a/project/resources/referenceReplacements/_layouts/static-site-main.html +++ b/project/resources/referenceReplacements/_layouts/static-site-main.html @@ -31,14 +31,20 @@ {{ content }} + diff --git a/scaladoc-testcases/docs/_docs/docs/f5.html b/scaladoc-testcases/docs/_docs/docs/f5.html new file mode 100644 index 000000000000..0311b9e80729 --- /dev/null +++ b/scaladoc-testcases/docs/_docs/docs/f5.html @@ -0,0 +1,3 @@ +

Header 1

+

Header 2

+

Header 3

\ No newline at end of file diff --git a/scaladoc-testcases/docs/_layouts/static-site-main.html b/scaladoc-testcases/docs/_layouts/static-site-main.html index 2d4c086446fc..52b23307db27 100644 --- a/scaladoc-testcases/docs/_layouts/static-site-main.html +++ b/scaladoc-testcases/docs/_layouts/static-site-main.html @@ -9,14 +9,20 @@ {{ content }} + diff --git a/scaladoc-testcases/docs/sidebar.yml b/scaladoc-testcases/docs/sidebar.yml index 51f8e87f2ad6..532b80c30ccd 100644 --- a/scaladoc-testcases/docs/sidebar.yml +++ b/scaladoc-testcases/docs/sidebar.yml @@ -4,3 +4,4 @@ subsection: - page: docs/f2.md - page: docs/f3.md - page: docs/f4.md + - page: docs/f5.html \ No newline at end of file diff --git a/scaladoc/resources/dotty_res/scripts/ux.js b/scaladoc/resources/dotty_res/scripts/ux.js index 2a631e987bd0..27976bb8ad28 100644 --- a/scaladoc/resources/dotty_res/scripts/ux.js +++ b/scaladoc/resources/dotty_res/scripts/ux.js @@ -20,6 +20,22 @@ window.addEventListener("DOMContentLoaded", () => { $(this).parent().toggleClass("expanded") }); + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + const id = entry.target.getAttribute('id'); + if (entry.intersectionRatio > 0) { + document.querySelector(`#toc li a[href="#${id}"]`).parentElement.classList.add('active'); + } else { + document.querySelector(`#toc li a[href="#${id}"]`).parentElement.classList.remove('active'); + } + }); + }); + + + document.querySelectorAll('#content section[id]').forEach((section) => { + observer.observe(section); + }); + document.querySelectorAll("#sideMenu2 a").forEach(elem => elem.addEventListener('click', e => e.stopPropagation())) $('.names .tab').on('click', function() { diff --git a/scaladoc/resources/dotty_res/styles/scalastyle.css b/scaladoc/resources/dotty_res/styles/scalastyle.css index b0ef70209c71..5cbf92dfa1ee 100644 --- a/scaladoc/resources/dotty_res/styles/scalastyle.css +++ b/scaladoc/resources/dotty_res/styles/scalastyle.css @@ -10,6 +10,7 @@ /* Layout Settings (changes on small screens) */ --side-width: 300px; + --toc-width: 300px; --content-padding: 24px 42px; --footer-height: 42px; } @@ -43,6 +44,7 @@ body, button, input { #main-content { min-height: calc(100vh - var(--footer-height) - 24px); margin-left: var(--side-width); + margin-right: var(--toc-width); padding: var(--content-padding); padding-bottom: calc(24px + var(--footer-height)); @@ -905,6 +907,46 @@ footer .socials { height: 8px; } +#toc { + position: fixed; + right: 0px; + top: 0px; + width: var(--toc-width); + height: 100%; + + padding-top: 15vh; + padding-right: 10px; + + display: flex; + flex-direction: column; +} + +#toc .toc-title { + font-weight: bold; + padding-left: 16px; +} + +#toc ul { + list-style-type: none; + padding-left: 16px; +} + +#toc a { + display: block; + padding-top: 0.5em; + padding-bottom: 0.5em; + color: var(--leftbar-fg); + transition-duration: 0.2s; +} + +#toc li:hover > a { + color: var(--link-hover-fg); +} + +#toc li.active > a { + color: var(--link-hover-fg); +} + /* Signature coloring */ :root { @@ -941,6 +983,12 @@ footer .socials { } } /* Landscape phones, portait tablets */ +@media(max-width: 1200px) { + :root { + --toc-width: 0px; + } +} + @media(max-width: 768px) { :root { --content-padding: 12px 12px; @@ -1070,43 +1118,51 @@ footer .socials { /* Nav Icons */ +.arrows-wrapper { + display: flex; + justify-content: space-between; + padding-top: 16px; + padding-bottom: 16px; +} + .arrows { - font-size: 3em; + color: var(--link-fg); + font-size: 1em; text-align: center; - position: fixed; - top: 0; - bottom: 0; - margin: 0; - max-width: 150px; - min-width: 90px; - display: flex; - justify-content: center; - flex-direction: column; + align-items: center; transition: color 0.5s, background-color 0.5s; } +.arrows span { + max-width: 10vw; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.arrows i { + margin: 8px; +} + .arrows.previous, .arrows.next { color: var(--grey400); } .arrows:hover { text-decoration: none; - color: var(--grey300); - background-color: var(--grey900); + color: var(--link-hover-fg); transition: background-color 0.15s, color 0.15s; } .previous { - left: var(--side-width); - float: left; + flex-direction: row-reverse; } .next { - right: 0; - float: right; + flex-direction: row; } @media screen and (max-width: 1000px) { diff --git a/scaladoc/src/dotty/tools/scaladoc/api.scala b/scaladoc/src/dotty/tools/scaladoc/api.scala index 314dcc3c7261..3697f3089c8e 100644 --- a/scaladoc/src/dotty/tools/scaladoc/api.scala +++ b/scaladoc/src/dotty/tools/scaladoc/api.scala @@ -1,6 +1,7 @@ package dotty.tools.scaladoc import dotty.tools.scaladoc.tasty.comments.Comment +import util.HTML.AppliedTag enum Visibility(val name: String): case Unrestricted extends Visibility("") @@ -258,3 +259,18 @@ case class SnippetCompilerData( imports: List[String], position: SnippetCompilerData.Position ) + +case class PageContent(content: AppliedTag, toc: Seq[TocEntry]) + +case class TocEntry(level: Int, content: String, anchor: String) + +object TocEntry: + val tagLevels: Map[String, Int] = Map( + ("h1" -> 1), + ("h2" -> 2), + ("h3" -> 3), + ("h4" -> 4), + ("h5" -> 5), + ("h6" -> 6) + ) + def apply(tag: String, content: String, anchor: String): TocEntry = TocEntry(tagLevels(tag), content, anchor) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala index b61db9248a6d..3a5d47c8f46c 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala @@ -22,7 +22,7 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do html( mkHead(page), body( - if !page.hasFrame then renderContent(page) + if !page.hasFrame then renderContent(page).content else mkFrame(page.link, parents, renderContent(page)) ) ) @@ -148,7 +148,23 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do ) } - private def mkFrame(link: Link, parents: Vector[Link], content: => AppliedTag): AppliedTag = + private def renderTableOfContents(toc: Seq[TocEntry]): Option[AppliedTag] = + def renderTocRec(level: Int, rest: Seq[TocEntry]): Seq[AppliedTag] = + rest match { + case Nil => Nil + case head :: tail if head.level == level => + val (nested, rest) = tail.span(_.level > level) + val nestedList = if nested.nonEmpty then Seq(ul(renderTocRec(level + 1, nested))) else Nil + li(a(href := head.anchor)(head.content), nestedList) +: renderTocRec(level, rest) + case rest @ (head :: tail) if head.level > level => + val (prefix, suffix) = rest.span(_.level > level) + li(ul(renderTocRec(level + 1, prefix))) +: renderTocRec(level, suffix) + } + + renderTocRec(1, toc).headOption.map(toc => nav(cls := "toc-nav")(ul(cls := "toc-list")(toc))) + + + private def mkFrame(link: Link, parents: Vector[Link], content: => PageContent): AppliedTag = val projectLogo = args.projectLogo.map { path => val fileName = Paths.get(path).getFileName() @@ -202,7 +218,7 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do div(id := "scaladoc-searchBar"), main(id := "main-content")( parentsHtml, - div(id := "content")(content), + div(id := "content")(content.content), ), footer( div(id := "generated-by")( @@ -239,5 +255,11 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do ) ) ) - ) + ), + renderTableOfContents(content.toc).fold(Nil) { toc => + div(id := "toc")( + span(cls := "toc-title")("In this article"), + toc + ) + } ) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/MarkdownRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/MarkdownRenderer.scala index b49c1e268678..5b074ad0a524 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/MarkdownRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/MarkdownRenderer.scala @@ -22,7 +22,7 @@ class MarkdownRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx super.render() override def pageContent(page: Page, parents: Vector[Link]): AppliedTag = - renderContent(page) + renderContent(page).content private def renderResources(): Seq[String] = allResources(Nil).flatMap(renderResource) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala index 19dd4e4b7403..15da6049d452 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/MemberRenderer.scala @@ -387,7 +387,7 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext div(cls := "filterLowerContainer")() ) - def fullMember(m: Member): AppliedTag = + def fullMember(m: Member): PageContent = val intro = m.kind match case Kind.RootPackage =>Seq(h1(summon[DocContext].args.name)) case _ => @@ -401,11 +401,13 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext memberSignature(m) ) ) - - div( - intro, - memberInfo(m, withBrief = false), - classLikeParts(m), - buildDocumentableFilter, // TODO Need to make it work in JS :( - buildMembers(m) + PageContent( + div( + intro, + memberInfo(m, withBrief = false), + classLikeParts(m), + buildDocumentableFilter, // TODO Need to make it work in JS :( + buildMembers(m) + ), + Seq.empty // For now, we don't support table of contents in members ) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/Renderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/Renderer.scala index 2e5784c003fe..6d177e113c24 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/Renderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/Renderer.scala @@ -59,7 +59,26 @@ abstract class Renderer(rootPackage: Member, val members: Map[DRI, Member], prot val realSiblingPath = realPath(n.file.toPath) realMidPath.relativize(realSiblingPath).toString.stripPrefix("../") } - List(link(prev).map("previous" -> _), link(next).map("next" -> _)).flatten.toMap + List( + for { + link <- link(prev) + p <- prev + } yield ( + "previous" -> Map( + "title" -> p.templateFile.title.name, + "url" -> link + ) + ), + for { + link <- link(next) + n <- next + } yield ( + "next" -> Map( + "title" -> n.templateFile.title.name, + "url" -> link + ) + ), + ).flatten.toMap }.toList def updateSettings(templates: Seq[LoadedTemplate], additionalSettings: ListBuffer[Map[String, Object]]): List[LoadedTemplate] = @@ -115,7 +134,7 @@ abstract class Renderer(rootPackage: Member, val members: Map[DRI, Member], prot all - def renderContent(page: Page) = page.content match + def renderContent(page: Page): PageContent = page.content match case m: Member => val signatureRenderer = new SignatureRenderer: def currentDri: DRI = page.link.dri @@ -126,7 +145,7 @@ abstract class Renderer(rootPackage: Member, val members: Map[DRI, Member], prot MemberRenderer(signatureRenderer).fullMember(m) case t: ResolvedTemplate => siteContent(page.link.dri, t) - case a: String => raw(a) + case a: String => PageContent(raw(a), Seq.empty) diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/SiteRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/SiteRenderer.scala index ad97b19fbb24..5fa00c8f9374 100644 --- a/scaladoc/src/dotty/tools/scaladoc/renderers/SiteRenderer.scala +++ b/scaladoc/src/dotty/tools/scaladoc/renderers/SiteRenderer.scala @@ -8,10 +8,12 @@ import java.net.URL import dotty.tools.scaladoc.site._ import scala.util.Try import org.jsoup.Jsoup +import org.jsoup.nodes.Element import java.nio.file.Paths import java.nio.file.Path import java.nio.file.Files import java.io.File +import scala.util.chaining._ case class ResolvedTemplate(template: LoadedTemplate, ctx: StaticSiteContext): val resolved = template.resolveToHtml(ctx) @@ -26,7 +28,7 @@ trait SiteRenderer(using DocContext) extends Locations: private val HashRegex = "([^#]+)(#.+)".r - def siteContent(pageDri: DRI, content: ResolvedTemplate): AppliedTag = + def siteContent(pageDri: DRI, content: ResolvedTemplate): PageContent = import content.ctx def tryAsDri(str: String): Option[String] = val (path, prefix) = str match @@ -67,10 +69,19 @@ trait SiteRenderer(using DocContext) extends Locations: processLocalLink(str) val document = Jsoup.parse(content.resolved.code) + + val toc = document.select("section[id]").asScala.toSeq + .flatMap { elem => + val header = elem.selectFirst("h1, h2, h3, h4, h5, h6") + Option(header).map { h => + TocEntry(h.tag().getName, h.text(), s"#${elem.id()}") + } + } + document.select("a").forEach(element => element.attr("href", processLocalLinkWithGuard(element.attr("href"))) ) document.select("img").forEach { element => element.attr("src", processLocalLink(element.attr("src"))) } // foreach does not work here. Why? - raw(document.outerHtml()) + PageContent(raw(document.outerHtml()), toc) diff --git a/scaladoc/src/dotty/tools/scaladoc/site/FlexmarkSectionWrapper.scala b/scaladoc/src/dotty/tools/scaladoc/site/FlexmarkSectionWrapper.scala new file mode 100644 index 000000000000..66903dd46dd4 --- /dev/null +++ b/scaladoc/src/dotty/tools/scaladoc/site/FlexmarkSectionWrapper.scala @@ -0,0 +1,26 @@ +package dotty.tools.scaladoc +package site + +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.Section + +object FlexmarkSectionWrapper { + def apply(md: mdu.Document): mdu.Document = { + val children = md.getChildren.asScala.toList + val newChildren = getNewChildren(Nil, None, children) + md.removeChildren() + newChildren.foreach(md.appendChild) + md + } + + def getNewChildren(finished: List[mdu.Node], current: Option[(mda.Heading, List[mdu.Node])], rest: List[mdu.Node]): List[mdu.Node] = rest match { + case Nil => current.fold(finished)(finished :+ Section(_, _)) + case (h: mda.Heading) :: rest => current.fold(getNewChildren(finished, Some(h, Nil), rest))((head, b) => getNewChildren(finished :+ Section(head, b), Some(h, Nil), rest)) + case (n: mdu.Node) :: rest => current.fold(getNewChildren(finished :+ n, None, rest))((head, b) => getNewChildren(finished, Some(head, b :+ n), rest)) + } +} diff --git a/scaladoc/src/dotty/tools/scaladoc/site/common.scala b/scaladoc/src/dotty/tools/scaladoc/site/common.scala index e93a5a62cb63..5c3c9eb8470e 100644 --- a/scaladoc/src/dotty/tools/scaladoc/site/common.scala +++ b/scaladoc/src/dotty/tools/scaladoc/site/common.scala @@ -15,6 +15,7 @@ import com.vladsch.flexmark.parser.{Parser, ParserEmulationProfile} import com.vladsch.flexmark.util.options.{DataHolder, MutableDataSet} import com.vladsch.flexmark.ext.wikilink.WikiLinkExtension import com.vladsch.flexmark.formatter.Formatter +import com.vladsch.flexmark.html.HtmlRenderer import scala.collection.JavaConverters._ @@ -24,21 +25,21 @@ val apiPageDRI: DRI = DRI(location = "api/index") def defaultMarkdownOptions(using ctx: StaticSiteContext): DataHolder = new MutableDataSet() .setFrom(ParserEmulationProfile.COMMONMARK.getOptions) - .set(AnchorLinkExtension.ANCHORLINKS_WRAP_TEXT, false) - .set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "anchor") .set(EmojiExtension.ROOT_IMAGE_PATH, "https://github.global.ssl.fastly.net/images/icons/emoji/") .set(WikiLinkExtension.LINK_ESCAPE_CHARS, "") .set(Parser.EXTENSIONS, java.util.Arrays.asList( TablesExtension.create(), TaskListExtension.create(), AutolinkExtension.create(), - AnchorLinkExtension.create(), EmojiExtension.create(), YamlFrontMatterExtension.create(), StrikethroughExtension.create(), WikiLinkExtension.create(), - tasty.comments.markdown.SnippetRenderingExtension + tasty.comments.markdown.SnippetRenderingExtension, + tasty.comments.markdown.SectionRenderingExtension )) + .set(HtmlRenderer.GENERATE_HEADER_ID, false) + .set(HtmlRenderer.RENDER_HEADER_ID, false) def emptyTemplate(file: File, title: String): TemplateFile = TemplateFile( file = file, diff --git a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala index 8d7cc04cfec6..873fde2dd1bb 100644 --- a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala +++ b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala @@ -4,7 +4,6 @@ package site import java.io.File import java.nio.file.{Files, Paths} -import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension import com.vladsch.flexmark.ext.autolink.AutolinkExtension import com.vladsch.flexmark.ext.emoji.EmojiExtension import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension @@ -25,6 +24,7 @@ import scala.collection.JavaConverters._ import scala.io.Source import dotty.tools.scaladoc.snippets._ +import scala.util.chaining._ /** RenderingContext stores information about defined properties, layouts and sites being resolved * @@ -123,9 +123,12 @@ case class TemplateFile( val code = if (isHtml || layoutTemplate.exists(!_.isHtml)) rendered else // Snippet compiler currently supports markdown only val parser: Parser = Parser.builder(defaultMarkdownOptions).build() - val parsedMd = parser.parse(rendered) - val processed = FlexmarkSnippetProcessor.processSnippets(parsedMd, None, snippetCheckingFunc, withContext = false)(using ssctx.outerCtx) - HtmlRenderer.builder(defaultMarkdownOptions).build().render(processed) + val parsedMd = parser.parse(rendered).pipe { md => + FlexmarkSnippetProcessor.processSnippets(md, None, snippetCheckingFunc, withContext = false)(using ssctx.outerCtx) + }.pipe { md => + FlexmarkSectionWrapper(md) + } + HtmlRenderer.builder(defaultMarkdownOptions).build().render(parsedMd) // If we have a layout template, we need to embed rendered content in it. Otherwise, we just leave the content as is. layoutTemplate match { diff --git a/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala b/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala index 499e1c55a17c..f65d0e32508d 100644 --- a/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala +++ b/scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala @@ -11,7 +11,7 @@ import dotty.tools.scaladoc.tasty.comments.markdown.ExtendedFencedCodeBlock import dotty.tools.scaladoc.tasty.comments.PreparsedComment object FlexmarkSnippetProcessor: - def processSnippets(root: mdu.Node, preparsed: Option[PreparsedComment], checkingFunc: => SnippetChecker.SnippetCheckingFunc, withContext: Boolean)(using CompilerContext): mdu.Node = { + def processSnippets[T <: mdu.Node](root: T, preparsed: Option[PreparsedComment], checkingFunc: => SnippetChecker.SnippetCheckingFunc, withContext: Boolean)(using CompilerContext): T = { lazy val cf: SnippetChecker.SnippetCheckingFunc = checkingFunc val nodes = root.getDescendants().asScala.collect { @@ -97,4 +97,4 @@ object FlexmarkSnippetProcessor: } root - } \ No newline at end of file + } 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 b849da014780..8bee19864773 100644 --- a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala @@ -8,6 +8,7 @@ import com.vladsch.flexmark.parser._ import com.vladsch.flexmark.ext.wikilink._ import com.vladsch.flexmark.ext.wikilink.internal.WikiLinkLinkRefProcessor import com.vladsch.flexmark.util.ast._ +import com.vladsch.flexmark.ast._ import com.vladsch.flexmark.util.options._ import com.vladsch.flexmark.util.sequence.BasedSequence import com.vladsch.flexmark._ @@ -28,6 +29,11 @@ case class ExtendedFencedCodeBlock( hasContext: Boolean ) extends BlankLine(codeBlock.getContentChars()) +case class Section( + header: Heading, + body: List[Node] +) extends BlankLine(header.getContentChars()) + class DocFlexmarkParser(resolveLink: String => DocLink) extends Parser.ParserExtension: def parserOptions(opt: MutableDataHolder): Unit = () // noop @@ -80,7 +86,8 @@ object DocFlexmarkRenderer: val opts = MarkdownParser.mkMarkdownOptions( Seq( DocFlexmarkRenderer(renderLink), - SnippetRenderingExtension + SnippetRenderingExtension, + SectionRenderingExtension ) ) - HtmlRenderer.builder(opts).build().render(node) \ No newline at end of file + HtmlRenderer.builder(opts).build().render(node) diff --git a/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SectionRenderingExtension.scala b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SectionRenderingExtension.scala new file mode 100644 index 000000000000..9a075ea2a806 --- /dev/null +++ b/scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/SectionRenderingExtension.scala @@ -0,0 +1,49 @@ +package dotty.tools.scaladoc +package tasty.comments.markdown + +import com.vladsch.flexmark.html._ +import com.vladsch.flexmark.html.renderer._ +import com.vladsch.flexmark.parser._ +import com.vladsch.flexmark.ext.wikilink._ +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.util.html.AttributeImpl +import com.vladsch.flexmark._ +import com.vladsch.flexmark.ast.FencedCodeBlock + + +object SectionRenderingExtension extends HtmlRenderer.HtmlRendererExtension: + def rendererOptions(opt: MutableDataHolder): Unit = () + + case class AnchorLink(link: String) extends BlankLine(BasedSequence.EmptyBasedSequence()) + object SectionHandler extends CustomNodeRenderer[Section]: + val idGenerator = new HeaderIdGenerator.Factory().create() + override def render(node: Section, c: NodeRendererContext, html: HtmlWriter): Unit = + val Section(header, body) = node + val id = idGenerator.getId(header.getText) + val anchor = AnchorLink(s"#$id") + header.prependChild(anchor) + html.attr(AttributeImpl.of("id", id)).withAttr.tag("section", false, false, () => { + c.render(header) + body.foreach(c.render) + }) + + object AnchorLinkHandler extends CustomNodeRenderer[AnchorLink]: + override def render(node: AnchorLink, c: NodeRendererContext, html: HtmlWriter): Unit = + html.attr(AttributeImpl.of("href", node.link), AttributeImpl.of("class", "anchor")).withAttr.tag("a", false, false, () => ()) + + + object Render extends NodeRenderer: + override def getNodeRenderingHandlers: JSet[NodeRenderingHandler[_]] = + JSet( + new NodeRenderingHandler(classOf[Section], SectionHandler), + new NodeRenderingHandler(classOf[AnchorLink], AnchorLinkHandler) + ) + + object Factory extends NodeRendererFactory: + override def create(options: DataHolder): NodeRenderer = Render + + def extend(htmlRendererBuilder: HtmlRenderer.Builder, tpe: String): Unit = + htmlRendererBuilder.nodeRendererFactory(Factory) diff --git a/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala b/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala index cbb942e36476..aab9436c229d 100644 --- a/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala +++ b/scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala @@ -135,9 +135,12 @@ class TemplateFileTests: val expected = - """

Test page

+ """
+ |

Test page

|

Hello world!!

- |

Test page end

+ |
+ |

Test page end

+ |
|
""".stripMargin testContent( @@ -206,7 +209,9 @@ class TemplateFileTests: ext = "md" ) { t => assertEquals( - """

Hello there!

""", + """
+ |

Hello there!

+ |
""".stripMargin, t.resolveInner(RenderingContext(Map("msg" -> "there"))).code.trim()) } @@ -216,7 +221,10 @@ class TemplateFileTests: """# Hello {{ msg }}!""", ext = "md" ) { t => - assertEquals("""

Hello there!

""", + assertEquals( + """
+ |

Hello there!

+ |
""".stripMargin, t.resolveInner(RenderingContext(Map("msg" -> "there"))).code.trim()) }