explico 0.4.0 → 0.5.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 +273 -1
- package/README.md +103 -8
- package/dist/compiler/astro.d.ts +9 -1
- package/dist/compiler/astro.js +182 -3
- package/dist/compiler/cli.d.ts +13 -0
- package/dist/compiler/cli.js +28 -4
- package/dist/compiler/collections.d.ts +3 -2
- package/dist/compiler/collections.js +28 -1
- package/dist/compiler/course-config.d.ts +58 -1
- package/dist/compiler/course-config.js +28 -1
- package/dist/compiler/exports/standalone-html.d.ts +34 -0
- package/dist/compiler/exports/standalone-html.js +803 -0
- package/dist/compiler/manifest.d.ts +90 -6
- package/dist/compiler/manifest.js +150 -17
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-abbreviations.d.mts +30 -0
- package/dist/reference/base-abbreviations.mjs +60 -0
- package/dist/reference/base-operators.d.mts +15 -0
- package/dist/reference/base-operators.mjs +40 -0
- package/dist/reference/base-roman-labels.d.mts +35 -0
- package/dist/reference/base-roman-labels.mjs +43 -0
- package/dist/reference/consistency.d.ts +8 -2
- package/dist/reference/consistency.js +50 -3
- package/dist/reference/diagrams-validate.d.ts +21 -0
- package/dist/reference/diagrams-validate.js +13 -0
- package/dist/reference/equations.d.mts +10 -15
- package/dist/reference/equations.mjs +67 -14
- package/dist/reference/gate-abbreviations.d.ts +17 -0
- package/dist/reference/gate-abbreviations.js +64 -0
- package/dist/reference/gate-math.d.ts +28 -5
- package/dist/reference/gate-math.js +5 -2
- package/dist/reference/gate-prose-notation.d.ts +60 -0
- package/dist/reference/gate-prose-notation.js +125 -0
- package/dist/reference/keyed-float-validate.d.ts +5 -5
- package/dist/reference/keyed-jsx-floats.d.mts +7 -12
- package/dist/reference/keyed-jsx-floats.mjs +50 -25
- package/dist/reference/knowledge-checks-validate.d.ts +35 -0
- package/dist/reference/knowledge-checks-validate.js +78 -0
- package/dist/reference/knowledge-checks.d.mts +25 -0
- package/dist/reference/knowledge-checks.mjs +45 -0
- package/dist/reference/lab-math-scope.d.ts +9 -1
- package/dist/reference/lab-math-scope.js +32 -4
- package/dist/reference/math-glyphs.d.mts +30 -0
- package/dist/reference/math-glyphs.mjs +367 -0
- package/dist/reference/mdx-attributes.d.mts +13 -0
- package/dist/reference/mdx-attributes.mjs +38 -0
- package/dist/reference/mixed-content.d.ts +17 -0
- package/dist/reference/mixed-content.js +29 -0
- package/dist/reference/page-notation.d.ts +52 -0
- package/dist/reference/page-notation.js +171 -0
- package/dist/reference/raw-floats-validate.d.ts +18 -0
- package/dist/reference/raw-floats-validate.js +53 -0
- package/dist/reference/raw-floats.d.mts +18 -0
- package/dist/reference/raw-floats.mjs +129 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -1
- package/dist/reference/rehype-heading-anchors.mjs +107 -1
- package/dist/reference/rehype-table-header-labels.d.mts +26 -0
- package/dist/reference/rehype-table-header-labels.mjs +152 -0
- package/dist/reference/remark-abbreviations.d.mts +7 -0
- package/dist/reference/remark-abbreviations.mjs +174 -0
- package/dist/reference/remark-memo.d.mts +16 -0
- package/dist/reference/remark-memo.mjs +339 -0
- package/dist/reference/remark-notation.d.mts +57 -9
- package/dist/reference/remark-notation.mjs +354 -42
- package/dist/reference/remark-short-note.d.mts +4 -0
- package/dist/reference/remark-short-note.mjs +237 -0
- package/dist/reference/types.d.ts +16 -1
- package/dist/reference/units-vocab.js +4 -0
- package/docs/VERSIONING.md +33 -4
- package/docs/authoring/README.md +8 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +135 -0
- package/docs/authoring/migrating.md +92 -7
- package/docs/authoring/notation.md +560 -1
- package/docs/authoring/structure.md +120 -8
- package/docs/authoring/visualizations.md +36 -3
- package/package.json +5 -2
- package/src/compiler/astro.ts +208 -3
- package/src/compiler/cli.ts +29 -2
- package/src/compiler/collections.ts +45 -4
- package/src/compiler/course-config.ts +92 -0
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/manifest.ts +268 -22
- package/src/components/HoverPanel.astro +45 -4
- package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
- package/src/components/assessments/AssessmentRunner.tsx +52 -17
- package/src/components/assessments/AssessmentSet.astro +98 -14
- package/src/components/assessments/RenderedContent.tsx +55 -0
- package/src/components/assessments/assessment-randomization.ts +16 -0
- package/src/components/diagram/Diagram.astro +444 -83
- package/src/components/disclosure/hover-preview.ts +202 -0
- package/src/components/disclosure/persisted-disclosure.ts +66 -0
- package/src/components/examples/CompactExamples.astro +2 -0
- package/src/components/figure/Figure.astro +279 -10
- package/src/components/memo/Memo.astro +453 -0
- package/src/components/memo/ShortNote.astro +223 -0
- package/src/components/notation/NotationFormula.astro +18 -1
- package/src/components/notation/NotationLayer.astro +105 -8
- package/src/components/notation/render-notation-math.ts +39 -2
- package/src/components/notation/safe-math-to-react.ts +50 -0
- package/src/components/reference/FloatRefLayer.astro +233 -15
- package/src/components/starlight/CollapsibleSections.astro +461 -0
- package/src/components/starlight/DiagramEnhancer.astro +385 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
- package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
- package/src/components/starlight/LayoutHeader.astro +50 -0
- package/src/components/starlight/LayoutSocialIcons.astro +41 -2
- package/src/components/starlight/LessonExportActions.astro +436 -0
- package/src/components/starlight/LessonFooter.astro +27 -2
- package/src/components/starlight/section-parts.ts +31 -0
- package/src/components/table/Table.astro +293 -7
- package/src/components/table/TableOverflowExpander.astro +489 -0
- package/src/content-config.ts +20 -0
- package/src/reference/base-abbreviations.mjs +60 -0
- package/src/reference/base-operators.mjs +40 -0
- package/src/reference/base-roman-labels.mjs +43 -0
- package/src/reference/consistency.ts +73 -2
- package/src/reference/diagrams-validate.ts +40 -0
- package/src/reference/equations.mjs +67 -14
- package/src/reference/gate-abbreviations.ts +128 -0
- package/src/reference/gate-math.ts +27 -7
- package/src/reference/gate-prose-notation.ts +224 -0
- package/src/reference/keyed-float-validate.ts +5 -5
- package/src/reference/keyed-jsx-floats.mjs +50 -25
- package/src/reference/knowledge-checks-validate.ts +130 -0
- package/src/reference/knowledge-checks.mjs +45 -0
- package/src/reference/lab-math-scope.ts +45 -3
- package/src/reference/math-glyphs.mjs +367 -0
- package/src/reference/mdx-attributes.mjs +38 -0
- package/src/reference/mixed-content.ts +69 -0
- package/src/reference/page-notation.ts +252 -0
- package/src/reference/raw-floats-validate.ts +103 -0
- package/src/reference/raw-floats.mjs +129 -0
- package/src/reference/rehype-heading-anchors.mjs +107 -1
- package/src/reference/rehype-table-header-labels.mjs +152 -0
- package/src/reference/remark-abbreviations.mjs +174 -0
- package/src/reference/remark-memo.mjs +339 -0
- package/src/reference/remark-notation.mjs +354 -42
- package/src/reference/remark-short-note.mjs +237 -0
- package/src/reference/types.ts +16 -0
- package/src/reference/units-vocab.ts +4 -0
- package/src/styles/global.css +996 -50
|
@@ -33,7 +33,11 @@
|
|
|
33
33
|
|
|
34
34
|
export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
GlyphResolutionError,
|
|
38
|
+
resolveMathGlyphs,
|
|
39
|
+
stripNotationMarkers,
|
|
40
|
+
} from './math-glyphs.mjs';
|
|
37
41
|
import {
|
|
38
42
|
EQ_LABEL_PATTERN,
|
|
39
43
|
collectDisplayEquations,
|
|
@@ -47,6 +51,12 @@ import {
|
|
|
47
51
|
keyedFloatAnchorId,
|
|
48
52
|
parseKeyedFloatRef,
|
|
49
53
|
} from './keyed-jsx-floats.mjs';
|
|
54
|
+
import { collectRawFloats } from './raw-floats.mjs';
|
|
55
|
+
import {
|
|
56
|
+
knowledgeCheckAnchorId,
|
|
57
|
+
parseKnowledgeCheckRef,
|
|
58
|
+
} from './knowledge-checks.mjs';
|
|
59
|
+
import { injectMdxStringAttributes } from './mdx-attributes.mjs';
|
|
50
60
|
|
|
51
61
|
const PROSE_SKIP_TYPES = new Set([
|
|
52
62
|
'code',
|
|
@@ -313,13 +323,58 @@ function defaultHref(definition, key, base = '') {
|
|
|
313
323
|
return `${base.replace(/\/$/, '')}/glossary/#notation-${key}`;
|
|
314
324
|
}
|
|
315
325
|
|
|
326
|
+
/** A non-mdast hast element (a `<div>`, `<button>`, `<svg>`, …) built through
|
|
327
|
+
* the same `data.hName`/`hProperties` override `equationNode` already uses
|
|
328
|
+
* for its own wrapper — mdast-to-hast renders exactly what these say
|
|
329
|
+
* regardless of the carrier node's own `type`. */
|
|
330
|
+
function el(hName, hProperties, children = []) {
|
|
331
|
+
return { type: 'paragraph', data: { hName, hProperties }, children };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Feather/Lucide's "copy" glyph (two overlapping rounded rects), matching
|
|
335
|
+
* the stroke-icon-button style `LessonExportActions.astro` already uses for
|
|
336
|
+
* lesson-level actions. */
|
|
337
|
+
function copyIconNode() {
|
|
338
|
+
return el('svg', { viewBox: '0 0 24 24', 'aria-hidden': 'true' }, [
|
|
339
|
+
el('rect', { x: '9', y: '9', width: '10', height: '10', rx: '1' }),
|
|
340
|
+
el('path', {
|
|
341
|
+
d: 'M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1',
|
|
342
|
+
}),
|
|
343
|
+
]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Wrap an inline `$…$` `math` node in an inert `<span data-latex>` so a
|
|
348
|
+
* reader can trigger the same "click an unexplained part to copy" gesture
|
|
349
|
+
* a display equation gets (`KeyedFloatEnhancer.astro`), without a
|
|
350
|
+
* `.keyed-equation` block's room for a dedicated button — a persistent
|
|
351
|
+
* button here would break the reading line (see `global.css`, which gives
|
|
352
|
+
* this wrapper no visible styling of its own). `mathNode.value` is read
|
|
353
|
+
* after the resolver above has already bound the page's glyphs into it, same
|
|
354
|
+
* as `equationNode` below.
|
|
355
|
+
*/
|
|
356
|
+
function inlineEquationNode(mathNode) {
|
|
357
|
+
const latex = stripNotationMarkers(String(mathNode.value ?? ''));
|
|
358
|
+
return el('span', { className: ['inline-equation'], 'data-latex': latex }, [
|
|
359
|
+
mathNode,
|
|
360
|
+
]);
|
|
361
|
+
}
|
|
362
|
+
|
|
316
363
|
/**
|
|
317
364
|
* Wrap a display `$$…$$` `math` node so the container survives `rehype-katex`
|
|
318
365
|
* (which splices the inner `math-display` element out). Every display equation
|
|
319
366
|
* in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
|
|
320
367
|
* at its positional `#eq-<section>-<index>`. The visible number is a focusable
|
|
321
368
|
* `<a>` sibling — never authored, never inside KaTeX — so it survives print and
|
|
322
|
-
* screen readers name it "Equation 2.4".
|
|
369
|
+
* screen readers name it "Equation 2.4". A "Copy LaTeX" button sits beside it
|
|
370
|
+
* (`.keyed-equation__controls`, styled in `global.css`) — the equation's own
|
|
371
|
+
* copy affordance, distinct from the notation popup card's (`NotationLayer.astro`),
|
|
372
|
+
* which only exists for a symbol with a registry entry, not a bare display
|
|
373
|
+
* equation. `mathNode.value` is read here — after the resolver above has
|
|
374
|
+
* already bound the page's glyphs into it (`replaceMathSource`) — so an
|
|
375
|
+
* auto-bound symbol's `\explain{…}` marker gets stripped same as an
|
|
376
|
+
* author-written one; either way the reader copies plain LaTeX, never this
|
|
377
|
+
* system's own notation-key plumbing (`math-glyphs.mjs:stripNotationMarkers`).
|
|
323
378
|
*/
|
|
324
379
|
function equationNode(mathNode, { key, number }) {
|
|
325
380
|
const id = equationAnchorId({ key, number });
|
|
@@ -331,31 +386,47 @@ function equationNode(mathNode, { key, number }) {
|
|
|
331
386
|
if (key) hProperties['data-eq-key'] = key;
|
|
332
387
|
else hProperties['data-eq-auto'] = '';
|
|
333
388
|
|
|
389
|
+
const latex = stripNotationMarkers(String(mathNode.value ?? ''));
|
|
390
|
+
|
|
334
391
|
return {
|
|
335
392
|
type: 'paragraph',
|
|
336
393
|
data: { hName: 'div', hProperties },
|
|
337
394
|
children: [
|
|
338
395
|
mathNode,
|
|
339
|
-
{
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
396
|
+
el('div', { className: ['keyed-equation__controls'] }, [
|
|
397
|
+
{
|
|
398
|
+
type: 'link',
|
|
399
|
+
url: `#${id}`,
|
|
400
|
+
children: [{ type: 'text', value: `(${number})` }],
|
|
401
|
+
data: {
|
|
402
|
+
hProperties: {
|
|
403
|
+
className: ['keyed-equation__number'],
|
|
404
|
+
'data-eq-number': number,
|
|
405
|
+
'aria-label': `Equation ${number}`,
|
|
406
|
+
// Plain hover tooltip — the number is a permalink to this equation.
|
|
407
|
+
title: 'Anchor to this equation',
|
|
408
|
+
},
|
|
350
409
|
},
|
|
351
410
|
},
|
|
352
|
-
|
|
411
|
+
el(
|
|
412
|
+
'button',
|
|
413
|
+
{
|
|
414
|
+
type: 'button',
|
|
415
|
+
className: ['keyed-equation__copy'],
|
|
416
|
+
'data-eq-copy': '',
|
|
417
|
+
'data-latex': latex,
|
|
418
|
+
'aria-label': `Copy LaTeX for Equation ${number}`,
|
|
419
|
+
title: 'Copy LaTeX',
|
|
420
|
+
},
|
|
421
|
+
[copyIconNode()],
|
|
422
|
+
),
|
|
423
|
+
]),
|
|
353
424
|
],
|
|
354
425
|
};
|
|
355
426
|
}
|
|
356
427
|
|
|
357
428
|
/**
|
|
358
|
-
* Give a `<Table>`/`<Figure>` `mdxJsxFlowElement` its computed `number` and
|
|
429
|
+
* Give a `<Table>`/`<Figure>`/`<Diagram>` `mdxJsxFlowElement` its computed `number` and
|
|
359
430
|
* `anchorId` as real JSX attributes, so the actual Astro component (which
|
|
360
431
|
* renders the numbered anchor, unlike `equationNode`'s synthesized hast div)
|
|
361
432
|
* receives them as props. The compiler is the single source of truth for
|
|
@@ -367,17 +438,10 @@ function injectFloatNumberAttributes(node, float, prefix) {
|
|
|
367
438
|
number: float.number,
|
|
368
439
|
prefix,
|
|
369
440
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
(attribute.name === 'number' || attribute.name === 'anchorId')
|
|
375
|
-
),
|
|
376
|
-
);
|
|
377
|
-
node.attributes.push(
|
|
378
|
-
{ type: 'mdxJsxAttribute', name: 'number', value: float.number },
|
|
379
|
-
{ type: 'mdxJsxAttribute', name: 'anchorId', value: anchorId },
|
|
380
|
-
);
|
|
441
|
+
injectMdxStringAttributes(node, {
|
|
442
|
+
number: float.number,
|
|
443
|
+
anchorId,
|
|
444
|
+
});
|
|
381
445
|
}
|
|
382
446
|
|
|
383
447
|
/** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
|
|
@@ -409,7 +473,7 @@ function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
|
409
473
|
|
|
410
474
|
/**
|
|
411
475
|
* Resolve a `[[<prefix>-key]]` / `[[slug#<prefix>-key]]` reference (a
|
|
412
|
-
* `<Table>`/`<Figure>` float) to `{ number, href }` — the
|
|
476
|
+
* `<Table>`/`<Figure>`/`<Diagram>` float) to `{ number, href }` — the keyed-float
|
|
413
477
|
* equivalent of `resolveEquationRef`.
|
|
414
478
|
*/
|
|
415
479
|
function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
|
|
@@ -438,6 +502,33 @@ function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
|
|
|
438
502
|
return { number, href: `#${prefix}-${floatRef.key}` };
|
|
439
503
|
}
|
|
440
504
|
|
|
505
|
+
/** Resolve a numbered knowledge check declared by a lesson's `.checks.yml`. */
|
|
506
|
+
function resolveKnowledgeCheckRef(checkRef, ctx, file, node) {
|
|
507
|
+
if (checkRef.lesson) {
|
|
508
|
+
const number = ctx.crossPage?.[checkRef.lesson]?.[checkRef.key];
|
|
509
|
+
if (number === undefined) {
|
|
510
|
+
fail(
|
|
511
|
+
file,
|
|
512
|
+
`Unknown knowledge-check reference [[${checkRef.lesson}#check-${checkRef.key}]]: that lesson's .checks.yml does not list "${checkRef.key}".`,
|
|
513
|
+
node,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
number,
|
|
518
|
+
href: `${ctx.base.replace(/\/$/, '')}/${checkRef.lesson}/#${knowledgeCheckAnchorId(checkRef.key)}`,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const number = ctx.pageLabels.get(checkRef.key);
|
|
522
|
+
if (number === undefined) {
|
|
523
|
+
fail(
|
|
524
|
+
file,
|
|
525
|
+
`Unknown knowledge-check reference [[check-${checkRef.key}]]: this lesson's .checks.yml does not list "${checkRef.key}".`,
|
|
526
|
+
node,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
return { number, href: `#${knowledgeCheckAnchorId(checkRef.key)}` };
|
|
530
|
+
}
|
|
531
|
+
|
|
441
532
|
function proseNodes(
|
|
442
533
|
value,
|
|
443
534
|
resolve,
|
|
@@ -447,6 +538,8 @@ function proseNodes(
|
|
|
447
538
|
eqCtx,
|
|
448
539
|
tableCtx,
|
|
449
540
|
figureCtx,
|
|
541
|
+
diagramCtx,
|
|
542
|
+
checkCtx,
|
|
450
543
|
) {
|
|
451
544
|
const base = eqCtx?.base ?? '';
|
|
452
545
|
const strayExplain = firstUnescaped(value, EXPLAIN);
|
|
@@ -474,10 +567,20 @@ function proseNodes(
|
|
|
474
567
|
!eqRef && !tableRef && figureCtx
|
|
475
568
|
? parseKeyedFloatRef(key, { prefix: 'fig' })
|
|
476
569
|
: undefined;
|
|
570
|
+
const diagramRef =
|
|
571
|
+
!eqRef && !tableRef && !figureRef && diagramCtx
|
|
572
|
+
? parseKeyedFloatRef(key, { prefix: 'dia' })
|
|
573
|
+
: undefined;
|
|
574
|
+
const checkRef =
|
|
575
|
+
!eqRef && !tableRef && !figureRef && !diagramRef && checkCtx
|
|
576
|
+
? parseKnowledgeCheckRef(key)
|
|
577
|
+
: undefined;
|
|
477
578
|
if (
|
|
478
579
|
!eqRef &&
|
|
479
580
|
!tableRef &&
|
|
480
581
|
!figureRef &&
|
|
582
|
+
!diagramRef &&
|
|
583
|
+
!checkRef &&
|
|
481
584
|
!NOTATION_KEY_PATTERN.test(key)
|
|
482
585
|
) {
|
|
483
586
|
fail(file, `Invalid notation key "${key}" in [[...]].`, node);
|
|
@@ -510,14 +613,44 @@ function proseNodes(
|
|
|
510
613
|
continue;
|
|
511
614
|
}
|
|
512
615
|
|
|
513
|
-
|
|
616
|
+
if (checkRef) {
|
|
617
|
+
const { number, href } = resolveKnowledgeCheckRef(
|
|
618
|
+
checkRef,
|
|
619
|
+
checkCtx,
|
|
620
|
+
file,
|
|
621
|
+
node,
|
|
622
|
+
);
|
|
623
|
+
output.push({
|
|
624
|
+
type: 'link',
|
|
625
|
+
url: href,
|
|
626
|
+
children: [{ type: 'text', value: `Knowledge check ${number}` }],
|
|
627
|
+
data: {
|
|
628
|
+
hProperties: {
|
|
629
|
+
className: ['knowledge-check-ref'],
|
|
630
|
+
'data-check-ref': checkRef.key,
|
|
631
|
+
...(checkRef.lesson
|
|
632
|
+
? { 'data-check-lesson': checkRef.lesson }
|
|
633
|
+
: {}),
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
references.push({
|
|
638
|
+
key: checkRef.key,
|
|
639
|
+
kind: 'knowledge-check',
|
|
640
|
+
...(checkRef.lesson ? { lesson: checkRef.lesson } : {}),
|
|
641
|
+
});
|
|
642
|
+
cursor = match.index + match[0].length;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const floatRef = tableRef ?? figureRef ?? diagramRef;
|
|
514
647
|
if (floatRef) {
|
|
515
|
-
const kind = tableRef ? 'table' : 'figure';
|
|
516
|
-
const prefix = tableRef ? 'tab' : 'fig';
|
|
517
|
-
const tagName = tableRef ? 'Table' : 'Figure';
|
|
648
|
+
const kind = tableRef ? 'table' : figureRef ? 'figure' : 'diagram';
|
|
649
|
+
const prefix = tableRef ? 'tab' : figureRef ? 'fig' : 'dia';
|
|
650
|
+
const tagName = tableRef ? 'Table' : figureRef ? 'Figure' : 'Diagram';
|
|
518
651
|
const { number, href } = resolveKeyedFloatRef(
|
|
519
652
|
floatRef,
|
|
520
|
-
tableRef ? tableCtx : figureCtx,
|
|
653
|
+
tableRef ? tableCtx : figureRef ? figureCtx : diagramCtx,
|
|
521
654
|
{ prefix, tagName },
|
|
522
655
|
file,
|
|
523
656
|
node,
|
|
@@ -676,14 +809,16 @@ function inspectExplainCalls(value, resolve, file, node, references) {
|
|
|
676
809
|
*
|
|
677
810
|
* Every number on a page comes from two places that MUST agree: the page's own
|
|
678
811
|
* counters (`numbering`) and the manifest tables a cross-page `[[slug#eq-key]]`
|
|
679
|
-
* is resolved against (`equations` / `tables` / `figures`
|
|
680
|
-
*
|
|
812
|
+
* is resolved against (`equations` / `tables` / `figures` / `diagrams` /
|
|
813
|
+
* `checks`).
|
|
814
|
+
* Deriving every input from the same manifest in one step stops a course config from
|
|
681
815
|
* supplying one without the other — the omission that produced page-local
|
|
682
816
|
* `(2.2)` and cross-page `(4.5)` for the same equation.
|
|
683
817
|
*
|
|
684
|
-
* `numbering` maps a lesson slug to
|
|
685
|
-
*
|
|
686
|
-
*
|
|
818
|
+
* `numbering` maps a lesson slug to
|
|
819
|
+
* `{ part, equations, tables, figures, diagrams, checks }`: the part's ordinal
|
|
820
|
+
* (the number before the dot) and, per numbered kind, how many earlier lessons
|
|
821
|
+
* of that part already used.
|
|
687
822
|
*/
|
|
688
823
|
export function numberingFromManifest(manifest) {
|
|
689
824
|
if (!manifest || typeof manifest !== 'object') return {};
|
|
@@ -691,6 +826,8 @@ export function numberingFromManifest(manifest) {
|
|
|
691
826
|
equations: manifest.equations?.numbersBySlug ?? {},
|
|
692
827
|
tables: manifest.tables?.numbersBySlug ?? {},
|
|
693
828
|
figures: manifest.figures?.numbersBySlug ?? {},
|
|
829
|
+
diagrams: manifest.diagrams?.numbersBySlug ?? {},
|
|
830
|
+
checks: manifest.checks?.numbersBySlug ?? {},
|
|
694
831
|
numbering: Object.fromEntries(
|
|
695
832
|
(manifest.lessons ?? []).map((lesson) => [
|
|
696
833
|
lesson.slug,
|
|
@@ -699,12 +836,44 @@ export function numberingFromManifest(manifest) {
|
|
|
699
836
|
equations: lesson.numbering?.equations ?? 0,
|
|
700
837
|
tables: lesson.numbering?.tables ?? 0,
|
|
701
838
|
figures: lesson.numbering?.figures ?? 0,
|
|
839
|
+
diagrams: lesson.numbering?.diagrams ?? 0,
|
|
840
|
+
checks: lesson.numbering?.checks ?? 0,
|
|
702
841
|
},
|
|
703
842
|
]),
|
|
704
843
|
),
|
|
705
844
|
};
|
|
706
845
|
}
|
|
707
846
|
|
|
847
|
+
/**
|
|
848
|
+
* The options object `remarkNotation` needs for one manifest, in one call.
|
|
849
|
+
*
|
|
850
|
+
* A consumer used to hand-assemble `definitions`, `equations`, `tables`,
|
|
851
|
+
* `figures`, and `chapters` separately — easy to do partially, which is how a
|
|
852
|
+
* page ends up numbered under the legacy `##`-section scheme while the
|
|
853
|
+
* manifest (and every cross-page reference to that page) uses the part
|
|
854
|
+
* scheme. This does not return that literal four-map-plus-chapters shape:
|
|
855
|
+
* `chapters` is the superseded per-lesson form (see `pageNumbering` below),
|
|
856
|
+
* and passing `manifest` through already makes `numberingFromManifest()`
|
|
857
|
+
* derive `numbering` plus every `equations`/`tables`/`figures`/`diagrams`/
|
|
858
|
+
* `checks` map from the same source in one step — strictly more correct than
|
|
859
|
+
* reconstructing `chapters` by hand.
|
|
860
|
+
*
|
|
861
|
+
* `overrides` is spread last, so a caller can add `strict`, `unknown`,
|
|
862
|
+
* `resolve`, `base`, or override anything above:
|
|
863
|
+
*
|
|
864
|
+
* [remarkNotation, createRemarkNotationOptions(manifest, { strict: true })]
|
|
865
|
+
*
|
|
866
|
+
* @param {object} manifest - a compiled Explico manifest (`compileManifest()`).
|
|
867
|
+
* @param {object} [overrides] - additional/overriding `remarkNotation` options.
|
|
868
|
+
*/
|
|
869
|
+
export function createRemarkNotationOptions(manifest, overrides = {}) {
|
|
870
|
+
return {
|
|
871
|
+
definitions: manifest?.notation?.raw ?? [],
|
|
872
|
+
manifest,
|
|
873
|
+
...overrides,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
708
877
|
/**
|
|
709
878
|
* `{ prefix, offset }` for one kind of float on one page, from whichever
|
|
710
879
|
* numbering option the caller supplied. `numbering` is the current form;
|
|
@@ -773,31 +942,71 @@ function assertManifestNumberParity({
|
|
|
773
942
|
}
|
|
774
943
|
}
|
|
775
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Strict-mode guard: a catalogued lesson (one `remarkNotation` is told the
|
|
947
|
+
* slug of) that contains at least one equation/`<Table>`/`<Figure>` — keyed or
|
|
948
|
+
* not — must have both a `numbering` placement and a cross-page number table
|
|
949
|
+
* entry. Without this, `assertManifestNumberParity` above only ever catches a
|
|
950
|
+
* missing/wrong map for *keyed* items (it walks `pageLabels`, which unkeyed
|
|
951
|
+
* equations/floats never enter — see `keyed-jsx-floats.mjs`), so a lesson
|
|
952
|
+
* whose only floats are unkeyed can silently fall back to `##`-section
|
|
953
|
+
* numbering with no diagnostic at all. `Diagram` is deliberately excluded by
|
|
954
|
+
* every caller: `Diagram.astro` already tolerates an unplaced diagram
|
|
955
|
+
* rendering without a number, by design.
|
|
956
|
+
*/
|
|
957
|
+
function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }) {
|
|
958
|
+
if (!slug || count === 0) return;
|
|
959
|
+
const plural = count === 1 ? '' : 's';
|
|
960
|
+
if (placement === undefined) {
|
|
961
|
+
fail(
|
|
962
|
+
file,
|
|
963
|
+
`Strict mode: ${slug} has ${count} ${noun}${plural} but remarkNotation ` +
|
|
964
|
+
`has no \`numbering\` entry for this lesson, so it would silently ` +
|
|
965
|
+
`fall back to the legacy ##-section numbering. Pass \`manifest\` ` +
|
|
966
|
+
`(e.g. via createRemarkNotationOptions) or add a \`numbering\` entry ` +
|
|
967
|
+
`for "${slug}".`,
|
|
968
|
+
);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (crossPage?.[slug] === undefined) {
|
|
972
|
+
fail(
|
|
973
|
+
file,
|
|
974
|
+
`Strict mode: ${slug} has ${count} ${noun}${plural} but remarkNotation ` +
|
|
975
|
+
`has no cross-page ${noun} number table entry for this lesson. Pass ` +
|
|
976
|
+
`\`manifest\` (e.g. via createRemarkNotationOptions) so every ` +
|
|
977
|
+
`catalogued lesson is covered.`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
776
982
|
/**
|
|
777
983
|
* @param {{
|
|
778
984
|
* definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>),
|
|
779
985
|
* resolve?: (key: string, context: object) => object | undefined,
|
|
780
986
|
* unknown?: 'error' | 'warn' | 'ignore',
|
|
987
|
+
* strict?: boolean,
|
|
781
988
|
* base?: string,
|
|
782
989
|
* manifest?: object,
|
|
783
990
|
* equations?: Record<string, Record<string, string>>,
|
|
784
991
|
* tables?: Record<string, Record<string, string>>,
|
|
785
992
|
* figures?: Record<string, Record<string, string>>,
|
|
786
|
-
*
|
|
993
|
+
* diagrams?: Record<string, Record<string, string>>,
|
|
994
|
+
* checks?: Record<string, Record<string, string>>,
|
|
995
|
+
* numbering?: Record<string, { part: number, equations: number, tables: number, figures: number, diagrams: number, checks: number }>,
|
|
787
996
|
* chapters?: Record<string, number>,
|
|
788
997
|
* }} [options]
|
|
789
998
|
*
|
|
790
|
-
* `manifest` is the preferred wiring: pass the compiled manifest and
|
|
999
|
+
* `manifest` is the preferred wiring: pass the compiled manifest and all
|
|
791
1000
|
* numbering inputs below are read off it together, so they cannot drift apart.
|
|
792
1001
|
* They stay available individually for hand-built trees, and an explicitly
|
|
793
1002
|
* supplied one still wins over the manifest.
|
|
794
1003
|
*
|
|
795
1004
|
* `numbering` maps a lesson slug to its **part** (the number before the dot)
|
|
796
1005
|
* and, per kind of float, how many the earlier lessons of that part already
|
|
797
|
-
* used. Every display equation / `<Table>` / `<Figure>` on the page is then
|
|
1006
|
+
* used. Every display equation / `<Table>` / `<Figure>` / `<Diagram>` on the page is then
|
|
798
1007
|
* numbered `‹part›.‹offset + n›`, with `##` and `###` headings ignored; with no
|
|
799
1008
|
* entry the legacy `##`-section scheme applies. It must agree with
|
|
800
|
-
* `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`
|
|
1009
|
+
* `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`/`diagrams`
|
|
801
1010
|
* above — supplying the tables without `numbering` numbers the same equation
|
|
802
1011
|
* twice, and `assertManifestNumberParity` turns that into a build error rather
|
|
803
1012
|
* than a page whose references disagree with their targets.
|
|
@@ -805,9 +1014,18 @@ function assertManifestNumberParity({
|
|
|
805
1014
|
* `chapters` is the superseded per-lesson form (slug → the lesson's course
|
|
806
1015
|
* ordinal, no running offset). Still read when `numbering` has no entry, so a
|
|
807
1016
|
* config written against the old option keeps building.
|
|
1017
|
+
*
|
|
1018
|
+
* `strict` (default `false`) additionally fails the build when a catalogued
|
|
1019
|
+
* lesson contains an equation/`<Table>`/`<Figure>` — keyed or not — but has no
|
|
1020
|
+
* `numbering` placement or no cross-page number table entry, instead of
|
|
1021
|
+
* silently rendering it under the legacy `##`-section scheme. Off by default
|
|
1022
|
+
* for backward compatibility; `createRemarkNotationOptions(manifest, { strict:
|
|
1023
|
+
* true })` is the recommended way to turn it on for a course whose manifest
|
|
1024
|
+
* covers every catalogued lesson.
|
|
808
1025
|
*/
|
|
809
1026
|
export default function remarkNotation(options = {}) {
|
|
810
1027
|
const unknown = options.unknown ?? 'error';
|
|
1028
|
+
const strict = options.strict ?? false;
|
|
811
1029
|
// The manifest supplies every numbering input at once; an explicit option
|
|
812
1030
|
// still overrides it, so existing per-field wiring keeps working.
|
|
813
1031
|
const numbering = { ...numberingFromManifest(options.manifest), ...options };
|
|
@@ -878,8 +1096,18 @@ export default function remarkNotation(options = {}) {
|
|
|
878
1096
|
const equationByNode = new Map(
|
|
879
1097
|
displayEquations.map((equation) => [equation.node, equation]),
|
|
880
1098
|
);
|
|
1099
|
+
if (strict) {
|
|
1100
|
+
assertStrictNumbering({
|
|
1101
|
+
file,
|
|
1102
|
+
slug: lessonSlug,
|
|
1103
|
+
noun: 'equation',
|
|
1104
|
+
count: displayEquations.length,
|
|
1105
|
+
placement: equationNumbering,
|
|
1106
|
+
crossPage: numbering.equations,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
881
1109
|
|
|
882
|
-
// Keyed floats
|
|
1110
|
+
// Keyed floats: every `<Table>`/`<Figure>`/`<Diagram>` on a lesson
|
|
883
1111
|
// page is numbered and anchored, keyed or not — same rule as equations,
|
|
884
1112
|
// via the shared engine in `keyed-jsx-floats.mjs`.
|
|
885
1113
|
const makeFloatCtx = (tagName, crossPage, kind) => {
|
|
@@ -908,6 +1136,17 @@ export default function remarkNotation(options = {}) {
|
|
|
908
1136
|
noun: tagName,
|
|
909
1137
|
syntax: (key) => `<${tagName} id="${key}">`,
|
|
910
1138
|
});
|
|
1139
|
+
// Diagram is deliberately excluded — see `assertStrictNumbering`'s doc.
|
|
1140
|
+
if (strict && tagName !== 'Diagram') {
|
|
1141
|
+
assertStrictNumbering({
|
|
1142
|
+
file,
|
|
1143
|
+
slug: lessonSlug,
|
|
1144
|
+
noun: tagName,
|
|
1145
|
+
count: floats.length,
|
|
1146
|
+
placement: floatNumbering,
|
|
1147
|
+
crossPage,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
911
1150
|
return {
|
|
912
1151
|
ctx: {
|
|
913
1152
|
base: options.base ?? '',
|
|
@@ -927,6 +1166,18 @@ export default function remarkNotation(options = {}) {
|
|
|
927
1166
|
numbering.figures,
|
|
928
1167
|
'figures',
|
|
929
1168
|
);
|
|
1169
|
+
const { ctx: diagramCtx, byNode: diagramByNode } = makeFloatCtx(
|
|
1170
|
+
'Diagram',
|
|
1171
|
+
numbering.diagrams,
|
|
1172
|
+
'diagrams',
|
|
1173
|
+
);
|
|
1174
|
+
const checkCtx = {
|
|
1175
|
+
base: options.base ?? '',
|
|
1176
|
+
pageLabels: new Map(
|
|
1177
|
+
Object.entries(numbering.checks?.[lessonSlug] ?? {}),
|
|
1178
|
+
),
|
|
1179
|
+
crossPage: numbering.checks ?? {},
|
|
1180
|
+
};
|
|
930
1181
|
|
|
931
1182
|
const resolve = (key, kind, node) => {
|
|
932
1183
|
const context = {
|
|
@@ -1048,6 +1299,8 @@ export default function remarkNotation(options = {}) {
|
|
|
1048
1299
|
eqCtx,
|
|
1049
1300
|
tableCtx,
|
|
1050
1301
|
figureCtx,
|
|
1302
|
+
diagramCtx,
|
|
1303
|
+
checkCtx,
|
|
1051
1304
|
);
|
|
1052
1305
|
if (replacement) {
|
|
1053
1306
|
node.children.splice(index, 1, ...replacement);
|
|
@@ -1058,14 +1311,72 @@ export default function remarkNotation(options = {}) {
|
|
|
1058
1311
|
visit(child, false);
|
|
1059
1312
|
if (child && child.type === 'math' && equationByNode.has(child)) {
|
|
1060
1313
|
node.children[index] = equationNode(child, equationByNode.get(child));
|
|
1314
|
+
} else if (lessonId && child?.type === 'inlineMath') {
|
|
1315
|
+
// Scoped to lesson pages, same as `displayEquations` above — a
|
|
1316
|
+
// registry/glossary page's inline math is left exactly as before.
|
|
1317
|
+
node.children[index] = inlineEquationNode(child);
|
|
1061
1318
|
} else if (child?.type === 'mdxJsxFlowElement' && tableByNode.has(child)) {
|
|
1062
1319
|
injectFloatNumberAttributes(child, tableByNode.get(child), 'tab');
|
|
1063
1320
|
} else if (child?.type === 'mdxJsxFlowElement' && figureByNode.has(child)) {
|
|
1064
1321
|
injectFloatNumberAttributes(child, figureByNode.get(child), 'fig');
|
|
1322
|
+
} else if (child?.type === 'mdxJsxFlowElement' && diagramByNode.has(child)) {
|
|
1323
|
+
injectFloatNumberAttributes(child, diagramByNode.get(child), 'dia');
|
|
1065
1324
|
}
|
|
1066
1325
|
}
|
|
1067
1326
|
};
|
|
1068
1327
|
|
|
1328
|
+
// Render-time defense in depth for content invisible to keyed-float
|
|
1329
|
+
// validation — the manifest builder runs the same check via
|
|
1330
|
+
// `validateRawFloats`/`scanRawFloats` (raw-floats.mjs); this catches
|
|
1331
|
+
// content added since the manifest was last compiled.
|
|
1332
|
+
if (lessonId) {
|
|
1333
|
+
const rawFloats = collectRawFloats(tree);
|
|
1334
|
+
const plural = (count, noun) => (count === 1 ? noun : `${noun}s`);
|
|
1335
|
+
if (rawFloats.markdownTableNodes.length > 0) {
|
|
1336
|
+
warn(
|
|
1337
|
+
file,
|
|
1338
|
+
`${lessonSlug} has ${rawFloats.markdownTableNodes.length} raw ` +
|
|
1339
|
+
`Markdown ${plural(rawFloats.markdownTableNodes.length, 'table')} ` +
|
|
1340
|
+
`not wrapped in <Table>; it has no stable number and cannot be ` +
|
|
1341
|
+
`referenced with [[tab-…]]. Wrap it in <Table id="…">…</Table> ` +
|
|
1342
|
+
`to give it one.`,
|
|
1343
|
+
rawFloats.markdownTableNodes[0],
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
if (rawFloats.htmlTableNodes.length > 0) {
|
|
1347
|
+
warn(
|
|
1348
|
+
file,
|
|
1349
|
+
`${lessonSlug} has ${rawFloats.htmlTableNodes.length} raw HTML ` +
|
|
1350
|
+
`<table> ${plural(rawFloats.htmlTableNodes.length, 'element')} ` +
|
|
1351
|
+
`not wrapped in <Table>; it has no stable number and cannot be ` +
|
|
1352
|
+
`referenced with [[tab-…]]. Wrap it in <Table id="…">…</Table> ` +
|
|
1353
|
+
`to give it one.`,
|
|
1354
|
+
rawFloats.htmlTableNodes[0],
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (rawFloats.unkeyedDiagramNodes.length > 0) {
|
|
1358
|
+
warn(
|
|
1359
|
+
file,
|
|
1360
|
+
`${lessonSlug} has ${rawFloats.unkeyedDiagramNodes.length} ` +
|
|
1361
|
+
`<Diagram> ${plural(rawFloats.unkeyedDiagramNodes.length, 'component')} ` +
|
|
1362
|
+
`with no id; it cannot be referenced with [[dia-…]]. Add an id ` +
|
|
1363
|
+
`to give it a stable reference.`,
|
|
1364
|
+
rawFloats.unkeyedDiagramNodes[0],
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
if (rawFloats.nestedDiagramNodes.length > 0) {
|
|
1368
|
+
warn(
|
|
1369
|
+
file,
|
|
1370
|
+
`${lessonSlug} has ${rawFloats.nestedDiagramNodes.length} ` +
|
|
1371
|
+
`<Diagram> ${plural(rawFloats.nestedDiagramNodes.length, 'component')} ` +
|
|
1372
|
+
`nested inside another component; it is invisible to diagram ` +
|
|
1373
|
+
`numbering. Move it to the top level (or inside ` +
|
|
1374
|
+
`<Memo>/<ShortNote>), or wrap it in <Figure> to give it identity.`,
|
|
1375
|
+
rawFloats.nestedDiagramNodes[0],
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1069
1380
|
visit(tree);
|
|
1070
1381
|
|
|
1071
1382
|
file.data.notationReferences = references;
|
|
@@ -1089,5 +1400,6 @@ export default function remarkNotation(options = {}) {
|
|
|
1089
1400
|
}));
|
|
1090
1401
|
file.data.tableLabels = floatLabelData(tableByNode);
|
|
1091
1402
|
file.data.figureLabels = floatLabelData(figureByNode);
|
|
1403
|
+
file.data.diagramLabels = floatLabelData(diagramByNode);
|
|
1092
1404
|
};
|
|
1093
1405
|
}
|