@ascribed/astro 0.3.3-next.24 → 0.4.0

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/dist/nav.d.ts CHANGED
@@ -4,15 +4,61 @@ export interface NavGroup {
4
4
  /** Source files, relative to the content root, as `Guides/My Setup.md`. */
5
5
  pages: string[];
6
6
  }
7
- /** A page of the sidebar: its collection entry, and its address. */
7
+ /** A label in the site output's formatted form: HTML, with code spans as `<code>`. */
8
+ export interface Formatted {
9
+ label: string;
10
+ }
11
+ /** A page of the sidebar: its collection entry, its address, and its label. */
8
12
  export interface SidebarPage<E> {
9
13
  entry: E;
10
14
  href: string;
15
+ /** Its label as plain text: its `nav-label`, else its title. */
16
+ label: string;
17
+ /** Its label as HTML, when it's read with inline markup, as the title is. */
18
+ formatted?: Formatted;
11
19
  }
12
- /** A group of the sidebar, each of its pages with its entry. */
20
+ /**
21
+ * A group of the sidebar: a section of the navigation, or a group of a
22
+ * site's own list. `pages` starts with the section's own page when that's
23
+ * its `index.md`; an own page beside the folder (`checks/diagnostics.md`
24
+ * for `checks/diagnostics/`) is among the parent's pages instead, and the
25
+ * nested group sits right after it.
26
+ */
13
27
  export interface SidebarGroup<E> {
14
28
  label: string;
29
+ /** Its label as HTML, when it's its own page's title, read with inline markup. */
30
+ formatted?: Formatted;
31
+ /** The folder, from the content root, ending in `/`; absent for a group of a site's own list. */
32
+ folder?: string;
33
+ /** Its own page, when the build offers one. */
34
+ page?: SidebarPage<E>;
15
35
  pages: SidebarPage<E>[];
36
+ /** Its nested sections, in order; empty for a group of a site's own list. */
37
+ sections: SidebarGroup<E>[];
38
+ }
39
+ /** A page in `_ascribe/nav.json`. */
40
+ export interface NavPage {
41
+ /** Its path from the content root, as its record's `source`. */
42
+ source: string;
43
+ label: string;
44
+ formatted?: Formatted;
45
+ /** Its content type. */
46
+ type?: string;
47
+ }
48
+ /** A section in `_ascribe/nav.json`. */
49
+ export interface NavSection {
50
+ /** From the content root, ending in `/`; `""` for the root. */
51
+ folder: string;
52
+ label: string;
53
+ formatted?: Formatted;
54
+ page?: NavPage;
55
+ pages: NavPage[];
56
+ sections: NavSection[];
57
+ }
58
+ /** `_ascribe/nav.json`. */
59
+ export interface Navigation {
60
+ version: number;
61
+ root: NavSection;
16
62
  }
17
63
  /** What the check needs of a collection entry: its id, and the file it was loaded from. */
