Skip to content

Commit f704c48

Browse files
authored
markdown-it plugins (#965)
1 parent f6b9ab5 commit f704c48

File tree

5 files changed

+46
-8
lines changed

5 files changed

+46
-8
lines changed

docs/config.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,16 @@ toc: false
195195
## search
196196

197197
Whether to enable [search](./search) on the project; defaults to false.
198+
199+
## markdownIt
200+
201+
A hook for registering additional [markdown-it](https://github.com/markdown-it/markdown-it) plugins. For example, to use [markdown-it-footnote](https://github.com/markdown-it/markdown-it-footnote):
202+
203+
```js
204+
import type MarkdownIt from "markdown-it";
205+
import MarkdownItFootnote from "markdown-it-footnote";
206+
207+
export default {
208+
markdownIt: (md: MarkdownIt) => md.use(MarkdownItFootnote);
209+
};
210+
```

src/config.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import op from "node:path";
22
import {basename, dirname, join} from "node:path/posix";
33
import {cwd} from "node:process";
44
import {pathToFileURL} from "node:url";
5+
import type MarkdownIt from "markdown-it";
56
import {visitMarkdownFiles} from "./files.js";
67
import {formatIsoDate, formatLocaleDate} from "./format.js";
78
import {parseMarkdown} from "./markdown.js";
@@ -50,6 +51,7 @@ export interface Config {
5051
style: null | Style; // defaults to {theme: ["light", "dark"]}
5152
deploy: null | {workspace: string; project: string};
5253
search: boolean; // default to false
54+
markdownIt?: (md: MarkdownIt) => MarkdownIt;
5355
}
5456

5557
export async function readConfig(configPath?: string, root?: string): Promise<Config> {
@@ -107,6 +109,7 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
107109
currentDate
108110
)}">${formatLocaleDate(currentDate)}</a>.`
109111
} = spec;
112+
const {markdownIt} = spec;
110113
root = String(root);
111114
output = String(output);
112115
base = normalizeBase(base);
@@ -125,7 +128,25 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
125128
toc = normalizeToc(toc);
126129
deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null;
127130
search = Boolean(search);
128-
return {root, output, base, title, sidebar, pages, pager, scripts, head, header, footer, toc, style, deploy, search};
131+
if (markdownIt !== undefined && typeof markdownIt !== "function") throw new Error("markdownIt must be a function");
132+
return {
133+
root,
134+
output,
135+
base,
136+
title,
137+
sidebar,
138+
pages,
139+
pager,
140+
scripts,
141+
head,
142+
header,
143+
footer,
144+
toc,
145+
style,
146+
deploy,
147+
search,
148+
markdownIt
149+
};
129150
}
130151

131152
function normalizeBase(base: any): string {

src/markdown.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,16 +272,17 @@ function makeSoftbreakRenderer(baseRenderer: RenderRule): RenderRule {
272272
export interface ParseOptions {
273273
root: string;
274274
path: string;
275+
markdownIt?: Config["markdownIt"];
275276
style?: Config["style"];
276277
}
277278

278279
export async function parseMarkdown(
279280
sourcePath: string,
280-
{root, path, style: configStyle}: ParseOptions
281+
{root, path, markdownIt = (md) => md, style: configStyle}: ParseOptions
281282
): Promise<MarkdownPage> {
282283
const source = await readFile(sourcePath, "utf-8");
283284
const parts = matter(source, {});
284-
const md = MarkdownIt({html: true});
285+
const md = markdownIt(MarkdownIt({html: true}));
285286
md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})});
286287
md.inline.ruler.push("placeholder", transformPlaceholderInline);
287288
md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore);

src/preview.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ function getWatchFiles(resolvers: Resolvers): Iterable<string> {
282282
return files;
283283
}
284284

285-
function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style}: Config) {
285+
function handleWatch(socket: WebSocket, req: IncomingMessage, config: Config) {
286+
const {root} = config;
286287
let path: string | null = null;
287288
let hash: string | null = null;
288289
let html: string[] | null = null;
@@ -312,7 +313,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style}: Con
312313
break;
313314
}
314315
case "change": {
315-
const page = await parseMarkdown(join(root, path), {root, path, style});
316+
const page = await parseMarkdown(join(root, path), {path, ...config});
316317
// delay to avoid a possibly-empty file
317318
if (!force && page.html === "") {
318319
if (!emptyTimeout) {
@@ -361,7 +362,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style}: Con
361362
if (!(path = normalize(path)).startsWith("/")) throw new Error("Invalid path: " + initialPath);
362363
if (path.endsWith("/")) path += "index";
363364
path += ".md";
364-
const page = await parseMarkdown(join(root, path), {root, path, style});
365+
const page = await parseMarkdown(join(root, path), {path, ...config});
365366
const resolvers = await getResolvers(page, {root, path});
366367
if (resolvers.hash !== initialHash) return void send({type: "reload"});
367368
hash = resolvers.hash;

test/config-test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ describe("readConfig(undefined, root)", () => {
3030
workspace: "acme",
3131
project: "bi"
3232
},
33-
search: false
33+
search: false,
34+
markdownIt: undefined
3435
});
3536
});
3637
it("returns the default config if no config file is found", async () => {
@@ -50,7 +51,8 @@ describe("readConfig(undefined, root)", () => {
5051
footer:
5152
'Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="2024-01-11T01:02:03">Jan 11, 2024</a>.',
5253
deploy: null,
53-
search: false
54+
search: false,
55+
markdownIt: undefined
5456
});
5557
});
5658
});

0 commit comments

Comments
 (0)