Skip to content

Commit 12acddd

Browse files
authored
Table of contents extension (#76)
* Add a table of contents extension that shows up to the right side, sticky and highlights the first heading showing up in the page
1 parent 95318c4 commit 12acddd

File tree

7 files changed

+141
-2
lines changed

7 files changed

+141
-2
lines changed

cmd/assets/styles.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
font-size: 0.6em;
5858
margin: 0 0.4em;
5959
}
60-
&>p:first-child img:only-child {
60+
&>p:first-of-type img:only-child {
6161
border-radius: 0.5em;
6262
object-fit: cover;
6363
object-position: 0 50%;

extensions/all/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
_ "github.com/emad-elsaid/xlog/extensions/shortcode"
3434
_ "github.com/emad-elsaid/xlog/extensions/sitemap"
3535
_ "github.com/emad-elsaid/xlog/extensions/star"
36+
_ "github.com/emad-elsaid/xlog/extensions/toc"
3637
_ "github.com/emad-elsaid/xlog/extensions/todo"
3738
_ "github.com/emad-elsaid/xlog/extensions/upload_file"
3839
)

extensions/toc/extension.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package toc
2+
3+
import (
4+
"embed"
5+
"html/template"
6+
7+
"github.com/emad-elsaid/xlog"
8+
gtoc "go.abhg.dev/goldmark/toc"
9+
)
10+
11+
//go:embed templates
12+
var templates embed.FS
13+
14+
func init() {
15+
xlog.RegisterExtension(Extension{})
16+
}
17+
18+
type Extension struct{}
19+
20+
func (Extension) Name() string { return "toc" }
21+
func (Extension) Init() {
22+
xlog.RegisterWidget(xlog.WidgetBeforeView, 0, TOC)
23+
xlog.RegisterTemplate(templates, "templates")
24+
}
25+
26+
func TOC(p xlog.Page) template.HTML {
27+
if p == nil {
28+
return ""
29+
}
30+
31+
src, doc := p.AST()
32+
if src == nil || doc == nil {
33+
return ""
34+
}
35+
36+
tree, err := gtoc.Inspect(doc, src, gtoc.MaxDepth(1))
37+
if err != nil {
38+
return ""
39+
}
40+
41+
if len(tree.Items) == 0 {
42+
return ""
43+
}
44+
45+
return xlog.Partial("toc", xlog.Locals{"tree": tree})
46+
}

extensions/toc/templates/toc.html

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{{ define "toc-item" }}
2+
<li>
3+
<a href="#{{ printf "%s" .ID}}">{{ printf "%s" .Title}}</a>
4+
{{ with .Items }}
5+
<ul class="menu-list">
6+
{{ range . }}
7+
{{ template "toc-item" . }}
8+
{{ end }}
9+
</ul>
10+
{{ end }}
11+
</li>
12+
{{ end }}
13+
14+
{{ with .tree.Items }}
15+
<style>
16+
.table-of-contents{
17+
position: sticky;
18+
top: 8em;
19+
20+
#table-of-contents-list{
21+
position: absolute;
22+
top: 0;
23+
left: 100%;
24+
padding-left: 1em;
25+
white-space: nowrap;
26+
27+
ul{
28+
list-style: none;
29+
margin: 0;
30+
li a {
31+
max-width: 20em;
32+
overflow: hidden;
33+
text-overflow: ellipsis;
34+
}
35+
}
36+
}
37+
}
38+
</style>
39+
40+
<script>
41+
document.addEventListener("DOMContentLoaded", () => {
42+
const links = document.querySelectorAll("#table-of-contents-list a");
43+
const headings = document.querySelectorAll("h1");
44+
45+
const updateActiveLink = () => {
46+
let activeLink = null;
47+
let activeRect = null;
48+
let lastOut = null;
49+
50+
headings.forEach((heading, index) => {
51+
const rect = heading.getBoundingClientRect();
52+
let inViewPort = rect.top >= 0 && rect.top < window.innerHeight;
53+
let beforeActive = activeRect == null || rect.top < activeRect.top;
54+
if (inViewPort && beforeActive) {
55+
activeLink = links[index];
56+
activeRect = rect;
57+
}
58+
59+
let beforeViewPort = rect.top < 0;
60+
if(beforeViewPort){
61+
lastOut = links[index];
62+
}
63+
});
64+
65+
links.forEach((link) => link.classList.remove("is-active"));
66+
if (activeLink) {
67+
activeLink.classList.add("is-active");
68+
}else if(lastOut) {
69+
lastOut.classList.add("is-active");
70+
}
71+
};
72+
73+
window.addEventListener("scroll", updateActiveLink);
74+
updateActiveLink();
75+
});
76+
</script>
77+
78+
79+
<aside class="menu is-hidden-touch table-of-contents">
80+
<div id="table-of-contents-list">
81+
<p class="menu-label">TABLE OF CONTENTS</p>
82+
<ul class="menu-list">
83+
{{ range . }}
84+
{{ template "toc-item" . }}
85+
{{ end }}
86+
</ul>
87+
</div>
88+
</aside>
89+
{{ end }}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ require (
3030
require (
3131
github.com/davecgh/go-spew v1.1.1 // indirect
3232
github.com/pmezard/go-difflib v1.0.0 // indirect
33+
go.abhg.dev/goldmark/toc v0.10.0 // indirect
3334
golang.org/x/net v0.33.0 // indirect
3435
gopkg.in/yaml.v2 v2.3.0 // indirect
3536
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei
7070
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
7171
gitlab.com/greyxor/slogor v1.5.2 h1:ushBskVMbduRt3mWLpIct9mOGAkxnytVNoSn2IHBgXM=
7272
gitlab.com/greyxor/slogor v1.5.2/go.mod h1:P8dDYZxKsh/omu91z+vep/m78GxWEOc2KP2f0zaCasE=
73+
go.abhg.dev/goldmark/toc v0.10.0 h1:de3LrIimwtGhBMKh7aEl1c6n4XWwOdukIO5wOAMYZzg=
74+
go.abhg.dev/goldmark/toc v0.10.0/go.mod h1:OpH0qqRP9v/eosCV28ZeqGI78jZ8rri3C7Jh8fzEo2M=
7375
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
7476
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
7577
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=

public/style.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)