explico 0.11.1 → 0.12.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/CHANGELOG.md +171 -1
- package/dist/compiler/astro.d.ts +7 -1
- package/dist/compiler/astro.js +5 -2
- package/dist/compiler/cli.d.ts +1 -0
- package/dist/compiler/collections.d.ts +25 -2
- package/dist/compiler/collections.js +37 -4
- package/dist/compiler/exports/standalone-html.d.ts +1 -0
- package/dist/compiler/manifest.d.ts +30 -4
- package/dist/compiler/manifest.js +49 -10
- package/dist/compiler/resolution-report.d.ts +1 -0
- package/dist/reference/base-operators.d.mts +6 -0
- package/dist/reference/base-operators.mjs +6 -8
- package/dist/reference/figure-symbols.d.ts +32 -0
- package/dist/reference/figure-symbols.js +55 -0
- package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
- package/dist/reference/gate-non-chapter-pages.js +156 -0
- package/dist/reference/gate-part-pages.d.ts +3 -1
- package/dist/reference/gate-part-pages.js +2 -1
- package/dist/reference/lab-katex-validate.d.ts +43 -0
- package/dist/reference/lab-katex-validate.js +96 -0
- package/dist/reference/lint-ignore.js +1 -1
- package/dist/reference/page-notation.d.ts +5 -3
- package/dist/reference/page-notation.js +8 -8
- package/dist/reference/raw-floats.mjs +37 -13
- package/dist/reference/registry.js +11 -1
- package/dist/reference/remark-notation.mjs +195 -7
- package/dist/reference/render-lab-math.d.ts +11 -2
- package/dist/reference/render-lab-math.js +2 -2
- package/dist/rules/catalog.js +33 -3
- package/dist/rules/rules.schema.json +22 -4
- package/docs/authoring/README.md +2 -1
- package/docs/authoring/components.md +4 -4
- package/docs/authoring/migrating.md +215 -0
- package/docs/authoring/notation.md +4 -1
- package/docs/authoring/structure-rules.md +75 -5
- package/docs/authoring/structure.md +14 -0
- package/docs/authoring/visualizations.md +26 -10
- package/package.json +1 -1
- package/src/compiler/astro.ts +17 -6
- package/src/compiler/collections.ts +67 -9
- package/src/compiler/manifest.ts +89 -13
- package/src/components/CourseContents.astro +6 -0
- package/src/components/PartOverview.astro +6 -0
- package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
- package/src/components/disclosure/print-as-shown.ts +42 -6
- package/src/components/figure/Figure.astro +6 -1
- package/src/components/figure/figure-state.ts +26 -17
- package/src/components/starlight/LessonFooter.astro +27 -7
- package/src/components/table/Table.astro +1 -1
- package/src/reference/base-operators.mjs +6 -8
- package/src/reference/figure-symbols.ts +117 -0
- package/src/reference/gate-non-chapter-pages.ts +241 -0
- package/src/reference/gate-part-pages.ts +1 -1
- package/src/reference/lab-katex-validate.ts +156 -0
- package/src/reference/lint-ignore.ts +4 -1
- package/src/reference/page-notation.ts +14 -11
- package/src/reference/raw-floats.mjs +37 -13
- package/src/reference/registry.ts +16 -1
- package/src/reference/remark-notation.mjs +195 -7
- package/src/reference/render-lab-math.ts +12 -1
- package/src/rules/catalog.ts +37 -3
- package/src/styles/global.css +32 -9
package/src/compiler/manifest.ts
CHANGED
|
@@ -84,6 +84,10 @@ import {
|
|
|
84
84
|
validateRawFloats,
|
|
85
85
|
type RawFloatDiagnostic,
|
|
86
86
|
} from '../reference/raw-floats-validate';
|
|
87
|
+
import {
|
|
88
|
+
validateLabKatexImports,
|
|
89
|
+
type LabKatexDiagnostic,
|
|
90
|
+
} from '../reference/lab-katex-validate';
|
|
87
91
|
import {
|
|
88
92
|
gateContentMath,
|
|
89
93
|
pageScope,
|
|
@@ -102,6 +106,7 @@ import { gateCourseRules } from '../reference/gate-course-rules';
|
|
|
102
106
|
import { gateHeadingLength } from '../reference/gate-heading-length';
|
|
103
107
|
import { gateHeadingStructure } from '../reference/gate-heading-structure';
|
|
104
108
|
import { gatePartPages } from '../reference/gate-part-pages';
|
|
109
|
+
import { gateNonChapterPages } from '../reference/gate-non-chapter-pages';
|
|
105
110
|
import {
|
|
106
111
|
scanSectionHeadings,
|
|
107
112
|
scanTitledBlocks,
|
|
@@ -161,6 +166,7 @@ import {
|
|
|
161
166
|
loadCurriculumCatalog,
|
|
162
167
|
loadNotationDefinitions,
|
|
163
168
|
loadNotationRegistryInput,
|
|
169
|
+
loadNonChapterPages,
|
|
164
170
|
loadNotationSpecs,
|
|
165
171
|
loadPartOverviews,
|
|
166
172
|
loadSourceRecords,
|
|
@@ -290,8 +296,14 @@ import {
|
|
|
290
296
|
* state of a `<Fold>`; `disclosure.figures` now says whether a lab's tools
|
|
291
297
|
* start open; `diagnostics.folds` holds `fold-unnamed`
|
|
292
298
|
* (docs/plans/figures-and-short-captions.md, Parts 1–2).
|
|
299
|
+
*
|
|
300
|
+
* v25 (cards on non-chapter pages, rule S10): `pages` — every docs page that
|
|
301
|
+
* is not a chapter (the landing page, a part's overview page, …) with its
|
|
302
|
+
* `id`, `slug`, `file` and a `notation` bundle of the lesson shape, so the
|
|
303
|
+
* footer renders the cards its `[[key]]` terms and math open.
|
|
304
|
+
* `notation.bundles` and `notation.backlinks` now carry those pages' ids too.
|
|
293
305
|
*/
|
|
294
|
-
export const MANIFEST_SCHEMA_VERSION =
|
|
306
|
+
export const MANIFEST_SCHEMA_VERSION = 25;
|
|
295
307
|
|
|
296
308
|
const REPOSITORY_ROOT = process.cwd();
|
|
297
309
|
// Content is the top-level `content/` folder; the build output is `build/`
|
|
@@ -502,6 +514,20 @@ export interface ManifestLesson {
|
|
|
502
514
|
};
|
|
503
515
|
}
|
|
504
516
|
|
|
517
|
+
/**
|
|
518
|
+
* A docs page that is not a chapter (v25, rule S10) — the landing page, a
|
|
519
|
+
* part's overview page, any page outside a part directory. It numbers
|
|
520
|
+
* nothing, so it carries only what its cards need: the notation bundle its
|
|
521
|
+
* `[[key]]` terms and math reach, shared entries only.
|
|
522
|
+
*/
|
|
523
|
+
export interface ManifestPage {
|
|
524
|
+
/** `index`, `motion.index`, `glossary` — `lessonIdFromSlug` of the slug; never a lesson id. */
|
|
525
|
+
readonly id: string;
|
|
526
|
+
readonly slug: string;
|
|
527
|
+
readonly file: string;
|
|
528
|
+
readonly notation: ManifestLesson['notation'];
|
|
529
|
+
}
|
|
530
|
+
|
|
505
531
|
/** The reader-facing "about this material" bundle — the `[?]` panel's data. */
|
|
506
532
|
export interface ManifestAbout {
|
|
507
533
|
/** `explico`'s own identity, from its `package.json` (the version's golden source). */
|
|
@@ -597,6 +623,8 @@ export interface Manifest {
|
|
|
597
623
|
/** The course's parts in config order, with their chapters and overview pages. */
|
|
598
624
|
readonly parts: readonly ManifestPart[];
|
|
599
625
|
readonly lessons: readonly ManifestLesson[];
|
|
626
|
+
/** Every docs page that is not a chapter, by id (v25). */
|
|
627
|
+
readonly pages: readonly ManifestPage[];
|
|
600
628
|
/** Full competency prerequisite graph. */
|
|
601
629
|
readonly prereqEdges: readonly PrereqEdge[];
|
|
602
630
|
/** Equation identity (D7): every labelled display equation + the render-path lookup. */
|
|
@@ -660,8 +688,12 @@ export interface Manifest {
|
|
|
660
688
|
readonly equations: readonly EquationDiagnostic[];
|
|
661
689
|
/** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
|
|
662
690
|
readonly tables: readonly TableDiagnostic[];
|
|
663
|
-
/**
|
|
664
|
-
|
|
691
|
+
/**
|
|
692
|
+
* `<Figure>` identity checks (F-tables-figures) — same codes, `fig-`
|
|
693
|
+
* prefixed — and rule V3's `lab-katex-direct`: a file a lesson imports
|
|
694
|
+
* that calls KaTeX itself.
|
|
695
|
+
*/
|
|
696
|
+
readonly figures: readonly (FigureDiagnostic | LabKatexDiagnostic)[];
|
|
665
697
|
/** `<Diagram>` identity checks — same codes, `dia-` prefixed. */
|
|
666
698
|
readonly diagrams: readonly DiagramDiagnostic[];
|
|
667
699
|
/** Knowledge-check identity and `[[check-…]]` reference checks. */
|
|
@@ -925,7 +957,19 @@ export async function compileManifest(
|
|
|
925
957
|
loadAbbreviationRegistryInput(),
|
|
926
958
|
]);
|
|
927
959
|
|
|
928
|
-
|
|
960
|
+
// Rule S10: a page that is not a chapter explains its symbols like a
|
|
961
|
+
// chapter does, so its references resolve through the same registry and its
|
|
962
|
+
// text and math pass the same gates. It stays out of `notationInput.lessons`,
|
|
963
|
+
// which is the chapter list every numbering and curriculum pass reads.
|
|
964
|
+
const nonChapterPages = loadNonChapterPages();
|
|
965
|
+
const withPages: typeof notationInput = {
|
|
966
|
+
...notationInput,
|
|
967
|
+
lessons: [
|
|
968
|
+
...notationInput.lessons,
|
|
969
|
+
...nonChapterPages.map((page) => page.notation),
|
|
970
|
+
],
|
|
971
|
+
};
|
|
972
|
+
const registry = buildNotationRegistry(withPages);
|
|
929
973
|
// Cards a `formula` may name with `\explain{key}` (D15). Shared entries
|
|
930
974
|
// only: a page-local symbol is not addressable from another entry.
|
|
931
975
|
const cardsByKey = new Map<string, GlyphScopeEntry>(
|
|
@@ -956,7 +1000,7 @@ export async function compileManifest(
|
|
|
956
1000
|
};
|
|
957
1001
|
const mathIssues = [
|
|
958
1002
|
...gateContentMath({
|
|
959
|
-
notationInput,
|
|
1003
|
+
notationInput: withPages,
|
|
960
1004
|
assessments: assessmentsInput,
|
|
961
1005
|
lessonAssessments: catalog.lessons.map((lesson) => ({
|
|
962
1006
|
lessonId: lesson.id,
|
|
@@ -965,7 +1009,7 @@ export async function compileManifest(
|
|
|
965
1009
|
}),
|
|
966
1010
|
...(rules.isEnabled('N9')
|
|
967
1011
|
? [
|
|
968
|
-
...gateProseNotation(
|
|
1012
|
+
...gateProseNotation(withPages.lessons, {
|
|
969
1013
|
allowSpelledUnits: rules.strings('N9', 'allow'),
|
|
970
1014
|
}),
|
|
971
1015
|
...gateProseNotationInAssessments(assessmentsInput, {
|
|
@@ -975,13 +1019,13 @@ export async function compileManifest(
|
|
|
975
1019
|
: []),
|
|
976
1020
|
...(rules.isEnabled('N12')
|
|
977
1021
|
? [
|
|
978
|
-
...gateMathPunctuation(
|
|
1022
|
+
...gateMathPunctuation(withPages.lessons, mathEndAbbreviations),
|
|
979
1023
|
...gateMathPunctuationInAssessments(assessmentsInput, mathEndAbbreviations),
|
|
980
1024
|
]
|
|
981
1025
|
: []),
|
|
982
1026
|
...(rules.isEnabled('N14')
|
|
983
1027
|
? gateAbbreviations({
|
|
984
|
-
notationInput,
|
|
1028
|
+
notationInput: withPages,
|
|
985
1029
|
abbreviations: abbreviationsInput,
|
|
986
1030
|
courseAbbreviations: rules.settings('N14'),
|
|
987
1031
|
})
|
|
@@ -998,7 +1042,9 @@ export async function compileManifest(
|
|
|
998
1042
|
// The course's own rules (`courseRules`): one pass for every enabled `X`
|
|
999
1043
|
// rule that carries a `forbid` pattern. A disabled one, and a guide rule
|
|
1000
1044
|
// with no pattern, do no work here.
|
|
1001
|
-
...gateCourseRules(
|
|
1045
|
+
...gateCourseRules(withPages.lessons, rules.course),
|
|
1046
|
+
// S10 is locked: a float on a non-chapter page has no number to render.
|
|
1047
|
+
...gateNonChapterPages(nonChapterPages),
|
|
1002
1048
|
];
|
|
1003
1049
|
|
|
1004
1050
|
// Pre-render every assessment `prompt` / `explanation` into text/math
|
|
@@ -1077,10 +1123,12 @@ export async function compileManifest(
|
|
|
1077
1123
|
}
|
|
1078
1124
|
// Per-place waivers: drop the located findings each one covers, and
|
|
1079
1125
|
// report the waivers themselves — a stale one is a warning, a bad one an
|
|
1080
|
-
// error.
|
|
1126
|
+
// error. Chapters and non-chapter pages are scanned alike, since the text
|
|
1127
|
+
// and math gates run on both; S2 and S10 are per-file or locked, so no
|
|
1128
|
+
// waiver reaches them.
|
|
1081
1129
|
const waiverDiagnostics: WaiverDiagnostic[] = [];
|
|
1082
1130
|
const waiverRules = { courseRules: rules.course };
|
|
1083
|
-
const waivers =
|
|
1131
|
+
const waivers = withPages.lessons.flatMap((lesson) => {
|
|
1084
1132
|
const collected = collectWaivers(lesson, waiverRules);
|
|
1085
1133
|
waiverDiagnostics.push(...collected.diagnostics);
|
|
1086
1134
|
return collected.waivers;
|
|
@@ -1369,6 +1417,16 @@ export async function compileManifest(
|
|
|
1369
1417
|
},
|
|
1370
1418
|
);
|
|
1371
1419
|
};
|
|
1420
|
+
// Rule V3: the course files a lesson's imports reach draw math through
|
|
1421
|
+
// `renderLabMath`, never KaTeX itself.
|
|
1422
|
+
const labKatexDiagnostics = validateLabKatexImports(
|
|
1423
|
+
notationInput.lessons.map((lesson) => ({
|
|
1424
|
+
lessonId: lesson.lessonId,
|
|
1425
|
+
file: lesson.source.file,
|
|
1426
|
+
body: lesson.body,
|
|
1427
|
+
})),
|
|
1428
|
+
REPOSITORY_ROOT,
|
|
1429
|
+
);
|
|
1372
1430
|
const rawFloatDiagnostics = validateRawFloats(
|
|
1373
1431
|
notationInput.lessons.map((lesson) => ({
|
|
1374
1432
|
lessonId: lesson.lessonId,
|
|
@@ -1446,10 +1504,27 @@ export async function compileManifest(
|
|
|
1446
1504
|
});
|
|
1447
1505
|
}
|
|
1448
1506
|
lessons.sort((a, b) => a.id.localeCompare(b.id));
|
|
1507
|
+
const pages: ManifestPage[] = nonChapterPages
|
|
1508
|
+
.map((page) => ({
|
|
1509
|
+
id: page.id,
|
|
1510
|
+
slug: page.slug,
|
|
1511
|
+
file: page.file,
|
|
1512
|
+
notation: lessonNotationBundle(
|
|
1513
|
+
bundlesByLesson.get(page.id),
|
|
1514
|
+
definitionsById,
|
|
1515
|
+
lessonNotationResolution(
|
|
1516
|
+
page.notation,
|
|
1517
|
+
definitionsById,
|
|
1518
|
+
availableDefinitionIds,
|
|
1519
|
+
),
|
|
1520
|
+
cardsByKey,
|
|
1521
|
+
),
|
|
1522
|
+
}))
|
|
1523
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1449
1524
|
|
|
1450
1525
|
const consistencyIssues = runConsistencyChecks({
|
|
1451
1526
|
registry,
|
|
1452
|
-
notationInput,
|
|
1527
|
+
notationInput: withPages,
|
|
1453
1528
|
specs: loadNotationSpecs(),
|
|
1454
1529
|
catalog,
|
|
1455
1530
|
lintIgnore: loadLintIgnore(),
|
|
@@ -1543,6 +1618,7 @@ export async function compileManifest(
|
|
|
1543
1618
|
sidebar,
|
|
1544
1619
|
parts: manifestParts,
|
|
1545
1620
|
lessons,
|
|
1621
|
+
pages,
|
|
1546
1622
|
prereqEdges,
|
|
1547
1623
|
equations: {
|
|
1548
1624
|
labels: equationValidation.labels,
|
|
@@ -1578,7 +1654,7 @@ export async function compileManifest(
|
|
|
1578
1654
|
consistency: dropDisabled(consistencyIssues, off),
|
|
1579
1655
|
equations: dropDisabled(equationValidation.diagnostics, off),
|
|
1580
1656
|
tables: dropDisabled(tableValidation.diagnostics, off),
|
|
1581
|
-
figures: dropDisabled(figureValidation.diagnostics, off),
|
|
1657
|
+
figures: dropDisabled([...figureValidation.diagnostics, ...labKatexDiagnostics], off),
|
|
1582
1658
|
diagrams: dropDisabled(diagramValidation.diagnostics, off),
|
|
1583
1659
|
checks: dropDisabled(checkValidation.diagnostics, off),
|
|
1584
1660
|
examples: dropDisabled(exampleValidation.diagnostics, off),
|
|
@@ -121,6 +121,12 @@ const hours = (range: { min: number; max: number }) => {
|
|
|
121
121
|
margin-block-start: 1.5rem;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/* `.not-content` keeps Starlight's Markdown link colour out, so the
|
|
125
|
+
contents set the course accent on their own links. */
|
|
126
|
+
.course-contents a {
|
|
127
|
+
color: var(--sl-color-text-accent);
|
|
128
|
+
}
|
|
129
|
+
|
|
124
130
|
.course-meta,
|
|
125
131
|
.part-line {
|
|
126
132
|
display: flex;
|
|
@@ -262,6 +262,12 @@ const hasRail =
|
|
|
262
262
|
margin-block-start: 1.5rem;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
/* `.not-content` keeps Starlight's Markdown link colour out, so the
|
|
266
|
+
overview sets the course accent on its own links. */
|
|
267
|
+
.part-overview a {
|
|
268
|
+
color: var(--sl-color-text-accent);
|
|
269
|
+
}
|
|
270
|
+
|
|
265
271
|
.part-meta {
|
|
266
272
|
display: flex;
|
|
267
273
|
flex-wrap: wrap;
|
|
@@ -21,8 +21,8 @@ const entries = (await getCollection('abbreviations')).sort((left, right) =>
|
|
|
21
21
|
) : (
|
|
22
22
|
<dl class="abbreviation-glossary">
|
|
23
23
|
{entries.map((entry) => (
|
|
24
|
-
<div class="abbreviation-entry"
|
|
25
|
-
<dt>
|
|
24
|
+
<div class="abbreviation-entry">
|
|
25
|
+
<dt id={`abbr-${entry.data.key}`}>
|
|
26
26
|
{entry.data.abbr}
|
|
27
27
|
{entry.data.aliases.length > 0 && (
|
|
28
28
|
<span class="abbreviation-aliases">
|
|
@@ -46,6 +46,9 @@ const entries = (await getCollection('abbreviations')).sort((left, right) =>
|
|
|
46
46
|
row-gap: 0.75rem;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/* The wrapper puts each `<dt>`/`<dd>` pair straight into the grid, so it
|
|
50
|
+
has no box of its own: the entry's anchor id sits on the `<dt>`, since
|
|
51
|
+
an element with no box can never be scrolled to. */
|
|
49
52
|
.abbreviation-entry {
|
|
50
53
|
display: contents;
|
|
51
54
|
}
|
|
@@ -53,6 +56,7 @@ const entries = (await getCollection('abbreviations')).sort((left, right) =>
|
|
|
53
56
|
.abbreviation-entry dt {
|
|
54
57
|
font-weight: 600;
|
|
55
58
|
white-space: nowrap;
|
|
59
|
+
scroll-margin-block-start: 5rem;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
.abbreviation-entry dd {
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
* never prints, open or folded, and is left exactly as it is: no note, not
|
|
17
17
|
* opened for an export.
|
|
18
18
|
*
|
|
19
|
+
* A note never goes inside an Astro island. An island still waiting to
|
|
20
|
+
* hydrate (`client:visible` — every knowledge check) that starts while the
|
|
21
|
+
* page is prepared for print would find a node its framework never rendered
|
|
22
|
+
* and throw away its server render (React's error #418); a hydrated one's
|
|
23
|
+
* markup is its framework's to own either way. A closed disclosure inside
|
|
24
|
+
* an island gets its note after the outermost island instead, once per
|
|
25
|
+
* target URL: the folded questions of one check all point at the check's
|
|
26
|
+
* own anchor, so one note after the check says it for all of them.
|
|
27
|
+
*
|
|
19
28
|
* Two callers share this module:
|
|
20
29
|
* - `LessonExportActions.astro`, the print controller a course gets with
|
|
21
30
|
* `lessonExports.print`: `'as-shown'` for its print button and the
|
|
@@ -90,6 +99,15 @@ function anchorIdFor(element: Element): string | undefined {
|
|
|
90
99
|
return undefined;
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
/** The outermost `<astro-island>` around an element, if any. */
|
|
103
|
+
function outermostIsland(element: Element): Element | null {
|
|
104
|
+
let island: Element | null = null;
|
|
105
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
106
|
+
if (node.tagName === 'ASTRO-ISLAND') island = node;
|
|
107
|
+
}
|
|
108
|
+
return island;
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
function createNote(href: string): HTMLElement {
|
|
94
112
|
const note = document.createElement('div');
|
|
95
113
|
note.className = COLLAPSED_NOTE_CLASS;
|
|
@@ -152,20 +170,38 @@ export function prepareDisclosures(
|
|
|
152
170
|
const insideClosed = (element: Element) =>
|
|
153
171
|
!!element.parentElement?.closest('details:not([open])');
|
|
154
172
|
|
|
173
|
+
// The notes already placed after each island: the last one, which the
|
|
174
|
+
// next follows, and the URLs they name.
|
|
175
|
+
const afterIsland = new Map<Element, { last: Element; hrefs: Set<string> }>();
|
|
176
|
+
const place = (element: Element, where: 'before' | 'after') => {
|
|
177
|
+
const href = options.noteHref(anchorIdFor(element));
|
|
178
|
+
const island = outermostIsland(element);
|
|
179
|
+
if (!island) {
|
|
180
|
+
const note = createNote(href);
|
|
181
|
+
element[where](note);
|
|
182
|
+
notes.push(note);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const placed = afterIsland.get(island) ?? { last: island, hrefs: new Set<string>() };
|
|
186
|
+
if (placed.hrefs.has(href)) return;
|
|
187
|
+
const note = createNote(href);
|
|
188
|
+
placed.last.after(note);
|
|
189
|
+
placed.last = note;
|
|
190
|
+
placed.hrefs.add(href);
|
|
191
|
+
afterIsland.set(island, placed);
|
|
192
|
+
notes.push(note);
|
|
193
|
+
};
|
|
194
|
+
|
|
155
195
|
for (const details of main.querySelectorAll('details:not([open]):not([data-print-omit])')) {
|
|
156
196
|
if (insideClosed(details)) continue;
|
|
157
|
-
|
|
158
|
-
details.after(note);
|
|
159
|
-
notes.push(note);
|
|
197
|
+
place(details, 'after');
|
|
160
198
|
}
|
|
161
199
|
// Disclosures built on `hidden` rather than `<details>`.
|
|
162
200
|
for (const element of main.querySelectorAll(
|
|
163
201
|
':is(.short-note, .assessment-panel)[hidden]',
|
|
164
202
|
)) {
|
|
165
203
|
if (insideClosed(element)) continue;
|
|
166
|
-
|
|
167
|
-
element.before(note);
|
|
168
|
-
notes.push(note);
|
|
204
|
+
place(element, 'before');
|
|
169
205
|
}
|
|
170
206
|
}
|
|
171
207
|
|
|
@@ -84,7 +84,7 @@ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
|
|
|
84
84
|
}
|
|
85
85
|
if (!number || !anchorId) {
|
|
86
86
|
throw new Error(
|
|
87
|
-
|
|
87
|
+
`Figure is missing its computed \`number\`/\`anchorId\`. remark-notation numbers a <Figure> only on a lesson page, at its top level or in a <Memo>, <ShortNote> or <Fold>: elsewhere (a worked example) show the content without <Figure>, and on a page that is not a chapter (rule S10) link the chapter's figure. At a lesson's top level, this page is not running remark-notation with the manifest's \`figures\` (see astro.config.mjs).`,
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -176,8 +176,13 @@ const name = `Figure ${number}`;
|
|
|
176
176
|
Q8). A figure that should read as set apart goes in a `### … [card]`
|
|
177
177
|
section. The caption line is styled once for every float, in
|
|
178
178
|
`global.css` (`.float-caption`). */
|
|
179
|
+
/* A wide picture scrolls sideways. `overflow-x: auto` alone computes
|
|
180
|
+
`overflow-y` to `auto` too, and a body with no fixed height overflows
|
|
181
|
+
vertically only by rounding (a fractional height at a browser zoom),
|
|
182
|
+
which drew a one-pixel vertical scrollbar beside the figure. */
|
|
179
183
|
.keyed-figure__body {
|
|
180
184
|
overflow-x: auto;
|
|
185
|
+
overflow-y: hidden;
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
/* A lab's tools: a tray of their own after the caption line — a thin
|
|
@@ -7,35 +7,44 @@
|
|
|
7
7
|
* caption (a preview card, a fold's bar, the list of figures) still reads the
|
|
8
8
|
* caption alone, and it prints with the caption.
|
|
9
9
|
*
|
|
10
|
-
* A lab passes its clause as runs
|
|
11
|
-
* text, `{
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* A lab passes its clause as runs, never its own HTML: a string is plain
|
|
11
|
+
* text, and `{ math }` is a symbol or a formula — lab math rendered at build
|
|
12
|
+
* time by `renderLabMath(template, scope, { display: false })`, so each of
|
|
13
|
+
* its symbols is bound to the page's notation and opens its card, as it
|
|
14
|
+
* does in the lesson (rule N4). A symbol typed as text — the italic and
|
|
15
|
+
* subscript runs this took before — opened no card and was not even the
|
|
16
|
+
* corpus glyph (an italic `x` for a bold `\mathbf{x}`), so they are gone.
|
|
17
|
+
* There is no `aria-live`: the controls already announce their values.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| { readonly sub: string };
|
|
20
|
+
import type { RenderedLabMath } from '../../reference/lab-math-slot';
|
|
21
|
+
|
|
22
|
+
export type FigureStateRun = string | { readonly math: RenderedLabMath };
|
|
20
23
|
|
|
21
24
|
type NodeFactory = Pick<Document, 'createElement' | 'createTextNode'>;
|
|
22
25
|
|
|
23
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The nodes for `runs`, made by `doc`. Markup in a text run stays text; a
|
|
28
|
+
* math run is `renderLabMath`'s own KaTeX, made at build time.
|
|
29
|
+
*/
|
|
24
30
|
export function figureStateNodes(
|
|
25
31
|
runs: readonly FigureStateRun[],
|
|
26
32
|
doc: NodeFactory,
|
|
27
33
|
): Node[] {
|
|
28
34
|
return runs.map((run) => {
|
|
29
35
|
if (typeof run === 'string') return doc.createTextNode(run);
|
|
30
|
-
if ('
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
if (typeof run === 'object' && run !== null && typeof run.math?.html === 'string') {
|
|
37
|
+
const math = doc.createElement('span');
|
|
38
|
+
math.className = 'float-caption__math';
|
|
39
|
+
math.innerHTML = run.math.html;
|
|
40
|
+
return math;
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
'setFigureState takes text and { math } runs. A symbol in the clause is ' +
|
|
44
|
+
'lab math, rendered with renderLabMath(template, scope, { display: false }) ' +
|
|
45
|
+
'and passed to the island, so that it opens its notation card; an italic ' +
|
|
46
|
+
'or a sub run no longer exists.',
|
|
47
|
+
);
|
|
39
48
|
});
|
|
40
49
|
}
|
|
41
50
|
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* footer. Both layers are optional progressive enhancements over static
|
|
6
6
|
* markup that the remark adapters and the content collections already emit.
|
|
7
7
|
*
|
|
8
|
+
* A page that is not a chapter (rule S10: the landing page, a part's overview
|
|
9
|
+
* page) gets the card layers too — notation, citation, abbreviation and float
|
|
10
|
+
* reference — from its own manifest bundle. The layers that serve a chapter's
|
|
11
|
+
* numbered floats and outline stay on chapters.
|
|
12
|
+
*
|
|
8
13
|
* The resolved notation bundle comes from the one compiler manifest (Phase
|
|
9
14
|
* D5); this component never rebuilds the registry.
|
|
10
15
|
*/
|
|
@@ -38,6 +43,15 @@ const routeSlug = Astro.locals.starlightRoute.entry.id;
|
|
|
38
43
|
const lessonId = isLessonSlug(routeSlug)
|
|
39
44
|
? lessonIdFromSlug(routeSlug)
|
|
40
45
|
: undefined;
|
|
46
|
+
const manifest = await loadManifest();
|
|
47
|
+
// A chapter, or a non-chapter page the manifest knows (not the 404 route).
|
|
48
|
+
// The glob loader shortens `<dir>/index.mdx` to the id `<dir>`, and may give
|
|
49
|
+
// the landing page an empty one, so match the page's docs slug either way.
|
|
50
|
+
const routeSlugs = new Set(
|
|
51
|
+
routeSlug === '' ? ['index'] : [routeSlug, `${routeSlug}/index`],
|
|
52
|
+
);
|
|
53
|
+
const pageId =
|
|
54
|
+
lessonId ?? manifest.pages.find((page) => routeSlugs.has(page.slug))?.id;
|
|
41
55
|
|
|
42
56
|
const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
|
|
43
57
|
|
|
@@ -45,10 +59,11 @@ const definitions: NotationLayerDefinition[] = [];
|
|
|
45
59
|
let notationDefaultOpen = false;
|
|
46
60
|
let numberAliases = false;
|
|
47
61
|
let suspended: NotationSuspension[] = [];
|
|
48
|
-
if (
|
|
49
|
-
const manifest = await loadManifest();
|
|
62
|
+
if (pageId) {
|
|
50
63
|
notationDefaultOpen = manifest.disclosure.notation.defaultOpen;
|
|
51
64
|
numberAliases = manifest.numbering.aliases;
|
|
65
|
+
}
|
|
66
|
+
if (lessonId) {
|
|
52
67
|
const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
|
|
53
68
|
const routeData = Astro.locals.starlightRoute.entry.data as {
|
|
54
69
|
notation?: {
|
|
@@ -74,10 +89,15 @@ if (lessonId) {
|
|
|
74
89
|
.filter((definition) => definition.scope === 'local')
|
|
75
90
|
.map((definition) => definition.key);
|
|
76
91
|
assertLocalNotationParity(lessonId, authoredLocalKeys, renderedLocalKeys);
|
|
92
|
+
}
|
|
93
|
+
if (pageId) {
|
|
94
|
+
// A non-chapter page renders the bundle its body compiled to, and nothing a
|
|
95
|
+
// component registered: the glossary's formulas are its own layer's cards,
|
|
96
|
+
// and rule S10 keeps the floats that would register more off the page.
|
|
77
97
|
const renderedDefinitions = notationDefinitionsForRenderedPage(
|
|
78
98
|
manifest,
|
|
79
|
-
|
|
80
|
-
pageNotationDependencies(Astro.locals),
|
|
99
|
+
pageId,
|
|
100
|
+
lessonId ? pageNotationDependencies(Astro.locals) : [],
|
|
81
101
|
);
|
|
82
102
|
for (const definition of renderedDefinitions) {
|
|
83
103
|
definitions.push({
|
|
@@ -107,10 +127,10 @@ if (lessonId) {
|
|
|
107
127
|
suspended={suspended}
|
|
108
128
|
/>
|
|
109
129
|
<AnchorScrollEnhancer />
|
|
110
|
-
{
|
|
111
|
-
{
|
|
130
|
+
{pageId && <CitationLayer />}
|
|
131
|
+
{pageId && <AbbreviationLayer />}
|
|
112
132
|
{lessonId && <KeyedFloatEnhancer aliases={numberAliases} />}
|
|
113
|
-
{
|
|
133
|
+
{pageId && <FloatRefLayer />}
|
|
114
134
|
{lessonId && <CollapsibleSections />}
|
|
115
135
|
{lessonId && <TableOverflowExpander />}
|
|
116
136
|
<DiagramEnhancer />
|
|
@@ -49,7 +49,7 @@ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
|
|
|
49
49
|
}
|
|
50
50
|
if (!number || !anchorId) {
|
|
51
51
|
throw new Error(
|
|
52
|
-
|
|
52
|
+
`Table is missing its computed \`number\`/\`anchorId\`. remark-notation numbers a <Table> only on a lesson page, at its top level or in a <Memo>, <ShortNote> or <Fold>: elsewhere (a worked example) write a plain Markdown table, and on a page that is not a chapter (rule S10) link the chapter's table. At a lesson's top level, this page is not running remark-notation with the manifest's \`tables\` (see astro.config.mjs).`,
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
---
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
* baseline — trig, the exponential and logarithms, limits, extrema, and the
|
|
14
14
|
* big operators — kept greppable and small like the base library.
|
|
15
15
|
*
|
|
16
|
+
* Only the plain limit, `\lim`, is baseline. `\limsup` / `\liminf` and the
|
|
17
|
+
* category-theory `\injlim` / `\projlim` / `\varlimsup` / `\varliminf` /
|
|
18
|
+
* `\varinjlim` / `\varprojlim` read fine to someone who already knows them,
|
|
19
|
+
* but none is "everyone knows this" the way `\lim` or `\max` is — a course
|
|
20
|
+
* that uses one owes it a notation entry, like any other domain operator.
|
|
21
|
+
*
|
|
16
22
|
* A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
|
|
17
23
|
* written `\operatorname{…}` and is NOT here: it earns a card, a
|
|
18
24
|
* `notation.local` entry, or an explicit line in `course.config.ts`
|
|
@@ -47,14 +53,6 @@ export const BASE_OPERATORS = Object.freeze(
|
|
|
47
53
|
'coth',
|
|
48
54
|
// limits
|
|
49
55
|
'lim',
|
|
50
|
-
'limsup',
|
|
51
|
-
'liminf',
|
|
52
|
-
'injlim',
|
|
53
|
-
'projlim',
|
|
54
|
-
'varlimsup',
|
|
55
|
-
'varliminf',
|
|
56
|
-
'varinjlim',
|
|
57
|
-
'varprojlim',
|
|
58
56
|
// extrema
|
|
59
57
|
'max',
|
|
60
58
|
'min',
|