Skip to content

Commit 67062aa

Browse files
committed
Add scrollspy. Wrap header and its content into section.
1 parent 97d844e commit 67062aa

File tree

11 files changed

+142
-30
lines changed

11 files changed

+142
-30
lines changed

scaladoc/resources/dotty_res/scripts/ux.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ window.addEventListener("DOMContentLoaded", () => {
2020
$(this).parent().toggleClass("expanded")
2121
});
2222

23+
const observer = new IntersectionObserver(entries => {
24+
entries.forEach(entry => {
25+
const id = entry.target.getAttribute('id');
26+
if (entry.intersectionRatio > 0) {
27+
document.querySelector(`#toc li a[href="#${id}"]`).parentElement.classList.add('active');
28+
} else {
29+
document.querySelector(`#toc li a[href="#${id}"]`).parentElement.classList.remove('active');
30+
}
31+
});
32+
});
33+
34+
35+
document.querySelectorAll('#content section[id]').forEach((section) => {
36+
observer.observe(section);
37+
});
38+
2339
document.querySelectorAll("#sideMenu2 a").forEach(elem => elem.addEventListener('click', e => e.stopPropagation()))
2440

2541
$('.names .tab').on('click', function() {

scaladoc/resources/dotty_res/styles/scalastyle.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,16 @@ footer .socials {
935935
display: block;
936936
padding-top: 0.5em;
937937
padding-bottom: 0.5em;
938+
color: var(--leftbar-fg);
939+
transition-duration: 0.2s;
940+
}
941+
942+
#toc li:hover > a {
943+
color: var(--link-hover-fg);
944+
}
945+
946+
#toc li.active > a {
947+
color: var(--link-hover-fg);
938948
}
939949

940950
/* Signature coloring */

scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
161161
li(ul(renderTocRec(level + 1, prefix))) +: renderTocRec(level, suffix)
162162
}
163163

164-
renderTocRec(1, toc).headOption.map(toc => ul(cls := "toc-list")(toc))
164+
renderTocRec(1, toc).headOption.map(toc => nav(cls := "toc-nav")(ul(cls := "toc-list")(toc)))
165165

166166

167167
private def mkFrame(link: Link, parents: Vector[Link], content: => PageContent): AppliedTag =

scaladoc/src/dotty/tools/scaladoc/renderers/SiteRenderer.scala

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,12 @@ trait SiteRenderer(using DocContext) extends Locations:
7070

7171
val document = Jsoup.parse(content.resolved.code)
7272

