@brandon_m_behring/book-scaffold-astro 4.30.0 → 5.0.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 +40 -3
- package/MIGRATION-v4-to-v5.md +183 -0
- package/README.md +30 -1
- package/bin/book-scaffold.mjs +1 -1
- 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/Figure.astro +7 -6
- 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 +517 -43
- 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 +6 -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/03-asset-pipelines.md +12 -4
- package/recipes/09-validation.md +1 -1
- package/recipes/15-defining-styles.md +6 -6
- package/recipes/16-tikz-figures.md +13 -2
- 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/24-figure-authoring-standard.md +241 -0
- package/recipes/README.md +3 -2
- package/scripts/build-bib.mjs +113 -35
- package/scripts/build-exercises.mjs +101 -24
- package/scripts/build-figures.mjs +13 -10
- package/scripts/build-labels.mjs +199 -194
- package/scripts/build-tips.mjs +95 -23
- package/scripts/corpus-tooling.mjs +268 -0
- package/scripts/render-notebooks.mjs +2 -1
- package/scripts/resolve-book-config.mjs +99 -1
- package/scripts/sync-figure-tokens.mjs +44 -0
- package/scripts/validate.mjs +676 -100
- 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/figure-palette.mjs +162 -0
- package/src/lib/figure.mjs +113 -37
- package/src/lib/patterns.ts +15 -6
- package/src/lib/questions.ts +8 -3
- package/src/types.ts +603 -0
- package/styles/tokens.css +73 -9
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,29 @@ function bookScaffoldIntegration(opts) {
|
|
|
1282
1623
|
),
|
|
1283
1624
|
frontmatter: fmEnabled
|
|
1284
1625
|
};
|
|
1626
|
+
if (corpus) {
|
|
1627
|
+
for (const book of corpus.books) {
|
|
1628
|
+
for (const route of book.apparatus ?? []) {
|
|
1629
|
+
const toggle = CORPUS_APPARATUS_TOGGLE_BY_ROUTE[route];
|
|
1630
|
+
if (!enabledRoutes[toggle]) {
|
|
1631
|
+
throw new BookConfigError(
|
|
1632
|
+
`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.`
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const inheritedCorpusApparatusRoutes = corpus ? CORPUS_APPARATUS_ROUTES.filter(
|
|
1639
|
+
(route) => enabledRoutes[CORPUS_APPARATUS_TOGGLE_BY_ROUTE[route]]
|
|
1640
|
+
) : [];
|
|
1641
|
+
const corpusApparatusRoutes = corpus ? CORPUS_APPARATUS_ROUTES.filter(
|
|
1642
|
+
(route) => corpus.books.some(
|
|
1643
|
+
(book) => (book.apparatus ?? inheritedCorpusApparatusRoutes).includes(route)
|
|
1644
|
+
)
|
|
1645
|
+
) : [];
|
|
1646
|
+
const corpusApparatusToggleKeys = new Set(
|
|
1647
|
+
Object.values(CORPUS_APPARATUS_TOGGLE_BY_ROUTE)
|
|
1648
|
+
);
|
|
1285
1649
|
const integration = {
|
|
1286
1650
|
name: "book-scaffold-astro",
|
|
1287
1651
|
hooks: {
|
|
@@ -1296,18 +1660,34 @@ function bookScaffoldIntegration(opts) {
|
|
|
1296
1660
|
const routesToInject = [];
|
|
1297
1661
|
for (const [name, on] of Object.entries(enabledRoutes)) {
|
|
1298
1662
|
if (!on) continue;
|
|
1663
|
+
if (corpus && corpusApparatusToggleKeys.has(name)) continue;
|
|
1299
1664
|
routesToInject.push(name);
|
|
1300
|
-
if (name === "chapters")
|
|
1665
|
+
if (name === "chapters") {
|
|
1666
|
+
routesToInject.push("chaptersSlug");
|
|
1667
|
+
if (corpus) routesToInject.push("corpusChapters");
|
|
1668
|
+
}
|
|
1669
|
+
if (name === "landing" && corpus) routesToInject.push("corpusBook");
|
|
1301
1670
|
}
|
|
1302
1671
|
for (const name of routesToInject) {
|
|
1303
1672
|
const route = ROUTE_REGISTRY[name];
|
|
1304
1673
|
if (!route) continue;
|
|
1305
|
-
|
|
1674
|
+
let pattern = name === "frontmatter" ? frontmatterPatternFromPrefix(fmPrefix) : route.pattern;
|
|
1675
|
+
if (corpus && name === "chaptersSlug") {
|
|
1676
|
+
pattern = "/chapters/[book]/[...slug]";
|
|
1677
|
+
}
|
|
1306
1678
|
injectRoute({
|
|
1307
1679
|
pattern,
|
|
1308
1680
|
entrypoint: resolvePage(route.file)
|
|
1309
1681
|
});
|
|
1310
1682
|
}
|
|
1683
|
+
if (corpus) {
|
|
1684
|
+
for (const route of corpusApparatusRoutes) {
|
|
1685
|
+
injectRoute({
|
|
1686
|
+
pattern: `/[book]/${route}`,
|
|
1687
|
+
entrypoint: resolvePage(`corpus-apparatus/${route}.astro`)
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1311
1691
|
const consumerRoot = fileURLToPath(config.root);
|
|
1312
1692
|
const resolvedMdxPath = resolveMdxComponentsPath(consumerRoot, mdxComponentsModule);
|
|
1313
1693
|
const resolvedGithubRepo = resolveBookGithubRepo(githubRepo, consumerRoot);
|
|
@@ -1339,8 +1719,9 @@ function bookScaffoldIntegration(opts) {
|
|
|
1339
1719
|
// reproduce the single-book `/chapters/<id>/` behavior exactly.
|
|
1340
1720
|
chapterRoute: chapterRoute ?? "/chapters/:id/",
|
|
1341
1721
|
bookField: bookField ?? "book",
|
|
1342
|
-
apparatusRoute: apparatusRoute ?? "/:route/",
|
|
1343
|
-
apparatusRoutes: apparatusRoutes ?? []
|
|
1722
|
+
apparatusRoute: corpus ? "/:book/:route/" : apparatusRoute ?? "/:route/",
|
|
1723
|
+
apparatusRoutes: corpus ? inheritedCorpusApparatusRoutes : apparatusRoutes ?? [],
|
|
1724
|
+
corpus: corpus ?? null
|
|
1344
1725
|
})
|
|
1345
1726
|
],
|
|
1346
1727
|
define: {
|
|
@@ -1383,8 +1764,11 @@ function bookScaffoldIntegration(opts) {
|
|
|
1383
1764
|
preset: profile,
|
|
1384
1765
|
numberStyle,
|
|
1385
1766
|
siblingBooks: siblingBooks ?? {},
|
|
1386
|
-
|
|
1387
|
-
|
|
1767
|
+
corpus: corpus ?? null,
|
|
1768
|
+
chapterRoute: corpus ? "/chapters/:id/" : chapterRoute ?? "/chapters/:id/",
|
|
1769
|
+
bookField: bookField ?? "book",
|
|
1770
|
+
apparatusRoute: corpus ? "/:book/:route/" : apparatusRoute ?? "/:route/",
|
|
1771
|
+
apparatusRoutes: corpus ? inheritedCorpusApparatusRoutes : apparatusRoutes ?? []
|
|
1388
1772
|
}),
|
|
1389
1773
|
enumerable: false,
|
|
1390
1774
|
configurable: false,
|
|
@@ -1428,12 +1812,38 @@ async function defineBookConfig(opts) {
|
|
|
1428
1812
|
throw v3MigrationError(opts);
|
|
1429
1813
|
}
|
|
1430
1814
|
if (Object.prototype.hasOwnProperty.call(opts, "deploy")) {
|
|
1431
|
-
|
|
1432
|
-
"book-scaffold-astro
|
|
1815
|
+
throw new BookConfigError(
|
|
1816
|
+
"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
1817
|
);
|
|
1434
1818
|
}
|
|
1435
1819
|
const composed = composeStyles(opts.styles ?? []);
|
|
1436
|
-
const
|
|
1820
|
+
const corpus = opts.corpus;
|
|
1821
|
+
if (corpus !== void 0) {
|
|
1822
|
+
assertBookCorpus(corpus);
|
|
1823
|
+
}
|
|
1824
|
+
const profile = resolvePreset(composed.preset ?? corpus?.preset);
|
|
1825
|
+
if (corpus !== void 0) {
|
|
1826
|
+
if (corpus.preset !== profile) {
|
|
1827
|
+
throw new BookConfigError(
|
|
1828
|
+
`defineBookConfig corpus preset ${JSON.stringify(corpus.preset)} does not match the composed Style preset ${JSON.stringify(profile)}. One preset applies to the whole corpus.`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
const incompatible = CORPUS_OWNED_ROUTE_FIELDS.filter(
|
|
1832
|
+
(field) => Object.prototype.hasOwnProperty.call(opts, field)
|
|
1833
|
+
);
|
|
1834
|
+
if (incompatible.length > 0) {
|
|
1835
|
+
throw new BookConfigError(
|
|
1836
|
+
`Corpus mode owns ${incompatible.join(", ")}; remove the explicit ${incompatible.length === 1 ? "override" : "overrides"} so injected routes and navigation agree.`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
const siblingKeys = new Set(Object.keys(opts.siblingBooks ?? {}));
|
|
1840
|
+
const duplicateOwner = corpus.books.find((book) => siblingKeys.has(book.id));
|
|
1841
|
+
if (duplicateOwner) {
|
|
1842
|
+
throw new BookConfigError(
|
|
1843
|
+
`Book id ${JSON.stringify(duplicateOwner.id)} exists in both corpus.books and siblingBooks. A key must be either local or externally deployed, never both.`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1437
1847
|
const numberStyle = opts.numberStyle ?? composed.numberStyle ?? "shared";
|
|
1438
1848
|
if (!NUMBER_STYLES.includes(numberStyle)) {
|
|
1439
1849
|
throw new BookConfigError(
|
|
@@ -1507,6 +1917,7 @@ async function defineBookConfig(opts) {
|
|
|
1507
1917
|
sitemap(sitemapOptions),
|
|
1508
1918
|
bookScaffoldIntegration({
|
|
1509
1919
|
profile,
|
|
1920
|
+
corpus,
|
|
1510
1921
|
numberStyle,
|
|
1511
1922
|
routes: mergedRoutes,
|
|
1512
1923
|
mdxComponentsModule,
|
|
@@ -1538,11 +1949,16 @@ async function defineBookConfig(opts) {
|
|
|
1538
1949
|
siblingBooks: opts.siblingBooks,
|
|
1539
1950
|
// v4.17.0 (#112): per-book exam-domain taxonomy for the questions collection.
|
|
1540
1951
|
examDomains: opts.examDomains,
|
|
1541
|
-
// v4.26.0 (#80): book-aware nav route patterns (undefined → single-book
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1952
|
+
// v4.26.0 (#80): book-aware nav route patterns (undefined → single-book
|
|
1953
|
+
// defaults). Corpus mode owns these fields, so do not manufacture own
|
|
1954
|
+
// properties with undefined values when handing the validated manifest
|
|
1955
|
+
// to the independently exported integration.
|
|
1956
|
+
...corpus ? {} : {
|
|
1957
|
+
chapterRoute: opts.chapterRoute,
|
|
1958
|
+
bookField: opts.bookField,
|
|
1959
|
+
apparatusRoute: opts.apparatusRoute,
|
|
1960
|
+
apparatusRoutes: opts.apparatusRoutes
|
|
1961
|
+
}
|
|
1546
1962
|
}),
|
|
1547
1963
|
...mergedExtraIntegrations
|
|
1548
1964
|
];
|
|
@@ -1569,10 +1985,10 @@ async function defineBookConfig(opts) {
|
|
|
1569
1985
|
};
|
|
1570
1986
|
const {
|
|
1571
1987
|
styles: _styles,
|
|
1988
|
+
corpus: _corpus,
|
|
1572
1989
|
numberStyle: _numberStyle,
|
|
1573
1990
|
site: _site,
|
|
1574
1991
|
routes: _routes,
|
|
1575
|
-
deploy: _deploy,
|
|
1576
1992
|
mdxComponentsModule: _mdxComponentsModule,
|
|
1577
1993
|
extraIntegrations: _extraIntegrations,
|
|
1578
1994
|
extraStyles: _extraStyles,
|
|
@@ -1605,10 +2021,10 @@ async function defineBookConfig(opts) {
|
|
|
1605
2021
|
...rest
|
|
1606
2022
|
} = opts;
|
|
1607
2023
|
void _styles;
|
|
2024
|
+
void _corpus;
|
|
1608
2025
|
void _numberStyle;
|
|
1609
2026
|
void _site;
|
|
1610
2027
|
void _routes;
|
|
1611
|
-
void _deploy;
|
|
1612
2028
|
void _mdxComponentsModule;
|
|
1613
2029
|
void _extraIntegrations;
|
|
1614
2030
|
void _extraStyles;
|
|
@@ -1727,6 +2143,54 @@ function assertEnumProp(value, allowed, ctx) {
|
|
|
1727
2143
|
init_katex_macros();
|
|
1728
2144
|
|
|
1729
2145
|
// src/lib/book-link.ts
|
|
2146
|
+
var CORPUS_BOOK_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2147
|
+
function normalizedBase(baseUrl) {
|
|
2148
|
+
const inner = baseUrl.replace(/^\/+|\/+$/g, "");
|
|
2149
|
+
return inner.length === 0 ? "/" : `/${inner}/`;
|
|
2150
|
+
}
|
|
2151
|
+
function invalidLocalTarget(book, to, reason) {
|
|
2152
|
+
throw new Error(
|
|
2153
|
+
`<BookLink book="${book}" to=${JSON.stringify(to)}>: invalid local corpus target (${reason}).`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
function resolveCorpusBookHref(book, to, baseUrl = "/") {
|
|
2157
|
+
if (!CORPUS_BOOK_ID.test(book)) {
|
|
2158
|
+
throw new Error(`<BookLink book=${JSON.stringify(book)}>: invalid corpus book id.`);
|
|
2159
|
+
}
|
|
2160
|
+
if (typeof to !== "string" || to.trim().length === 0) {
|
|
2161
|
+
invalidLocalTarget(book, String(to), "target must be non-empty");
|
|
2162
|
+
}
|
|
2163
|
+
if (to !== to.trim()) invalidLocalTarget(book, to, "surrounding whitespace is not allowed");
|
|
2164
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(to) || to.startsWith("/") || to.includes("\\")) {
|
|
2165
|
+
invalidLocalTarget(book, to, "absolute URLs and paths are not allowed");
|
|
2166
|
+
}
|
|
2167
|
+
if (/[\u0000-\u001f\u007f]/.test(to)) {
|
|
2168
|
+
invalidLocalTarget(book, to, "control characters are not allowed");
|
|
2169
|
+
}
|
|
2170
|
+
const suffixIndex = [...[to.indexOf("?"), to.indexOf("#")]].filter((index) => index >= 0).reduce((minimum, index) => Math.min(minimum, index), to.length);
|
|
2171
|
+
const path = to.slice(0, suffixIndex);
|
|
2172
|
+
const suffix = to.slice(suffixIndex);
|
|
2173
|
+
if (path.length === 0) invalidLocalTarget(book, to, "query-only and fragment-only targets are not allowed");
|
|
2174
|
+
const segments = path.replace(/\/+$/, "").split("/");
|
|
2175
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
2176
|
+
invalidLocalTarget(book, to, "empty path segments are not allowed");
|
|
2177
|
+
}
|
|
2178
|
+
for (const segment of segments) {
|
|
2179
|
+
let decoded;
|
|
2180
|
+
try {
|
|
2181
|
+
decoded = decodeURIComponent(segment);
|
|
2182
|
+
} catch {
|
|
2183
|
+
invalidLocalTarget(book, to, "malformed percent encoding");
|
|
2184
|
+
}
|
|
2185
|
+
if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
|
|
2186
|
+
invalidLocalTarget(book, to, "path traversal is not allowed");
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
const base = normalizedBase(baseUrl);
|
|
2190
|
+
const local = segments.join("/");
|
|
2191
|
+
const destination = local === "chapters" ? `chapters/${book}` : local.startsWith("chapters/") ? `chapters/${book}/${local.slice("chapters/".length)}` : `${book}/${local}`;
|
|
2192
|
+
return `${base}${destination}/`.replace(/\/{2,}/g, "/") + suffix;
|
|
2193
|
+
}
|
|
1730
2194
|
function entryUrl(entry) {
|
|
1731
2195
|
if (typeof entry === "string") return entry;
|
|
1732
2196
|
return entry?.url;
|
|
@@ -1922,7 +2386,7 @@ function buildExamManifest(entries) {
|
|
|
1922
2386
|
options: (e.data.options ?? []).map((o) => ({ id: o.id, correct: o.correct === true }))
|
|
1923
2387
|
}));
|
|
1924
2388
|
}
|
|
1925
|
-
function deriveDomainRouting(entries, baseUrl = "/") {
|
|
2389
|
+
function deriveDomainRouting(entries, baseUrl = "/", bookId) {
|
|
1926
2390
|
const base = baseUrl.replace(/\/?$/, "/");
|
|
1927
2391
|
const out = {};
|
|
1928
2392
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1933,7 +2397,7 @@ function deriveDomainRouting(entries, baseUrl = "/") {
|
|
|
1933
2397
|
seen.add(key);
|
|
1934
2398
|
(out[e.data.domain] ??= []).push({
|
|
1935
2399
|
label,
|
|
1936
|
-
href: typeof e.data.chapter === "string" ? `${base}chapters/${e.data.chapter}/` : null
|
|
2400
|
+
href: typeof e.data.chapter === "string" ? `${base}chapters/${bookId ? `${bookId}/` : ""}${e.data.chapter}/` : null
|
|
1937
2401
|
});
|
|
1938
2402
|
}
|
|
1939
2403
|
return out;
|
|
@@ -1984,28 +2448,23 @@ function selectPartExercises(chapters, byChapter, part) {
|
|
|
1984
2448
|
// src/styles/built-in.ts
|
|
1985
2449
|
var academicStyle = defineStyle({
|
|
1986
2450
|
name: "academic",
|
|
1987
|
-
preset: "academic"
|
|
1988
|
-
deploy: "workers"
|
|
2451
|
+
preset: "academic"
|
|
1989
2452
|
});
|
|
1990
2453
|
var toolsStyle = defineStyle({
|
|
1991
2454
|
name: "tools",
|
|
1992
|
-
preset: "tools"
|
|
1993
|
-
deploy: "workers"
|
|
2455
|
+
preset: "tools"
|
|
1994
2456
|
});
|
|
1995
2457
|
var minimalStyle = defineStyle({
|
|
1996
2458
|
name: "minimal",
|
|
1997
|
-
preset: "minimal"
|
|
1998
|
-
deploy: "workers"
|
|
2459
|
+
preset: "minimal"
|
|
1999
2460
|
});
|
|
2000
2461
|
var courseNotesStyle = defineStyle({
|
|
2001
2462
|
name: "course-notes",
|
|
2002
|
-
preset: "course-notes"
|
|
2003
|
-
deploy: "pages"
|
|
2463
|
+
preset: "course-notes"
|
|
2004
2464
|
});
|
|
2005
2465
|
var researchPortfolioStyle = defineStyle({
|
|
2006
2466
|
name: "research-portfolio",
|
|
2007
2467
|
preset: "research-portfolio",
|
|
2008
|
-
deploy: "pages",
|
|
2009
2468
|
routes: { frontmatter: { enabled: true, prefix: "frontmatter" } }
|
|
2010
2469
|
});
|
|
2011
2470
|
var BUILTIN_STYLES = {
|
|
@@ -2027,9 +2486,12 @@ export {
|
|
|
2027
2486
|
BRANDON_PORTFOLIO_DEFAULT,
|
|
2028
2487
|
BUILTIN_STYLES,
|
|
2029
2488
|
BookConfigError,
|
|
2489
|
+
CORPUS_APPARATUS_ROUTES,
|
|
2490
|
+
CORPUS_APPARATUS_TOGGLE_BY_ROUTE,
|
|
2030
2491
|
DEFAULT_GITHUB_BRANCH,
|
|
2031
2492
|
KIND_LABEL,
|
|
2032
2493
|
NUMBER_STYLES,
|
|
2494
|
+
RESERVED_CORPUS_BOOK_IDS,
|
|
2033
2495
|
THEOREM_KINDS,
|
|
2034
2496
|
UNKNOWN_PART_ORDINAL,
|
|
2035
2497
|
academicChapterSchema,
|
|
@@ -2040,6 +2502,7 @@ export {
|
|
|
2040
2502
|
academicParts,
|
|
2041
2503
|
academicStyle,
|
|
2042
2504
|
apparatusHref,
|
|
2505
|
+
assertBookCorpus,
|
|
2043
2506
|
assertEnumProp,
|
|
2044
2507
|
assertKnownDomain,
|
|
2045
2508
|
baseNoSlash,
|
|
@@ -2057,9 +2520,15 @@ export {
|
|
|
2057
2520
|
chapterStatus,
|
|
2058
2521
|
citationBackstops,
|
|
2059
2522
|
composeStyles,
|
|
2523
|
+
corpusApparatusRoutesForBook,
|
|
2524
|
+
corpusBookHasApparatusRoute,
|
|
2525
|
+
corpusBookIdFromPath,
|
|
2526
|
+
corpusBookIdOf,
|
|
2527
|
+
corpusCollectionEntryId,
|
|
2060
2528
|
courseNotesChapterSchema,
|
|
2061
2529
|
courseNotesStyle,
|
|
2062
2530
|
defineBookConfig,
|
|
2531
|
+
defineBookCorpus,
|
|
2063
2532
|
defineMdxComponents,
|
|
2064
2533
|
defineProfile,
|
|
2065
2534
|
defineStyle,
|
|
@@ -2068,6 +2537,7 @@ export {
|
|
|
2068
2537
|
deriveObjectiveMap,
|
|
2069
2538
|
distinctChaptersSorted,
|
|
2070
2539
|
fallbackChaptersRenderer,
|
|
2540
|
+
filterCorpusEntries,
|
|
2071
2541
|
freshnessLabel,
|
|
2072
2542
|
getFreshness,
|
|
2073
2543
|
glossarySchema,
|
|
@@ -2075,6 +2545,7 @@ export {
|
|
|
2075
2545
|
groupByDomain,
|
|
2076
2546
|
isCurrentChapter,
|
|
2077
2547
|
layoutModes,
|
|
2548
|
+
localCorpusEntryId,
|
|
2078
2549
|
minimalChapterSchema,
|
|
2079
2550
|
minimalStyle,
|
|
2080
2551
|
normalizeBase,
|
|
@@ -2095,12 +2566,15 @@ export {
|
|
|
2095
2566
|
researchPortfolioChaptersRenderer,
|
|
2096
2567
|
researchPortfolioStyle,
|
|
2097
2568
|
resolveBookHref,
|
|
2569
|
+
resolveCorpusBook,
|
|
2570
|
+
resolveCorpusBookHref,
|
|
2098
2571
|
resolveGithubRepo,
|
|
2099
2572
|
resolvePreset,
|
|
2100
2573
|
resolveProfile,
|
|
2101
2574
|
resolveTheoremNumber,
|
|
2102
2575
|
sampleExam,
|
|
2103
2576
|
scoreExam,
|
|
2577
|
+
selectBookArtifact,
|
|
2104
2578
|
selectPartExercises,
|
|
2105
2579
|
shuffle,
|
|
2106
2580
|
slugOf,
|