explico 0.7.0 → 0.11.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.
Files changed (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +14 -1
  15. package/dist/compiler/logging.js +6 -2
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Rule N17's engine half: make `\left`/`\right` grow strictly outward.
3
+ *
4
+ * Importing this module for its side effect installs a KaTeX whose
5
+ * `\delimitershortfall` is `-1pt` instead of the stock `5pt`, so a scalable
6
+ * delimiter pair is always *strictly* taller than what it wraps and nesting
7
+ * depth shows up on its own. See `docs/katex-bracket-scaling.md` for the
8
+ * derivation and `docs/authoring/notation.md#n17` for the authoring rule.
9
+ *
10
+ * ## Why this is not a `patches/` file
11
+ *
12
+ * `delimiterExtend` is a function-local `const` inside `makeLeftRightDelim`.
13
+ * Nothing KaTeX exports reaches it — `__defineMacro`, `__defineFunction`,
14
+ * `__defineSymbol`, `__setFontMetrics` and the settings schema all sit above
15
+ * the layout engine — so the constant can only be changed in the module's own
16
+ * source. A `pnpm patch` does that, but a patch is project-local: it cannot
17
+ * travel inside the published `explico` artifact, so every downstream course
18
+ * would have to reapply it or silently render flat nests.
19
+ *
20
+ * So the engine does it in code instead. It reads KaTeX's own ESM bundle,
21
+ * rewrites that one constant, imports the result as a module, and moves the
22
+ * rendering entry points onto the shared `katex` object every consumer in the
23
+ * process already imported. KaTeX's default export is a plain mutable object
24
+ * and `rehype-katex` reads `katex.renderToString` at call time, so the swap
25
+ * reaches lesson-body math without forking the plugin.
26
+ *
27
+ * ## Why this is safe to rely on
28
+ *
29
+ * The assumptions above (a matching constant, a mutable export, a bundle with
30
+ * no imports of its own) are all checked here, and `assertDelimiterGrowth()`
31
+ * renders a known nest afterwards and throws when the sizes do not actually
32
+ * increase. A course therefore either renders with engine-sized delimiters or
33
+ * fails its build saying so — it cannot quietly get the stock behaviour.
34
+ */
35
+ import katex from 'katex';
36
+
37
+ /** TeX's `\delimitershortfall`, in points, as KaTeX hard-codes it. */
38
+ const STOCK_SHORTFALL = '5.0';
39
+ /** The negative shortfall that forces strict outward growth. */
40
+ const GROWN_SHORTFALL = '-1.0';
41
+
42
+ const needle = (points) =>
43
+ `var delimiterExtend = ${points} / options.fontMetrics().ptPerEm;`;
44
+
45
+ /** The entry points that build a delimiter and must come from the rewrite. */
46
+ const RENDER_EXPORTS = [
47
+ 'render',
48
+ 'renderToString',
49
+ '__renderToDomTree',
50
+ '__renderToHTMLTree',
51
+ ];
52
+
53
+ /**
54
+ * Exported for the test that pins both failure messages — a KaTeX upgrade that
55
+ * moves the constant, and a leftover `patches/` file, are the two ways this
56
+ * stops being able to install itself.
57
+ */
58
+ export function rewriteBundle(source) {
59
+ const stock = needle(STOCK_SHORTFALL);
60
+ if (!source.includes(stock)) {
61
+ // Either the constant moved or someone already rewrote it. Both mean the
62
+ // assumption this module rests on no longer holds for this KaTeX build.
63
+ const alreadyGrown = source.includes(needle(GROWN_SHORTFALL));
64
+ throw new Error(
65
+ alreadyGrown
66
+ ? `explico: katex ${katex.version} already carries the -1pt delimiter ` +
67
+ 'shortfall (a patches/ file?). Remove that patch — the engine ' +
68
+ 'installs it in code (src/reference/katex-delimiters.mjs).'
69
+ : `explico: cannot install the N17 delimiter sizing into katex ` +
70
+ `${katex.version}: the \\delimitershortfall constant ` +
71
+ `(${stock}) is not in its ESM bundle. Re-derive the one-line ` +
72
+ 'change against the new bundle — see docs/katex-bracket-scaling.md.',
73
+ );
74
+ }
75
+ return source.replace(stock, needle(GROWN_SHORTFALL));
76
+ }
77
+
78
+ async function loadGrownKatex() {
79
+ // Imported here, not at the top: should a browser graph ever reach this
80
+ // module (a lab island once did, through `render-lab-math.ts`; islands now
81
+ // import `lab-math-slot.ts`), Vite's `node:fs` stub throws on first access —
82
+ // which, for a static named import, is module evaluation itself, so the
83
+ // island would never hydrate.
84
+ const { readFileSync } = await import('node:fs');
85
+ const { createRequire } = await import('node:module');
86
+ const require = createRequire(import.meta.url);
87
+ const source = readFileSync(require.resolve('katex/dist/katex.mjs'), 'utf8');
88
+ // The bundle is self-contained, so it can be re-imported from a data URL
89
+ // with no base to resolve relative specifiers against. The URL is built at
90
+ // run time on purpose, so Vite has nothing to analyze here.
91
+ const rewritten = rewriteBundle(source);
92
+ const module = await import(
93
+ /* @vite-ignore */
94
+ `data:text/javascript;base64,${Buffer.from(rewritten).toString('base64')}`
95
+ );
96
+ return module.default;
97
+ }
98
+
99
+ /**
100
+ * Render a known nest and confirm the outer pair really is larger than the
101
+ * inner one. Cheap, and it turns every way this could go wrong — an upstream
102
+ * refactor, a frozen export, an import that beat us to it — into one loud
103
+ * failure at the point of installation.
104
+ */
105
+ export function assertDelimiterGrowth(instance = katex) {
106
+ const html = instance.renderToString(
107
+ String.raw`\left[1+\left(\frac{a}{b}\right)\right]`,
108
+ { displayMode: true, output: 'html' },
109
+ );
110
+ const sizes = [...html.matchAll(/delimsizing size(\d)/g)].map((m) =>
111
+ Number(m[1]),
112
+ );
113
+ // Two pairs, so four glyphs: outer, inner, inner, outer.
114
+ const grew = sizes.length === 4 && sizes[0] > sizes[1];
115
+ if (!grew) {
116
+ throw new Error(
117
+ 'explico: the N17 delimiter sizing did not take effect — a nested ' +
118
+ `\\left…\\right pair rendered as [${sizes.join(', ')}] instead of ` +
119
+ 'growing outward. Something rendered math before ' +
120
+ "'explico/reference/katex-delimiters.mjs' was imported, or katex " +
121
+ 'changed shape. See docs/katex-bracket-scaling.md.',
122
+ );
123
+ }
124
+ }
125
+
126
+ // Build and server rendering only. In a browser there is no KaTeX bundle on
127
+ // disk to rewrite. No client graph should include this module (lab math is
128
+ // rendered on the server); if one does, a client-side render falls back to
129
+ // stock sizing rather than breaking the island.
130
+ if (typeof window === 'undefined') {
131
+ const grown = await loadGrownKatex();
132
+ for (const name of RENDER_EXPORTS) {
133
+ if (typeof grown[name] === 'function') katex[name] = grown[name];
134
+ }
135
+ assertDelimiterGrowth();
136
+ }
137
+
138
+ export default katex;
@@ -1,3 +1,7 @@
1
+ // Side-effect import: installs rule N17's delimiter sizing onto the shared
2
+ // `katex` module before anything renders. A course wires this file into its
3
+ // `astro.config.mjs`, so `rehype-katex` inherits it without a patch.
4
+ import './katex-delimiters.mjs';
1
5
  import { NOTATION_KEY_PATTERN } from './remark-notation.mjs';
2
6
 
3
7
  export const EXPLAIN_MACRO = String.raw`\htmlData{notation-key=#1}{#2}`;
@@ -8,11 +8,18 @@
8
8
  * - `<prefix>-ref-resolves` (error): every `[[<prefix>-key]]` /
9
9
  * `[[slug#<prefix>-key]]` points at the matching keyed float with that `id`.
10
10
  * - `<prefix>-key-unused` (info): a keyed float is never referenced.
11
+ * - `<prefix>-removed-prop` (error): the tag sets a prop that no longer
12
+ * exists. Mostly a name prop (`<Figure title>`, `<Diagram label>`) — every
13
+ * float is named by the one caption pair, `caption` (long) and `short`
14
+ * (optional), and a removed prop would otherwise render nothing and escape
15
+ * rule S1 — and a diagram's compaction props (`defaultOpen`,
16
+ * `rememberState`), since a diagram no longer folds by itself.
11
17
  *
