Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ Whether to show the sidebar. Defaults to true if **pages** is not empty.

An array containing pages and sections. If not specified, it defaults to all Markdown files found in the source root in directory listing order.

Both pages and sections have a **name**, which typically corresponds to the page’s title. The name gets displayed in the sidebar. Clicking on a page navigates to the corresponding **path**, which should start with a leading slash and be relative to the root; the path can also be specified as a full URL to navigate to an external site. Clicking on a section header opens or closes that section. Each section must specify an array of **pages**, and optionally whether the section is **open** by default. If **open** is not set, it defaults to true. If **open** is false, the section is closed unless the current page belongs to that section.
Both pages and sections have a **name**, which typically corresponds to the page’s title. The name gets displayed in the sidebar. Clicking on a page navigates to the corresponding **path**, which should start with a leading slash and be relative to the root; the path can also be specified as a full URL to navigate to an external site. Each section must specify an array of **pages**.

Sections may be **collapsible**. <a href="https://github.com/observablehq/framework/pull/1208" class="observablehq-version-badge" data-version="prerelease" title="Added in #1208"></a> If the **open** option is set, the **collapsible** option defaults to true; otherwise it defaults to false. If the section is not collapsible, the **open** option is ignored and the section is always open; otherwise, the **open** option defaults to true. When a section is collapsible, clicking on a section header opens or closes that section. A section will always be open if the current page belongs to that section.

For example, here **pages** specifies two sections and a total of four pages:

Expand Down
36 changes: 20 additions & 16 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@ import {createMarkdownIt, parseMarkdownMetadata} from "./markdown.js";
import {isAssetPath, parseRelativeUrl, resolvePath} from "./path.js";
import {resolveTheme} from "./theme.js";

export interface TableOfContents {
show: boolean;
label: string;
}

export interface Page {
name: string;
path: string;
}

