Skip to content

Commit

Permalink
include pages in default paths (#1622)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Sep 1, 2024
1 parent a93dda3 commit 9160a70
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 60 deletions.
2 changes: 0 additions & 2 deletions observablehq.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,12 @@ export default {
{name: "Contributing", path: "/contributing", pager: false}
],
dynamicPaths: [
"/page-loaders",
"/theme/dark",
"/theme/dark-alt",
"/theme/dashboard",
"/theme/light",
"/theme/light-alt",
"/theme/wide",
"/themes",
...themes.dark.map((theme) => `/theme/${theme}`),
...themes.light.map((theme) => `/theme/${theme}`)
],
Expand Down
18 changes: 15 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {formatIsoDate, formatLocaleDate} from "./format.js";
import type {FrontMatter} from "./frontMatter.js";
import {LoaderResolver} from "./loader.js";
import {createMarkdownIt, parseMarkdownMetadata} from "./markdown.js";
import {getPagePaths} from "./pager.js";
import {isAssetPath, parseRelativeUrl, resolvePath} from "./path.js";
import {isParameterizedPath} from "./route.js";
import {resolveTheme} from "./theme.js";
Expand Down Expand Up @@ -257,6 +258,7 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
// end of the path. Otherwise, remove the .html extension (we use clean
// paths as the internal canonical representation; see normalizePage).
function normalizePagePath(pathname: string): string {
({pathname} = parseRelativeUrl(pathname)); // ignore query & anchor
pathname = normalizePath(pathname);
if (pathname.endsWith("/")) pathname = join(pathname, "index");
else pathname = pathname.replace(/\.html$/, "");
Expand All @@ -272,11 +274,21 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
pages: pages!, // see below
pager,
async *paths() {
for await (const path of getDefaultPaths(root)) {
yield normalizePagePath(path);
const visited = new Set<string>();
function* visit(path: string): Generator<string> {
if (!visited.has((path = normalizePagePath(path)))) {
visited.add(path);
yield path;
}
}
for (const path of getDefaultPaths(root)) {
yield* visit(path);
}
for (const path of getPagePaths(this)) {
yield* visit(path);
}
for await (const path of dynamicPaths()) {
yield normalizePagePath(path);
yield* visit(path);
}
},
scripts,
Expand Down
10 changes: 9 additions & 1 deletion src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class LoaderResolver {
* page object.
*/
async loadPage(path: string, options: LoadOptions & ParseOptions, effects?: LoadEffects): Promise<MarkdownPage> {
const loader = this.find(`${path}.md`);
const loader = this.findPage(path, options);
if (!loader) throw enoent(path);
const source = await readFile(join(this.root, await loader.load(effects)), "utf8");
return parseMarkdown(source, {params: loader.params, ...options});
Expand All @@ -102,6 +102,14 @@ export class LoaderResolver {
return watch(join(this.root, loader.path), listener);
}

/**
* Finds the page loader for the specified target path, relative to the source
* root, if the loader exists. If there is no such loader, returns undefined.
*/
findPage(path: string, options?: LoadOptions): Loader | undefined {
return this.find(`${path}.md`, options);
}

/**
* Finds the loader for the specified target path, relative to the source
* root, if the loader exists. If there is no such loader, returns undefined.
Expand Down
25 changes: 18 additions & 7 deletions src/pager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ export function normalizePath(path: string): string {
return path.replace(/[?#].*$/, "");
}

export function findLink(path: string, options: Pick<Config, "pages" | "title"> = {pages: []}): PageLink | undefined {
const {pages, title} = options;
export function findLink(path: string, config: Config): PageLink | undefined {
const {pages} = config;
let links = linkCache.get(pages);
if (!links) {
links = new Map<string, PageLink>();
for (const pageGroup of walk(pages, title)) {
for (const pageGroup of walk(config)) {
let prev: Page | undefined;
for (const page of pageGroup) {
const path = normalizePath(page.path);
Expand All @@ -40,9 +40,12 @@ export function findLink(path: string, options: Pick<Config, "pages" | "title">
return links.get(path);
}

// Walks the unique pages in the site so as to avoid creating cycles. Implicitly
// adds a link at the beginning to the home page (/index).
function walk(pages: Config["pages"], title = "Home"): Iterable<Iterable<Page>> {
/**
* Walks the unique pages in the site so as to avoid creating cycles. Implicitly
* adds a link at the beginning to the home page (/index).
*/
function walk(config: Config): Iterable<Iterable<Page>> {
const {pages, loaders, title = "Home"} = config;
const pageGroups = new Map<string, Page[]>();
const visited = new Set<string>();

Expand All @@ -54,7 +57,7 @@ function walk(pages: Config["pages"], title = "Home"): Iterable<Iterable<Page>>
pageGroup.push(page);
}

visit({name: title, path: "/index", pager: "main"});
if (loaders.findPage("/index")) visit({name: title, path: "/index", pager: "main"});

for (const page of pages) {
if (page.path !== null) visit(page as Page);
Expand All @@ -63,3 +66,11 @@ function walk(pages: Config["pages"], title = "Home"): Iterable<Iterable<Page>>

return pageGroups.values();
}

export function* getPagePaths(config: Config): Generator<string> {
for (const pageGroup of walk(config)) {
for (const page of pageGroup) {
yield page.path;
}
}
}
2 changes: 1 addition & 1 deletion test/output/build/archives.posix/tar.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ <h1 id="tar" tabindex="-1"><a class="observablehq-header-anchor" href="#tar">Tar
<div class="observablehq observablehq--block"><observablehq-loading></observablehq-loading><!--:d0a58efd:--></div>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a><a rel="next" href="./zip"><span>Zip</span></a></nav>
<nav><a rel="next" href="./zip"><span>Zip</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
2 changes: 1 addition & 1 deletion test/output/build/archives.win32/tar.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ <h1 id="tar" tabindex="-1"><a class="observablehq-header-anchor" href="#tar">Tar
<div class="observablehq observablehq--block"><observablehq-loading></observablehq-loading><!--:d84cd7fb:--></div>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a><a rel="next" href="./zip"><span>Zip</span></a></nav>
<nav><a rel="next" href="./zip"><span>Zip</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
2 changes: 1 addition & 1 deletion test/output/build/fetches/foo.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ <h1 id="top" tabindex="-1"><a class="observablehq-header-anchor" href="#top">Top
<div class="observablehq observablehq--block"><!--:47a695da:--></div>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a><a rel="next" href="./top"><span>Top</span></a></nav>
<nav><a rel="next" href="./top"><span>Top</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
2 changes: 1 addition & 1 deletion test/output/build/files/files.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<p><img src="./_file/observable%20logo%20small.8a915536.png" alt=""></p>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a><a rel="next" href="./subsection/subfiles"><span>Untitled</span></a></nav>
<nav><a rel="next" href="./subsection/subfiles"><span>Untitled</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
2 changes: 1 addition & 1 deletion test/output/build/imports/script.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ <h1 id="scripts" tabindex="-1"><a class="observablehq-header-anchor" href="#scri
<script src="./_file/top.a53c5d5b.js" type="other"></script>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a><a rel="next" href="./foo/foo"><span>Foo</span></a></nav>
<nav><a rel="next" href="./foo/foo"><span>Foo</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
2 changes: 1 addition & 1 deletion test/output/build/search-public/page1.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ <h1 id="page-1" tabindex="-1"><a class="observablehq-header-anchor" href="#page-
<p><ignore> <me also="ignore"> </me></ignore></p>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a><a rel="next" href="./page3"><span>This page is not indexable</span></a></nav>
<nav><a rel="next" href="./page3"><span>This page is not indexable</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
1 change: 0 additions & 1 deletion test/output/build/simple/simple.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ <h1 id="build-test-case" tabindex="-1"><a class="observablehq-header-anchor" hre
<div class="observablehq observablehq--block"><!--:815178e4:--></div>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
83 changes: 43 additions & 40 deletions test/pager-test.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,81 @@
import assert from "node:assert";
import {normalizeConfig} from "../src/config.js";
import {findLink as pager} from "../src/pager.js";
import {findLink} from "../src/pager.js";

describe("findLink(path, options)", () => {
it("returns the previous and next links for three pages", () => {
const a = {name: "a", path: "/a", pager: "main"};
const b = {name: "b", path: "/b", pager: "main"};
const c = {name: "c", path: "/c", pager: "main"};
const config = {pages: [a, b, c]};
assert.deepStrictEqual(pager("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(pager("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(pager("/b", config), {prev: a, next: c});
assert.deepStrictEqual(pager("/c", config), {prev: b, next: undefined});
const config = normalizeConfig({pages: [a, b, c]});
assert.deepStrictEqual(findLink("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(findLink("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(findLink("/b", config), {prev: a, next: c});
assert.deepStrictEqual(findLink("/c", config), {prev: b, next: undefined});
});
it("returns the previous and next links for three pages with sections", () => {
const a = {name: "a", path: "/a", pager: "main"};
const b = {name: "b", path: "/b", pager: "main"};
const c = {name: "c", path: "/c", pager: "main"};
const section = {name: "section", collapsible: true, open: true, path: null, pager: null, pages: [a, b, c]};
const config = {pages: [section]};
assert.deepStrictEqual(pager("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(pager("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(pager("/b", config), {prev: a, next: c});
assert.deepStrictEqual(pager("/c", config), {prev: b, next: undefined});
const config = normalizeConfig({pages: [section]});
assert.deepStrictEqual(findLink("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(findLink("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(findLink("/b", config), {prev: a, next: c});
assert.deepStrictEqual(findLink("/c", config), {prev: b, next: undefined});
});
it("returns the previous and next links for two pages", () => {
const a = {name: "a", path: "/a", pager: "main"};
const b = {name: "b", path: "/b", pager: "main"};
const config = {pages: [a, b]};
assert.deepStrictEqual(pager("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(pager("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(pager("/b", config), {prev: a, next: undefined});
const config = normalizeConfig({pages: [a, b]});
assert.deepStrictEqual(findLink("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(findLink("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(findLink("/b", config), {prev: a, next: undefined});
});
it("returns the previous and next links for one pages", () => {
const a = {name: "a", path: "/a", pager: "main"};
const config = {pages: [a]};
assert.deepStrictEqual(pager("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(pager("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: undefined});
const config = normalizeConfig({pages: [a]});
assert.deepStrictEqual(findLink("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(findLink("/a", config), {
prev: {name: "Home", path: "/index", pager: "main"},
next: undefined
});
});
it("returns undefined for zero pages", () => {
const config = {pages: []};
assert.deepStrictEqual(pager("/index", config), undefined);
const config = normalizeConfig({pages: []});
assert.deepStrictEqual(findLink("/index", config), undefined);
});
it("returns undefined for non-referenced pages", () => {
const a = {name: "a", path: "/a", pager: "main"};
const b = {name: "b", path: "/b", pager: "main"};
const c = {name: "c", path: "/c", pager: "main"};
const config = {pages: [a, b, c]};
assert.deepStrictEqual(pager("/d", config), undefined);
const config = normalizeConfig({pages: [a, b, c]});
assert.deepStrictEqual(findLink("/d", config), undefined);
});
it("avoids cycles when a path is listed multiple times", () => {
const a = {name: "a", path: "/a", pager: "main"};
const b = {name: "b", path: "/b", pager: "main"};
const c = {name: "c", path: "/c", pager: "main"};
const config = {pages: [a, b, a, c]};
assert.deepStrictEqual(pager("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(pager("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(pager("/b", config), {prev: a, next: c});
assert.deepStrictEqual(pager("/c", config), {prev: b, next: undefined});
const config = normalizeConfig({pages: [a, b, a, c]});
assert.deepStrictEqual(findLink("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(findLink("/a", config), {prev: {name: "Home", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(findLink("/b", config), {prev: a, next: c});
assert.deepStrictEqual(findLink("/c", config), {prev: b, next: undefined});
});
it("implicitly includes the index page if there is a title", () => {
const a = {name: "a", path: "/a", pager: "main"};
const b = {name: "b", path: "/b", pager: "main"};
const c = {name: "c", path: "/c", pager: "main"};
const config = {title: "Test", pages: [a, b, c]};
assert.deepStrictEqual(pager("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(pager("/a", config), {prev: {name: "Test", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(pager("/b", config), {prev: a, next: c});
assert.deepStrictEqual(pager("/c", config), {prev: b, next: undefined});
const config = normalizeConfig({title: "Test", pages: [a, b, c]});
assert.deepStrictEqual(findLink("/index", config), {prev: undefined, next: a});
assert.deepStrictEqual(findLink("/a", config), {prev: {name: "Test", path: "/index", pager: "main"}, next: b});
assert.deepStrictEqual(findLink("/b", config), {prev: a, next: c});
assert.deepStrictEqual(findLink("/c", config), {prev: b, next: undefined});
});
it("normalizes / to /index", async () => {
const config = normalizeConfig({pages: [{name: "Home", path: "/", pager: "main"}]});
assert.strictEqual(pager("/index", config), undefined);
assert.strictEqual(pager("/", config), undefined);
assert.strictEqual(findLink("/index", config), undefined);
assert.strictEqual(findLink("/", config), undefined);
});
it("normalizes / to /index (2)", async () => {
const config = normalizeConfig({
Expand All @@ -81,15 +84,15 @@ describe("findLink(path, options)", () => {
{name: "Second Home", path: "/second"}
]
});
assert.deepStrictEqual(pager("/second", config), {
assert.deepStrictEqual(findLink("/second", config), {
next: undefined,
prev: {name: "Home", path: "/index", pager: "main"}
});
assert.deepStrictEqual(pager("/index", config), {
assert.deepStrictEqual(findLink("/index", config), {
next: {name: "Second Home", path: "/second", pager: "main"},
prev: undefined
});
assert.strictEqual(pager("/", config), undefined);
assert.strictEqual(findLink("/", config), undefined);
});
it("normalizes / to /index (3)", async () => {
const config = normalizeConfig({
Expand All @@ -99,14 +102,14 @@ describe("findLink(path, options)", () => {
{name: "By The Sea", path: "/by-the-sea"}
]
});
assert.deepStrictEqual(pager("/second", config), {
assert.deepStrictEqual(findLink("/second", config), {
next: {name: "By The Sea", path: "/by-the-sea", pager: "main"},
prev: {name: "Home", path: "/index", pager: "main"}
});
assert.deepStrictEqual(pager("/index", config), {
assert.deepStrictEqual(findLink("/index", config), {
next: {name: "Second Home", path: "/second", pager: "main"},
prev: undefined
});
assert.strictEqual(pager("/", config), undefined);
assert.strictEqual(findLink("/", config), undefined);
});
});

0 comments on commit 9160a70

Please sign in to comment.