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
@@ -1,6 +1,16 @@
1
+ import { effectiveAccentAllowlist } from "./base-accents.mjs";
1
2
  import { effectiveOperatorAllowlist } from "./base-operators.mjs";
2
3
  import { effectiveRomanLabelAllowlist } from "./base-roman-labels.mjs";
3
- import { collectOperators, collectRomanLabels } from "./math-glyphs.mjs";
4
+ import { stripEquationLabels } from "./equations.mjs";
5
+ import { pageScope } from "./gate-math.js";
6
+ import {
7
+ collectDecorations,
8
+ collectOperators,
9
+ collectRomanLabels,
10
+ decorationOfEntry,
11
+ operatorEntryLatex,
12
+ resolveMathGlyphs
13
+ } from "./math-glyphs.mjs";
4
14
  import { offVocabularyTokens } from "./units-vocab.js";
5
15
  const CONVENTION_RE = /\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
6
16
  function stripCode(text) {
@@ -10,7 +20,7 @@ function mathSpans(body) {
10
20
  const searchable = stripCode(body);
11
21
  const spans = [];
12
22
  for (const match of searchable.matchAll(/\$\$([\s\S]+?)\$\$/g)) {
13
- spans.push({ latex: match[1] ?? "" });
23
+ spans.push({ latex: stripEquationLabels(match[1] ?? "") });
14
24
  }
15
25
  for (const match of searchable.replace(/\$\$([\s\S]+?)\$\$/g, (block) => block.replace(/[^\n]/g, " ")).matchAll(/(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g)) {
16
26
  spans.push({ latex: match[1] ?? "" });
@@ -335,6 +345,94 @@ function checkRomanLabelExplained(input, out) {
335
345
  }
336
346
  }
337
347
  }
348
+ function sharedScopeByKey(input) {
349
+ return new Map(
350
+ input.notationInput.sharedDefinitions.map((definition) => [
351
+ definition.key,
352
+ { key: definition.key, notation: definition.notation }
353
+ ])
354
+ );
355
+ }
356
+ function checkAccentExplained(input, out) {
357
+ const allowlist = effectiveAccentAllowlist(input.accents);
358
+ const sharedByKey = sharedScopeByKey(input);
359
+ for (const lesson of input.notationInput.lessons) {
360
+ const scope = pageScope(lesson, sharedByKey);
361
+ const suspended = (lesson.suspended ?? []).map((item) => item.operator);
362
+ const reported = /* @__PURE__ */ new Set();
363
+ for (const span of mathSpans(lesson.body)) {
364
+ let decorations;
365
+ try {
366
+ ({ decorations } = resolveMathGlyphs(span.latex, scope, { suspended }));
367
+ } catch {
368
+ continue;
369
+ }
370
+ for (const decoration of decorations) {
371
+ if (decoration.wholeForm !== null || decoration.operator !== null) continue;
372
+ if (suspended.includes(decoration.name)) continue;
373
+ if (allowlist.has(decoration.name)) continue;
374
+ if (reported.has(decoration.name)) continue;
375
+ reported.add(decoration.name);
376
+ if (input.lintIgnore.matches("accent-explained", decoration.name)) {
377
+ continue;
378
+ }
379
+ out.push({
380
+ code: "accent-explained",
381
+ severity: "warning",
382
+ lessonId: lesson.lessonId,
383
+ file: lesson.source.file,
384
+ glyph: decoration.name,
385
+ message: `${lesson.lessonId}: decoration ${JSON.stringify(decoration.name)} (in ${JSON.stringify(decoration.token)}) is an operator that is neither universal for this course nor explained; give it an operator entry (latex ${JSON.stringify(operatorEntryLatex(decoration.name))}) introduced with [[key]] or in notation.local, give the whole form its own entry (latex ${JSON.stringify(decoration.token)}), or add ${JSON.stringify(decoration.name)} to rules.N22.allow`
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ function checkOperatorSuspension(input, out) {
392
+ const allowlist = effectiveAccentAllowlist(input.accents);
393
+ const sharedByKey = sharedScopeByKey(input);
394
+ for (const lesson of input.notationInput.lessons) {
395
+ const suspensions = lesson.suspended ?? [];
396
+ for (const { operator, reason } of suspensions) {
397
+ const code = reason === void 0 ? "operator-suspension-reason" : "operator-suspended";
398
+ if (input.lintIgnore.matches(code, operator)) continue;
399
+ out.push({
400
+ code,
401
+ severity: reason === void 0 ? "warning" : "info",
402
+ lessonId: lesson.lessonId,
403
+ file: lesson.source.file,
404
+ glyph: operator,
405
+ message: reason === void 0 ? `${lesson.lessonId}: notation.suspend suspends ${JSON.stringify(operator)} with no reason; say why it is not an operator on this page (reason: \u2026)` : `${lesson.lessonId}: ${JSON.stringify(operator)} is not an operator on this page (notation.suspend): ${reason}`
406
+ });
407
+ }
408
+ const suspended = new Set(suspensions.map((item) => item.operator));
409
+ const scope = pageScope(lesson, sharedByKey);
410
+ const explained = new Set(
411
+ scope.map((entry) => decorationOfEntry(entry.notation)).filter((name) => name !== void 0)
412
+ );
413
+ for (const entry of scope) {
414
+ if (decorationOfEntry(entry.notation) !== void 0) continue;
415
+ const names = new Set(
416
+ collectDecorations(entry.notation).map((decoration) => decoration.name)
417
+ );
418
+ for (const name of names) {
419
+ if (suspended.has(name)) continue;
420
+ const how = explained.has(name) ? "explained by an operator entry" : allowlist.has(name) ? "universal for this course" : void 0;
421
+ if (how === void 0) continue;
422
+ if (input.lintIgnore.matches("operator-shadowed", entry.key)) continue;
423
+ out.push({
424
+ code: "operator-shadowed",
425
+ severity: "info",
426
+ lessonId: lesson.lessonId,
427
+ file: lesson.source.file,
428
+ key: entry.key,
429
+ glyph: name,
430
+ message: `${lesson.lessonId}: ${entry.key} (${entry.notation}) has its own entry, so its card wins over the ${JSON.stringify(name)} operator (${how}) for that form; if ${JSON.stringify(name)} never means the operator on this page, suspend it (notation.suspend)`
431
+ });
432
+ }
433
+ }
434
+ }
435
+ }
338
436
  const CHECKS = [
339
437
  checkGlyphUniqueness,
340
438
  checkNotationUnits,
@@ -345,7 +443,9 @@ const CHECKS = [
345
443
  checkCardWantsDemoting,
346
444
  checkGlossNameShape,
347
445
  checkOperatorExplained,
348
- checkRomanLabelExplained
446
+ checkRomanLabelExplained,
447
+ checkAccentExplained,
448
+ checkOperatorSuspension
349
449
  ];
350
450
  function runConsistencyChecks(input) {
351
451
  const diagnostics = [];
@@ -366,7 +466,11 @@ function consistencyCounts(diagnostics) {
366
466
  "card-wants-demoting": 0,
367
467
  "gloss-name-shape": 0,
368
468
  "operator-explained": 0,
369
- "roman-label-explained": 0
469
+ "roman-label-explained": 0,
470
+ "accent-explained": 0,
471
+ "operator-suspended": 0,
472
+ "operator-suspension-reason": 0,
473
+ "operator-shadowed": 0
370
474
  };
371
475
  for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
372
476
  return counts;
@@ -1,15 +1,21 @@
1
1
  /**
2
- * Rule N17: nested scalable delimiters must make their hierarchy visible.
2
+ * Rule N17: a nested scalable delimiter is sized by the engine, never by hand.
3
+ *
4
+ * The engine installs a KaTeX whose `\delimitershortfall` is `-1pt`
5
+ * (`katex-delimiters.mjs`), so a `\left`/`\right` pair is always
6
+ * strictly taller than what it wraps. Nesting depth therefore shows up on its
7
+ * own, and a hand-picked `\bigl`…`\Biggr` inside a nest only replaces a size
8
+ * that tracks the content with a fixed glyph that does not.
3
9
  *
4
10
  * This deliberately parses only sizing syntax. It does not decide whether a
5
11
  * parenthesis, bracket, or brace is semantic, and it never recommends changing
6
- * one delimiter shape into another. N16's alternating-shape preference remains
7
- * a review-only authoring concern.
12
+ * one delimiter shape into another — N17's other half, that a delimiter's shape
13
+ * is fixed by its meaning and never alternates to show depth, stays a
14
+ * review-only authoring concern (which is why N17 is `partial`).
8
15
  */
9
- declare const DELIMITER_SIZE_DIAGNOSTIC_CODE: "delimiter-size-hierarchy";
16
+ declare const DELIMITER_SIZE_DIAGNOSTIC_CODE: "delimiter-manual-size";
10
17
  type DelimiterSizeName = 'automatic' | 'big' | 'Big' | 'bigg' | 'Bigg';
11
18
  interface DelimiterSizeSuggestion {
12
- readonly size: Exclude<DelimiterSizeName, 'automatic' | 'big'>;
13
19
  readonly open: string;
14
20
  readonly close: string;
15
21
  readonly latex: string;
@@ -20,15 +26,19 @@ interface DelimiterSizeDiagnostic {
20
26
  readonly offset: number;
21
27
  readonly line: number;
22
28
  readonly column: number;
23
- readonly outerSize: DelimiterSizeName;
24
- readonly nestedSize: DelimiterSizeName;
25
- readonly suggestion?: DelimiterSizeSuggestion;
29
+ /** The hand-picked size that should become `\left`/`\right`. */
30
+ readonly size: Exclude<DelimiterSizeName, 'automatic'>;
31
+ readonly suggestion: DelimiterSizeSuggestion;
26
32
  readonly message: string;
27
33
  }
28
34
  /**
29
- * Warn when a scalable outer pair is not at least one manual size level above
30
- * everything nested inside it. `\left` and `\big` form the baseline level;
31
- * progressively larger levels are `\Big`, `\bigg`, and `\Bigg`.
35
+ * Warn when a pair that takes part in a nest of scalable delimiters is sized
36
+ * by hand instead of by `\left`/`\right`.
37
+ *
38
+ * A pair takes part in a nest when it contains, or sits inside, another pair
39
+ * this linter tracks. A lone hand-sized pair around otherwise bare content —
40
+ * `\bigl(x(t),\ y(t)\bigr)` for a tuple — is a deliberate typographic
41
+ * choice at a single level, has no hierarchy to get wrong, and is left alone.
32
42
  *
33
43
  * Only `()`, `[]`, and `\{\}` are parsed. Bare delimiters are intentionally
34
44
  * ignored: N15 owns that separate error, while ordinary semantic notation
@@ -1,4 +1,4 @@
1
- const DELIMITER_SIZE_DIAGNOSTIC_CODE = "delimiter-size-hierarchy";
1
+ const DELIMITER_SIZE_DIAGNOSTIC_CODE = "delimiter-manual-size";
2
2
  const SIZE_SPECS = [
3
3
  {
4
4
  name: "automatic",
@@ -40,9 +40,6 @@ const COMMAND_SPECS = SIZE_SPECS.flatMap((spec) => [
40
40
  { ...spec, command: spec.openCommand, tokenKind: "open" },
41
41
  { ...spec, command: spec.closeCommand, tokenKind: "close" }
42
42
  ]).sort((left, right) => right.command.length - left.command.length);
43
- const SUGGESTION_BY_RANK = new Map(
44
- SIZE_SPECS.filter((spec) => spec.rank >= 2).map((spec) => [spec.rank, spec])
45
- );
46
43
  function delimiterAt(latex, offset, kind) {
47
44
  const candidates = kind === "open" ? ["\\{", "(", "["] : ["\\}", ")", "]"];
48
45
  for (const delimiter of candidates) {
@@ -120,55 +117,34 @@ function lineAndColumn(source, offset) {
120
117
  column: offset - lastNewline
121
118
  };
122
119
  }
123
- function sizeName(rank) {
124
- return SUGGESTION_BY_RANK.get(rank)?.name ?? "Bigg";
125
- }
126
- function suggestionFor(pair, requiredRank) {
127
- const close = pair.close;
128
- const spec = SUGGESTION_BY_RANK.get(requiredRank);
129
- if (!close || !spec || spec.name === "automatic" || spec.name === "big") {
130
- return void 0;
131
- }
132
- return {
133
- size: spec.name,
134
- open: `${spec.openCommand}${pair.open.delimiter}`,
135
- close: `${spec.closeCommand}${close.delimiter}`,
136
- latex: `${spec.openCommand}${pair.open.delimiter} ... ${spec.closeCommand}${close.delimiter}`
137
- };
120
+ const SPEC_BY_SIZE = new Map(SIZE_SPECS.map((spec) => [spec.name, spec]));
121
+ function suggestionFor(pair) {
122
+ const open = `\\left${pair.open.delimiter}`;
123
+ const close = `\\right${pair.close?.delimiter ?? pair.open.delimiter}`;
124
+ return { open, close, latex: `${open} ... ${close}` };
138
125
  }
139
126
  function lintDelimiterSizes(latex) {
140
127
  const roots = parseDelimiterPairs(latex);
141
128
  if (!roots) return [];
142
129
  const diagnostics = [];
143
- const assess = (pair) => {
144
- let largestNestedRequirement = 0;
145
- let representativeNestedSize = "automatic";
146
- for (const child of pair.children) {
147
- const childRequired = assess(child);
148
- if (childRequired > largestNestedRequirement) {
149
- largestNestedRequirement = childRequired;
150
- representativeNestedSize = sizeName(childRequired);
151
- }
152
- }
153
- const requiredRank = largestNestedRequirement === 0 ? pair.open.rank : largestNestedRequirement + 1;
154
- pair.requiredRank = Math.max(pair.open.rank, requiredRank);
155
- if (largestNestedRequirement > 0 && pair.open.rank < requiredRank) {
130
+ const visit = (pair, nested) => {
131
+ const inNest = nested || pair.children.length > 0;
132
+ if (inNest && pair.open.pairKind === "manual") {
156
133
  const location = lineAndColumn(latex, pair.open.offset);
157
- const suggestion = suggestionFor(pair, requiredRank);
134
+ const suggestion = suggestionFor(pair);
158
135
  diagnostics.push({
159
136
  code: DELIMITER_SIZE_DIAGNOSTIC_CODE,
160
137
  severity: "warning",
161
138
  offset: pair.open.offset,
162
139
  ...location,
163
- outerSize: pair.open.size,
164
- nestedSize: representativeNestedSize,
165
- ...suggestion ? { suggestion } : {},
166
- message: suggestion ? `Outer delimiters are not larger than their nested pair. Use ${suggestion.latex}; preserve the original opening and closing delimiter shapes (rule N17).` : `Outer delimiters are not larger than their nested pair, and no larger standard manual delimiter size remains. Preserve the original opening and closing shapes and restructure the expression if necessary (rule N17).`
140
+ size: pair.open.size,
141
+ suggestion,
142
+ message: `A nested delimiter pair is sized by hand with ${SPEC_BY_SIZE.get(pair.open.size)?.openCommand ?? pair.open.size}${pair.open.delimiter}. Use ${suggestion.latex} and let the engine size the nest; preserve the original opening and closing delimiter shapes (rule N17).`
167
143
  });
168
144
  }
169
- return pair.requiredRank;
145
+ for (const child of pair.children) visit(child, true);
170
146
  };
171
- for (const root of roots) assess(root);
147
+ for (const root of roots) visit(root, false);
172
148
  return diagnostics.sort((left, right) => left.offset - right.offset);
173
149
  }
174
150
  export {
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
2
2
 
3
3
  /** `validateKeyedFloats` for `<Diagram>` — see `keyed-float-validate.ts`. */
4
4
 
5
- type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused';
5
+ type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused' | 'dia-removed-prop';
6
6
  interface DiagramDiagnostic {
7
7
  readonly code: DiagramDiagnosticCode;
8
8
  readonly severity: 'error' | 'warning' | 'info';
@@ -16,6 +16,11 @@ type DiagramLessonInput = KeyedFloatLessonInput;
16
16
  interface DiagramValidation extends KeyedFloatValidation {
17
17
  readonly diagnostics: readonly DiagramDiagnostic[];
18
18
  }
19
+ /**
20
+ * The fix for a compaction prop: a diagram is always its graph and then its
21
+ * caption line (docs/plans/figures-and-short-captions.md, decision 7).
22
+ */
23
+ declare const DIAGRAM_NO_LONGER_FOLDS = "a diagram no longer folds by itself \u2014 delete the prop. To let a reader fold it away, wrap it in a `<Fold>` or put it in a collapsible section.";
19
24
  declare function validateDiagrams(lessons: readonly DiagramLessonInput[]): DiagramValidation;
20
25
 
21
- export { type DiagramDiagnostic, type DiagramDiagnosticCode, type DiagramLabelRecord, type DiagramLessonInput, type DiagramValidation, validateDiagrams };
26
+ export { DIAGRAM_NO_LONGER_FOLDS, type DiagramDiagnostic, type DiagramDiagnosticCode, type DiagramLabelRecord, type DiagramLessonInput, type DiagramValidation, validateDiagrams };
@@ -1,13 +1,21 @@
1
1
  import {
2
2
  validateKeyedFloats
3
3
  } from "./keyed-float-validate.js";
4
+ const DIAGRAM_NO_LONGER_FOLDS = "a diagram no longer folds by itself \u2014 delete the prop. To let a reader fold it away, wrap it in a `<Fold>` or put it in a collapsible section.";
4
5
  function validateDiagrams(lessons) {
5
6
  return validateKeyedFloats(lessons, {
6
7
  tagName: "Diagram",
7
8
  prefix: "dia",
8
- noun: "diagram"
9
+ noun: "diagram",
10
+ removedProps: {
11
+ label: "rename it `caption` (shown below the diagram with its number), and add an optional `short` caption if the caption is long.",
12
+ title: "a diagram is named by its `caption` (shown below it with its number) and an optional `short` caption.",
13
+ defaultOpen: DIAGRAM_NO_LONGER_FOLDS,
14
+ rememberState: DIAGRAM_NO_LONGER_FOLDS
15
+ }
9
16
  });
10
17
  }
11
18
  export {
19
+ DIAGRAM_NO_LONGER_FOLDS,
12
20
  validateDiagrams
13
21
  };
@@ -12,6 +12,8 @@
12
12
  interface EquationLabelRecord {
13
13
  readonly key: string;
14
14
  readonly number: string;
15
+ /** The equation's `\caption{…}` short caption, when it has one. */
16
+ readonly short?: string;
15
17
  readonly lessonId: string;
16
18
  readonly slug: string;
17
19
  readonly section: number;
@@ -45,13 +47,14 @@ interface EquationLessonInput {
45
47
  readonly file: string;
46
48
  readonly body: string;
47
49
  /**
48
- * `{ prefix, offset }` — the part's 1-based ordinal among the sidebar groups,
49
- * and how many display equations earlier lessons of that part already used.
50
- * When set, equations are numbered `‹part›.‹offset + n›`; when omitted, the
51
- * legacy `##`-section scheme. See `docs/authoring/structure.md`.
50
+ * `{ prefix, offset }` — the lesson's float-number prefix (`‹part›.‹chapter›`,
51
+ * or `‹part›` under `numbering.floats: "part"`) and how many display
52
+ * equations precede the lesson in that scope. When set, equations are
53
+ * numbered `‹prefix›.‹offset + n›`; when omitted, the legacy `##`-section
54
+ * scheme. See `docs/authoring/structure.md`.
52
55
  */
53
56
  readonly numbering?: {
54
- readonly prefix: number;
57
+ readonly prefix: number | string;
55
58
  readonly offset: number;
56
59
  };
57
60
  }
@@ -42,6 +42,7 @@ function validateEquations(lessons) {
42
42
  labels.push({
43
43
  key: raw.key,
44
44
  number: raw.number,
45
+ ...raw.short ? { short: raw.short } : {},
45
46
  lessonId: lesson.lessonId,
46
47
  slug: lesson.slug,
47
48
  section: raw.section,
@@ -1,3 +1,9 @@
1
+ /**
2
+ * The short caption a display equation carries, trimmed, or `null` when it
3
+ * has none. Only the first `\caption{}` counts; `remark-notation.mjs` fails a
4
+ * page that gives one equation two, or an empty one.
5
+ */
6
+ export function equationCaption(latex: any): string | null;
1
7
  /** `{ lesson?, key }` when `raw` is an equation reference, else `undefined`. */
2
8
  export function parseEquationRef(raw: any): {
3
9
  lesson: string;
@@ -6,27 +12,31 @@ export function parseEquationRef(raw: any): {
6
12
  key: string;
7
13
  lesson?: undefined;
8
14
  } | undefined;
9
- /** Remove every `\label{eq:…}` (and the whitespace it leaves) from a math source. */
15
+ /**
16
+ * Remove every `\label{eq:…}` and `\caption{…}` (and the whitespace they
17
+ * leave) from a math source: both are identity markers, not math KaTeX parses.
18
+ */
10
19
  export function stripEquationLabels(latex: any): string;
11
20
  /**
12
21
  * Normalise the numbering input to `{ prefix, offset }`.
13
22
  *
14
- * `prefix` is the number before the dot — the **part**'s ordinal (its position
15
- * among the sidebar groups). `offset` is how many display equations already
16
- * appeared in earlier lessons of that same part, so `n` runs continuously
17
- * across a part instead of restarting on every page. A bare number is the
18
- * legacy per-lesson form (`prefix = n`, `offset = 0`) and is still accepted so
19
- * hand-built trees and unit tests keep working. `undefined`/`null`/`0` ⇒ the
20
- * `##`-section fallback for non-lesson pages.
23
+ * `prefix` is everything before the last dot — `‹part›.‹chapter›` under the
24
+ * default `numbering.floats: "chapter"`, the part's ordinal under `"part"`.
25
+ * `offset` is how many items of the kind precede the lesson in that scope: 0
26
+ * for a chapter, the earlier chapters' count for a part, so `n` runs on
27
+ * across it. A bare number is the legacy per-lesson form (`prefix = n`,
28
+ * `offset = 0`) and is still accepted so hand-built trees and unit tests keep
29
+ * working. `undefined`/`null`/`0`/`''` ⇒ the `##`-section fallback for
30
+ * non-lesson pages.
21
31
  */
22
32
  export function resolveNumbering(input: any): {
23
33
  prefix: any;
24
34
  offset: number;
25
35
  };
26
36
  /**
27
- * The visible number for one display equation. With a `prefix` (the part's
28
- * ordinal) the scheme is `‹part›.‹offset + indexInLesson›` and `##` headings
29
- * are ignored; without it, the legacy `##`-section scheme.
37
+ * The visible number for one display equation. With a `prefix` the scheme is
38
+ * `‹prefix›.‹offset + indexInLesson›` and `##` headings are ignored; without
39
+ * it, the legacy `##`-section scheme.
30
40
  */
31
41
  export function displayEquationNumber({ prefix, offset, section, indexInSection, indexInLesson, }: {
32
42
  prefix: any;
@@ -35,10 +45,16 @@ export function displayEquationNumber({ prefix, offset, section, indexInSection,
35
45
  indexInSection: any;
36
46
  indexInLesson: any;
37
47
  }): string;
38
- /** The positional anchor id for an unkeyed equation, e.g. `2.4` → `eq-2-4`. */
48
+ /**
49
+ * A display equation's anchor id: `eq-<key>` when it carries a
50
+ * `\label{eq:…}`, else the positional form, e.g. `2.4` → `eq-2-4`.
51
+ *
52
+ * @param {{ key?: string, number?: string | number }} equation - `number` is
53
+ * read only when there is no `key`, so a keyed caller may omit it.
54
+ */
39
55
  export function equationAnchorId({ key, number }: {
40
- key: any;
41
- number: any;
56
+ key?: string;
57
+ number?: string | number;
42
58
  }): string;
43
59
  export function stripFencedCode(markdown: any): string;
44
60
  /**
@@ -53,20 +69,20 @@ export function stripFencedCode(markdown: any): string;
53
69
  * component while still finding `<Table>`/`<Figure>` tags at the top level.
54
70
  *
55
71
  * `transparent`, when given, removes only those components' opening/closing
56
- * tags and exposes their body to the later blanking pass. `<Memo>` and
57
- * `<ShortNote>` are intentional uses: unlike a worked-example/presentation
58
- * wrapper, their bodies are details in the lesson's reading order, so numbered
59
- * equations/floats in them stay in the official sequence. Section headings in
72
+ * tags and exposes their body to the later blanking pass. The
73
+ * {@link TRANSPARENT_COMPONENTS} are the intentional uses. Section headings in
60
74
  * a transparent body do not change the surrounding page's section counter.
61
75
  */
62
76
  export function stripJsxComponents(markdown: any, keep: any, transparent?: any[]): string;
63
77
  /**
64
78
  * Every display equation in a Markdown body, keyed or not, in appearance order
65
- * with its assigned number. `$$…$$` blocks only. With `numbering`
66
- * (`{ prefix, offset }`, the part's ordinal and the count of display equations
67
- * in earlier lessons of that part) the number is `‹part›.‹offset + n›`, `n`
68
- * sequential over every display block in the lesson; without it, `##` headings
69
- * advance a section counter and the number is `‹section›.‹indexInSection›`.
79
+ * with its assigned number and its `\caption{}` short caption (`short`, or
80
+ * `null`). `$$…$$` blocks only. With `numbering`
81
+ * (`{ prefix, offset }`, the lesson's float-number prefix and the count of
82
+ * display equations before it in that scope) the number is
83
+ * `‹prefix›.‹offset + n›`, `n` sequential over every display block in the
84
+ * lesson; without it, `##` headings advance a section counter and the number
85
+ * is `‹section›.‹indexInSection›`.
70
86
  * Either way every display block advances the index, so a keyed equation's
71
87
  * number reflects its position among *all* display equations, not only the
72
88
  * keyed ones.
@@ -74,6 +90,7 @@ export function stripJsxComponents(markdown: any, keep: any, transparent?: any[]
74
90
  export function scanDisplayEquations(markdown: any, numbering: any): {
75
91
  key: string | null | undefined;
76
92
  number: string;
93
+ short: string | null | undefined;
77
94
  section: number;
78
95
  indexInSection: number;
79
96
  }[];
@@ -81,6 +98,7 @@ export function scanDisplayEquations(markdown: any, numbering: any): {
81
98
  export function scanEquationLabels(markdown: any, numbering: any): {
82
99
  key: string | null | undefined;
83
100
  number: string;
101
+ short: string | null | undefined;
84
102
  section: number;
85
103
  indexInSection: number;
86
104
  }[];
@@ -109,23 +127,24 @@ export function collectEquationLabels(tree: any, numbering: any): any[];
109
127
  * Every display equation (`$$…$$`) in a lesson is numbered and given an `id`
110
128
  * from appearance order. When the caller supplies `numbering`
111
129
  * (`{ prefix, offset }` — see `resolveNumbering`), the number is
112
- * `‹part›.‹offset + n›`: the prefix is the **part**'s ordinal among the sidebar
113
- * groups, `n` counts every display equation in the lesson, and `offset` carries
114
- * the count from earlier lessons of the same part so the sequence runs
115
- * continuously across a part rather than restarting on every page. `##` and
116
- * `###` headings never affect it — `(2.4)` is the fourth display equation of
117
- * part 2, wherever in that part it sits. See `docs/authoring/structure.md` for
118
- * the level names. With no `numbering` the number falls back to the
119
- * `##`-section scheme `‹section›.‹indexInSection›` (used by hand-built test
120
- * trees and non-lesson pages). An author additionally
130
+ * `‹prefix›.‹offset + n›`: `n` counts every display equation in the lesson,
131
+ * and the course's `numbering.floats` scope decides the rest — `chapter` (the
132
+ * default) makes the prefix `‹part›.‹chapter›` with no offset, so `(2.2.4)` is
133
+ * the fourth display equation of part 2's second chapter; `part` makes it the
134
+ * part's ordinal alone, with `offset` carrying the count from earlier lessons
135
+ * of the part so the sequence runs on across it. `##` and `###` headings never
136
+ * affect it. See `docs/authoring/structure.md` for the level names. With no
137
+ * `numbering` the number falls back to the `##`-section scheme
138
+ * `‹section›.‹indexInSection›` (used by hand-built test trees and non-lesson
139
+ * pages). An author additionally
121
140
  * marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
122
141
  * block (inside math, so the `:` is safe and the form matches amsmath) when
123
142
  * they want a *stable* anchor that survives edits and cross-page references:
124
143
  *
125
- * [[eq-discount-factor-def]] same page → "(2.4)" linked to #eq-…
144
+ * [[eq-discount-factor-def]] same page → "(2.2.4)" linked to #eq-…
126
145
  * [[foundations/present-value#eq-pv]] cross page → number from the manifest
127
146
  *
128
- * An unkeyed equation still gets a positional anchor (`#eq-2-4`) so it can be
147
+ * An unkeyed equation still gets a positional anchor (`#eq-2-2-4`) so it can be
129
148
  * deep-linked, but only a keyed equation is referenceable by name.
130
149
  *
131
150
  * `scanEquationLabels` (raw Markdown) and `collectEquationLabels` (mdast tree)
@@ -138,9 +157,25 @@ export function collectEquationLabels(tree: any, numbering: any): any[];
138
157
  export const EQ_KEY_PATTERN: RegExp;
139
158
  /** A `\label{eq:<key>}` anywhere in a math source. Global; `key` in group 1. */
140
159
  export const EQ_LABEL_PATTERN: RegExp;
160
+ /**
161
+ * A `\caption{<short caption>}` anywhere in a math source — the equation's
162
+ * short caption, plain text with no nested braces. Global; the text in group
163
+ * 1. An equation takes only a short caption: it names the equation in the
164
+ * `[[eq-…]]` preview card and in the "Copy LaTeX — ‹caption›" tooltip, and is
165
+ * stripped before KaTeX exactly like `\label{}`.
166
+ */
167
+ export const EQ_CAPTION_PATTERN: RegExp;
141
168
  /**
142
169
  * A `[[ … ]]` payload that targets an equation: `eq-<key>` on the current page,
143
170
  * or `<lesson-slug>#eq-<key>` across pages. `lesson` (group 1) is a `/`-slug.
144
171
  * The `eq-` prefix is reserved — a notation key may not start with it.
145
172
  */
146
173
  export const EQ_REF_PATTERN: RegExp;
174
+ /**
175
+ * The components whose bodies stay in the lesson's reading order: `<Memo>`,
176
+ * `<ShortNote>` and `<Fold>`. Unlike a worked-example/presentation wrapper,
177
+ * their bodies are details or folded stretches of the lesson itself, so a
178
+ * numbered equation or float in one stays in the official sequence. Every
179
+ * scan and tree walk that numbers equations or floats reads this list.
180
+ */
181
+ export const TRANSPARENT_COMPONENTS: readonly string[];