@brandon_m_behring/book-scaffold-astro 4.17.0 → 4.18.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 +1 -1
- package/components/Theorem.astro +34 -9
- package/dist/index.d.ts +2 -49
- package/dist/index.mjs +4 -0
- package/dist/lib/theorem-label.d.ts +64 -0
- package/dist/lib/theorem-label.mjs +49 -0
- package/package.json +1 -1
- package/scripts/build-labels.mjs +58 -10
- package/scripts/validate.mjs +17 -2
- package/src/lib/theorem-label.ts +19 -0
package/CLAUDE.md
CHANGED
|
@@ -96,7 +96,7 @@ Two callout families coexist. Authors import what they need.
|
|
|
96
96
|
|
|
97
97
|
**Tools family** (`src/components/callouts/`, 8 components): `SkillBox`, `CaseStudy`, `ConceptBox`, `KeyIdea`, `TryThis`, `Recovery`, `Convergence`, `Divergence`.
|
|
98
98
|
|
|
99
|
-
**Academic family** (`src/components/callouts/`, 10 components): `NoteBox`, `ExampleBox`, `DynConnect`, `InsightBox`, `WarnBox`, `CounterBox`, `TipBox`, `OpenQuestion`, `PaperBox`, `ResultBox`. Plus `Theorem` (unified for theorem/proposition/lemma/corollary/definition/example/exercise/remark/proof). **Props (v4.14.3, #121):** `kind=` is canonical; `type=` is accepted as a legacy alias (likewise `title=`/`label=` alias `name=`). An absent or unknown kind **throws at build** (via `src/lib/theorem-label`) rather than rendering an empty label; `book-scaffold validate` flags a `<Theorem>` with neither `kind=` nor `type=` even earlier.
|
|
99
|
+
**Academic family** (`src/components/callouts/`, 10 components): `NoteBox`, `ExampleBox`, `DynConnect`, `InsightBox`, `WarnBox`, `CounterBox`, `TipBox`, `OpenQuestion`, `PaperBox`, `ResultBox`. Plus `Theorem` (unified for theorem/proposition/lemma/corollary/definition/example/exercise/remark/proof). **Props (v4.14.3, #121):** `kind=` is canonical; `type=` is accepted as a legacy alias (likewise `title=`/`label=` alias `name=`). An absent or unknown kind **throws at build** (via `src/lib/theorem-label`) rather than rendering an empty label; `book-scaffold validate` flags a `<Theorem>` with neither `kind=` nor `type=` even earlier. **Numbering (v4.18.0, #126):** a theorem with an `id` auto-numbers from `labels.json` — the same index `<XRef>` reads — so the heading number equals every cross-reference to it by construction; explicit `n=` is a fallback for un-id'd theorems. `build-labels` indexes the kind-accurate word (`Proposition 8.1`, not a kind-blind `Theorem 8.1`) and throws on an unknown kind.
|
|
100
100
|
|
|
101
101
|
**Pedagogy family** (v4.1.0+, any profile, 3 components): `Pitfall` (rose; "common mistake" — distinct from `WarnBox`'s preemptive warning), `WorkedExample` (plum; collapsible `<details>` block with `#worked-example-{id}` anchor for deep links), `YouWillLearn` (gold; chapter-opener with optional `prerequisites` prop). Slot bullets/code freely; render at any preset.
|
|
102
102
|
|
package/components/Theorem.astro
CHANGED
|
@@ -10,29 +10,54 @@
|
|
|
10
10
|
* (ssm-foundations ch1–11 + the LaTeX-env mental model pass `type=`). `name`
|
|
11
11
|
* is canonical with `title=`/`label=` accepted. An absent or unknown kind
|
|
12
12
|
* THROWS at build (see `src/lib/theorem-label`) — it never renders an empty
|
|
13
|
-
* label.
|
|
13
|
+
* label.
|
|
14
14
|
*
|
|
15
|
-
* Auto-numbering
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Auto-numbering (#126): when the theorem has an `id`, its number is read from
|
|
16
|
+
* `src/data/labels.json` — the same map <XRef> resolves — so the heading number
|
|
17
|
+
* EQUALS every cross-reference to it by construction. No hand-passed `n=`, no
|
|
18
|
+
* drift; inserting a theorem renumbers the chapter from one counter
|
|
19
|
+
* (`book-scaffold build-labels`). Explicit `n=` stays a fallback for an un-id'd
|
|
20
|
+
* theorem (or before labels.json is built); when an id resolves, the labels.json
|
|
21
|
+
* number wins so the two surfaces can never disagree.
|
|
18
22
|
*
|
|
19
23
|
* Usage:
|
|
20
|
-
* <Theorem kind="theorem"
|
|
21
|
-
* For …
|
|
24
|
+
* <Theorem kind="theorem" name="Stable continuous-time eigenvalues" id="thm-w4-stability">
|
|
25
|
+
* For … // number auto-resolved from labels.json
|
|
22
26
|
* </Theorem>
|
|
23
27
|
*
|
|
24
28
|
* <Theorem kind="definition" n="4.1" name="HiPPO-LegS">
|
|
25
|
-
* The HiPPO-LegS state matrix …
|
|
29
|
+
* The HiPPO-LegS state matrix … // explicit n= (un-cross-referenced)
|
|
26
30
|
* </Theorem>
|
|
27
31
|
*/
|
|
28
|
-
import { theoremLabel, type TheoremLabelProps } from '../src/lib/theorem-label';
|
|
32
|
+
import { theoremLabel, resolveTheoremNumber, type TheoremLabelProps } from '../src/lib/theorem-label';
|
|
29
33
|
|
|
30
34
|
interface Props extends TheoremLabelProps {
|
|
31
35
|
id?: string;
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// #126: resolve THIS theorem's number from the same labels.json that <XRef>
|
|
39
|
+
// reads, so the heading number == the cross-reference display by construction.
|
|
40
|
+
// build-labels.mjs writes { href, display, number } keyed by id. Mirror XRef's
|
|
41
|
+
// project-root glob (Vite resolves `/` to the consumer root, not the package);
|
|
42
|
+
// a missing file → empty map → fall back to explicit n= (the same soft-degrade
|
|
43
|
+
// path as XRef — the validator catches unknown ids at CI).
|
|
44
|
+
type LabelEntry = { href: string; display: string; number?: string | null };
|
|
45
|
+
const labelsModules = import.meta.glob<{ default: Record<string, LabelEntry> }>(
|
|
46
|
+
'/src/data/labels.json',
|
|
47
|
+
{ eager: true },
|
|
48
|
+
);
|
|
49
|
+
const labelsMap = (labelsModules['/src/data/labels.json']?.default ?? {}) as Record<
|
|
50
|
+
string,
|
|
51
|
+
LabelEntry
|
|
52
|
+
>;
|
|
53
|
+
|
|
34
54
|
const { id } = Astro.props;
|
|
35
|
-
|
|
55
|
+
// labels.json (by id) is the number source; explicit n= is the fallback when no
|
|
56
|
+
// id resolves. When both exist the shared source wins — that is what guarantees
|
|
57
|
+
// heading == xref (a stale n= can't reintroduce drift).
|
|
58
|
+
const entry = id ? labelsMap[id] : undefined;
|
|
59
|
+
const resolvedN = resolveTheoremNumber(entry, Astro.props.n);
|
|
60
|
+
const { kind, fullLabel } = theoremLabel({ ...Astro.props, n: resolvedN });
|
|
36
61
|
---
|
|
37
62
|
<div class="theorem" data-kind={kind} id={id}>
|
|
38
63
|
<span class="theorem-label">{fullLabel}.</span>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
2
|
import { d as BookConfigOptions, g as BookScaffoldIntegrationOptions, a5 as volatilityLevels, i as ChaptersRenderer, t as academicParts, Q as Question, q as Style } from './types-CMPuyZGP.js';
|
|
3
3
|
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BloomLevel, c as BookConfigError, e as BookPreset, f as BookProfile, h as BookSchemasOptions, C as ChapterFor, j as CourseNotesChapter, F as FreshnessAffordance, k as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, l as PartialRouteToggles, m as ProfileDefinition, n as Provenance, o as QuestionType, R as ResearchPortfolioChapter, p as RouteToggles, S as StatusBadge, r as StyleInput, T as ToolsChapter, V as VolatilityBadge, s as academicChapterSchema, u as bloomLevels, v as changeKinds, w as changelogSchema, x as chapterStatus, y as citationBackstops, z as composeStyles, D as courseNotesChapterSchema, E as defineProfile, G as defineStyle, H as minimalChapterSchema, I as normalizeFrontmatterConfig, J as patternCategories, K as patternsSchema, L as provenanceObject, N as provenanceSchema, O as questionDifficulties, U as questionSchema, W as questionTypes, X as refineQuestion, Y as refinedQuestionSchema, Z as researchPortfolioChapterSchema, _ as resolvePreset, $ as resolveProfile, a0 as sourceTiers, a1 as sourceTiersResearch, a2 as sourcesSchema, a3 as toolSlugs, a4 as toolsChapterSchema } from './types-CMPuyZGP.js';
|
|
4
|
+
export { KIND_LABEL, ResolvedTheoremLabel, THEOREM_KINDS, TheoremKind, TheoremLabelProps, resolveTheoremNumber, theoremLabel } from './lib/theorem-label.js';
|
|
4
5
|
import 'astro/zod';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -204,54 +205,6 @@ declare function academicPartOrdinal(part: string): number;
|
|
|
204
205
|
*/
|
|
205
206
|
declare function academicPartHeading(part: string): string;
|
|
206
207
|
|
|
207
|
-
/**
|
|
208
|
-
* theorem-label — resolve a `<Theorem>` component's label from its props,
|
|
209
|
-
* failing loud instead of rendering a silent empty label (#121).
|
|
210
|
-
*
|
|
211
|
-
* Vocabulary: `kind` is canonical; `type` is an accepted **legacy alias** (many
|
|
212
|
-
* consumer books — ssm-foundations ch1–11 — and the LaTeX `\begin{<env>}`
|
|
213
|
-
* mental model pass `type=`). Likewise `name` is canonical with `title`/`label`
|
|
214
|
-
* accepted as aliases. Aliases keep existing content valid; they are synonyms,
|
|
215
|
-
* not deprecations, so they neither warn nor throw.
|
|
216
|
-
*
|
|
217
|
-
* Failure mode: an absent or unrecognized kind THROWS a build-failing,
|
|
218
|
-
* actionable error rather than computing `KIND_LABEL[undefined]` → a bare "."
|
|
219
|
-
* (the v4.8–4.14 silent defect, live across 32+ ssm-foundations theorems). A
|
|
220
|
-
* typo'd kind — silent before — now stops the build with the offending value.
|
|
221
|
-
*
|
|
222
|
-
* Extracted from the `Theorem.astro` frontmatter so the contract is unit-tested
|
|
223
|
-
* in the pure `node --test` suite (see tests/theorem-label.test.mjs) — the same
|
|
224
|
-
* single-source-of-truth move as `academic-parts.ts` (#95).
|
|
225
|
-
*/
|
|
226
|
-
declare const THEOREM_KINDS: readonly ["theorem", "proposition", "lemma", "corollary", "definition", "example", "exercise", "remark", "proof"];
|
|
227
|
-
type TheoremKind = (typeof THEOREM_KINDS)[number];
|
|
228
|
-
declare const KIND_LABEL: Record<TheoremKind, string>;
|
|
229
|
-
interface TheoremLabelProps {
|
|
230
|
-
/** Canonical environment selector. */
|
|
231
|
-
kind?: string;
|
|
232
|
-
/** Legacy alias for `kind` (LaTeX-env mental model; ssm-foundations ch1–11). */
|
|
233
|
-
type?: string;
|
|
234
|
-
/** Explicit number, e.g. "4.2"; omit for an unnumbered environment. */
|
|
235
|
-
n?: string;
|
|
236
|
-
/** Canonical display name shown in parentheses. */
|
|
237
|
-
name?: string;
|
|
238
|
-
/** Legacy aliases for `name`. */
|
|
239
|
-
title?: string;
|
|
240
|
-
label?: string;
|
|
241
|
-
}
|
|
242
|
-
interface ResolvedTheoremLabel {
|
|
243
|
-
/** The validated, canonical kind (for `data-kind`). */
|
|
244
|
-
kind: TheoremKind;
|
|
245
|
-
/** The composed "Kind N (Name)" label (never empty). */
|
|
246
|
-
fullLabel: string;
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Resolve a `<Theorem>`'s `kind` (canonical or legacy `type=`) and compose its
|
|
250
|
-
* full label. Throws — never returns an empty label — when the kind is absent
|
|
251
|
-
* or not one of {@link THEOREM_KINDS}.
|
|
252
|
-
*/
|
|
253
|
-
declare function theoremLabel(props: TheoremLabelProps): ResolvedTheoremLabel;
|
|
254
|
-
|
|
255
208
|
/**
|
|
256
209
|
* repo-url — resolve and build GitHub source links for CodeRef / CodeBlock.
|
|
257
210
|
*
|
|
@@ -504,4 +457,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
504
457
|
*/
|
|
505
458
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
506
459
|
|
|
507
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus,
|
|
460
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, Question, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, assertEnumProp, assertKnownDomain, bookScaffoldIntegration, buildGithubUrl, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, minimalStyle, originUrlFromGitConfig, parseRepoSlug, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, sortQuestions, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -1498,6 +1498,9 @@ function theoremLabel(props) {
|
|
|
1498
1498
|
const fullLabel = name ? `${numbered} (${name})` : numbered;
|
|
1499
1499
|
return { kind: raw, fullLabel };
|
|
1500
1500
|
}
|
|
1501
|
+
function resolveTheoremNumber(entry, n) {
|
|
1502
|
+
return entry?.number ?? n;
|
|
1503
|
+
}
|
|
1501
1504
|
|
|
1502
1505
|
// src/lib/assert-prop.ts
|
|
1503
1506
|
function assertEnumProp(value, allowed, ctx) {
|
|
@@ -1691,6 +1694,7 @@ export {
|
|
|
1691
1694
|
resolveGithubRepo,
|
|
1692
1695
|
resolvePreset,
|
|
1693
1696
|
resolveProfile,
|
|
1697
|
+
resolveTheoremNumber,
|
|
1694
1698
|
sortQuestions,
|
|
1695
1699
|
sourceTiers,
|
|
1696
1700
|
sourceTiersResearch,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* theorem-label — resolve a `<Theorem>` component's label from its props,
|
|
3
|
+
* failing loud instead of rendering a silent empty label (#121).
|
|
4
|
+
*
|
|
5
|
+
* Vocabulary: `kind` is canonical; `type` is an accepted **legacy alias** (many
|
|
6
|
+
* consumer books — ssm-foundations ch1–11 — and the LaTeX `\begin{<env>}`
|
|
7
|
+
* mental model pass `type=`). Likewise `name` is canonical with `title`/`label`
|
|
8
|
+
* accepted as aliases. Aliases keep existing content valid; they are synonyms,
|
|
9
|
+
* not deprecations, so they neither warn nor throw.
|
|
10
|
+
*
|
|
11
|
+
* Failure mode: an absent or unrecognized kind THROWS a build-failing,
|
|
12
|
+
* actionable error rather than computing `KIND_LABEL[undefined]` → a bare "."
|
|
13
|
+
* (the v4.8–4.14 silent defect, live across 32+ ssm-foundations theorems). A
|
|
14
|
+
* typo'd kind — silent before — now stops the build with the offending value.
|
|
15
|
+
*
|
|
16
|
+
* Extracted from the `Theorem.astro` frontmatter so the contract is unit-tested
|
|
17
|
+
* in the pure `node --test` suite (see tests/theorem-label.test.mjs) — the same
|
|
18
|
+
* single-source-of-truth move as `academic-parts.ts` (#95).
|
|
19
|
+
*/
|
|
20
|
+
declare const THEOREM_KINDS: readonly ["theorem", "proposition", "lemma", "corollary", "definition", "example", "exercise", "remark", "proof"];
|
|
21
|
+
type TheoremKind = (typeof THEOREM_KINDS)[number];
|
|
22
|
+
declare const KIND_LABEL: Record<TheoremKind, string>;
|
|
23
|
+
interface TheoremLabelProps {
|
|
24
|
+
/** Canonical environment selector. */
|
|
25
|
+
kind?: string;
|
|
26
|
+
/** Legacy alias for `kind` (LaTeX-env mental model; ssm-foundations ch1–11). */
|
|
27
|
+
type?: string;
|
|
28
|
+
/** Explicit number, e.g. "4.2"; omit for an unnumbered environment. */
|
|
29
|
+
n?: string;
|
|
30
|
+
/** Canonical display name shown in parentheses. */
|
|
31
|
+
name?: string;
|
|
32
|
+
/** Legacy aliases for `name`. */
|
|
33
|
+
title?: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
}
|
|
36
|
+
interface ResolvedTheoremLabel {
|
|
37
|
+
/** The validated, canonical kind (for `data-kind`). */
|
|
38
|
+
kind: TheoremKind;
|
|
39
|
+
/** The composed "Kind N (Name)" label (never empty). */
|
|
40
|
+
fullLabel: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a `<Theorem>`'s `kind` (canonical or legacy `type=`) and compose its
|
|
44
|
+
* full label. Throws — never returns an empty label — when the kind is absent
|
|
45
|
+
* or not one of {@link THEOREM_KINDS}.
|
|
46
|
+
*/
|
|
47
|
+
declare function theoremLabel(props: TheoremLabelProps): ResolvedTheoremLabel;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a theorem's display number for {@link theoremLabel}'s `n` (#126). The
|
|
50
|
+
* labels.json entry's `number` — the single source `<XRef>` also reads — wins;
|
|
51
|
+
* an explicit `n=` is the fallback for an un-id'd theorem (or before
|
|
52
|
+
* labels.json is built). Both `null` (a `label=` display override that opted
|
|
53
|
+
* out of auto-numbering) and `undefined` (no entry) fall through to `n`.
|
|
54
|
+
*
|
|
55
|
+
* The #126 invariant in one place: when an id resolves, the index wins, so a
|
|
56
|
+
* stale `n=` can't reintroduce heading/cross-reference drift. Extracted from
|
|
57
|
+
* `Theorem.astro` so the precedence is unit-tested in the pure node:test suite
|
|
58
|
+
* (Astro is peerDep-only — the `.astro` frontmatter can't be loaded there).
|
|
59
|
+
*/
|
|
60
|
+
declare function resolveTheoremNumber(entry: {
|
|
61
|
+
number?: string | null;
|
|
62
|
+
} | undefined, n: string | undefined): string | undefined;
|
|
63
|
+
|
|
64
|
+
export { KIND_LABEL, type ResolvedTheoremLabel, THEOREM_KINDS, type TheoremKind, type TheoremLabelProps, resolveTheoremNumber, theoremLabel };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/lib/theorem-label.ts
|
|
2
|
+
var THEOREM_KINDS = [
|
|
3
|
+
"theorem",
|
|
4
|
+
"proposition",
|
|
5
|
+
"lemma",
|
|
6
|
+
"corollary",
|
|
7
|
+
"definition",
|
|
8
|
+
"example",
|
|
9
|
+
"exercise",
|
|
10
|
+
"remark",
|
|
11
|
+
"proof"
|
|
12
|
+
];
|
|
13
|
+
var KIND_LABEL = {
|
|
14
|
+
theorem: "Theorem",
|
|
15
|
+
proposition: "Proposition",
|
|
16
|
+
lemma: "Lemma",
|
|
17
|
+
corollary: "Corollary",
|
|
18
|
+
definition: "Definition",
|
|
19
|
+
example: "Example",
|
|
20
|
+
exercise: "Exercise",
|
|
21
|
+
remark: "Remark",
|
|
22
|
+
proof: "Proof"
|
|
23
|
+
};
|
|
24
|
+
function isTheoremKind(value) {
|
|
25
|
+
return value !== void 0 && THEOREM_KINDS.includes(value);
|
|
26
|
+
}
|
|
27
|
+
function theoremLabel(props) {
|
|
28
|
+
const raw = props.kind ?? props.type;
|
|
29
|
+
if (!isTheoremKind(raw)) {
|
|
30
|
+
const detail = raw === void 0 ? "no kind= (or legacy type=) prop was passed" : `kind="${raw}" is not one of ${THEOREM_KINDS.join(", ")}`;
|
|
31
|
+
const hint = props.type !== void 0 && props.kind === void 0 ? " (you passed type=; kind= is canonical, type= is accepted)" : "";
|
|
32
|
+
throw new Error(
|
|
33
|
+
`<Theorem>: cannot resolve a label \u2014 ${detail}. Pass a valid kind, e.g. kind="theorem"${hint}.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const name = props.name ?? props.title ?? props.label;
|
|
37
|
+
const numbered = props.n ? `${KIND_LABEL[raw]} ${props.n}` : KIND_LABEL[raw];
|
|
38
|
+
const fullLabel = name ? `${numbered} (${name})` : numbered;
|
|
39
|
+
return { kind: raw, fullLabel };
|
|
40
|
+
}
|
|
41
|
+
function resolveTheoremNumber(entry, n) {
|
|
42
|
+
return entry?.number ?? n;
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
KIND_LABEL,
|
|
46
|
+
THEOREM_KINDS,
|
|
47
|
+
resolveTheoremNumber,
|
|
48
|
+
theoremLabel
|
|
49
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.18.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
package/scripts/build-labels.mjs
CHANGED
|
@@ -4,9 +4,18 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Walks the consumer's `src/content/chapters/**\/*.mdx`, extracts each
|
|
6
6
|
* labelable component invocation (Theorem, Figure, Section, … — see
|
|
7
|
-
* `LABELABLE_TYPES` below), and assigns it
|
|
8
|
-
*
|
|
9
|
-
* convention. The
|
|
7
|
+
* `LABELABLE_TYPES` below), and assigns it an entry of the form
|
|
8
|
+
* { href, display: "Theorem 4.2", number: "4.2" }
|
|
9
|
+
* matching the LaTeX `\cref` convention. The map is consumed by XRef.astro
|
|
10
|
+
* (display + href) AND Theorem.astro (#126: `number`, so a heading auto-
|
|
11
|
+
* numbers from the same source the xref reads — they agree by construction).
|
|
12
|
+
*
|
|
13
|
+
* Kind-aware (#126): a `<Theorem kind="proposition">` resolves its display
|
|
14
|
+
* WORD through the shared theorem-label vocabulary (`Proposition 8.1`), not a
|
|
15
|
+
* kind-blind `Theorem 8.1`. The theorem family shares one counter (keyed by
|
|
16
|
+
* the JSX component, as amsthm shares its counter), so numbers are unchanged.
|
|
17
|
+
*
|
|
18
|
+
* The resulting map is consumed by XRef.astro via
|
|
10
19
|
* `import.meta.glob('/src/data/labels.json', { eager: true })`.
|
|
11
20
|
*
|
|
12
21
|
* Per-chapter, per-type counter: each chapter resets the counter, so two
|
|
@@ -37,6 +46,12 @@
|
|
|
37
46
|
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
38
47
|
import { resolve, relative, join, basename, dirname } from 'node:path';
|
|
39
48
|
import { readChaptersBase } from './walk-mdx.mjs';
|
|
49
|
+
// #126: reuse the ONE kind vocabulary (theorem-label.ts → its own lean tsup
|
|
50
|
+
// entry) so a <Theorem kind="proposition"> xref reads "Proposition N.M", not a
|
|
51
|
+
// kind-blind "Theorem N.M" — and so an unknown/absent kind FAILS HERE (same
|
|
52
|
+
// throw as the render path, #121) one build step earlier. Requires `dist/`
|
|
53
|
+
// (run `npm run build` first; the published tarball ships dist + scripts).
|
|
54
|
+
import { theoremLabel } from '../dist/lib/theorem-label.mjs';
|
|
40
55
|
|
|
41
56
|
// --help / -h: non-mutating (closes #14).
|
|
42
57
|
const USAGE = `Usage: book-scaffold build-labels
|
|
@@ -188,20 +203,52 @@ async function main() {
|
|
|
188
203
|
let foundInChapter = 0;
|
|
189
204
|
|
|
190
205
|
for (const match of source.matchAll(tagRegex)) {
|
|
191
|
-
const [,
|
|
206
|
+
const [, componentName, attrs] = match;
|
|
192
207
|
const id = extractAttr(attrs, 'id');
|
|
193
208
|
if (!id) continue;
|
|
194
209
|
|
|
195
|
-
|
|
210
|
+
// One shared counter per component (keyed by the JSX name, NOT the
|
|
211
|
+
// amsthm kind) — so theorem/proposition/lemma share a sequence exactly
|
|
212
|
+
// as they do under amsthm, and existing numbers never shift (#126).
|
|
213
|
+
counters[componentName] = (counters[componentName] ?? 0) + 1;
|
|
196
214
|
foundInChapter += 1;
|
|
197
215
|
totalIds += 1;
|
|
198
216
|
|
|
217
|
+
// Resolve the display word only when it will actually be used. A `label=`
|
|
218
|
+
// override supplies its own display, so we neither compute nor (for
|
|
219
|
+
// <Theorem>) kind-validate it — computing would throw on a kindless
|
|
220
|
+
// override, the documented `<Theorem id label="…">` form. For <Theorem>
|
|
221
|
+
// the word is kind-aware and THROWS on an absent/unknown kind (the #121
|
|
222
|
+
// contract, one build step earlier than render). extractAttr returns null
|
|
223
|
+
// for an absent attr → normalize to undefined so theoremLabel reports
|
|
224
|
+
// "no kind=" rather than the misleading kind="null".
|
|
199
225
|
const labelOverride = extractAttr(attrs, 'label');
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
(
|
|
203
|
-
|
|
204
|
-
|
|
226
|
+
let word;
|
|
227
|
+
if (labelOverride == null) {
|
|
228
|
+
if (componentName === 'Theorem') {
|
|
229
|
+
try {
|
|
230
|
+
word = theoremLabel({
|
|
231
|
+
kind: extractAttr(attrs, 'kind') ?? undefined,
|
|
232
|
+
type: extractAttr(attrs, 'type') ?? undefined,
|
|
233
|
+
}).fullLabel;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`<Theorem id="${id}"> in ${relative(cwd, file)}: ${err.message}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
word = TYPE_DISPLAY[componentName];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The bare counter string the heading reuses: Theorem.astro reads
|
|
245
|
+
// `number` by id and renders it, so heading == xref by construction.
|
|
246
|
+
// A `label=` override opts out of auto-numbering → number is null.
|
|
247
|
+
const number =
|
|
248
|
+
chapterNum != null
|
|
249
|
+
? `${chapterNum}.${counters[componentName]}`
|
|
250
|
+
: String(counters[componentName]);
|
|
251
|
+
const display = labelOverride ?? `${word} ${number}`;
|
|
205
252
|
|
|
206
253
|
if (labels[id]) {
|
|
207
254
|
// Duplicate id — surface but don't fail; consumer's validator
|
|
@@ -214,6 +261,7 @@ async function main() {
|
|
|
214
261
|
labels[id] = {
|
|
215
262
|
href: `/chapters/${slug}#${id}`,
|
|
216
263
|
display,
|
|
264
|
+
number: labelOverride ? null : number,
|
|
217
265
|
};
|
|
218
266
|
}
|
|
219
267
|
|
package/scripts/validate.mjs
CHANGED
|
@@ -314,14 +314,27 @@ for (const rel of chapterFiles) {
|
|
|
314
314
|
// 6. Theorem requires a resolvable kind (#121) — kind= canonical, type=
|
|
315
315
|
// legacy alias. Catches the silent-empty-label / build-throw case at the
|
|
316
316
|
// earliest gate. (Value typos are caught at build by theoremLabel's throw.)
|
|
317
|
+
// Plus (#126): an id'd <Theorem> without a label= override auto-numbers
|
|
318
|
+
// from labels.json — an id absent from the index silently renders the
|
|
319
|
+
// heading UNNUMBERED (no [?id] placeholder, unlike <XRef>). Fail loud to
|
|
320
|
+
// restore symmetry with check #2. (A label= override opts out → number:null.)
|
|
317
321
|
for (const m of content.matchAll(RE_THEOREM)) {
|
|
318
|
-
|
|
322
|
+
const attrs = m[1];
|
|
323
|
+
if (!/\b(?:kind|type)\s*=/.test(attrs)) {
|
|
319
324
|
fail(
|
|
320
325
|
rel,
|
|
321
326
|
lineOf(content, m.index),
|
|
322
327
|
`<Theorem> has no kind= (or legacy type=) — renders an empty label / throws at build. Add e.g. kind="theorem".`,
|
|
323
328
|
);
|
|
324
329
|
}
|
|
330
|
+
const thmId = attrs.match(/\bid=["']([^"']+)["']/);
|
|
331
|
+
if (thmId && !/\blabel\s*=\s*["']/.test(attrs) && !labels[thmId[1]]) {
|
|
332
|
+
fail(
|
|
333
|
+
rel,
|
|
334
|
+
lineOf(content, m.index),
|
|
335
|
+
`<Theorem id="${thmId[1]}"> — not in labels.json; heading silently renders unnumbered. Run build:labels, or fix the id.`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
325
338
|
}
|
|
326
339
|
|
|
327
340
|
// 7. BookLink (#96): structural (book= + to=) + best-effort registry membership.
|
|
@@ -431,7 +444,9 @@ let questionsChecked = 0;
|
|
|
431
444
|
}
|
|
432
445
|
}
|
|
433
446
|
const labelsPath = join(DATA_DIR, 'labels.json');
|
|
434
|
-
|
|
447
|
+
// #126: also collapse <Theorem id> not-in-labels errors (they share the
|
|
448
|
+
// "not in labels.json" phrase) under the same missing-prereq message.
|
|
449
|
+
const hasXrefErrors = errors.some((e) => /not in labels\.json/.test(e.msg));
|
|
435
450
|
if (hasXrefErrors && !existsSync(labelsPath)) {
|
|
436
451
|
console.error(
|
|
437
452
|
`\n✗ Validate cannot run: src/data/labels.json is missing.\n\n` +
|
package/src/lib/theorem-label.ts
CHANGED
|
@@ -96,3 +96,22 @@ export function theoremLabel(props: TheoremLabelProps): ResolvedTheoremLabel {
|
|
|
96
96
|
const fullLabel = name ? `${numbered} (${name})` : numbered;
|
|
97
97
|
return { kind: raw, fullLabel };
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a theorem's display number for {@link theoremLabel}'s `n` (#126). The
|
|
102
|
+
* labels.json entry's `number` — the single source `<XRef>` also reads — wins;
|
|
103
|
+
* an explicit `n=` is the fallback for an un-id'd theorem (or before
|
|
104
|
+
* labels.json is built). Both `null` (a `label=` display override that opted
|
|
105
|
+
* out of auto-numbering) and `undefined` (no entry) fall through to `n`.
|
|
106
|
+
*
|
|
107
|
+
* The #126 invariant in one place: when an id resolves, the index wins, so a
|
|
108
|
+
* stale `n=` can't reintroduce heading/cross-reference drift. Extracted from
|
|
109
|
+
* `Theorem.astro` so the precedence is unit-tested in the pure node:test suite
|
|
110
|
+
* (Astro is peerDep-only — the `.astro` frontmatter can't be loaded there).
|
|
111
|
+
*/
|
|
112
|
+
export function resolveTheoremNumber(
|
|
113
|
+
entry: { number?: string | null } | undefined,
|
|
114
|
+
n: string | undefined,
|
|
115
|
+
): string | undefined {
|
|
116
|
+
return entry?.number ?? n;
|
|
117
|
+
}
|