@docpensieve/core 0.4.0-beta.1 → 0.4.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/config.js +64 -18
- package/src/generator.js +32 -7
- package/src/index.js +7 -1
- package/src/sidebar.js +52 -0
- package/templates/header-menu.hbs +23 -1
- package/templates/layout.hbs +9 -0
- package/templates/nav-items.hbs +17 -1
- package/types/config.d.ts +21 -4
- package/types/index.d.ts +3 -1
- package/types/sidebar.d.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docpensieve/core",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.2",
|
|
4
4
|
"description": "DocPensieve engine: loading, MDX compilation, structured data, site generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"types"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@docpensieve/shared": "0.4.0-beta.
|
|
24
|
+
"@docpensieve/shared": "0.4.0-beta.2",
|
|
25
25
|
"@mdx-js/mdx": "^3.1.1",
|
|
26
26
|
"@shikijs/rehype": "^4.4.3",
|
|
27
27
|
"gray-matter": "^4.0.3",
|
package/src/config.js
CHANGED
|
@@ -39,8 +39,11 @@ import {
|
|
|
39
39
|
* @property {{ framework: string, darkMode?: string, toggle?: boolean, tokens?: Record<string, string>, css?: string, source?: string }} theme
|
|
40
40
|
* @property {string} sidebar `'auto'`, or the path of a description.
|
|
41
41
|
* @property {string} [authors] Path of a JSON describing the authors, read in each version folder.
|
|
42
|
-
* @property {{ label: string, href: string, version?: string }[]} [headerLinks]
|
|
43
|
-
* Links of the header, beside the version switcher.
|
|
42
|
+
* @property {{ label: string, href?: string, version?: string, columns?: { title?: string, items: { label: string, href: string, version?: string }[] }[] }[]} [headerLinks]
|
|
43
|
+
* Links of the header, beside the version switcher. An entry carrying
|
|
44
|
+
* `columns` opens a panel of links instead of leading anywhere itself.
|
|
45
|
+
* @property {boolean} [foldedSidebar] Categories of the menu fold, opened on
|
|
46
|
+
* the branch of the page being read.
|
|
44
47
|
* @property {boolean} globalComponents
|
|
45
48
|
* @property {boolean} scrollToTop Back-to-top button on every page.
|
|
46
49
|
* @property {{ enabled: boolean }} jsonld
|
|
@@ -80,6 +83,9 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
80
83
|
// No link in the header by default: the version switcher and the search
|
|
81
84
|
// field are there already.
|
|
82
85
|
headerLinks: [],
|
|
86
|
+
// The menu shows whole by default: a documentation of a few dozen pages
|
|
87
|
+
// reads better open than behind folds. Long ones turn this on.
|
|
88
|
+
foldedSidebar: false,
|
|
83
89
|
globalComponents: true,
|
|
84
90
|
scrollToTop: true,
|
|
85
91
|
jsonld: { enabled: true },
|
|
@@ -279,29 +285,69 @@ export function normalizeConfig(userConfig) {
|
|
|
279
285
|
});
|
|
280
286
|
}
|
|
281
287
|
const slugs = new Set(config.versions.map((version) => version.slug));
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
hint: "Write { label: 'Examples', href: '/examples/' }.",
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
+
|
|
289
|
+
/** @param {any} link @param {string} where */
|
|
290
|
+
const checkTarget = (link, where) => {
|
|
288
291
|
if (
|
|
289
292
|
typeof link.href !== 'string' ||
|
|
290
293
|
!(link.href.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(link.href))
|
|
291
294
|
) {
|
|
292
|
-
throw new ConfigError(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
hint: "Start from the root of the version — '/examples/' — or give a full address.",
|
|
296
|
-
},
|
|
297
|
-
);
|
|
295
|
+
throw new ConfigError(`${where} needs an absolute target: "${String(link.href)}".`, {
|
|
296
|
+
hint: "Start from the root of the version — '/examples/' — or give a full address.",
|
|
297
|
+
});
|
|
298
298
|
}
|
|
299
299
|
if (link.version !== undefined && !slugs.has(link.version)) {
|
|
300
|
-
throw new ConfigError(
|
|
301
|
-
`
|
|
302
|
-
|
|
303
|
-
);
|
|
300
|
+
throw new ConfigError(`${where} names an unknown version: "${String(link.version)}".`, {
|
|
301
|
+
hint: `Declared versions: ${[...slugs].join(', ')}.`,
|
|
302
|
+
});
|
|
304
303
|
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
for (const link of config.headerLinks) {
|
|
307
|
+
if (!link || typeof link.label !== 'string' || link.label.trim() === '') {
|
|
308
|
+
throw new ConfigError(`A header link has no label: ${JSON.stringify(link)}.`, {
|
|
309
|
+
hint: "Write { label: 'Examples', href: '/examples/' }.",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// An entry either leads somewhere, or opens a panel of links: both at
|
|
314
|
+
// once would leave a click meaning two things.
|
|
315
|
+
if (link.columns !== undefined) {
|
|
316
|
+
if (!Array.isArray(link.columns) || link.columns.length === 0) {
|
|
317
|
+
throw new ConfigError(`The columns of "${link.label}" must be a list of columns.`, {
|
|
318
|
+
hint: "Write columns: [{ title: 'Guide', items: [{ label: 'Install', href: '/guide/install/' }] }].",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (link.href !== undefined) {
|
|
322
|
+
throw new ConfigError(`The header entry "${link.label}" has both href and columns.`, {
|
|
323
|
+
hint: 'An entry either leads somewhere, or opens a panel: drop one of the two.',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
for (const column of link.columns) {
|
|
327
|
+
if (!column || !Array.isArray(column.items) || column.items.length === 0) {
|
|
328
|
+
throw new ConfigError(`A column of "${link.label}" holds no link.`, {
|
|
329
|
+
hint: "Every column needs items: [{ label: 'Install', href: '/guide/install/' }].",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (column.title !== undefined && typeof column.title !== 'string') {
|
|
333
|
+
throw new ConfigError(`A column title of "${link.label}" must be text.`, {
|
|
334
|
+
hint: 'Either write a title, or leave the field out.',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
for (const item of column.items) {
|
|
338
|
+
if (!item || typeof item.label !== 'string' || item.label.trim() === '') {
|
|
339
|
+
throw new ConfigError(
|
|
340
|
+
`A link of "${link.label}" has no label: ${JSON.stringify(item)}.`,
|
|
341
|
+
{ hint: "Write { label: 'Install', href: '/guide/install/' }." },
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
checkTarget(item, `The link "${item.label}" of "${link.label}"`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
checkTarget(link, `The header link "${link.label}"`);
|
|
305
351
|
}
|
|
306
352
|
}
|
|
307
353
|
|
package/src/generator.js
CHANGED
|
@@ -28,7 +28,12 @@ import { buildFeed, buildRobots, buildSitemap } from './discovery.js';
|
|
|
28
28
|
import { imageSize } from './image-size.js';
|
|
29
29
|
import { minifyCss } from './minify-css.js';
|
|
30
30
|
import { SEARCH_SLUG, htmlToText, searchPageContent } from './search-index.js';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
buildSidebar,
|
|
33
|
+
buildSidebarFromDescription,
|
|
34
|
+
collectSectionTitles,
|
|
35
|
+
foldSidebar,
|
|
36
|
+
} from './sidebar.js';
|
|
32
37
|
import { StructuredDataBuilder } from './structured-data.js';
|
|
33
38
|
|
|
34
39
|
/** Template folder, resolved from this module rather than from the cwd. */
|
|
@@ -245,6 +250,7 @@ export class SiteGenerator {
|
|
|
245
250
|
? await this.#describedSidebar(sourceDir, docs, pageUrl, version.folder)
|
|
246
251
|
: null;
|
|
247
252
|
const sidebar = described ?? buildSidebar(docs, pageUrl, { brand: this.config.projectName });
|
|
253
|
+
const folded = this.config.foldedSidebar === true;
|
|
248
254
|
const breadcrumbTitles = collectSectionTitles(docs);
|
|
249
255
|
const layout = await this.#loadLayout();
|
|
250
256
|
const classes = this.#classes();
|
|
@@ -276,13 +282,29 @@ export class SiteGenerator {
|
|
|
276
282
|
// Links of the header, resolved once per version. A link naming a version
|
|
277
283
|
// leads there from every version: a section written in one version only
|
|
278
284
|
// stays reachable from the others.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
/** @param {{ href: string, version?: string }} link */
|
|
286
|
+
const headerTarget = (link) =>
|
|
287
|
+
EXTERNAL_PREVIEW.test(link.href)
|
|
282
288
|
? link.href
|
|
283
289
|
: joinUrl(this.config.baseUrl, 'versions', link.version ?? version.slug) +
|
|
284
|
-
link.href.replace(/^\/+/, '')
|
|
285
|
-
|
|
290
|
+
link.href.replace(/^\/+/, '');
|
|
291
|
+
|
|
292
|
+
const headerLinks = (this.config.headerLinks ?? []).map((link) =>
|
|
293
|
+
link.columns
|
|
294
|
+
? {
|
|
295
|
+
label: link.label,
|
|
296
|
+
columns: link.columns.map((column) => ({
|
|
297
|
+
title: column.title,
|
|
298
|
+
items: column.items.map((item) => ({ label: item.label, href: headerTarget(item) })),
|
|
299
|
+
})),
|
|
300
|
+
}
|
|
301
|
+
: // Without columns the configuration has checked the target: an entry
|
|
302
|
+
// that leads nowhere and opens nothing never gets here.
|
|
303
|
+
{
|
|
304
|
+
label: link.label,
|
|
305
|
+
href: headerTarget({ href: String(link.href), version: link.version }),
|
|
306
|
+
},
|
|
307
|
+
);
|
|
286
308
|
|
|
287
309
|
const shell = {
|
|
288
310
|
lang: this.config.lang ?? 'en',
|
|
@@ -436,7 +458,10 @@ export class SiteGenerator {
|
|
|
436
458
|
noindex: version.prerelease === true,
|
|
437
459
|
byline,
|
|
438
460
|
tags: wide ? [] : pageTags(doc.frontmatter.tags),
|
|
439
|
-
|
|
461
|
+
// Folded, the menu opens on the branch of the page being rendered, so
|
|
462
|
+
// it is prepared per page rather than once per version.
|
|
463
|
+
sidebar: wide ? [] : folded ? foldSidebar(sidebar, url) : sidebar,
|
|
464
|
+
foldedSidebar: folded,
|
|
440
465
|
toc: wide ? [] : toc,
|
|
441
466
|
preloads,
|
|
442
467
|
content: html,
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* @typedef {import('./compiler.js').TocEntry} TocEntry
|
|
20
20
|
* @typedef {import('./compiler.js').Preload} Preload
|
|
21
21
|
* @typedef {import('./sidebar.js').SidebarNode} SidebarNode
|
|
22
|
+
* @typedef {import('./sidebar.js').FoldedNode} FoldedNode
|
|
22
23
|
* @typedef {import('./authors.js').Author} Author
|
|
23
24
|
* @typedef {import('./authors.js').Byline} Byline
|
|
24
25
|
*/
|
|
@@ -34,6 +35,11 @@ export { DocLoader } from './loader.js';
|
|
|
34
35
|
export { Compiler } from './compiler.js';
|
|
35
36
|
export { StructuredDataBuilder } from './structured-data.js';
|
|
36
37
|
export { SiteGenerator } from './generator.js';
|
|
37
|
-
export {
|
|
38
|
+
export {
|
|
39
|
+
buildSidebar,
|
|
40
|
+
buildSidebarFromDescription,
|
|
41
|
+
collectSectionTitles,
|
|
42
|
+
foldSidebar,
|
|
43
|
+
} from './sidebar.js';
|
|
38
44
|
export { buildFeed, buildRobots, buildSitemap } from './discovery.js';
|
|
39
45
|
export { buildAuthorTable, buildByline, readDate, resolvePageAuthors } from './authors.js';
|
package/src/sidebar.js
CHANGED
|
@@ -104,6 +104,58 @@ export function collectSectionTitles(docs) {
|
|
|
104
104
|
return titles;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Prepares a menu whose categories fold, for the page being rendered.
|
|
109
|
+
*
|
|
110
|
+
* Two things change. Every category carries `open`, true along the branch
|
|
111
|
+
* holding the current page: a long menu opens where the reader stands, and
|
|
112
|
+
* stays closed everywhere else. And a category that is itself a page gains
|
|
113
|
+
* that page as its first entry — folded, its title becomes the handle of the
|
|
114
|
+
* fold, which cannot be a link as well without a click meaning two things.
|
|
115
|
+
*
|
|
116
|
+
* The tree is rebuilt rather than marked in place: it is shared by every page
|
|
117
|
+
* of the version, and marking it would leave one page's branch open on all the
|
|
118
|
+
* others.
|
|
119
|
+
*
|
|
120
|
+
* @typedef {object} FoldedNode
|
|
121
|
+
* @property {string} label
|
|
122
|
+
* @property {string | null} url
|
|
123
|
+
* @property {FoldedNode[]} items
|
|
124
|
+
* @property {boolean} [open] Whether the category starts open. Absent on a
|
|
125
|
+
* plain entry, which has nothing to fold.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {SidebarNode[]} nodes
|
|
130
|
+
* @param {string} [currentUrl] URL of the page being rendered.
|
|
131
|
+
* @returns {FoldedNode[]}
|
|
132
|
+
*/
|
|
133
|
+
export function foldSidebar(nodes, currentUrl = '') {
|
|
134
|
+
return nodes.map((node) => {
|
|
135
|
+
if (node.items.length === 0) return { ...node };
|
|
136
|
+
|
|
137
|
+
const items = foldSidebar(
|
|
138
|
+
node.url ? [{ label: node.label, url: node.url, items: [] }, ...node.items] : node.items,
|
|
139
|
+
currentUrl,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Open when the reader is inside: on the category's own page, or on any
|
|
143
|
+
// page it holds, however deep.
|
|
144
|
+
return { ...node, items, open: node.url === currentUrl || holds(items, currentUrl) };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether a branch holds the current page.
|
|
150
|
+
*
|
|
151
|
+
* @param {FoldedNode[]} nodes
|
|
152
|
+
* @param {string} currentUrl
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
function holds(nodes, currentUrl) {
|
|
156
|
+
return nodes.some((node) => node.url === currentUrl || holds(node.items, currentUrl));
|
|
157
|
+
}
|
|
158
|
+
|
|
107
159
|
/**
|
|
108
160
|
* An entry of a sidebar description: a page path, or an object — see
|
|
109
161
|
* `buildSidebarFromDescription`.
|
|
@@ -15,7 +15,29 @@ a row on a wide screen, behind a menu button on a narrow one. --}}
|
|
|
15
15
|
{{/if}}
|
|
16
16
|
{{#if headerLinks.length}}
|
|
17
17
|
<ul class="{{{cls.headerLinks}}}">
|
|
18
|
-
{{#each headerLinks}}
|
|
18
|
+
{{#each headerLinks}}
|
|
19
|
+
<li>
|
|
20
|
+
{{#if columns.length}}
|
|
21
|
+
{{!-- A panel of links rather than a link: a details again, so that it
|
|
22
|
+
opens without a script, and folds into the menu of a narrow screen. --}}
|
|
23
|
+
<details class="{{{@root.cls.mega}}}">
|
|
24
|
+
<summary>{{label}}</summary>
|
|
25
|
+
<div class="{{{@root.cls.megaPanel}}}">
|
|
26
|
+
{{#each columns}}
|
|
27
|
+
<div class="{{{@root.cls.megaColumn}}}">
|
|
28
|
+
{{#if title}}<p class="{{{@root.cls.megaTitle}}}">{{title}}</p>{{/if}}
|
|
29
|
+
<ul>
|
|
30
|
+
{{#each items}}<li><a href="{{href}}">{{label}}</a></li>{{/each}}
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
{{/each}}
|
|
34
|
+
</div>
|
|
35
|
+
</details>
|
|
36
|
+
{{else}}
|
|
37
|
+
<a href="{{href}}">{{label}}</a>
|
|
38
|
+
{{/if}}
|
|
39
|
+
</li>
|
|
40
|
+
{{/each}}
|
|
19
41
|
</ul>
|
|
20
42
|
{{/if}}
|
|
21
43
|
{{!-- A plain form: it leads to the search page, and needs no script. --}}
|
package/templates/layout.hbs
CHANGED
|
@@ -85,10 +85,19 @@
|
|
|
85
85
|
</header>
|
|
86
86
|
|
|
87
87
|
<div class="{{#if wide}}{{{cls.shellWide}}}{{else}}{{{cls.shell}}}{{/if}}">
|
|
88
|
+
{{!-- Written once, shown twice: the column on a wide screen, and a folded
|
|
89
|
+
menu above the content on a narrow one, where a full menu would eat the
|
|
90
|
+
first screen. Only one of the two is displayed at a time. --}}
|
|
88
91
|
{{#if sidebar.length}}
|
|
89
92
|
<nav class="{{{cls.sidebar}}}" aria-label="Documentation navigation">
|
|
90
93
|
{{> navItems sidebar}}
|
|
91
94
|
</nav>
|
|
95
|
+
<details class="{{{cls.sidebarMenu}}}">
|
|
96
|
+
<summary>Documentation menu</summary>
|
|
97
|
+
<nav aria-label="Documentation navigation">
|
|
98
|
+
{{> navItems sidebar}}
|
|
99
|
+
</nav>
|
|
100
|
+
</details>
|
|
92
101
|
{{/if}}
|
|
93
102
|
|
|
94
103
|
<main class="{{{cls.main}}}" id="content" tabindex="-1">
|
package/templates/nav-items.hbs
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
<ul class="{{{@root.cls.nav}}}">
|
|
2
2
|
{{#each this}}
|
|
3
3
|
<li class="{{{@root.cls.navItem}}}{{#if items.length}} {{{@root.cls.navItemParent}}}{{/if}}">
|
|
4
|
+
{{#if items.length}}
|
|
5
|
+
{{#if @root.foldedSidebar}}
|
|
6
|
+
{{!-- Folded, a category is a native details: it opens without a script,
|
|
7
|
+
and starts open on the branch the reader is in. Its own page sits as the
|
|
8
|
+
first entry, since the handle of the fold cannot be a link as well. --}}
|
|
9
|
+
<details class="{{{@root.cls.navGroup}}}"{{#if open}} open{{/if}}>
|
|
10
|
+
<summary class="{{{@root.cls.navSummary}}}">{{label}}</summary>
|
|
11
|
+
{{> navItems items}}
|
|
12
|
+
</details>
|
|
13
|
+
{{else}}
|
|
4
14
|
{{#if url}}
|
|
5
15
|
<a class="{{{@root.cls.navLink}}}" href="{{url}}"{{#if (eq url @root.currentUrl)}} aria-current="page"{{/if}}>{{label}}</a>
|
|
6
16
|
{{else}}
|
|
7
17
|
<span class="{{{@root.cls.navLabel}}}">{{label}}</span>
|
|
8
18
|
{{/if}}
|
|
9
|
-
{{#if items.length}}
|
|
10
19
|
{{> navItems items}}
|
|
11
20
|
{{/if}}
|
|
21
|
+
{{else}}
|
|
22
|
+
{{#if url}}
|
|
23
|
+
<a class="{{{@root.cls.navLink}}}" href="{{url}}"{{#if (eq url @root.currentUrl)}} aria-current="page"{{/if}}>{{label}}</a>
|
|
24
|
+
{{else}}
|
|
25
|
+
<span class="{{{@root.cls.navLabel}}}">{{label}}</span>
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{/if}}
|
|
12
28
|
</li>
|
|
13
29
|
{{/each}}
|
|
14
30
|
</ul>
|
package/types/config.d.ts
CHANGED
|
@@ -76,13 +76,27 @@ export type DocPensieveConfig = {
|
|
|
76
76
|
*/
|
|
77
77
|
authors?: string;
|
|
78
78
|
/**
|
|
79
|
-
* Links of the header, beside the version switcher.
|
|
79
|
+
* Links of the header, beside the version switcher. An entry carrying
|
|
80
|
+
* `columns` opens a panel of links instead of leading anywhere itself.
|
|
80
81
|
*/
|
|
81
82
|
headerLinks?: {
|
|
82
83
|
label: string;
|
|
83
|
-
href
|
|
84
|
+
href?: string;
|
|
84
85
|
version?: string;
|
|
86
|
+
columns?: {
|
|
87
|
+
title?: string;
|
|
88
|
+
items: {
|
|
89
|
+
label: string;
|
|
90
|
+
href: string;
|
|
91
|
+
version?: string;
|
|
92
|
+
}[];
|
|
93
|
+
}[];
|
|
85
94
|
}[];
|
|
95
|
+
/**
|
|
96
|
+
* Categories of the menu fold, opened on
|
|
97
|
+
* the branch of the page being read.
|
|
98
|
+
*/
|
|
99
|
+
foldedSidebar?: boolean;
|
|
86
100
|
globalComponents: boolean;
|
|
87
101
|
/**
|
|
88
102
|
* Back-to-top button on every page.
|
|
@@ -150,8 +164,11 @@ export type DocPensieveConfig = {
|
|
|
150
164
|
* @property {{ framework: string, darkMode?: string, toggle?: boolean, tokens?: Record<string, string>, css?: string, source?: string }} theme
|
|
151
165
|
* @property {string} sidebar `'auto'`, or the path of a description.
|
|
152
166
|
* @property {string} [authors] Path of a JSON describing the authors, read in each version folder.
|
|
153
|
-
* @property {{ label: string, href: string, version?: string }[]} [headerLinks]
|
|
154
|
-
* Links of the header, beside the version switcher.
|
|
167
|
+
* @property {{ label: string, href?: string, version?: string, columns?: { title?: string, items: { label: string, href: string, version?: string }[] }[] }[]} [headerLinks]
|
|
168
|
+
* Links of the header, beside the version switcher. An entry carrying
|
|
169
|
+
* `columns` opens a panel of links instead of leading anywhere itself.
|
|
170
|
+
* @property {boolean} [foldedSidebar] Categories of the menu fold, opened on
|
|
171
|
+
* the branch of the page being read.
|
|
155
172
|
* @property {boolean} globalComponents
|
|
156
173
|
* @property {boolean} scrollToTop Back-to-top button on every page.
|
|
157
174
|
* @property {{ enabled: boolean }} jsonld
|
package/types/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type CompileResult = import('./compiler.js').CompileResult;
|
|
|
14
14
|
export type TocEntry = import('./compiler.js').TocEntry;
|
|
15
15
|
export type Preload = import('./compiler.js').Preload;
|
|
16
16
|
export type SidebarNode = import('./sidebar.js').SidebarNode;
|
|
17
|
+
export type FoldedNode = import('./sidebar.js').FoldedNode;
|
|
17
18
|
export type Author = import('./authors.js').Author;
|
|
18
19
|
export type Byline = import('./authors.js').Byline;
|
|
19
20
|
/**
|
|
@@ -27,6 +28,7 @@ export type Byline = import('./authors.js').Byline;
|
|
|
27
28
|
* @typedef {import('./compiler.js').TocEntry} TocEntry
|
|
28
29
|
* @typedef {import('./compiler.js').Preload} Preload
|
|
29
30
|
* @typedef {import('./sidebar.js').SidebarNode} SidebarNode
|
|
31
|
+
* @typedef {import('./sidebar.js').FoldedNode} FoldedNode
|
|
30
32
|
* @typedef {import('./authors.js').Author} Author
|
|
31
33
|
* @typedef {import('./authors.js').Byline} Byline
|
|
32
34
|
*/
|
|
@@ -35,6 +37,6 @@ export { DocLoader } from './loader.js';
|
|
|
35
37
|
export { Compiler } from './compiler.js';
|
|
36
38
|
export { StructuredDataBuilder } from './structured-data.js';
|
|
37
39
|
export { SiteGenerator } from './generator.js';
|
|
38
|
-
export { buildSidebar, buildSidebarFromDescription, collectSectionTitles } from './sidebar.js';
|
|
40
|
+
export { buildSidebar, buildSidebarFromDescription, collectSectionTitles, foldSidebar, } from './sidebar.js';
|
|
39
41
|
export { buildFeed, buildRobots, buildSitemap } from './discovery.js';
|
|
40
42
|
export { buildAuthorTable, buildByline, readDate, resolvePageAuthors } from './authors.js';
|
package/types/sidebar.d.ts
CHANGED
|
@@ -58,6 +58,42 @@ export declare function buildSidebar(docs: import('./loader.js').Doc[], toUrl?:
|
|
|
58
58
|
* @returns {Record<string, string>} Full folder slug to title.
|
|
59
59
|
*/
|
|
60
60
|
export declare function collectSectionTitles(docs: import('./loader.js').Doc[]): Record<string, string>;
|
|
61
|
+
export type FoldedNode = {
|
|
62
|
+
label: string;
|
|
63
|
+
url: string | null;
|
|
64
|
+
items: FoldedNode[];
|
|
65
|
+
/**
|
|
66
|
+
* Whether the category starts open. Absent on a
|
|
67
|
+
* plain entry, which has nothing to fold.
|
|
68
|
+
*/
|
|
69
|
+
open?: boolean;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Prepares a menu whose categories fold, for the page being rendered.
|
|
73
|
+
*
|
|
74
|
+
* Two things change. Every category carries `open`, true along the branch
|
|
75
|
+
* holding the current page: a long menu opens where the reader stands, and
|
|
76
|
+
* stays closed everywhere else. And a category that is itself a page gains
|
|
77
|
+
* that page as its first entry — folded, its title becomes the handle of the
|
|
78
|
+
* fold, which cannot be a link as well without a click meaning two things.
|
|
79
|
+
*
|
|
80
|
+
* The tree is rebuilt rather than marked in place: it is shared by every page
|
|
81
|
+
* of the version, and marking it would leave one page's branch open on all the
|
|
82
|
+
* others.
|
|
83
|
+
*
|
|
84
|
+
* @typedef {object} FoldedNode
|
|
85
|
+
* @property {string} label
|
|
86
|
+
* @property {string | null} url
|
|
87
|
+
* @property {FoldedNode[]} items
|
|
88
|
+
* @property {boolean} [open] Whether the category starts open. Absent on a
|
|
89
|
+
* plain entry, which has nothing to fold.
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* @param {SidebarNode[]} nodes
|
|
93
|
+
* @param {string} [currentUrl] URL of the page being rendered.
|
|
94
|
+
* @returns {FoldedNode[]}
|
|
95
|
+
*/
|
|
96
|
+
export declare function foldSidebar(nodes: SidebarNode[], currentUrl?: string): FoldedNode[];
|
|
61
97
|
export type SidebarEntry = string | {
|
|
62
98
|
page?: string;
|
|
63
99
|
label?: string;
|