Skip to content

Commit bfd9dfa

Browse files
committed
Add Table of Contents to static sites
1 parent ba57cdd commit bfd9dfa

File tree

10 files changed

+120
-18
lines changed

10 files changed

+120
-18
lines changed

docs/_layouts/doc-page.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ <h1>{{ page.title }}</h1>
1515
</header>
1616
{{ content }}
1717
<div class="content-contributors hidden">
18-
<h5>Contributors to this page:</h5>
18+
<span><b>Contributors to this page</b></span>
1919
<div id="documentation-contributors" class="contributors-container"></div>
2020
</div>
2121
</main>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<h1>Header 1</h1>
2+
<h2>Header 2</h2>
3+
<h3>Header 3</h3>

scaladoc-testcases/docs/sidebar.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ subsection:
44
- page: docs/f2.md
55
- page: docs/f3.md
66
- page: docs/f4.md
7+
- page: docs/f5.html

scaladoc/resources/dotty_res/styles/scalastyle.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
/* Layout Settings (changes on small screens) */
1212
--side-width: 300px;
13+
--toc-width: 300px;
1314
--content-padding: 24px 42px;
1415
--footer-height: 42px;
1516
}
@@ -43,6 +44,7 @@ body, button, input {
4344
#main-content {
4445
min-height: calc(100vh - var(--footer-height) - 24px);
4546
margin-left: var(--side-width);
47+
margin-right: var(--toc-width);
4648
padding: var(--content-padding);
4749
padding-bottom: calc(24px + var(--footer-height));
4850

@@ -905,6 +907,36 @@ footer .socials {
905907
height: 8px;
906908
}
907909

910+
#toc {
911+
position: fixed;
912+
right: 0px;
913+
top: 0px;
914+
width: var(--toc-width);
915+
height: 100%;
916+
917+
padding-top: 15vh;
918+
padding-right: 10px;
919+
920+
display: flex;
921+
flex-direction: column;
922+
}
923+
924+
#toc .toc-title {
925+
font-weight: bold;
926+
padding-left: 16px;
927+
}
928+
929+
#toc ul {
930+
list-style-type: none;
931+
padding-left: 16px;
932+
}
933+
934+
#toc a {
935+
display: block;
936+
padding-top: 0.5em;
937+
padding-bottom: 0.5em;
938+
}
939+
908940
/* Signature coloring */
909941

910942
:root {
@@ -941,6 +973,12 @@ footer .socials {
941973
}
942974
}
943975
/* Landscape phones, portait tablets */
976+
@media(max-width: 1200px) {
977+
:root {
978+
--toc-width: 0px;
979+
}
980+
}
981+
944982
@media(max-width: 768px) {
945983
:root {
946984
--content-padding: 12px 12px;
@@ -1086,6 +1124,7 @@ footer .socials {
10861124
flex-direction: column;
10871125

10881126
transition: color 0.5s, background-color 0.5s;
1127+
z-index: 3;
10891128
}
10901129

10911130
.arrows.previous, .arrows.next {

scaladoc/src/dotty/tools/scaladoc/api.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dotty.tools.scaladoc
22

33
import dotty.tools.scaladoc.tasty.comments.Comment
4+
import util.HTML.AppliedTag
45

56
enum Visibility(val name: String):
67
case Unrestricted extends Visibility("")
@@ -258,3 +259,18 @@ case class SnippetCompilerData(
258259
imports: List[String],
259260
position: SnippetCompilerData.Position
260261
)
262+
263+
case class PageContent(content: AppliedTag, toc: Seq[TocEntry])
264+
265+
case class TocEntry(level: Int, content: String, anchor: String)
266+
267+
object TocEntry:
268+
val tagLevels: Map[String, Int] = Map(
269+
("h1" -> 1),
270+
("h2" -> 2),
271+
("h3" -> 3),
272+
("h4" -> 4),
273+
("h5" -> 5),
274+
("h6" -> 6)
275+
)
276+
def apply(tag: String, content: String, anchor: String): TocEntry = TocEntry(tagLevels(tag), content, anchor)

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
2222
html(
2323
mkHead(page),
2424
body(
25-
if !page.hasFrame then renderContent(page)
25+
if !page.hasFrame then renderContent(page).content
2626
else mkFrame(page.link, parents, renderContent(page))
2727
)
2828
)
@@ -148,7 +148,23 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
148148
)
149149
}
150150

151-
private def mkFrame(link: Link, parents: Vector[Link], content: => AppliedTag): AppliedTag =
151+
private def renderTableOfContents(toc: Seq[TocEntry]): Option[AppliedTag] =
152+
def renderTocRec(level: Int, rest: Seq[TocEntry]): Seq[AppliedTag] =
153+
rest match {
154+
case Nil => Nil
155+
case head :: tail if head.level == level =>
156+
val (nested, rest) = tail.span(_.level > level)
157+
val nestedList = if nested.nonEmpty then Seq(ul(renderTocRec(level + 1, nested))) else Nil
158+
li(a(href := head.anchor)(head.content), nestedList) +: renderTocRec(level, rest)
159+
case rest @ (head :: tail) if head.level > level =>
160+
val (prefix, suffix) = rest.span(_.level > level)
161+
li(ul(renderTocRec(level + 1, prefix))) +: renderTocRec(level, suffix)
162+
}
163+
164+
renderTocRec(1, toc).headOption.map(toc => ul(cls := "toc-list")(toc))
165+
166+
167+
private def mkFrame(link: Link, parents: Vector[Link], content: => PageContent): AppliedTag =
152168
val projectLogo =
153169
args.projectLogo.map { path =>
154170
val fileName = Paths.get(path).getFileName()
@@ -202,7 +218,7 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
202218
div(id := "scaladoc-searchBar"),
203219
main(id := "main-content")(
204220
parentsHtml,
205-
div(id := "content")(content),
221+
div(id := "content")(content.content),
206222
),
207223
footer(
208224
div(id := "generated-by")(
@@ -239,5 +255,11 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
239255
)
240256
)
241257
)
242-
)
258+
),
259+
renderTableOfContents(content.toc).fold(Nil) { toc =>
260+
div(id := "toc")(
261+
span(cls := "toc-title")("In this article"),
262+
toc
263+
)
264+
}
243265
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class MarkdownRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx
2222
super.render()
2323