12
- * `tables-validate.ts`, `figures-validate.ts`, and `diagrams-validate.ts` are
13
- * thin wrappers over this so callers import a kind-specific validator with
14
- * narrow diagnostic-code types, while `equations-validate.ts` stays untouched
15
- * — equations predate this and keep their own dedicated path.
18
+ * `tables-validate.ts`, `figures-validate.ts`, `diagrams-validate.ts` and
19
+ * `examples-validate.ts` (`<CompactExamples>`, which adds its required-prop
20
+ * check) are thin wrappers over this so callers import a kind-specific
21
+ * validator with narrow diagnostic-code types, while `equations-validate.ts`
22
+ * stays untouched — equations predate this and keep their own dedicated path.
16
23
  */
17
24
  interface KeyedFloatKind {
18
25
  /** The MDX component tag, e.g. `Table` or `Figure`. */
@@ -21,10 +28,19 @@ interface KeyedFloatKind {
21
28
  readonly prefix: string;
22
29
  /** Lowercase noun used in diagnostic messages, e.g. `table` or `figure`. */
23
30
  readonly noun: string;
31
+ /**
32
+ * Name props this kind no longer takes, each with the fix its diagnostic
33
+ * gives, e.g. `{ title: 'use \`caption\` …' }`. Omitted: none.
34
+ */
35
+ readonly removedProps?: Readonly<Record<string, string>>;
24
36
  }
25
37
  interface KeyedFloatLabelRecord {
26
38
  readonly key: string;
27
39
  readonly number: string;
40
+ /** The float's literal `short` caption, when it has one. */
41
+ readonly short?: string;
42
+ /** The float's literal `caption`; absent when it is an expression. */
43
+ readonly caption?: string;
28
44
  readonly lessonId: string;
29
45
  readonly slug: string;
30
46
  readonly section: number;
@@ -57,14 +73,15 @@ interface KeyedFloatLessonInput {
57
73
  readonly file: string;
58
74
  readonly body: string;
59
75
  /**
60
- * `{ prefix, offset }` — the part's 1-based ordinal among the sidebar groups,
61
- * and how many floats of this kind earlier lessons of that part already used.
62
- * When set, floats are numbered `‹part›.‹offset + n›`; when omitted, the
63
- * legacy `##`-section scheme. Each kind counts on its own, so the caller
64
- * passes a different `offset` for `<Table>` than for `<Figure>`.
76
+ * `{ prefix, offset }` — the lesson's float-number prefix (`‹part›.‹chapter›`,
77
+ * or `‹part›` under `numbering.floats: "part"`) and how many floats of this
78
+ * kind precede the lesson in that scope. When set, floats are numbered
79
+ * `‹prefix›.‹offset + n›`; when omitted, the legacy `##`-section scheme.
80
+ * Each kind counts on its own, so the caller passes a different `offset`
81
+ * for `<Table>` than for `<Figure>`.
65
82
  */
66
83
  readonly numbering?: {
67
- readonly prefix: number;
84
+ readonly prefix: number | string;
68
85
  readonly offset: number;
69
86
  };
70
87
  }
@@ -21,7 +21,7 @@ function bodyFloatRefs(lesson, prefix) {
21
21
  return refs;
22
22
  }
23
23
  function validateKeyedFloats(lessons, kind) {
24
- const { tagName, prefix, noun } = kind;
24
+ const { tagName, prefix, noun, removedProps = {} } = kind;
25
25
  const labels = [];
26
26
  const diagnostics = [];
27
27
  const numbersBySlug = {};
@@ -34,6 +34,18 @@ function validateKeyedFloats(lessons, kind) {
34
34
  tagName,
35
35
  numbering: lesson.numbering
36
36
  })) {
37
+ for (const prop of raw.props) {
38
+ const fix = removedProps[prop];
39
+ if (fix === void 0) continue;
40
+ diagnostics.push({
41
+ code: `${prefix}-removed-prop`,
42
+ severity: "error",
43
+ message: `${lesson.lessonId} has a <${tagName} ${prop}="\u2026">${raw.key ? ` (id="${raw.key}")` : ""}, but <${tagName}> has no \`${prop}\` prop: ${fix}`,
44
+ file: lesson.file,
45
+ lessonId: lesson.lessonId,
46
+ ...raw.key ? { key: raw.key } : {}
47
+ });
48
+ }
37
49
  if (raw.key == null) continue;
38
50
  if (!KEYED_FLOAT_KEY_PATTERN.test(raw.key)) {
39
51
  diagnostics.push({
@@ -62,6 +74,8 @@ function validateKeyedFloats(lessons, kind) {
62
74
  labels.push({
63
75
  key: raw.key,
64
76
  number: raw.number,
77
+ ...raw.short ? { short: raw.short } : {},
78
+ ...raw.caption ? { caption: raw.caption } : {},
65
79
  lessonId: lesson.lessonId,
66
80
  slug: lesson.slug,
67
81
  section: raw.section,
@@ -19,10 +19,13 @@ export function parseKeyedFloatRef(raw: any, { prefix }: {
19
19
  } | undefined;
20
20
  /**
21
21
  * The raw-text equivalent of `collectKeyedJsxFloats`, mirroring
22
- * `scanEquationLabels`: every top-level `<TagName …>` in appearance order.
23
- * With `numbering` (`{ prefix, offset }` — the part's ordinal and the count of
24
- * this kind of float in earlier lessons of that part) the number is
25
- * `‹part›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
22
+ * `scanEquationLabels`: every top-level `<TagName …>` in appearance order,
23
+ * with its literal `short` / `caption` (the caption pair every float takes;
24
+ * `null` when absent or an expression) and `props`, the names of every prop
25
+ * the tag sets, for `keyed-float-validate.ts`'s removed-prop check.
26
+ * With `numbering` (`{ prefix, offset }` — the lesson's float-number prefix
27
+ * and the count of this kind of float before it in that scope) the number is
28
+ * `‹prefix›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
26
29
  * headings advance a section counter. Other JSX components' bodies are blanked
27
30
  * first so a `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like
28
31
  * text inside an unrelated component) is never counted.
@@ -33,6 +36,9 @@ export function scanKeyedJsxFloats(markdown: any, { tagName, numbering }: {
33
36
  }): {
34
37
  key: string | null | undefined;
35
38
  number: string;
39
+ short: string | null | undefined;
40
+ caption: string | null | undefined;
41
+ props: any[] | undefined;
36
42
  section: number;
37
43
  indexInSection: number;
38
44
  }[];
@@ -45,7 +51,7 @@ export function countKeyedJsxFloats(markdown: any, { tagName }: {
45
51
  }): number;
46
52
  /**
47
53
  * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
48
- * children (and through `<Memo>` bodies) in document order, advancing the
54
+ * children (and through the `TRANSPARENT_COMPONENTS`' bodies) in document order, advancing the
49
55
  * section counter on every top-level depth-2 heading and emitting a record for
50
56
  * every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
51
57
  * `collectDisplayEquations`. `numbering` is threaded the same way as in
@@ -11,8 +11,9 @@
11
11
  * `#eq-key` — `keyedFloatAnchorId` prepends `tab-`/`fig-`/`dia-`.
12
12
  *
13
13
  * Only top-level occurrences count, exactly like `collectDisplayEquations`,
14
- * except that `<Memo>` is transparent: a float in a memo remains a side-note
15
- * in the lesson's reading order. A keyed float nested inside a
14
+ * except that the `TRANSPARENT_COMPONENTS` (`<Memo>`, `<ShortNote>`,
15
+ * `<Fold>`) are see-through: a float in a memo or a fold stays in the
16
+ * lesson's reading order. A keyed float nested inside a
16
17
  * `<CompactExample>` (or any other component) is illustrative, not part of the
17
18
  * official sequence.
18
19
  *
@@ -26,6 +27,7 @@ import {
26
27
  resolveNumbering,
27
28
  stripFencedCode,
28
29
  stripJsxComponents,
30
+ TRANSPARENT_COMPONENTS,
29
31
  } from './equations.mjs';
30
32
 
31
33
  /** Same shape as `EQ_KEY_PATTERN` — the notation-key grammar without dots. */
@@ -37,10 +39,11 @@ function floatNumber(section, index) {
37
39
  }
38
40
 
39
41
  /**
40
- * The visible number for one float. With a `prefix` (the part's ordinal) the
41
- * scheme is `‹part›.‹offset + indexInLesson›` and `##` headings are ignored;
42
- * without it, the legacy `##`-section scheme. Mirrors `displayEquationNumber`
43
- * in `equations.mjs`. Each kind of float counts on its own — a `<Table>` never
42
+ * The visible number for one float. With a `prefix` (`‹part›.‹chapter›`, or
43
+ * the part alone under `numbering.floats: "part"`) the scheme is
44
+ * `‹prefix›.‹offset + indexInLesson›` and `##` headings are ignored; without
45
+ * it, the legacy `##`-section scheme. Mirrors `displayEquationNumber` in
46
+ * `equations.mjs`. Each kind of float counts on its own — a `<Table>` never
44
47
  * advances the `<Figure>` or `<Diagram>` sequence — so `offset` is per kind too.
45
48
  */
46
49
  function keyedFloatNumber({
@@ -79,22 +82,60 @@ function readIdAttribute(attributesSource) {
79
82
  return match ? match[2] : undefined;
80
83
  }
81
84
 
85
+ /**
86
+ * A literal string attribute (`name="…"` / `name='…'`) of a JSX opening tag's
87
+ * attribute source, trimmed; `null` when absent, empty, or an expression.
88
+ */
89
+ function readStringAttribute(attributesSource, name) {
90
+ const match = new RegExp(
91
+ String.raw`(?:^|\s)${name}\s*=\s*(?:"([^"]*)"|'([^']*)')`,
92
+ ).exec(attributesSource);
93
+ const value = (match?.[1] ?? match?.[2])?.trim();
94
+ return value ? value : null;
95
+ }
96
+
97
+ /** Every prop name a JSX opening tag's attribute source sets (`name=`). */
98
+ function attributeNames(attributesSource) {
99
+ const bare = attributesSource.replace(
100
+ /"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}/g,
101
+ ' ',
102
+ );
103
+ return [...bare.matchAll(/(?:^|\s)([A-Za-z_][\w:.-]*)\s*=/g)].map(
104
+ (match) => match[1],
105
+ );
106
+ }
107
+
108
+ /** A literal string value of an mdast JSX attribute, trimmed, else `null`. */
109
+ function mdxStringAttribute(node, name) {
110
+ const attribute = (node.attributes ?? []).find(
111
+ (candidate) =>
112
+ candidate?.type === 'mdxJsxAttribute' && candidate.name === name,
113
+ );
114
+ const value =
115
+ typeof attribute?.value === 'string' ? attribute.value.trim() : '';
116
+ return value ? value : null;
117
+ }
118
+
82
119
  /**
83
120
  * The raw-text equivalent of `collectKeyedJsxFloats`, mirroring
84
- * `scanEquationLabels`: every top-level `<TagName …>` in appearance order.
85
- * With `numbering` (`{ prefix, offset }` — the part's ordinal and the count of
86
- * this kind of float in earlier lessons of that part) the number is
87
- * `‹part›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
121
+ * `scanEquationLabels`: every top-level `<TagName …>` in appearance order,
122
+ * with its literal `short` / `caption` (the caption pair every float takes;
123
+ * `null` when absent or an expression) and `props`, the names of every prop
124
+ * the tag sets, for `keyed-float-validate.ts`'s removed-prop check.
125
+ * With `numbering` (`{ prefix, offset }` — the lesson's float-number prefix
126
+ * and the count of this kind of float before it in that scope) the number is
127
+ * `‹prefix›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
88
128
  * headings advance a section counter. Other JSX components' bodies are blanked
89
129
  * first so a `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like
90
130
  * text inside an unrelated component) is never counted.
91
131
  */
92
132
  export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
93
133
  const { prefix, offset } = resolveNumbering(numbering);
94
- const text = stripJsxComponents(stripFencedCode(markdown), tagName, [
95
- 'Memo',
96
- 'ShortNote',
97
- ]);
134
+ const text = stripJsxComponents(
135
+ stripFencedCode(markdown),
136
+ tagName,
137
+ TRANSPARENT_COMPONENTS,
138
+ );
98
139
  const tokens = [];
99
140
  const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
100
141
  const opening = new RegExp(`<${tagName}\\b([^>]*)>`, 'g');
@@ -102,8 +143,16 @@ export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
102
143
  tokens.push({ kind: 'section', index: match.index });
103
144
  }
104
145
  for (const match of text.matchAll(opening)) {
105
- const key = readIdAttribute(match[1] ?? '');
106
- tokens.push({ kind: 'float', index: match.index, key: key ?? null });
146
+ const attributes = match[1] ?? '';
147
+ const key = readIdAttribute(attributes);
148
+ tokens.push({
149
+ kind: 'float',
150
+ index: match.index,
151
+ key: key ?? null,
152
+ short: readStringAttribute(attributes, 'short'),
153
+ caption: readStringAttribute(attributes, 'caption'),
154
+ props: attributeNames(attributes),
155
+ });
107
156
  }
108
157
  tokens.sort((a, b) => a.index - b.index);
109
158
 
@@ -128,6 +177,9 @@ export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
128
177
  indexInSection,
129
178
  indexInLesson,
130
179
  }),
180
+ short: token.short,
181
+ caption: token.caption,
182
+ props: token.props,
131
183
  section,
132
184
  indexInSection,
133
185
  });
@@ -145,7 +197,7 @@ export function countKeyedJsxFloats(markdown, { tagName }) {
145
197
 
146
198
  /**
147
199
  * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
148
- * children (and through `<Memo>` bodies) in document order, advancing the
200
+ * children (and through the `TRANSPARENT_COMPONENTS`' bodies) in document order, advancing the
149
201
  * section counter on every top-level depth-2 heading and emitting a record for
150
202
  * every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
151
203
  * `collectDisplayEquations`. `numbering` is threaded the same way as in
@@ -171,6 +223,8 @@ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
171
223
  typeof idAttribute?.value === 'string' ? idAttribute.value : undefined;
172
224
  floats.push({
173
225
  key: key ?? null,
226
+ short: mdxStringAttribute(node, 'short'),
227
+ caption: mdxStringAttribute(node, 'caption'),
174
228
  number: keyedFloatNumber({
175
229
  prefix,
176
230
  offset,
@@ -186,8 +240,7 @@ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
186
240
  }
187
241
  if (
188
242
  node.type === 'mdxJsxFlowElement' &&
189
- node.name !== 'Memo' &&
190
- node.name !== 'ShortNote'
243
+ !TRANSPARENT_COMPONENTS.includes(node.name)
191
244
  ) {
192
245
  return;
193
246
  }
@@ -207,7 +260,7 @@ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
207
260
  collectFromDetail(node);
208
261
  } else if (
209
262
  node.type === 'mdxJsxFlowElement' &&
210
- (node.name === 'Memo' || node.name === 'ShortNote')
263
+ TRANSPARENT_COMPONENTS.includes(node.name)
211
264
  ) {
212
265
  collectFromDetail(node);
213
266
  }
@@ -6,7 +6,7 @@ interface KnowledgeCheckLessonInput {
6
6
  readonly body: string;
7
7
  readonly assessmentIds: readonly string[];
8
8
  readonly numbering?: {
9
- readonly prefix: number;
9
+ readonly prefix: number | string;
10
10
  readonly offset: number;
11
11
  };
12
12
  }
@@ -10,7 +10,9 @@ export function parseKnowledgeCheckRef(raw: any): {
10
10
  export function knowledgeCheckAnchorId(key: any): string;
11
11
  /**
12
12
  * Number an ordered assessment-id list. With manifest placement, the number
13
- * is `part.offset + index`; without it, the list remains usefully page-local.
13
+ * is `‹prefix›.‹offset + index›` — `prefix` is `‹part›.‹chapter›`, or the part
14
+ * alone under `numbering.floats: "part"`; without it, the list remains
15
+ * usefully page-local.
14
16
  */
15
17
  export function collectKnowledgeChecks(assessmentIds: any, numbering: any): any;
16
18
  /**
@@ -28,12 +28,17 @@ export function knowledgeCheckAnchorId(key) {
28
28
 
29
29
  /**
30
30
  * Number an ordered assessment-id list. With manifest placement, the number
31
- * is `part.offset + index`; without it, the list remains usefully page-local.
31
+ * is `‹prefix›.‹offset + index›` — `prefix` is `‹part›.‹chapter›`, or the part
32
+ * alone under `numbering.floats: "part"`; without it, the list remains
33
+ * usefully page-local.
32
34
  */
33
35
  export function collectKnowledgeChecks(assessmentIds, numbering) {
34
- const prefix = Number.isInteger(numbering?.prefix) && numbering.prefix > 0
35
- ? numbering.prefix
36
- : undefined;
36
+ const rawPrefix = numbering?.prefix;
37
+ const prefix =
38
+ (Number.isInteger(rawPrefix) && rawPrefix > 0) ||
39
+ (typeof rawPrefix === 'string' && /^\d+(?:\.\d+)*$/.test(rawPrefix))
40
+ ? rawPrefix
41
+ : undefined;
37
42
  const offset = Number.isInteger(numbering?.offset) && numbering.offset >= 0
38
43
  ? numbering.offset
39
44
  : 0;
@@ -1,4 +1,5 @@
1
1
  import { LabMathScopeDefinition } from './render-lab-math.js';
2
+ import './lab-math-slot.js';
2
3
 
3
4
  interface LessonNotationFrontmatter {
4
5
  readonly local?: ReadonlyArray<{
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The half of lab math an interactive island needs in the browser: the shape
3
+ * of a compiled template and its slot placeholder. It imports nothing, so an
4
+ * island that uses it stays clear of the KaTeX build chain behind
5
+ * `render-lab-math.ts`, which is server-only (`katex-delimiters.mjs` reads
6
+ * KaTeX's bundle from disk).
7
+ */
8
+ interface RenderedLabMath {
9
+ /** KaTeX HTML+MathML with `data-notation-key` and `data-lab-slot` spans. */
10
+ readonly html: string;
11
+ /** Slot names, in template order, the island must supply at runtime. */
12
+ readonly slots: readonly string[];
13
+ /** Notation keys the template resolved to, for diagnostics. */
14
+ readonly keys: readonly string[];
15
+ /**
16
+ * Plain, copyable LaTeX — every explained glyph resolved to its bare
17
+ * symbol, every `\slot{name}{…}` replaced by `slotPlaceholder(name)` — so
18
+ * the island can substitute each slot's *current* live value in and keep
19
+ * `[data-notation-formula]`'s `data-latex` accurate as the reader drags a
20
+ * control, the same "click the equation to copy it" gesture a static
21
+ * lesson or glossary equation gets (`NotationLayer.astro`).
22
+ */
23
+ readonly plainLatexTemplate: string;
24
+ }
25
+ /**
26
+ * A slot's placeholder inside `plainLatexTemplate` — distinct from the
27
+ * numeric sentinel `render-lab-math.ts` uses internally (that one only has to
28
+ * survive a single notation-binding pass at build time; this one is the
29
+ * stable, name-keyed token an island substitutes with a live value at
30
+ * runtime, so it's exported for that purpose).
31
+ */
32
+ declare function slotPlaceholder(name: string): string;
33
+
34
+ export { type RenderedLabMath, slotPlaceholder };
@@ -0,0 +1,6 @@
1
+ function slotPlaceholder(name) {
2
+ return `\u27E6${name}\u27E7`;
3
+ }
4
+ export {
5
+ slotPlaceholder
6
+ };