Skip to content

Commit

Permalink
Accept path?search and path#hash in the pager and the sidebar. (#370)
Browse files Browse the repository at this point in the history
* Accept path?search and path#hash in the pager and the sidebar.
closes #197.

The pager now supports paths containing ?search #hash. However when there are several of them, only the first page is taken into account by the pager, and a warning is logged.

The sidebar opens and highlights these pages as necessary.

* normalizePath(string)

---------

Co-authored-by: Mike Bostock <[email protected]>
  • Loading branch information
Fil and mbostock authored Jan 3, 2024
1 parent 0a484a8 commit 2a65840
Show file tree
Hide file tree
Showing 27 changed files with 740 additions and 12 deletions.
19 changes: 14 additions & 5 deletions src/pager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,29 @@ export type PageLink =
// Pager links in the footer are computed once for a given navigation.
const linkCache = new WeakMap<Config["pages"], Map<string, PageLink>>();

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;
let links = linkCache.get(pages);
if (!links) {
links = new Map<string, PageLink>();
let prev: Page | undefined;
for (const page of walk(pages, title)) {
if (prev) {
links.set(page.path, {prev, next: undefined});
links.get(prev.path)!.next = page;
const path = normalizePath(page.path);
if (links.has(path)) {
console.warn(`ignoring duplicate page: ${page.path}`);
} else {
links.set(page.path, {prev: undefined, next: undefined as unknown as Page}); // next set lazily
if (prev) {
links.set(path, {prev, next: undefined});
links.get(normalizePath(prev.path))!.next = page;
} else {
links.set(path, {prev: undefined, next: undefined as unknown as Page}); // next set lazily
}
prev = page;
}
prev = page;
}
if (links.size === 1) links.clear(); // no links if only one page
linkCache.set(pages, links);
Expand Down
17 changes: 10 additions & 7 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {createImportResolver, resolveModuleIntegrity, resolveModulePreloads} fro
import type {FileReference, ImportReference, Transpile} from "./javascript.js";
import {addImplicitSpecifiers, addImplicitStylesheets} from "./libraries.js";
import {type ParseResult, parseMarkdown} from "./markdown.js";
import {type PageLink, findLink} from "./pager.js";
import {type PageLink, findLink, normalizePath} from "./pager.js";
import {getClientPath, rollupClient} from "./rollup.js";
import {relativeUrl} from "./url.js";

Expand Down Expand Up @@ -99,16 +99,19 @@ async function renderSidebar(title = "Home", pages: (Page | Section)[], path: st
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
<ol>
<li class="observablehq-link${path === "/index" ? " observablehq-link-active" : ""}"><a href="${relativeUrl(
path,
"/"
)}">${title}</a></li>
<li class="observablehq-link${
normalizePath(path) === "/index" ? " observablehq-link-active" : ""
}"><a href="${relativeUrl(path, "/")}">${title}</a></li>
</ol>
<ol>${pages.map((p, i) =>
"pages" in p
? html`${i > 0 && "path" in pages[i - 1] ? html`</ol>` : ""}
<details${
p.pages.some((p) => p.path === path) ? html` open class="observablehq-section-active"` : p.open ? " open" : ""
p.pages.some((p) => normalizePath(p.path) === path)
? html` open class="observablehq-section-active"`
: p.open
? " open"
: ""
}>
<summary>${p.name}</summary>
<ol>${p.pages.map((p) => renderListItem(p, path))}
Expand Down Expand Up @@ -158,7 +161,7 @@ function renderToc(headers: Header[], label: string): Html {

function renderListItem(p: Page, path: string): Html {
return html`\n <li class="observablehq-link${
p.path === path ? " observablehq-link-active" : ""
normalizePath(p.path) === path ? " observablehq-link-active" : ""
}"><a href="${relativeUrl(path, prettyPath(p.path))}">${p.name}</a></li>`;
}