18
64
  export interface NavEntry {
@@ -30,6 +76,41 @@ export declare function entryId(pagePath: string): string;
30
76
  * plus its entry id, and the root page (`index`) at the base path itself.
31
77
  */
32
78
  export declare function pageRoute(id: string, basePath: string, trailingSlash: "always" | "never"): string;
79
+ /**
80
+ * The sidebar's groups from the project's navigation: the root's own page
81
+ * and pages under its label, "Start" unless `ascribe.nav` gives another,
82
+ * when it has any, then each top section, each page with its entry and
83
+ * address. `source` gives an entry's page's source path, which the
84
+ * navigation names it by. Throws, naming each, when the navigation names a
85
+ * page the collection has no entry for.
86
+ */
87
+ export declare function navigationGroups<E extends NavEntry>(navigation: Navigation, entries: readonly E[], route: (entry: E) => string, source: (entry: E) => string): SidebarGroup<E>[];
88
+ /**
89
+ * The navigation's pages in reading order, depth first: a section's own
90
+ * page, then its pages, each nested section whose own page is beside its
91
+ * folder right after that page, with its pages and nested sections, and the
92
+ * other nested sections after them, each from its own page. The content
93
+ * root reads as the sidebar groups it: its own page and its pages that
94
+ * aren't beside a folder, then each top section from its own page. Previous
95
+ * and next follow it, as an archive and `llms.txt` do.
96
+ */
97
+ export declare function readingOrder(navigation: Navigation): NavPage[];
98
+ /** A page's previous and next, each with its entry, its address, and its label. */
99
+ export interface Neighbors<E> {
100
+ previous?: SidebarPage<E>;
101
+ next?: SidebarPage<E>;
102
+ }
103
+ /**
104
+ * The pages before and after `at` in `pages`, the order a reader goes in.
105
+ * Neither, for a page that isn't in it.
106
+ */
107
+ export declare function neighborsIn<E>(pages: readonly SidebarPage<E>[], at: (page: SidebarPage<E>) => boolean): Neighbors<E>;
108
+ /**
109
+ * The navigation's pages in reading order, each with its entry, its
110
+ * address, and its label. `source` gives an entry's page's source path.
111
+ * A page the collection has no entry for is passed over.
112
+ */
113
+ export declare function readingPages<E extends NavEntry>(navigation: Navigation, entries: readonly E[], route: (entry: E) => string, source: (entry: E) => string): SidebarPage<E>[];
33
114
  /**
34
115
  * The ways the list and the collection disagree, as sentences: a page the
35
116
  * list leaves out, a file it names that isn't a page, and a page it names twice.
@@ -37,9 +118,10 @@ export declare function pageRoute(id: string, basePath: string, trailingSlash: "
37
118
  */
38
119
  export declare function navProblems(nav: readonly NavGroup[], entries: readonly NavEntry[], source?: (entry: NavEntry) => string): string[];
39
120
  /**
40
- * The sidebar's groups, each page with its entry and address. Throws, naming
41
- * each, when the list leaves out a page of the collection, names a file that
42
- * isn't one, or names a page twice, so the list stays complete.
121
+ * The sidebar's groups from a site's own list, each page with its entry, its
122
+ * address, and its label, which `label` gives, or else its entry id. Throws,
123
+ * naming each, when the list leaves out a page of the collection, names a
124
+ * file that isn't one, or names a page twice, so the list stays complete.
43
125
  *
44
126
  * A versioned build's collection has every line's pages, and the list is
45
127
  * checked against them all: `id` gives an entry's id within its line, and
@@ -49,11 +131,4 @@ export declare function navProblems(nav: readonly NavGroup[], entries: readonly
49
131
  export declare function sidebarGroups<E extends NavEntry>(nav: readonly NavGroup[], entries: readonly E[], route: (id: string) => string, source?: (entry: NavEntry) => string, lines?: {
50
132
  id: (entry: E) => string;
51
133
  shown: (entry: E) => boolean;
52
- }): SidebarGroup<E>[];
53
- /**
54
- * The starter's list for a project's pages, by their paths in the site
55
- * output: the pages at the content root in a group named "Start", the root
56
- * page first, then a group for each folder, named as the folder is, in order
57
- * of their names.
58
- */
59
- export declare function navFor(pages: readonly string[]): NavGroup[];
134
+ }, label?: (entry: E) => string | undefined): SidebarGroup<E>[];
package/dist/nav.js CHANGED
@@ -1,8 +1,6 @@
1
- // A site's sidebar: its list of pages, which the starter writes as the site's
2
- // `src/nav.ts` (`npx @ascribed/astro init`), checked against the collection.
3
- // Ascribe's content model has no page order or grouping yet, so the site keeps
4
- // the list, and the check keeps it complete: `ascribe.toml` giving a site its
5
- // navigation is the surface audit's finding 28, which has no design yet.
1
+ // A site's sidebar: the project's navigation, `_ascribe/nav.json` in a
2
+ // build's site output (the navigation contract), joined to the collection;
3
+ // or a site's own list of pages, `{ nav }`, checked against the collection.
6
4
  import { slug } from "github-slugger";