2424
override def pageContent(page: Page, parents: Vector[Link]): AppliedTag =
25-
renderContent(page)
25+
renderContent(page).content
2626

2727
private def renderResources(): Seq[String] =
2828
allResources(Nil).flatMap(renderResource)

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext
387387
div(cls := "filterLowerContainer")()
388388
)
389389

390-
def fullMember(m: Member): AppliedTag =
390+
def fullMember(m: Member): PageContent =
391391
val intro = m.kind match
392392
case Kind.RootPackage =>Seq(h1(summon[DocContext].args.name))
393393
case _ =>
@@ -401,11 +401,13 @@ class MemberRenderer(signatureRenderer: SignatureRenderer)(using DocContext) ext
401401
memberSignature(m)
402402
)
403403
)
404-
405-
div(
406-
intro,
407-
memberInfo(m, withBrief = false),
408-
classLikeParts(m),
409-
buildDocumentableFilter, // TODO Need to make it work in JS :(
410-
buildMembers(m)
404+
PageContent(
405+
div(
406+
intro,
407+
memberInfo(m, withBrief = false),
408+
classLikeParts(m),
409+
buildDocumentableFilter, // TODO Need to make it work in JS :(
410+
buildMembers(m)
411+
),
412+
Seq.empty // For now, we don't support table of contents in members
411413
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ abstract class Renderer(rootPackage: Member, val members: Map[DRI, Member], prot
115115

116116
all
117117

118-
def renderContent(page: Page) = page.content match
118+
def renderContent(page: Page): PageContent = page.content match
119119
case m: Member =>
120120
val signatureRenderer = new SignatureRenderer:
121121
def currentDri: DRI = page.link.dri
@@ -126,7 +126,7 @@ abstract class Renderer(rootPackage: Member, val members: Map[DRI, Member], prot
126126

127127
MemberRenderer(signatureRenderer).fullMember(m)
128128
case t: ResolvedTemplate => siteContent(page.link.dri, t)
129-
case a: String => raw(a)
129+
case a: String => PageContent(raw(a), Seq.empty)
130130

131131

132132

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import java.net.URL
88
import dotty.tools.scaladoc.site._
99
import scala.util.Try
1010
import org.jsoup.Jsoup
11+
import org.jsoup.nodes.Element
1112
import java.nio.file.Paths
1213
import java.nio.file.Path
1314
import java.nio.file.Files
1415
import java.io.File
16+
import scala.util.chaining._
1517

1618
case class ResolvedTemplate(template: LoadedTemplate, ctx: StaticSiteContext):
1719
val resolved = template.resolveToHtml(ctx)
@@ -26,7 +28,7 @@ trait SiteRenderer(using DocContext) extends Locations:
2628

2729
private val HashRegex = "([^#]+)(#.+)".r
2830

29-
def siteContent(pageDri: DRI, content: ResolvedTemplate): AppliedTag =
31+
def siteContent(pageDri: DRI, content: ResolvedTemplate): PageContent =
3032
import content.ctx
3133
def tryAsDri(str: String): Option[String] =
3234
val (path, prefix) = str match
@@ -67,10 +69,27 @@ trait SiteRenderer(using DocContext) extends Locations:
6769
processLocalLink(str)
6870

6971
val document = Jsoup.parse(content.resolved.code)
72+
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))
84+
}
85+
86+
TocEntry(elem.tag().getName, content, s"#${elem.selectFirst("a[href]").id()}")
87+
}
88+
7089
document.select("a").forEach(element =>
7190
element.attr("href", processLocalLinkWithGuard(element.attr("href")))
7291
)
7392
document.select("img").forEach { element =>
7493
element.attr("src", processLocalLink(element.attr("src")))
7594
} // foreach does not work here. Why?
76-
raw(document.outerHtml())
95+
PageContent(raw(document.outerHtml()), toc)

0 commit comments

Comments
 (0)