explico 0.8.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 (230) hide show
  1. package/CHANGELOG.md +1239 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  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 +4 -1
  15. package/dist/compiler/logging.js +4 -1
  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 +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +2 -2
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1096 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1471 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -1,3 +1,4 @@
1
+ import { findSpelledUnits } from "./unit-words.js";
1
2
  const CODE_FENCE = /```[\s\S]*?```/g;
2
3
  const INLINE_CODE = /(^|[^`])`[^`\n]*`/g;
3
4
  const DISPLAY_MATH = /\$\$([\s\S]+?)\$\$/g;
@@ -5,9 +6,12 @@ const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g;
5
6
  const IMPORT_LINE = /^[ \t]*import\b[^\n]*$/gm;
6
7
  const KEY_REFERENCE = /\[\[[^\]\n]*\]\]/g;
7
8
  const JSX_TAG = /<\/?[A-Za-z][^<>]*>/g;
8
- function proseOnly(body) {
9
+ const MDX_COMMENT = /\{\/\*[\s\S]*?\*\/\}/g;
10
+ function proseOnly(body, options = {}) {
9
11
  const blank = (match) => match.replace(/[^\n]/g, " ");
10
- return body.replace(CODE_FENCE, blank).replace(INLINE_CODE, (match, lead) => lead + blank(match.slice(lead.length))).replace(DISPLAY_MATH, blank).replace(INLINE_MATH, blank).replace(IMPORT_LINE, blank).replace(KEY_REFERENCE, blank).replace(JSX_TAG, blank);
12
+ const code = body.replace(MDX_COMMENT, blank).replace(CODE_FENCE, blank).replace(INLINE_CODE, (match, lead) => lead + blank(match.slice(lead.length)));
13
+ const math = options.keepMath ? code : code.replace(DISPLAY_MATH, blank).replace(INLINE_MATH, blank);
14
+ return math.replace(IMPORT_LINE, blank).replace(KEY_REFERENCE, blank).replace(JSX_TAG, blank);
11
15
  }
12
16
  function lineOf(text, index) {
13
17
  return text.slice(0, index).split("\n").length;
@@ -40,6 +44,7 @@ const SLASH_IDIOMS = /* @__PURE__ */ new Set([
40
44
  ]);
41
45
  const SLASH_TOKENS = /\b([A-Za-z]{1,6})\/([A-Za-z]{1,6})\b/g;
42
46
  const FAKE_SCRIPT = /[A-Za-z0-9][_^][A-Za-z0-9{]/g;
47
+ const PROSE_NOTATION_CODE = "prose-notation";
43
48
  function findAll(text, pattern, toFinding) {
44
49
  const out = [];
45
50
  const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
@@ -51,8 +56,17 @@ function findAll(text, pattern, toFinding) {
51
56
  return out;
52
57
  }
53
58
  const FIX = "Wrap it in $\u2026$ (or $$\u2026$$) instead \u2014 see docs/authoring/notation.md#n9.";
54
- function findingsIn(text) {
59
+ function findingsIn(text, withMath, options) {
60
+ const spelled = findSpelledUnits(text, withMath, options.allowSpelledUnits).map(
61
+ (unit) => ({
62
+ index: unit.index,
63
+ token: unit.token,
64
+ code: "spelled-unit",
65
+ message: `Spelled-out unit ${JSON.stringify(unit.token)}: write it as ${unit.suggestion}. A unit that quantifies something is an upright symbol inside $\u2026$, even in flowing prose. Where the sentence defines or names the unit itself, waive the block with {/* explico-allow N9: reason */} \u2014 see docs/authoring/notation.md#n9.`
66
+ })
67
+ );
55
68
  return [
69
+ ...spelled,
56
70
  ...findAll(text, /\\\|/g, (m) => ({
57
71
  index: m.index,
58
72
  token: m[0],
@@ -83,13 +97,15 @@ function findingsIn(text) {
83
97
  }).filter(Boolean)
84
98
  ];
85
99
  }
86
- function gateProseNotation(lessons) {
100
+ function gateProseNotation(lessons, options = {}) {
87
101
  const diagnostics = [];
88
102
  for (const lesson of lessons) {
89
103
  const text = proseOnly(lesson.body);
90
- for (const finding of findingsIn(text)) {
104
+ const withMath = proseOnly(lesson.body, { keepMath: true });
105
+ for (const finding of findingsIn(text, withMath, options)) {
91
106
  diagnostics.push({
92
107
  context: "prose",
108
+ code: finding.code ?? PROSE_NOTATION_CODE,
93
109
  severity: "error",
94
110
  file: lesson.source.file,
95
111
  line: lessonLine(lesson, finding.index),
@@ -100,15 +116,17 @@ function gateProseNotation(lessons) {
100
116
  }
101
117
  return diagnostics;
102
118
  }
103
- function gateProseNotationInAssessments(assessments) {
119
+ function gateProseNotationInAssessments(assessments, options = {}) {
104
120
  const diagnostics = [];
105
121
  for (const assessment of assessments) {
106
122
  for (const item of assessment.items ?? []) {
107
123
  for (const field of [item.prompt, item.explanation]) {
108
124
  if (typeof field !== "string") continue;
109
- for (const finding of findingsIn(proseOnly(field))) {
125
+ const withMath = proseOnly(field, { keepMath: true });
126
+ for (const finding of findingsIn(proseOnly(field), withMath, options)) {
110
127
  diagnostics.push({
111
128
  context: "prose",
129
+ code: finding.code ?? PROSE_NOTATION_CODE,
112
130
  severity: "error",
113
131
  file: `${assessment.id} (${item.id ?? "?"})`,
114
132
  token: finding.token,
@@ -32,8 +32,6 @@
32
32
  * increase. A course therefore either renders with engine-sized delimiters or
33
33
  * fails its build saying so — it cannot quietly get the stock behaviour.
34
34
  */
35
- import { readFileSync } from 'node:fs';
36
- import { createRequire } from 'node:module';
37
35
  import katex from 'katex';
38
36
 
39
37
  /** TeX's `\delimitershortfall`, in points, as KaTeX hard-codes it. */
@@ -78,12 +76,21 @@ export function rewriteBundle(source) {
78
76
  }
79
77
 
80
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');
81
86
  const require = createRequire(import.meta.url);
82
87
  const source = readFileSync(require.resolve('katex/dist/katex.mjs'), 'utf8');
83
88
  // The bundle is self-contained, so it can be re-imported from a data URL
84
- // with no base to resolve relative specifiers against.
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.
85
91
  const rewritten = rewriteBundle(source);
86
92
  const module = await import(
93
+ /* @vite-ignore */
87
94
  `data:text/javascript;base64,${Buffer.from(rewritten).toString('base64')}`
88
95
  );
89
96
  return module.default;
@@ -116,10 +123,16 @@ export function assertDelimiterGrowth(instance = katex) {
116
123
  }
117
124
  }
118
125
 
119
- const grown = await loadGrownKatex();
120
- for (const name of RENDER_EXPORTS) {
121
- if (typeof grown[name] === 'function') katex[name] = grown[name];
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();
122
136
  }
123
- assertDelimiterGrowth();
124
137
 
125
138
  export default katex;
@@ -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
+ };