Google Ads 廣告欄位
🚧 請讀者考慮關閉廣告封鎖器 🚧
或將本站設定為白名單
以支持本站之運營

Hugo 主題開發 - Table of Contents 跟隨閱讀進度標示章節

前一陣子久違的更新一下 blog 主題 hugo-theme-monochrome,加了一個 TOC 跟隨閱讀進度標示章節的功能。

之前的做法,只是簡單的把 TOC 渲染出來,不過因為左右兩邊很空,一直都有想要把他調整到右邊然後做到跟隨閱讀進度同步,只是總是想著能用就好就擱置了 (X)。

實際研究出來後覺得做法滿酷的,所以在這邊記錄一下。

問題

主要會遇到的問題是 hugo 預設會將文章渲染出以下樣子:

1
2
3
4
<h1>...</h1>
<p>...</p>
<h2>...</h2>
<p>...</p>

但這個功能會需要以下這個結構比較好做:

1
2
3
4
5
6
7
8
<section>
    <h1>...</h1>
    <p>...</p>
</section>
<section>
    <h2>...</h2>
    <p>...</p>
</section>

所以會需要一些小技巧在渲染 html 時,就將 section 切好,方便後續使用 javascript 控制。

將渲染的文章拆解成 section

利用到的技巧是 Hugo 的 Heading render hooks,透過 layouts/_markup/render-heading.html 自訂 header 的 render template:

1
2
3
{{ printf "<!-- end-chunk -->" | safeHTML }}
{{ (printf "<!-- begin-chunk data-anchor=%q -->" .Anchor) | safeHTML }}
<h{{ .Level }}>{{ .Text | safeHTML }}</h{{ .Level }}>

所以上面的例子 html 在渲染時,會產生出以下的內容,存在 .Content 內:

1
2
3
4
5
6
7
8
<!-- end-chunk -->
<!-- begin-chunk data-anchor=A -->
<h1>...</h1>
<p>...</p>
<!-- end-chunk -->
<!-- begin-chunk data-anchor=B -->
<h2>...</h2>
<p>...</p>

之後進到 layout 的 template render 時,就可以透過正則把前面偷偷插入的 <!-- end-chunk --> 來拆解 section,將內部的 section 內容取出,順便將 data-anchor 的 id 取出,包在 <section> 內:

layouts/single.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<article>
    {{ $chunks := split .Content "<!-- end-chunk -->" }}
    {{ range $chunks }}
        {{ $section_content := "" }}
        {{ if (gt (len (findRE `<!-- begin-chunk.+?-->` .)) 0) }}
            {{ $anchor := replaceRE `(?s).+data-anchor="(.+?)".+` "$1" . }}
            {{ $chunk := replaceRE `(?s)<!-- begin-chunk.+?-->(.+?)(?:<!-- end-chunk -->|$)` "$1" . }}
            {{ $section_content = printf "<section id=\"%s\">%s</section>" $anchor $chunk }}
        {{ else if (gt (len .) 0) }}
            {{ $section_content = printf "<section>%s</section>" . }}
        {{ end }}
        {{ $section_content | safeHTML }}
    {{ end }}
</article>

就可以變出這個結構了:

1
2
3
4
5
6
7
8
<section id="A">
    <h1>...</h1>
    <p>...</p>
</section>
<section id="B">
    <h2>...</h2>
    <p>...</p>
</section>

透過 Javascript 的 IntersectionObserver 追蹤目前閱讀進度

有了以上的準備,就可以用 Javascript 的 IntersectionObserver 來實現閱讀進度的追蹤:

toc-tracker.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
window.addEventListener('DOMContentLoaded', () => {
    const sections = document.querySelectorAll('section[id]');

    const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
            const id = entry.target.getAttribute('id');
            const activeLink = document.querySelector(`#TableOfContents li a[href="#${id}"]`);
            if (!activeLink) return;

            if (entry.intersectionRatio > 0) {
                activeLink.parentElement.classList.add('active');
            } else {
                activeLink.parentElement.classList.remove('active');
            }
        });
    });

    sections.forEach((section) => {
        observer.observe(section);
    });
});

當 section 進入當前視角時,去對 #TableOfContents li a[href="#${id}"] 對應 id 的 entry 加上 class,就可以做出跟隨閱讀進度的效果了。

Google Ads 廣告欄位
🚧 請讀者考慮關閉廣告封鎖器 🚧
或將本站設定為白名單
以支持本站之運營