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
@@ -39,6 +39,7 @@ import {
39
39
  stripNotationMarkers,
40
40
  } from './math-glyphs.mjs';
41
41
  import {
42
+ EQ_CAPTION_PATTERN,
42
43
  EQ_LABEL_PATTERN,
43
44
  collectDisplayEquations,
44
45
  collectEquationLabels,
@@ -57,6 +58,7 @@ import {
57
58
  parseKnowledgeCheckRef,
58
59
  } from './knowledge-checks.mjs';
59
60
  import { injectMdxStringAttributes } from './mdx-attributes.mjs';
61
+ import { collectSectionHeadings, parseSectionRef } from './sections.mjs';
60
62
 
61
63
  const PROSE_SKIP_TYPES = new Set([
62
64
  'code',
@@ -128,6 +130,23 @@ function localDefinitionsFromFile(file) {
128
130
  return definitionsMap(candidates.find(Array.isArray));
129
131
  }
130
132
 
133
+ /**
134
+ * The decorations a lesson suspends (`notation.suspend`, rule N22), by name.
135
+ * Read here as well as in the gate because `astro dev` renders without
136
+ * running the gate. The loader (`collections.ts`) validates the names.
137
+ */
138
+ function suspendedOperatorsFromFile(file) {
139
+ const candidates = [
140
+ file?.data?.astro?.frontmatter?.notation?.suspend,
141
+ file?.data?.frontmatter?.notation?.suspend,
142
+ file?.data?.notation?.suspend,
143
+ ];
144
+ const suspend = candidates.find(Array.isArray) ?? [];
145
+ return suspend
146
+ .map((item) => (typeof item?.operator === 'string' ? item.operator.trim() : ''))
147
+ .filter((operator) => operator !== '');
148
+ }
149
+
131
150
  // Registry definition pages (`src/content/notation/*.md`) carry their own
132
151
  // worked equations. Their math is resolved against the whole shared registry:
133
152
  // an author writes ordinary LaTeX (or an explicit `\explain{key}{latex}`) and
@@ -367,7 +386,8 @@ function inlineEquationNode(mathNode) {
367
386
  * at its positional `#eq-<section>-<index>`. The visible number is a focusable
368
387
  * `<a>` sibling — never authored, never inside KaTeX — so it survives print and
369
388
  * screen readers name it "Equation 2.4". A "Copy LaTeX" button sits beside it
370
- * (`.keyed-equation__controls`, styled in `global.css`) — the equation's own
389
+ * (`.keyed-equation__controls`, styled in `global.css`; titled "Copy LaTeX —
390
+ * ‹short›" when the equation has a `\caption{}`) — the equation's own
371
391
  * copy affordance, distinct from the notation popup card's (`NotationLayer.astro`),
372
392
  * which only exists for a symbol with a registry entry, not a bare display
373
393
  * equation. `mathNode.value` is read here — after the resolver above has
@@ -376,7 +396,7 @@ function inlineEquationNode(mathNode) {
376
396
  * author-written one; either way the reader copies plain LaTeX, never this
377
397
  * system's own notation-key plumbing (`math-glyphs.mjs:stripNotationMarkers`).
378
398
  */
379
- function equationNode(mathNode, { key, number }) {
399
+ function equationNode(mathNode, { key, number, short }) {
380
400
  const id = equationAnchorId({ key, number });
381
401
  const hProperties = {
382
402
  id,
@@ -385,6 +405,9 @@ function equationNode(mathNode, { key, number }) {
385
405
  };
386
406
  if (key) hProperties['data-eq-key'] = key;
387
407
  else hProperties['data-eq-auto'] = '';
408
+ // The `\caption{}` short caption names the equation wherever it is shown
409
+ // away from itself: the `[[eq-…]]` preview card and the copy tooltip.
410
+ if (short) hProperties['data-eq-short'] = short;
388
411
 
389
412
  const latex = stripNotationMarkers(String(mathNode.value ?? ''));
390
413
 
@@ -416,7 +439,7 @@ function equationNode(mathNode, { key, number }) {
416
439
  'data-eq-copy': '',
417
440
  'data-latex': latex,
418
441
  'aria-label': `Copy LaTeX for Equation ${number}`,
419
- title: 'Copy LaTeX',
442
+ title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
420
443
  },
421
444
  [copyIconNode()],
422
445
  ),
@@ -502,6 +525,47 @@ function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
502
525
  return { number, href: `#${prefix}-${floatRef.key}` };
503
526
  }
504
527
 
528
+ /**
529
+ * Resolve a `[[sec-slug]]` / `[[lesson/slug#sec-slug]]` reference to the
530
+ * heading's `{ href, text }`: `§ 1.1.2` for a numbered section when the
531
+ * course shows section numbers, else the heading's own title in quotes — a
532
+ * reference never prints a number the reader cannot find on the page.
533
+ */
534
+ function resolveSectionRef(secRef, ctx, file, node) {
535
+ const heading = secRef.lesson
536
+ ? ctx.crossPage?.[secRef.lesson]?.[secRef.key]
537
+ : ctx.pageHeadings.get(secRef.key);
538
+ if (heading === undefined) {
539
+ fail(
540
+ file,
541
+ secRef.lesson
542
+ ? `Unknown section reference [[${secRef.lesson}#sec-${secRef.key}]]: lesson ${secRef.lesson} has no top-level heading anchored #${secRef.key}.`
543
+ : `Unknown section reference [[sec-${secRef.key}]]: no top-level heading on this page is anchored #${secRef.key}.`,
544
+ node,
545
+ );
546
+ }
547
+ return {
548
+ href: secRef.lesson
549
+ ? `${ctx.base.replace(/\/$/, '')}/${secRef.lesson}/#${secRef.key}`
550
+ : `#${secRef.key}`,
551
+ text:
552
+ ctx.show && heading.number
553
+ ? `§ ${heading.number}`
554
+ : `“${heading.text}”`,
555
+ };
556
+ }
557
+
558
+ /**
559
+ * How a reference names a knowledge check or a worked-example set: `‹Kind›
560
+ * 1.1.1` when the course shows section numbers — those blocks' numbers follow
561
+ * the sections' (docs/plans/titles-and-containers.md Q11) — else `‹Kind›
562
+ * “‹title›”`, so a reference never prints a number the page does not show.
563
+ * The number is the fallback when the manifest carries no title.
564
+ */
565
+ function blockRefText(kind, number, title, show) {
566
+ return show || !title ? `${kind} ${number}` : `${kind} “${title}”`;
567
+ }
568
+
505
569
  /** Resolve a numbered knowledge check declared by a lesson's `.checks.yml`. */
506
570
  function resolveKnowledgeCheckRef(checkRef, ctx, file, node) {
507
571
  if (checkRef.lesson) {
@@ -515,6 +579,7 @@ function resolveKnowledgeCheckRef(checkRef, ctx, file, node) {
515
579
  }
516
580
  return {
517
581
  number,
582
+ title: ctx.titles?.[checkRef.lesson]?.[checkRef.key],
518
583
  href: `${ctx.base.replace(/\/$/, '')}/${checkRef.lesson}/#${knowledgeCheckAnchorId(checkRef.key)}`,
519
584
  };
520
585
  }
@@ -526,7 +591,11 @@ function resolveKnowledgeCheckRef(checkRef, ctx, file, node) {
526
591
  node,
527
592
  );
528
593
  }
529
- return { number, href: `#${knowledgeCheckAnchorId(checkRef.key)}` };
594
+ return {
595
+ number,
596
+ title: ctx.titles?.[ctx.slug]?.[checkRef.key],
597
+ href: `#${knowledgeCheckAnchorId(checkRef.key)}`,
598
+ };
530
599
  }
531
600
 
532
601
  function proseNodes(
@@ -540,6 +609,8 @@ function proseNodes(
540
609
  figureCtx,
541
610
  diagramCtx,
542
611
  checkCtx,
612
+ sectionCtx,
613
+ exampleCtx,
543
614
  ) {
544
615
  const base = eqCtx?.base ?? '';
545
616
  const strayExplain = firstUnescaped(value, EXPLAIN);
@@ -571,16 +642,35 @@ function proseNodes(
571
642
  !eqRef && !tableRef && !figureRef && diagramCtx
572
643
  ? parseKeyedFloatRef(key, { prefix: 'dia' })
573
644
  : undefined;
645
+ // `ex-` (a worked-example set) is reserved the same way as `sec-` below.
646
+ const exampleRef =
647
+ !eqRef && !tableRef && !figureRef && !diagramRef && exampleCtx
648
+ ? parseKeyedFloatRef(key, { prefix: 'ex' })
649
+ : undefined;
574
650
  const checkRef =
575
- !eqRef && !tableRef && !figureRef && !diagramRef && checkCtx
651
+ !eqRef && !tableRef && !figureRef && !diagramRef && !exampleRef && checkCtx
576
652
  ? parseKnowledgeCheckRef(key)
577
653
  : undefined;
654
+ // `sec-` is a reserved prefix like the others: `[[sec-foo]]` would also
655
+ // pass as a notation key, so it has to be claimed before that check.
656
+ const secRef =
657
+ !eqRef &&
658
+ !tableRef &&
659
+ !figureRef &&
660
+ !diagramRef &&
661
+ !exampleRef &&
662
+ !checkRef &&
663
+ sectionCtx
664
+ ? parseSectionRef(key)
665
+ : undefined;
578
666
  if (
579
667
  !eqRef &&
580
668
  !tableRef &&
581
669
  !figureRef &&
582
670
  !diagramRef &&
671
+ !exampleRef &&
583
672
  !checkRef &&
673
+ !secRef &&
584
674
  !NOTATION_KEY_PATTERN.test(key)
585
675
  ) {
586
676
  fail(file, `Invalid notation key "${key}" in [[...]].`, node);
@@ -613,8 +703,31 @@ function proseNodes(
613
703
  continue;
614
704
  }
615
705
 
706
+ if (secRef) {
707
+ const { href, text } = resolveSectionRef(secRef, sectionCtx, file, node);
708
+ output.push({
709
+ type: 'link',
710
+ url: href,
711
+ children: [{ type: 'text', value: text }],
712
+ data: {
713
+ hProperties: {
714
+ className: ['section-ref'],
715
+ 'data-sec-ref': secRef.key,
716
+ ...(secRef.lesson ? { 'data-sec-lesson': secRef.lesson } : {}),
717
+ },
718
+ },
719
+ });
720
+ references.push({
721
+ key: secRef.key,
722
+ kind: 'section',
723
+ ...(secRef.lesson ? { lesson: secRef.lesson } : {}),
724
+ });
725
+ cursor = match.index + match[0].length;
726
+ continue;
727
+ }
728
+
616
729
  if (checkRef) {
617
- const { number, href } = resolveKnowledgeCheckRef(
730
+ const { number, title, href } = resolveKnowledgeCheckRef(
618
731
  checkRef,
619
732
  checkCtx,
620
733
  file,
@@ -623,7 +736,12 @@ function proseNodes(
623
736
  output.push({
624
737
  type: 'link',
625
738
  url: href,
626
- children: [{ type: 'text', value: `Knowledge check ${number}` }],
739
+ children: [
740
+ {
741
+ type: 'text',
742
+ value: blockRefText('Knowledge check', number, title, checkCtx.show),
743
+ },
744
+ ],
627
745
  data: {
628
746
  hProperties: {
629
747
  className: ['knowledge-check-ref'],
@@ -643,6 +761,43 @@ function proseNodes(
643
761
  continue;
644
762
  }
645
763
 
764
+ if (exampleRef) {
765
+ const { number, href } = resolveKeyedFloatRef(
766
+ exampleRef,
767
+ exampleCtx,
768
+ { prefix: 'ex', tagName: 'CompactExamples' },
769
+ file,
770
+ node,
771
+ );
772
+ const title = exampleCtx.titles?.[exampleRef.lesson ?? exampleCtx.slug]?.[
773
+ exampleRef.key
774
+ ];
775
+ output.push({
776
+ type: 'link',
777
+ url: href,
778
+ children: [
779
+ {
780
+ type: 'text',
781
+ value: blockRefText('Example', number, title, exampleCtx.show),
782
+ },
783
+ ],
784
+ data: {
785
+ hProperties: {
786
+ className: ['example-ref'],
787
+ 'data-ex-ref': exampleRef.key,
788
+ ...(exampleRef.lesson ? { 'data-ex-lesson': exampleRef.lesson } : {}),
789
+ },
790
+ },
791
+ });
792
+ references.push({
793
+ key: exampleRef.key,
794
+ kind: 'example',
795
+ ...(exampleRef.lesson ? { lesson: exampleRef.lesson } : {}),
796
+ });
797
+ cursor = match.index + match[0].length;
798
+ continue;
799
+ }
800
+
646
801
  const floatRef = tableRef ?? figureRef ?? diagramRef;
647
802
  if (floatRef) {
648
803
  const kind = tableRef ? 'table' : figureRef ? 'figure' : 'diagram';
@@ -810,34 +965,67 @@ function inspectExplainCalls(value, resolve, file, node, references) {
810
965
  * Every number on a page comes from two places that MUST agree: the page's own
811
966
  * counters (`numbering`) and the manifest tables a cross-page `[[slug#eq-key]]`
812
967
  * is resolved against (`equations` / `tables` / `figures` / `diagrams` /
813
- * `checks`).
968
+ * `checks` / `examples`).
814
969
  * Deriving every input from the same manifest in one step stops a course config from
815
970
  * supplying one without the other — the omission that produced page-local
816
971
  * `(2.2)` and cross-page `(4.5)` for the same equation.
817
972
  *
818
973
  * `numbering` maps a lesson slug to
819
- * `{ part, equations, tables, figures, diagrams, checks }`: the part's ordinal
820
- * (the number before the dot) and, per numbered kind, how many earlier lessons
821
- * of that part already used.
974
+ * `{ part, chapter, prefix, equations, tables, figures, diagrams, checks,
975
+ * examples }`:
976
+ * the part and chapter ordinals, the float-number prefix (`‹part›.‹chapter›`,
977
+ * or the part alone under `numbering.floats: "part"`), and, per numbered kind,
978
+ * how many precede the lesson in that scope. `sections` maps a lesson slug to
979
+ * its top-level headings (`manifest.lessons[].headings`), and
980
+ * `sectionNumbering` is the course's `{ show, depth }`. `checkTitles` and
981
+ * `exampleTitles` map a lesson slug to its checks' and example sets' titles,
982
+ * which a reference prints when the course hides section numbers.
822
983
  */
823
984
  export function numberingFromManifest(manifest) {
824
985
  if (!manifest || typeof manifest !== 'object') return {};
986
+ const titlesOf = (entriesOf) =>
987
+ Object.fromEntries(
988
+ (manifest.lessons ?? []).map((lesson) => [
989
+ lesson.slug,
990
+ Object.fromEntries(
991
+ (entriesOf(lesson) ?? [])
992
+ .filter((entry) => entry.title)
993
+ .map((entry) => [entry.key, entry.title]),
994
+ ),
995
+ ]),
996
+ );
825
997
  return {
826
998
  equations: manifest.equations?.numbersBySlug ?? {},
827
999
  tables: manifest.tables?.numbersBySlug ?? {},
828
1000
  figures: manifest.figures?.numbersBySlug ?? {},
829
1001
  diagrams: manifest.diagrams?.numbersBySlug ?? {},
830
1002
  checks: manifest.checks?.numbersBySlug ?? {},
1003
+ examples: manifest.examples?.numbersBySlug ?? {},
1004
+ checkTitles: titlesOf((lesson) => lesson.knowledgeChecks),
1005
+ exampleTitles: titlesOf((lesson) =>
1006
+ lesson.examples?.map(({ key, short }) => ({ key, title: short })),
1007
+ ),
1008
+ // Only a lesson that carries `headings` (schema v20 on) — a hand-built
1009
+ // manifest without them leaves the page's headings as Astro would.
1010
+ sections: Object.fromEntries(
1011
+ (manifest.lessons ?? [])
1012
+ .filter((lesson) => Array.isArray(lesson.headings))
1013
+ .map((lesson) => [lesson.slug, lesson.headings]),
1014
+ ),
1015
+ sectionNumbering: manifest.numbering?.sections ?? { show: true, depth: 4 },
831
1016
  numbering: Object.fromEntries(
832
1017
  (manifest.lessons ?? []).map((lesson) => [
833
1018
  lesson.slug,
834
1019
  {
835
1020
  part: lesson.part,
1021
+ chapter: lesson.chapter,
1022
+ prefix: lesson.numbering?.prefix ?? String(lesson.part || ''),
836
1023
  equations: lesson.numbering?.equations ?? 0,
837
1024
  tables: lesson.numbering?.tables ?? 0,
838
1025
  figures: lesson.numbering?.figures ?? 0,
839
1026
  diagrams: lesson.numbering?.diagrams ?? 0,
840
1027
  checks: lesson.numbering?.checks ?? 0,
1028
+ examples: lesson.numbering?.examples ?? 0,
841
1029
  },
842
1030
  ]),
843
1031
  ),
@@ -876,20 +1064,77 @@ export function createRemarkNotationOptions(manifest, overrides = {}) {
876
1064
 
877
1065
  /**
878
1066
  * `{ prefix, offset }` for one kind of float on one page, from whichever
879
- * numbering option the caller supplied. `numbering` is the current form;
880
- * `chapters` is the superseded per-lesson form (prefix = the lesson's ordinal,
881
- * no offset) and is still read so an unmigrated config keeps working.
1067
+ * numbering option the caller supplied. `numbering` is the current form (its
1068
+ * `prefix` when given, else the part alone); `chapters` is the superseded
1069
+ * per-lesson form (prefix = the lesson's ordinal, no offset) and is still read
1070
+ * so an unmigrated config keeps working.
882
1071
  */
883
1072
  function pageNumbering(options, slug, kind) {
884
1073
  if (!slug) return undefined;
885
1074
  const placement = options.numbering?.[slug];
886
1075
  if (placement?.part) {
887
- return { prefix: placement.part, offset: placement[kind] ?? 0 };
1076
+ return {
1077
+ prefix: placement.prefix || placement.part,
1078
+ offset: placement[kind] ?? 0,
1079
+ };
888
1080
  }
889
1081
  const chapter = options.chapters?.[slug];
890
1082
  return chapter ? { prefix: chapter, offset: 0 } : undefined;
891
1083
  }
892
1084
 
1085
+ /**
1086
+ * Give a lesson's top-level headings the anchors and outline numbers the
1087
+ * manifest decided (`sections.mjs`): an `id` on every one, and
1088
+ * `data-sec-number` on a numbered one when the course shows section numbers
1089
+ * (`global.css` prints it). The page must see the headings the manifest saw —
1090
+ * same count, same levels, so the same numbers — or a `[[sec-…]]` from another
1091
+ * page would print a number this page does not show; any difference fails the
1092
+ * build, the section counterpart of `assertManifestNumberParity`.
1093
+ */
1094
+ function stampSectionHeadings({ tree, file, slug, published, placement, settings }) {
1095
+ const onPage = collectSectionHeadings(tree, {
1096
+ ...(placement?.part && placement?.chapter
1097
+ ? { prefix: `${placement.part}.${placement.chapter}` }
1098
+ : {}),
1099
+ depth: settings.depth,
1100
+ });
1101
+ const outline = (headings) =>
1102
+ headings.length === 0
1103
+ ? '(none)'
1104
+ : headings
1105
+ .map((heading) =>
1106
+ `${'#'.repeat(heading.level)}${heading.number ? ` ${heading.number}` : ''}`,
1107
+ )
1108
+ .join(', ');
1109
+ const agree =
1110
+ onPage.length === published.length &&
1111
+ onPage.every(
1112
+ (heading, index) =>
1113
+ heading.level === published[index].level &&
1114
+ heading.number === published[index].number,
1115
+ );
1116
+ if (!agree) {
1117
+ fail(
1118
+ file,
1119
+ `Stale Explico manifest for ${slug}: this page's top-level headings are ` +
1120
+ `${outline(onPage)}, but the manifest lists ${outline(published)}. ` +
1121
+ 'Recompile the manifest from the current content. If it is current, a ' +
1122
+ 'heading is written in a form the manifest does not read — an ' +
1123
+ 'underlined (setext) heading, or one inside a list or a blockquote; ' +
1124
+ 'write it as `## Heading` on a line of its own.',
1125
+ );
1126
+ }
1127
+ onPage.forEach(({ node }, index) => {
1128
+ const { slug: id, number } = published[index];
1129
+ node.data = node.data ?? {};
1130
+ node.data.hProperties = {
1131
+ ...node.data.hProperties,
1132
+ id,
1133
+ ...(settings.show && number ? { 'data-sec-number': number } : {}),
1134
+ };
1135
+ });
1136
+ }
1137
+
893
1138
  /**
894
1139
  * Fail when the number this page just computed for a keyed float disagrees
895
1140
  * with the number the manifest publishes for the same key (WERN — a reference
@@ -950,9 +1195,10 @@ function assertManifestNumberParity({
950
1195
  * missing/wrong map for *keyed* items (it walks `pageLabels`, which unkeyed
951
1196
  * equations/floats never enter — see `keyed-jsx-floats.mjs`), so a lesson
952
1197
  * whose only floats are unkeyed can silently fall back to `##`-section
953
- * numbering with no diagnostic at all. `Diagram` is deliberately excluded by
954
- * every caller: `Diagram.astro` already tolerates an unplaced diagram
955
- * rendering without a number, by design.
1198
+ * numbering with no diagnostic at all. `Diagram` and `CompactExamples` are
1199
+ * deliberately excluded by every caller: `Diagram.astro` and
1200
+ * `CompactExamples.astro` already tolerate an unplaced one rendering without
1201
+ * a number, by design.
956
1202
  */
957
1203
  function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }) {
958
1204
  if (!slug || count === 0) return;
@@ -992,7 +1238,12 @@ function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }
992
1238
  * figures?: Record<string, Record<string, string>>,
993
1239
  * diagrams?: Record<string, Record<string, string>>,
994
1240
  * checks?: Record<string, Record<string, string>>,
995
- * numbering?: Record<string, { part: number, equations: number, tables: number, figures: number, diagrams: number, checks: number }>,
1241
+ * examples?: Record<string, Record<string, string>>,
1242
+ * checkTitles?: Record<string, Record<string, string>>,
1243
+ * exampleTitles?: Record<string, Record<string, string>>,
1244
+ * sections?: Record<string, readonly { slug: string, text: string, level: number, number?: string }[]>,
1245
+ * sectionNumbering?: { show: boolean, depth: number },
1246
+ * numbering?: Record<string, { part: number, chapter?: number, prefix?: string, equations: number, tables: number, figures: number, diagrams: number, checks: number, examples?: number }>,
996
1247
  * chapters?: Record<string, number>,
997
1248
  * }} [options]
998
1249
  *
@@ -1001,16 +1252,23 @@ function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }
1001
1252
  * They stay available individually for hand-built trees, and an explicitly
1002
1253
  * supplied one still wins over the manifest.
1003
1254
  *
1004
- * `numbering` maps a lesson slug to its **part** (the number before the dot)
1005
- * and, per kind of float, how many the earlier lessons of that part already
1006
- * used. Every display equation / `<Table>` / `<Figure>` / `<Diagram>` on the page is then
1007
- * numbered `‹part›.‹offset + n›`, with `##` and `###` headings ignored; with no
1008
- * entry the legacy `##`-section scheme applies. It must agree with
1255
+ * `numbering` maps a lesson slug to its **part** and **chapter**, its float
1256
+ * `prefix` (`‹part›.‹chapter›`, or the part alone under `numbering.floats:
1257
+ * "part"`) and, per kind of float, how many precede the lesson in that scope.
1258
+ * Every display equation / `<Table>` / `<Figure>` / `<Diagram>` /
1259
+ * `<CompactExamples>` on the page is then numbered `‹prefix›.‹offset + n›`,
1260
+ * with `##` and `###` headings ignored;
1261
+ * with no entry the legacy `##`-section scheme applies. It must agree with
1009
1262
  * `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`/`diagrams`
1010
1263
  * above — supplying the tables without `numbering` numbers the same equation
1011
1264
  * twice, and `assertManifestNumberParity` turns that into a build error rather
1012
1265
  * than a page whose references disagree with their targets.
1013
1266
  *
1267
+ * `sections` maps a lesson slug to its top-level headings, and
1268
+ * `sectionNumbering` is the course's `{ show, depth }`: a page's own headings
1269
+ * take the anchors and outline numbers listed there (`stampSectionHeadings`),
1270
+ * and `[[sec-…]]` prints `§ 1.1.2` from the same list.
1271
+ *
1014
1272
  * `chapters` is the superseded per-lesson form (slug → the lesson's course
1015
1273
  * ordinal, no running offset). Still read when `numbering` has no entry, so a
1016
1274
  * config written against the old option keeps building.
@@ -1029,6 +1287,15 @@ export default function remarkNotation(options = {}) {
1029
1287
  // The manifest supplies every numbering input at once; an explicit option
1030
1288
  // still overrides it, so existing per-field wiring keeps working.
1031
1289
  const numbering = { ...numberingFromManifest(options.manifest), ...options };
1290
+ // Every lesson's headings by anchor, built once: `[[lesson#sec-…]]` reads
1291
+ // another page's, `[[sec-…]]` the page's own.
1292
+ const sectionSettings = { show: true, depth: 4, ...numbering.sectionNumbering };
1293
+ const headingsBySlug = Object.fromEntries(
1294
+ Object.entries(numbering.sections ?? {}).map(([slug, headings]) => [
1295
+ slug,
1296
+ Object.fromEntries(headings.map((heading) => [heading.slug, heading])),
1297
+ ]),
1298
+ );
1032
1299
 
1033
1300
  return function transform(tree, file) {
1034
1301
  const suppliedDefinitions =
@@ -1040,6 +1307,7 @@ export default function remarkNotation(options = {}) {
1040
1307
  (definition) => ({ key: definition.key, notation: definition.notation }),
1041
1308
  );
1042
1309
  const localDefinitions = localDefinitionsFromFile(file);
1310
+ const suspendedOperators = suspendedOperatorsFromFile(file);
1043
1311
  const lessonId = lessonIdFrom(file);
1044
1312
  const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
1045
1313
  // `{ prefix, offset }` per kind of float — see the `numbering` option.
@@ -1136,8 +1404,9 @@ export default function remarkNotation(options = {}) {
1136
1404
  noun: tagName,
1137
1405
  syntax: (key) => `<${tagName} id="${key}">`,
1138
1406
  });
1139
- // Diagram is deliberately excluded — see `assertStrictNumbering`'s doc.
1140
- if (strict && tagName !== 'Diagram') {
1407
+ // Diagram and CompactExamples are deliberately excluded — see
1408
+ // `assertStrictNumbering`'s doc.
1409
+ if (strict && tagName !== 'Diagram' && tagName !== 'CompactExamples') {
1141
1410
  assertStrictNumbering({
1142
1411
  file,
1143
1412
  slug: lessonSlug,
@@ -1171,12 +1440,52 @@ export default function remarkNotation(options = {}) {
1171
1440
  numbering.diagrams,
1172
1441
  'diagrams',
1173
1442
  );
1443
+ // A worked-example set is numbered like a float (docs/plans/
1444
+ // titles-and-containers.md Part 6), but its number shows — and a
1445
+ // reference prints it — by the section rules (`show`).
1446
+ const { ctx: exampleFloatCtx, byNode: exampleByNode } = makeFloatCtx(
1447
+ 'CompactExamples',
1448
+ numbering.examples,
1449
+ 'examples',
1450
+ );
1451
+ const exampleCtx = {
1452
+ ...exampleFloatCtx,
1453
+ slug: lessonSlug,
1454
+ show: sectionSettings.show,
1455
+ titles: numbering.exampleTitles ?? {},
1456
+ };
1174
1457
  const checkCtx = {
1175
1458
  base: options.base ?? '',
1176
1459
  pageLabels: new Map(
1177
1460
  Object.entries(numbering.checks?.[lessonSlug] ?? {}),
1178
1461
  ),
1179
1462
  crossPage: numbering.checks ?? {},
1463
+ slug: lessonSlug,
1464
+ show: sectionSettings.show,
1465
+ titles: numbering.checkTitles ?? {},
1466
+ };
1467
+
1468
+ // Outline numbering: the lesson's top-level headings take the manifest's
1469
+ // anchors and numbers before anything else reads them, and `[[sec-…]]`
1470
+ // resolves against that same table.
1471
+ const publishedHeadings = lessonSlug
1472
+ ? numbering.sections?.[lessonSlug]
1473
+ : undefined;
1474
+ if (lessonId && publishedHeadings) {
1475
+ stampSectionHeadings({
1476
+ tree,
1477
+ file,
1478
+ slug: lessonSlug,
1479
+ published: publishedHeadings,
1480
+ placement: numbering.numbering?.[lessonSlug],
1481
+ settings: sectionSettings,
1482
+ });
1483
+ }
1484
+ const sectionCtx = {
1485
+ base: options.base ?? '',
1486
+ show: sectionSettings.show,
1487
+ pageHeadings: new Map(Object.entries(headingsBySlug[lessonSlug] ?? {})),
1488
+ crossPage: headingsBySlug,
1180
1489
  };
1181
1490
 
1182
1491
  const resolve = (key, kind, node) => {
@@ -1220,14 +1529,39 @@ export default function remarkNotation(options = {}) {
1220
1529
 
1221
1530
  EQ_LABEL_PATTERN.lastIndex = 0;
1222
1531
  const labelMatch = EQ_LABEL_PATTERN.exec(value);
1223
- if (labelMatch) {
1532
+ const captions = [...value.matchAll(EQ_CAPTION_PATTERN)];
1533
+ if (labelMatch && node.type === 'inlineMath') {
1534
+ fail(
1535
+ file,
1536
+ `\\label{eq:${labelMatch[1]}} is only valid in a $$…$$ display equation, not inline $…$.`,
1537
+ node,
1538
+ );
1539
+ }
1540
+ if (captions.length > 0) {
1224
1541
  if (node.type === 'inlineMath') {
1225
1542
  fail(
1226
1543
  file,
1227
- `\\label{eq:${labelMatch[1]}} is only valid in a $$…$$ display equation, not inline $…$.`,
1544
+ `\\caption{…} is only valid in a $$…$$ display equation, not inline $…$.`,
1545
+ node,
1546
+ );
1547
+ }
1548
+ if (captions.length > 1) {
1549
+ fail(
1550
+ file,
1551
+ 'A display equation takes one \\caption{…}; this one has ' +
1552
+ `${captions.length}.`,
1228
1553
  node,
1229
1554
  );
1230
1555
  }
1556
+ if (!captions[0][1].trim()) {
1557
+ fail(
1558
+ file,
1559
+ '\\caption{} is empty: give the equation a short caption, or drop it.',
1560
+ node,
1561
+ );
1562
+ }
1563
+ }
1564
+ if (labelMatch || captions.length > 0) {
1231
1565
  value = stripEquationLabels(value);
1232
1566
  replaceMathSource(node, value);
1233
1567
  }
@@ -1245,7 +1579,10 @@ export default function remarkNotation(options = {}) {
1245
1579
 
1246
1580
  let result;
1247
1581
  try {
1248
- result = resolveMathGlyphs(value, bindingScope);
1582
+ // Suspension (rule N22) is lesson-body scope only.
1583
+ result = resolveMathGlyphs(value, bindingScope, {
1584
+ suspended: lessonId ? suspendedOperators : [],
1585
+ });
1249
1586
  } catch (error) {
1250
1587
  const message =
1251
1588
  error instanceof GlyphResolutionError
@@ -1259,8 +1596,14 @@ export default function remarkNotation(options = {}) {
1259
1596
  if (result.unresolved.length > 0) {
1260
1597
  const unresolved = result.unresolved
1261
1598
  .map(
1262
- ({ token, start }) =>
1263
- `${tokenLocation(file, node, value, start)}: "${token}"`,
1599
+ ({ token, start, suspended, font }) =>
1600
+ `${tokenLocation(file, node, value, start)}: "${token}"${
1601
+ suspended
1602
+ ? ` (${suspended} is suspended on this page, so this form needs its own entry)`
1603
+ : font
1604
+ ? ' (a different symbol from the plain letter; it needs its own entry)'
1605
+ : ''
1606
+ }`,
1264
1607
  )
1265
1608
  .join(', ');
1266
1609
  fail(
@@ -1301,6 +1644,8 @@ export default function remarkNotation(options = {}) {
1301
1644
  figureCtx,
1302
1645
  diagramCtx,
1303
1646
  checkCtx,
1647
+ sectionCtx,
1648
+ exampleCtx,
1304
1649
  );
1305
1650
  if (replacement) {
1306
1651
  node.children.splice(index, 1, ...replacement);
@@ -1321,6 +1666,8 @@ export default function remarkNotation(options = {}) {
1321
1666
  injectFloatNumberAttributes(child, figureByNode.get(child), 'fig');
1322
1667
  } else if (child?.type === 'mdxJsxFlowElement' && diagramByNode.has(child)) {
1323
1668
  injectFloatNumberAttributes(child, diagramByNode.get(child), 'dia');
1669
+ } else if (child?.type === 'mdxJsxFlowElement' && exampleByNode.has(child)) {
1670
+ injectFloatNumberAttributes(child, exampleByNode.get(child), 'ex');
1324
1671
  }
1325
1672
  }
1326
1673
  };
@@ -1329,10 +1676,13 @@ export default function remarkNotation(options = {}) {
1329
1676
  // validation — the manifest builder runs the same check via
1330
1677
  // `validateRawFloats`/`scanRawFloats` (raw-floats.mjs); this catches
1331
1678
  // content added since the manifest was last compiled.
1679
+ // A course that turned rule S3 (tables) or S4 (diagrams) off in its
1680
+ // settings has `manifest.rules.disabled` say so; honour it here too.
1681
+ const disabledRules = new Set(options.manifest?.rules?.disabled ?? []);
1332
1682
  if (lessonId) {
1333
1683
  const rawFloats = collectRawFloats(tree);
1334
1684
  const plural = (count, noun) => (count === 1 ? noun : `${noun}s`);
1335
- if (rawFloats.markdownTableNodes.length > 0) {
1685
+ if (rawFloats.markdownTableNodes.length > 0 && !disabledRules.has('S3')) {
1336
1686
  warn(
1337
1687
  file,
1338
1688
  `${lessonSlug} has ${rawFloats.markdownTableNodes.length} raw ` +
@@ -1343,7 +1693,7 @@ export default function remarkNotation(options = {}) {
1343
1693
  rawFloats.markdownTableNodes[0],
1344
1694
  );
1345
1695
  }
1346
- if (rawFloats.htmlTableNodes.length > 0) {
1696
+ if (rawFloats.htmlTableNodes.length > 0 && !disabledRules.has('S3')) {
1347
1697
  warn(
1348
1698
  file,
1349
1699
  `${lessonSlug} has ${rawFloats.htmlTableNodes.length} raw HTML ` +
@@ -1354,7 +1704,7 @@ export default function remarkNotation(options = {}) {
1354
1704
  rawFloats.htmlTableNodes[0],
1355
1705
  );
1356
1706
  }
1357
- if (rawFloats.unkeyedDiagramNodes.length > 0) {
1707
+ if (rawFloats.unkeyedDiagramNodes.length > 0 && !disabledRules.has('S4')) {
1358
1708
  warn(
1359
1709
  file,
1360
1710
  `${lessonSlug} has ${rawFloats.unkeyedDiagramNodes.length} ` +
@@ -1364,14 +1714,14 @@ export default function remarkNotation(options = {}) {
1364
1714
  rawFloats.unkeyedDiagramNodes[0],
1365
1715
  );
1366
1716
  }
1367
- if (rawFloats.nestedDiagramNodes.length > 0) {
1717
+ if (rawFloats.nestedDiagramNodes.length > 0 && !disabledRules.has('S4')) {
1368
1718
  warn(
1369
1719
  file,
1370
1720
  `${lessonSlug} has ${rawFloats.nestedDiagramNodes.length} ` +
1371
1721
  `<Diagram> ${plural(rawFloats.nestedDiagramNodes.length, 'component')} ` +
1372
1722
  `nested inside another component; it is invisible to diagram ` +
1373
1723
  `numbering. Move it to the top level (or inside ` +
1374
- `<Memo>/<ShortNote>), or wrap it in <Figure> to give it identity.`,
1724
+ `<Memo>/<ShortNote>/<Fold>), or wrap it in <Figure> to give it identity.`,
1375
1725
  rawFloats.nestedDiagramNodes[0],
1376
1726
  );
1377
1727
  }
@@ -1401,5 +1751,6 @@ export default function remarkNotation(options = {}) {
1401
1751
  file.data.tableLabels = floatLabelData(tableByNode);
1402
1752
  file.data.figureLabels = floatLabelData(figureByNode);
1403
1753
  file.data.diagramLabels = floatLabelData(diagramByNode);
1754
+ file.data.exampleLabels = floatLabelData(exampleByNode);
1404
1755
  };
1405
1756
  }