73-
val toc = document.select("h1, h2, h3, h4, h5, h6").asScala.toSeq
74-
.map { elem =>
75-
val content = elem.text()
76-
77-
if elem.select("a[href]").isEmpty then {
78-
val normalizedText = content.trim.toLowerCase.split("\\s+").mkString("-")
79-
val anchor = Element("a")
80-
.attr("href", s"#$normalizedText")
81-
.id(normalizedText)
82-
.addClass("anchor")
83-
elem.insertChildren(0, JList(anchor))
73+
val toc = document.select("section[id]").asScala.toSeq
74+
.flatMap { elem =>
75+
val header = elem.selectFirst("h1, h2, h3, h4, h5, h6")
76+
Option(header).map { h =>
77+
TocEntry(h.tag().getName, h.text(), s"#${elem.id()}")
8478
}
85-
86-
TocEntry(elem.tag().getName, content, s"#${elem.selectFirst("a[href]").id()}")
8779
}
8880

8981
document.select("a").forEach(element =>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dotty.tools.scaladoc
2+
package site
3+
4+
import com.vladsch.flexmark.util.{ast => mdu, sequence}
5+
import com.vladsch.flexmark.{ast => mda}
6+
import com.vladsch.flexmark.formatter.Formatter
7+
import com.vladsch.flexmark.util.options.MutableDataSet
8+
import collection.JavaConverters._
9+
10+
import dotty.tools.scaladoc.tasty.comments.markdown.Section
11+
12+
object FlexmarkSectionWrapper {
13+
def apply(md: mdu.Document): mdu.Document = {
14+
val children = md.getChildren.asScala.toList
15+
val newChildren = getNewChildren(Nil, None, children)
16+
md.removeChildren()
17+
newChildren.foreach(md.appendChild)
18+
md
19+
}
20+
21+
def getNewChildren(finished: List[mdu.Node], current: Option[(mda.Heading, List[mdu.Node])], rest: List[mdu.Node]): List[mdu.Node] = rest match {
22+
case Nil => current.fold(finished)(finished :+ Section(_, _))
23+
case (h: mda.Heading) :: rest => current.fold(getNewChildren(finished, Some(h, Nil), rest))((head, b) => getNewChildren(finished :+ Section(head, b), Some(h, Nil), rest))
24+
case (n: mdu.Node) :: rest => current.fold(getNewChildren(finished :+ n, None, rest))((head, b) => getNewChildren(finished, Some(head, b :+ n), rest))
25+
}
26+
}

scaladoc/src/dotty/tools/scaladoc/site/common.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.vladsch.flexmark.parser.{Parser, ParserEmulationProfile}
1515
import com.vladsch.flexmark.util.options.{DataHolder, MutableDataSet}
1616
import com.vladsch.flexmark.ext.wikilink.WikiLinkExtension
1717
import com.vladsch.flexmark.formatter.Formatter
18+
import com.vladsch.flexmark.html.HtmlRenderer
1819

1920
import scala.collection.JavaConverters._
2021

@@ -24,21 +25,21 @@ val apiPageDRI: DRI = DRI(location = "api/index")
2425
def defaultMarkdownOptions(using ctx: StaticSiteContext): DataHolder =
2526
new MutableDataSet()
2627
.setFrom(ParserEmulationProfile.COMMONMARK.getOptions)
27-
.set(AnchorLinkExtension.ANCHORLINKS_WRAP_TEXT, false)
28-
.set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "anchor")
2928
.set(EmojiExtension.ROOT_IMAGE_PATH, "https://github.global.ssl.fastly.net/images/icons/emoji/")
3029
.set(WikiLinkExtension.LINK_ESCAPE_CHARS, "")
3130
.set(Parser.EXTENSIONS, java.util.Arrays.asList(
3231
TablesExtension.create(),
3332
TaskListExtension.create(),
3433
AutolinkExtension.create(),
35-
AnchorLinkExtension.create(),
3634
EmojiExtension.create(),
3735
YamlFrontMatterExtension.create(),
3836
StrikethroughExtension.create(),
3937
WikiLinkExtension.create(),
40-
tasty.comments.markdown.SnippetRenderingExtension
38+
tasty.comments.markdown.SnippetRenderingExtension,
39+
tasty.comments.markdown.SectionRenderingExtension
4140
))
41+
.set(HtmlRenderer.GENERATE_HEADER_ID, false)
42+
.set(HtmlRenderer.RENDER_HEADER_ID, false)
4243

4344
def emptyTemplate(file: File, title: String): TemplateFile = TemplateFile(
4445
file = file,

scaladoc/src/dotty/tools/scaladoc/site/templates.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package site
44
import java.io.File
55
import java.nio.file.{Files, Paths}
66

7-
import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
87
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
98
import com.vladsch.flexmark.ext.emoji.EmojiExtension
109
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
@@ -25,6 +24,7 @@ import scala.collection.JavaConverters._
2524

2625
import scala.io.Source
2726
import dotty.tools.scaladoc.snippets._
27+
import scala.util.chaining._
2828

2929
/** RenderingContext stores information about defined properties, layouts and sites being resolved
3030
*
@@ -123,9 +123,12 @@ case class TemplateFile(
123123
val code = if (isHtml || layoutTemplate.exists(!_.isHtml)) rendered else
124124
// Snippet compiler currently supports markdown only
125125
val parser: Parser = Parser.builder(defaultMarkdownOptions).build()
126-
val parsedMd = parser.parse(rendered)
127-
val processed = FlexmarkSnippetProcessor.processSnippets(parsedMd, None, snippetCheckingFunc, withContext = false)(using ssctx.outerCtx)
128-
HtmlRenderer.builder(defaultMarkdownOptions).build().render(processed)
126+
val parsedMd = parser.parse(rendered).pipe { md =>
127+
FlexmarkSnippetProcessor.processSnippets(md, None, snippetCheckingFunc, withContext = false)(using ssctx.outerCtx)
128+
}.pipe { md =>
129+
FlexmarkSectionWrapper(md)
130+
}
131+
HtmlRenderer.builder(defaultMarkdownOptions).build().render(parsedMd)
129132

130133
// If we have a layout template, we need to embed rendered content in it. Otherwise, we just leave the content as is.
131134
layoutTemplate match {

scaladoc/src/dotty/tools/scaladoc/snippets/FlexmarkSnippetProcessor.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import dotty.tools.scaladoc.tasty.comments.markdown.ExtendedFencedCodeBlock
1111
import dotty.tools.scaladoc.tasty.comments.PreparsedComment
1212

1313
object FlexmarkSnippetProcessor:
14-
def processSnippets(root: mdu.Node, preparsed: Option[PreparsedComment], checkingFunc: => SnippetChecker.SnippetCheckingFunc, withContext: Boolean)(using CompilerContext): mdu.Node = {
14+
def processSnippets[T <: mdu.Node](root: T, preparsed: Option[PreparsedComment], checkingFunc: => SnippetChecker.SnippetCheckingFunc, withContext: Boolean)(using CompilerContext): T = {
1515
lazy val cf: SnippetChecker.SnippetCheckingFunc = checkingFunc
1616

1717
val nodes = root.getDescendants().asScala.collect {
@@ -97,4 +97,4 @@ object FlexmarkSnippetProcessor:
9797
}
9898

9999
root
100-
}
100+
}

scaladoc/src/dotty/tools/scaladoc/tasty/comments/markdown/DocFlexmarkExtension.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.vladsch.flexmark.parser._
88
import com.vladsch.flexmark.ext.wikilink._
99
import com.vladsch.flexmark.ext.wikilink.internal.WikiLinkLinkRefProcessor
1010
import com.vladsch.flexmark.util.ast._
11+
import com.vladsch.flexmark.ast._
1112
import com.vladsch.flexmark.util.options._
1213
import com.vladsch.flexmark.util.sequence.BasedSequence
1314
import com.vladsch.flexmark._
@@ -28,6 +29,11 @@ case class ExtendedFencedCodeBlock(
2829
hasContext: Boolean
2930
) extends BlankLine(codeBlock.getContentChars())
3031

32+
case class Section(
33+
header: Heading,
34+
body: List[Node]
35+
) extends BlankLine(header.getContentChars())
36+
3137
class DocFlexmarkParser(resolveLink: String => DocLink) extends Parser.ParserExtension:
3238

3339
def parserOptions(opt: MutableDataHolder): Unit = () // noop
@@ -80,7 +86,8 @@ object DocFlexmarkRenderer:
8086
val opts = MarkdownParser.mkMarkdownOptions(
8187
Seq(
8288
DocFlexmarkRenderer(renderLink),
83-
SnippetRenderingExtension
89+
SnippetRenderingExtension,
90+
SectionRenderingExtension
8491
)
8592
)
86-
HtmlRenderer.builder(opts).build().render(node)
93+
HtmlRenderer.builder(opts).build().render(node)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package dotty.tools.scaladoc
2+
package tasty.comments.markdown
3+
4+
import com.vladsch.flexmark.html._
5+
import com.vladsch.flexmark.html.renderer._
6+
import com.vladsch.flexmark.parser._
7+
import com.vladsch.flexmark.ext.wikilink._
8+
import com.vladsch.flexmark.ext.wikilink.internal.WikiLinkLinkRefProcessor
9+
import com.vladsch.flexmark.util.ast._
10+
import com.vladsch.flexmark.util.options._
11+
import com.vladsch.flexmark.util.sequence.BasedSequence
12+
import com.vladsch.flexmark.util.html.AttributeImpl
13+
import com.vladsch.flexmark._
14+
import com.vladsch.flexmark.ast.FencedCodeBlock
15+
16+
17+
object SectionRenderingExtension extends HtmlRenderer.HtmlRendererExtension:
18+
def rendererOptions(opt: MutableDataHolder): Unit = ()
19+
20+
case class AnchorLink(link: String) extends BlankLine(BasedSequence.EmptyBasedSequence())
21+
object SectionHandler extends CustomNodeRenderer[Section]:
22+
val idGenerator = new HeaderIdGenerator.Factory().create()
23+
override def render(node: Section, c: NodeRendererContext, html: HtmlWriter): Unit =
24+
val Section(header, body) = node
25+
val id = idGenerator.getId(header.getText)
26+
val anchor = AnchorLink(s"#$id")
27+
header.prependChild(anchor)
28+
html.attr(AttributeImpl.of("id", id)).withAttr.tag("section", false, false, () => {
29+
c.render(header)
30+
body.foreach(c.render)
31+
})
32+
33+
object AnchorLinkHandler extends CustomNodeRenderer[AnchorLink]:
34+
override def render(node: AnchorLink, c: NodeRendererContext, html: HtmlWriter): Unit =
35+
html.attr(AttributeImpl.of("href", node.link), AttributeImpl.of("class", "anchor")).withAttr.tag("a", false, false, () => ())
36+
37+
38+
object Render extends NodeRenderer:
39+
override def getNodeRenderingHandlers: JSet[NodeRenderingHandler[_]] =
40+
JSet(
41+
new NodeRenderingHandler(classOf[Section], SectionHandler),
42+
new NodeRenderingHandler(classOf[AnchorLink], AnchorLinkHandler)
43+
)
44+
45+
object Factory extends NodeRendererFactory:
46+
override def create(options: DataHolder): NodeRenderer = Render
47+
48+
def extend(htmlRendererBuilder: HtmlRenderer.Builder, tpe: String): Unit =
49+
htmlRendererBuilder.nodeRendererFactory(Factory)

scaladoc/test/dotty/tools/scaladoc/site/TemplateFileTests.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,12 @@ class TemplateFileTests:
135135

136136

137137
val expected =
138-
"""<div id="root"><h1><a href="#test-page" id="test-page" class="anchor"></a>Test page</h1>
138+
"""<div id="root"><section id="test-page">
139+
|<h1><a href="#test-page" class="anchor"></a>Test page</h1>
139140
|<p>Hello world!!</p>
140-
|<h2><a href="#test-page-end" id="test-page-end" class="anchor"></a>Test page end</h2>
141+
|</section><section id="test-page-end">
142+
|<h2><a href="#test-page-end" class="anchor"></a>Test page end</h2>
143+
|</section>
141144
|</div>""".stripMargin
142145

143146
testContent(
@@ -206,7 +209,9 @@ class TemplateFileTests:
206209
ext = "md"
207210
) { t =>
208211
assertEquals(
209-
"""<h1><a href="#hello-there" id="hello-there" class="anchor"></a>Hello there!</h1>""",
212+
"""<section id="hello-there">
213+
|<h1><a href="#hello-there" class="anchor"></a>Hello there!</h1>
214+
|</section>""".stripMargin,
210215
t.resolveInner(RenderingContext(Map("msg" -> "there"))).code.trim())
211216
}
212217

@@ -216,7 +221,10 @@ class TemplateFileTests:
216221
"""# Hello {{ msg }}!""",
217222
ext = "md"
218223
) { t =>
219-
assertEquals("""<h1><a href="#hello-there" id="hello-there" class="anchor"></a>Hello there!</h1>""",
224+
assertEquals(
225+
"""<section id="hello-there">
226+
|<h1><a href="#hello-there" class="anchor"></a>Hello there!</h1>
227+
|</section>""".stripMargin,
220228
t.resolveInner(RenderingContext(Map("msg" -> "there"))).code.trim())
221229
}
222230

0 commit comments

Comments
 (0)