7
5
  /**
8
6
  * A page's entry id, as Astro's `glob` loader computes it from the page's
@@ -26,6 +24,138 @@ export function pageRoute(id, basePath, trailingSlash) {
26
24
  return trailingSlash === "never" && basePath !== "/" ? basePath.slice(0, -1) : basePath;
27
25
  return `${basePath}${id}${trailingSlash === "always" ? "/" : ""}`;
28
26
  }
27
+ /**
28
+ * The sidebar's groups from the project's navigation: the root's own page
29
+ * and pages under its label, "Start" unless `ascribe.nav` gives another,
30
+ * when it has any, then each top section, each page with its entry and
31
+ * address. `source` gives an entry's page's source path, which the
32
+ * navigation names it by. Throws, naming each, when the navigation names a
33
+ * page the collection has no entry for.
34
+ */
35
+ export function navigationGroups(navigation, entries, route, source) {
36
+ const bySource = new Map(entries.map((entry) => [source(entry), entry]));
37
+ const missing = [];
38
+ const page = (nav) => {
39
+ const entry = bySource.get(nav.source);
40
+ if (entry === undefined) {
41
+ missing.push(nav.source);
42
+ return [];
43
+ }
44
+ return [
45
+ {
46
+ entry,
47
+ href: route(entry),
48
+ label: nav.label,
49
+ ...(nav.formatted === undefined ? {} : { formatted: nav.formatted }),
50
+ },
51
+ ];
52
+ };
53
+ const group = (section, parent) => {
54
+ const own = section.page === undefined ? [] : page(section.page);
55
+ // An own page beside the folder is one of the parent's pages.
56
+ const beside = section.page !== undefined &&
57
+ parent?.pages.some((other) => other.source === section.page?.source) === true;
58
+ return {
59
+ label: section.label,
60
+ ...(section.formatted === undefined ? {} : { formatted: section.formatted }),
61
+ folder: section.folder,
62
+ ...(own[0] === undefined ? {} : { page: own[0] }),
63
+ pages: [...(beside ? [] : own), ...section.pages.flatMap(page)],
64
+ sections: section.sections.map((nested) => group(nested, section)),
65
+ };
66
+ };
67
+ const { root } = navigation;
68
+ // A top section's own page beside its folder is in that section's group.
69
+ const top = {
70
+ ...group({ ...root, pages: root.pages.filter((nav) => !beside(root, nav)) }, undefined),
71
+ sections: [],
72
+ };
73
+ const groups = [
74
+ ...(top.pages.length === 0 ? [] : [top]),
75
+ ...root.sections.map((section) => group(section, undefined)),
76
+ ];
77
+ if (missing.length > 0) {
78
+ throw new Error(`@ascribed/astro: the project's navigation (_ascribe/nav.json) names pages the collection has no entry for:\n${[...new Set(missing)].map((file) => `- ${file}`).join("\n")}\nBuild the project again (\`ascribe build\`), so the navigation and the pages are from one build.`);
79
+ }
80
+ return groups;
81
+ }
82
+ /**
83
+ * The navigation's pages in reading order, depth first: a section's own
84
+ * page, then its pages, each nested section whose own page is beside its
85
+ * folder right after that page, with its pages and nested sections, and the
86
+ * other nested sections after them, each from its own page. The content
87
+ * root reads as the sidebar groups it: its own page and its pages that
88
+ * aren't beside a folder, then each top section from its own page. Previous
89
+ * and next follow it, as an archive and `llms.txt` do.
90
+ */
91
+ export function readingOrder(navigation) {
92
+ const out = [];
93
+ const read = (section, own) => {
94
+ if (own && section.page !== undefined)
95
+ out.push(section.page);
96
+ const done = section.sections.map(() => false);
97
+ for (const page of section.pages) {
98
+ out.push(page);
99
+ section.sections.forEach((nested, at) => {
100
+ if (!done[at] && nested.page?.source === page.source) {
101
+ done[at] = true;
102
+ read(nested, false);
103
+ }
104
+ });
105
+ }
106
+ section.sections.forEach((nested, at) => {
107
+ if (!done[at])
108
+ read(nested, true);
109
+ });
110
+ };
111
+ const { root } = navigation;
112
+ if (root.page !== undefined)
113
+ out.push(root.page);
114
+ out.push(...root.pages.filter((nav) => !beside(root, nav)));
115
+ for (const section of root.sections)
116
+ read(section, true);
117
+ return out;
118
+ }
119
+ /** Whether `page` is the own page of one of `section`'s nested sections. */
120
+ function beside(section, page) {
121
+ return section.sections.some((nested) => nested.page?.source === page.source);
122
+ }
123
+ /**
124
+ * The pages before and after `at` in `pages`, the order a reader goes in.
125
+ * Neither, for a page that isn't in it.
126
+ */
127
+ export function neighborsIn(pages, at) {
128
+ const i = pages.findIndex(at);
129
+ if (i === -1)
130
+ return {};
131
+ const previous = pages[i - 1];
132
+ const next = pages[i + 1];
133
+ return {
134
+ ...(previous === undefined ? {} : { previous }),
135
+ ...(next === undefined ? {} : { next }),
136
+ };
137
+ }
138
+ /**
139
+ * The navigation's pages in reading order, each with its entry, its
140
+ * address, and its label. `source` gives an entry's page's source path.
141
+ * A page the collection has no entry for is passed over.
142
+ */
143
+ export function readingPages(navigation, entries, route, source) {
144
+ const bySource = new Map(entries.map((entry) => [source(entry), entry]));
145
+ return readingOrder(navigation).flatMap((nav) => {
146
+ const entry = bySource.get(nav.source);
147
+ if (entry === undefined)
148
+ return [];
149
+ return [
150
+ {
151
+ entry,
152
+ href: route(entry),
153
+ label: nav.label,
154
+ ...(nav.formatted === undefined ? {} : { formatted: nav.formatted }),
155
+ },
156
+ ];
157
+ });
158
+ }
29
159
  /**
30
160
  * The ways the list and the collection disagree, as sentences: a page the
31
161
  * list leaves out, a file it names that isn't a page, and a page it names twice.
@@ -49,9 +179,10 @@ export function navProblems(nav, entries, source = (entry) => entry.id) {
49
179
  return problems;
50
180
  }
51
181
  /**
52
- * The sidebar's groups, each page with its entry and address. Throws, naming
53
- * each, when the list leaves out a page of the collection, names a file that
54
- * isn't one, or names a page twice, so the list stays complete.
182
+ * The sidebar's groups from a site's own list, each page with its entry, its
183
+ * address, and its label, which `label` gives, or else its entry id. Throws,
184
+ * naming each, when the list leaves out a page of the collection, names a
185
+ * file that isn't one, or names a page twice, so the list stays complete.
55
186
  *
56
187
  * A versioned build's collection has every line's pages, and the list is
57
188
  * checked against them all: `id` gives an entry's id within its line, and
@@ -61,7 +192,7 @@ export function navProblems(nav, entries, source = (entry) => entry.id) {
61
192
  export function sidebarGroups(nav, entries, route, source, lines = {
62
193
  id: (entry) => entry.id,
63
194
  shown: () => true,
64
- }) {
195
+ }, label) {
65
196
  // A page in several lines is one page of the list, and one problem.
66
197
  const checked = new Map(entries.map((entry) => [{ id: lines.id(entry), filePath: entry.filePath }, entry]));
67
198
  const problems = navProblems(nav, [...checked.keys()], source === undefined ? undefined : (entry) => source(checked.get(entry) ?? entry)).filter((problem, i, all) => all.indexOf(problem) === i);
@@ -73,35 +204,11 @@ export function sidebarGroups(nav, entries, route, source, lines = {
73
204
  label: group.label,
74
205
  pages: group.pages.flatMap((file) => {
75
206
  const entry = byId.get(entryId(file));
76
- return entry === undefined ? [] : [{ entry, href: route(entry.id) }];
207
+ return entry === undefined
208
+ ? []
209
+ : [{ entry, href: route(entry.id), label: label?.(entry) ?? entry.id }];
77
210
  }),
211
+ sections: [],
78
212
  }));
79
213
  }
80
- /**
81
- * The starter's list for a project's pages, by their paths in the site
82
- * output: the pages at the content root in a group named "Start", the root
83
- * page first, then a group for each folder, named as the folder is, in order
84
- * of their names.
85
- */
86
- export function navFor(pages) {
87
- const groups = new Map();
88
- const sorted = pages.toSorted((a, b) => {
89
- const rootA = a === "index.md";
90
- const rootB = b === "index.md";
91
- if (rootA !== rootB)
92
- return rootA ? -1 : 1;
93
- return a < b ? -1 : a > b ? 1 : 0;
94
- });
95
- for (const page of sorted) {
96
- const slash = page.indexOf("/");
97
- const label = slash === -1 ? "Start" : page.slice(0, slash);
98
- groups.set(label, [...(groups.get(label) ?? []), page]);
99
- }
100
- const start = groups.get("Start");
101
- groups.delete("Start");
102
- return [
103
- ...(start === undefined ? [] : [{ label: "Start", pages: start }]),
104
- ...[...groups].map(([label, files]) => ({ label, pages: files })),
105
- ];
106
- }
107
214
  //# sourceMappingURL=nav.js.map