export interface Section {
export interface Section<T = Page> {
name: string;
open: boolean; // defaults to true
pages: Page[];
}

export interface TableOfContents {
label: string; // defaults to "Contents"
show: boolean; // defaults to true
collapsible: boolean; // defaults to false
open: boolean; // defaults to true; always true if collapsible is false
pages: T[];
}

export type Style =
Expand All @@ -45,7 +46,7 @@ export interface Config {
base: string; // defaults to "/"
title?: string;
sidebar: boolean; // defaults to true if pages isn’t empty
pages: (Page | Section)[];
pages: (Page | Section<Page>)[];
pager: boolean; // defaults to true
scripts: Script[]; // defaults to empty array
head: string; // defaults to empty string
Expand Down Expand Up @@ -156,7 +157,7 @@ export function normalizeConfig(spec: any = {}, defaultRoot = "docs", watchPath?
const md = createMarkdownIt(spec);
let {title, pages, pager = true, toc = true} = spec;
if (title !== undefined) title = String(title);
if (pages !== undefined) pages = Array.from(pages, normalizePageOrSection);
if (pages !== undefined) pages = normalizePages(pages);
if (sidebar !== undefined) sidebar = Boolean(sidebar);
pager = Boolean(pager);
scripts = Array.from(scripts, normalizeScript);
Expand Down Expand Up @@ -213,16 +214,19 @@ function normalizeScript(spec: any): Script {
return {src, async, type};
}

function normalizePageOrSection(spec: any): Page | Section {
return ("pages" in spec ? normalizeSection : normalizePage)(spec);
function normalizePages(spec: any): Config["pages"] {
return Array.from(spec, (spec: any) =>
"pages" in spec ? normalizeSection(spec, (spec: any) => normalizePage(spec)) : normalizePage(spec)
);
}

function normalizeSection(spec: any): Section {
let {name, open = true, pages} = spec;
function normalizeSection<T>(spec: any, normalizePage: (spec: any) => T): Section<T> {
let {name, open, collapsible = open === undefined ? false : true, pages} = spec;
name = String(name);
open = Boolean(open);
collapsible = Boolean(collapsible);
open = collapsible ? Boolean(open) : true;
pages = Array.from(pages, normalizePage);
return {name, open, pages};
return {name, collapsible, open, pages};
}

function normalizePage(spec: any): Page {
Expand Down
16 changes: 8 additions & 8 deletions src/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import mime from "mime";
import type {Config, Page, Script} from "./config.js";
import type {Config, Page, Script, Section} from "./config.js";
import {mergeToc} from "./config.js";
import {getClientPath} from "./files.js";
import type {Html, HtmlResolvers} from "./html.js";
Expand Down Expand Up @@ -146,17 +146,13 @@ async function renderSidebar(options: RenderOptions): Promise<Html> {
<ol>${pages.map((p, i) =>
"pages" in p
? html`${i > 0 && "path" in pages[i - 1] ? html`</ol>` : ""}
<details${
p.pages.some((p) => normalizePath(p.path) === path)
? html` open class="observablehq-section-active"`
: p.open
? " open"
: ""
<${p.collapsible ? (p.open || isSectionActive(p, path) ? "details open" : "details") : "section"}${
isSectionActive(p, path) ? html` class="observablehq-section-active"` : ""
}>
<summary>${p.name}</summary>
<ol>${p.pages.map((p) => renderListItem(p, path, normalizeLink))}
</ol>
</details>`
</${p.collapsible ? "details" : "section"}>`
: "path" in p
? html`${i > 0 && "pages" in pages[i - 1] ? html`\n </ol>\n <ol>` : ""}${renderListItem(
p,
Expand All @@ -172,6 +168,10 @@ async function renderSidebar(options: RenderOptions): Promise<Html> {
)}}</script>`;
}

function isSectionActive(s: Section<Page>, path: string): boolean {
return s.pages.some((p) => normalizePath(p.path) === path);
}

interface Header {
label: string;
href: string;
Expand Down
15 changes: 9 additions & 6 deletions src/style/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
}

#observablehq-sidebar > ol,
#observablehq-sidebar > details {
#observablehq-sidebar > details,
#observablehq-sidebar > section {
position: relative;
padding-bottom: 0.5rem;
margin: 0.5rem 0;
Expand All @@ -127,7 +128,8 @@
}

#observablehq-sidebar > ol:last-child,
#observablehq-sidebar > details:last-child {
#observablehq-sidebar > details:last-child,
#observablehq-sidebar > section:last-child {
border-bottom: none;
}

Expand All @@ -142,7 +144,7 @@
display: none;
}

#observablehq-sidebar summary::after {
#observablehq-sidebar details summary::after {
position: absolute;
right: 0.5rem;
width: 1rem;
Expand All @@ -156,7 +158,7 @@
transform-origin: 50% 50%;
}

#observablehq-sidebar summary:hover::after {
#observablehq-sidebar details summary:hover::after {
color: var(--theme-foreground);
}

Expand Down Expand Up @@ -217,7 +219,7 @@
align-items: center;
}

#observablehq-sidebar summary:hover,
#observablehq-sidebar details summary:hover,
.observablehq-link-active a,
.observablehq-link a:hover {
background: var(--theme-background);
Expand Down Expand Up @@ -467,7 +469,8 @@
}

#observablehq-sidebar.observablehq-search-results > ol:not(:first-child),
#observablehq-sidebar.observablehq-search-results > details {
#observablehq-sidebar.observablehq-search-results > details,
#observablehq-sidebar.observablehq-search-results > section {
display: none;
}

Expand Down
9 changes: 7 additions & 2 deletions test/config-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ describe("readConfig(undefined, root)", () => {
{path: "/index", name: "Index"},
{path: "/one", name: "One<Two"},
{name: "Two", path: "/sub/two"},
{name: "Closed subsection", open: false, pages: [{name: "Closed page", path: "/closed/page"}]}
{
name: "Closed subsection",
collapsible: true,
open: false,
pages: [{name: "Closed page", path: "/closed/page"}]
}
],
title: undefined,
toc: {label: "On this page", show: true},
Expand Down Expand Up @@ -136,7 +141,7 @@ describe("normalizeConfig(spec, root)", () => {
});
it("coerces sections", () => {
const inpages = [{name: 42, pages: new Set([{name: null, path: {toString: () => "yes"}}])}];
const outpages = [{name: "42", open: true, pages: [{name: "null", path: "/yes"}]}];
const outpages = [{name: "42", collapsible: false, open: true, pages: [{name: "null", path: "/yes"}]}];
assert.deepStrictEqual(config({pages: inpages}, root).pages, outpages);
});
it("coerces toc", () => {
Expand Down
1 change: 1 addition & 0 deletions test/pager-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("findLink(path, options)", () => {
pages: [
{
name: "section",
collapsible: true,
open: true,
pages: [
{name: "a", path: "/a"},
Expand Down