Expand Down
1 change: 1 addition & 0 deletions test/input/build/pager/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# index
22 changes: 22 additions & 0 deletions test/input/build/pager/observablehq.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default {
pages: [
{
name: "Sub",
open: false,
pages: [
{name: "Page 0", path: "/sub/page0"},
{name: "Page 1 ?x=1", path: "/sub/page1?x=1"},
{name: "Page 2", path: "/sub/page2"},
{name: "Page 3 ?x=1", path: "/sub/page3?x=1"},
{name: "Page 3 ?x=2", path: "/sub/page3?x=2"},
{name: "Page 4", path: "/sub/page4"},
{name: "Page 5 #hello", path: "/sub/page5#hello"},
{name: "Page 6", path: "/sub/page6"},
{name: "Page 7", path: "/sub/page7"},
{name: "Page 7 #hello", path: "/sub/page7#hello"},
{name: "Page 7 #world", path: "/sub/page7#world"},
{name: "Page 8", path: "/sub/page8"}
]
}
]
};
1 change: 1 addition & 0 deletions test/input/build/pager/sub/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# subindex
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 0
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page1..10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 1..10
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 1
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 2
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 3
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 4
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 5
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 6
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 7
1 change: 1 addition & 0 deletions test/input/build/pager/sub/page8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# page 8
57 changes: 57 additions & 0 deletions test/output/build/pager/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>index</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="./_observablehq/style.css">
<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="stylesheet" type="text/css" href="./_observablehq/style.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="modulepreload" href="./_observablehq/client.js">
<link rel="modulepreload" href="./_observablehq/runtime.js">
<link rel="modulepreload" href="./_observablehq/stdlib.js">
<script type="module">

import "./_observablehq/client.js";

</script>
<input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar">
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
<ol>
<li class="observablehq-link observablehq-link-active"><a href="./">Home</a></li>
</ol>
<ol>
<details>
<summary>Sub</summary>
<ol>
<li class="observablehq-link"><a href="./sub/page0">Page 0</a></li>
<li class="observablehq-link"><a href="./sub/page1?x=1">Page 1 ?x=1</a></li>
<li class="observablehq-link"><a href="./sub/page2">Page 2</a></li>
<li class="observablehq-link"><a href="./sub/page3?x=1">Page 3 ?x=1</a></li>
<li class="observablehq-link"><a href="./sub/page3?x=2">Page 3 ?x=2</a></li>
<li class="observablehq-link"><a href="./sub/page4">Page 4</a></li>
<li class="observablehq-link"><a href="./sub/page5#hello">Page 5 #hello</a></li>
<li class="observablehq-link"><a href="./sub/page6">Page 6</a></li>
<li class="observablehq-link"><a href="./sub/page7">Page 7</a></li>
<li class="observablehq-link"><a href="./sub/page7#hello">Page 7 #hello</a></li>
<li class="observablehq-link"><a href="./sub/page7#world">Page 7 #world</a></li>
<li class="observablehq-link"><a href="./sub/page8">Page 8</a></li>
</ol>
</details>
</ol>
</nav>
<script>{/* redacted init script */}</script>
<aside id="observablehq-toc" data-selector="#observablehq-main h1:not(:first-of-type), #observablehq-main h2:not(h1 + h2)">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="index" tabindex="-1"><a class="observablehq-header-anchor" href="#index">index</a></h1>
</main>
<footer id="observablehq-footer">
<nav><a rel="next" href="./sub/page0"><span>Page 0</span></a></nav>
<div>© 2024 Observable, Inc.</div>
</footer>
</div>
56 changes: 56 additions & 0 deletions test/output/build/pager/sub/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>subindex</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="../_observablehq/style.css">
<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="stylesheet" type="text/css" href="../_observablehq/style.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="modulepreload" href="../_observablehq/client.js">
<link rel="modulepreload" href="../_observablehq/runtime.js">
<link rel="modulepreload" href="../_observablehq/stdlib.js">
<script type="module">

import "../_observablehq/client.js";

</script>
<input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar">
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
<ol>
<li class="observablehq-link"><a href="../">Home</a></li>
</ol>
<ol>
<details>
<summary>Sub</summary>
<ol>
<li class="observablehq-link"><a href="./page0">Page 0</a></li>
<li class="observablehq-link"><a href="./page1?x=1">Page 1 ?x=1</a></li>
<li class="observablehq-link"><a href="./page2">Page 2</a></li>
<li class="observablehq-link"><a href="./page3?x=1">Page 3 ?x=1</a></li>
<li class="observablehq-link"><a href="./page3?x=2">Page 3 ?x=2</a></li>
<li class="observablehq-link"><a href="./page4">Page 4</a></li>
<li class="observablehq-link"><a href="./page5#hello">Page 5 #hello</a></li>
<li class="observablehq-link"><a href="./page6">Page 6</a></li>
<li class="observablehq-link"><a href="./page7">Page 7</a></li>
<li class="observablehq-link"><a href="./page7#hello">Page 7 #hello</a></li>
<li class="observablehq-link"><a href="./page7#world">Page 7 #world</a></li>
<li class="observablehq-link"><a href="./page8">Page 8</a></li>
</ol>
</details>
</ol>
</nav>
<script>{/* redacted init script */}</script>
<aside id="observablehq-toc" data-selector="#observablehq-main h1:not(:first-of-type), #observablehq-main h2:not(h1 + h2)">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="subindex" tabindex="-1"><a class="observablehq-header-anchor" href="#subindex">subindex</a></h1>
</main>
<footer id="observablehq-footer">
<div>© 2024 Observable, Inc.</div>
</footer>
</div>
57 changes: 57 additions & 0 deletions test/output/build/pager/sub/page0.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>page 0</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="../_observablehq/style.css">
<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="stylesheet" type="text/css" href="../_observablehq/style.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="modulepreload" href="../_observablehq/client.js">
<link rel="modulepreload" href="../_observablehq/runtime.js">
<link rel="modulepreload" href="../_observablehq/stdlib.js">
<script type="module">

