@brandon_m_behring/book-scaffold-astro 4.31.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 +37 -2
- package/MIGRATION-v4-to-v5.md +183 -0
- package/README.md +30 -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/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 +4 -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/README.md +2 -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/resolve-book-config.mjs +99 -1
- 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/patterns.ts +15 -6
- package/src/lib/questions.ts +8 -3
- package/src/types.ts +603 -0
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/schemas-entry.ts
|
|
2
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
3
3
|
import { defineCollection } from "astro:content";
|
|
4
4
|
import { glob, file } from "astro/loaders";
|
|
5
5
|
|
|
@@ -790,6 +790,17 @@ var BOOK_PROFILES = Object.keys(PROFILES);
|
|
|
790
790
|
// src/types.ts
|
|
791
791
|
import { existsSync, readFileSync } from "fs";
|
|
792
792
|
var BOOK_PRESETS = BOOK_PROFILES;
|
|
793
|
+
var CORPUS_APPARATUS_ROUTES = [
|
|
794
|
+
"references",
|
|
795
|
+
"print",
|
|
796
|
+
"convergence",
|
|
797
|
+
"tips",
|
|
798
|
+
"exercises",
|
|
799
|
+
"practice-exam",
|
|
800
|
+
"glossary",
|
|
801
|
+
"flashcards",
|
|
802
|
+
"answers"
|
|
803
|
+
];
|
|
793
804
|
var BookConfigError = class extends Error {
|
|
794
805
|
constructor(message) {
|
|
795
806
|
super(message);
|
|
@@ -814,36 +825,230 @@ function readEnvFile(path = ".env") {
|
|
|
814
825
|
return {};
|
|
815
826
|
}
|
|
816
827
|
}
|
|
817
|
-
var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
|
|
818
828
|
function resolvePreset(explicitPreset, explicitProfile) {
|
|
819
829
|
let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
|
|
820
|
-
let source = "default";
|
|
821
|
-
if (explicitPreset || explicitProfile) source = "param";
|
|
822
|
-
else if (process.env.BOOK_PRESET || process.env.BOOK_PROFILE) source = "env";
|
|
823
830
|
if (!candidate) {
|
|
824
831
|
const env = readEnvFile();
|
|
825
832
|
const fromFile = env.BOOK_PRESET ?? env.BOOK_PROFILE;
|
|
826
833
|
if (fromFile) {
|
|
827
834
|
candidate = fromFile;
|
|
828
|
-
source = "dotenv";
|
|
829
835
|
}
|
|
830
836
|
}
|
|
831
|
-
|
|
837
|
+
if (!candidate) {
|
|
838
|
+
throw new BookConfigError(
|
|
839
|
+
`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.`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
832
842
|
if (!BOOK_PRESETS.includes(candidate)) {
|
|
833
843
|
throw new BookConfigError(
|
|
834
844
|
`preset must be one of ${BOOK_PRESETS.join(" | ")} (got ${JSON.stringify(candidate)})`
|
|
835
845
|
);
|
|
836
846
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
847
|
+
return candidate;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/lib/corpus.ts
|
|
851
|
+
var BOOK_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
852
|
+
var BOOK_CORPUS_BRAND = /* @__PURE__ */ Symbol.for(
|
|
853
|
+
"@brandon_m_behring/book-scaffold-astro/BookCorpus/v1"
|
|
854
|
+
);
|
|
855
|
+
var RESERVED_CORPUS_BOOK_IDS = Object.freeze([
|
|
856
|
+
"assets",
|
|
857
|
+
"chapters",
|
|
858
|
+
"search",
|
|
859
|
+
"questions",
|
|
860
|
+
"glossary",
|
|
861
|
+
"frontmatter",
|
|
862
|
+
"_astro",
|
|
863
|
+
"_og",
|
|
864
|
+
"pagefind"
|
|
865
|
+
]);
|
|
866
|
+
var CORPUS_OWNED_ROUTE_FIELDS = Object.freeze([
|
|
867
|
+
"chapterRoute",
|
|
868
|
+
"bookField",
|
|
869
|
+
"apparatusRoute",
|
|
870
|
+
"apparatusRoutes"
|
|
871
|
+
]);
|
|
872
|
+
var CORPUS_APPARATUS_TOGGLE_BY_ROUTE = Object.freeze({
|
|
873
|
+
references: "references",
|
|
874
|
+
print: "print",
|
|
875
|
+
convergence: "convergence",
|
|
876
|
+
tips: "tips",
|
|
877
|
+
exercises: "exercises",
|
|
878
|
+
"practice-exam": "practiceExam",
|
|
879
|
+
glossary: "glossary",
|
|
880
|
+
flashcards: "flashcards",
|
|
881
|
+
answers: "answers"
|
|
882
|
+
});
|
|
883
|
+
var RESERVED = new Set(RESERVED_CORPUS_BOOK_IDS);
|
|
884
|
+
var APPARATUS = new Set(CORPUS_APPARATUS_ROUTES);
|
|
885
|
+
var RESERVED_CONTENT_ROOTS = Object.freeze({
|
|
886
|
+
questions: "src/content/questions/ is the dedicated questions collection root",
|
|
887
|
+
glossary: "src/content/glossary/ is the dedicated glossary collection root",
|
|
888
|
+
frontmatter: "src/content/frontmatter/ is the shared frontmatter collection root"
|
|
889
|
+
});
|
|
890
|
+
var BRANDED_CORPUS_KEYS = /* @__PURE__ */ new Set(["__bookCorpusVersion", "preset", "books"]);
|
|
891
|
+
var BOOK_KEYS = /* @__PURE__ */ new Set([
|
|
892
|
+
"id",
|
|
893
|
+
"title",
|
|
894
|
+
"subtitle",
|
|
895
|
+
"description",
|
|
896
|
+
"author",
|
|
897
|
+
"image",
|
|
898
|
+
"apparatus"
|
|
899
|
+
]);
|
|
900
|
+
function objectRecord(value, label) {
|
|
901
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
902
|
+
throw new BookConfigError(`${label} must be an object.`);
|
|
903
|
+
}
|
|
904
|
+
return value;
|
|
905
|
+
}
|
|
906
|
+
function rejectUnknownKeys(value, allowed, label) {
|
|
907
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
908
|
+
if (unknown.length > 0) {
|
|
909
|
+
throw new BookConfigError(
|
|
910
|
+
`${label} has unknown field${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}.`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function optionalNonBlankString(value, label) {
|
|
915
|
+
if (value === void 0) return void 0;
|
|
916
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
917
|
+
throw new BookConfigError(`${label} must be a non-blank string when provided.`);
|
|
918
|
+
}
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
function validateBook(value, index) {
|
|
922
|
+
const label = `defineBookCorpus books[${index}]`;
|
|
923
|
+
const input = objectRecord(value, label);
|
|
924
|
+
rejectUnknownKeys(input, BOOK_KEYS, label);
|
|
925
|
+
const id = optionalNonBlankString(input.id, `${label}.id`);
|
|
926
|
+
if (id === void 0 || !BOOK_ID.test(id)) {
|
|
927
|
+
throw new BookConfigError(
|
|
928
|
+
`${label}.id must match [a-z0-9]+(?:-[a-z0-9]+)* (got ${JSON.stringify(input.id)}).`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
if (RESERVED.has(id)) {
|
|
932
|
+
const reason = RESERVED_CONTENT_ROOTS[id] ?? "the scaffold owns that route or asset namespace";
|
|
933
|
+
throw new BookConfigError(
|
|
934
|
+
`${label}.id ${JSON.stringify(id)} is reserved because ${reason}; choose a book-specific id.`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const title = optionalNonBlankString(input.title, `${label}.title`);
|
|
938
|
+
if (title === void 0) {
|
|
939
|
+
throw new BookConfigError(`${label}.title must be a non-blank string.`);
|
|
940
|
+
}
|
|
941
|
+
let apparatus;
|
|
942
|
+
if (input.apparatus !== void 0) {
|
|
943
|
+
if (!Array.isArray(input.apparatus)) {
|
|
944
|
+
throw new BookConfigError(`${label}.apparatus must be an array when provided.`);
|
|
945
|
+
}
|
|
946
|
+
const seen = /* @__PURE__ */ new Set();
|
|
947
|
+
const routes = [];
|
|
948
|
+
for (const route of input.apparatus) {
|
|
949
|
+
if (typeof route !== "string" || !APPARATUS.has(route)) {
|
|
950
|
+
throw new BookConfigError(
|
|
951
|
+
`${label}.apparatus contains ${JSON.stringify(route)}; expected a subset of ` + CORPUS_APPARATUS_ROUTES.join(" | ") + "."
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
if (seen.has(route)) {
|
|
955
|
+
throw new BookConfigError(`${label}.apparatus contains duplicate route ${JSON.stringify(route)}.`);
|
|
956
|
+
}
|
|
957
|
+
seen.add(route);
|
|
958
|
+
routes.push(route);
|
|
844
959
|
}
|
|
960
|
+
apparatus = Object.freeze(routes);
|
|
961
|
+
}
|
|
962
|
+
const book = {
|
|
963
|
+
id,
|
|
964
|
+
title,
|
|
965
|
+
...optionalField("subtitle", optionalNonBlankString(input.subtitle, `${label}.subtitle`)),
|
|
966
|
+
...optionalField("description", optionalNonBlankString(input.description, `${label}.description`)),
|
|
967
|
+
...optionalField("author", optionalNonBlankString(input.author, `${label}.author`)),
|
|
968
|
+
...optionalField("image", optionalNonBlankString(input.image, `${label}.image`)),
|
|
969
|
+
...apparatus === void 0 ? {} : { apparatus }
|
|
970
|
+
};
|
|
971
|
+
return Object.freeze(book);
|
|
972
|
+
}
|
|
973
|
+
function optionalField(key, value) {
|
|
974
|
+
return value === void 0 ? {} : { [key]: value };
|
|
975
|
+
}
|
|
976
|
+
function assertBookCorpus(value) {
|
|
977
|
+
const invalid = () => new BookConfigError(
|
|
978
|
+
"corpus must be created by defineBookCorpus() from this book-scaffold major."
|
|
979
|
+
);
|
|
980
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
981
|
+
throw invalid();
|
|
982
|
+
}
|
|
983
|
+
const input = value;
|
|
984
|
+
if (input[BOOK_CORPUS_BRAND] !== true || input.__bookCorpusVersion !== 1 || !Object.isFrozen(value) || !Object.isFrozen(input.books)) {
|
|
985
|
+
throw invalid();
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
rejectUnknownKeys(input, BRANDED_CORPUS_KEYS, "corpus");
|
|
989
|
+
if (typeof input.preset !== "string" || !BOOK_PRESETS.includes(input.preset)) {
|
|
990
|
+
throw invalid();
|
|
991
|
+
}
|
|
992
|
+
if (!Array.isArray(input.books) || input.books.length === 0) throw invalid();
|
|
993
|
+
const seen = /* @__PURE__ */ new Set();
|
|
994
|
+
for (const [index, rawBook] of input.books.entries()) {
|
|
995
|
+
if (!Object.isFrozen(rawBook)) throw invalid();
|
|
996
|
+
const book = validateBook(rawBook, index);
|
|
997
|
+
if (seen.has(book.id)) throw invalid();
|
|
998
|
+
seen.add(book.id);
|
|
999
|
+
if (book.apparatus !== void 0) {
|
|
1000
|
+
const original = rawBook.apparatus;
|
|
1001
|
+
if (!Object.isFrozen(original)) throw invalid();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
if (error instanceof BookConfigError && error.message.startsWith("corpus must be created")) {
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
1008
|
+
throw invalid();
|
|
845
1009
|
}
|
|
846
|
-
|
|
1010
|
+
}
|
|
1011
|
+
function corpusCollectionEntryId(corpus, entry, data, options = {}) {
|
|
1012
|
+
const label = options.label ?? "Content entry";
|
|
1013
|
+
const normalized = entry.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1014
|
+
const [bookId, ...localParts] = normalized.split("/");
|
|
1015
|
+
if (!bookId || !corpus.books.some((book) => book.id === bookId)) {
|
|
1016
|
+
throw new Error(`${label} ${JSON.stringify(entry)} is outside the registered corpus books.`);
|
|
1017
|
+
}
|
|
1018
|
+
if (Object.prototype.hasOwnProperty.call(data, "book") && data.book !== bookId) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`${label} ${JSON.stringify(entry)} has frontmatter book ${JSON.stringify(data.book)}, but its registered path owner is ${JSON.stringify(bookId)}.`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const fileId = localParts.join("/").replace(/\.(?:md|mdx)$/i, "");
|
|
1024
|
+
const configured = options.slugField ? data[options.slugField] : void 0;
|
|
1025
|
+
const localId = typeof configured === "string" ? configured : fileId;
|
|
1026
|
+
const localSegments = localId.split("/");
|
|
1027
|
+
let hasInvalidEncodedSegment = false;
|
|
1028
|
+
for (const segment of localSegments) {
|
|
1029
|
+
try {
|
|
1030
|
+
const decoded = decodeURIComponent(segment);
|
|
1031
|
+
if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
|
|
1032
|
+
hasInvalidEncodedSegment = true;
|
|
1033
|
+
}
|
|
1034
|
+
} catch {
|
|
1035
|
+
hasInvalidEncodedSegment = true;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (localId.length === 0 || localId.startsWith("/") || localId.endsWith("/") || localId.includes("\\") || localSegments.some((part) => part === "." || part === ".." || part.length === 0) || hasInvalidEncodedSegment) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
`${label} ${JSON.stringify(entry)} resolved invalid corpus id ${JSON.stringify(localId)}.`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
return `${bookId}/${localId}`;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/lib/corpus-collateral.ts
|
|
1047
|
+
function corpusPatternsCollection(bookId) {
|
|
1048
|
+
return `corpus-patterns-${bookId}`;
|
|
1049
|
+
}
|
|
1050
|
+
function corpusChangelogCollection(bookId) {
|
|
1051
|
+
return `corpus-changelog-${bookId}`;
|
|
847
1052
|
}
|
|
848
1053
|
|
|
849
1054
|
// src/schemas-entry.ts
|
|
@@ -856,6 +1061,30 @@ function isYamlEmpty(path) {
|
|
|
856
1061
|
return false;
|
|
857
1062
|
}
|
|
858
1063
|
}
|
|
1064
|
+
function assertCorpusConvergenceLayout(books) {
|
|
1065
|
+
const registered = new Set(books.map((book) => book.id));
|
|
1066
|
+
const legacy = existsSync2("./changelog/patterns.yaml") ? ["./changelog/patterns.yaml"] : [];
|
|
1067
|
+
if (existsSync2("./changelog/tools") && readdirSync("./changelog/tools", { withFileTypes: true }).some(
|
|
1068
|
+
(entry) => entry.isFile() && entry.name.endsWith(".yaml") && !(registered.has("tools") && entry.name === "patterns.yaml")
|
|
1069
|
+
)) {
|
|
1070
|
+
legacy.push("./changelog/tools/*.yaml");
|
|
1071
|
+
}
|
|
1072
|
+
if (legacy.length > 0) {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
`Corpus convergence collateral must be book-owned; move ${legacy.join(" and ")} to changelog/<book>/patterns.yaml and changelog/<book>/tools/.`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
if (!existsSync2("./changelog")) return;
|
|
1078
|
+
for (const entry of readdirSync("./changelog", { withFileTypes: true })) {
|
|
1079
|
+
if (!entry.isDirectory() || registered.has(entry.name)) continue;
|
|
1080
|
+
const base = `./changelog/${entry.name}`;
|
|
1081
|
+
if (existsSync2(`${base}/patterns.yaml`) || existsSync2(`${base}/tools`)) {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
`Corpus convergence collateral owner ${JSON.stringify(entry.name)} is not a registered book. Known books: ${books.map((book) => book.id).join(", ")}.`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
859
1088
|
function frontmatterCollection(schema, base = "./src/content/frontmatter") {
|
|
860
1089
|
return defineCollection({
|
|
861
1090
|
loader: glob({
|
|
@@ -866,14 +1095,33 @@ function frontmatterCollection(schema, base = "./src/content/frontmatter") {
|
|
|
866
1095
|
});
|
|
867
1096
|
}
|
|
868
1097
|
function defineBookSchemas(opts = {}) {
|
|
869
|
-
const
|
|
870
|
-
|
|
1098
|
+
const corpus = opts.corpus;
|
|
1099
|
+
if (corpus !== void 0) assertBookCorpus(corpus);
|
|
1100
|
+
const explicitProfile = opts.preset ?? opts.profile;
|
|
1101
|
+
if (corpus && explicitProfile !== void 0 && explicitProfile !== corpus.preset) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
`defineBookSchemas preset ${JSON.stringify(explicitProfile)} does not match corpus preset ${JSON.stringify(corpus.preset)}.`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const profile = corpus?.preset ?? resolvePreset(opts.preset, opts.profile);
|
|
1107
|
+
const chaptersBase = opts.chaptersBase ?? (corpus ? "./src/content" : "./src/content/chapters");
|
|
871
1108
|
const schemaForProfile = profile === "academic" ? academicChapterSchema : profile === "course-notes" ? courseNotesChapterSchema : profile === "research-portfolio" ? researchPortfolioChapterSchema : profile === "minimal" ? minimalChapterSchema : toolsChapterSchema;
|
|
872
1109
|
const chapters = defineCollection({
|
|
873
1110
|
loader: glob({
|
|
874
1111
|
// Exclude underscore-prefixed files (standard "hidden" convention).
|
|
875
|
-
pattern: [
|
|
876
|
-
|
|
1112
|
+
pattern: corpus ? [
|
|
1113
|
+
...corpus.books.map((book) => `${book.id}/**/*.{md,mdx}`),
|
|
1114
|
+
"!**/_*"
|
|
1115
|
+
] : ["**/*.{md,mdx}", "!**/_*"],
|
|
1116
|
+
base: chaptersBase,
|
|
1117
|
+
...corpus ? {
|
|
1118
|
+
generateId: ({ entry, data }) => {
|
|
1119
|
+
return corpusCollectionEntryId(corpus, entry, data, {
|
|
1120
|
+
label: "Chapter",
|
|
1121
|
+
slugField: "slug"
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
} : {}
|
|
877
1125
|
}),
|
|
878
1126
|
schema: schemaForProfile
|
|
879
1127
|
});
|
|
@@ -886,23 +1134,49 @@ function defineBookSchemas(opts = {}) {
|
|
|
886
1134
|
schema: sourcesSchema
|
|
887
1135
|
});
|
|
888
1136
|
}
|
|
889
|
-
if (
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1137
|
+
if (corpus) {
|
|
1138
|
+
assertCorpusConvergenceLayout(corpus.books);
|
|
1139
|
+
for (const book of corpus.books) {
|
|
1140
|
+
const changelogBase = `./changelog/${book.id}/tools`;
|
|
1141
|
+
if (existsSync2(changelogBase)) {
|
|
1142
|
+
collections[corpusChangelogCollection(book.id)] = defineCollection({
|
|
1143
|
+
loader: glob({ pattern: "*.yaml", base: changelogBase }),
|
|
1144
|
+
schema: changelogSchema
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
const patternsPath = `changelog/${book.id}/patterns.yaml`;
|
|
1148
|
+
if (existsSync2(`./${patternsPath}`)) {
|
|
1149
|
+
collections[corpusPatternsCollection(book.id)] = defineCollection({
|
|
1150
|
+
loader: file(patternsPath),
|
|
1151
|
+
schema: patternsSchema
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
} else {
|
|
1156
|
+
if (existsSync2("./changelog/tools")) {
|
|
1157
|
+
collections.changelog = defineCollection({
|
|
1158
|
+
loader: glob({ pattern: "*.yaml", base: "./changelog/tools" }),
|
|
1159
|
+
schema: changelogSchema
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
if (existsSync2("./changelog/patterns.yaml")) {
|
|
1163
|
+
collections.patterns = defineCollection({
|
|
1164
|
+
loader: file("changelog/patterns.yaml"),
|
|
1165
|
+
schema: patternsSchema
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
900
1168
|
}
|
|
901
1169
|
if (existsSync2("./src/content/questions")) {
|
|
902
1170
|
collections.questions = defineCollection({
|
|
903
1171
|
loader: glob({
|
|
1172
|
+
// In corpus mode this intentionally scans the entire dedicated root.
|
|
1173
|
+
// `generateId` is the ownership boundary: a flat file or an unknown
|
|
1174
|
+
// first directory must fail instead of disappearing from the loader.
|
|
904
1175
|
pattern: ["**/*.{md,mdx}", "!**/_*"],
|
|
905
|
-
base: "./src/content/questions"
|
|
1176
|
+
base: "./src/content/questions",
|
|
1177
|
+
...corpus ? {
|
|
1178
|
+
generateId: ({ entry, data }) => corpusCollectionEntryId(corpus, entry, data, { label: "Question" })
|
|
1179
|
+
} : {}
|
|
906
1180
|
}),
|
|
907
1181
|
schema: refinedQuestionSchema
|
|
908
1182
|
});
|
|
@@ -910,8 +1184,13 @@ function defineBookSchemas(opts = {}) {
|
|
|
910
1184
|
if (existsSync2("./src/content/glossary")) {
|
|
911
1185
|
collections.glossary = defineCollection({
|
|
912
1186
|
loader: glob({
|
|
1187
|
+
// As with questions, discover all non-hidden entries so malformed
|
|
1188
|
+
// corpus ownership is rejected rather than silently ignored.
|
|
913
1189
|
pattern: ["**/*.{md,mdx}", "!**/_*"],
|
|
914
|
-
base: "./src/content/glossary"
|
|
1190
|
+
base: "./src/content/glossary",
|
|
1191
|
+
...corpus ? {
|
|
1192
|
+
generateId: ({ entry, data }) => corpusCollectionEntryId(corpus, entry, data, { label: "Glossary term" })
|
|
1193
|
+
} : {}
|
|
915
1194
|
}),
|
|
916
1195
|
schema: glossarySchema
|
|
917
1196
|
});
|
|
@@ -302,8 +302,8 @@ type ChapterFor<P extends BookProfile> = P extends 'academic' ? AcademicChapter
|
|
|
302
302
|
* - Version marker (`__styleVersion`) — future API-shape changes can
|
|
303
303
|
* be detected at composition time without breaking existing styles.
|
|
304
304
|
*
|
|
305
|
-
* See `recipes/15-defining-styles.md` for usage patterns
|
|
306
|
-
* for
|
|
305
|
+
* See `recipes/15-defining-styles.md` for usage patterns and the migration
|
|
306
|
+
* guides for version-specific breaking changes.
|
|
307
307
|
*/
|
|
308
308
|
|
|
309
309
|
/** Internal brand symbol — prevents confusion between Style and plain config objects.
|
|
@@ -376,11 +376,6 @@ interface Style {
|
|
|
376
376
|
* `remarkPlugins` and `rehypePlugins` arrays concat across the chain;
|
|
377
377
|
* scalar fields override. */
|
|
378
378
|
readonly markdown?: AstroUserConfig['markdown'];
|
|
379
|
-
/** Reserved legacy metadata. It is composed but has no runtime or
|
|
380
|
-
* create-book effect: create-book chooses wrangler.toml from its CLI
|
|
381
|
-
* preset before a consumer Style exists (#180).
|
|
382
|
-
* @deprecated Inert in v4; scheduled for removal in v5. */
|
|
383
|
-
readonly deploy?: 'pages' | 'workers';
|
|
384
379
|
/**
|
|
385
380
|
* v4.26.2 (#149; style inheritance fixed in v4.26.3): release-state
|
|
386
381
|
* banner. Shallow override (last defined wins); `false` suppresses a
|
|
@@ -417,7 +412,6 @@ type StyleInput = Omit<Style, typeof StyleBrand | '__styleVersion'>;
|
|
|
417
412
|
* preset: 'research-portfolio',
|
|
418
413
|
* site: 'https://guides.brandon-behring.dev/',
|
|
419
414
|
* routes: { frontmatter: { enabled: true, prefix: '' } },
|
|
420
|
-
* deploy: 'pages',
|
|
421
415
|
* });
|
|
422
416
|
*
|
|
423
417
|
* // guides/foo/astro.config.mjs
|
|
@@ -447,7 +441,7 @@ declare function defineStyle(opts: StyleInput): Style;
|
|
|
447
441
|
* - Top-level `defineBookConfig` fields beat any style (handled in config.ts)
|
|
448
442
|
*
|
|
449
443
|
* Per-key merge strategy:
|
|
450
|
-
* - `preset`, `numberStyle`, `site`, `
|
|
444
|
+
* - `preset`, `numberStyle`, `site`, `mdxComponentsModule`, `name`, `releaseStatus`
|
|
451
445
|
* → shallow override (last defined wins; `releaseStatus: false` suppresses)
|
|
452
446
|
* - `routes` → per-route spread (each route key independently overridable)
|
|
453
447
|
* - `katexMacros` → per-macro spread (each macro key independently overridable)
|
|
@@ -487,6 +481,44 @@ declare const BOOK_PRESETS: readonly ("academic" | "tools" | "minimal" | "course
|
|
|
487
481
|
/** Theorem-family numbering strategy used by build-labels and validate. */
|
|
488
482
|
type NumberStyle = 'shared' | 'per-kind';
|
|
489
483
|
declare const NUMBER_STYLES: readonly ["shared", "per-kind"];
|
|
484
|
+
/** Scaffold-owned book routes that may be enabled per corpus book (#80). */
|
|
485
|
+
declare const CORPUS_APPARATUS_ROUTES: readonly ["references", "print", "convergence", "tips", "exercises", "practice-exam", "glossary", "flashcards", "answers"];
|
|
486
|
+
type CorpusApparatusRoute = (typeof CORPUS_APPARATUS_ROUTES)[number];
|
|
487
|
+
/** Consumer-authored metadata for one book in a corpus manifest. */
|
|
488
|
+
interface CorpusBookInput {
|
|
489
|
+
id: string;
|
|
490
|
+
title: string;
|
|
491
|
+
subtitle?: string;
|
|
492
|
+
description?: string;
|
|
493
|
+
author?: string;
|
|
494
|
+
image?: string;
|
|
495
|
+
apparatus?: readonly CorpusApparatusRoute[];
|
|
496
|
+
}
|
|
497
|
+
/** Frozen, validated book metadata returned by `defineBookCorpus`. */
|
|
498
|
+
interface CorpusBook extends Omit<CorpusBookInput, 'apparatus'> {
|
|
499
|
+
readonly id: string;
|
|
500
|
+
readonly title: string;
|
|
501
|
+
readonly subtitle?: string;
|
|
502
|
+
readonly description?: string;
|
|
503
|
+
readonly author?: string;
|
|
504
|
+
readonly image?: string;
|
|
505
|
+
readonly apparatus?: readonly CorpusApparatusRoute[];
|
|
506
|
+
}
|
|
507
|
+
/** Input accepted by `defineBookCorpus`. One preset applies to the whole app. */
|
|
508
|
+
interface BookCorpusInput {
|
|
509
|
+
preset: BookPreset;
|
|
510
|
+
books: readonly CorpusBookInput[];
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Branded, deeply frozen corpus registry shared by Astro and content config.
|
|
514
|
+
* The string version marker is intentionally serializable through Vite's
|
|
515
|
+
* virtual module and inspectable by plain-Node CLI tooling.
|
|
516
|
+
*/
|
|
517
|
+
interface BookCorpus {
|
|
518
|
+
readonly __bookCorpusVersion: 1;
|
|
519
|
+
readonly preset: BookPreset;
|
|
520
|
+
readonly books: readonly CorpusBook[];
|
|
521
|
+
}
|
|
490
522
|
/**
|
|
491
523
|
* A sibling book with an optional vendored label index for cross-book
|
|
492
524
|
* `<BookLink>` validation (#147).
|
|
@@ -573,6 +605,12 @@ interface BookConfigOptions {
|
|
|
573
605
|
* });
|
|
574
606
|
*/
|
|
575
607
|
styles?: readonly Style[];
|
|
608
|
+
/**
|
|
609
|
+
* v5.0.0 (#80): opt into one-app/one-build corpus mode. Define the value
|
|
610
|
+
* once with `defineBookCorpus` and pass the same object to
|
|
611
|
+
* `defineBookSchemas({ corpus })`.
|
|
612
|
+
*/
|
|
613
|
+
corpus?: BookCorpus;
|
|
576
614
|
/**
|
|
577
615
|
* v4.27.0 (#175): theorem-family counter strategy. `shared` preserves the
|
|
578
616
|
* amsthm-style sequence used through v4.26; `per-kind` gives theorem,
|
|
@@ -593,16 +631,6 @@ interface BookConfigOptions {
|
|
|
593
631
|
* Closes #3 (v3.3.0).
|
|
594
632
|
*/
|
|
595
633
|
routes?: PartialRouteToggles;
|
|
596
|
-
/**
|
|
597
|
-
* v4.0.0 (#50): RESERVED — accepted and style-chain-merged, but currently
|
|
598
|
-
* has NO runtime effect (#180). The wrangler.toml shape is decided once, at
|
|
599
|
-
* scaffold time, by `create-book` from the profile name (academic/tools/
|
|
600
|
-
* minimal → Workers; course-notes/research-portfolio → Pages); setting this
|
|
601
|
-
* field changes nothing afterward. Wire-or-remove is a v5 decision.
|
|
602
|
-
* @deprecated Inert in v4; remove this option before v5. Deployment shape
|
|
603
|
-
* is chosen by create-book or by the consumer's own deployment files.
|
|
604
|
-
*/
|
|
605
|
-
deploy?: 'pages' | 'workers';
|
|
606
634
|
/**
|
|
607
635
|
* Optional explicit path to the consumer's MDX-components map (relative
|
|
608
636
|
* to project root). When omitted, the toolkit auto-detects one of
|
|
@@ -819,12 +847,16 @@ interface BookSchemasOptions {
|
|
|
819
847
|
preset?: BookPreset;
|
|
820
848
|
/** Backward-compat alias for `preset`. */
|
|
821
849
|
profile?: BookProfile;
|
|
822
|
-
/** Defaults to `'./src/content/chapters'
|
|
850
|
+
/** Defaults to `'./src/content/chapters'`, or `'./src/content'` in corpus mode. */
|
|
823
851
|
chaptersBase?: string;
|
|
852
|
+
/** v5.0.0 (#80): the same branded manifest passed to `defineBookConfig`. */
|
|
853
|
+
corpus?: BookCorpus;
|
|
824
854
|
}
|
|
825
855
|
/** Options for the internal `bookScaffoldIntegration`. See PACKAGE_DESIGN.md §6. */
|
|
826
856
|
interface BookScaffoldIntegrationOptions {
|
|
827
857
|
profile: BookProfile;
|
|
858
|
+
/** v5.0.0 (#80): resolved one-app corpus manifest. */
|
|
859
|
+
corpus?: BookCorpus;
|
|
828
860
|
/** Resolved theorem-family counter strategy exposed to package CLI tooling. */
|
|
829
861
|
numberStyle?: NumberStyle;
|
|
830
862
|
/**
|
|
@@ -903,6 +935,26 @@ interface BookScaffoldIntegrationOptions {
|
|
|
903
935
|
declare class BookConfigError extends Error {
|
|
904
936
|
constructor(message: string);
|
|
905
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Resolve a required preset from explicit args → env → .env.
|
|
940
|
+
* Throws when the selected value is invalid or every source is absent.
|
|
941
|
+
*
|
|
942
|
+
* v3.4.0 (closes #9): canonical resolver. Accepts both `preset` and `profile`
|
|
943
|
+
* (back-compat) explicit args; reads both `BOOK_PRESET` (preferred) and
|
|
944
|
+
* `BOOK_PROFILE` (alias) env vars; same for .env file lookups.
|
|
945
|
+
*
|
|
946
|
+
* Resolution order:
|
|
947
|
+
* 1. explicitPreset (from defineBookConfig({ preset: ... }))
|
|
948
|
+
* 2. explicitProfile (from defineBookConfig({ profile: ... }))
|
|
949
|
+
* 3. process.env.BOOK_PRESET
|
|
950
|
+
* 4. process.env.BOOK_PROFILE
|
|
951
|
+
* 5. .env BOOK_PRESET
|
|
952
|
+
* 6. .env BOOK_PROFILE
|
|
953
|
+
*
|
|
954
|
+
* v5.0.0 (#212): there is deliberately no implicit `minimal` fallback. A
|
|
955
|
+
* missing preset can otherwise make CI validate and render against a schema
|
|
956
|
+
* that the author never selected.
|
|
957
|
+
*/
|
|
906
958
|
declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: BookProfile): BookPreset;
|
|
907
959
|
/**
|
|
908
960
|
* Backward-compat alias. New code should use `resolvePreset()`.
|
|
@@ -911,4 +963,4 @@ declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: Bo
|
|
|
911
963
|
*/
|
|
912
964
|
declare function resolveProfile(explicit?: BookProfile): BookProfile;
|
|
913
965
|
|
|
914
|
-
export { BOOK_PRESETS as B,
|
|
966
|
+
export { defineProfile as A, BOOK_PRESETS as B, CORPUS_APPARATUS_ROUTES as C, defineStyle as D, normalizeFrontmatterConfig as E, type FreshnessAffordance as F, resolvePreset as G, resolveProfile as H, NUMBER_STYLES as N, type PartKey as P, type ReleaseStatusConfig as R, type SecurityHeadersConfig as S, type VolatilityBadge as V, BOOK_PROFILES as a, BookConfigError as b, type BookConfigOptions as c, type BookCorpus as d, type BookCorpusInput as e, type BookPreset as f, type BookProfile as g, type BookScaffoldIntegrationOptions as h, type BookSchemasOptions as i, type ChapterFor as j, type ChaptersRenderer as k, type CorpusApparatusRoute as l, type CorpusBook as m, type CorpusBookInput as n, type FrontmatterRouteConfig as o, type NumberStyle as p, type PartialRouteToggles as q, type ProfileDefinition as r, type RouteToggles as s, type SiblingBookDescriptor as t, type SiblingBookEntry as u, type SiblingBooks as v, type StatusBadge as w, type Style as x, type StyleInput as y, composeStyles as z };
|