Skip to content

Commit

Permalink
head, header and footer can be specified as a function (#1255)
Browse files Browse the repository at this point in the history
* rebase & simplify

* Update src/markdown.ts

Co-authored-by: Mike Bostock <[email protected]>

* Update src/config.ts

Co-authored-by: Mike Bostock <[email protected]>

* no quotes necessary

* prettier

* combine arguments

* document

* Update src/config.ts

* ignore null and undefined

---------

Co-authored-by: Mike Bostock <[email protected]>
  • Loading branch information
Fil and mbostock authored May 15, 2024
1 parent 8d50e57 commit 83397e1
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 20 deletions.
12 changes: 9 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,21 @@ Whether to show the previous & next links in the footer; defaults to true. The p

## head

An HTML fragment to add to the head. Defaults to the empty string.
An HTML fragment to add to the head. Defaults to the empty string. If specified as a function, receives an object with the page’s `title`, (front-matter) `data`, and `path`, and must return a string.

## header

An HTML fragment to add to the header. Defaults to the empty string.
An HTML fragment to add to the header. Defaults to the empty string. If specified as a function, receives an object with the page’s `title`, (front-matter) `data`, and `path`, and must return a string.

## footer

An HTML fragment to add to the footer. Defaults to “Built with Observable.”
An HTML fragment to add to the footer. Defaults to “Built with Observable.” If specified as a function, receives an object with the page’s `title`, (front-matter) `data`, and `path`, and must return a string.

For example, the following adds a link to the bottom of each page:

```js run=false
footer: ({path}) => `<a href="https://github.com/example/test/blob/main/src${path}.md?plain=1">view source</a>`,
```

## base

Expand Down
30 changes: 24 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import wrapAnsi from "wrap-ansi";
import {LoaderResolver} from "./dataloader.js";
import {visitMarkdownFiles} from "./files.js";
import {formatIsoDate, formatLocaleDate} from "./format.js";
import type {FrontMatter} from "./frontMatter.js";
import {createMarkdownIt, parseMarkdownMetadata} from "./markdown.js";
import {isAssetPath, parseRelativeUrl, resolvePath} from "./path.js";
import {resolveTheme} from "./theme.js";
Expand Down Expand Up @@ -43,6 +44,19 @@ export interface Script {
type: string | null;
}

/**
* A function that generates a page fragment such as head, header or footer.
*/
export type PageFragmentFunction = ({
title,
data,
path
}: {
title: string | null;
data: FrontMatter;
path: string;
}) => string | null;

export interface Config {
root: string; // defaults to src
output: string; // defaults to dist
Expand All @@ -52,9 +66,9 @@ export interface Config {
pages: (Page | Section<Page>)[];
pager: boolean; // defaults to true
scripts: Script[]; // deprecated; defaults to empty array
head: string | null; // defaults to null
header: string | null; // defaults to null
footer: string | null; // defaults to “Built with Observable on [date].”
head: PageFragmentFunction | string | null; // defaults to null
header: PageFragmentFunction | string | null; // defaults to null
footer: PageFragmentFunction | string | null; // defaults to “Built with Observable on [date].”
toc: TableOfContents;
style: null | Style; // defaults to {theme: ["light", "dark"]}
search: boolean; // default to false
Expand Down Expand Up @@ -205,9 +219,9 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
const toc = normalizeToc(spec.toc as any);
const sidebar = spec.sidebar === undefined ? undefined : Boolean(spec.sidebar);
const scripts = spec.scripts === undefined ? [] : normalizeScripts(spec.scripts);
const head = spec.head === undefined ? "" : stringOrNull(spec.head);
const header = spec.header === undefined ? "" : stringOrNull(spec.header);
const footer = spec.footer === undefined ? defaultFooter() : stringOrNull(spec.footer);
const head = pageFragment(spec.head === undefined ? "" : spec.head);
const header = pageFragment(spec.header === undefined ? "" : spec.header);
const footer = pageFragment(spec.footer === undefined ? defaultFooter() : spec.footer);
const search = Boolean(spec.search);
const interpreters = normalizeInterpreters(spec.interpreters as any);
const config: Config = {
Expand Down Expand Up @@ -247,6 +261,10 @@ function getPathNormalizer(spec: unknown = true): (path: string) => string {
};
}

function pageFragment(spec: unknown): PageFragmentFunction | string | null {
return typeof spec === "function" ? (spec as PageFragmentFunction) : stringOrNull(spec);
}

function defaultFooter(): string {
const date = currentDate ?? new Date();
return `Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="${formatIsoDate(
Expand Down
27 changes: 16 additions & 11 deletions src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,14 @@ export function parseMarkdown(input: string, options: ParseOptions): MarkdownPag
const context: ParseContext = {code, startLine: 0, currentLine: 0, path};
const tokens = md.parse(content, context);
const body = md.renderer.render(tokens, md.options, context); // Note: mutates code!
const title = data.title !== undefined ? data.title : findTitle(tokens);
return {
head: getHead(data, options),
header: getHeader(data, options),
head: getHead(title, data, options),
header: getHeader(title, data, options),
body,
footer: getFooter(data, options),
footer: getFooter(title, data, options),
data,
title: data.title !== undefined ? data.title : findTitle(tokens),
title,
style: getStyle(data, options),
code
};
Expand All @@ -344,9 +345,9 @@ export function parseMarkdownMetadata(input: string, options: ParseOptions): Pic
};
}

function getHead(data: FrontMatter, options: ParseOptions): string | null {
function getHead(title: string | null, data: FrontMatter, options: ParseOptions): string | null {
const {scripts, path} = options;
let head = getHtml("head", data, options);
let head = getHtml("head", title, data, options);
if (scripts?.length) {
head ??= "";
for (const {type, async, src} of scripts) {
Expand All @@ -358,16 +359,17 @@ function getHead(data: FrontMatter, options: ParseOptions): string | null {
return head;
}

function getHeader(data: FrontMatter, options: ParseOptions): string | null {
return getHtml("header", data, options);
function getHeader(title: string | null, data: FrontMatter, options: ParseOptions): string | null {
return getHtml("header", title, data, options);
}

function getFooter(data: FrontMatter, options: ParseOptions): string | null {
return getHtml("footer", data, options);
function getFooter(title: string | null, data: FrontMatter, options: ParseOptions): string | null {
return getHtml("footer", title, data, options);
}

function getHtml(
key: "head" | "header" | "footer",
title: string | null,
data: FrontMatter,
{path, [key]: defaultValue}: ParseOptions
): string | null {
Expand All @@ -376,7 +378,10 @@ function getHtml(
? String(data[key])
: null
: defaultValue != null
? rewriteHtmlPaths(defaultValue, path)
? rewriteHtmlPaths(
typeof defaultValue === "function" ? defaultValue({title, data, path}) ?? "" : defaultValue,
path
)
: null;
}

Expand Down
10 changes: 10 additions & 0 deletions test/input/build/fragments/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
author: Ignored Anonymous
title: Testing fragment functions
date: 2024-04-18
keywords: [very, much]
---

# Display title

Contents.
5 changes: 5 additions & 0 deletions test/input/build/fragments/observablehq.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
head: (data) => `<!-- ${JSON.stringify({fragment: "head", data})} -->`,
header: (data) => `<!-- ${JSON.stringify({fragment: "header", data})} -->`,
footer: (data) => `<!-- ${JSON.stringify({fragment: "footer", data})} -->`
};
34 changes: 34 additions & 0 deletions test/output/build/fragments/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Testing fragment functions</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.css">
<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>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.css">
<link rel="modulepreload" href="./_observablehq/client.js">
<link rel="modulepreload" href="./_observablehq/runtime.js">
<link rel="modulepreload" href="./_observablehq/stdlib.js">
<!-- {"fragment":"head","data":{"title":"Testing fragment functions","data":{"title":"Testing fragment functions","keywords":["very","much"]},"path":"/index"}} -->
<script type="module">

import "./_observablehq/client.js";

</script>
<aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<header id="observablehq-header">
<!-- {"fragment":"header","data":{"title":"Testing fragment functions","data":{"title":"Testing fragment functions","keywords":["very","much"]},"path":"/index"}} -->
</header>
<main id="observablehq-main" class="observablehq">
<h1 id="display-title" tabindex="-1"><a class="observablehq-header-anchor" href="#display-title">Display title</a></h1>
<p>Contents.</p>
</main>
<footer id="observablehq-footer">
<div><!-- {"fragment":"footer","data":{"title":"Testing fragment functions","data":{"title":"Testing fragment functions","keywords":["very","much"]},"path":"/index"}} --></div>
</footer>
</div>

0 comments on commit 83397e1

Please sign in to comment.