explico 0.2.0 → 0.4.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 +173 -0
- package/README.md +67 -7
- package/dist/compiler/astro.d.ts +30 -0
- package/dist/compiler/astro.js +79 -0
- package/dist/compiler/cli.d.ts +16 -0
- package/dist/compiler/cli.js +43 -9
- package/dist/compiler/collections.js +4 -1
- package/dist/compiler/course-config.d.ts +80 -4
- package/dist/compiler/course-config.js +12 -0
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/lesson-derivation.d.ts +1 -1
- package/dist/compiler/lesson-derivation.js +2 -2
- package/dist/compiler/manifest.d.ts +98 -2
- package/dist/compiler/manifest.js +153 -24
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +51 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +10 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +80 -13
- package/dist/reference/equations.mjs +109 -19
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/keyed-float-validate.d.ts +73 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +64 -0
- package/dist/reference/keyed-jsx-floats.mjs +191 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/remark-notation.d.mts +61 -0
- package/dist/reference/remark-notation.mjs +344 -10
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +100 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +169 -0
- package/docs/authoring/notation.md +297 -0
- package/docs/authoring/structure.md +139 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/astro.ts +127 -0
- package/src/compiler/cli.ts +52 -7
- package/src/compiler/collections.ts +12 -3
- package/src/compiler/course-config.ts +93 -3
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/lesson-derivation.ts +2 -2
- package/src/compiler/manifest.ts +320 -26
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/notation/NotationLayer.astro +11 -0
- package/src/components/notation/render-notation-math.ts +13 -2
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSidebar.astro +62 -1
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +15 -2
- package/src/components/table/Table.astro +105 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +83 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +8 -1
- package/src/reference/equations.mjs +109 -19
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/keyed-float-validate.ts +210 -0
- package/src/reference/keyed-jsx-floats.mjs +191 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/remark-notation.mjs +344 -10
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +244 -7
- package/src/components/starlight/EquationEnhancer.astro +0 -87
|
@@ -42,6 +42,11 @@ import {
|
|
|
42
42
|
parseEquationRef,
|
|
43
43
|
stripEquationLabels,
|
|
44
44
|
} from './equations.mjs';
|
|
45
|
+
import {
|
|
46
|
+
collectKeyedJsxFloats,
|
|
47
|
+
keyedFloatAnchorId,
|
|
48
|
+
parseKeyedFloatRef,
|
|
49
|
+
} from './keyed-jsx-floats.mjs';
|
|
45
50
|
|
|
46
51
|
const PROSE_SKIP_TYPES = new Set([
|
|
47
52
|
'code',
|
|
@@ -340,6 +345,8 @@ function equationNode(mathNode, { key, number }) {
|
|
|
340
345
|
className: ['keyed-equation__number'],
|
|
341
346
|
'data-eq-number': number,
|
|
342
347
|
'aria-label': `Equation ${number}`,
|
|
348
|
+
// Plain hover tooltip — the number is a permalink to this equation.
|
|
349
|
+
title: 'Anchor to this equation',
|
|
343
350
|
},
|
|
344
351
|
},
|
|
345
352
|
},
|
|
@@ -347,6 +354,32 @@ function equationNode(mathNode, { key, number }) {
|
|
|
347
354
|
};
|
|
348
355
|
}
|
|
349
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Give a `<Table>`/`<Figure>` `mdxJsxFlowElement` its computed `number` and
|
|
359
|
+
* `anchorId` as real JSX attributes, so the actual Astro component (which
|
|
360
|
+
* renders the numbered anchor, unlike `equationNode`'s synthesized hast div)
|
|
361
|
+
* receives them as props. The compiler is the single source of truth for
|
|
362
|
+
* both — the component only renders what it's given.
|
|
363
|
+
*/
|
|
364
|
+
function injectFloatNumberAttributes(node, float, prefix) {
|
|
365
|
+
const anchorId = keyedFloatAnchorId({
|
|
366
|
+
key: float.key,
|
|
367
|
+
number: float.number,
|
|
368
|
+
prefix,
|
|
369
|
+
});
|
|
370
|
+
node.attributes = (node.attributes ?? []).filter(
|
|
371
|
+
(attribute) =>
|
|
372
|
+
!(
|
|
373
|
+
attribute?.type === 'mdxJsxAttribute' &&
|
|
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
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
350
383
|
/** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
|
|
351
384
|
function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
352
385
|
if (eqRef.lesson) {
|
|
@@ -374,7 +407,47 @@ function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
|
374
407
|
return { number, href: `#eq-${eqRef.key}` };
|
|
375
408
|
}
|
|
376
409
|
|
|
377
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Resolve a `[[<prefix>-key]]` / `[[slug#<prefix>-key]]` reference (a
|
|
412
|
+
* `<Table>`/`<Figure>` float) to `{ number, href }` — the table/figure
|
|
413
|
+
* equivalent of `resolveEquationRef`.
|
|
414
|
+
*/
|
|
415
|
+
function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
|
|
416
|
+
if (floatRef.lesson) {
|
|
417
|
+
const number = ctx.crossPage?.[floatRef.lesson]?.[floatRef.key];
|
|
418
|
+
if (number === undefined) {
|
|
419
|
+
fail(
|
|
420
|
+
file,
|
|
421
|
+
`Unknown ${tagName} reference [[${floatRef.lesson}#${prefix}-${floatRef.key}]]: lesson ${floatRef.lesson} has no <${tagName}> with id="${floatRef.key}".`,
|
|
422
|
+
node,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
number,
|
|
427
|
+
href: `${ctx.base.replace(/\/$/, '')}/${floatRef.lesson}/#${prefix}-${floatRef.key}`,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const number = ctx.pageLabels.get(floatRef.key);
|
|
431
|
+
if (number === undefined) {
|
|
432
|
+
fail(
|
|
433
|
+
file,
|
|
434
|
+
`Unknown ${tagName} reference [[${prefix}-${floatRef.key}]]: no <${tagName}> on this page carries id="${floatRef.key}".`,
|
|
435
|
+
node,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
return { number, href: `#${prefix}-${floatRef.key}` };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function proseNodes(
|
|
442
|
+
value,
|
|
443
|
+
resolve,
|
|
444
|
+
file,
|
|
445
|
+
node,
|
|
446
|
+
references,
|
|
447
|
+
eqCtx,
|
|
448
|
+
tableCtx,
|
|
449
|
+
figureCtx,
|
|
450
|
+
) {
|
|
378
451
|
const base = eqCtx?.base ?? '';
|
|
379
452
|
const strayExplain = firstUnescaped(value, EXPLAIN);
|
|
380
453
|
if (strayExplain !== -1) {
|
|
@@ -395,7 +468,18 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
|
|
|
395
468
|
|
|
396
469
|
const key = match[1].trim();
|
|
397
470
|
const eqRef = eqCtx ? parseEquationRef(key) : undefined;
|
|
398
|
-
|
|
471
|
+
const tableRef =
|
|
472
|
+
!eqRef && tableCtx ? parseKeyedFloatRef(key, { prefix: 'tab' }) : undefined;
|
|
473
|
+
const figureRef =
|
|
474
|
+
!eqRef && !tableRef && figureCtx
|
|
475
|
+
? parseKeyedFloatRef(key, { prefix: 'fig' })
|
|
476
|
+
: undefined;
|
|
477
|
+
if (
|
|
478
|
+
!eqRef &&
|
|
479
|
+
!tableRef &&
|
|
480
|
+
!figureRef &&
|
|
481
|
+
!NOTATION_KEY_PATTERN.test(key)
|
|
482
|
+
) {
|
|
399
483
|
fail(file, `Invalid notation key "${key}" in [[...]].`, node);
|
|
400
484
|
}
|
|
401
485
|
|
|
@@ -426,9 +510,46 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
|
|
|
426
510
|
continue;
|
|
427
511
|
}
|
|
428
512
|
|
|
513
|
+
const floatRef = tableRef ?? figureRef;
|
|
514
|
+
if (floatRef) {
|
|
515
|
+
const kind = tableRef ? 'table' : 'figure';
|
|
516
|
+
const prefix = tableRef ? 'tab' : 'fig';
|
|
517
|
+
const tagName = tableRef ? 'Table' : 'Figure';
|
|
518
|
+
const { number, href } = resolveKeyedFloatRef(
|
|
519
|
+
floatRef,
|
|
520
|
+
tableRef ? tableCtx : figureCtx,
|
|
521
|
+
{ prefix, tagName },
|
|
522
|
+
file,
|
|
523
|
+
node,
|
|
524
|
+
);
|
|
525
|
+
output.push({
|
|
526
|
+
type: 'link',
|
|
527
|
+
url: href,
|
|
528
|
+
children: [{ type: 'text', value: `${tagName} ${number}` }],
|
|
529
|
+
data: {
|
|
530
|
+
hProperties: {
|
|
531
|
+
className: [`${kind}-ref`],
|
|
532
|
+
[`data-${prefix}-ref`]: floatRef.key,
|
|
533
|
+
...(floatRef.lesson ? { [`data-${prefix}-lesson`]: floatRef.lesson } : {}),
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
references.push({
|
|
538
|
+
key: floatRef.key,
|
|
539
|
+
kind,
|
|
540
|
+
...(floatRef.lesson ? { lesson: floatRef.lesson } : {}),
|
|
541
|
+
});
|
|
542
|
+
cursor = match.index + match[0].length;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
429
546
|
const definition = resolve(key, 'prose', node);
|
|
430
|
-
|
|
431
|
-
|
|
547
|
+
// `title`/`summary` are the pre-C1b field names, kept as the first
|
|
548
|
+
// fallback for a caller still supplying them; real content authors
|
|
549
|
+
// `label`/`meaning` (see `content-config.ts`), which raw frontmatter
|
|
550
|
+
// (`manifest.notation.raw`, a page's own `notation.local`) carries as-is.
|
|
551
|
+
const title = definition?.title ?? definition?.label ?? key;
|
|
552
|
+
const summary = definition?.summary ?? definition?.meaning;
|
|
432
553
|
const href = defaultHref(definition, key, base);
|
|
433
554
|
|
|
434
555
|
const properties = {
|
|
@@ -550,17 +671,146 @@ function inspectExplainCalls(value, resolve, file, node, references) {
|
|
|
550
671
|
}
|
|
551
672
|
}
|
|
552
673
|
|
|
674
|
+
/**
|
|
675
|
+
* The numbering inputs a lesson page needs, read off one manifest.
|
|
676
|
+
*
|
|
677
|
+
* Every number on a page comes from two places that MUST agree: the page's own
|
|
678
|
+
* counters (`numbering`) and the manifest tables a cross-page `[[slug#eq-key]]`
|
|
679
|
+
* is resolved against (`equations` / `tables` / `figures`). Deriving all four
|
|
680
|
+
* from the same manifest in one step is what stops a course config from
|
|
681
|
+
* supplying one without the other — the omission that produced page-local
|
|
682
|
+
* `(2.2)` and cross-page `(4.5)` for the same equation.
|
|
683
|
+
*
|
|
684
|
+
* `numbering` maps a lesson slug to `{ part, equations, tables, figures }`:
|
|
685
|
+
* the part's ordinal (the number before the dot) and, per kind of float, how
|
|
686
|
+
* many of them the earlier lessons of that part already used.
|
|
687
|
+
*/
|
|
688
|
+
export function numberingFromManifest(manifest) {
|
|
689
|
+
if (!manifest || typeof manifest !== 'object') return {};
|
|
690
|
+
return {
|
|
691
|
+
equations: manifest.equations?.numbersBySlug ?? {},
|
|
692
|
+
tables: manifest.tables?.numbersBySlug ?? {},
|
|
693
|
+
figures: manifest.figures?.numbersBySlug ?? {},
|
|
694
|
+
numbering: Object.fromEntries(
|
|
695
|
+
(manifest.lessons ?? []).map((lesson) => [
|
|
696
|
+
lesson.slug,
|
|
697
|
+
{
|
|
698
|
+
part: lesson.part,
|
|
699
|
+
equations: lesson.numbering?.equations ?? 0,
|
|
700
|
+
tables: lesson.numbering?.tables ?? 0,
|
|
701
|
+
figures: lesson.numbering?.figures ?? 0,
|
|
702
|
+
},
|
|
703
|
+
]),
|
|
704
|
+
),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* `{ prefix, offset }` for one kind of float on one page, from whichever
|
|
710
|
+
* numbering option the caller supplied. `numbering` is the current form;
|
|
711
|
+
* `chapters` is the superseded per-lesson form (prefix = the lesson's ordinal,
|
|
712
|
+
* no offset) and is still read so an unmigrated config keeps working.
|
|
713
|
+
*/
|
|
714
|
+
function pageNumbering(options, slug, kind) {
|
|
715
|
+
if (!slug) return undefined;
|
|
716
|
+
const placement = options.numbering?.[slug];
|
|
717
|
+
if (placement?.part) {
|
|
718
|
+
return { prefix: placement.part, offset: placement[kind] ?? 0 };
|
|
719
|
+
}
|
|
720
|
+
const chapter = options.chapters?.[slug];
|
|
721
|
+
return chapter ? { prefix: chapter, offset: 0 } : undefined;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Fail when the number this page just computed for a keyed float disagrees
|
|
726
|
+
* with the number the manifest publishes for the same key (WERN — a reference
|
|
727
|
+
* that shows a different number than its target). The two are produced by the
|
|
728
|
+
* same algorithm from the same content, so any disagreement means the render
|
|
729
|
+
* path and the manifest were fed different inputs; in practice, an
|
|
730
|
+
* `astro.config.mjs` that passes the manifest number tables but not
|
|
731
|
+
* `numbering`, which silently drops the page back to the legacy `##`-section
|
|
732
|
+
* scheme.
|
|
733
|
+
*/
|
|
734
|
+
function assertManifestNumberParity({
|
|
735
|
+
file,
|
|
736
|
+
slug,
|
|
737
|
+
pageLabels,
|
|
738
|
+
crossPage,
|
|
739
|
+
placement,
|
|
740
|
+
noun,
|
|
741
|
+
syntax,
|
|
742
|
+
}) {
|
|
743
|
+
// No entry for this slug at all means the caller supplied no manifest table
|
|
744
|
+
// (hand-built test trees, non-lesson pages) — nothing to cross-check.
|
|
745
|
+
const published = crossPage?.[slug];
|
|
746
|
+
if (!published) return;
|
|
747
|
+
|
|
748
|
+
for (const [key, number] of pageLabels) {
|
|
749
|
+
const expected = published[key];
|
|
750
|
+
if (expected === undefined) {
|
|
751
|
+
fail(
|
|
752
|
+
file,
|
|
753
|
+
`Stale Explico manifest for ${slug}: this page numbers ${noun} ` +
|
|
754
|
+
`${syntax(key)} as (${number}), but the manifest lists no such ` +
|
|
755
|
+
`${noun} for the lesson. Recompile the manifest before rendering.`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (expected !== number) {
|
|
759
|
+
fail(
|
|
760
|
+
file,
|
|
761
|
+
`Inconsistent ${noun} numbering for ${syntax(key)} in ${slug}: this ` +
|
|
762
|
+
`page renders it as (${number}) while a cross-page reference to it ` +
|
|
763
|
+
`resolves to (${expected}). ` +
|
|
764
|
+
(placement === undefined
|
|
765
|
+
? 'The remarkNotation options carry the manifest number tables ' +
|
|
766
|
+
'but no `numbering` entry for this lesson, so the page fell ' +
|
|
767
|
+
'back to the legacy `##`-section scheme. Pass `manifest` to ' +
|
|
768
|
+
'remarkNotation so both sides use the part scheme.'
|
|
769
|
+
: 'The page counters and the manifest disagree; recompile the ' +
|
|
770
|
+
'manifest from the current content.'),
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
553
776
|
/**
|
|
554
777
|
* @param {{
|
|
555
778
|
* definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>),
|
|
556
779
|
* resolve?: (key: string, context: object) => object | undefined,
|
|
557
780
|
* unknown?: 'error' | 'warn' | 'ignore',
|
|
558
781
|
* base?: string,
|
|
782
|
+
* manifest?: object,
|
|
559
783
|
* equations?: Record<string, Record<string, string>>,
|
|
784
|
+
* tables?: Record<string, Record<string, string>>,
|
|
785
|
+
* figures?: Record<string, Record<string, string>>,
|
|
786
|
+
* numbering?: Record<string, { part: number, equations: number, tables: number, figures: number }>,
|
|
787
|
+
* chapters?: Record<string, number>,
|
|
560
788
|
* }} [options]
|
|
789
|
+
*
|
|
790
|
+
* `manifest` is the preferred wiring: pass the compiled manifest and the four
|
|
791
|
+
* numbering inputs below are read off it together, so they cannot drift apart.
|
|
792
|
+
* They stay available individually for hand-built trees, and an explicitly
|
|
793
|
+
* supplied one still wins over the manifest.
|
|
794
|
+
*
|
|
795
|
+
* `numbering` maps a lesson slug to its **part** (the number before the dot)
|
|
796
|
+
* 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
|
|
798
|
+
* numbered `‹part›.‹offset + n›`, with `##` and `###` headings ignored; with no
|
|
799
|
+
* entry the legacy `##`-section scheme applies. It must agree with
|
|
800
|
+
* `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`
|
|
801
|
+
* above — supplying the tables without `numbering` numbers the same equation
|
|
802
|
+
* twice, and `assertManifestNumberParity` turns that into a build error rather
|
|
803
|
+
* than a page whose references disagree with their targets.
|
|
804
|
+
*
|
|
805
|
+
* `chapters` is the superseded per-lesson form (slug → the lesson's course
|
|
806
|
+
* ordinal, no running offset). Still read when `numbering` has no entry, so a
|
|
807
|
+
* config written against the old option keeps building.
|
|
561
808
|
*/
|
|
562
809
|
export default function remarkNotation(options = {}) {
|
|
563
810
|
const unknown = options.unknown ?? 'error';
|
|
811
|
+
// The manifest supplies every numbering input at once; an explicit option
|
|
812
|
+
// still overrides it, so existing per-field wiring keeps working.
|
|
813
|
+
const numbering = { ...numberingFromManifest(options.manifest), ...options };
|
|
564
814
|
|
|
565
815
|
return function transform(tree, file) {
|
|
566
816
|
const suppliedDefinitions =
|
|
@@ -573,6 +823,10 @@ export default function remarkNotation(options = {}) {
|
|
|
573
823
|
);
|
|
574
824
|
const localDefinitions = localDefinitionsFromFile(file);
|
|
575
825
|
const lessonId = lessonIdFrom(file);
|
|
826
|
+
const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
|
|
827
|
+
// `{ prefix, offset }` per kind of float — see the `numbering` option.
|
|
828
|
+
// Missing ⇒ the legacy `##`-section numbering.
|
|
829
|
+
const equationNumbering = pageNumbering(numbering, lessonSlug, 'equations');
|
|
576
830
|
const registryFile = isNotationRegistryFile(file);
|
|
577
831
|
const mathScope = pageGlyphScope(
|
|
578
832
|
localDefinitions,
|
|
@@ -582,10 +836,11 @@ export default function remarkNotation(options = {}) {
|
|
|
582
836
|
const references = [];
|
|
583
837
|
const resolved = new Map();
|
|
584
838
|
|
|
585
|
-
// Equation identity (D7): assign every `\label{eq:…}` its
|
|
586
|
-
//
|
|
587
|
-
// the
|
|
588
|
-
|
|
839
|
+
// Equation identity (D7): assign every `\label{eq:…}` its number before any
|
|
840
|
+
// math node is rewritten, so a `[[eq-key]]` earlier in the page still
|
|
841
|
+
// resolves. `equationNumbering` picks the `‹part›.‹n›` vs `##`-section
|
|
842
|
+
// scheme.
|
|
843
|
+
const equationLabels = collectEquationLabels(tree, equationNumbering);
|
|
589
844
|
const pageLabels = new Map();
|
|
590
845
|
for (const label of equationLabels) {
|
|
591
846
|
if (pageLabels.has(label.key)) {
|
|
@@ -600,17 +855,79 @@ export default function remarkNotation(options = {}) {
|
|
|
600
855
|
const eqCtx = {
|
|
601
856
|
base: options.base ?? '',
|
|
602
857
|
pageLabels,
|
|
603
|
-
crossPage:
|
|
858
|
+
crossPage: numbering.equations ?? {},
|
|
604
859
|
};
|
|
860
|
+
// WERN guard: what this page prints next to the equation must be what a
|
|
861
|
+
// reference from another page prints for it.
|
|
862
|
+
assertManifestNumberParity({
|
|
863
|
+
file,
|
|
864
|
+
slug: lessonSlug,
|
|
865
|
+
pageLabels,
|
|
866
|
+
crossPage: eqCtx.crossPage,
|
|
867
|
+
placement: equationNumbering,
|
|
868
|
+
noun: 'equation',
|
|
869
|
+
syntax: (key) => `eq:${key}`,
|
|
870
|
+
});
|
|
605
871
|
|
|
606
872
|
// Number and anchor every display equation on a lesson page, keyed or
|
|
607
873
|
// not (F3). Non-lesson pages (notation registry, stray docs) keep the
|
|
608
874
|
// keyed-only behaviour so their body math is left untouched.
|
|
609
|
-
const displayEquations = lessonId
|
|
875
|
+
const displayEquations = lessonId
|
|
876
|
+
? collectDisplayEquations(tree, equationNumbering)
|
|
877
|
+
: [];
|
|
610
878
|
const equationByNode = new Map(
|
|
611
879
|
displayEquations.map((equation) => [equation.node, equation]),
|
|
612
880
|
);
|
|
613
881
|
|
|
882
|
+
// Keyed floats (F-tables-figures): every `<Table>`/`<Figure>` on a lesson
|
|
883
|
+
// page is numbered and anchored, keyed or not — same rule as equations,
|
|
884
|
+
// via the shared engine in `keyed-jsx-floats.mjs`.
|
|
885
|
+
const makeFloatCtx = (tagName, crossPage, kind) => {
|
|
886
|
+
const floatNumbering = pageNumbering(numbering, lessonSlug, kind);
|
|
887
|
+
const floats = lessonId
|
|
888
|
+
? collectKeyedJsxFloats(tree, { tagName, numbering: floatNumbering })
|
|
889
|
+
: [];
|
|
890
|
+
const pageFloatLabels = new Map();
|
|
891
|
+
for (const float of floats) {
|
|
892
|
+
if (float.key == null) continue;
|
|
893
|
+
if (pageFloatLabels.has(float.key)) {
|
|
894
|
+
fail(
|
|
895
|
+
file,
|
|
896
|
+
`Duplicate <${tagName} id="${float.key}"> on this page; an id is unique per lesson.`,
|
|
897
|
+
float.node,
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
pageFloatLabels.set(float.key, float.number);
|
|
901
|
+
}
|
|
902
|
+
assertManifestNumberParity({
|
|
903
|
+
file,
|
|
904
|
+
slug: lessonSlug,
|
|
905
|
+
pageLabels: pageFloatLabels,
|
|
906
|
+
crossPage: crossPage ?? {},
|
|
907
|
+
placement: floatNumbering,
|
|
908
|
+
noun: tagName,
|
|
909
|
+
syntax: (key) => `<${tagName} id="${key}">`,
|
|
910
|
+
});
|
|
911
|
+
return {
|
|
912
|
+
ctx: {
|
|
913
|
+
base: options.base ?? '',
|
|
914
|
+
pageLabels: pageFloatLabels,
|
|
915
|
+
crossPage: crossPage ?? {},
|
|
916
|
+
},
|
|
917
|
+
byNode: new Map(floats.map((float) => [float.node, float])),
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
const { ctx: tableCtx, byNode: tableByNode } = makeFloatCtx(
|
|
921
|
+
'Table',
|
|
922
|
+
numbering.tables,
|
|
923
|
+
'tables',
|
|
924
|
+
);
|
|
925
|
+
const { ctx: figureCtx, byNode: figureByNode } = makeFloatCtx(
|
|
926
|
+
'Figure',
|
|
927
|
+
numbering.figures,
|
|
928
|
+
'figures',
|
|
929
|
+
);
|
|
930
|
+
|
|
614
931
|
const resolve = (key, kind, node) => {
|
|
615
932
|
const context = {
|
|
616
933
|
file,
|
|
@@ -729,6 +1046,8 @@ export default function remarkNotation(options = {}) {
|
|
|
729
1046
|
child,
|
|
730
1047
|
references,
|
|
731
1048
|
eqCtx,
|
|
1049
|
+
tableCtx,
|
|
1050
|
+
figureCtx,
|
|
732
1051
|
);
|
|
733
1052
|
if (replacement) {
|
|
734
1053
|
node.children.splice(index, 1, ...replacement);
|
|
@@ -739,6 +1058,10 @@ export default function remarkNotation(options = {}) {
|
|
|
739
1058
|
visit(child, false);
|
|
740
1059
|
if (child && child.type === 'math' && equationByNode.has(child)) {
|
|
741
1060
|
node.children[index] = equationNode(child, equationByNode.get(child));
|
|
1061
|
+
} else if (child?.type === 'mdxJsxFlowElement' && tableByNode.has(child)) {
|
|
1062
|
+
injectFloatNumberAttributes(child, tableByNode.get(child), 'tab');
|
|
1063
|
+
} else if (child?.type === 'mdxJsxFlowElement' && figureByNode.has(child)) {
|
|
1064
|
+
injectFloatNumberAttributes(child, figureByNode.get(child), 'fig');
|
|
742
1065
|
}
|
|
743
1066
|
}
|
|
744
1067
|
};
|
|
@@ -755,5 +1078,16 @@ export default function remarkNotation(options = {}) {
|
|
|
755
1078
|
indexInSection,
|
|
756
1079
|
}),
|
|
757
1080
|
);
|
|
1081
|
+
const floatLabelData = (byNode) =>
|
|
1082
|
+
[...byNode.values()]
|
|
1083
|
+
.filter((float) => float.key != null)
|
|
1084
|
+
.map(({ key, number, section, indexInSection }) => ({
|
|
1085
|
+
key,
|
|
1086
|
+
number,
|
|
1087
|
+
section,
|
|
1088
|
+
indexInSection,
|
|
1089
|
+
}));
|
|
1090
|
+
file.data.tableLabels = floatLabelData(tableByNode);
|
|
1091
|
+
file.data.figureLabels = floatLabelData(figureByNode);
|
|
758
1092
|
};
|
|
759
1093
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } from './keyed-float-validate.js';
|
|
2
|
+
|
|
3
|
+
/** `validateKeyedFloats` for `<Table>` — see `keyed-float-validate.ts`. */
|
|
4
|
+
|
|
5
|
+
type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused';
|
|
6
|
+
interface TableDiagnostic {
|
|
7
|
+
readonly code: TableDiagnosticCode;
|
|
8
|
+
readonly severity: 'error' | 'warning';
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly file?: string;
|
|
11
|
+
readonly lessonId?: string;
|
|
12
|
+
readonly key?: string;
|
|
13
|
+
}
|
|
14
|
+
type TableLabelRecord = KeyedFloatLabelRecord;
|
|
15
|
+
type TableLessonInput = KeyedFloatLessonInput;
|
|
16
|
+
interface TableValidation extends KeyedFloatValidation {
|
|
17
|
+
readonly diagnostics: readonly TableDiagnostic[];
|
|
18
|
+
}
|
|
19
|
+
declare function validateTables(lessons: readonly TableLessonInput[]): TableValidation;
|
|
20
|
+
|
|
21
|
+
export { type TableDiagnostic, type TableDiagnosticCode, type TableLabelRecord, type TableLessonInput, type TableValidation, validateTables };
|
|
@@ -97,7 +97,25 @@ const UNITS_VOCAB = /* @__PURE__ */ new Set([
|
|
|
97
97
|
"quantity",
|
|
98
98
|
"quoted",
|
|
99
99
|
"explicitly",
|
|
100
|
-
"calculations"
|
|
100
|
+
"calculations",
|
|
101
|
+
// motion / physical quantities (playground corpus)
|
|
102
|
+
"meter",
|
|
103
|
+
"meters",
|
|
104
|
+
"second",
|
|
105
|
+
"seconds",
|
|
106
|
+
"speed",
|
|
107
|
+
"velocity",
|
|
108
|
+
"acceleration",
|
|
109
|
+
"gravitational",
|
|
110
|
+
"launch",
|
|
111
|
+
"angle",
|
|
112
|
+
"degrees",
|
|
113
|
+
"horizontal",
|
|
114
|
+
"vertical",
|
|
115
|
+
"level",
|
|
116
|
+
"ground",
|
|
117
|
+
"squared",
|
|
118
|
+
"above"
|
|
101
119
|
]);
|
|
102
120
|
function unitsTokens(units) {
|
|
103
121
|
return units.toLowerCase().split(/[\s]+/).flatMap((word) => word.split(/(?<=[a-z0-9])-(?=[a-z])/)).map((word) => word.replace(/^[^a-z0-9_]+|[^a-z0-9_]+$/g, "")).filter((word) => word.length > 0 && !/^\d+$/.test(word));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Versioning
|
|
2
|
+
|
|
3
|
+
`explico` follows [Semantic Versioning](https://semver.org/). The installed
|
|
4
|
+
`package.json` `version` is authoritative for what a consumer has;
|
|
5
|
+
[`CHANGELOG.md`](../CHANGELOG.md) records what changed; this file defines what a
|
|
6
|
+
bump *means*.
|
|
7
|
+
|
|
8
|
+
There are two independent version streams. This document is about the **engine**.
|
|
9
|
+
A course built on explico carries its own **knowledge version** over its
|
|
10
|
+
content — see [`authoring/versioning.md`](authoring/versioning.md).
|
|
11
|
+
|
|
12
|
+
## The public surface
|
|
13
|
+
|
|
14
|
+
A change is "breaking" when it can break a consumer who followed the docs. The
|
|
15
|
+
consumer-visible surface is:
|
|
16
|
+
|
|
17
|
+
| Surface | Where |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Published module entry points | `package.json` `publishConfig.exports` — the `explico/*` specifiers and their `.d.ts` |
|
|
20
|
+
| Astro / React components | names + props of everything under `components/` that the README wires |
|
|
21
|
+
| remark / rehype adapter options | the option objects of `reference/*.mjs` (`remarkNotation`, `remarkCitation`, `createNotationKatexOptions`, …) |
|
|
22
|
+
| The compiler manifest | `MANIFEST_SCHEMA_VERSION` and the shape of `Manifest` |
|
|
23
|
+
| The `content` CLI | the commands, their flags, and every diagnostic `code` string |
|
|
24
|
+
| `CourseConfig` | the interface in `compiler/course-config.ts` |
|
|
25
|
+
| Build-enforced authoring rules | the `N*` / `V*` / `C*` rules marked *Build-enforced* in `docs/authoring/` |
|
|
26
|
+
|
|
27
|
+
## What each bump means
|
|
28
|
+
|
|
29
|
+
**MAJOR** — an incompatible change to any surface above: removing or renaming an
|
|
30
|
+
export, a component, a prop, or a CLI command; adding a required `CourseConfig`
|
|
31
|
+
field; a breaking change to the manifest shape (which also bumps
|
|
32
|
+
`MANIFEST_SCHEMA_VERSION`); tightening a build-enforced rule so content that
|
|
33
|
+
passed now fails.
|
|
34
|
+
|
|
35
|
+
**MINOR** — a backward-compatible addition: a new export, component, or optional
|
|
36
|
+
`CourseConfig` field; a new opt-in authoring rule; a new manifest field
|
|
37
|
+
(additive — still bump `MANIFEST_SCHEMA_VERSION`, note it additive in
|
|
38
|
+
`CHANGELOG.md`).
|
|
39
|
+
|
|
40
|
+
**PATCH** — a bug fix, a docs change, an internal refactor with no surface
|
|
41
|
+
change, or a new **Tier-3 warning** (warnings never fail a build, so surfacing
|
|
42
|
+
one does not break a consumer).
|
|
43
|
+
|
|
44
|
+
### Pre-1.0
|
|
45
|
+
|
|
46
|
+
While the version is `0.x`, a **MINOR** bump *may* include a breaking change —
|
|
47
|
+
every one is listed under a `### Breaking` heading in `CHANGELOG.md` for that
|
|
48
|
+
version. A **PATCH** never breaks. Once the API settles this clause is dropped
|
|
49
|
+
and normal semver applies.
|
|
50
|
+
|
|
51
|
+
## Cutting a release
|
|
52
|
+
|
|
53
|
+
1. Decide the next version from the rules above and the `## [Unreleased]`
|
|
54
|
+
section of `CHANGELOG.md`.
|
|
55
|
+
2. `CHANGELOG.md`: rename `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD`, add a
|
|
56
|
+
fresh empty `## [Unreleased]`, and fix up the compare links at the bottom.
|
|
57
|
+
3. Bump `package.json` `version` (`npm version x.y.z --no-git-tag-version`).
|
|
58
|
+
4. `pnpm build && pnpm test`.
|
|
59
|
+
5. Commit, then `git tag vx.y.z`.
|
|
60
|
+
|
|
61
|
+
## When you change the engine
|
|
62
|
+
|
|
63
|
+
Every PR that touches the public surface adds a bullet under `## [Unreleased]`
|
|
64
|
+
in `CHANGELOG.md`, in the right group (`Added` / `Changed` / `Fixed` /
|
|
65
|
+
`Removed`, plus `Breaking` when it is). Pick the group by the rules above; the
|
|
66
|
+
version number itself is set only at release time. See [`AGENTS.md`](../AGENTS.md).
|