@brandon_m_behring/book-scaffold-astro 4.26.3 → 4.27.1
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/AGENTS.md +6 -0
- package/CLAUDE.md +35 -10
- package/LATEX_TO_MDX_MAPPING.md +1 -1
- package/LICENSE +27 -0
- package/LICENSE-CONTENT +19 -0
- package/README.md +33 -0
- package/components/AssessmentTest.astro +2 -1
- package/components/ChapterNav.astro +2 -2
- package/components/Cite.astro +2 -1
- package/components/NavContent.astro +2 -2
- package/components/PartReview.astro +2 -1
- package/components/Rationale.astro +3 -2
- package/components/Sidebar.astro +2 -1
- package/components/Term.astro +2 -1
- package/components/TipsCard.astro +2 -1
- package/components/VersionSelector.tsx +39 -36
- package/components/WeekRef.astro +2 -1
- package/components/XRef.astro +2 -1
- package/dist/components/VersionSelector.d.ts +16 -2
- package/dist/components/VersionSelector.mjs +18 -17
- package/dist/index.d.ts +24 -6
- package/dist/index.mjs +105 -5
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +8 -1
- package/dist/{types-CWXP1S4b.d.ts → types-CZrkqzpC.d.ts} +58 -26
- package/layouts/Base.astro +30 -19
- package/package.json +7 -4
- package/pages/answers.astro +2 -1
- package/pages/chapters.astro +2 -1
- package/pages/exercises.astro +2 -1
- package/pages/index.astro +4 -3
- package/pages/search.astro +2 -1
- package/pages/tips.astro +2 -1
- package/recipes/01-add-math.md +40 -28
- package/recipes/04-component-library.md +14 -4
- package/recipes/05-deploy-cloudflare.md +44 -0
- package/recipes/06-mobile-first-layout.md +25 -2
- package/recipes/09-validation.md +18 -9
- package/recipes/15-defining-styles.md +5 -2
- package/recipes/19-prevalidate-hook.md +29 -83
- package/scripts/build-bib.mjs +7 -3
- package/scripts/build-labels.mjs +33 -16
- package/scripts/read-env.mjs +25 -0
- package/scripts/resolve-book-config.mjs +96 -0
- package/scripts/validate.mjs +125 -84
- package/src/lib/define-style.ts +15 -6
- package/src/lib/nav-href.ts +24 -2
- package/styles/layout.css +14 -8
- package/styles/tokens.css +8 -10
package/dist/index.mjs
CHANGED
|
@@ -912,6 +912,7 @@ var BOOK_PROFILES = Object.keys(PROFILES);
|
|
|
912
912
|
// src/types.ts
|
|
913
913
|
import { existsSync, readFileSync } from "fs";
|
|
914
914
|
var BOOK_PRESETS = BOOK_PROFILES;
|
|
915
|
+
var NUMBER_STYLES = ["shared", "per-kind"];
|
|
915
916
|
var BookConfigError = class extends Error {
|
|
916
917
|
constructor(message) {
|
|
917
918
|
super(message);
|
|
@@ -936,6 +937,7 @@ function readEnvFile(path = ".env") {
|
|
|
936
937
|
return {};
|
|
937
938
|
}
|
|
938
939
|
}
|
|
940
|
+
var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
|
|
939
941
|
function resolvePreset(explicitPreset, explicitProfile) {
|
|
940
942
|
let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
|
|
941
943
|
let source = "default";
|
|
@@ -956,7 +958,13 @@ function resolvePreset(explicitPreset, explicitProfile) {
|
|
|
956
958
|
);
|
|
957
959
|
}
|
|
958
960
|
if (source === "default") {
|
|
959
|
-
|
|
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
|
+
}
|
|
960
968
|
}
|
|
961
969
|
return candidate;
|
|
962
970
|
}
|
|
@@ -966,7 +974,7 @@ function resolveProfile(explicit) {
|
|
|
966
974
|
|
|
967
975
|
// src/integration.ts
|
|
968
976
|
import { fileURLToPath } from "url";
|
|
969
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
977
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
970
978
|
import { join } from "path";
|
|
971
979
|
|
|
972
980
|
// src/lib/define-style.ts
|
|
@@ -981,6 +989,7 @@ function composeStyles(styles) {
|
|
|
981
989
|
for (const style of styles) {
|
|
982
990
|
if (style.name !== void 0) merged.name = style.name;
|
|
983
991
|
if (style.preset !== void 0) merged.preset = style.preset;
|
|
992
|
+
if (style.numberStyle !== void 0) merged.numberStyle = style.numberStyle;
|
|
984
993
|
if (style.site !== void 0) merged.site = style.site;
|
|
985
994
|
if (style.deploy !== void 0) merged.deploy = style.deploy;
|
|
986
995
|
if (style.releaseStatus !== void 0) {
|
|
@@ -1121,6 +1130,21 @@ function defineMdxComponents(components) {
|
|
|
1121
1130
|
// src/integration.ts
|
|
1122
1131
|
var BOOK_CONFIG_VIRTUAL_ID = "virtual:book-scaffold/book-config";
|
|
1123
1132
|
var BOOK_CONFIG_RESOLVED_ID = "\0" + BOOK_CONFIG_VIRTUAL_ID;
|
|
1133
|
+
function makeRobotoFontDisplayVitePlugin() {
|
|
1134
|
+
return {
|
|
1135
|
+
name: "book-scaffold:roboto-font-display",
|
|
1136
|
+
enforce: "pre",
|
|
1137
|
+
transform(code, id) {
|
|
1138
|
+
const [path = id] = id.split("?");
|
|
1139
|
+
const normalizedPath = path.replaceAll("\\", "/");
|
|
1140
|
+
if (!normalizedPath.endsWith("/@fontsource-variable/roboto/index.css")) {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
const transformed = code.replace(/font-display:\s*swap/g, "font-display: optional");
|
|
1144
|
+
return transformed === code ? null : { code: transformed, map: null };
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1124
1148
|
function makeBookConfigVitePlugin(config) {
|
|
1125
1149
|
const serialized = `export default ${JSON.stringify(config)};`;
|
|
1126
1150
|
return {
|
|
@@ -1197,6 +1221,17 @@ var ROUTE_REGISTRY = {
|
|
|
1197
1221
|
// empty string → '/[slug]'; arbitrary string → '/<prefix>/[slug]').
|
|
1198
1222
|
frontmatter: { pattern: "/frontmatter/[slug]", file: "frontmatter/[...slug].astro" }
|
|
1199
1223
|
};
|
|
1224
|
+
var DEFAULT_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'";
|
|
1225
|
+
function renderSecurityHeaders(contentSecurityPolicy) {
|
|
1226
|
+
const csp = contentSecurityPolicy ?? DEFAULT_CONTENT_SECURITY_POLICY;
|
|
1227
|
+
return `/*
|
|
1228
|
+
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
|
1229
|
+
X-Content-Type-Options: nosniff
|
|
1230
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
1231
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
1232
|
+
Content-Security-Policy: ${csp}
|
|
1233
|
+
`;
|
|
1234
|
+
}
|
|
1200
1235
|
function frontmatterPatternFromPrefix(prefix) {
|
|
1201
1236
|
if (prefix === void 0) return ROUTE_REGISTRY.frontmatter.pattern;
|
|
1202
1237
|
if (prefix === "") return "/[slug]";
|
|
@@ -1208,6 +1243,7 @@ function resolvePage(file) {
|
|
|
1208
1243
|
function bookScaffoldIntegration(opts) {
|
|
1209
1244
|
const {
|
|
1210
1245
|
profile,
|
|
1246
|
+
numberStyle = "shared",
|
|
1211
1247
|
routes: userOverrides = {},
|
|
1212
1248
|
extraStyles = [],
|
|
1213
1249
|
mdxComponentsModule,
|
|
@@ -1215,6 +1251,7 @@ function bookScaffoldIntegration(opts) {
|
|
|
1215
1251
|
title,
|
|
1216
1252
|
subtitle,
|
|
1217
1253
|
releaseStatus,
|
|
1254
|
+
securityHeaders,
|
|
1218
1255
|
description,
|
|
1219
1256
|
portfolio,
|
|
1220
1257
|
// v4.6.0: book-level author + SEO config, propagated through the
|
|
@@ -1245,7 +1282,7 @@ function bookScaffoldIntegration(opts) {
|
|
|
1245
1282
|
),
|
|
1246
1283
|
frontmatter: fmEnabled
|
|
1247
1284
|
};
|
|
1248
|
-
|
|
1285
|
+
const integration = {
|
|
1249
1286
|
name: "book-scaffold-astro",
|
|
1250
1287
|
hooks: {
|
|
1251
1288
|
"astro:config:setup": ({ injectScript, injectRoute, updateConfig, config }) => {
|
|
@@ -1280,6 +1317,7 @@ function bookScaffoldIntegration(opts) {
|
|
|
1280
1317
|
updateConfig({
|
|
1281
1318
|
vite: {
|
|
1282
1319
|
plugins: [
|
|
1320
|
+
makeRobotoFontDisplayVitePlugin(),
|
|
1283
1321
|
makeMdxComponentsVitePlugin(resolvedMdxPath),
|
|
1284
1322
|
makeBookConfigVitePlugin({
|
|
1285
1323
|
title: title ?? null,
|
|
@@ -1315,9 +1353,38 @@ function bookScaffoldIntegration(opts) {
|
|
|
1315
1353
|
}
|
|
1316
1354
|
}
|
|
1317
1355
|
});
|
|
1356
|
+
},
|
|
1357
|
+
// v4.27.0 (#188): emit defaults for every build, not only newly
|
|
1358
|
+
// scaffolded projects, so existing consumers become protected on their
|
|
1359
|
+
// next package upgrade. Astro copies public/_headers into the output
|
|
1360
|
+
// before build:done; an existing target therefore means the consumer
|
|
1361
|
+
// owns the complete file and must win byte-for-byte.
|
|
1362
|
+
"astro:build:done": ({ dir, logger }) => {
|
|
1363
|
+
if (securityHeaders === false) {
|
|
1364
|
+
logger.info("security-header emission disabled by defineBookConfig (#188)");
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const target = join(fileURLToPath(dir), "_headers");
|
|
1368
|
+
if (existsSync3(target)) {
|
|
1369
|
+
logger.info("consumer public/_headers present; scaffold defaults skipped (#188)");
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
writeFileSync(
|
|
1373
|
+
target,
|
|
1374
|
+
renderSecurityHeaders(securityHeaders?.contentSecurityPolicy),
|
|
1375
|
+
"utf8"
|
|
1376
|
+
);
|
|
1377
|
+
logger.info("emitted default security headers; public/_headers overrides them (#188)");
|
|
1318
1378
|
}
|
|
1319
1379
|
}
|
|
1320
1380
|
};
|
|
1381
|
+
Object.defineProperty(integration, "__bookScaffoldResolvedConfig", {
|
|
1382
|
+
value: Object.freeze({ preset: profile, numberStyle }),
|
|
1383
|
+
enumerable: false,
|
|
1384
|
+
configurable: false,
|
|
1385
|
+
writable: false
|
|
1386
|
+
});
|
|
1387
|
+
return integration;
|
|
1321
1388
|
}
|
|
1322
1389
|
|
|
1323
1390
|
// src/config.ts
|
|
@@ -1354,8 +1421,19 @@ async function defineBookConfig(opts) {
|
|
|
1354
1421
|
if ("preset" in opts || "profile" in opts) {
|
|
1355
1422
|
throw v3MigrationError(opts);
|
|
1356
1423
|
}
|
|
1424
|
+
if (Object.prototype.hasOwnProperty.call(opts, "deploy")) {
|
|
1425
|
+
console.warn(
|
|
1426
|
+
"book-scaffold-astro: defineBookConfig({ deploy }) is inert and deprecated; create-book chooses wrangler.toml from its CLI preset. Remove this field before upgrading to v5 (#180)."
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1357
1429
|
const composed = composeStyles(opts.styles ?? []);
|
|
1358
|
-
const profile =
|
|
1430
|
+
const profile = resolvePreset(composed.preset);
|
|
1431
|
+
const numberStyle = opts.numberStyle ?? composed.numberStyle ?? "shared";
|
|
1432
|
+
if (!NUMBER_STYLES.includes(numberStyle)) {
|
|
1433
|
+
throw new BookConfigError(
|
|
1434
|
+
`numberStyle must be one of ${NUMBER_STYLES.join(" | ")} (got ${JSON.stringify(numberStyle)})`
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1359
1437
|
const site = opts.site ?? composed.site;
|
|
1360
1438
|
if (!site) {
|
|
1361
1439
|
throw new BookConfigError(
|
|
@@ -1423,6 +1501,7 @@ async function defineBookConfig(opts) {
|
|
|
1423
1501
|
sitemap(sitemapOptions),
|
|
1424
1502
|
bookScaffoldIntegration({
|
|
1425
1503
|
profile,
|
|
1504
|
+
numberStyle,
|
|
1426
1505
|
routes: mergedRoutes,
|
|
1427
1506
|
mdxComponentsModule,
|
|
1428
1507
|
extraStyles: mergedExtraStyles,
|
|
@@ -1433,6 +1512,11 @@ async function defineBookConfig(opts) {
|
|
|
1433
1512
|
subtitle: opts.subtitle,
|
|
1434
1513
|
// v4.26.2 (#149; style inheritance + opt-out fixed in v4.26.3).
|
|
1435
1514
|
releaseStatus: resolvedReleaseStatus === false ? void 0 : resolvedReleaseStatus,
|
|
1515
|
+
// v4.27.0 (#188): undefined enables the audited defaults, false opts
|
|
1516
|
+
// out, and an object can replace only the CSP. The integration owns
|
|
1517
|
+
// the build:done emission because that is where Astro's output dir is
|
|
1518
|
+
// known and a copied consumer public/_headers can be detected.
|
|
1519
|
+
securityHeaders: opts.securityHeaders,
|
|
1436
1520
|
description: opts.description,
|
|
1437
1521
|
portfolio: resolvedPortfolio,
|
|
1438
1522
|
// v4.6.0: book-level author + SEO config (ogImage, twitterHandle),
|
|
@@ -1479,6 +1563,7 @@ async function defineBookConfig(opts) {
|
|
|
1479
1563
|
};
|
|
1480
1564
|
const {
|
|
1481
1565
|
styles: _styles,
|
|
1566
|
+
numberStyle: _numberStyle,
|
|
1482
1567
|
site: _site,
|
|
1483
1568
|
routes: _routes,
|
|
1484
1569
|
deploy: _deploy,
|
|
@@ -1492,6 +1577,8 @@ async function defineBookConfig(opts) {
|
|
|
1492
1577
|
subtitle: _subtitle,
|
|
1493
1578
|
// v4.26.2 (#149): strip the release-state banner config.
|
|
1494
1579
|
releaseStatus: _releaseStatus,
|
|
1580
|
+
// v4.27.0 (#188): consumed by the scaffold integration at build:done.
|
|
1581
|
+
securityHeaders: _securityHeaders,
|
|
1495
1582
|
description: _description,
|
|
1496
1583
|
portfolio: _portfolio,
|
|
1497
1584
|
// v4.6.0: strip new book-level SEO opts (author + seo block).
|
|
@@ -1512,6 +1599,7 @@ async function defineBookConfig(opts) {
|
|
|
1512
1599
|
...rest
|
|
1513
1600
|
} = opts;
|
|
1514
1601
|
void _styles;
|
|
1602
|
+
void _numberStyle;
|
|
1515
1603
|
void _site;
|
|
1516
1604
|
void _routes;
|
|
1517
1605
|
void _deploy;
|
|
@@ -1523,6 +1611,7 @@ async function defineBookConfig(opts) {
|
|
|
1523
1611
|
void _title;
|
|
1524
1612
|
void _subtitle;
|
|
1525
1613
|
void _releaseStatus;
|
|
1614
|
+
void _securityHeaders;
|
|
1526
1615
|
void _description;
|
|
1527
1616
|
void _portfolio;
|
|
1528
1617
|
void _author;
|
|
@@ -1628,6 +1717,9 @@ function assertEnumProp(value, allowed, ctx) {
|
|
|
1628
1717
|
);
|
|
1629
1718
|
}
|
|
1630
1719
|
|
|
1720
|
+
// src/index.ts
|
|
1721
|
+
init_katex_macros();
|
|
1722
|
+
|
|
1631
1723
|
// src/lib/book-link.ts
|
|
1632
1724
|
function resolveBookHref(siblingBooks, book, to) {
|
|
1633
1725
|
const base = siblingBooks?.[book];
|
|
@@ -1641,9 +1733,13 @@ function resolveBookHref(siblingBooks, book, to) {
|
|
|
1641
1733
|
}
|
|
1642
1734
|
|
|
1643
1735
|
// src/lib/nav-href.ts
|
|
1644
|
-
function
|
|
1736
|
+
function normalizeBase(baseUrl) {
|
|
1645
1737
|
return (baseUrl || "/").replace(/\/*$/, "/");
|
|
1646
1738
|
}
|
|
1739
|
+
function baseNoSlash(baseUrl) {
|
|
1740
|
+
return (baseUrl || "/").replace(/\/+$/, "");
|
|
1741
|
+
}
|
|
1742
|
+
var normBase = normalizeBase;
|
|
1647
1743
|
function fillTokens(pattern, tokens) {
|
|
1648
1744
|
return pattern.replace(/:(book|slug|route|id)\b/g, (_m, k) => tokens[k] ?? "");
|
|
1649
1745
|
}
|
|
@@ -1916,6 +2012,7 @@ export {
|
|
|
1916
2012
|
BookConfigError,
|
|
1917
2013
|
DEFAULT_GITHUB_BRANCH,
|
|
1918
2014
|
KIND_LABEL,
|
|
2015
|
+
NUMBER_STYLES,
|
|
1919
2016
|
THEOREM_KINDS,
|
|
1920
2017
|
UNKNOWN_PART_ORDINAL,
|
|
1921
2018
|
academicChapterSchema,
|
|
@@ -1928,6 +2025,7 @@ export {
|
|
|
1928
2025
|
apparatusHref,
|
|
1929
2026
|
assertEnumProp,
|
|
1930
2027
|
assertKnownDomain,
|
|
2028
|
+
baseNoSlash,
|
|
1931
2029
|
bloomLevels,
|
|
1932
2030
|
bookOf,
|
|
1933
2031
|
bookScaffoldIntegration,
|
|
@@ -1962,6 +2060,7 @@ export {
|
|
|
1962
2060
|
layoutModes,
|
|
1963
2061
|
minimalChapterSchema,
|
|
1964
2062
|
minimalStyle,
|
|
2063
|
+
normalizeBase,
|
|
1965
2064
|
normalizeFrontmatterConfig,
|
|
1966
2065
|
originUrlFromGitConfig,
|
|
1967
2066
|
parseRepoSlug,
|
|
@@ -1993,6 +2092,7 @@ export {
|
|
|
1993
2092
|
sourceTiersResearch,
|
|
1994
2093
|
sourcesSchema,
|
|
1995
2094
|
spreadBlueprint,
|
|
2095
|
+
ssmMacros,
|
|
1996
2096
|
theoremLabel,
|
|
1997
2097
|
tocHeadings,
|
|
1998
2098
|
toolSlugs,
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -814,6 +814,7 @@ function readEnvFile(path = ".env") {
|
|
|
814
814
|
return {};
|
|
815
815
|
}
|
|
816
816
|
}
|
|
817
|
+
var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
|
|
817
818
|
function resolvePreset(explicitPreset, explicitProfile) {
|
|
818
819
|
let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
|
|
819
820
|
let source = "default";
|
|
@@ -834,7 +835,13 @@ function resolvePreset(explicitPreset, explicitProfile) {
|
|
|
834
835
|
);
|
|
835
836
|
}
|
|
836
837
|
if (source === "default") {
|
|
837
|
-
|
|
838
|
+
const warningState = globalThis;
|
|
839
|
+
if (!warningState[PRESET_FALLBACK_WARNING]) {
|
|
840
|
+
warningState[PRESET_FALLBACK_WARNING] = true;
|
|
841
|
+
console.warn(
|
|
842
|
+
"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."
|
|
843
|
+
);
|
|
844
|
+
}
|
|
838
845
|
}
|
|
839
846
|
return candidate;
|
|
840
847
|
}
|
|
@@ -353,6 +353,8 @@ interface Style {
|
|
|
353
353
|
readonly name?: string;
|
|
354
354
|
/** Profile that backs this style — determines schema + default routes + styles + KaTeX wiring. */
|
|
355
355
|
readonly preset?: BookPreset;
|
|
356
|
+
/** Theorem-family numbering strategy; shallow override (last wins). */
|
|
357
|
+
readonly numberStyle?: NumberStyle;
|
|
356
358
|
/** Book's deployed origin (sitemap, canonical, Pagefind). Required at composition end;
|
|
357
359
|
* optional inside a Style (so styles can omit it and consumers can provide per-book). */
|
|
358
360
|
readonly site?: string;
|
|
@@ -374,10 +376,10 @@ interface Style {
|
|
|
374
376
|
* `remarkPlugins` and `rehypePlugins` arrays concat across the chain;
|
|
375
377
|
* scalar fields override. */
|
|
376
378
|
readonly markdown?: AstroUserConfig['markdown'];
|
|
377
|
-
/**
|
|
378
|
-
* -
|
|
379
|
-
*
|
|
380
|
-
*
|
|
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. */
|
|
381
383
|
readonly deploy?: 'pages' | 'workers';
|
|
382
384
|
/**
|
|
383
385
|
* v4.26.2 (#149; style inheritance fixed in v4.26.3): release-state
|
|
@@ -445,7 +447,7 @@ declare function defineStyle(opts: StyleInput): Style;
|
|
|
445
447
|
* - Top-level `defineBookConfig` fields beat any style (handled in config.ts)
|
|
446
448
|
*
|
|
447
449
|
* Per-key merge strategy:
|
|
448
|
-
* - `preset`, `site`, `deploy`, `mdxComponentsModule`, `name`, `releaseStatus`
|
|
450
|
+
* - `preset`, `numberStyle`, `site`, `deploy`, `mdxComponentsModule`, `name`, `releaseStatus`
|
|
449
451
|
* → shallow override (last defined wins; `releaseStatus: false` suppresses)
|
|
450
452
|
* - `routes` → per-route spread (each route key independently overridable)
|
|
451
453
|
* - `katexMacros` → per-macro spread (each macro key independently overridable)
|
|
@@ -482,6 +484,30 @@ declare function normalizeFrontmatterConfig(v: FrontmatterRouteConfig | undefine
|
|
|
482
484
|
|
|
483
485
|
type BookPreset = BookProfile;
|
|
484
486
|
declare const BOOK_PRESETS: readonly ("academic" | "tools" | "minimal" | "course-notes" | "research-portfolio")[];
|
|
487
|
+
/** Theorem-family numbering strategy used by build-labels and validate. */
|
|
488
|
+
type NumberStyle = 'shared' | 'per-kind';
|
|
489
|
+
declare const NUMBER_STYLES: readonly ["shared", "per-kind"];
|
|
490
|
+
/**
|
|
491
|
+
* v4.27.0 (#188): build-time security-header customization.
|
|
492
|
+
*
|
|
493
|
+
* The integration emits a Cloudflare-compatible `dist/_headers` file by
|
|
494
|
+
* default. Consumers that need a different CSP can replace that one header
|
|
495
|
+
* while retaining the scaffold's other audited defaults.
|
|
496
|
+
*/
|
|
497
|
+
interface SecurityHeadersConfig {
|
|
498
|
+
contentSecurityPolicy?: string;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* v4.26.2 (#149): book-level release state rendered by
|
|
502
|
+
* `<PreReleaseBanner>` across every page.
|
|
503
|
+
*
|
|
504
|
+
* Style inheritance and explicit suppression were fixed in v4.26.3.
|
|
505
|
+
*/
|
|
506
|
+
interface ReleaseStatusConfig {
|
|
507
|
+
state: 'alpha' | 'beta' | 'rc' | 'locked';
|
|
508
|
+
dismissAt?: string;
|
|
509
|
+
message?: string;
|
|
510
|
+
}
|
|
485
511
|
/**
|
|
486
512
|
* v4.26.2 (#149): book-level release state rendered by
|
|
487
513
|
* `<PreReleaseBanner>` across every page.
|
|
@@ -531,6 +557,13 @@ interface BookConfigOptions {
|
|
|
531
557
|
* });
|
|
532
558
|
*/
|
|
533
559
|
styles?: readonly Style[];
|
|
560
|
+
/**
|
|
561
|
+
* v4.27.0 (#175): theorem-family counter strategy. `shared` preserves the
|
|
562
|
+
* amsthm-style sequence used through v4.26; `per-kind` gives theorem,
|
|
563
|
+
* proposition, lemma, and each other theorem kind an independent sequence.
|
|
564
|
+
* May also be supplied by a Style; top-level config wins.
|
|
565
|
+
*/
|
|
566
|
+
numberStyle?: NumberStyle;
|
|
534
567
|
/**
|
|
535
568
|
* Optional per-route override of the composed profile's defaults. Use to
|
|
536
569
|
* disable an auto-injected route (e.g. multi-book consumer that ships
|
|
@@ -545,10 +578,13 @@ interface BookConfigOptions {
|
|
|
545
578
|
*/
|
|
546
579
|
routes?: PartialRouteToggles;
|
|
547
580
|
/**
|
|
548
|
-
* v4.0.0 (
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
581
|
+
* v4.0.0 (#50): RESERVED — accepted and style-chain-merged, but currently
|
|
582
|
+
* has NO runtime effect (#180). The wrangler.toml shape is decided once, at
|
|
583
|
+
* scaffold time, by `create-book` from the profile name (academic/tools/
|
|
584
|
+
* minimal → Workers; course-notes/research-portfolio → Pages); setting this
|
|
585
|
+
* field changes nothing afterward. Wire-or-remove is a v5 decision.
|
|
586
|
+
* @deprecated Inert in v4; remove this option before v5. Deployment shape
|
|
587
|
+
* is chosen by create-book or by the consumer's own deployment files.
|
|
552
588
|
*/
|
|
553
589
|
deploy?: 'pages' | 'workers';
|
|
554
590
|
/**
|
|
@@ -625,6 +661,14 @@ interface BookConfigOptions {
|
|
|
625
661
|
* composed style chain; set `false` to suppress an inherited banner.
|
|
626
662
|
*/
|
|
627
663
|
releaseStatus?: ReleaseStatusConfig | false;
|
|
664
|
+
/**
|
|
665
|
+
* v4.27.0 (#188): security headers emitted to `dist/_headers` after an
|
|
666
|
+
* Astro build. Omit for the scaffold defaults; set `false` to emit no
|
|
667
|
+
* scaffold-owned file; provide `contentSecurityPolicy` to replace only the
|
|
668
|
+
* default CSP while retaining HSTS, XCTO, Referrer-Policy, and
|
|
669
|
+
* Permissions-Policy. A consumer-owned `public/_headers` always wins.
|
|
670
|
+
*/
|
|
671
|
+
securityHeaders?: SecurityHeadersConfig | false;
|
|
628
672
|
/**
|
|
629
673
|
* v4.5.0: Book description. Read by the auto-injected `/` landing page (lead paragraph + <meta description>).
|
|
630
674
|
* Optional; landing renders no description paragraph if unset.
|
|
@@ -757,6 +801,8 @@ interface BookSchemasOptions {
|
|
|
757
801
|
/** Options for the internal `bookScaffoldIntegration`. See PACKAGE_DESIGN.md §6. */
|
|
758
802
|
interface BookScaffoldIntegrationOptions {
|
|
759
803
|
profile: BookProfile;
|
|
804
|
+
/** Resolved theorem-family counter strategy exposed to package CLI tooling. */
|
|
805
|
+
numberStyle?: NumberStyle;
|
|
760
806
|
/**
|
|
761
807
|
* Per-route override; merged into the profile's defaults.
|
|
762
808
|
* v4.0.0: `routes.frontmatter` widened to `boolean | { enabled, prefix? }`
|
|
@@ -780,6 +826,8 @@ interface BookScaffoldIntegrationOptions {
|
|
|
780
826
|
/** v4.26.2 (#149; style inheritance fixed in v4.26.3): resolved
|
|
781
827
|
* release-state banner propagated via the book-config virtual module. */
|
|
782
828
|
releaseStatus?: ReleaseStatusConfig;
|
|
829
|
+
/** v4.27.0 (#188): resolved security-header emission policy. */
|
|
830
|
+
securityHeaders?: SecurityHeadersConfig | false;
|
|
783
831
|
/** v4.5.0: book description, propagated to `/` landing via vite.define. */
|
|
784
832
|
description?: string;
|
|
785
833
|
/**
|
|
@@ -831,22 +879,6 @@ interface BookScaffoldIntegrationOptions {
|
|
|
831
879
|
declare class BookConfigError extends Error {
|
|
832
880
|
constructor(message: string);
|
|
833
881
|
}
|
|
834
|
-
/**
|
|
835
|
-
* Resolve preset from explicit args → env → .env → default. Throws on invalid.
|
|
836
|
-
*
|
|
837
|
-
* v3.4.0 (closes #9): canonical resolver. Accepts both `preset` and `profile`
|
|
838
|
-
* (back-compat) explicit args; reads both `BOOK_PRESET` (preferred) and
|
|
839
|
-
* `BOOK_PROFILE` (alias) env vars; same for .env file lookups.
|
|
840
|
-
*
|
|
841
|
-
* Resolution order:
|
|
842
|
-
* 1. explicitPreset (from defineBookConfig({ preset: ... }))
|
|
843
|
-
* 2. explicitProfile (from defineBookConfig({ profile: ... }))
|
|
844
|
-
* 3. process.env.BOOK_PRESET
|
|
845
|
-
* 4. process.env.BOOK_PROFILE
|
|
846
|
-
* 5. .env BOOK_PRESET
|
|
847
|
-
* 6. .env BOOK_PROFILE
|
|
848
|
-
* 7. 'minimal' (with console.warn)
|
|
849
|
-
*/
|
|
850
882
|
declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: BookProfile): BookPreset;
|
|
851
883
|
/**
|
|
852
884
|
* Backward-compat alias. New code should use `resolvePreset()`.
|
|
@@ -855,4 +887,4 @@ declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: Bo
|
|
|
855
887
|
*/
|
|
856
888
|
declare function resolveProfile(explicit?: BookProfile): BookProfile;
|
|
857
889
|
|
|
858
|
-
export { BOOK_PRESETS as B, type ChapterFor as C, type FreshnessAffordance as F, type PartKey as P, type ReleaseStatusConfig as R, type
|
|
890
|
+
export { BOOK_PRESETS as B, type ChapterFor as C, type FreshnessAffordance as F, 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 BookPreset as d, type BookProfile as e, type BookScaffoldIntegrationOptions as f, type BookSchemasOptions as g, type ChaptersRenderer as h, type FrontmatterRouteConfig as i, type NumberStyle as j, type PartialRouteToggles as k, type ProfileDefinition as l, type RouteToggles as m, type StatusBadge as n, type Style as o, type StyleInput as p, composeStyles as q, defineProfile as r, defineStyle as s, normalizeFrontmatterConfig as t, resolvePreset as u, resolveProfile as v };
|
package/layouts/Base.astro
CHANGED
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
* lang — html lang attribute (default: en)
|
|
16
16
|
* showSidebar — render the left chapter-navigation sidebar (default: true).
|
|
17
17
|
* Set false for the landing page or any full-bleed surface.
|
|
18
|
-
* Below
|
|
19
|
-
*
|
|
18
|
+
* Below 80rem (1280px) the sidebar is auto-hidden via CSS and
|
|
19
|
+
* the mobile nav drawer is the chapter nav (v4.26.0, #80).
|
|
20
|
+
* showChrome — render the automatic tools chrome (ToolFilter)
|
|
20
21
|
* (default: true). Set false on a landing/hub page with no
|
|
21
|
-
*
|
|
22
|
+
* chapter filtering; search + theme toggle always render.
|
|
22
23
|
*
|
|
23
|
-
* Tools chrome (ToolFilter
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* (search + theme toggle only).
|
|
24
|
+
* Tools chrome (ToolFilter): only mounted when BOOK_PROFILE !== 'academic' AND
|
|
25
|
+
* showChrome !== false. VersionSelector is a prop-driven public component that
|
|
26
|
+
* consumers mount explicitly once they have real deployed-version data.
|
|
27
27
|
*
|
|
28
28
|
* Usage:
|
|
29
29
|
* ---
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* — or the page ends up with duplicate `<main>` landmarks (a11y, #91).
|
|
40
40
|
*/
|
|
41
41
|
import '@fontsource-variable/roboto';
|
|
42
|
+
import robotoLatinUrl from '@fontsource-variable/roboto/files/roboto-latin-wght-normal.woff2?url';
|
|
42
43
|
import '@fontsource-variable/source-code-pro';
|
|
43
44
|
// KaTeX CSS is injected by bookScaffoldIntegration for academic profile
|
|
44
45
|
// only (since v3.0 alpha.8) — tools/minimal profiles don't install katex
|
|
@@ -51,12 +52,11 @@ import '../styles/chapter.css';
|
|
|
51
52
|
import '../styles/tool-filter.css';
|
|
52
53
|
import '../styles/convergence.css';
|
|
53
54
|
import '../styles/print.css';
|
|
54
|
-
// Use package-path
|
|
55
|
+
// Use the package-path import for the .tsx island so Vite resolves it
|
|
55
56
|
// via the exports map (→ pre-compiled dist/components/*.mjs with proper
|
|
56
57
|
// preact JSX). The raw .tsx files are also shipped so consumers can
|
|
57
58
|
// import them directly, but relative imports from inside the package
|
|
58
59
|
// must route through the exports map, not the raw source.
|
|
59
|
-
import VersionSelector from '@brandon_m_behring/book-scaffold-astro/components/VersionSelector';
|
|
60
60
|
import ToolFilter from '@brandon_m_behring/book-scaffold-astro/components/ToolFilter';
|
|
61
61
|
import Sidebar from '../components/Sidebar.astro';
|
|
62
62
|
// v4.26.2 (#149; style inheritance fixed in v4.26.3): site-wide
|
|
@@ -68,6 +68,7 @@ import NavContent from '../components/NavContent.astro';
|
|
|
68
68
|
// description fallback, ogImage default, twitterHandle) from the
|
|
69
69
|
// book-config virtual module (was landing-config in v4.5.1).
|
|
70
70
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
71
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
71
72
|
|
|
72
73
|
const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
|
|
73
74
|
|
|
@@ -76,10 +77,10 @@ interface Props {
|
|
|
76
77
|
description?: string;
|
|
77
78
|
lang?: string;
|
|
78
79
|
showSidebar?: boolean;
|
|
79
|
-
/** #163: render the optional
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
/** #163: render the optional automatic tools chrome — the ToolFilter island
|
|
81
|
+
* (default true). Set false on landing/hub pages with no chapter filtering;
|
|
82
|
+
* the search + theme-toggle cluster always renders. (No-op for the academic
|
|
83
|
+
* profile, which never shows the ToolFilter.) */
|
|
83
84
|
showChrome?: boolean;
|
|
84
85
|
/** v4.6.0: Open Graph image URL (relative to site root, or absolute). */
|
|
85
86
|
ogImage?: string;
|
|
@@ -97,9 +98,9 @@ const {
|
|
|
97
98
|
ogType = 'website',
|
|
98
99
|
} = Astro.props;
|
|
99
100
|
|
|
100
|
-
//
|
|
101
|
+
// Automatic tools chrome (ToolFilter) renders only for non-academic
|
|
101
102
|
// profiles AND when the page opts in via showChrome (default true). A
|
|
102
|
-
// landing/hub page (no
|
|
103
|
+
// landing/hub page (no chapter filtering) passes showChrome={false} to
|
|
103
104
|
// get the search + theme-toggle cluster only — the chrome-free path that
|
|
104
105
|
// previously forced borrowing the academic profile (and its katex deps). #163.
|
|
105
106
|
const showToolsChrome = (profile !== 'academic') && showChrome;
|
|
@@ -120,7 +121,7 @@ const absoluteOgImage = resolvedOgImage
|
|
|
120
121
|
const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
|
|
121
122
|
const ogSiteName = bookConfig.title ?? title;
|
|
122
123
|
const ogDescription = description ?? bookConfig.description ?? '';
|
|
123
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
124
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
124
125
|
---
|
|
125
126
|
|
|
126
127
|
<!doctype html>
|
|
@@ -128,6 +129,15 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
|
|
|
128
129
|
<head>
|
|
129
130
|
<meta charset="utf-8" />
|
|
130
131
|
<link rel="icon" type="image/svg+xml" href={`${baseUrl}favicon.svg`} />
|
|
132
|
+
{/* #187: start the common Latin body face early. Its Fontsource rules use
|
|
133
|
+
font-display: optional, so a slow response never swaps after paint. */}
|
|
134
|
+
<link
|
|
135
|
+
rel="preload"
|
|
136
|
+
href={robotoLatinUrl}
|
|
137
|
+
as="font"
|
|
138
|
+
type="font/woff2"
|
|
139
|
+
crossorigin="anonymous"
|
|
140
|
+
/>
|
|
131
141
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
132
142
|
<meta name="color-scheme" content="light dark" />
|
|
133
143
|
<meta name="generator" content={Astro.generator} />
|
|
@@ -196,7 +206,6 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
|
|
|
196
206
|
</a>
|
|
197
207
|
)}
|
|
198
208
|
{showToolsChrome && <ToolFilter client:idle />}
|
|
199
|
-
{showToolsChrome && <VersionSelector client:idle />}
|
|
200
209
|
<a
|
|
201
210
|
href={`${baseUrl}search/`}
|
|
202
211
|
class="chrome-button"
|
|
@@ -217,7 +226,8 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
|
|
|
217
226
|
</button>
|
|
218
227
|
</div>
|
|
219
228
|
{/* v4.26.0 (#80): mobile/tablet nav drawer (sibling of <main>, NOT a <main>).
|
|
220
|
-
CSS-hidden ≥
|
|
229
|
+
CSS-hidden ≥80rem where the Sidebar is the nav (layout.css and the
|
|
230
|
+
controller's desktop auto-close agree on 80rem). role=dialog + the inline
|
|
221
231
|
controller below provide focus-trap/ESC; `:target` is the no-JS fallback. */}
|
|
222
232
|
{showSidebar && (
|
|
223
233
|
<div id="nav-drawer" class="nav-drawer" data-nav-drawer>
|
|
@@ -390,7 +400,8 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
|
|
|
390
400
|
text-decoration: none;
|
|
391
401
|
}
|
|
392
402
|
|
|
393
|
-
/*
|
|
403
|
+
/* Opt-in VersionSelector dropdown. Base provides its visual language but
|
|
404
|
+
* deliberately does not mount it without a consumer-owned version manifest. */
|
|
394
405
|
.version-selector {
|
|
395
406
|
position: relative;
|
|
396
407
|
display: inline-block;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
|
-
"description": "Astro 6 + MDX toolkit for long-form technical books
|
|
4
|
-
"version": "4.
|
|
3
|
+
"description": "Astro 6 + MDX toolkit for long-form technical books with five typed presets, Tufte typography, citations, search, PDF, and Cloudflare deployment.",
|
|
4
|
+
"version": "4.27.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -161,9 +161,11 @@
|
|
|
161
161
|
"pedagogy",
|
|
162
162
|
"examples",
|
|
163
163
|
"CLAUDE.md",
|
|
164
|
+
"AGENTS.md",
|
|
164
165
|
"README.md",
|
|
165
|
-
"
|
|
166
|
-
"
|
|
166
|
+
"LICENSE",
|
|
167
|
+
"LICENSE-CONTENT",
|
|
168
|
+
"LATEX_TO_MDX_MAPPING.md"
|
|
167
169
|
],
|
|
168
170
|
"scripts": {
|
|
169
171
|
"build": "tsup",
|
|
@@ -195,6 +197,7 @@
|
|
|
195
197
|
"@fontsource-variable/roboto": "^5.2.10",
|
|
196
198
|
"@fontsource-variable/source-code-pro": "^5.2.7",
|
|
197
199
|
"pagefind": "^1.5.2",
|
|
200
|
+
"vite": "^7.3.2",
|
|
198
201
|
"yaml": "^2.9.0"
|
|
199
202
|
},
|
|
200
203
|
"devDependencies": {
|
package/pages/answers.astro
CHANGED
|
@@ -26,6 +26,7 @@ import { render } from 'astro:content';
|
|
|
26
26
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
27
27
|
import { getAllQuestions, groupByChapter } from '../src/lib/questions';
|
|
28
28
|
import { assertKnownDomain } from '../src/lib/exam-domains';
|
|
29
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
29
30
|
|
|
30
31
|
// Presence-gate: only touch the collection when the directory holds files.
|
|
31
32
|
const questionModules = import.meta.glob('/src/content/questions/**/*.{md,mdx}', {
|
|
@@ -51,7 +52,7 @@ const rendered = await Promise.all(
|
|
|
51
52
|
const byChapter = groupByChapter(rendered);
|
|
52
53
|
const total = rendered.length;
|
|
53
54
|
// #142: practice-exam + chapter links must prefix the deploy base.
|
|
54
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
55
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
55
56
|
const practiceHref = (bookConfig.enabledRoutes ?? []).includes('practiceExam')
|
|
56
57
|
? `${baseUrl}practice-exam`
|
|
57
58
|
: null;
|
package/pages/chapters.astro
CHANGED
|
@@ -26,9 +26,10 @@ import { getAllChapters, type Chapter } from '../src/lib/chapters';
|
|
|
26
26
|
import { PROFILES } from '../src/profiles/index';
|
|
27
27
|
import { fallbackChaptersRenderer } from '../src/profiles/renderers/fallback-chapters';
|
|
28
28
|
import type { ChaptersRenderer, PartKey } from '../src/lib/chapters-renderer';
|
|
29
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
29
30
|
|
|
30
31
|
// #142: prefix BASE_URL so chapter-card links stay inside a non-root deploy base.
|
|
31
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
32
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
32
33
|
|
|
33
34
|
const profileName = (import.meta.env.BOOK_PROFILE ?? 'minimal') as keyof typeof PROFILES;
|
|
34
35
|
const profileDef = PROFILES[profileName];
|