import "../_observablehq/client.js";

</script>
<input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar">
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
<ol>
<li class="observablehq-link"><a href="../">Home</a></li>
</ol>
<ol>
<details open class="observablehq-section-active">
<summary>Sub</summary>
<ol>
<li class="observablehq-link observablehq-link-active"><a href="./page0">Page 0</a></li>
<li class="observablehq-link"><a href="./page1?x=1">Page 1 ?x=1</a></li>
<li class="observablehq-link"><a href="./page2">Page 2</a></li>
<li class="observablehq-link"><a href="./page3?x=1">Page 3 ?x=1</a></li>
<li class="observablehq-link"><a href="./page3?x=2">Page 3 ?x=2</a></li>
<li class="observablehq-link"><a href="./page4">Page 4</a></li>
<li class="observablehq-link"><a href="./page5#hello">Page 5 #hello</a></li>
<li class="observablehq-link"><a href="./page6">Page 6</a></li>
<li class="observablehq-link"><a href="./page7">Page 7</a></li>
<li class="observablehq-link"><a href="./page7#hello">Page 7 #hello</a></li>
<li class="observablehq-link"><a href="./page7#world">Page 7 #world</a></li>
<li class="observablehq-link"><a href="./page8">Page 8</a></li>
</ol>
</details>
</ol>
</nav>
<script>{/* redacted init script */}</script>
<aside id="observablehq-toc" data-selector="#observablehq-main h1:not(:first-of-type), #observablehq-main h2:not(h1 + h2)">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="page-0" tabindex="-1"><a class="observablehq-header-anchor" href="#page-0">page 0</a></h1>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="../"><span>Home</span></a><a rel="next" href="./page1?x=1"><span>Page 1 ?x=1</span></a></nav>
<div>© 2024 Observable, Inc.</div>
</footer>
</div>
56 changes: 56 additions & 0 deletions test/output/build/pager/sub/page1..10.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>page 1..10</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="../_observablehq/style.css">
<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="stylesheet" type="text/css" href="../_observablehq/style.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="modulepreload" href="../_observablehq/client.js">
<link rel="modulepreload" href="../_observablehq/runtime.js">
<link rel="modulepreload" href="../_observablehq/stdlib.js">
<script type="module">

import "../_observablehq/client.js";

</script>
<input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar">
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
<ol>
<li class="observablehq-link"><a href="../">Home</a></li>
</ol>
<ol>
<details>
<summary>Sub</summary>
<ol>
<li class="observablehq-link"><a href="./page0">Page 0</a></li>
<li class="observablehq-link"><a href="./page1?x=1">Page 1 ?x=1</a></li>
<li class="observablehq-link"><a href="./page2">Page 2</a></li>
<li class="observablehq-link"><a href="./page3?x=1">Page 3 ?x=1</a></li>
<li class="observablehq-link"><a href="./page3?x=2">Page 3 ?x=2</a></li>
<li class="observablehq-link"><a href="./page4">Page 4</a></li>
<li class="observablehq-link"><a href="./page5#hello">Page 5 #hello</a></li>
<li class="observablehq-link"><a href="./page6">Page 6</a></li>
<li class="observablehq-link"><a href="./page7">Page 7</a></li>
<li class="observablehq-link"><a href="./page7#hello">Page 7 #hello</a></li>
<li class="observablehq-link"><a href="./page7#world">Page 7 #world</a></li>
<li class="observablehq-link"><a href="./page8">Page 8</a></li>
</ol>
</details>
</ol>
</nav>
<script>{/* redacted init script */}</script>
<aside id="observablehq-toc" data-selector="#observablehq-main h1:not(:first-of-type), #observablehq-main h2:not(h1 + h2)">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="page-1..10" tabindex="-1"><a class="observablehq-header-anchor" href="#page-1..10">page 1..10</a></h1>
</main>
<footer id="observablehq-footer">
<div>© 2024 Observable, Inc.</div>
</footer>
</div>
Loading

0 comments on commit 2a65840

Please sign in to comment.