explico 0.1.1 → 0.3.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 +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +17 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +31 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +87 -3
- package/dist/compiler/manifest.js +133 -20
- package/dist/compiler/resolution-report.d.ts +5 -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 +142 -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 +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +76 -18
- package/dist/reference/equations.mjs +118 -41
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/gate-math.d.ts +5 -4
- package/dist/reference/gate-math.js +73 -2
- package/dist/reference/gloss.d.ts +87 -0
- package/dist/reference/gloss.js +59 -0
- package/dist/reference/keyed-float-validate.d.ts +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/registry.js +25 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-notation.d.mts +12 -0
- package/dist/reference/remark-notation.mjs +228 -32
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/types.d.ts +25 -2
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +52 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +270 -17
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -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/hover-panel.ts +71 -5
- package/src/components/notation/NotationFormula.astro +154 -0
- package/src/components/notation/NotationGlossary.astro +60 -0
- package/src/components/notation/NotationLayer.astro +248 -103
- package/src/components/notation/render-notation-math.ts +66 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +8 -2
- package/src/components/table/Table.astro +104 -0
- package/src/content-config.ts +38 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +236 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +6 -1
- package/src/reference/equations.mjs +118 -41
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +228 -32
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/types.ts +26 -1
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +193 -3
- package/src/components/starlight/EquationEnhancer.astro +0 -72
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
* unknown?: 'error' | 'warn' | 'ignore',
|
|
6
6
|
* base?: string,
|
|
7
7
|
* equations?: Record<string, Record<string, string>>,
|
|
8
|
+
* tables?: Record<string, Record<string, string>>,
|
|
9
|
+
* figures?: Record<string, Record<string, string>>,
|
|
10
|
+
* chapters?: Record<string, number>,
|
|
8
11
|
* }} [options]
|
|
12
|
+
*
|
|
13
|
+
* `chapters` maps a lesson slug to its 1-based course ordinal. When the
|
|
14
|
+
* current page has one, every display equation / `<Table>` / `<Figure>` is
|
|
15
|
+
* numbered `‹chapter›.‹n›` (`##` headings ignored); with no entry the legacy
|
|
16
|
+
* `##`-section scheme applies. It must agree with `manifest.lessons[].chapter`,
|
|
17
|
+
* which is what feeds `equations`/`tables`/`figures` above.
|
|
9
18
|
*/
|
|
10
19
|
export default function remarkNotation(options?: {
|
|
11
20
|
definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>);
|
|
@@ -13,6 +22,9 @@ export default function remarkNotation(options?: {
|
|
|
13
22
|
unknown?: "error" | "warn" | "ignore";
|
|
14
23
|
base?: string;
|
|
15
24
|
equations?: Record<string, Record<string, string>>;
|
|
25
|
+
tables?: Record<string, Record<string, string>>;
|
|
26
|
+
figures?: Record<string, Record<string, string>>;
|
|
27
|
+
chapters?: Record<string, number>;
|
|
16
28
|
}): (tree: any, file: any) => void;
|
|
17
29
|
/**
|
|
18
30
|
* Remark adapter for semantic notation references.
|
|
@@ -36,10 +36,17 @@ export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
|
36
36
|
import { GlyphResolutionError, resolveMathGlyphs } from './math-glyphs.mjs';
|
|
37
37
|
import {
|
|
38
38
|
EQ_LABEL_PATTERN,
|
|
39
|
+
collectDisplayEquations,
|
|
39
40
|
collectEquationLabels,
|
|
41
|
+
equationAnchorId,
|
|
40
42
|
parseEquationRef,
|
|
41
43
|
stripEquationLabels,
|
|
42
44
|
} from './equations.mjs';
|
|
45
|
+
import {
|
|
46
|
+
collectKeyedJsxFloats,
|
|
47
|
+
keyedFloatAnchorId,
|
|
48
|
+
parseKeyedFloatRef,
|
|
49
|
+
} from './keyed-jsx-floats.mjs';
|
|
43
50
|
|
|
44
51
|
const PROSE_SKIP_TYPES = new Set([
|
|
45
52
|
'code',
|
|
@@ -307,34 +314,39 @@ function defaultHref(definition, key, base = '') {
|
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
/**
|
|
310
|
-
* Wrap a
|
|
311
|
-
* (which splices the inner `math-display` element out).
|
|
312
|
-
* a
|
|
313
|
-
*
|
|
317
|
+
* Wrap a display `$$…$$` `math` node so the container survives `rehype-katex`
|
|
318
|
+
* (which splices the inner `math-display` element out). Every display equation
|
|
319
|
+
* in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
|
|
320
|
+
* at its positional `#eq-<section>-<index>`. The visible number is a focusable
|
|
321
|
+
* `<a>` sibling — never authored, never inside KaTeX — so it survives print and
|
|
322
|
+
* screen readers name it "Equation 2.4".
|
|
314
323
|
*/
|
|
315
|
-
function
|
|
324
|
+
function equationNode(mathNode, { key, number }) {
|
|
325
|
+
const id = equationAnchorId({ key, number });
|
|
326
|
+
const hProperties = {
|
|
327
|
+
id,
|
|
328
|
+
className: ['keyed-equation'],
|
|
329
|
+
'data-eq-number': number,
|
|
330
|
+
};
|
|
331
|
+
if (key) hProperties['data-eq-key'] = key;
|
|
332
|
+
else hProperties['data-eq-auto'] = '';
|
|
333
|
+
|
|
316
334
|
return {
|
|
317
335
|
type: 'paragraph',
|
|
318
|
-
data: {
|
|
319
|
-
hName: 'div',
|
|
320
|
-
hProperties: {
|
|
321
|
-
id: `eq-${key}`,
|
|
322
|
-
className: ['keyed-equation'],
|
|
323
|
-
'data-eq-key': key,
|
|
324
|
-
'data-eq-number': number,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
336
|
+
data: { hName: 'div', hProperties },
|
|
327
337
|
children: [
|
|
328
338
|
mathNode,
|
|
329
339
|
{
|
|
330
340
|
type: 'link',
|
|
331
|
-
url:
|
|
341
|
+
url: `#${id}`,
|
|
332
342
|
children: [{ type: 'text', value: `(${number})` }],
|
|
333
343
|
data: {
|
|
334
344
|
hProperties: {
|
|
335
345
|
className: ['keyed-equation__number'],
|
|
336
346
|
'data-eq-number': number,
|
|
337
347
|
'aria-label': `Equation ${number}`,
|
|
348
|
+
// Plain hover tooltip — the number is a permalink to this equation.
|
|
349
|
+
title: 'Anchor to this equation',
|
|
338
350
|
},
|
|
339
351
|
},
|
|
340
352
|
},
|
|
@@ -342,6 +354,32 @@ function keyedEquationNode(mathNode, key, number) {
|
|
|
342
354
|
};
|
|
343
355
|
}
|
|
344
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
|
+
|
|
345
383
|
/** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
|
|
346
384
|
function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
347
385
|
if (eqRef.lesson) {
|
|
@@ -369,7 +407,47 @@ function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
|
369
407
|
return { number, href: `#eq-${eqRef.key}` };
|
|
370
408
|
}
|
|
371
409
|
|
|
372
|
-
|
|
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
|
+
) {
|
|
373
451
|
const base = eqCtx?.base ?? '';
|
|
374
452
|
const strayExplain = firstUnescaped(value, EXPLAIN);
|
|
375
453
|
if (strayExplain !== -1) {
|
|
@@ -390,7 +468,18 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
|
|
|
390
468
|
|
|
391
469
|
const key = match[1].trim();
|
|
392
470
|
const eqRef = eqCtx ? parseEquationRef(key) : undefined;
|
|
393
|
-
|
|
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
|
+
) {
|
|
394
483
|
fail(file, `Invalid notation key "${key}" in [[...]].`, node);
|
|
395
484
|
}
|
|
396
485
|
|
|
@@ -421,9 +510,46 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
|
|
|
421
510
|
continue;
|
|
422
511
|
}
|
|
423
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
|
+
|
|
424
546
|
const definition = resolve(key, 'prose', node);
|
|
425
|
-
|
|
426
|
-
|
|
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;
|
|
427
553
|
const href = defaultHref(definition, key, base);
|
|
428
554
|
|
|
429
555
|
const properties = {
|
|
@@ -552,7 +678,16 @@ function inspectExplainCalls(value, resolve, file, node, references) {
|
|
|
552
678
|
* unknown?: 'error' | 'warn' | 'ignore',
|
|
553
679
|
* base?: string,
|
|
554
680
|
* equations?: Record<string, Record<string, string>>,
|
|
681
|
+
* tables?: Record<string, Record<string, string>>,
|
|
682
|
+
* figures?: Record<string, Record<string, string>>,
|
|
683
|
+
* chapters?: Record<string, number>,
|
|
555
684
|
* }} [options]
|
|
685
|
+
*
|
|
686
|
+
* `chapters` maps a lesson slug to its 1-based course ordinal. When the
|
|
687
|
+
* current page has one, every display equation / `<Table>` / `<Figure>` is
|
|
688
|
+
* numbered `‹chapter›.‹n›` (`##` headings ignored); with no entry the legacy
|
|
689
|
+
* `##`-section scheme applies. It must agree with `manifest.lessons[].chapter`,
|
|
690
|
+
* which is what feeds `equations`/`tables`/`figures` above.
|
|
556
691
|
*/
|
|
557
692
|
export default function remarkNotation(options = {}) {
|
|
558
693
|
const unknown = options.unknown ?? 'error';
|
|
@@ -568,6 +703,10 @@ export default function remarkNotation(options = {}) {
|
|
|
568
703
|
);
|
|
569
704
|
const localDefinitions = localDefinitionsFromFile(file);
|
|
570
705
|
const lessonId = lessonIdFrom(file);
|
|
706
|
+
// The lesson's course ordinal (see the `chapters` option). Falsy / missing
|
|
707
|
+
// ⇒ the legacy `##`-section numbering, so `0` and `undefined` behave alike.
|
|
708
|
+
const chapter =
|
|
709
|
+
(lessonId && options.chapters?.[lessonId.replace(/\./g, '/')]) || undefined;
|
|
571
710
|
const registryFile = isNotationRegistryFile(file);
|
|
572
711
|
const mathScope = pageGlyphScope(
|
|
573
712
|
localDefinitions,
|
|
@@ -577,10 +716,10 @@ export default function remarkNotation(options = {}) {
|
|
|
577
716
|
const references = [];
|
|
578
717
|
const resolved = new Map();
|
|
579
718
|
|
|
580
|
-
// Equation identity (D7): assign every `\label{eq:…}` its
|
|
581
|
-
//
|
|
582
|
-
// the
|
|
583
|
-
const equationLabels = collectEquationLabels(tree);
|
|
719
|
+
// Equation identity (D7): assign every `\label{eq:…}` its number before any
|
|
720
|
+
// math node is rewritten, so a `[[eq-key]]` earlier in the page still
|
|
721
|
+
// resolves. `chapter` picks the `‹chapter›.‹n›` vs `##`-section scheme.
|
|
722
|
+
const equationLabels = collectEquationLabels(tree, chapter);
|
|
584
723
|
const pageLabels = new Map();
|
|
585
724
|
for (const label of equationLabels) {
|
|
586
725
|
if (pageLabels.has(label.key)) {
|
|
@@ -598,6 +737,53 @@ export default function remarkNotation(options = {}) {
|
|
|
598
737
|
crossPage: options.equations ?? {},
|
|
599
738
|
};
|
|
600
739
|
|
|
740
|
+
// Number and anchor every display equation on a lesson page, keyed or
|
|
741
|
+
// not (F3). Non-lesson pages (notation registry, stray docs) keep the
|
|
742
|
+
// keyed-only behaviour so their body math is left untouched.
|
|
743
|
+
const displayEquations = lessonId
|
|
744
|
+
? collectDisplayEquations(tree, chapter)
|
|
745
|
+
: [];
|
|
746
|
+
const equationByNode = new Map(
|
|
747
|
+
displayEquations.map((equation) => [equation.node, equation]),
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
// Keyed floats (F-tables-figures): every `<Table>`/`<Figure>` on a lesson
|
|
751
|
+
// page is numbered and anchored, keyed or not — same rule as equations,
|
|
752
|
+
// via the shared engine in `keyed-jsx-floats.mjs`.
|
|
753
|
+
const makeFloatCtx = (tagName, crossPage) => {
|
|
754
|
+
const floats = lessonId
|
|
755
|
+
? collectKeyedJsxFloats(tree, { tagName, chapter })
|
|
756
|
+
: [];
|
|
757
|
+
const pageFloatLabels = new Map();
|
|
758
|
+
for (const float of floats) {
|
|
759
|
+
if (float.key == null) continue;
|
|
760
|
+
if (pageFloatLabels.has(float.key)) {
|
|
761
|
+
fail(
|
|
762
|
+
file,
|
|
763
|
+
`Duplicate <${tagName} id="${float.key}"> on this page; an id is unique per lesson.`,
|
|
764
|
+
float.node,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
pageFloatLabels.set(float.key, float.number);
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
ctx: {
|
|
771
|
+
base: options.base ?? '',
|
|
772
|
+
pageLabels: pageFloatLabels,
|
|
773
|
+
crossPage: crossPage ?? {},
|
|
774
|
+
},
|
|
775
|
+
byNode: new Map(floats.map((float) => [float.node, float])),
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
const { ctx: tableCtx, byNode: tableByNode } = makeFloatCtx(
|
|
779
|
+
'Table',
|
|
780
|
+
options.tables,
|
|
781
|
+
);
|
|
782
|
+
const { ctx: figureCtx, byNode: figureByNode } = makeFloatCtx(
|
|
783
|
+
'Figure',
|
|
784
|
+
options.figures,
|
|
785
|
+
);
|
|
786
|
+
|
|
601
787
|
const resolve = (key, kind, node) => {
|
|
602
788
|
const context = {
|
|
603
789
|
file,
|
|
@@ -647,7 +833,6 @@ export default function remarkNotation(options = {}) {
|
|
|
647
833
|
node,
|
|
648
834
|
);
|
|
649
835
|
}
|
|
650
|
-
node.__eqKey = labelMatch[1];
|
|
651
836
|
value = stripEquationLabels(value);
|
|
652
837
|
replaceMathSource(node, value);
|
|
653
838
|
}
|
|
@@ -717,6 +902,8 @@ export default function remarkNotation(options = {}) {
|
|
|
717
902
|
child,
|
|
718
903
|
references,
|
|
719
904
|
eqCtx,
|
|
905
|
+
tableCtx,
|
|
906
|
+
figureCtx,
|
|
720
907
|
);
|
|
721
908
|
if (replacement) {
|
|
722
909
|
node.children.splice(index, 1, ...replacement);
|
|
@@ -725,14 +912,12 @@ export default function remarkNotation(options = {}) {
|
|
|
725
912
|
continue;
|
|
726
913
|
}
|
|
727
914
|
visit(child, false);
|
|
728
|
-
if (child && child.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
pageLabels.get(key),
|
|
735
|
-
);
|
|
915
|
+
if (child && child.type === 'math' && equationByNode.has(child)) {
|
|
916
|
+
node.children[index] = equationNode(child, equationByNode.get(child));
|
|
917
|
+
} else if (child?.type === 'mdxJsxFlowElement' && tableByNode.has(child)) {
|
|
918
|
+
injectFloatNumberAttributes(child, tableByNode.get(child), 'tab');
|
|
919
|
+
} else if (child?.type === 'mdxJsxFlowElement' && figureByNode.has(child)) {
|
|
920
|
+
injectFloatNumberAttributes(child, figureByNode.get(child), 'fig');
|
|
736
921
|
}
|
|
737
922
|
}
|
|
738
923
|
};
|
|
@@ -749,5 +934,16 @@ export default function remarkNotation(options = {}) {
|
|
|
749
934
|
indexInSection,
|
|
750
935
|
}),
|
|
751
936
|
);
|
|
937
|
+
const floatLabelData = (byNode) =>
|
|
938
|
+
[...byNode.values()]
|
|
939
|
+
.filter((float) => float.key != null)
|
|
940
|
+
.map(({ key, number, section, indexInSection }) => ({
|
|
941
|
+
key,
|
|
942
|
+
number,
|
|
943
|
+
section,
|
|
944
|
+
indexInSection,
|
|
945
|
+
}));
|
|
946
|
+
file.data.tableLabels = floatLabelData(tableByNode);
|
|
947
|
+
file.data.figureLabels = floatLabelData(figureByNode);
|
|
752
948
|
};
|
|
753
949
|
}
|
|
@@ -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 };
|
|
@@ -15,6 +15,19 @@ type NotationAlignment = {
|
|
|
15
15
|
readonly rationale: string;
|
|
16
16
|
};
|
|
17
17
|
type NotationReferenceKind = 'prose' | 'math' | 'definition';
|
|
18
|
+
/**
|
|
19
|
+
* A gloss resolved by the loader (D15). Declared on a card, keyed
|
|
20
|
+
* `<card key>.<slug(name)>`, and deliberately absent from the registry: a
|
|
21
|
+
* gloss is never a definition record, never a reference target, and never a
|
|
22
|
+
* curriculum node. See `reference/gloss.ts`.
|
|
23
|
+
*/
|
|
24
|
+
interface NotationGlossEntry {
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly notation: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly units?: string;
|
|
29
|
+
readonly ownerKey: string;
|
|
30
|
+
}
|
|
18
31
|
interface NotationReferenceInput {
|
|
19
32
|
readonly key: string;
|
|
20
33
|
readonly kind: NotationReferenceKind;
|
|
@@ -37,6 +50,8 @@ interface SharedNotationDefinitionInput {
|
|
|
37
50
|
readonly body: string;
|
|
38
51
|
/** References harvested from the Markdown body. */
|
|
39
52
|
readonly references: readonly NotationReferenceInput[];
|
|
53
|
+
/** Symbols this entry's `formula` names but does not make cards of. */
|
|
54
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
40
55
|
readonly source: SourceSpan;
|
|
41
56
|
}
|
|
42
57
|
interface LocalNotationDefinitionInput {
|
|
@@ -51,6 +66,8 @@ interface LocalNotationDefinitionInput {
|
|
|
51
66
|
readonly alignment: NotationAlignment;
|
|
52
67
|
/** References harvested from summary content. */
|
|
53
68
|
readonly references: readonly NotationReferenceInput[];
|
|
69
|
+
/** Symbols this entry's `formula` names but does not make cards of. */
|
|
70
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
54
71
|
readonly source: SourceSpan;
|
|
55
72
|
}
|
|
56
73
|
interface NotationLessonInput {
|
|
@@ -67,7 +84,7 @@ interface NotationRegistryInput {
|
|
|
67
84
|
readonly sharedDefinitions: readonly SharedNotationDefinitionInput[];
|
|
68
85
|
readonly lessons: readonly NotationLessonInput[];
|
|
69
86
|
}
|
|
70
|
-
type NotationDiagnosticCode = 'invalid-key' | 'duplicate-page' | 'duplicate-definition' | 'conflicting-definition' | 'undefined-reference' | 'reference-cycle' | 'unused-definition' | 'alignment-unknown-competency' | 'alignment-unknown-lesson' | 'alignment-introduction' | 'alignment-unavailable' | 'alignment-review-state';
|
|
87
|
+
type NotationDiagnosticCode = 'invalid-key' | 'duplicate-page' | 'duplicate-definition' | 'conflicting-definition' | 'undefined-reference' | 'reference-cycle' | 'unused-definition' | 'alignment-unknown-competency' | 'alignment-unknown-lesson' | 'alignment-introduction' | 'alignment-unavailable' | 'alignment-review-state' | 'gloss-collides-with-card';
|
|
71
88
|
type DiagnosticSeverity = 'error' | 'warning';
|
|
72
89
|
interface NotationDiagnostic {
|
|
73
90
|
readonly code: NotationDiagnosticCode;
|
|
@@ -95,6 +112,12 @@ interface ResolvedNotationReference {
|
|
|
95
112
|
interface NotationDefinitionRecordBase {
|
|
96
113
|
readonly id: string;
|
|
97
114
|
readonly key: string;
|
|
115
|
+
/**
|
|
116
|
+
* Symbols this entry's `formula` names but does not make cards of (D15).
|
|
117
|
+
* Carried on the record so the manifest can build the entry-scoped formula
|
|
118
|
+
* table; deliberately **not** a definition, a reference target, or an edge.
|
|
119
|
+
*/
|
|
120
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
98
121
|
readonly scope: NotationDefinitionScope;
|
|
99
122
|
readonly notation: string;
|
|
100
123
|
readonly label: string;
|
|
@@ -148,4 +171,4 @@ interface NotationRegistry {
|
|
|
148
171
|
readonly diagnostics: readonly NotationDiagnostic[];
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
export type { DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
|
|
174
|
+
export type { DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
|
|
@@ -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).
|