explico 0.11.0 → 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 +189 -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/VERSIONING.md +3 -1
- package/docs/authoring/README.md +2 -1
- package/docs/authoring/components.md +4 -4
- package/docs/authoring/migrating.md +271 -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 +57 -38
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
* `keyed-jsx-floats.mjs`'s top-level-only scan entirely, so it never even
|
|
13
13
|
* reaches diagram numbering.
|
|
14
14
|
*
|
|
15
|
-
* A raw table
|
|
16
|
-
*
|
|
17
|
-
* `<
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* A raw table counts only where wrapping it would number it: at the top level
|
|
16
|
+
* of the lesson body or in a `TRANSPARENT_COMPONENTS` body (`<Memo>`,
|
|
17
|
+
* `<ShortNote>`, `<Fold>`), exactly where `keyed-jsx-floats.mjs` numbers a
|
|
18
|
+
* `<Table>`. A raw table in `<Table>`/`<Figure>` is the intended, wrapped
|
|
19
|
+
* case; one in any other component (a worked example's `<CompactExample>`,
|
|
20
|
+
* an `<Aside>`) is illustrative, like an equation there, and a `<Table>`
|
|
21
|
+
* there would get no number — so neither is flagged.
|
|
20
22
|
*
|
|
21
23
|
* `scanRawFloats` (raw Markdown, for the manifest builder) and
|
|
22
24
|
* `collectRawFloats` (mdast, for `remark-notation.mjs`) MUST agree on counts
|
|
@@ -24,7 +26,11 @@
|
|
|
24
26
|
* `keyed-jsx-floats.mjs` holds `scanKeyedJsxFloats`/`collectKeyedJsxFloats` to.
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
stripFencedCode,
|
|
31
|
+
stripJsxComponents,
|
|
32
|
+
TRANSPARENT_COMPONENTS,
|
|
33
|
+
} from './equations.mjs';
|
|
28
34
|
import { collectKeyedJsxFloats, scanKeyedJsxFloats } from './keyed-jsx-floats.mjs';
|
|
29
35
|
|
|
30
36
|
/** Blank `<Table>…</Table>` and `<Figure>…</Figure>` bodies — legitimately wrapped, not raw. */
|
|
@@ -67,9 +73,16 @@ function findDiagramTags(text) {
|
|
|
67
73
|
export function scanRawFloats(markdown) {
|
|
68
74
|
const codeStripped = stripFencedCode(String(markdown));
|
|
69
75
|
const withoutWrappedFloats = blankFloatWrappers(codeStripped);
|
|
76
|
+
// Every component body but a transparent one is blanked — `<Table>` and
|
|
77
|
+
// `<Figure>` among them — so only the tables a `<Table>` would number remain.
|
|
78
|
+
const numberableText = stripJsxComponents(
|
|
79
|
+
codeStripped,
|
|
80
|
+
undefined,
|
|
81
|
+
TRANSPARENT_COMPONENTS,
|
|
82
|
+
);
|
|
70
83
|
|
|
71
|
-
const markdownTableCount = findMarkdownTableStarts(
|
|
72
|
-
const htmlTableCount = findHtmlTableTags(
|
|
84
|
+
const markdownTableCount = findMarkdownTableStarts(numberableText).length;
|
|
85
|
+
const htmlTableCount = findHtmlTableTags(numberableText).length;
|
|
73
86
|
|
|
74
87
|
const diagramsOutsideWrappers = findDiagramTags(withoutWrappedFloats).length;
|
|
75
88
|
const visibleDiagrams = scanKeyedJsxFloats(markdown, { tagName: 'Diagram' });
|
|
@@ -95,7 +108,10 @@ export function collectRawFloats(tree) {
|
|
|
95
108
|
const htmlTableNodes = [];
|
|
96
109
|
const allDiagramNodes = [];
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
// `numberable`: a `<Table>` here would be numbered — the top level, or a
|
|
112
|
+
// transparent component's body. Any other MDX component (a capitalized
|
|
113
|
+
// name, the ones `stripJsxComponents` blanks) ends it for its subtree.
|
|
114
|
+
const walk = (node, numberable) => {
|
|
99
115
|
if (!node || typeof node !== 'object') return;
|
|
100
116
|
if (
|
|
101
117
|
node.type === 'mdxJsxFlowElement' &&
|
|
@@ -103,18 +119,26 @@ export function collectRawFloats(tree) {
|
|
|
103
119
|
) {
|
|
104
120
|
return; // legitimately wrapped — don't descend
|
|
105
121
|
}
|
|
106
|
-
if (node.type === 'table') markdownTableNodes.push(node);
|
|
107
|
-
if (
|
|
122
|
+
if (numberable && node.type === 'table') markdownTableNodes.push(node);
|
|
123
|
+
if (
|
|
124
|
+
numberable &&
|
|
125
|
+
node.type === 'html' &&
|
|
126
|
+
/<table\b/i.test(String(node.value ?? ''))
|
|
127
|
+
) {
|
|
108
128
|
htmlTableNodes.push(node);
|
|
109
129
|
}
|
|
110
130
|
if (node.type === 'mdxJsxFlowElement' && node.name === 'Diagram') {
|
|
111
131
|
allDiagramNodes.push(node);
|
|
112
132
|
}
|
|
133
|
+
const component =
|
|
134
|
+
(node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
|
|
135
|
+
/^[A-Z]/.test(String(node.name ?? '')) &&
|
|
136
|
+
!TRANSPARENT_COMPONENTS.includes(node.name);
|
|
113
137
|
if (Array.isArray(node.children)) {
|
|
114
|
-
for (const child of node.children) walk(child);
|
|
138
|
+
for (const child of node.children) walk(child, numberable && !component);
|
|
115
139
|
}
|
|
116
140
|
};
|
|
117
|
-
walk(tree);
|
|
141
|
+
walk(tree, true);
|
|
118
142
|
|
|
119
143
|
const visibleDiagrams = collectKeyedJsxFloats(tree, { tagName: 'Diagram' });
|
|
120
144
|
const visibleNodes = new Set(visibleDiagrams.map((diagram) => diagram.node));
|
|
@@ -167,6 +167,18 @@ export function buildNotationRegistry(
|
|
|
167
167
|
}
|
|
168
168
|
detectCycles(definitions, edgeIndex, diagnostics);
|
|
169
169
|
|
|
170
|
+
// A page's own `notation.local` entries are in its scope by construction:
|
|
171
|
+
// its math binds to one by shape — an operator entry `{}'` for `w'`, the
|
|
172
|
+
// fix rule N22 itself suggests — even where no `[[key]]` or `\explain`
|
|
173
|
+
// names it. Each seeds its page's bundle, so the page renders its card.
|
|
174
|
+
const localIdsByLesson = new Map<string, string[]>();
|
|
175
|
+
for (const candidate of definitions.values()) {
|
|
176
|
+
if (candidate.kind !== 'local') continue;
|
|
177
|
+
const ids = localIdsByLesson.get(candidate.lesson.lessonId) ?? [];
|
|
178
|
+
ids.push(candidate.id);
|
|
179
|
+
localIdsByLesson.set(candidate.lesson.lessonId, ids);
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
const bundles: NotationPageBundle[] = [];
|
|
171
183
|
const backlinks: NotationBacklink[] = [];
|
|
172
184
|
const reachableDefinitions = new Set<string>();
|
|
@@ -176,7 +188,10 @@ export function buildNotationRegistry(
|
|
|
176
188
|
const directIds = new Set(
|
|
177
189
|
references.map((reference) => reference.definitionId),
|
|
178
190
|
);
|
|
179
|
-
const definitionIds = transitiveClosure(
|
|
191
|
+
const definitionIds = transitiveClosure(
|
|
192
|
+
new Set([...directIds, ...(localIdsByLesson.get(lesson.lessonId) ?? [])]),
|
|
193
|
+
edgeIndex,
|
|
194
|
+
);
|
|
180
195
|
for (const id of definitionIds) reachableDefinitions.add(id);
|
|
181
196
|
|
|
182
197
|
const bindingByKey = new Map<string, string>();
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
equationAnchorId,
|
|
47
47
|
parseEquationRef,
|
|
48
48
|
stripEquationLabels,
|
|
49
|
+
TRANSPARENT_COMPONENTS,
|
|
49
50
|
} from './equations.mjs';
|
|
50
51
|
import {
|
|
51
52
|
collectKeyedJsxFloats,
|
|
@@ -184,6 +185,25 @@ function lessonIdFrom(file) {
|
|
|
184
185
|
return typeof lessonId === 'string' ? lessonId : undefined;
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
/** True for any page under `content/docs/`; a lesson is one of them. */
|
|
189
|
+
function isDocsPageFile(file) {
|
|
190
|
+
return [file?.path, file?.history?.[0], file?.data?.file].some(
|
|
191
|
+
(value) => typeof value === 'string' && LESSON_DOC_PATH.test(value),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The blocks a chapter numbers (`makeFloatCtx` below, and the knowledge
|
|
197
|
+
* check). Rule S10 keeps each off a page that is not a chapter.
|
|
198
|
+
*/
|
|
199
|
+
const NUMBERED_BLOCK_NAMES = new Set([
|
|
200
|
+
'Table',
|
|
201
|
+
'Figure',
|
|
202
|
+
'Diagram',
|
|
203
|
+
'CompactExamples',
|
|
204
|
+
'AssessmentSet',
|
|
205
|
+
]);
|
|
206
|
+
|
|
187
207
|
const ANY_TERM_KEY = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
188
208
|
const ANY_EXPLAIN_KEY = /\\explain\s*\{([^{}]+)\}/g;
|
|
189
209
|
|
|
@@ -467,6 +487,130 @@ function injectFloatNumberAttributes(node, float, prefix) {
|
|
|
467
487
|
});
|
|
468
488
|
}
|
|
469
489
|
|
|
490
|
+
/**
|
|
491
|
+
* The URL path Starlight serves a docs page at: `motion/index.mdx` is
|
|
492
|
+
* `/motion/`, `motion/speed.mdx` is `/motion/speed/`, `index.mdx` is `/`.
|
|
493
|
+
*/
|
|
494
|
+
function pageUrlFrom(file) {
|
|
495
|
+
for (const value of [file?.path, file?.history?.[0], file?.data?.file]) {
|
|
496
|
+
if (typeof value !== 'string') continue;
|
|
497
|
+
const match = value.match(LESSON_DOC_PATH);
|
|
498
|
+
if (!match) continue;
|
|
499
|
+
const route = match[1].replace(/\\/g, '/').replace(/(?:^|\/)index$/, '');
|
|
500
|
+
return route ? `/${route}/` : '/';
|
|
501
|
+
}
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The chapter slug a link's `href` names — `motion/speed` for
|
|
507
|
+
* `/motion/speed/#sec-x`, for `../speed/` from a sibling page, and for
|
|
508
|
+
* `/base/motion/speed/` under the course's `base` — or `undefined` for a
|
|
509
|
+
* same-page anchor, another site, or a scheme such as `mailto:`.
|
|
510
|
+
*/
|
|
511
|
+
function linkedSlug(href, pageUrl, base) {
|
|
512
|
+
if (typeof href !== 'string' || href === '' || href.startsWith('#')) return undefined;
|
|
513
|
+
if (href.startsWith('//') || /^[a-z][a-z0-9+.-]*:/i.test(href)) return undefined;
|
|
514
|
+
let url;
|
|
515
|
+
try {
|
|
516
|
+
url = new URL(href, `https://course.invalid${pageUrl ?? '/'}`);
|
|
517
|
+
} catch {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
let path;
|
|
521
|
+
try {
|
|
522
|
+
path = decodeURIComponent(url.pathname);
|
|
523
|
+
} catch {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
const prefix = base && base !== '/' ? base.replace(/\/+$/, '') : '';
|
|
527
|
+
if (prefix && (path === prefix || path.startsWith(`${prefix}/`))) {
|
|
528
|
+
path = path.slice(prefix.length);
|
|
529
|
+
}
|
|
530
|
+
return path.replace(/^\/+|\/+$/g, '');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* A link the author wrote to another chapter shows that chapter's full title
|
|
535
|
+
* as its native tooltip (`title`): prose names a chapter any way it likes —
|
|
536
|
+
* "the previous chapter", "Chapter 1.2", "this derivation" — and a reader can
|
|
537
|
+
* see where the link goes before following it. A Markdown link and a JSX
|
|
538
|
+
* `<a href>` both get it; a link with a title of its own, or one to the page
|
|
539
|
+
* it is on, keeps what it has.
|
|
540
|
+
*/
|
|
541
|
+
function titleChapterLinks(tree, file, titleBySlug, base) {
|
|
542
|
+
if (titleBySlug.size === 0) return;
|
|
543
|
+
const pageUrl = pageUrlFrom(file);
|
|
544
|
+
const here = pageUrl?.replace(/^\/+|\/+$/g, '');
|
|
545
|
+
const titleOf = (href) => {
|
|
546
|
+
const slug = linkedSlug(href, pageUrl, base);
|
|
547
|
+
return slug !== undefined && slug !== here ? titleBySlug.get(slug) : undefined;
|
|
548
|
+
};
|
|
549
|
+
const walk = (node) => {
|
|
550
|
+
if (!node || typeof node !== 'object') return;
|
|
551
|
+
if (node.type === 'link' && !node.title) {
|
|
552
|
+
const title = titleOf(node.url);
|
|
553
|
+
if (title) node.title = title;
|
|
554
|
+
} else if (
|
|
555
|
+
(node.type === 'mdxJsxTextElement' || node.type === 'mdxJsxFlowElement') &&
|
|
556
|
+
node.name === 'a'
|
|
557
|
+
) {
|
|
558
|
+
const attributes = node.attributes ?? [];
|
|
559
|
+
const named = (name) =>
|
|
560
|
+
attributes.find((attribute) => attribute?.type === 'mdxJsxAttribute' && attribute.name === name);
|
|
561
|
+
const title = named('title') ? undefined : titleOf(named('href')?.value);
|
|
562
|
+
if (title) {
|
|
563
|
+
node.attributes = [...attributes, { type: 'mdxJsxAttribute', name: 'title', value: title }];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (Array.isArray(node.children)) node.children.forEach(walk);
|
|
567
|
+
};
|
|
568
|
+
walk(tree);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* The innermost component around `target` that numbering does not see
|
|
573
|
+
* through — the reason a float in it has no number — or `undefined`.
|
|
574
|
+
*/
|
|
575
|
+
function blockingComponent(tree, target) {
|
|
576
|
+
const search = (node, blocking) => {
|
|
577
|
+
if (node === target) return blocking;
|
|
578
|
+
if (!node || typeof node !== 'object' || !Array.isArray(node.children)) return undefined;
|
|
579
|
+
const here =
|
|
580
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
581
|
+
node.name &&
|
|
582
|
+
!TRANSPARENT_COMPONENTS.includes(node.name)
|
|
583
|
+
? node.name
|
|
584
|
+
: blocking;
|
|
585
|
+
for (const child of node.children) {
|
|
586
|
+
const found = search(child, here);
|
|
587
|
+
if (found !== undefined) return found;
|
|
588
|
+
}
|
|
589
|
+
return undefined;
|
|
590
|
+
};
|
|
591
|
+
return search(tree, undefined);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Why a `<Table>`/`<Figure>` on a lesson page got no number: it sits in a
|
|
596
|
+
* component other than the `TRANSPARENT_COMPONENTS`, where a float is
|
|
597
|
+
* illustrative, as an equation there is (`keyed-jsx-floats.mjs`).
|
|
598
|
+
*/
|
|
599
|
+
function unnumberedFloatMessage(tagName, component) {
|
|
600
|
+
const where = component ? `<${component}>` : 'another component';
|
|
601
|
+
const noun = tagName === 'Table' ? 'table' : 'figure';
|
|
602
|
+
const instead =
|
|
603
|
+
tagName === 'Table'
|
|
604
|
+
? 'write it as a plain Markdown table (rule S3 does not flag one there)'
|
|
605
|
+
: 'show its content without <Figure>';
|
|
606
|
+
return (
|
|
607
|
+
`<${tagName}> inside ${where} gets no number: a lesson numbers a ${noun} ` +
|
|
608
|
+
`at its top level or in a <Memo>, <ShortNote> or <Fold>. A ${noun} in a ` +
|
|
609
|
+
`worked example or another component is illustrative, like an equation ` +
|
|
610
|
+
`there: ${instead}, or move the <${tagName}> out of ${where}.`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
470
614
|
/** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
|
|
471
615
|
function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
472
616
|
if (eqRef.lesson) {
|
|
@@ -1287,6 +1431,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1287
1431
|
// The manifest supplies every numbering input at once; an explicit option
|
|
1288
1432
|
// still overrides it, so existing per-field wiring keeps working.
|
|
1289
1433
|
const numbering = { ...numberingFromManifest(options.manifest), ...options };
|
|
1434
|
+
const chapterTitleBySlug = new Map(
|
|
1435
|
+
(options.manifest?.lessons ?? []).map((lesson) => [lesson.slug, lesson.title]),
|
|
1436
|
+
);
|
|
1290
1437
|
// Every lesson's headings by anchor, built once: `[[lesson#sec-…]]` reads
|
|
1291
1438
|
// another page's, `[[sec-…]]` the page's own.
|
|
1292
1439
|
const sectionSettings = { show: true, depth: 4, ...numbering.sectionNumbering };
|
|
@@ -1310,6 +1457,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1310
1457
|
const suspendedOperators = suspendedOperatorsFromFile(file);
|
|
1311
1458
|
const lessonId = lessonIdFrom(file);
|
|
1312
1459
|
const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
|
|
1460
|
+
// The landing page, a part's overview page (rule S10): its math binds
|
|
1461
|
+
// like a chapter's, but it numbers nothing.
|
|
1462
|
+
const nonChapterPage = !lessonId && isDocsPageFile(file);
|
|
1313
1463
|
// `{ prefix, offset }` per kind of float — see the `numbering` option.
|
|
1314
1464
|
// Missing ⇒ the legacy `##`-section numbering.
|
|
1315
1465
|
const equationNumbering = pageNumbering(numbering, lessonSlug, 'equations');
|
|
@@ -1329,6 +1479,13 @@ export default function remarkNotation(options = {}) {
|
|
|
1329
1479
|
const equationLabels = collectEquationLabels(tree, equationNumbering);
|
|
1330
1480
|
const pageLabels = new Map();
|
|
1331
1481
|
for (const label of equationLabels) {
|
|
1482
|
+
if (nonChapterPage) {
|
|
1483
|
+
fail(
|
|
1484
|
+
file,
|
|
1485
|
+
`\\label{eq:${label.key}} on a page that is not a chapter (rule S10): only a chapter numbers an equation. Drop the label to show the equation unnumbered, or link the chapter's own with [[<chapter>#eq-<key>]].`,
|
|
1486
|
+
label.node,
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1332
1489
|
if (pageLabels.has(label.key)) {
|
|
1333
1490
|
fail(
|
|
1334
1491
|
file,
|
|
@@ -1568,14 +1725,19 @@ export default function remarkNotation(options = {}) {
|
|
|
1568
1725
|
|
|
1569
1726
|
inspectExplainCalls(value, resolve, file, node, references);
|
|
1570
1727
|
|
|
1571
|
-
const bindingScope =
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1728
|
+
const bindingScope =
|
|
1729
|
+
lessonId || nonChapterPage
|
|
1730
|
+
? mathScope
|
|
1731
|
+
: registryFile
|
|
1732
|
+
? registryMathDefinitions
|
|
1733
|
+
: undefined;
|
|
1576
1734
|
|
|
1577
1735
|
if (bindingScope) {
|
|
1578
|
-
const context = lessonId
|
|
1736
|
+
const context = lessonId
|
|
1737
|
+
? 'lesson math'
|
|
1738
|
+
: nonChapterPage
|
|
1739
|
+
? 'page math'
|
|
1740
|
+
: 'notation definition math';
|
|
1579
1741
|
|
|
1580
1742
|
let result;
|
|
1581
1743
|
try {
|
|
@@ -1608,7 +1770,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1608
1770
|
.join(', ');
|
|
1609
1771
|
fail(
|
|
1610
1772
|
file,
|
|
1611
|
-
|
|
1773
|
+
nonChapterPage
|
|
1774
|
+
? `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], or wrap it in \\explain{key}{latex}: a page that is not a chapter recalls shared notation and defines none (rule S10).`
|
|
1775
|
+
: `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.`,
|
|
1612
1776
|
node,
|
|
1613
1777
|
);
|
|
1614
1778
|
}
|
|
@@ -1668,6 +1832,27 @@ export default function remarkNotation(options = {}) {
|
|
|
1668
1832
|
injectFloatNumberAttributes(child, diagramByNode.get(child), 'dia');
|
|
1669
1833
|
} else if (child?.type === 'mdxJsxFlowElement' && exampleByNode.has(child)) {
|
|
1670
1834
|
injectFloatNumberAttributes(child, exampleByNode.get(child), 'ex');
|
|
1835
|
+
} else if (
|
|
1836
|
+
nonChapterPage &&
|
|
1837
|
+
child?.type === 'mdxJsxFlowElement' &&
|
|
1838
|
+
NUMBERED_BLOCK_NAMES.has(child.name)
|
|
1839
|
+
) {
|
|
1840
|
+
// `validate:content` reports this first (gate-non-chapter-pages.ts);
|
|
1841
|
+
// this is the same message for a build that skipped it.
|
|
1842
|
+
fail(
|
|
1843
|
+
file,
|
|
1844
|
+
`<${child.name}> on a page that is not a chapter (rule S10): only a chapter numbers it. Move it into the chapter it belongs to, and link it from here: the link shows it in its card.`,
|
|
1845
|
+
child,
|
|
1846
|
+
);
|
|
1847
|
+
} else if (
|
|
1848
|
+
lessonId &&
|
|
1849
|
+
child?.type === 'mdxJsxFlowElement' &&
|
|
1850
|
+
(child.name === 'Table' || child.name === 'Figure')
|
|
1851
|
+
) {
|
|
1852
|
+
// Numbered everywhere a lesson numbers one, so this one is nested
|
|
1853
|
+
// in a component that is not transparent. Say so here, at its
|
|
1854
|
+
// line, before the component fails on its missing number.
|
|
1855
|
+
fail(file, unnumberedFloatMessage(child.name, blockingComponent(tree, child)), child);
|
|
1671
1856
|
}
|
|
1672
1857
|
}
|
|
1673
1858
|
};
|
|
@@ -1727,6 +1912,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1727
1912
|
}
|
|
1728
1913
|
}
|
|
1729
1914
|
|
|
1915
|
+
// Before `visit`, which writes the `[[…]]` reference links: each of
|
|
1916
|
+
// those opens a card of its own.
|
|
1917
|
+
titleChapterLinks(tree, file, chapterTitleBySlug, options.base);
|
|
1730
1918
|
visit(tree);
|
|
1731
1919
|
|
|
1732
1920
|
file.data.notationReferences = references;
|
|
@@ -12,6 +12,16 @@ export interface LabMathScopeDefinition {
|
|
|
12
12
|
readonly notation: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface RenderLabMathOptions {
|
|
16
|
+
/**
|
|
17
|
+
* `false` renders the template as inline math — a `.katex` with no
|
|
18
|
+
* `.katex-display` — for a formula that sits in a line: a legend entry,
|
|
19
|
+
* an axis title, a control label. It never scrolls (`global.css`
|
|
20
|
+
* `.lab-math`). Default `true`, a display equation.
|
|
21
|
+
*/
|
|
22
|
+
readonly display?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
interface BindResult {
|
|
16
26
|
latex: string;
|
|
17
27
|
bindings: ReadonlyArray<{ key: string }>;
|
|
@@ -36,6 +46,7 @@ const sentinelFor = (index: number): string => `909909${index}.909909`;
|
|
|
36
46
|
export function renderLabMath(
|
|
37
47
|
template: string,
|
|
38
48
|
scope: readonly LabMathScopeDefinition[],
|
|
49
|
+
{ display = true }: RenderLabMathOptions = {},
|
|
39
50
|
): RenderedLabMath {
|
|
40
51
|
const slots: Array<{ name: string; fallback: string }> = [];
|
|
41
52
|
const forBinding = template.replace(
|
|
@@ -95,7 +106,7 @@ export function renderLabMath(
|
|
|
95
106
|
|
|
96
107
|
const html = katex.renderToString(
|
|
97
108
|
bound,
|
|
98
|
-
createNotationKatexOptions({ displayMode:
|
|
109
|
+
createNotationKatexOptions({ displayMode: display }),
|
|
99
110
|
);
|
|
100
111
|
|
|
101
112
|
return {
|
package/src/rules/catalog.ts
CHANGED
|
@@ -613,8 +613,15 @@ const RULES: readonly RuleEntry[] = [
|
|
|
613
613
|
elements: ['plot'],
|
|
614
614
|
doc: 'visualizations.md#v3',
|
|
615
615
|
summary:
|
|
616
|
-
'An interactive figure is an island under `content/labs/` inside a `<Figure>`, renders its equations through `renderLabMath
|
|
617
|
-
checks: [
|
|
616
|
+
'An interactive figure is an island under `content/labs/` inside a `<Figure>`, renders its equations through `renderLabMath` (`{ display: false }` for a legend, an axis title or a label; a `{ math }` run for the caption clause) and never KaTeX directly, and labels its controls; the figure’s caption is its name, so the lab writes no accessible name or text alternative of its own.',
|
|
617
|
+
checks: [
|
|
618
|
+
check('lab-katex-direct', 'error', false, {
|
|
619
|
+
module: 'src/reference/lab-katex-validate.ts',
|
|
620
|
+
fn: 'validateLabKatexImports',
|
|
621
|
+
}),
|
|
622
|
+
],
|
|
623
|
+
unchecked:
|
|
624
|
+
'that a lab labels its controls and writes no name of its own, and math a lab draws without importing `katex` — the build’s rendered check (`figure-symbols.ts`) covers the page it renders.',
|
|
618
625
|
},
|
|
619
626
|
{
|
|
620
627
|
kind: 'rule',
|
|
@@ -799,7 +806,7 @@ const RULES: readonly RuleEntry[] = [
|
|
|
799
806
|
elements: ['table'],
|
|
800
807
|
doc: 'structure-rules.md#s3',
|
|
801
808
|
summary:
|
|
802
|
-
'Wrap every pipe table or `<table>` in `<Table id="…" caption="…">` so it gets a number and `[[tab-…]]` can reach it.',
|
|
809
|
+
'Wrap every pipe table or `<table>` in `<Table id="…" caption="…">` so it gets a number and `[[tab-…]]` can reach it; a table in a worked example stays plain.',
|
|
803
810
|
checks: [
|
|
804
811
|
check('raw-table-markdown', 'warning', false, {
|
|
805
812
|
module: 'src/reference/raw-floats-validate.ts',
|
|
@@ -906,6 +913,33 @@ const RULES: readonly RuleEntry[] = [
|
|
|
906
913
|
'Give each figure, table and diagram a `short` caption that names it: it opens the caption line in bold, and names the float in "Hidden here", a copied link and the "Figures and tables" page. The long `caption` goes on from that name to say what the float shows — its subject, its marks and symbols, the case shown — and at most the one thing to notice. The argument drawn from a figure is lesson text after it, and a lab’s instructions are the first line of its `tools`.',
|
|
907
914
|
checks: [],
|
|
908
915
|
},
|
|
916
|
+
{
|
|
917
|
+
kind: 'rule',
|
|
918
|
+
id: 'S10',
|
|
919
|
+
short: 'A page that is not a chapter explains its symbols but numbers nothing: it links a chapter’s float instead',
|
|
920
|
+
title:
|
|
921
|
+
'A page that is not a chapter — the landing page, a part’s overview page — explains its symbols like a chapter, but carries no numbered float: it links the chapter’s float instead.',
|
|
922
|
+
elements: ['part-page', 'plot', 'table', 'diagram', 'equation', 'notation'],
|
|
923
|
+
doc: 'structure-rules.md#s10',
|
|
924
|
+
summary:
|
|
925
|
+
'On `content/docs/index.mdx`, a `content/docs/<part>/index.mdx`, or any page outside a part: `[[key]]`, `\\explain`, math, citations and `[[chapter#tab-key]]` links work and open cards, and the math and prose gates apply. No `<Table>`, `<Figure>`, `<Diagram>`, `<CompactExamples>`, `<AssessmentSet>`, lab, `\\label{eq:…}` or `notation.local`/`notation.suspend`; no `$…$` or `[[…]]` in `hero.title`, `hero.tagline` or a `<PartOverview chain>` step.',
|
|
926
|
+
checks: [
|
|
927
|
+
check('non-chapter-float', 'error', true, {
|
|
928
|
+
module: 'src/reference/gate-non-chapter-pages.ts',
|
|
929
|
+
fn: 'gateNonChapterPages',
|
|
930
|
+
}),
|
|
931
|
+
check('non-chapter-local-notation', 'error', false, {
|
|
932
|
+
module: 'src/reference/gate-non-chapter-pages.ts',
|
|
933
|
+
fn: 'gateNonChapterPages',
|
|
934
|
+
}),
|
|
935
|
+
check('non-chapter-plain-text', 'error', false, {
|
|
936
|
+
module: 'src/reference/gate-non-chapter-pages.ts',
|
|
937
|
+
fn: 'gateNonChapterPages',
|
|
938
|
+
}),
|
|
939
|
+
],
|
|
940
|
+
locked:
|
|
941
|
+
'a float needs a chapter number, and such a page has none — turning the rule off would not render one.',
|
|
942
|
+
},
|
|
909
943
|
|
|
910
944
|
// ---- P — prose --------------------------------------------------------------
|
|
911
945
|
{
|
package/src/styles/global.css
CHANGED
|
@@ -149,14 +149,15 @@ body {
|
|
|
149
149
|
color: var(--sl-color-text);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
/*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
/* A lab's math (`renderLabMath`), in the element the lab puts it in. The
|
|
153
|
+
wrapper never scrolls: a display template's `.katex-display` scrolls
|
|
154
|
+
sideways when it is wider than its box, like a lesson equation (below),
|
|
155
|
+
so a lab equation is one scroll container at most, and one that fits
|
|
156
|
+
draws no scrollbar. An inline template (`{ display: false }` — a legend
|
|
157
|
+
entry, an axis title, a control label) has no `.katex-display`: it sits
|
|
158
|
+
in its line, with no margin and nothing to scroll. */
|
|
159
|
+
.lab-math:has(.katex-display) {
|
|
156
160
|
margin-block: 1.25rem;
|
|
157
|
-
padding-block: 0.15rem;
|
|
158
|
-
overflow-x: auto;
|
|
159
|
-
overflow-y: hidden;
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
.lab-math .katex-display {
|
|
@@ -171,12 +172,20 @@ body {
|
|
|
171
172
|
edge is KaTeX's metric strut — but glyph ink overshoots its metrics (a
|
|
172
173
|
denominator's `g` descender, a stretched `\right]`), so the bottom of the
|
|
173
174
|
equation was cut off. The padding is the room that ink needs; em-based so
|
|
174
|
-
it scales with the math.
|
|
175
|
-
|
|
175
|
+
it scales with the math.
|
|
176
|
+
The inline padding is KaTeX's own: every stacked script box
|
|
177
|
+
(`.vlist-t2`) has `margin-right: -2px` and holds a 2px spacer cell, which
|
|
178
|
+
lies outside the box's layout width but inside its scrollable overflow.
|
|
179
|
+
An equation within 2px of its column's width scrolled by those 2px for
|
|
180
|
+
nothing. Padding both sides reserves them and keeps a centred equation
|
|
181
|
+
centred. The same holds for a lab's display math (`.lab-math`, above). */
|
|
182
|
+
.sl-markdown-content .katex-display,
|
|
183
|
+
.lab-math .katex-display {
|
|
176
184
|
max-width: 100%;
|
|
177
185
|
overflow-x: auto;
|
|
178
186
|
overflow-y: hidden;
|
|
179
187
|
padding-block: 0.25em;
|
|
188
|
+
padding-inline: 2px;
|
|
180
189
|
}
|
|
181
190
|
|
|
182
191
|
/* Focusable only while it overflows (`KeyedFloatEnhancer.astro`), so it can
|
|
@@ -1043,28 +1052,24 @@ details.layout-part-has-open > summary > .group-label {
|
|
|
1043
1052
|
display: none;
|
|
1044
1053
|
}
|
|
1045
1054
|
|
|
1046
|
-
/* A
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1055
|
+
/* A label that does not fit wraps onto more lines, and the side panels
|
|
1056
|
+
never scroll sideways: a word longer than the panel is wide is
|
|
1057
|
+
hyphenated where the language allows, else broken. Starlight's
|
|
1058
|
+
`overflow-wrap: anywhere` on the contents links broke words at any
|
|
1059
|
+
letter before hyphenation got a chance. */
|
|
1060
|
+
.layout-navigation-content,
|
|
1061
|
+
.layout-page-sidebar-content,
|
|
1062
|
+
.layout-page-sidebar-content a {
|
|
1063
|
+
overflow-wrap: break-word;
|
|
1064
|
+
hyphens: auto;
|
|
1050
1065
|
}
|
|
1051
1066
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
not by the panel, so inheriting `nowrap` would run its text straight out
|
|
1059
|
-
of its own background. */
|
|
1060
|
-
html[data-navigation-resized]
|
|
1061
|
-
.layout-compact-sidebar
|
|
1062
|
-
details[data-compact]
|
|
1063
|
-
> ul:not(.top-level)
|
|
1064
|
-
> li
|
|
1065
|
-
> a
|
|
1066
|
-
> span {
|
|
1067
|
-
white-space: normal;
|
|
1067
|
+
/* With every label wrapping, what still reaches past the navigation's
|
|
1068
|
+
edge is a compact chapter's resting tooltip (absolutely placed, and
|
|
1069
|
+
invisible): it would only add a sideways scrollbar. A showing tooltip is
|
|
1070
|
+
`position: fixed` (below), so this clip never cuts it. */
|
|
1071
|
+
.sidebar-pane {
|
|
1072
|
+
overflow-x: hidden;
|
|
1068
1073
|
}
|
|
1069
1074
|
|
|
1070
1075
|
html[data-navigation-hidden] {
|
|
@@ -1249,15 +1254,15 @@ details.layout-part-has-open > summary > .group-label {
|
|
|
1249
1254
|
max-width: none;
|
|
1250
1255
|
}
|
|
1251
1256
|
|
|
1252
|
-
/* Starlight
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1257
|
+
/* Starlight sizes the column inside from the navigation's width
|
|
1258
|
+
(`--sl-sidebar-width`), not from this panel: in a panel the reader set,
|
|
1259
|
+
it fills the panel, so its headings wrap at the panel's edge instead of
|
|
1260
|
+
running past it. */
|
|
1261
|
+
html[data-contents-resized]:not([data-page-sidebar-hidden])
|
|
1262
|
+
.right-sidebar-panel
|
|
1263
|
+
.sl-container {
|
|
1264
|
+
width: auto;
|
|
1265
|
+
max-width: none;
|
|
1261
1266
|
}
|
|
1262
1267
|
|
|
1263
1268
|
html[data-page-sidebar-hidden] {
|
|
@@ -1362,6 +1367,15 @@ details.layout-part-has-open > summary > .group-label {
|
|
|
1362
1367
|
z-index: calc(var(--sl-z-index-navbar) + 1);
|
|
1363
1368
|
}
|
|
1364
1369
|
|
|
1370
|
+
/* Below 50rem Starlight's menu button is not in the header: it is a fixed
|
|
1371
|
+
button in `nav.sidebar`, at the navbar's level, laid over the header's
|
|
1372
|
+
end. It rises above the raised header, or the header's box takes every
|
|
1373
|
+
tap and the navigation never opens on a phone. Starlight hides the
|
|
1374
|
+
button from 50rem up, so the rule needs no media query. */
|
|
1375
|
+
starlight-menu-button > button {
|
|
1376
|
+
z-index: calc(var(--sl-z-index-navbar) + 2);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1365
1379
|
/* A slimmer site header: 3rem at every width, where Starlight's is 3.5rem
|
|
1366
1380
|
and 4rem from 50rem up. The title, the logo (sized from the height and
|
|
1367
1381
|
padding) and the search box scale with it, so more of the window goes to
|
|
@@ -2024,7 +2038,11 @@ details.layout-part-has-open > summary > .group-label {
|
|
|
2024
2038
|
gap: 0.25rem;
|
|
2025
2039
|
}
|
|
2026
2040
|
|
|
2041
|
+
/* The line is at least as tall as its buttons (`.float-tool`'s height), so
|
|
2042
|
+
the row never hangs below it — past a figure's body, or into a table's
|
|
2043
|
+
header — even when a course hides the caption's text. */
|
|
2027
2044
|
.sl-markdown-content .float-caption:has(> .float-tools > .float-tool:not([hidden])) {
|
|
2045
|
+
min-block-size: 1.5rem;
|
|
2028
2046
|
padding-inline-end: 2.25rem;
|
|
2029
2047
|
}
|
|
2030
2048
|
|
|
@@ -2096,6 +2114,7 @@ details.layout-part-has-open > summary > .group-label {
|
|
|
2096
2114
|
}
|
|
2097
2115
|
|
|
2098
2116
|
.sl-markdown-content .float-caption:has(> .float-tools) {
|
|
2117
|
+
min-block-size: auto;
|
|
2099
2118
|
padding-inline-end: 0;
|
|
2100
2119
|
}
|
|
2101
2120
|
}
|