Skip to content

Commit 2d33349

Browse files
authored
link headings with id (#1293)
* link headings with id * fix win32 test * encodeURI
1 parent 34426f2 commit 2d33349

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+75
-67
lines changed

src/client/toc.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ const toc = document.querySelector<HTMLElement>("#observablehq-toc");
22
if (toc) {
33
const highlight = toc.appendChild(document.createElement("div"));
44
highlight.classList.add("observablehq-secondary-link-highlight");
5-
const headings = Array.from(document.querySelector("#observablehq-main")!.querySelectorAll(toc.dataset.selector!))
6-
.filter((e) => e.querySelector("a.observablehq-header-anchor"))
7-
.reverse();
5+
const main = document.querySelector("#observablehq-main")!;
6+
const headings = Array.from(main.querySelectorAll(toc.dataset.selector!)).reverse();
87
const links = toc.querySelectorAll<HTMLElement>(".observablehq-secondary-link");
98
const relink = (): HTMLElement | undefined => {
109
for (const link of links) {
@@ -13,7 +12,7 @@ if (toc) {
1312
// If there’s a location.hash, highlight that if it’s at the top of the viewport.
1413
if (location.hash) {
1514
for (const heading of headings) {
16-
const hash = heading.querySelector<HTMLAnchorElement>("a[href]")?.hash;
15+
const hash = encodeURI(`#${heading.id}`);
1716
if (hash === location.hash) {
1817
const top = heading.getBoundingClientRect().top;
1918
if (0 < top && top < 40) {

src/html.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ export function rewriteHtml(
172172
code.innerHTML = html;
173173
}
174174

175+
// Wrap <h2 id> etc. elements in <a> tags for linking.
176+
for (const h of document.querySelectorAll<HTMLHeadingElement>("h1[id], h2[id], h3[id], h4[id]")) {
177+
const a = document.createElement("a");
178+
a.className = "observablehq-header-anchor";
179+
a.href = `#${h.id}`;
180+
a.append(...h.childNodes);
181+
h.append(a);
182+
}
183+
175184
return document.body.innerHTML;
176185
}
177186

src/markdown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export function createMarkdownIt({
322322
} = {}): MarkdownIt {
323323
const md = MarkdownIt({html: true, linkify, typographer, quotes});
324324
if (linkify) md.linkify.set({fuzzyLink: false, fuzzyEmail: false});
325-
md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})});
325+
md.use(MarkdownItAnchor);
326326
md.inline.ruler.push("placeholder", transformPlaceholderInline);
327327
md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore);
328328
md.renderer.rules.placeholder = makePlaceholderRenderer();

src/render.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,12 @@ interface Header {
177177
href: string;
178178
}
179179

180-
const tocSelector = "h1:not(:first-of-type), h2:first-child, :not(h1) + h2";
180+
const tocSelector = "h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]";
181181

182182
function findHeaders(page: MarkdownPage): Header[] {
183183
return Array.from(parseHtml(page.body).document.querySelectorAll(tocSelector))
184-
.map((node) => ({label: node.textContent, href: node.firstElementChild?.getAttribute("href")}))
185-
.filter((d): d is Header => !!d.label && !!d.href);
184+
.map((node) => ({label: node.textContent, href: `#${node.id}`}))
185+
.filter((d): d is Header => !!d.label);
186186
}
187187

188188
function renderToc(headers: Header[], label: string): Html {

test/output/build/404/404.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import "./_observablehq/client.js";
2525

2626
</script>
27-
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type), h2:first-child, :not(h1) + h2">
27+
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
2828
<nav>
2929
</nav>
3030
</aside>

test/output/build/archives.posix/tar.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
</ol>
8080
</nav>
8181
<script>{/* redacted init script */}</script>
82-
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type), h2:first-child, :not(h1) + h2">
82+
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
8383
<nav>
8484
</nav>
8585
</aside>

test/output/build/archives.posix/zip.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
</ol>
6363
</nav>
6464
<script>{/* redacted init script */}</script>
65-
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type), h2:first-child, :not(h1) + h2">
65+
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
6666
<nav>
6767
</nav>
6868
</aside>

test/output/build/archives.win32/tar.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
</ol>
5252
</nav>
5353
<script>{/* redacted init script */}</script>
54-
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type), h2:first-child, :not(h1) + h2">
54+
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
5555
<nav>
5656
</nav>
5757
</aside>

test/output/build/archives.win32/zip.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
</ol>
4545
</nav>
4646
<script>{/* redacted init script */}</script>
47-
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type), h2:first-child, :not(h1) + h2">
47+
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
4848
<nav>
4949
</nav>
5050
</aside>

test/output/build/config/closed/page.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
</ol>
3636
</nav>
3737
<script>{/* redacted init script */}</script>
38-
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type), h2:first-child, :not(h1) + h2">
38+
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
3939
<nav>
4040
</nav>
4141
</aside>

0 commit comments

Comments
 (0)