Skip to content

Commit 30bb912

Browse files
committed
heading anchors
1 parent 4b1e30d commit 30bb912

13 files changed

+29
-10
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ TODO
9494
- implicitly await ${…} expressions before calling display?
9595
- promote img, video, audio, and picture elements to file attachments
9696
- add rel="nofollow noindex" to external links by default
97+
- ✅ automatic anchor links for heading elements
9798
- themes
9899
- ✅ default light/dark theme
99100
- dashboard theme for wide layout

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"gray-matter": "^4.0.3",
3636
"highlight.js": "^11.8.0",
3737
"markdown-it": "^13.0.2",
38+
"markdown-it-anchor": "^8.6.7",
3839
"mime": "^3.0.0",
3940
"send": "^0.18.0",
4041
"tsx": "^3.13.0",

public/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ figcaption {
358358
color: var(--syntax-unknown-variable);
359359
}
360360

361+
a[href].observablehq-header-anchor {
362+
color: inherit;
363+
}
364+
361365
.observablehq--block {
362366
margin: 1rem 0;
363367
}

src/markdown.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import matter from "gray-matter";
22
import hljs from "highlight.js";
33
import MarkdownIt from "markdown-it";
4+
import MarkdownItAnchor from "markdown-it-anchor";
45
import type {RuleCore} from "markdown-it/lib/parser_core.js";
56
import type {RuleInline} from "markdown-it/lib/parser_inline.js";
67
import type {RenderRule} from "markdown-it/lib/renderer.js";
@@ -197,6 +198,7 @@ export function parseMarkdown(source: string, root: string): ParseResult {
197198
return ""; // defaults to escapeHtml(str)
198199
}
199200
});
201+
md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})});
200202
md.inline.ruler.push("placeholder", transformPlaceholderInline);
201203
md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore);
202204
md.renderer.rules.placeholder = makePlaceholderRenderer(root);
@@ -225,8 +227,14 @@ function findTitle(tokens: ReturnType<MarkdownIt["parse"]>): string | undefined
225227
for (const [i, token] of tokens.entries()) {
226228
if (token.type === "heading_open" && token.tag === "h1") {
227229
const next = tokens[i + 1];
228-
if (next?.type === "inline" && next.children?.[0]?.type === "text") {
229-
return next.content;
230+
if (next?.type === "inline") {
231+
const text = next.children
232+
?.filter((t) => t.type === "text")
233+
.map((t) => t.content)
234+
.join("");
235+
if (text) {
236+
return text;
237+
}
230238
}
231239
}
232240
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
<h1>Embedded expression</h1>
1+
<h1 id="embedded-expression" tabindex="-1"><a class="observablehq-header-anchor" href="#embedded-expression">Embedded expression</a></h1>
22
<p>One plus two is <span id="cell-1"></span>.</p>

test/output/fenced-code.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<h1>Fenced code</h1>
1+
<h1 id="fenced-code" tabindex="-1"><a class="observablehq-header-anchor" href="#fenced-code">Fenced code</a></h1>
22
<div id="cell-1" class="observablehq observablehq--block"></div>
33
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">add</span>(<span class="hljs-params">a, b</span>) {
44
<span class="hljs-keyword">return</span> a + b;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<h1><span id="cell-1"></span></h1>
1+
<h1 id="" tabindex="-1"><a class="observablehq-header-anchor" href="#"><span id="cell-1"></span></a></h1>

test/output/hello-world.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
<h1>Hello, world!</h1>
1+
<h1 id="hello%2C-world!" tabindex="-1"><a class="observablehq-header-anchor" href="#hello%2C-world!">Hello, world!</a></h1>
22
<p>This is a test.</p>

test/output/script-expression.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<h1>Script expression</h1>
1+
<h1 id="script-expression" tabindex="-1"><a class="observablehq-header-anchor" href="#script-expression">Script expression</a></h1>
22
<script type="module">
33

44
const subject = "world";

test/output/tex-expression.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
<h1>Hello, <span id="cell-1"></span></h1>
1+
<h1 id="hello%2C" tabindex="-1"><a class="observablehq-header-anchor" href="#hello%2C">Hello, <span id="cell-1"></span></a></h1>
22
<p>My favorite equation is <span id="cell-2"></span>.</p>

0 commit comments

Comments
 (0)