@brandon_m_behring/book-scaffold-astro 3.5.1 → 3.5.3
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/index.d.ts +25 -1
- package/dist/index.mjs +17 -0
- package/package.json +1 -1
- package/pages/chapters.astro +100 -54
- package/scripts/validate.mjs +46 -2
- package/src/lib/chapter-sort.ts +55 -0
- package/src/lib/chapters.ts +12 -5
package/dist/index.d.ts
CHANGED
|
@@ -79,4 +79,28 @@ declare function getFreshness(lastVerified: Date | undefined, volatility: Volati
|
|
|
79
79
|
* affordance without a separate branch. */
|
|
80
80
|
declare function freshnessLabel(f: Freshness | null): string;
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* src/lib/chapter-sort.ts — pure sort-key helper for chapter frontmatter.
|
|
84
|
+
*
|
|
85
|
+
* Lives in its own file (no `astro:content` imports) so tsup can include it
|
|
86
|
+
* in the toolkit's DTS bundle without trying to resolve Astro's virtual
|
|
87
|
+
* modules at build time. The Astro-context wrappers (sortKey, getAllChapters,
|
|
88
|
+
* getNeighbors) live in src/lib/chapters.ts and import from here.
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* Pure-function sort key from a chapter frontmatter object. Spans both
|
|
92
|
+
* tools and academic schemas:
|
|
93
|
+
*
|
|
94
|
+
* - Tools: numeric `part` (0-10), numeric `chapter` (0-99). Encodes as
|
|
95
|
+
* `part * 1000 + chapter` so chapters within a part stay grouped.
|
|
96
|
+
* - Academic: string-enum `part` (foundations|ssm-core|...), numeric `week`
|
|
97
|
+
* (1-99), no `chapter`. Maps `part` to a fixed ordinal then encodes as
|
|
98
|
+
* `partOrdinal * 1000 + week`.
|
|
99
|
+
*
|
|
100
|
+
* v3.5.2 (closes #24): previously the tools-only formula crashed on
|
|
101
|
+
* academic chapters (string `part`, no `chapter`) by producing NaN sort
|
|
102
|
+
* keys.
|
|
103
|
+
*/
|
|
104
|
+
declare function chapterSortKey(data: Record<string, unknown>): number;
|
|
105
|
+
|
|
106
|
+
export { BookConfigOptions, BookScaffoldIntegrationOptions, type Freshness, type FreshnessStatus, type VolatilityLevel, bookScaffoldIntegration, chapterSortKey, defineBookConfig, defineMdxComponents, freshnessLabel, getFreshness, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -697,6 +697,22 @@ function freshnessLabel(f) {
|
|
|
697
697
|
return `Stale (${f.daysOld}d old; ${Math.abs(f.daysUntil)}d past threshold)`;
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
|
+
|
|
701
|
+
// src/lib/chapter-sort.ts
|
|
702
|
+
var ACADEMIC_PART_ORDINAL = {
|
|
703
|
+
foundations: 1,
|
|
704
|
+
"ssm-core": 2,
|
|
705
|
+
"beyond-ssm": 3,
|
|
706
|
+
integration: 4,
|
|
707
|
+
synthesis: 5
|
|
708
|
+
};
|
|
709
|
+
var UNKNOWN_PART_ORDINAL = 99;
|
|
710
|
+
function chapterSortKey(data) {
|
|
711
|
+
const partRaw = data.part;
|
|
712
|
+
const partOrdinal = typeof partRaw === "number" ? partRaw : typeof partRaw === "string" ? ACADEMIC_PART_ORDINAL[partRaw] ?? UNKNOWN_PART_ORDINAL : UNKNOWN_PART_ORDINAL;
|
|
713
|
+
const within = typeof data.chapter === "number" ? data.chapter : typeof data.week === "number" ? data.week : 0;
|
|
714
|
+
return partOrdinal * 1e3 + within;
|
|
715
|
+
}
|
|
700
716
|
export {
|
|
701
717
|
BOOK_PRESETS,
|
|
702
718
|
BOOK_PROFILES,
|
|
@@ -706,6 +722,7 @@ export {
|
|
|
706
722
|
bookScaffoldIntegration,
|
|
707
723
|
changeKinds,
|
|
708
724
|
changelogSchema,
|
|
725
|
+
chapterSortKey,
|
|
709
726
|
chapterStatus,
|
|
710
727
|
courseNotesChapterSchema,
|
|
711
728
|
defineBookConfig,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "3.5.
|
|
4
|
+
"version": "3.5.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
package/pages/chapters.astro
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* /chapters — book index page.
|
|
4
4
|
*
|
|
5
|
-
* Groups non-draft chapters by Part in ascending
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Groups non-draft chapters by Part in ascending order. Each card exposes
|
|
6
|
+
* `data-tools="<slug> <slug>..."` so the ToolFilter island can hide cards via
|
|
7
|
+
* a single attribute selector without touching card rendering.
|
|
8
|
+
*
|
|
9
|
+
* v3.5.2 (closes #24): schema-aware. Previously hardcoded the tools-profile
|
|
10
|
+
* shape and crashed on academic profile (no `chapter` / `volatility` /
|
|
11
|
+
* `tools_compared` / `last_verified`). Now renders either shape:
|
|
12
|
+
* - tools: `Chapter N` + volatility badge + freshness + tools-compared tags
|
|
13
|
+
* - academic: `Week N` + status badge (no freshness or tools-compared)
|
|
14
|
+
* Field presence is the schema discriminator (cheaper than reading
|
|
15
|
+
* BOOK_PROFILE at the route layer).
|
|
9
16
|
*/
|
|
10
17
|
import Base from '../layouts/Base.astro';
|
|
11
18
|
import { getAllChapters, type Chapter } from '../src/lib/chapters';
|
|
@@ -13,94 +20,133 @@ import { getFreshness, freshnessLabel } from '../src/lib/freshness';
|
|
|
13
20
|
|
|
14
21
|
const chapters = await getAllChapters();
|
|
15
22
|
|
|
16
|
-
// Stable insertion-order grouping
|
|
17
|
-
|
|
23
|
+
// Stable insertion-order grouping by `part`. Map key may be number (tools) or
|
|
24
|
+
// string (academic enum); Map preserves insertion order for both.
|
|
25
|
+
type PartKey = string | number;
|
|
26
|
+
const byPart = new Map<PartKey, Chapter[]>();
|
|
18
27
|
for (const c of chapters) {
|
|
19
|
-
const
|
|
28
|
+
const key = (c.data as { part: PartKey }).part;
|
|
29
|
+
const list = byPart.get(key);
|
|
20
30
|
if (list) list.push(c);
|
|
21
|
-
else byPart.set(
|
|
31
|
+
else byPart.set(key, [c]);
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
function formatDate(d: Date): string {
|
|
25
35
|
return d.toISOString().slice(0, 10);
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
/** Render-ready label for a Part heading. Tools: "Part N" or "Appendices"
|
|
39
|
+
* (parts >= 6 are appendices). Academic: titlecase the enum string. */
|
|
40
|
+
function partLabel(part: PartKey): string {
|
|
41
|
+
if (typeof part === 'number') {
|
|
42
|
+
return part >= 6 ? 'Appendices' : `Part ${part}`;
|
|
43
|
+
}
|
|
44
|
+
return part
|
|
45
|
+
.split('-')
|
|
46
|
+
.map((w) => (w.length ? w[0].toUpperCase() + w.slice(1) : ''))
|
|
47
|
+
.join(' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isAppendix(part: PartKey): boolean {
|
|
51
|
+
return typeof part === 'number' && part >= 6;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Per-card schema detection. Cheaper than reading BOOK_PROFILE — the shape
|
|
55
|
+
* of the chapter's data already tells us which profile produced it. */
|
|
56
|
+
function isToolsShape(data: Record<string, unknown>): boolean {
|
|
57
|
+
return 'volatility' in data && 'chapter' in data;
|
|
30
58
|
}
|
|
31
59
|
---
|
|
32
60
|
<Base
|
|
33
|
-
title="Chapters
|
|
34
|
-
description="All chapters grouped by Part
|
|
61
|
+
title="Chapters"
|
|
62
|
+
description="All chapters grouped by Part."
|
|
35
63
|
>
|
|
36
64
|
<article class="prose chapters-index">
|
|
37
65
|
<header class="chapters-index-header">
|
|
38
66
|
<h1>Chapters</h1>
|
|
39
67
|
<p class="chapters-index-lede">
|
|
40
|
-
Every chapter, grouped by Part.
|
|
41
|
-
|
|
42
|
-
card's metadata to calibrate how much trust to place in its
|
|
43
|
-
specific claims — stable principles age slowly; feature surfaces
|
|
44
|
-
age fast.
|
|
45
|
-
</p>
|
|
46
|
-
<p class="chapters-index-cross-ref">
|
|
47
|
-
See also: <a href="/convergence/">convergence dashboard</a> —
|
|
48
|
-
which patterns have landed in which tools, when.
|
|
68
|
+
Every chapter, grouped by Part. Use the card metadata to calibrate
|
|
69
|
+
how much trust to place in a chapter's specific claims.
|
|
49
70
|
</p>
|
|
50
71
|
</header>
|
|
51
72
|
|
|
52
73
|
<p class="chapters-filter-hint" id="filter-hint" aria-live="polite"></p>
|
|
53
74
|
|
|
54
75
|
{Array.from(byPart.entries()).map(([part, list]) => {
|
|
55
|
-
const appendix = part
|
|
76
|
+
const appendix = isAppendix(part);
|
|
56
77
|
return (
|
|
57
78
|
<section class="part-group">
|
|
58
79
|
<h2 class="part-heading">
|
|
59
|
-
<span class="part-label">{partLabel(part
|
|
80
|
+
<span class="part-label">{partLabel(part)}</span>
|
|
60
81
|
</h2>
|
|
61
82
|
<ol class="chapter-list">
|
|
62
83
|
{list.map((c) => {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
84
|
+
const data = c.data as Record<string, unknown>;
|
|
85
|
+
const tools = isToolsShape(data);
|
|
86
|
+
// data-tools attribute drives the ToolFilter island. Academic
|
|
87
|
+
// cards opt out by claiming "cross-tool" — always visible
|
|
88
|
+
// regardless of filter state.
|
|
89
|
+
const toolsAttr = tools
|
|
90
|
+
? (data.tools_compared as string[]).join(' ')
|
|
91
|
+
: 'cross-tool';
|
|
92
|
+
const freshness = tools
|
|
93
|
+
? getFreshness(data.last_verified as Date, data.volatility as Parameters<typeof getFreshness>[1])
|
|
94
|
+
: null;
|
|
95
|
+
const freshnessText = freshness
|
|
96
|
+
? freshness.status === 'fresh'
|
|
66
97
|
? 'Fresh'
|
|
67
98
|
: freshness.status === 'verify-soon'
|
|
68
99
|
? 'Verify soon'
|
|
69
|
-
: 'Stale'
|
|
70
|
-
|
|
100
|
+
: 'Stale'
|
|
101
|
+
: null;
|
|
71
102
|
return (
|
|
72
|
-
<li
|
|
73
|
-
class="chapter-card"
|
|
74
|
-
data-tools={toolsAttr}
|
|
75
|
-
>
|
|
103
|
+
<li class="chapter-card" data-tools={toolsAttr}>
|
|
76
104
|
<a href={`/${c.id}/`} class="chapter-card-link">
|
|
77
105
|
<div class="chapter-card-meta">
|
|
78
106
|
<span class="chapter-card-number">
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
>{c.data.volatility}</span>
|
|
85
|
-
<span
|
|
86
|
-
class="freshness-badge"
|
|
87
|
-
data-status={freshness.status}
|
|
88
|
-
aria-label={freshnessLabel(freshness)}
|
|
89
|
-
title={freshnessLabel(freshness)}
|
|
90
|
-
>{freshnessText}</span>
|
|
91
|
-
<span class="chapter-card-verified">
|
|
92
|
-
verified {formatDate(c.data.last_verified)}
|
|
107
|
+
{tools
|
|
108
|
+
? appendix
|
|
109
|
+
? `Appendix ${String.fromCharCode(64 + (data.chapter as number)).toLowerCase()}`
|
|
110
|
+
: `Chapter ${data.chapter}`
|
|
111
|
+
: `Week ${data.week}`}
|
|
93
112
|
</span>
|
|
113
|
+
{tools && (
|
|
114
|
+
<span
|
|
115
|
+
class={`volatility-badge volatility-${data.volatility}`}
|
|
116
|
+
title={`Volatility: ${data.volatility}`}
|
|
117
|
+
>{data.volatility}</span>
|
|
118
|
+
)}
|
|
119
|
+
{!tools && data.status && (
|
|
120
|
+
<span
|
|
121
|
+
class={`status-badge status-${data.status}`}
|
|
122
|
+
title={`Status: ${data.status}`}
|
|
123
|
+
>{data.status}</span>
|
|
124
|
+
)}
|
|
125
|
+
{freshness && freshnessText && (
|
|
126
|
+
<span
|
|
127
|
+
class="freshness-badge"
|
|
128
|
+
data-status={freshness.status}
|
|
129
|
+
aria-label={freshnessLabel(freshness)}
|
|
130
|
+
title={freshnessLabel(freshness)}
|
|
131
|
+
>{freshnessText}</span>
|
|
132
|
+
)}
|
|
133
|
+
{tools && data.last_verified && (
|
|
134
|
+
<span class="chapter-card-verified">
|
|
135
|
+
verified {formatDate(data.last_verified as Date)}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
94
138
|
</div>
|
|
95
|
-
<h3 class="chapter-card-title">{
|
|
96
|
-
{
|
|
97
|
-
<p class="chapter-card-description">{
|
|
139
|
+
<h3 class="chapter-card-title">{data.title}</h3>
|
|
140
|
+
{data.description && (
|
|
141
|
+
<p class="chapter-card-description">{data.description as string}</p>
|
|
142
|
+
)}
|
|
143
|
+
{tools && (
|
|
144
|
+
<div class="chapter-card-tools">
|
|
145
|
+
{(data.tools_compared as string[]).map((t) => (
|
|
146
|
+
<span class="tool-badge">{t}</span>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
98
149
|
)}
|
|
99
|
-
<div class="chapter-card-tools">
|
|
100
|
-
{c.data.tools_compared.map((t) => (
|
|
101
|
-
<span class="tool-badge">{t}</span>
|
|
102
|
-
))}
|
|
103
|
-
</div>
|
|
104
150
|
</a>
|
|
105
151
|
</li>
|
|
106
152
|
);
|
package/scripts/validate.mjs
CHANGED
|
@@ -27,8 +27,40 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { readFile, access } from 'node:fs/promises';
|
|
29
29
|
import { glob } from 'node:fs/promises';
|
|
30
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
30
31
|
import { resolve, dirname, join } from 'node:path';
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Best-effort .env reader. Mirrors `readEnvFile` in src/types.ts; kept inline
|
|
35
|
+
* here because scripts/ is shipped as plain JS without compiling src/.
|
|
36
|
+
*
|
|
37
|
+
* Closes #20 — validate.mjs previously skipped the .env fallback that
|
|
38
|
+
* `resolveProfileWithSource` honors, so consumers who set BOOK_PROFILE in
|
|
39
|
+
* .env (per the SKILL.md and scaffold's create-book defaults) saw the CLI
|
|
40
|
+
* silently default to minimal, masking academic-profile errors.
|
|
41
|
+
*/
|
|
42
|
+
function readEnvFile(path = '.env') {
|
|
43
|
+
try {
|
|
44
|
+
if (!existsSync(path)) return {};
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) {
|
|
47
|
+
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
48
|
+
if (!m) continue;
|
|
49
|
+
let val = m[2] ?? '';
|
|
50
|
+
if (
|
|
51
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
52
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
53
|
+
) {
|
|
54
|
+
val = val.slice(1, -1);
|
|
55
|
+
}
|
|
56
|
+
out[m[1]] = val;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
} catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
// --help / -h: non-mutating (closes #14).
|
|
33
65
|
const USAGE = `Usage: book-scaffold validate [--preset <name>]
|
|
34
66
|
|
|
@@ -67,8 +99,20 @@ const CHAPTERS_DIR = resolve(ROOT, 'src/content/chapters');
|
|
|
67
99
|
const PUBLIC_DIR = resolve(ROOT, 'public');
|
|
68
100
|
const DATA_DIR = resolve(ROOT, 'src/data');
|
|
69
101
|
|
|
70
|
-
// Preset resolution
|
|
71
|
-
|
|
102
|
+
// Preset resolution (matches resolvePreset in src/types.ts):
|
|
103
|
+
// --preset flag > BOOK_PRESET env > BOOK_PROFILE env >
|
|
104
|
+
// .env BOOK_PRESET > .env BOOK_PROFILE > 'minimal'.
|
|
105
|
+
// .env fallback closes #20 — without it, consumers who set BOOK_PROFILE in
|
|
106
|
+
// .env (the documented convenience in SKILL.md + create-book defaults) saw
|
|
107
|
+
// the CLI silently default to minimal, hiding academic-profile errors.
|
|
108
|
+
const dotenv = readEnvFile(resolve(ROOT, '.env'));
|
|
109
|
+
const PRESET =
|
|
110
|
+
presetFromFlag ??
|
|
111
|
+
process.env.BOOK_PRESET ??
|
|
112
|
+
process.env.BOOK_PROFILE ??
|
|
113
|
+
dotenv.BOOK_PRESET ??
|
|
114
|
+
dotenv.BOOK_PROFILE ??
|
|
115
|
+
'minimal';
|
|
72
116
|
// Alias kept for downstream message text only; the resolution above is canonical.
|
|
73
117
|
const PROFILE = PRESET;
|
|
74
118
|
const REPO_ROOT = process.env.BOOK_REPO_ROOT ?? null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/chapter-sort.ts — pure sort-key helper for chapter frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own file (no `astro:content` imports) so tsup can include it
|
|
5
|
+
* in the toolkit's DTS bundle without trying to resolve Astro's virtual
|
|
6
|
+
* modules at build time. The Astro-context wrappers (sortKey, getAllChapters,
|
|
7
|
+
* getNeighbors) live in src/lib/chapters.ts and import from here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ordinal positions for the academic-profile `part` enum (src/schemas.ts:
|
|
12
|
+
* academicParts). Order here is load-bearing: it determines the on-page
|
|
13
|
+
* grouping order of academic books. Anything outside the enum (e.g., a
|
|
14
|
+
* consumer-extended part name) sorts to the end.
|
|
15
|
+
*/
|
|
16
|
+
const ACADEMIC_PART_ORDINAL: Record<string, number> = {
|
|
17
|
+
foundations: 1,
|
|
18
|
+
'ssm-core': 2,
|
|
19
|
+
'beyond-ssm': 3,
|
|
20
|
+
integration: 4,
|
|
21
|
+
synthesis: 5,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const UNKNOWN_PART_ORDINAL = 99;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pure-function sort key from a chapter frontmatter object. Spans both
|
|
28
|
+
* tools and academic schemas:
|
|
29
|
+
*
|
|
30
|
+
* - Tools: numeric `part` (0-10), numeric `chapter` (0-99). Encodes as
|
|
31
|
+
* `part * 1000 + chapter` so chapters within a part stay grouped.
|
|
32
|
+
* - Academic: string-enum `part` (foundations|ssm-core|...), numeric `week`
|
|
33
|
+
* (1-99), no `chapter`. Maps `part` to a fixed ordinal then encodes as
|
|
34
|
+
* `partOrdinal * 1000 + week`.
|
|
35
|
+
*
|
|
36
|
+
* v3.5.2 (closes #24): previously the tools-only formula crashed on
|
|
37
|
+
* academic chapters (string `part`, no `chapter`) by producing NaN sort
|
|
38
|
+
* keys.
|
|
39
|
+
*/
|
|
40
|
+
export function chapterSortKey(data: Record<string, unknown>): number {
|
|
41
|
+
const partRaw = data.part;
|
|
42
|
+
const partOrdinal =
|
|
43
|
+
typeof partRaw === 'number'
|
|
44
|
+
? partRaw
|
|
45
|
+
: typeof partRaw === 'string'
|
|
46
|
+
? (ACADEMIC_PART_ORDINAL[partRaw] ?? UNKNOWN_PART_ORDINAL)
|
|
47
|
+
: UNKNOWN_PART_ORDINAL;
|
|
48
|
+
const within =
|
|
49
|
+
typeof data.chapter === 'number'
|
|
50
|
+
? data.chapter
|
|
51
|
+
: typeof data.week === 'number'
|
|
52
|
+
? data.week
|
|
53
|
+
: 0;
|
|
54
|
+
return partOrdinal * 1000 + within;
|
|
55
|
+
}
|
package/src/lib/chapters.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/lib/chapters.ts — ordering + nav helpers for the chapters collection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Astro-context wrappers (need `astro:content`). The pure sort-key logic
|
|
5
|
+
* lives in src/lib/chapter-sort.ts so it can be included in the toolkit's
|
|
6
|
+
* DTS bundle without dragging Astro virtual modules into the build graph.
|
|
7
|
+
*
|
|
8
|
+
* v3.5.2 (closes #24): schema-aware sort. Previously assumed tools-profile
|
|
9
|
+
* shape (numeric `part` * 1000 + numeric `chapter`); academic chapters
|
|
10
|
+
* (string `part` enum + numeric `week`, no `chapter`) crashed.
|
|
6
11
|
*/
|
|
7
12
|
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
13
|
+
import { chapterSortKey } from './chapter-sort.js';
|
|
8
14
|
|
|
9
15
|
export type Chapter = CollectionEntry<'chapters'>;
|
|
10
16
|
|
|
11
|
-
/**
|
|
17
|
+
/** Sort key for an Astro Chapter collection entry. Thin wrapper over the
|
|
18
|
+
* pure `chapterSortKey` helper. */
|
|
12
19
|
export function sortKey(c: Chapter): number {
|
|
13
|
-
return c.data
|
|
20
|
+
return chapterSortKey(c.data as Record<string, unknown>);
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
/** All non-draft chapters, ordered by part+chapter
|
|
23
|
+
/** All non-draft chapters, ordered by part+chapter (tools) or part+week (academic). */
|
|
17
24
|
export async function getAllChapters(): Promise<Chapter[]> {
|
|
18
25
|
const all = await getCollection('chapters', (entry) => !entry.data.draft);
|
|
19
26
|
return all.sort((a, b) => sortKey(a) - sortKey(b));
|