package/dist/nav.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"nav.js","sourceRoot":"","sources":["../src/nav.ts"],"names":[],"mappings":"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","sourcesContent":["// A site's sidebar: its list of pages, which the starter writes as the site's\n// `src/nav.ts` (`npx @ascribed/astro init`), checked against the collection.\n// Ascribe's content model has no page order or grouping yet, so the site keeps\n// the list, and the check keeps it complete: `ascribe.toml` giving a site its\n// navigation is the surface audit's finding 28, which has no design yet.\nimport { slug } from \"github-slugger\";\n\n/** A group of the sidebar: a label, and its pages' source files, in order. */\nexport interface NavGroup {\n label: string;\n /** Source files, relative to the content root, as `Guides/My Setup.md`. */\n pages: string[];\n}\n\n/** A page of the sidebar: its collection entry, and its address. */\nexport interface SidebarPage<E> {\n entry: E;\n href: string;\n}\n\n/** A group of the sidebar, each of its pages with its entry. */\nexport interface SidebarGroup<E> {\n label: string;\n pages: SidebarPage<E>[];\n}\n\n/** What the check needs of a collection entry: its id, and the file it was loaded from. */\nexport interface NavEntry {\n id: string;\n filePath?: string | undefined;\n}\n\n/**\n * A page's entry id, as Astro's `glob` loader computes it from the page's\n * path in the site output: each segment slugged, without the extension, and\n * without a final `/index`.\n */\nexport function entryId(pagePath: string): string {\n return pagePath\n .replace(/\\.md$/, \"\")\n .split(\"/\")\n .map((segment) => slug(segment))\n .join(\"/\")\n .replace(/\\/index$/, \"\");\n}\n\n/**\n * A page's route, as Ascribe's router writes every link to it: the base path\n * plus its entry id, and the root page (`index`) at the base path itself.\n */\nexport function pageRoute(id: string, basePath: string, trailingSlash: \"always\" | \"never\"): string {\n if (id === \"index\")\n return trailingSlash === \"never\" && basePath !== \"/\" ? basePath.slice(0, -1) : basePath;\n return `${basePath}${id}${trailingSlash === \"always\" ? \"/\" : \"\"}`;\n}\n\n/**\n * The ways the list and the collection disagree, as sentences: a page the\n * list leaves out, a file it names that isn't a page, and a page it names twice.\n * `source` names an entry for a person, by its source file where it can.\n */\nexport function navProblems(\n nav: readonly NavGroup[],\n entries: readonly NavEntry[],\n source: (entry: NavEntry) => string = (entry) => entry.id,\n): string[] {\n const listed = nav.flatMap((group) => group.pages);\n const ids = new Set(entries.map((entry) => entry.id));\n const listedIds = new Set(listed.map(entryId));\n const problems: string[] = [];\n for (const entry of entries) {\n if (!listedIds.has(entry.id))\n problems.push(`${source(entry)} isn't in the sidebar's list. Add it to a group.`);\n }\n for (const [i, file] of listed.entries()) {\n if (!ids.has(entryId(file)))\n problems.push(`the sidebar's list names ${file}, which isn't a page of the site.`);\n else if (listed.findIndex((other) => entryId(other) === entryId(file)) !== i)\n problems.push(`the sidebar's list names ${file} twice.`);\n }\n return problems;\n}\n\n/**\n * The sidebar's groups, each page with its entry and address. Throws, naming\n * each, when the list leaves out a page of the collection, names a file that\n * isn't one, or names a page twice, so the list stays complete.\n *\n * A versioned build's collection has every line's pages, and the list is\n * checked against them all: `id` gives an entry's id within its line, and\n * the groups have the pages `shown` keeps, one line's, each listed page that\n * line has.\n */\nexport function sidebarGroups<E extends NavEntry>(\n nav: readonly NavGroup[],\n entries: readonly E[],\n route: (id: string) => string,\n source?: (entry: NavEntry) => string,\n lines: { id: (entry: E) => string; shown: (entry: E) => boolean } = {\n id: (entry) => entry.id,\n shown: () => true,\n },\n): SidebarGroup<E>[] {\n // A page in several lines is one page of the list, and one problem.\n const checked = new Map<NavEntry, E>(\n entries.map((entry) => [{ id: lines.id(entry), filePath: entry.filePath }, entry]),\n );\n const problems = navProblems(\n nav,\n [...checked.keys()],\n source === undefined ? undefined : (entry) => source(checked.get(entry) ?? entry),\n ).filter((problem, i, all) => all.indexOf(problem) === i);\n if (problems.length > 0) {\n throw new Error(\n `@ascribed/astro: the sidebar's list (src/nav.ts) doesn't match the site's pages:\\n${problems.map((problem) => `- ${problem}`).join(\"\\n\")}`,\n );\n }\n const byId = new Map(\n entries.filter((entry) => lines.shown(entry)).map((entry) => [lines.id(entry), entry]),\n );\n return nav.map((group) => ({\n label: group.label,\n pages: group.pages.flatMap((file) => {\n const entry = byId.get(entryId(file));\n return entry === undefined ? [] : [{ entry, href: route(entry.id) }];\n }),\n }));\n}\n\n/**\n * The starter's list for a project's pages, by their paths in the site\n * output: the pages at the content root in a group named \"Start\", the root\n * page first, then a group for each folder, named as the folder is, in order\n * of their names.\n */\nexport function navFor(pages: readonly string[]): NavGroup[] {\n const groups = new Map<string, string[]>();\n const sorted = pages.toSorted((a, b) => {\n const rootA = a === \"index.md\";\n const rootB = b === \"index.md\";\n if (rootA !== rootB) return rootA ? -1 : 1;\n return a < b ? -1 : a > b ? 1 : 0;\n });\n for (const page of sorted) {\n const slash = page.indexOf(\"/\");\n const label = slash === -1 ? \"Start\" : page.slice(0, slash);\n groups.set(label, [...(groups.get(label) ?? []), page]);\n }\n const start = groups.get(\"Start\");\n groups.delete(\"Start\");\n return [\n ...(start === undefined ? [] : [{ label: \"Start\", pages: start }]),\n ...[...groups].map(([label, files]) => ({ label, pages: files })),\n ];\n}\n"]}
1
+ {"version":3,"file":"nav.js","sourceRoot":"","sources":["../src/nav.ts"],"names":[],"mappings":"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","sourcesContent":["// A site's sidebar: the project's navigation, `_ascribe/nav.json` in a\n// build's site output (the navigation contract), joined to the collection;\n// or a site's own list of pages, `{ nav }`, checked against the collection.\nimport { slug } from \"github-slugger\";\n\n/** A group of the sidebar: a label, and its pages' source files, in order. */\nexport interface NavGroup {\n label: string;\n /** Source files, relative to the content root, as `Guides/My Setup.md`. */\n pages: string[];\n}\n\n/** A label in the site output's formatted form: HTML, with code spans as `<code>`. */\nexport interface Formatted {\n label: string;\n}\n\n/** A page of the sidebar: its collection entry, its address, and its label. */\nexport interface SidebarPage<E> {\n entry: E;\n href: string;\n /** Its label as plain text: its `nav-label`, else its title. */\n label: string;\n /** Its label as HTML, when it's read with inline markup, as the title is. */\n formatted?: Formatted;\n}\n\n/**\n * A group of the sidebar: a section of the navigation, or a group of a\n * site's own list. `pages` starts with the section's own page when that's\n * its `index.md`; an own page beside the folder (`checks/diagnostics.md`\n * for `checks/diagnostics/`) is among the parent's pages instead, and the\n * nested group sits right after it.\n */\nexport interface SidebarGroup<E> {\n label: string;\n /** Its label as HTML, when it's its own page's title, read with inline markup. */\n formatted?: Formatted;\n /** The folder, from the content root, ending in `/`; absent for a group of a site's own list. */\n folder?: string;\n /** Its own page, when the build offers one. */\n page?: SidebarPage<E>;\n pages: SidebarPage<E>[];\n /** Its nested sections, in order; empty for a group of a site's own list. */\n sections: SidebarGroup<E>[];\n}\n\n/** A page in `_ascribe/nav.json`. */\nexport interface NavPage {\n /** Its path from the content root, as its record's `source`. */\n source: string;\n label: string;\n formatted?: Formatted;\n /** Its content type. */\n type?: string;\n}\n\n/** A section in `_ascribe/nav.json`. */\nexport interface NavSection {\n /** From the content root, ending in `/`; `\"\"` for the root. */\n folder: string;\n label: string;\n formatted?: Formatted;\n page?: NavPage;\n pages: NavPage[];\n sections: NavSection[];\n}\n\n/** `_ascribe/nav.json`. */\nexport interface Navigation {\n version: number;\n root: NavSection;\n}\n\n/** What the check needs of a collection entry: its id, and the file it was loaded from. */\nexport interface NavEntry {\n id: string;\n filePath?: string | undefined;\n}\n\n/**\n * A page's entry id, as Astro's `glob` loader computes it from the page's\n * path in the site output: each segment slugged, without the extension, and\n * without a final `/index`.\n */\nexport function entryId(pagePath: string): string {\n return pagePath\n .replace(/\\.md$/, \"\")\n .split(\"/\")\n .map((segment) => slug(segment))\n .join(\"/\")\n .replace(/\\/index$/, \"\");\n}\n\n/**\n * A page's route, as Ascribe's router writes every link to it: the base path\n * plus its entry id, and the root page (`index`) at the base path itself.\n */\nexport function pageRoute(id: string, basePath: string, trailingSlash: \"always\" | \"never\"): string {\n if (id === \"index\")\n return trailingSlash === \"never\" && basePath !== \"/\" ? basePath.slice(0, -1) : basePath;\n return `${basePath}${id}${trailingSlash === \"always\" ? \"/\" : \"\"}`;\n}\n\n/**\n * The sidebar's groups from the project's navigation: the root's own page\n * and pages under its label, \"Start\" unless `ascribe.nav` gives another,\n * when it has any, then each top section, each page with its entry and\n * address. `source` gives an entry's page's source path, which the\n * navigation names it by. Throws, naming each, when the navigation names a\n * page the collection has no entry for.\n */\nexport function navigationGroups<E extends NavEntry>(\n navigation: Navigation,\n entries: readonly E[],\n route: (entry: E) => string,\n source: (entry: E) => string,\n): SidebarGroup<E>[] {\n const bySource = new Map(entries.map((entry) => [source(entry), entry]));\n const missing: string[] = [];\n const page = (nav: NavPage): SidebarPage<E>[] => {\n const entry = bySource.get(nav.source);\n if (entry === undefined) {\n missing.push(nav.source);\n return [];\n }\n return [\n {\n entry,\n href: route(entry),\n label: nav.label,\n ...(nav.formatted === undefined ? {} : { formatted: nav.formatted }),\n },\n ];\n };\n const group = (section: NavSection, parent: NavSection | undefined): SidebarGroup<E> => {\n const own = section.page === undefined ? [] : page(section.page);\n // An own page beside the folder is one of the parent's pages.\n const beside =\n section.page !== undefined &&\n parent?.pages.some((other) => other.source === section.page?.source) === true;\n return {\n label: section.label,\n ...(section.formatted === undefined ? {} : { formatted: section.formatted }),\n folder: section.folder,\n ...(own[0] === undefined ? {} : { page: own[0] }),\n pages: [...(beside ? [] : own), ...section.pages.flatMap(page)],\n sections: section.sections.map((nested) => group(nested, section)),\n };\n };\n const { root } = navigation;\n // A top section's own page beside its folder is in that section's group.\n const top = {\n ...group({ ...root, pages: root.pages.filter((nav) => !beside(root, nav)) }, undefined),\n sections: [],\n };\n const groups = [\n ...(top.pages.length === 0 ? [] : [top]),\n ...root.sections.map((section) => group(section, undefined)),\n ];\n if (missing.length > 0) {\n throw new Error(\n `@ascribed/astro: the project's navigation (_ascribe/nav.json) names pages the collection has no entry for:\\n${[...new Set(missing)].map((file) => `- ${file}`).join(\"\\n\")}\\nBuild the project again (\\`ascribe build\\`), so the navigation and the pages are from one build.`,\n );\n }\n return groups;\n}\n\n/**\n * The navigation's pages in reading order, depth first: a section's own\n * page, then its pages, each nested section whose own page is beside its\n * folder right after that page, with its pages and nested sections, and the\n * other nested sections after them, each from its own page. The content\n * root reads as the sidebar groups it: its own page and its pages that\n * aren't beside a folder, then each top section from its own page. Previous\n * and next follow it, as an archive and `llms.txt` do.\n */\nexport function readingOrder(navigation: Navigation): NavPage[] {\n const out: NavPage[] = [];\n const read = (section: NavSection, own: boolean) => {\n if (own && section.page !== undefined) out.push(section.page);\n const done = section.sections.map(() => false);\n for (const page of section.pages) {\n out.push(page);\n section.sections.forEach((nested, at) => {\n if (!done[at] && nested.page?.source === page.source) {\n done[at] = true;\n read(nested, false);\n }\n });\n }\n section.sections.forEach((nested, at) => {\n if (!done[at]) read(nested, true);\n });\n };\n const { root } = navigation;\n if (root.page !== undefined) out.push(root.page);\n out.push(...root.pages.filter((nav) => !beside(root, nav)));\n for (const section of root.sections) read(section, true);\n return out;\n}\n\n/** Whether `page` is the own page of one of `section`'s nested sections. */\nfunction beside(section: NavSection, page: NavPage): boolean {\n return section.sections.some((nested) => nested.page?.source === page.source);\n}\n\n/** A page's previous and next, each with its entry, its address, and its label. */\nexport interface Neighbors<E> {\n previous?: SidebarPage<E>;\n next?: SidebarPage<E>;\n}\n\n/**\n * The pages before and after `at` in `pages`, the order a reader goes in.\n * Neither, for a page that isn't in it.\n */\nexport function neighborsIn<E>(\n pages: readonly SidebarPage<E>[],\n at: (page: SidebarPage<E>) => boolean,\n): Neighbors<E> {\n const i = pages.findIndex(at);\n if (i === -1) return {};\n const previous = pages[i - 1];\n const next = pages[i + 1];\n return {\n ...(previous === undefined ? {} : { previous }),\n ...(next === undefined ? {} : { next }),\n };\n}\n\n/**\n * The navigation's pages in reading order, each with its entry, its\n * address, and its label. `source` gives an entry's page's source path.\n * A page the collection has no entry for is passed over.\n */\nexport function readingPages<E extends NavEntry>(\n navigation: Navigation,\n entries: readonly E[],\n route: (entry: E) => string,\n source: (entry: E) => string,\n): SidebarPage<E>[] {\n const bySource = new Map(entries.map((entry) => [source(entry), entry]));\n return readingOrder(navigation).flatMap((nav) => {\n const entry = bySource.get(nav.source);\n if (entry === undefined) return [];\n return [\n {\n entry,\n href: route(entry),\n label: nav.label,\n ...(nav.formatted === undefined ? {} : { formatted: nav.formatted }),\n },\n ];\n });\n}\n\n/**\n * The ways the list and the collection disagree, as sentences: a page the\n * list leaves out, a file it names that isn't a page, and a page it names twice.\n * `source` names an entry for a person, by its source file where it can.\n */\nexport function navProblems(\n nav: readonly NavGroup[],\n entries: readonly NavEntry[],\n source: (entry: NavEntry) => string = (entry) => entry.id,\n): string[] {\n const listed = nav.flatMap((group) => group.pages);\n const ids = new Set(entries.map((entry) => entry.id));\n const listedIds = new Set(listed.map(entryId));\n const problems: string[] = [];\n for (const entry of entries) {\n if (!listedIds.has(entry.id))\n problems.push(`${source(entry)} isn't in the sidebar's list. Add it to a group.`);\n }\n for (const [i, file] of listed.entries()) {\n if (!ids.has(entryId(file)))\n problems.push(`the sidebar's list names ${file}, which isn't a page of the site.`);\n else if (listed.findIndex((other) => entryId(other) === entryId(file)) !== i)\n problems.push(`the sidebar's list names ${file} twice.`);\n }\n return problems;\n}\n\n/**\n * The sidebar's groups from a site's own list, each page with its entry, its\n * address, and its label, which `label` gives, or else its entry id. Throws,\n * naming each, when the list leaves out a page of the collection, names a\n * file that isn't one, or names a page twice, so the list stays complete.\n *\n * A versioned build's collection has every line's pages, and the list is\n * checked against them all: `id` gives an entry's id within its line, and\n * the groups have the pages `shown` keeps, one line's, each listed page that\n * line has.\n */\nexport function sidebarGroups<E extends NavEntry>(\n nav: readonly NavGroup[],\n entries: readonly E[],\n route: (id: string) => string,\n source?: (entry: NavEntry) => string,\n lines: { id: (entry: E) => string; shown: (entry: E) => boolean } = {\n id: (entry) => entry.id,\n shown: () => true,\n },\n label?: (entry: E) => string | undefined,\n): SidebarGroup<E>[] {\n // A page in several lines is one page of the list, and one problem.\n const checked = new Map<NavEntry, E>(\n entries.map((entry) => [{ id: lines.id(entry), filePath: entry.filePath }, entry]),\n );\n const problems = navProblems(\n nav,\n [...checked.keys()],\n source === undefined ? undefined : (entry) => source(checked.get(entry) ?? entry),\n ).filter((problem, i, all) => all.indexOf(problem) === i);\n if (problems.length > 0) {\n throw new Error(\n `@ascribed/astro: the sidebar's list (src/nav.ts) doesn't match the site's pages:\\n${problems.map((problem) => `- ${problem}`).join(\"\\n\")}`,\n );\n }\n const byId = new Map(\n entries.filter((entry) => lines.shown(entry)).map((entry) => [lines.id(entry), entry]),\n );\n return nav.map((group) => ({\n label: group.label,\n pages: group.pages.flatMap((file) => {\n const entry = byId.get(entryId(file));\n return entry === undefined\n ? []\n : [{ entry, href: route(entry.id), label: label?.(entry) ?? entry.id }];\n }),\n sections: [],\n }));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascribed/astro",
3
- "version": "0.3.3-next.24",
3
+ "version": "0.4.0",
4
4
  "description": "The Astro integration for Ascribe.",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -58,9 +58,9 @@
58
58
  "node": ">=24"
59
59
  },
60
60
  "dependencies": {
61
- "@ascribed/cli": "0.3.3-next.24",
62
- "@ascribed/elements": "0.3.3-next.24",
63
- "@ascribed/review": "0.3.3-next.24",
61
+ "@ascribed/cli": "0.4.0",
62
+ "@ascribed/elements": "0.4.0",
63
+ "@ascribed/review": "0.4.0",
64
64
  "github-slugger": "^2.0.0",
65
65
  "property-information": "^7.2.0",
66
66
  "smol-toml": "^1.9.1"
@@ -96,7 +96,6 @@
96
96
  "publishConfig": {
97
97
  "access": "public"
98
98
  },
99
- "gitHead": "bd4c22cf71d61a205925fb81304dbec66bb400b4",
100
99
  "scripts": {
101
100
  "build": "tsc -p tsconfig.build.json",
102
101
  "typecheck": "tsc --noEmit",
@@ -9,8 +9,13 @@ import Head from "@ascribed/astro/Head.astro";
9
9
  // what it is and where the current one is.
10
10
  import Versions from "@ascribed/astro/Versions.astro";
11
11
  import VersionBanner from "@ascribed/astro/VersionBanner.astro";
12
- import { sidebar } from "@ascribed/astro/content";
13
- import { nav } from "../nav.ts";
12
+ import {
13
+ neighbors,
14
+ sidebar,
15
+ type Formatted,
16
+ type SidebarGroup,
17
+ type SidebarPage,
18
+ } from "@ascribed/astro/content";
14
19
 
15
20
  interface Props {
16
21
  entry: CollectionEntry<"docs">;
@@ -31,15 +36,50 @@ interface Page {
31
36
 
32
37
  const { entry, headings } = Astro.props;
33
38
  const page = (of: CollectionEntry<"docs">) => of.data as Page;
39
+ const escape = (text: string) =>
40
+ text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
34
41
  /** A page's title as HTML: its formatted form, or its plain text escaped. */
35
42
  const titleHtml = (of: CollectionEntry<"docs">) =>
36
- page(of).formatted?.title ??
37
- page(of).title.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
43
+ page(of).formatted?.title ?? escape(page(of).title);
44
+ /** A sidebar label as HTML: its formatted form, or its plain text escaped. */
45
+ const labelHtml = (of: { label: string; formatted?: Formatted }) =>
46
+ of.formatted?.label ?? escape(of.label);
38
47
  const { available } = page(entry);
39
- // The sidebar: src/nav.ts's groups, each page with its address. The build
40
- // fails when the list leaves out a page, or names one that doesn't exist.
41
- // With a versioned build, it shows the pages of the line this page is in.
42
- const groups = sidebar(nav, await getCollection("docs"), { entry });
48
+ // The sidebar: the project's navigation, in ascribe.nav's order or the
49
+ // default one, each page with its address and label, and a nested section's
50
+ // pages right after its own page, each with its depth. With a versioned build,
51
+ // it shows the pages of the line this page is in. Previous and next are the
52
+ // pages before and after this one in the navigation's reading order.
53
+ const entries = await getCollection("docs");
54
+ const groups = sidebar(entries, { entry });
55
+ const { previous, next } = neighbors(entry, entries);
56
+
57
+ /** A sidebar page, with how many sections deep it is: 0 in a top section. */
58
+ interface Item {
59
+ page: SidebarPage<CollectionEntry<"docs">>;
60
+ depth: number;
61
+ }
62
+
63
+ /**
64
+ * A group's pages in order, each nested section's right after its own page,
65
+ * or after the group's pages when its own page is its `index.md`.
66
+ */
67
+ function items(group: SidebarGroup<CollectionEntry<"docs">>, depth = 0): Item[] {
68
+ const placed = new Set<SidebarGroup<CollectionEntry<"docs">>>();
69
+ const out: Item[] = [];
70
+ for (const page of group.pages) {
71
+ out.push({ page, depth });
72
+ for (const section of group.sections) {
73
+ if (section.page?.entry !== page.entry) continue;
74
+ placed.add(section);
75
+ out.push(...items(section, depth + 1));
76
+ }
77
+ }
78
+ for (const section of group.sections) {
79
+ if (!placed.has(section)) out.push(...items(section, depth + 1));
80
+ }
81
+ return out;
82
+ }
43
83
  ---
44
84
 
45
85
  <!doctype html>
@@ -55,15 +95,15 @@ const groups = sidebar(nav, await getCollection("docs"), { entry });
55
95
  {
56
96
  groups.map((group) => (
57
97
  <section>
58
- <h2>{group.label}</h2>
98
+ <h2 set:html={labelHtml(group)} />
59
99
  <ul>
60
- {group.pages.map((item) => (
61
- <li>
100
+ {items(group).map(({ page: item, depth }) => (
101
+ <li data-depth={depth}>
62
102
  <a
63
103
  href={item.href}
64
104
  aria-current={item.entry.id === entry.id ? "page" : undefined}
65
105
  >
66
- <Fragment set:html={titleHtml(item.entry)} />
106
+ <Fragment set:html={labelHtml(item)} />
67
107
  </a>
68
108
  </li>
69
109
  ))}
@@ -89,6 +129,22 @@ const groups = sidebar(nav, await getCollection("docs"), { entry });
89
129
  </ul>
90
130
  </nav>
91
131
  <article><slot /></article>
132
+ <nav aria-label="Previous and next">
133
+ {
134
+ previous && (
135
+ <a rel="prev" href={previous.href}>
136
+ Previous: <Fragment set:html={labelHtml(previous)} />
137
+ </a>
138
+ )
139
+ }
140
+ {
141
+ next && (
142
+ <a rel="next" href={next.href}>
143
+ Next: <Fragment set:html={labelHtml(next)} />
144
+ </a>
145
+ )
146
+ }
147
+ </nav>
92
148
  </main>
93
149
  </body>
94
150
  </html>
@@ -1,13 +0,0 @@
1
- // The sidebar's list: its groups, in order, each with its pages' source files,
2
- // relative to the content root. `npx @ascribed/astro init` wrote it from the
3
- // pages there were then; it's yours to reorder, relabel, and add to. Ascribe's
4
- // content model has no page order or grouping yet, so the site keeps the list,
5
- // and the layout's `sidebar()` keeps it complete: the build fails on a page the
6
- // list leaves out, and on a file it names that isn't a page.
7
- import type { NavGroup } from "@ascribed/astro/content";
8
-
9
- export const nav: NavGroup[] = [
10
- { label: "Start", pages: ["index.md"] },
11
- { label: "Guides", pages: ["Guides/My Setup.md"] },
12
- { label: "Reference", pages: ["Reference/Options.md"] },
13
- ];