Skip to content

Commit 07d3669

Browse files
committed
rebase & simplify
1 parent d244fa1 commit 07d3669

File tree

6 files changed

+81
-16
lines changed

6 files changed

+81
-16
lines changed

docs/config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ An HTML fragment to add to the header. Defaults to the empty string.
164164

165165
An HTML fragment to add to the footer. Defaults to “Built with Observable.”
166166

167+
head, header and footer can be specified as strings, or as functions that receive as arguments the page’s title, front matter, and path, and return a string.
168+
167169
## base
168170

169171
The base path when serving the site. Currently this only affects the custom 404 page, if any.

src/config.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import wrapAnsi from "wrap-ansi";
1010
import {LoaderResolver} from "./dataloader.js";
1111
import {visitMarkdownFiles} from "./files.js";
1212
import {formatIsoDate, formatLocaleDate} from "./format.js";
13+
import type {FrontMatter} from "./frontMatter.js";
1314
import {createMarkdownIt, parseMarkdownMetadata} from "./markdown.js";
1415
import {isAssetPath, parseRelativeUrl, resolvePath} from "./path.js";
1516
import {resolveTheme} from "./theme.js";
@@ -43,6 +44,11 @@ export interface Script {
4344
type: string | null;
4445
}
4546

47+
/**
48+
* A function that generates a page fragment such as head, header or footer.
49+
*/
50+
type PageFragmentFunction = (title: string | null, data: FrontMatter, path: string) => string;
51+
4652
export interface Config {
4753
root: string; // defaults to src
4854
output: string; // defaults to dist
@@ -52,9 +58,9 @@ export interface Config {
5258
pages: (Page | Section<Page>)[];
5359
pager: boolean; // defaults to true
5460
scripts: Script[]; // deprecated; defaults to empty array
55-
head: string | null; // defaults to null
56-
header: string | null; // defaults to null
57-
footer: string | null; // defaults to “Built with Observable on [date].”
61+
head: PageFragmentFunction | string | null; // defaults to null
62+
header: PageFragmentFunction | string | null; // defaults to null
63+
footer: PageFragmentFunction | string | null; // defaults to “Built with Observable on [date].”
5864
toc: TableOfContents;
5965
style: null | Style; // defaults to {theme: ["light", "dark"]}
6066
search: boolean; // default to false
@@ -205,9 +211,9 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
205211
const toc = normalizeToc(spec.toc as any);
206212
const sidebar = spec.sidebar === undefined ? undefined : Boolean(spec.sidebar);
207213
const scripts = spec.scripts === undefined ? [] : normalizeScripts(spec.scripts);
208-
const head = spec.head === undefined ? "" : stringOrNull(spec.head);
209-
const header = spec.header === undefined ? "" : stringOrNull(spec.header);
210-
const footer = spec.footer === undefined ? defaultFooter() : stringOrNull(spec.footer);
214+
const head = pageFragment(spec.head === undefined ? "" : spec.head);
215+
const header = pageFragment(spec.header === undefined ? "" : spec.header);
216+
const footer = pageFragment(spec.footer === undefined ? defaultFooter() : spec.footer);
211217
const search = Boolean(spec.search);
212218
const interpreters = normalizeInterpreters(spec.interpreters as any);
213219
const config: Config = {
@@ -247,6 +253,10 @@ function getPathNormalizer(spec: unknown = true): (path: string) => string {
247253
};
248254
}
249255

256+
function pageFragment(spec: unknown) {
257+
return typeof spec === "function" ? (spec as PageFragmentFunction) : stringOrNull(spec);
258+
}
259+
250260
function defaultFooter(): string {
251261
const date = currentDate ?? new Date();
252262
return `Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="${formatIsoDate(

src/markdown.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -319,13 +319,14 @@ export function parseMarkdown(input: string, options: ParseOptions): MarkdownPag
319319
const context: ParseContext = {code, startLine: 0, currentLine: 0, path};
320320
const tokens = md.parse(content, context);
321321
const body = md.renderer.render(tokens, md.options, context); // Note: mutates code!
322+
const title = data.title !== undefined ? data.title : findTitle(tokens);
322323
return {
323-
head: getHead(data, options),
324-
header: getHeader(data, options),
324+
head: getHead(title, data, options),
325+
header: getHeader(title, data, options),
325326
body,
326-
footer: getFooter(data, options),
327+
footer: getFooter(title, data, options),
327328
data,
328-
title: data.title !== undefined ? data.title : findTitle(tokens),
329+
title,
329330
style: getStyle(data, options),
330331
code
331332
};
@@ -344,9 +345,9 @@ export function parseMarkdownMetadata(input: string, options: ParseOptions): Pic
344345
};
345346
}
346347

347-
function getHead(data: FrontMatter, options: ParseOptions): string | null {
348+
function getHead(title: string | null, data: FrontMatter, options: ParseOptions): string | null {
348349
const {scripts, path} = options;
349-
let head = getHtml("head", data, options);
350+
let head = getHtml("head", title, data, options);
350351
if (scripts?.length) {
351352
head ??= "";
352353
for (const {type, async, src} of scripts) {
@@ -358,23 +359,26 @@ function getHead(data: FrontMatter, options: ParseOptions): string | null {
358359
return head;
359360
}
360361

361-
function getHeader(data: FrontMatter, options: ParseOptions): string | null {
362-
return getHtml("header", data, options);
362+
function getHeader(title: string | null, data: FrontMatter, options: ParseOptions): string | null {
363+
return getHtml("header", title, data, options);
363364
}
364365

365-
function getFooter(data: FrontMatter, options: ParseOptions): string | null {
366-
return getHtml("footer", data, options);
366+
function getFooter(title: string | null, data: FrontMatter, options: ParseOptions): string | null {
367+
return getHtml("footer", title, data, options);
367368
}
368369

369370
function getHtml(
370371
key: "head" | "header" | "footer",
372+
title: string | null,
371373
data: FrontMatter,
372374
{path, [key]: defaultValue}: ParseOptions
373375
): string | null {
374376
return data[key] !== undefined
375377
? data[key]
376378
? String(data[key])
377379
: null
380+
: typeof defaultValue === "function"
381+
? rewriteHtmlPaths(defaultValue(title, data, path), path)
378382
: defaultValue != null
379383
? rewriteHtmlPaths(defaultValue, path)
380384
: null;

test/input/build/fragments/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
author: "Ignored Anonymous"
3+
title: Testing fragment functions
4+
date: 2024-04-18
5+
keywords: ["very", "much"]
6+
---
7+
8+
# Display title
9+
10+
Contents.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
head: (data) => `<!-- ${JSON.stringify({fragment: "head", data})} -->`,
3+
header: (data) => `<!-- ${JSON.stringify({fragment: "header", data})} -->`,
4+
footer: (data) => `<!-- ${JSON.stringify({fragment: "footer", data})} -->`,
5+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
4+
<title>Testing fragment functions</title>
5+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
6+
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
7+
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.css">
8+
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
9+
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.css">
10+
<link rel="modulepreload" href="./_observablehq/client.js">
11+
<link rel="modulepreload" href="./_observablehq/runtime.js">
12+
<link rel="modulepreload" href="./_observablehq/stdlib.js">
13+
<!-- {"fragment":"head","data":"Testing fragment functions"} -->
14+
<script type="module">
15+
16+
import "./_observablehq/client.js";
17+
18+
</script>
19+
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
20+
<nav>
21+
</nav>
22+
</aside>
23+
<div id="observablehq-center">
24+
<header id="observablehq-header">
25+
<!-- {"fragment":"header","data":"Testing fragment functions"} -->
26+
</header>
27+
<main id="observablehq-main" class="observablehq">
28+
<h1 id="display-title" tabindex="-1"><a class="observablehq-header-anchor" href="#display-title">Display title</a></h1>
29+
<p>Contents.</p>
30+
</main>
31+
<footer id="observablehq-footer">
32+
<div><!-- {"fragment":"footer","data":"Testing fragment functions"} --></div>
33+
</footer>
34+
</div>

0 commit comments

Comments
 (0)