@brandon_m_behring/book-scaffold-astro 4.31.0 → 5.1.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/CLAUDE.md +61 -2
- package/MIGRATION-v4-to-v5.md +183 -0
- package/README.md +64 -1
- package/bin/book-scaffold.mjs +4 -0
- package/components/AssessmentTest.astro +26 -5
- package/components/BookLink.astro +6 -3
- package/components/ChapterNav.astro +1 -1
- package/components/Cite.astro +37 -4
- package/components/ExerciseSolutions.astro +24 -4
- package/components/Flashcards.tsx +19 -11
- package/components/NavContent.astro +60 -11
- package/components/ObjectiveMap.astro +14 -2
- package/components/PartReview.astro +32 -4
- package/components/PatternTimeline.astro +6 -2
- package/components/Rationale.astro +20 -3
- package/components/Sidebar.astro +11 -3
- package/components/SourceArchive.astro +33 -3
- package/components/Term.astro +18 -1
- package/components/Theorem.astro +15 -5
- package/components/TipsCard.astro +40 -3
- package/components/XRef.astro +14 -2
- package/dist/components/ExamRunner.d.ts +1 -1
- package/dist/components/Flashcards.d.ts +6 -1
- package/dist/components/Flashcards.mjs +14 -11
- package/dist/{exam-manifest-X9IrX1G3.d.ts → exam-manifest-DttY7kyZ.d.ts} +1 -1
- package/dist/index.d.ts +76 -8
- package/dist/index.mjs +522 -44
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +311 -32
- package/dist/{types-DgSlAew3.d.ts → types-D1QZgKMO.d.ts} +73 -21
- package/layouts/Base.astro +38 -9
- package/layouts/Chapter.astro +10 -2
- package/package.json +5 -2
- package/pages/answers.astro +25 -6
- package/pages/book.astro +75 -0
- package/pages/chapters/[...slug].astro +39 -7
- package/pages/chapters-book.astro +17 -0
- package/pages/chapters.astro +87 -9
- package/pages/convergence.astro +40 -10
- package/pages/corpus-apparatus/answers.astro +15 -0
- package/pages/corpus-apparatus/convergence.astro +15 -0
- package/pages/corpus-apparatus/exercises.astro +15 -0
- package/pages/corpus-apparatus/flashcards.astro +15 -0
- package/pages/corpus-apparatus/glossary.astro +15 -0
- package/pages/corpus-apparatus/practice-exam.astro +15 -0
- package/pages/corpus-apparatus/print.astro +15 -0
- package/pages/corpus-apparatus/references.astro +15 -0
- package/pages/corpus-apparatus/tips.astro +15 -0
- package/pages/exercises.astro +18 -5
- package/pages/flashcards.astro +26 -5
- package/pages/glossary.astro +20 -3
- package/pages/index.astro +54 -1
- package/pages/practice-exam.astro +8 -2
- package/pages/print.astro +7 -2
- package/pages/references.astro +26 -6
- package/pages/search.astro +65 -4
- package/pages/tips.astro +17 -4
- package/recipes/09-validation.md +1 -1
- package/recipes/15-defining-styles.md +6 -6
- package/recipes/20-anki-export.md +15 -8
- package/recipes/21-multi-guide-single-app.md +293 -44
- package/recipes/22-responsive-nav-and-multibook-routing.md +7 -1
- package/recipes/25-qa-readiness.md +197 -0
- package/recipes/README.md +3 -2
- package/scripts/build-bib.mjs +113 -35
- package/scripts/build-exercises.mjs +101 -24
- package/scripts/build-labels.mjs +199 -194
- package/scripts/build-tips.mjs +95 -23
- package/scripts/corpus-tooling.mjs +268 -0
- package/scripts/init-qa.mjs +209 -0
- package/scripts/qa-core.mjs +1511 -0
- package/scripts/qa.mjs +293 -0
- package/scripts/resolve-book-config.mjs +138 -1
- package/scripts/validate-core.mjs +1675 -0
- package/scripts/validate.mjs +18 -916
- package/scripts/validation-artifacts.mjs +23 -0
- package/scripts/walk-mdx.mjs +16 -4
- package/src/lib/book-link.ts +72 -0
- package/src/lib/chapters.ts +10 -4
- package/src/lib/corpus-collateral.ts +9 -0
- package/src/lib/corpus.ts +458 -0
- package/src/lib/define-style.ts +28 -15
- package/src/lib/exam-manifest.ts +4 -1
- package/src/lib/patterns.ts +15 -6
- package/src/lib/questions.ts +8 -3
- package/src/types.ts +603 -0
package/dist/index.mjs
CHANGED
|
@@ -913,6 +913,17 @@ var BOOK_PROFILES = Object.keys(PROFILES);
|
|
|
913
913
|
import { existsSync, readFileSync } from "fs";
|
|
914
914
|
var BOOK_PRESETS = BOOK_PROFILES;
|
|
915
915
|
var NUMBER_STYLES = ["shared", "per-kind"];
|
|
916
|
+
var CORPUS_APPARATUS_ROUTES = [
|
|
917
|
+
"references",
|
|
918
|
+
"print",
|
|
919
|
+
"convergence",
|
|
920
|
+
"tips",
|
|
921
|
+
"exercises",
|
|
922
|
+
"practice-exam",
|
|
923
|
+
"glossary",
|
|
924
|
+
"flashcards",
|
|
925
|
+
"answers"
|
|
926
|
+
];
|
|
916
927
|
var BookConfigError = class extends Error {
|
|
917
928
|
constructor(message) {
|
|
918
929
|
super(message);
|
|
@@ -937,35 +948,25 @@ function readEnvFile(path = ".env") {
|
|
|
937
948
|
return {};
|
|
938
949
|
}
|
|
939
950
|
}
|
|
940
|
-
var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
|
|
941
951
|
function resolvePreset(explicitPreset, explicitProfile) {
|
|
942
952
|
let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
|
|
943
|
-
let source = "default";
|
|
944
|
-
if (explicitPreset || explicitProfile) source = "param";
|
|
945
|
-
else if (process.env.BOOK_PRESET || process.env.BOOK_PROFILE) source = "env";
|
|
946
953
|
if (!candidate) {
|
|
947
954
|
const env = readEnvFile();
|
|
948
955
|
const fromFile = env.BOOK_PRESET ?? env.BOOK_PROFILE;
|
|
949
956
|
if (fromFile) {
|
|
950
957
|
candidate = fromFile;
|
|
951
|
-
source = "dotenv";
|
|
952
958
|
}
|
|
953
959
|
}
|
|
954
|
-
|
|
960
|
+
if (!candidate) {
|
|
961
|
+
throw new BookConfigError(
|
|
962
|
+
`book-scaffold-astro: no book preset was resolved. Add a built-in Style to \`defineBookConfig({ styles: [...] })\` and pass the same preset to \`defineBookSchemas({ preset: "..." })\`, pass one \`defineBookCorpus\` manifest to both entrypoints, or set BOOK_PRESET in the environment or .env. Valid presets: ${BOOK_PRESETS.join(" | ")}. See https://github.com/brandon-behring/book-scaffold-astro/blob/main/package/MIGRATION-v4-to-v5.md.`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
955
965
|
if (!BOOK_PRESETS.includes(candidate)) {
|
|
956
966
|
throw new BookConfigError(
|
|
957
967
|
`preset must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(candidate)})`
|
|
958
968
|
);
|
|
959
969
|
}
|
|
960
|
-
if (source === "default") {
|
|
961
|
-
const warningState = globalThis;
|
|
962
|
-
if (!warningState[PRESET_FALLBACK_WARNING]) {
|
|
963
|
-
warningState[PRESET_FALLBACK_WARNING] = true;
|
|
964
|
-
console.warn(
|
|
965
|
-
"book-scaffold-astro: no preset resolved; falling back to 'minimal' for v4 compatibility. This fallback will be removed in v5. Add a built-in style to defineBookConfig and pass the same preset to defineBookSchemas, or set BOOK_PRESET."
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
970
|
return candidate;
|
|
970
971
|
}
|
|
971
972
|
function resolveProfile(explicit) {
|
|
@@ -977,8 +978,316 @@ import { fileURLToPath } from "url";
|
|
|
977
978
|
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
978
979
|
import { join } from "path";
|
|
979
980
|
|
|
981
|
+
// src/lib/corpus.ts
|
|
982
|
+
var BOOK_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
983
|
+
var BOOK_CORPUS_BRAND = /* @__PURE__ */ Symbol.for(
|
|
984
|
+
"@brandon_m_behring/book-scaffold-astro/BookCorpus/v1"
|
|
985
|
+
);
|
|
986
|
+
var RESERVED_CORPUS_BOOK_IDS = Object.freeze([
|
|
987
|
+
"assets",
|
|
988
|
+
"chapters",
|
|
989
|
+
"search",
|
|
990
|
+
"questions",
|
|
991
|
+
"glossary",
|
|
992
|
+
"frontmatter",
|
|
993
|
+
"_astro",
|
|
994
|
+
"_og",
|
|
995
|
+
"pagefind"
|
|
996
|
+
]);
|
|
997
|
+
var CORPUS_OWNED_ROUTE_FIELDS = Object.freeze([
|
|
998
|
+
"chapterRoute",
|
|
999
|
+
"bookField",
|
|
1000
|
+
"apparatusRoute",
|
|
1001
|
+
"apparatusRoutes"
|
|
1002
|
+
]);
|
|
1003
|
+
var CORPUS_APPARATUS_TOGGLE_BY_ROUTE = Object.freeze({
|
|
1004
|
+
references: "references",
|
|
1005
|
+
print: "print",
|
|
1006
|
+
convergence: "convergence",
|
|
1007
|
+
tips: "tips",
|
|
1008
|
+
exercises: "exercises",
|
|
1009
|
+
"practice-exam": "practiceExam",
|
|
1010
|
+
glossary: "glossary",
|
|
1011
|
+
flashcards: "flashcards",
|
|
1012
|
+
answers: "answers"
|
|
1013
|
+
});
|
|
1014
|
+
var RESERVED = new Set(RESERVED_CORPUS_BOOK_IDS);
|
|
1015
|
+
var APPARATUS = new Set(CORPUS_APPARATUS_ROUTES);
|
|
1016
|
+
var RESERVED_CONTENT_ROOTS = Object.freeze({
|
|
1017
|
+
questions: "src/content/questions/ is the dedicated questions collection root",
|
|
1018
|
+
glossary: "src/content/glossary/ is the dedicated glossary collection root",
|
|
1019
|
+
frontmatter: "src/content/frontmatter/ is the shared frontmatter collection root"
|
|
1020
|
+
});
|
|
1021
|
+
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["preset", "books"]);
|
|
1022
|
+
var BRANDED_CORPUS_KEYS = /* @__PURE__ */ new Set(["__bookCorpusVersion", "preset", "books"]);
|
|
1023
|
+
var CORPUS_ARTIFACT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "books"]);
|
|
1024
|
+
var BOOK_KEYS = /* @__PURE__ */ new Set([
|
|
1025
|
+
"id",
|
|
1026
|
+
"title",
|
|
1027
|
+
"subtitle",
|
|
1028
|
+
"description",
|
|
1029
|
+
"author",
|
|
1030
|
+
"image",
|
|
1031
|
+
"apparatus"
|
|
1032
|
+
]);
|
|
1033
|
+
function objectRecord(value, label) {
|
|
1034
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1035
|
+
throw new BookConfigError(`${label} must be an object.`);
|
|
1036
|
+
}
|
|
1037
|
+
return value;
|
|
1038
|
+
}
|
|
1039
|
+
function rejectUnknownKeys(value, allowed, label) {
|
|
1040
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
1041
|
+
if (unknown.length > 0) {
|
|
1042
|
+
throw new BookConfigError(
|
|
1043
|
+
`${label} has unknown field${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}.`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function optionalNonBlankString(value, label) {
|
|
1048
|
+
if (value === void 0) return void 0;
|
|
1049
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1050
|
+
throw new BookConfigError(`${label} must be a non-blank string when provided.`);
|
|
1051
|
+
}
|
|
1052
|
+
return value;
|
|
1053
|
+
}
|
|
1054
|
+
function validateBook(value, index) {
|
|
1055
|
+
const label = `defineBookCorpus books[${index}]`;
|
|
1056
|
+
const input = objectRecord(value, label);
|
|
1057
|
+
rejectUnknownKeys(input, BOOK_KEYS, label);
|
|
1058
|
+
const id = optionalNonBlankString(input.id, `${label}.id`);
|
|
1059
|
+
if (id === void 0 || !BOOK_ID.test(id)) {
|
|
1060
|
+
throw new BookConfigError(
|
|
1061
|
+
`${label}.id must match [a-z0-9]+(?:-[a-z0-9]+)* (got ${JSON.stringify(input.id)}).`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
if (RESERVED.has(id)) {
|
|
1065
|
+
const reason = RESERVED_CONTENT_ROOTS[id] ?? "the scaffold owns that route or asset namespace";
|
|
1066
|
+
throw new BookConfigError(
|
|
1067
|
+
`${label}.id ${JSON.stringify(id)} is reserved because ${reason}; choose a book-specific id.`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const title = optionalNonBlankString(input.title, `${label}.title`);
|
|
1071
|
+
if (title === void 0) {
|
|
1072
|
+
throw new BookConfigError(`${label}.title must be a non-blank string.`);
|
|
1073
|
+
}
|
|
1074
|
+
let apparatus;
|
|
1075
|
+
if (input.apparatus !== void 0) {
|
|
1076
|
+
if (!Array.isArray(input.apparatus)) {
|
|
1077
|
+
throw new BookConfigError(`${label}.apparatus must be an array when provided.`);
|
|
1078
|
+
}
|
|
1079
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1080
|
+
const routes = [];
|
|
1081
|
+
for (const route of input.apparatus) {
|
|
1082
|
+
if (typeof route !== "string" || !APPARATUS.has(route)) {
|
|
1083
|
+
throw new BookConfigError(
|
|
1084
|
+
`${label}.apparatus contains ${JSON.stringify(route)}; expected a subset of ` + CORPUS_APPARATUS_ROUTES.join(" | ") + "."
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (seen.has(route)) {
|
|
1088
|
+
throw new BookConfigError(`${label}.apparatus contains duplicate route ${JSON.stringify(route)}.`);
|
|
1089
|
+
}
|
|
1090
|
+
seen.add(route);
|
|
1091
|
+
routes.push(route);
|
|
1092
|
+
}
|
|
1093
|
+
apparatus = Object.freeze(routes);
|
|
1094
|
+
}
|
|
1095
|
+
const book = {
|
|
1096
|
+
id,
|
|
1097
|
+
title,
|
|
1098
|
+
...optionalField("subtitle", optionalNonBlankString(input.subtitle, `${label}.subtitle`)),
|
|
1099
|
+
...optionalField("description", optionalNonBlankString(input.description, `${label}.description`)),
|
|
1100
|
+
...optionalField("author", optionalNonBlankString(input.author, `${label}.author`)),
|
|
1101
|
+
...optionalField("image", optionalNonBlankString(input.image, `${label}.image`)),
|
|
1102
|
+
...apparatus === void 0 ? {} : { apparatus }
|
|
1103
|
+
};
|
|
1104
|
+
return Object.freeze(book);
|
|
1105
|
+
}
|
|
1106
|
+
function optionalField(key, value) {
|
|
1107
|
+
return value === void 0 ? {} : { [key]: value };
|
|
1108
|
+
}
|
|
1109
|
+
function defineBookCorpus(inputValue) {
|
|
1110
|
+
const input = objectRecord(inputValue, "defineBookCorpus input");
|
|
1111
|
+
rejectUnknownKeys(input, TOP_LEVEL_KEYS, "defineBookCorpus input");
|
|
1112
|
+
if (typeof input.preset !== "string" || !BOOK_PRESETS.includes(input.preset)) {
|
|
1113
|
+
throw new BookConfigError(
|
|
1114
|
+
`defineBookCorpus preset must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(input.preset)}).`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if (!Array.isArray(input.books) || input.books.length === 0) {
|
|
1118
|
+
throw new BookConfigError("defineBookCorpus books must be a non-empty array.");
|
|
1119
|
+
}
|
|
1120
|
+
const books = input.books.map(validateBook);
|
|
1121
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1122
|
+
for (const book of books) {
|
|
1123
|
+
if (seen.has(book.id)) {
|
|
1124
|
+
throw new BookConfigError(`defineBookCorpus book id ${JSON.stringify(book.id)} is duplicated.`);
|
|
1125
|
+
}
|
|
1126
|
+
seen.add(book.id);
|
|
1127
|
+
}
|
|
1128
|
+
const corpus = {
|
|
1129
|
+
__bookCorpusVersion: 1,
|
|
1130
|
+
preset: input.preset,
|
|
1131
|
+
books: Object.freeze(books)
|
|
1132
|
+
};
|
|
1133
|
+
Object.defineProperty(corpus, BOOK_CORPUS_BRAND, {
|
|
1134
|
+
value: true,
|
|
1135
|
+
enumerable: false,
|
|
1136
|
+
configurable: false,
|
|
1137
|
+
writable: false
|
|
1138
|
+
});
|
|
1139
|
+
return Object.freeze(corpus);
|
|
1140
|
+
}
|
|
1141
|
+
function assertBookCorpus(value) {
|
|
1142
|
+
const invalid = () => new BookConfigError(
|
|
1143
|
+
"corpus must be created by defineBookCorpus() from this book-scaffold major."
|
|
1144
|
+
);
|
|
1145
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1146
|
+
throw invalid();
|
|
1147
|
+
}
|
|
1148
|
+
const input = value;
|
|
1149
|
+
if (input[BOOK_CORPUS_BRAND] !== true || input.__bookCorpusVersion !== 1 || !Object.isFrozen(value) || !Object.isFrozen(input.books)) {
|
|
1150
|
+
throw invalid();
|
|
1151
|
+
}
|
|
1152
|
+
try {
|
|
1153
|
+
rejectUnknownKeys(input, BRANDED_CORPUS_KEYS, "corpus");
|
|
1154
|
+
if (typeof input.preset !== "string" || !BOOK_PRESETS.includes(input.preset)) {
|
|
1155
|
+
throw invalid();
|
|
1156
|
+
}
|
|
1157
|
+
if (!Array.isArray(input.books) || input.books.length === 0) throw invalid();
|
|
1158
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1159
|
+
for (const [index, rawBook] of input.books.entries()) {
|
|
1160
|
+
if (!Object.isFrozen(rawBook)) throw invalid();
|
|
1161
|
+
const book = validateBook(rawBook, index);
|
|
1162
|
+
if (seen.has(book.id)) throw invalid();
|
|
1163
|
+
seen.add(book.id);
|
|
1164
|
+
if (book.apparatus !== void 0) {
|
|
1165
|
+
const original = rawBook.apparatus;
|
|
1166
|
+
if (!Object.isFrozen(original)) throw invalid();
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
if (error instanceof BookConfigError && error.message.startsWith("corpus must be created")) {
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
throw invalid();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function corpusApparatusRoutesForBook(corpus, bookId, inheritedRoutes = []) {
|
|
1177
|
+
const book = resolveCorpusBook(corpus, bookId);
|
|
1178
|
+
return book.apparatus ?? inheritedRoutes;
|
|
1179
|
+
}
|
|
1180
|
+
function corpusBookHasApparatusRoute(corpus, bookId, route, inheritedRoutes = []) {
|
|
1181
|
+
return corpusApparatusRoutesForBook(corpus, bookId, inheritedRoutes).includes(route);
|
|
1182
|
+
}
|
|
1183
|
+
function selectBookArtifact(value, corpus, bookId, artifact = "artifact") {
|
|
1184
|
+
if (!corpus) return value;
|
|
1185
|
+
if (!bookId) {
|
|
1186
|
+
throw new Error(`${artifact}: cannot select a corpus payload without a current book.`);
|
|
1187
|
+
}
|
|
1188
|
+
resolveCorpusBook(corpus, bookId);
|
|
1189
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`${artifact} is not a corpus artifact envelope; expected \`{ "schemaVersion": 1, "books": { ... } }\`.`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
const envelopeRecord = value;
|
|
1195
|
+
rejectUnknownKeys(envelopeRecord, CORPUS_ARTIFACT_KEYS, artifact);
|
|
1196
|
+
const envelope = envelopeRecord;
|
|
1197
|
+
if (envelope.schemaVersion !== 1 || envelope.books === null || typeof envelope.books !== "object" || Array.isArray(envelope.books)) {
|
|
1198
|
+
throw new BookConfigError(
|
|
1199
|
+
`${artifact} is not a corpus artifact envelope; expected \`{ "schemaVersion": 1, "books": { ... } }\`.`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
const books = envelope.books;
|
|
1203
|
+
const expected = corpus.books.map((book) => book.id);
|
|
1204
|
+
const actual = Object.keys(books);
|
|
1205
|
+
const missing = expected.filter((id) => !Object.prototype.hasOwnProperty.call(books, id));
|
|
1206
|
+
const unknown = actual.filter((id) => !expected.includes(id));
|
|
1207
|
+
if (missing.length > 0 || unknown.length > 0) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`${artifact} book keys do not match the corpus manifest` + (missing.length > 0 ? `; missing ${missing.join(", ")}` : "") + (unknown.length > 0 ? `; unknown ${unknown.join(", ")}` : "") + "."
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
return books[bookId];
|
|
1213
|
+
}
|
|
1214
|
+
function resolveCorpusBook(corpus, id) {
|
|
1215
|
+
const book = corpus.books.find((candidate) => candidate.id === id);
|
|
1216
|
+
if (!book) {
|
|
1217
|
+
throw new BookConfigError(
|
|
1218
|
+
`Unknown corpus book ${JSON.stringify(id)}; expected one of ${corpus.books.map((candidate) => candidate.id).join(" | ")}.`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
return book;
|
|
1222
|
+
}
|
|
1223
|
+
function corpusBookIdOf(corpus, entryId) {
|
|
1224
|
+
const candidate = entryId.split("/")[0] ?? "";
|
|
1225
|
+
return corpus.books.some((book) => book.id === candidate) ? candidate : null;
|
|
1226
|
+
}
|
|
1227
|
+
function localCorpusEntryId(corpus, bookId, entryId) {
|
|
1228
|
+
resolveCorpusBook(corpus, bookId);
|
|
1229
|
+
const prefix = `${bookId}/`;
|
|
1230
|
+
if (!entryId.startsWith(prefix) || entryId.length === prefix.length) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
`Collection entry ${JSON.stringify(entryId)} is outside corpus book ${JSON.stringify(bookId)}.`
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
return entryId.slice(prefix.length);
|
|
1236
|
+
}
|
|
1237
|
+
function filterCorpusEntries(entries, corpus, bookId) {
|
|
1238
|
+
if (!corpus) return [...entries];
|
|
1239
|
+
if (!bookId) throw new Error("Cannot scope corpus content without a current book.");
|
|
1240
|
+
resolveCorpusBook(corpus, bookId);
|
|
1241
|
+
const prefix = `${bookId}/`;
|
|
1242
|
+
return entries.filter((entry) => entry.id.startsWith(prefix));
|
|
1243
|
+
}
|
|
1244
|
+
function corpusCollectionEntryId(corpus, entry, data, options = {}) {
|
|
1245
|
+
const label = options.label ?? "Content entry";
|
|
1246
|
+
const normalized = entry.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1247
|
+
const [bookId, ...localParts] = normalized.split("/");
|
|
1248
|
+
if (!bookId || !corpus.books.some((book) => book.id === bookId)) {
|
|
1249
|
+
throw new Error(`${label} ${JSON.stringify(entry)} is outside the registered corpus books.`);
|
|
1250
|
+
}
|
|
1251
|
+
if (Object.prototype.hasOwnProperty.call(data, "book") && data.book !== bookId) {
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
`${label} ${JSON.stringify(entry)} has frontmatter book ${JSON.stringify(data.book)}, but its registered path owner is ${JSON.stringify(bookId)}.`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
const fileId = localParts.join("/").replace(/\.(?:md|mdx)$/i, "");
|
|
1257
|
+
const configured = options.slugField ? data[options.slugField] : void 0;
|
|
1258
|
+
const localId = typeof configured === "string" ? configured : fileId;
|
|
1259
|
+
const localSegments = localId.split("/");
|
|
1260
|
+
let hasInvalidEncodedSegment = false;
|
|
1261
|
+
for (const segment of localSegments) {
|
|
1262
|
+
try {
|
|
1263
|
+
const decoded = decodeURIComponent(segment);
|
|
1264
|
+
if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
|
|
1265
|
+
hasInvalidEncodedSegment = true;
|
|
1266
|
+
}
|
|
1267
|
+
} catch {
|
|
1268
|
+
hasInvalidEncodedSegment = true;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (localId.length === 0 || localId.startsWith("/") || localId.endsWith("/") || localId.includes("\\") || localSegments.some((part) => part === "." || part === ".." || part.length === 0) || hasInvalidEncodedSegment) {
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
`${label} ${JSON.stringify(entry)} resolved invalid corpus id ${JSON.stringify(localId)}.`
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
return `${bookId}/${localId}`;
|
|
1277
|
+
}
|
|
1278
|
+
function corpusBookIdFromPath(corpus, pathname, baseUrl = "/") {
|
|
1279
|
+
const normalizedBase2 = baseUrl === "/" ? "/" : `/${baseUrl.replace(/^\/+|\/+$/g, "")}/`;
|
|
1280
|
+
const relative = pathname.startsWith(normalizedBase2) ? pathname.slice(normalizedBase2.length) : pathname.replace(/^\/+/, "");
|
|
1281
|
+
const segments = relative.split("/").filter(Boolean);
|
|
1282
|
+
const candidate = segments[0] === "chapters" ? segments[1] : segments[0];
|
|
1283
|
+
return candidate && corpus.books.some((book) => book.id === candidate) ? candidate : null;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
980
1286
|
// src/lib/define-style.ts
|
|
981
1287
|
function defineStyle(opts) {
|
|
1288
|
+
if (Object.prototype.hasOwnProperty.call(opts, "deploy")) {
|
|
1289
|
+
throw removedDeployError("defineStyle");
|
|
1290
|
+
}
|
|
982
1291
|
return { __styleVersion: 1, ...opts };
|
|
983
1292
|
}
|
|
984
1293
|
function composeStyles(styles) {
|
|
@@ -987,11 +1296,13 @@ function composeStyles(styles) {
|
|
|
987
1296
|
}
|
|
988
1297
|
const merged = {};
|
|
989
1298
|
for (const style of styles) {
|
|
1299
|
+
if (Object.prototype.hasOwnProperty.call(style, "deploy")) {
|
|
1300
|
+
throw removedDeployError("composeStyles");
|
|
1301
|
+
}
|
|
990
1302
|
if (style.name !== void 0) merged.name = style.name;
|
|
991
1303
|
if (style.preset !== void 0) merged.preset = style.preset;
|
|
992
1304
|
if (style.numberStyle !== void 0) merged.numberStyle = style.numberStyle;
|
|
993
1305
|
if (style.site !== void 0) merged.site = style.site;
|
|
994
|
-
if (style.deploy !== void 0) merged.deploy = style.deploy;
|
|
995
1306
|
if (style.releaseStatus !== void 0) {
|
|
996
1307
|
merged.releaseStatus = style.releaseStatus;
|
|
997
1308
|
}
|
|
@@ -1031,6 +1342,11 @@ function composeStyles(styles) {
|
|
|
1031
1342
|
}
|
|
1032
1343
|
return defineStyle(merged);
|
|
1033
1344
|
}
|
|
1345
|
+
function removedDeployError(context) {
|
|
1346
|
+
return new BookConfigError(
|
|
1347
|
+
`book-scaffold-astro v5 removed ${context}({ deploy }) because the field never controlled deployment. Remove it and configure wrangler.toml or your deployment platform directly. See https://github.com/brandon-behring/book-scaffold-astro/blob/main/package/MIGRATION-v4-to-v5.md.`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1034
1350
|
function mergeMarkdown(a, b) {
|
|
1035
1351
|
if (!a) return b;
|
|
1036
1352
|
if (!b) return a;
|
|
@@ -1189,6 +1505,9 @@ var ROUTE_REGISTRY = {
|
|
|
1189
1505
|
// Pre-v4.3.0 each consumer wrote this file by hand; all instances were
|
|
1190
1506
|
// mechanical copies of the same boilerplate.
|
|
1191
1507
|
chaptersSlug: { pattern: "/chapters/[...slug]", file: "chapters/[...slug].astro" },
|
|
1508
|
+
// v5.0.0 (#80): corpus-only landing and per-book chapter index.
|
|
1509
|
+
corpusBook: { pattern: "/[book]", file: "book.astro" },
|
|
1510
|
+
corpusChapters: { pattern: "/chapters/[book]", file: "chapters-book.astro" },
|
|
1192
1511
|
convergence: { pattern: "/convergence", file: "convergence.astro" },
|
|
1193
1512
|
// v4.3.0 (#70): cross-volume numbered-tips index. Opt-in via
|
|
1194
1513
|
// routes.tips: true; pairs with build-tips script + <Tip> component.
|
|
@@ -1241,8 +1560,30 @@ function resolvePage(file) {
|
|
|
1241
1560
|
return fileURLToPath(new URL(`../pages/${file}`, import.meta.url));
|
|
1242
1561
|
}
|
|
1243
1562
|
function bookScaffoldIntegration(opts) {
|
|
1563
|
+
if (!BOOK_PRESETS.includes(opts.profile)) {
|
|
1564
|
+
throw new BookConfigError(
|
|
1565
|
+
`bookScaffoldIntegration profile must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(opts.profile)}).`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
if (opts.corpus !== void 0) {
|
|
1569
|
+
assertBookCorpus(opts.corpus);
|
|
1570
|
+
if (opts.corpus.preset !== opts.profile) {
|
|
1571
|
+
throw new BookConfigError(
|
|
1572
|
+
`bookScaffoldIntegration corpus preset ${JSON.stringify(opts.corpus.preset)} does not match profile ${JSON.stringify(opts.profile)}. One preset applies to the whole corpus.`
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
const incompatible = CORPUS_OWNED_ROUTE_FIELDS.filter(
|
|
1576
|
+
(field) => Object.prototype.hasOwnProperty.call(opts, field)
|
|
1577
|
+
);
|
|
1578
|
+
if (incompatible.length > 0) {
|
|
1579
|
+
throw new BookConfigError(
|
|
1580
|
+
`Corpus mode owns ${incompatible.join(", ")}; remove the explicit ${incompatible.length === 1 ? "override" : "overrides"} so injected routes and navigation agree.`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1244
1584
|
const {
|
|
1245
1585
|
profile,
|
|
1586
|
+
corpus,
|
|
1246
1587
|
numberStyle = "shared",
|
|
1247
1588
|
routes: userOverrides = {},
|
|
1248
1589
|
extraStyles = [],
|
|
@@ -1282,6 +1623,32 @@ function bookScaffoldIntegration(opts) {
|
|
|
1282
1623
|
),
|
|
1283
1624
|
frontmatter: fmEnabled
|
|
1284
1625
|
};
|
|
1626
|
+
const enabledRouteNames = Object.freeze(
|
|
1627
|
+
Object.entries(enabledRoutes).filter(([, on]) => on).map(([name]) => name)
|
|
1628
|
+
);
|
|
1629
|
+
if (corpus) {
|
|
1630
|
+
for (const book of corpus.books) {
|
|
1631
|
+
for (const route of book.apparatus ?? []) {
|
|
1632
|
+
const toggle = CORPUS_APPARATUS_TOGGLE_BY_ROUTE[route];
|
|
1633
|
+
if (!enabledRoutes[toggle]) {
|
|
1634
|
+
throw new BookConfigError(
|
|
1635
|
+
`Corpus book ${JSON.stringify(book.id)} enables apparatus route ${JSON.stringify(route)}, but RouteToggles.${toggle} is disabled. Enable routes.${toggle} globally or remove the book apparatus entry.`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const inheritedCorpusApparatusRoutes = corpus ? CORPUS_APPARATUS_ROUTES.filter(
|
|
1642
|
+
(route) => enabledRoutes[CORPUS_APPARATUS_TOGGLE_BY_ROUTE[route]]
|
|
1643
|
+
) : [];
|
|
1644
|
+
const corpusApparatusRoutes = corpus ? CORPUS_APPARATUS_ROUTES.filter(
|
|
1645
|
+
(route) => corpus.books.some(
|
|
1646
|
+
(book) => (book.apparatus ?? inheritedCorpusApparatusRoutes).includes(route)
|
|
1647
|
+
)
|
|
1648
|
+
) : [];
|
|
1649
|
+
const corpusApparatusToggleKeys = new Set(
|
|
1650
|
+
Object.values(CORPUS_APPARATUS_TOGGLE_BY_ROUTE)
|
|
1651
|
+
);
|
|
1285
1652
|
const integration = {
|
|
1286
1653
|
name: "book-scaffold-astro",
|
|
1287
1654
|
hooks: {
|
|
@@ -1296,24 +1663,39 @@ function bookScaffoldIntegration(opts) {
|
|
|
1296
1663
|
const routesToInject = [];
|
|
1297
1664
|
for (const [name, on] of Object.entries(enabledRoutes)) {
|
|
1298
1665
|
if (!on) continue;
|
|
1666
|
+
if (corpus && corpusApparatusToggleKeys.has(name)) continue;
|
|
1299
1667
|
routesToInject.push(name);
|
|
1300
|
-
if (name === "chapters")
|
|
1668
|
+
if (name === "chapters") {
|
|
1669
|
+
routesToInject.push("chaptersSlug");
|
|
1670
|
+
if (corpus) routesToInject.push("corpusChapters");
|
|
1671
|
+
}
|
|
1672
|
+
if (name === "landing" && corpus) routesToInject.push("corpusBook");
|
|
1301
1673
|
}
|
|
1302
1674
|
for (const name of routesToInject) {
|
|
1303
1675
|
const route = ROUTE_REGISTRY[name];
|
|
1304
1676
|
if (!route) continue;
|
|
1305
|
-
|
|
1677
|
+
let pattern = name === "frontmatter" ? frontmatterPatternFromPrefix(fmPrefix) : route.pattern;
|
|
1678
|
+
if (corpus && name === "chaptersSlug") {
|
|
1679
|
+
pattern = "/chapters/[book]/[...slug]";
|
|
1680
|
+
}
|
|
1306
1681
|
injectRoute({
|
|
1307
1682
|
pattern,
|
|
1308
1683
|
entrypoint: resolvePage(route.file)
|
|
1309
1684
|
});
|
|
1310
1685
|
}
|
|
1686
|
+
if (corpus) {
|
|
1687
|
+
for (const route of corpusApparatusRoutes) {
|
|
1688
|
+
injectRoute({
|
|
1689
|
+
pattern: `/[book]/${route}`,
|
|
1690
|
+
entrypoint: resolvePage(`corpus-apparatus/${route}.astro`)
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1311
1694
|
const consumerRoot = fileURLToPath(config.root);
|
|
1312
1695
|
const resolvedMdxPath = resolveMdxComponentsPath(consumerRoot, mdxComponentsModule);
|
|
1313
1696
|
const resolvedGithubRepo = resolveBookGithubRepo(githubRepo, consumerRoot);
|
|
1314
1697
|
const resolvedGithubBranch = githubBranch ?? DEFAULT_GITHUB_BRANCH;
|
|
1315
1698
|
const presetLiteral = JSON.stringify(profile);
|
|
1316
|
-
const enabledRouteNames = Object.entries(enabledRoutes).filter(([, on]) => on).map(([name]) => name);
|
|
1317
1699
|
updateConfig({
|
|
1318
1700
|
vite: {
|
|
1319
1701
|
plugins: [
|
|
@@ -1339,8 +1721,9 @@ function bookScaffoldIntegration(opts) {
|
|
|
1339
1721
|
// reproduce the single-book `/chapters/<id>/` behavior exactly.
|
|
1340
1722
|
chapterRoute: chapterRoute ?? "/chapters/:id/",
|
|
1341
1723
|
bookField: bookField ?? "book",
|
|
1342
|
-
apparatusRoute: apparatusRoute ?? "/:route/",
|
|
1343
|
-
apparatusRoutes: apparatusRoutes ?? []
|
|
1724
|
+
apparatusRoute: corpus ? "/:book/:route/" : apparatusRoute ?? "/:route/",
|
|
1725
|
+
apparatusRoutes: corpus ? inheritedCorpusApparatusRoutes : apparatusRoutes ?? [],
|
|
1726
|
+
corpus: corpus ?? null
|
|
1344
1727
|
})
|
|
1345
1728
|
],
|
|
1346
1729
|
define: {
|
|
@@ -1383,8 +1766,13 @@ function bookScaffoldIntegration(opts) {
|
|
|
1383
1766
|
preset: profile,
|
|
1384
1767
|
numberStyle,
|
|
1385
1768
|
siblingBooks: siblingBooks ?? {},
|
|
1386
|
-
|
|
1387
|
-
|
|
1769
|
+
corpus: corpus ?? null,
|
|
1770
|
+
chapterRoute: corpus ? "/chapters/:id/" : chapterRoute ?? "/chapters/:id/",
|
|
1771
|
+
bookField: bookField ?? "book",
|
|
1772
|
+
apparatusRoute: corpus ? "/:book/:route/" : apparatusRoute ?? "/:route/",
|
|
1773
|
+
apparatusRoutes: corpus ? inheritedCorpusApparatusRoutes : apparatusRoutes ?? [],
|
|
1774
|
+
enabledRoutes: enabledRouteNames,
|
|
1775
|
+
frontmatterRoute: frontmatterPatternFromPrefix(fmPrefix)
|
|
1388
1776
|
}),
|
|
1389
1777
|
enumerable: false,
|
|
1390
1778
|
configurable: false,
|
|
@@ -1428,12 +1816,38 @@ async function defineBookConfig(opts) {
|
|
|
1428
1816
|
throw v3MigrationError(opts);
|
|
1429
1817
|
}
|
|
1430
1818
|
if (Object.prototype.hasOwnProperty.call(opts, "deploy")) {
|
|
1431
|
-
|
|
1432
|
-
"book-scaffold-astro
|
|
1819
|
+
throw new BookConfigError(
|
|
1820
|
+
"book-scaffold-astro v5 removed defineBookConfig({ deploy }) because the field never controlled deployment. Remove it and configure wrangler.toml or your deployment platform directly. See https://github.com/brandon-behring/book-scaffold-astro/blob/main/package/MIGRATION-v4-to-v5.md."
|
|
1433
1821
|
);
|
|
1434
1822
|
}
|
|
1435
1823
|
const composed = composeStyles(opts.styles ?? []);
|
|
1436
|
-
const
|
|
1824
|
+
const corpus = opts.corpus;
|
|
1825
|
+
if (corpus !== void 0) {
|
|
1826
|
+
assertBookCorpus(corpus);
|
|
1827
|
+
}
|
|
1828
|
+
const profile = resolvePreset(composed.preset ?? corpus?.preset);
|
|
1829
|
+
if (corpus !== void 0) {
|
|
1830
|
+
if (corpus.preset !== profile) {
|
|
1831
|
+
throw new BookConfigError(
|
|
1832
|
+
`defineBookConfig corpus preset ${JSON.stringify(corpus.preset)} does not match the composed Style preset ${JSON.stringify(profile)}. One preset applies to the whole corpus.`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
const incompatible = CORPUS_OWNED_ROUTE_FIELDS.filter(
|
|
1836
|
+
(field) => Object.prototype.hasOwnProperty.call(opts, field)
|
|
1837
|
+
);
|
|
1838
|
+
if (incompatible.length > 0) {
|
|
1839
|
+
throw new BookConfigError(
|
|
1840
|
+
`Corpus mode owns ${incompatible.join(", ")}; remove the explicit ${incompatible.length === 1 ? "override" : "overrides"} so injected routes and navigation agree.`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
const siblingKeys = new Set(Object.keys(opts.siblingBooks ?? {}));
|
|
1844
|
+
const duplicateOwner = corpus.books.find((book) => siblingKeys.has(book.id));
|
|
1845
|
+
if (duplicateOwner) {
|
|
1846
|
+
throw new BookConfigError(
|
|
1847
|
+
`Book id ${JSON.stringify(duplicateOwner.id)} exists in both corpus.books and siblingBooks. A key must be either local or externally deployed, never both.`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1437
1851
|
const numberStyle = opts.numberStyle ?? composed.numberStyle ?? "shared";
|
|
1438
1852
|
if (!NUMBER_STYLES.includes(numberStyle)) {
|
|
1439
1853
|
throw new BookConfigError(
|
|
@@ -1507,6 +1921,7 @@ async function defineBookConfig(opts) {
|
|
|
1507
1921
|
sitemap(sitemapOptions),
|
|
1508
1922
|
bookScaffoldIntegration({
|
|
1509
1923
|
profile,
|
|
1924
|
+
corpus,
|
|
1510
1925
|
numberStyle,
|
|
1511
1926
|
routes: mergedRoutes,
|
|
1512
1927
|
mdxComponentsModule,
|
|
@@ -1538,11 +1953,16 @@ async function defineBookConfig(opts) {
|
|
|
1538
1953
|
siblingBooks: opts.siblingBooks,
|
|
1539
1954
|
// v4.17.0 (#112): per-book exam-domain taxonomy for the questions collection.
|
|
1540
1955
|
examDomains: opts.examDomains,
|
|
1541
|
-
// v4.26.0 (#80): book-aware nav route patterns (undefined → single-book
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1956
|
+
// v4.26.0 (#80): book-aware nav route patterns (undefined → single-book
|
|
1957
|
+
// defaults). Corpus mode owns these fields, so do not manufacture own
|
|
1958
|
+
// properties with undefined values when handing the validated manifest
|
|
1959
|
+
// to the independently exported integration.
|
|
1960
|
+
...corpus ? {} : {
|
|
1961
|
+
chapterRoute: opts.chapterRoute,
|
|
1962
|
+
bookField: opts.bookField,
|
|
1963
|
+
apparatusRoute: opts.apparatusRoute,
|
|
1964
|
+
apparatusRoutes: opts.apparatusRoutes
|
|
1965
|
+
}
|
|
1546
1966
|
}),
|
|
1547
1967
|
...mergedExtraIntegrations
|
|
1548
1968
|
];
|
|
@@ -1569,10 +1989,10 @@ async function defineBookConfig(opts) {
|
|
|
1569
1989
|
};
|
|
1570
1990
|
const {
|
|
1571
1991
|
styles: _styles,
|
|
1992
|
+
corpus: _corpus,
|
|
1572
1993
|
numberStyle: _numberStyle,
|
|
1573
1994
|
site: _site,
|
|
1574
1995
|
routes: _routes,
|
|
1575
|
-
deploy: _deploy,
|
|
1576
1996
|
mdxComponentsModule: _mdxComponentsModule,
|
|
1577
1997
|
extraIntegrations: _extraIntegrations,
|
|
1578
1998
|
extraStyles: _extraStyles,
|
|
@@ -1605,10 +2025,10 @@ async function defineBookConfig(opts) {
|
|
|
1605
2025
|
...rest
|
|
1606
2026
|
} = opts;
|
|
1607
2027
|
void _styles;
|
|
2028
|
+
void _corpus;
|
|
1608
2029
|
void _numberStyle;
|
|
1609
2030
|
void _site;
|
|
1610
2031
|
void _routes;
|
|
1611
|
-
void _deploy;
|
|
1612
2032
|
void _mdxComponentsModule;
|
|
1613
2033
|
void _extraIntegrations;
|
|
1614
2034
|
void _extraStyles;
|
|
@@ -1727,6 +2147,54 @@ function assertEnumProp(value, allowed, ctx) {
|
|
|
1727
2147
|
init_katex_macros();
|
|
1728
2148
|
|
|
1729
2149
|
// src/lib/book-link.ts
|
|
2150
|
+
var CORPUS_BOOK_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2151
|
+
function normalizedBase(baseUrl) {
|
|
2152
|
+
const inner = baseUrl.replace(/^\/+|\/+$/g, "");
|
|
2153
|
+
return inner.length === 0 ? "/" : `/${inner}/`;
|
|
2154
|
+
}
|
|
2155
|
+
function invalidLocalTarget(book, to, reason) {
|
|
2156
|
+
throw new Error(
|
|
2157
|
+
`<BookLink book="${book}" to=${JSON.stringify(to)}>: invalid local corpus target (${reason}).`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
function resolveCorpusBookHref(book, to, baseUrl = "/") {
|
|
2161
|
+
if (!CORPUS_BOOK_ID.test(book)) {
|
|
2162
|
+
throw new Error(`<BookLink book=${JSON.stringify(book)}>: invalid corpus book id.`);
|
|
2163
|
+
}
|
|
2164
|
+
if (typeof to !== "string" || to.trim().length === 0) {
|
|
2165
|
+
invalidLocalTarget(book, String(to), "target must be non-empty");
|
|
2166
|
+
}
|
|
2167
|
+
if (to !== to.trim()) invalidLocalTarget(book, to, "surrounding whitespace is not allowed");
|
|
2168
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(to) || to.startsWith("/") || to.includes("\\")) {
|
|
2169
|
+
invalidLocalTarget(book, to, "absolute URLs and paths are not allowed");
|
|
2170
|
+
}
|
|
2171
|
+
if (/[\u0000-\u001f\u007f]/.test(to)) {
|
|
2172
|
+
invalidLocalTarget(book, to, "control characters are not allowed");
|
|
2173
|
+
}
|
|
2174
|
+
const suffixIndex = [...[to.indexOf("?"), to.indexOf("#")]].filter((index) => index >= 0).reduce((minimum, index) => Math.min(minimum, index), to.length);
|
|
2175
|
+
const path = to.slice(0, suffixIndex);
|
|
2176
|
+
const suffix = to.slice(suffixIndex);
|
|
2177
|
+
if (path.length === 0) invalidLocalTarget(book, to, "query-only and fragment-only targets are not allowed");
|
|
2178
|
+
const segments = path.replace(/\/+$/, "").split("/");
|
|
2179
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
2180
|
+
invalidLocalTarget(book, to, "empty path segments are not allowed");
|
|
2181
|
+
}
|
|
2182
|
+
for (const segment of segments) {
|
|
2183
|
+
let decoded;
|
|
2184
|
+
try {
|
|
2185
|
+
decoded = decodeURIComponent(segment);
|
|
2186
|
+
} catch {
|
|
2187
|
+
invalidLocalTarget(book, to, "malformed percent encoding");
|
|
2188
|
+
}
|
|
2189
|
+
if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
|
|
2190
|
+
invalidLocalTarget(book, to, "path traversal is not allowed");
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
const base = normalizedBase(baseUrl);
|
|
2194
|
+
const local = segments.join("/");
|
|
2195
|
+
const destination = local === "chapters" ? `chapters/${book}` : local.startsWith("chapters/") ? `chapters/${book}/${local.slice("chapters/".length)}` : `${book}/${local}`;
|
|
2196
|
+
return `${base}${destination}/`.replace(/\/{2,}/g, "/") + suffix;
|
|
2197
|
+
}
|
|
1730
2198
|
function entryUrl(entry) {
|
|
1731
2199
|
if (typeof entry === "string") return entry;
|
|
1732
2200
|
return entry?.url;
|
|
@@ -1922,7 +2390,7 @@ function buildExamManifest(entries) {
|
|
|
1922
2390
|
options: (e.data.options ?? []).map((o) => ({ id: o.id, correct: o.correct === true }))
|
|
1923
2391
|
}));
|
|
1924
2392
|
}
|
|
1925
|
-
function deriveDomainRouting(entries, baseUrl = "/") {
|
|
2393
|
+
function deriveDomainRouting(entries, baseUrl = "/", bookId) {
|
|
1926
2394
|
const base = baseUrl.replace(/\/?$/, "/");
|
|
1927
2395
|
const out = {};
|
|
1928
2396
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1933,7 +2401,7 @@ function deriveDomainRouting(entries, baseUrl = "/") {
|
|
|
1933
2401
|
seen.add(key);
|
|
1934
2402
|
(out[e.data.domain] ??= []).push({
|
|
1935
2403
|
label,
|
|
1936
|
-
href: typeof e.data.chapter === "string" ? `${base}chapters/${e.data.chapter}/` : null
|
|
2404
|
+
href: typeof e.data.chapter === "string" ? `${base}chapters/${bookId ? `${bookId}/` : ""}${e.data.chapter}/` : null
|
|
1937
2405
|
});
|
|
1938
2406
|
}
|
|
1939
2407
|
return out;
|
|
@@ -1984,28 +2452,23 @@ function selectPartExercises(chapters, byChapter, part) {
|
|
|
1984
2452
|
// src/styles/built-in.ts
|
|
1985
2453
|
var academicStyle = defineStyle({
|
|
1986
2454
|
name: "academic",
|
|
1987
|
-
preset: "academic"
|
|
1988
|
-
deploy: "workers"
|
|
2455
|
+
preset: "academic"
|
|
1989
2456
|
});
|
|
1990
2457
|
var toolsStyle = defineStyle({
|
|
1991
2458
|
name: "tools",
|
|
1992
|
-
preset: "tools"
|
|
1993
|
-
deploy: "workers"
|
|
2459
|
+
preset: "tools"
|
|
1994
2460
|
});
|
|
1995
2461
|
var minimalStyle = defineStyle({
|
|
1996
2462
|
name: "minimal",
|
|
1997
|
-
preset: "minimal"
|
|
1998
|
-
deploy: "workers"
|
|
2463
|
+
preset: "minimal"
|
|
1999
2464
|
});
|
|
2000
2465
|
var courseNotesStyle = defineStyle({
|
|
2001
2466
|
name: "course-notes",
|
|
2002
|
-
preset: "course-notes"
|
|
2003
|
-
deploy: "pages"
|
|
2467
|
+
preset: "course-notes"
|
|
2004
2468
|
});
|
|
2005
2469
|
var researchPortfolioStyle = defineStyle({
|
|
2006
2470
|
name: "research-portfolio",
|
|
2007
2471
|
preset: "research-portfolio",
|
|
2008
|
-
deploy: "pages",
|
|
2009
2472
|
routes: { frontmatter: { enabled: true, prefix: "frontmatter" } }
|
|
2010
2473
|
});
|
|
2011
2474
|
var BUILTIN_STYLES = {
|
|
@@ -2027,9 +2490,12 @@ export {
|
|
|
2027
2490
|
BRANDON_PORTFOLIO_DEFAULT,
|
|
2028
2491
|
BUILTIN_STYLES,
|
|
2029
2492
|
BookConfigError,
|
|
2493
|
+
CORPUS_APPARATUS_ROUTES,
|
|
2494
|
+
CORPUS_APPARATUS_TOGGLE_BY_ROUTE,
|
|
2030
2495
|
DEFAULT_GITHUB_BRANCH,
|
|
2031
2496
|
KIND_LABEL,
|
|
2032
2497
|
NUMBER_STYLES,
|
|
2498
|
+
RESERVED_CORPUS_BOOK_IDS,
|
|
2033
2499
|
THEOREM_KINDS,
|
|
2034
2500
|
UNKNOWN_PART_ORDINAL,
|
|
2035
2501
|
academicChapterSchema,
|
|
@@ -2040,6 +2506,7 @@ export {
|
|
|
2040
2506
|
academicParts,
|
|
2041
2507
|
academicStyle,
|
|
2042
2508
|
apparatusHref,
|
|
2509
|
+
assertBookCorpus,
|
|
2043
2510
|
assertEnumProp,
|
|
2044
2511
|
assertKnownDomain,
|
|
2045
2512
|
baseNoSlash,
|
|
@@ -2057,9 +2524,15 @@ export {
|
|
|
2057
2524
|
chapterStatus,
|
|
2058
2525
|
citationBackstops,
|
|
2059
2526
|
composeStyles,
|
|
2527
|
+
corpusApparatusRoutesForBook,
|
|
2528
|
+
corpusBookHasApparatusRoute,
|
|
2529
|
+
corpusBookIdFromPath,
|
|
2530
|
+
corpusBookIdOf,
|
|
2531
|
+
corpusCollectionEntryId,
|
|
2060
2532
|
courseNotesChapterSchema,
|
|
2061
2533
|
courseNotesStyle,
|
|
2062
2534
|
defineBookConfig,
|
|
2535
|
+
defineBookCorpus,
|
|
2063
2536
|
defineMdxComponents,
|
|
2064
2537
|
defineProfile,
|
|
2065
2538
|
defineStyle,
|
|
@@ -2068,6 +2541,7 @@ export {
|
|
|
2068
2541
|
deriveObjectiveMap,
|
|
2069
2542
|
distinctChaptersSorted,
|
|
2070
2543
|
fallbackChaptersRenderer,
|
|
2544
|
+
filterCorpusEntries,
|
|
2071
2545
|
freshnessLabel,
|
|
2072
2546
|
getFreshness,
|
|
2073
2547
|
glossarySchema,
|
|
@@ -2075,6 +2549,7 @@ export {
|
|
|
2075
2549
|
groupByDomain,
|
|
2076
2550
|
isCurrentChapter,
|
|
2077
2551
|
layoutModes,
|
|
2552
|
+
localCorpusEntryId,
|
|
2078
2553
|
minimalChapterSchema,
|
|
2079
2554
|
minimalStyle,
|
|
2080
2555
|
normalizeBase,
|
|
@@ -2095,12 +2570,15 @@ export {
|
|
|
2095
2570
|
researchPortfolioChaptersRenderer,
|
|
2096
2571
|
researchPortfolioStyle,
|
|
2097
2572
|
resolveBookHref,
|
|
2573
|
+
resolveCorpusBook,
|
|
2574
|
+
resolveCorpusBookHref,
|
|
2098
2575
|
resolveGithubRepo,
|
|
2099
2576
|
resolvePreset,
|
|
2100
2577
|
resolveProfile,
|
|
2101
2578
|
resolveTheoremNumber,
|
|
2102
2579
|
sampleExam,
|
|
2103
2580
|
scoreExam,
|
|
2581
|
+
selectBookArtifact,
|
|
2104
2582
|
selectPartExercises,
|
|
2105
2583
|
shuffle,
|
|
2106
2584
|
slugOf,
|