explico 0.12.0 → 0.13.1

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 (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -74,6 +74,27 @@ export function stripFencedCode(markdown: any): string;
74
74
  * a transparent body do not change the surrounding page's section counter.
75
75
  */
76
76
  export function stripJsxComponents(markdown: any, keep: any, transparent?: any[]): string;
77
+ /**
78
+ * Blank the body of every `:::name … :::` container directive — a Starlight
79
+ * aside (`:::note`, `:::tip`, …) — keeping newlines, the text-scan twin of a
80
+ * tree walk that never enters a `containerDirective`. An aside is a callout,
81
+ * like `<Aside>`: what it holds is illustrative and numbers nothing. A fence
82
+ * of more colons closes only on as many; one left open runs to the end.
83
+ */
84
+ export function stripContainerDirectives(markdown: any): string;
85
+ /**
86
+ * Every `$$` display block in a Markdown text the way remark-math reads one:
87
+ * an opening fence of two or more `$` that starts a line — inside a list item
88
+ * or a blockquote too — with nothing but a `$`-free meta after it, up to a
89
+ * closing fence of at least as many `$` on a line of its own (or the end of
90
+ * the text). `$$x$$` inside a paragraph, or on one line, is inline math and
91
+ * is not a display block. Each block is `{ index, body }`: the offset of its
92
+ * opening line and the text between the fences.
93
+ */
94
+ export function displayMathBlocks(markdown: any): {
95
+ index: number;
96
+ body: string;
97
+ }[];
77
98
  /**
78
99
  * Every display equation in a Markdown body, keyed or not, in appearance order
79
100
  * with its assigned number and its `\caption{}` short caption (`short`, or
@@ -94,6 +115,12 @@ export function scanDisplayEquations(markdown: any, numbering: any): {
94
115
  section: number;
95
116
  indexInSection: number;
96
117
  }[];
118
+ /**
119
+ * The keys of each display block that carries more than one `\label{eq:…}`,
120
+ * in order. A block is one numbered equation: its first label names it, and
121
+ * a later one would name nothing.
122
+ */
123
+ export function repeatedEquationLabels(markdown: any): string[][];
97
124
  /** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
98
125
  export function scanEquationLabels(markdown: any, numbering: any): {
99
126
  key: string | null | undefined;
@@ -204,6 +204,87 @@ export function stripJsxComponents(markdown, keep, transparent = []) {
204
204
  */
205
205
  export const TRANSPARENT_COMPONENTS = Object.freeze(['Memo', 'ShortNote', 'Fold']);
206
206
 
207
+ /**
208
+ * Blank the body of every `:::name … :::` container directive — a Starlight
209
+ * aside (`:::note`, `:::tip`, …) — keeping newlines, the text-scan twin of a
210
+ * tree walk that never enters a `containerDirective`. An aside is a callout,
211
+ * like `<Aside>`: what it holds is illustrative and numbers nothing. A fence
212
+ * of more colons closes only on as many; one left open runs to the end.
213
+ */
214
+ export function stripContainerDirectives(markdown) {
215
+ const lines = String(markdown).split('\n');
216
+ let fence = 0;
217
+ for (let index = 0; index < lines.length; index += 1) {
218
+ const line = lines[index] ?? '';
219
+ if (fence === 0) {
220
+ const open = /^[ \t]{0,3}(:{3,})[A-Za-z]/.exec(line);
221
+ if (!open) continue;
222
+ fence = open[1].length;
223
+ lines[index] = line.replace(/[^\n]/g, ' ');
224
+ continue;
225
+ }
226
+ const close = /^[ \t]{0,3}(:{3,})[ \t]*$/.exec(line);
227
+ if (close && close[1].length >= fence) fence = 0;
228
+ lines[index] = line.replace(/[^\n]/g, ' ');
229
+ }
230
+ return lines.join('\n');
231
+ }
232
+
233
+ /** A line with its blockquote markers, list marker and indentation removed. */
234
+ function containerContent(line) {
235
+ let rest = String(line);
236
+ for (;;) {
237
+ const next = rest
238
+ .replace(/^[ \t]*>[ \t]?/, '')
239
+ .replace(/^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]+/, '');
240
+ if (next === rest) return rest.replace(/^[ \t]+/, '');
241
+ rest = next;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Every `$$` display block in a Markdown text the way remark-math reads one:
247
+ * an opening fence of two or more `$` that starts a line — inside a list item
248
+ * or a blockquote too — with nothing but a `$`-free meta after it, up to a
249
+ * closing fence of at least as many `$` on a line of its own (or the end of
250
+ * the text). `$$x$$` inside a paragraph, or on one line, is inline math and
251
+ * is not a display block. Each block is `{ index, body }`: the offset of its
252
+ * opening line and the text between the fences.
253
+ */
254
+ export function displayMathBlocks(markdown) {
255
+ const text = String(markdown);
256
+ const lines = text.split('\n');
257
+ const starts = [];
258
+ let offset = 0;
259
+ for (const line of lines) {
260
+ starts.push(offset);
261
+ offset += line.length + 1;
262
+ }
263
+ const blocks = [];
264
+ for (let index = 0; index < lines.length; index += 1) {
265
+ const open = /^(\${2,})[ \t]*[^$]*$/.exec(containerContent(lines[index]));
266
+ if (!open) continue;
267
+ const size = open[1].length;
268
+ let end = lines.length;
269
+ for (let next = index + 1; next < lines.length; next += 1) {
270
+ const close = /^(\${2,})[ \t]*$/.exec(containerContent(lines[next]));
271
+ if (close && close[1].length >= size) {
272
+ end = next;
273
+ break;
274
+ }
275
+ }
276
+ blocks.push({
277
+ index: starts[index],
278
+ body: lines
279
+ .slice(index + 1, end)
280
+ .map((line) => containerContent(line))
281
+ .join('\n'),
282
+ });
283
+ index = end;
284
+ }
285
+ return blocks;
286
+ }
287
+
207
288
  /**
208
289
  * Every display equation in a Markdown body, keyed or not, in appearance order
209
290
  * with its assigned number and its `\caption{}` short caption (`short`, or
@@ -219,25 +300,20 @@ export const TRANSPARENT_COMPONENTS = Object.freeze(['Memo', 'ShortNote', 'Fold'
219
300
  */
220
301
  export function scanDisplayEquations(markdown, numbering) {
221
302
  const { prefix, offset } = resolveNumbering(numbering);
222
- const text = stripJsxComponents(
223
- stripFencedCode(markdown),
224
- undefined,
225
- TRANSPARENT_COMPONENTS,
226
- );
303
+ const text = equationText(markdown);
227
304
  const tokens = [];
228
305
  const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
229
- const display = /\$\$([\s\S]+?)\$\$/g;
230
306
  for (const match of text.matchAll(heading)) {
231
307
  tokens.push({ kind: 'section', index: match.index });
232
308
  }
233
- for (const match of text.matchAll(display)) {
309
+ for (const block of displayMathBlocks(text)) {
234
310
  EQ_LABEL_PATTERN.lastIndex = 0;
235
- const label = EQ_LABEL_PATTERN.exec(match[1]);
311
+ const label = EQ_LABEL_PATTERN.exec(block.body);
236
312
  tokens.push({
237
313
  kind: 'equation',
238
- index: match.index,
314
+ index: block.index,
239
315
  key: label ? label[1] : null,
240
- short: equationCaption(match[1]),
316
+ short: equationCaption(block.body),
241
317
  });
242
318
  }
243
319
  tokens.sort((a, b) => a.index - b.index);
@@ -271,6 +347,31 @@ export function scanDisplayEquations(markdown, numbering) {
271
347
  return labels;
272
348
  }
273
349
 
350
+ /**
351
+ * A lesson body as the equation scans read it: no fenced code, no component
352
+ * but the transparent ones, no `:::` directive body.
353
+ */
354
+ function equationText(markdown) {
355
+ return stripContainerDirectives(
356
+ stripJsxComponents(stripFencedCode(markdown), undefined, TRANSPARENT_COMPONENTS),
357
+ );
358
+ }
359
+
360
+ /**
361
+ * The keys of each display block that carries more than one `\label{eq:…}`,
362
+ * in order. A block is one numbered equation: its first label names it, and
363
+ * a later one would name nothing.
364
+ */
365
+ export function repeatedEquationLabels(markdown) {
366
+ return displayMathBlocks(equationText(markdown))
367
+ // A copy: `matchAll` starts at the shared pattern's `lastIndex`, which
368
+ // `scanDisplayEquations`' `exec` leaves past the label it found.
369
+ .map((block) =>
370
+ [...block.body.matchAll(new RegExp(EQ_LABEL_PATTERN.source, 'g'))].map((match) => match[1]),
371
+ )
372
+ .filter((keys) => keys.length > 1);
373
+ }
374
+
274
375
  /** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
275
376
  export function scanEquationLabels(markdown, numbering) {
276
377
  return scanDisplayEquations(markdown, numbering).filter(
@@ -301,13 +402,16 @@ export function collectDisplayEquations(tree, numbering) {
301
402
  let indexInSection = 0;
302
403
  let indexInLesson = 0;
303
404
 
304
- // Top-level blocks plus the bodies of the `TRANSPARENT_COMPONENTS`: other
305
- // MDX components (a `<CompactExample>` worked example, an `<Aside>`) remain
306
- // illustrative and outside the lesson's official equation sequence. The
307
- // transparent ones occupy an exact point in the reading order, even while
308
- // visually collapsed.
405
+ // The lesson's flow — top-level blocks, list items, blockquotes — plus the
406
+ // bodies of the `TRANSPARENT_COMPONENTS`: other MDX components (a
407
+ // `<CompactExample>` worked example, an `<Aside>`) and `:::note`-style
408
+ // asides remain illustrative and outside the lesson's official equation
409
+ // sequence. The transparent ones occupy an exact point in the reading
410
+ // order, even while visually collapsed. `scanDisplayEquations` reads the
411
+ // same set from the text.
309
412
  const collectFromDetail = (node) => {
310
413
  if (!node || typeof node !== 'object') return;
414
+ if (node.type === 'containerDirective') return;
311
415
  if (node.type === 'math') {
312
416
  indexInSection += 1;
313
417
  indexInLesson += 1;
@@ -347,13 +451,7 @@ export function collectDisplayEquations(tree, numbering) {
347
451
  indexInSection = 0;
348
452
  continue;
349
453
  }
350
- if (node.type === 'math') collectFromDetail(node);
351
- else if (
352
- node.type === 'mdxJsxFlowElement' &&
353
- TRANSPARENT_COMPONENTS.includes(node.name)
354
- ) {
355
- collectFromDetail(node);
356
- }
454
+ collectFromDetail(node);
357
455
  }
358
456
  return equations;
359
457
  }
@@ -12,7 +12,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
12
12
  * no default title to fall back on.
13
13
  */
14
14
 
15
- type ExampleDiagnosticCode = 'ex-invalid-key' | 'ex-duplicate-key' | 'ex-ref-resolves' | 'ex-key-unused' | 'ex-removed-prop' | 'ex-missing-prop';
15
+ type ExampleDiagnosticCode = 'ex-invalid-key' | 'ex-duplicate-key' | 'ex-ref-resolves' | 'ex-key-unused' | 'ex-removed-prop' | 'ex-unread-prop' | 'ex-missing-prop';
16
16
  interface ExampleDiagnostic {
17
17
  readonly code: ExampleDiagnosticCode;
18
18
  readonly severity: 'error' | 'warning' | 'info';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Rule V7 — a figure's picture fits on one screen.
3
+ *
4
+ * A picture's height is only known once the browser has laid the page out and
5
+ * hydrated its islands, so this check runs in a course's Playwright suite, not
6
+ * in the build: open each lesson that has a figure, call
7
+ * {@link checkFigureFit}, and expect no findings. The pure half,
8
+ * {@link figureFitFindings}, turns measured heights into findings and is what
9
+ * the unit tests pin.
10
+ */
11
+ /** The share of the screen height a picture may take unless the course sets `rules.V7.maxPercent`. */
12
+ declare const FIGURE_FIT_MAX_PERCENT = 80;
13
+ /** One `<Figure>` as measured on a rendered page. */
14
+ interface FigureMeasurement {
15
+ /** The figure's anchor, `fig-<key>`. */
16
+ readonly anchor: string;
17
+ /** Its number, `2.1.3`. */
18
+ readonly number: string;
19
+ /** Height of the picture — the figure's default slot — in CSS pixels. */
20
+ readonly height: number;
21
+ }
22
+ interface FigureFitFinding {
23
+ readonly code: 'figure-too-tall';
24
+ readonly severity: 'warning';
25
+ readonly rule: 'V7';
26
+ readonly anchor: string;
27
+ readonly number: string;
28
+ readonly height: number;
29
+ /** The tallest picture the budget allows at this screen height, in CSS pixels. */
30
+ readonly limit: number;
31
+ readonly message: string;
32
+ }
33
+ /**
34
+ * The pictures taller than `maxPercent` of a `screenHeight`-pixel screen.
35
+ * A picture of height zero is skipped: nothing was drawn to judge.
36
+ */
37
+ declare function figureFitFindings(measured: readonly FigureMeasurement[], screenHeight: number, maxPercent?: number): FigureFitFinding[];
38
+ /** The part of a Playwright `Page` the check uses. */
39
+ interface FigureFitPage {
40
+ evaluate<R>(pageFunction: () => R | Promise<R>): Promise<R>;
41
+ viewportSize(): {
42
+ readonly width: number;
43
+ readonly height: number;
44
+ } | null;
45
+ }
46
+ /**
47
+ * Measure every `<Figure>` on the open page and report the pictures that break
48
+ * rule V7 at the page's current viewport. A figure inside a closed `<Fold>` or
49
+ * `<details>` is opened first, since a reader can open it; each figure is
50
+ * scrolled into view and waits for its islands to hydrate and its height to
51
+ * settle before it is measured.
52
+ */
53
+ declare function checkFigureFit(page: FigureFitPage, options?: {
54
+ readonly maxPercent?: number;
55
+ /** Figures exempt from the budget, by anchor (`fig-<key>`), each with the reason it may be taller. */
56
+ readonly allow?: Readonly<Record<string, string>>;
57
+ }): Promise<FigureFitFinding[]>;
58
+ /**
59
+ * The lesson paths whose manifest entry has at least one figure — the pages a
60
+ * course's V7 test opens. Pass the manifest the build wrote
61
+ * (`build/manifest.json`).
62
+ */
63
+ declare function lessonsWithFigures(manifest: {
64
+ readonly lessons: readonly {
65
+ readonly slug: string;
66
+ readonly figures: readonly unknown[];
67
+ }[];
68
+ }): string[];
69
+
70
+ export { FIGURE_FIT_MAX_PERCENT, type FigureFitFinding, type FigureFitPage, type FigureMeasurement, checkFigureFit, figureFitFindings, lessonsWithFigures };
@@ -0,0 +1,60 @@
1
+ const FIGURE_FIT_MAX_PERCENT = 80;
2
+ function figureFitFindings(measured, screenHeight, maxPercent = FIGURE_FIT_MAX_PERCENT) {
3
+ const limit = Math.floor(screenHeight * maxPercent / 100);
4
+ return measured.filter((figure) => figure.height > limit).map((figure) => ({
5
+ code: "figure-too-tall",
6
+ severity: "warning",
7
+ rule: "V7",
8
+ anchor: figure.anchor,
9
+ number: figure.number,
10
+ height: figure.height,
11
+ limit,
12
+ message: `Figure ${figure.number} (#${figure.anchor}): its picture is ${figure.height}px tall, ${Math.round(figure.height / screenHeight * 100)}% of a ${screenHeight}px screen; rule V7 allows ${maxPercent}% (${limit}px). Give a square or tall plot a fixed width instead of the full column, or split stacked panels.`
13
+ }));
14
+ }
15
+ async function checkFigureFit(page, options = {}) {
16
+ const viewport = page.viewportSize();
17
+ if (!viewport) throw new Error("checkFigureFit needs a page with a fixed viewport size.");
18
+ const measured = await page.evaluate(measureFigures);
19
+ const allow = options.allow ?? {};
20
+ for (const [anchor, reason] of Object.entries(allow)) {
21
+ if (!reason.trim()) throw new Error(`checkFigureFit: the exemption for #${anchor} needs a reason.`);
22
+ }
23
+ return figureFitFindings(
24
+ measured.filter((figure) => !(figure.anchor in allow)),
25
+ viewport.height,
26
+ options.maxPercent
27
+ );
28
+ }
29
+ function lessonsWithFigures(manifest) {
30
+ return manifest.lessons.filter((lesson) => lesson.figures.length > 0).map((lesson) => `/${lesson.slug}/`);
31
+ }
32
+ async function measureFigures() {
33
+ const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
34
+ const out = [];
35
+ for (const figure of document.querySelectorAll("figure.keyed-figure")) {
36
+ for (let fold = figure.parentElement?.closest("details"); fold; fold = fold.parentElement?.closest("details")) {
37
+ fold.open = true;
38
+ }
39
+ const body = figure.querySelector(":scope > .keyed-figure__body");
40
+ if (!body) continue;
41
+ figure.scrollIntoView({ block: "start" });
42
+ const deadline = performance.now() + 5e3;
43
+ while (body.querySelector("astro-island[ssr]") && performance.now() < deadline) await nextFrame();
44
+ let height = -1;
45
+ for (let still = 0, frames = 0; still < 5 && frames < 120; frames++) {
46
+ await nextFrame();
47
+ const now = body.getBoundingClientRect().height;
48
+ still = Math.abs(now - height) < 0.5 ? still + 1 : 0;
49
+ height = now;
50
+ }
51
+ out.push({ anchor: figure.id, number: figure.dataset.figNumber ?? "", height: Math.round(height) });
52
+ }
53
+ return out;
54
+ }
55
+ export {
56
+ FIGURE_FIT_MAX_PERCENT,
57
+ checkFigureFit,
58
+ figureFitFindings,
59
+ lessonsWithFigures
60
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Every figure has its picture in the built page (rule V3). A lab draws its
3
+ * plot in the browser, after the page loads, so without a copy drawn at
4
+ * build time the page holds an empty box until then: a reference card made
5
+ * from the page (from another page, or before the figure scrolls into view)
6
+ * has only the caption, and so do print without JavaScript and the
7
+ * standalone HTML export. A lab draws its default picture at build time and
8
+ * hands it to its island as children (`still-plot.ts` in the playground).
9
+ *
10
+ * `undrawnFigurePictures` reads rendered HTML: a `figure.keyed-figure` whose
11
+ * picture (`.keyed-figure__body`) holds an `<astro-island>` but nothing drawn
12
+ * (`svg`, `img`, `picture`, `canvas`, `video` or `table`) is reported.
13
+ * `compiler/astro.ts` runs it on every built page and warns; it is a
14
+ * warning, not an error, since the figure still works once hydrated.
15
+ */
16
+ /** The diagnostic a built page with an undrawn picture gets. */
17
+ interface UndrawnFigurePicture {
18
+ readonly code: 'lab-picture-undrawn';
19
+ /** The figure's anchor id, e.g. `fig-launch-angle-explorer`. */
20
+ readonly figure: string;
21
+ }
22
+ /** The figures in `html` whose picture is an island with nothing drawn. */
23
+ declare function undrawnFigurePictures(html: string): UndrawnFigurePicture[];
24
+ /** The build's warning for one page, naming each figure and what to do. */
25
+ declare function undrawnFigureWarning(page: string, figures: readonly UndrawnFigurePicture[]): string;
26
+
27
+ export { type UndrawnFigurePicture, undrawnFigurePictures, undrawnFigureWarning };
@@ -0,0 +1,49 @@
1
+ import { parse } from "parse5";
2
+ const DRAWN = /* @__PURE__ */ new Set(["svg", "img", "picture", "canvas", "video", "table"]);
3
+ function isElement(node) {
4
+ return "tagName" in node;
5
+ }
6
+ function attribute(node, name) {
7
+ return node.attrs.find((attr) => attr.name === name)?.value;
8
+ }
9
+ function hasClass(node, name) {
10
+ return (attribute(node, "class") ?? "").split(/\s+/u).includes(name);
11
+ }
12
+ function childrenOf(node) {
13
+ if (isElement(node) && node.tagName === "template") return [];
14
+ return "childNodes" in node ? node.childNodes : [];
15
+ }
16
+ function find(node, test) {
17
+ for (const child of childrenOf(node)) {
18
+ if (!isElement(child)) continue;
19
+ if (test(child)) return child;
20
+ const inner = find(child, test);
21
+ if (inner) return inner;
22
+ }
23
+ return void 0;
24
+ }
25
+ function undrawnFigurePictures(html) {
26
+ if (!html.includes("keyed-figure") || !html.includes("astro-island")) return [];
27
+ const found = [];
28
+ const visit = (node) => {
29
+ for (const child of childrenOf(node)) {
30
+ if (!isElement(child)) continue;
31
+ if (child.tagName === "figure" && hasClass(child, "keyed-figure")) {
32
+ const body = find(child, (el) => hasClass(el, "keyed-figure__body"));
33
+ if (body && find(body, (el) => el.tagName === "astro-island") && !find(body, (el) => DRAWN.has(el.tagName))) {
34
+ found.push({ code: "lab-picture-undrawn", figure: attribute(child, "id") ?? "" });
35
+ }
36
+ }
37
+ visit(child);
38
+ }
39
+ };
40
+ visit(parse(html));
41
+ return found;
42
+ }
43
+ function undrawnFigureWarning(page, figures) {
44
+ return `lab-picture-undrawn (rule V3): on rendered page "${page}", ` + figures.map(({ figure }) => `#${figure}`).join(", ") + " draw nothing until the browser runs the island, so a preview card, print without JavaScript and the HTML export show no picture. Draw the default picture at build time and pass it to the island as its children.";
45
+ }
46
+ export {
47
+ undrawnFigurePictures,
48
+ undrawnFigureWarning
49
+ };
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
2
2
 
3
3
  /** `validateKeyedFloats` for `<Figure>` — see `keyed-float-validate.ts`. */
4
4
 
5
- type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused' | 'fig-removed-prop';
5
+ type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused' | 'fig-removed-prop' | 'fig-unread-prop';
6
6
  interface FigureDiagnostic {
7
7
  readonly code: FigureDiagnosticCode;
8
8
  readonly severity: 'error' | 'warning' | 'info';
@@ -1,6 +1,6 @@
1
1
  import { MathGateDiagnostic } from './gate-math.js';
2
2
  import { NotationRegistryInput, AbbreviationDefinitionInput } from './types.js';
3
- import { a as CourseAbbreviations } from '../course-config-Dsnri9Tk.js';
3
+ import { h as CourseAbbreviations } from '../course-config-rRtME5UV.js';
4
4
  import '../rules/catalog.js';
5
5
  import '../rules/course-rules.js';
6
6
 
@@ -14,7 +14,7 @@ function findAbbreviationLikeTokens(text) {
14
14
  }
15
15
  return out.sort((a, b) => a.index - b.index);
16
16
  }
17
- const FIX = "Add it to an abbreviations-table entry (content/abbreviations/*.md), allowlist it in abbreviations.allow in course.config.ts if it is common, non-technical usage, or \u2014 if it names a math quantity \u2014 introduce it as \\mathrm{\u2026} (or \\operatorname{\u2026}) bound to a notation entry the reader can hover. See docs/authoring/notation.md#n14.";
17
+ const FIX = "Add it to an abbreviations-table entry (content/abbreviations/*.md), allowlist it in rules.N14.allow in the course config (course.config.ts, or the course.settings.json it reads) if it is common, non-technical usage, or \u2014 if it names a math quantity \u2014 introduce it as \\mathrm{\u2026} (or \\operatorname{\u2026}) bound to a notation entry the reader can hover. See docs/authoring/notation.md#n14.";
18
18
  function gateAbbreviations(input) {
19
19
  const { notationInput, abbreviations, courseAbbreviations } = input;
20
20
  const tableTokens = /* @__PURE__ */ new Set();
@@ -9,9 +9,10 @@ import '../rules/catalog.js';
9
9
  *
10
10
  * One loop over every enabled course rule's `forbid` patterns, matched against
11
11
  * the same "is this really prose" surface rule N9 uses — `proseOnly` blanks
12
- * code fences, inline code, `$…$` / `$$…$$`, `[[key]]` references, `import`
13
- * lines, JSX tags and MDX comments to spaces, so offsets still point at the
14
- * source. A match becomes a `MathGateDiagnostic` with the rule's own ID as its
12
+ * code fences, inline code, `$…$` / `$$…$$`, `[[key]]` references, link
13
+ * addresses, `import` lines, JSX tags and MDX comments to spaces, so offsets
14
+ * still point at the source — plus the text of each visible prop (`caption`,
15
+ * `short`, `title`, `label`) as a paragraph of its own. A match becomes a `MathGateDiagnostic` with the rule's own ID as its
15
16
  * code and a lesson `file` + `line`, which is what lets `{/* explico-allow X1:
16
17
  * reason *\/}` waive one place.
17
18
  *
@@ -1,4 +1,5 @@
1
1
  import { lessonLine, proseOnly } from "./gate-prose-notation.js";
2
+ import { propStrings } from "./gate-prop-text.js";
2
3
  const HEADING = /^[ \t]{0,3}(#{1,6})[ \t]+(.*)$/;
3
4
  function paragraphs(text) {
4
5
  const units = [];
@@ -32,14 +33,18 @@ function headings(text) {
32
33
  }
33
34
  return units;
34
35
  }
35
- function unitsFor(scope, prose) {
36
+ const VISIBLE_PROPS = /* @__PURE__ */ new Set(["caption", "short", "title", "label"]);
37
+ function visibleProps(body) {
38
+ return propStrings(body).filter((prop) => VISIBLE_PROPS.has(prop.name) && prop.value.trim() !== "").map((prop) => ({ text: prop.value, index: prop.offset }));
39
+ }
40
+ function unitsFor(scope, prose, body) {
36
41
  switch (scope) {
37
42
  case "prose":
38
- return [{ text: prose, index: 0 }];
43
+ return [{ text: prose, index: 0 }, ...visibleProps(body)];
39
44
  case "heading":
40
45
  return headings(prose);
41
46
  case "paragraph":
42
- return paragraphs(prose);
47
+ return [...paragraphs(prose), ...visibleProps(body)];
43
48
  }
44
49
  }
45
50
  function* matches(pattern, unit) {
@@ -59,7 +64,7 @@ function gateCourseRules(lessons, rules) {
59
64
  const cache = /* @__PURE__ */ new Map();
60
65
  for (const rule of active) {
61
66
  for (const pattern of rule.forbid) {
62
- if (!cache.has(pattern.scope)) cache.set(pattern.scope, unitsFor(pattern.scope, prose));
67
+ if (!cache.has(pattern.scope)) cache.set(pattern.scope, unitsFor(pattern.scope, prose, lesson.body));
63
68
  for (const unit of cache.get(pattern.scope)) {
64
69
  for (const [index, token] of matches(pattern, unit)) {
65
70
  diagnostics.push({
@@ -10,6 +10,8 @@ declare const HEADING_LENGTH_LIMIT = 65;
10
10
  */
11
11
  declare function gateHeadingLength(lessons: readonly NotationLessonInput[], options?: {
12
12
  readonly maxLength?: number;
13
+ /** A `[[key]]` term's label on a lesson's page, as a heading shows it. */
14
+ readonly labelFor?: (lessonId: string, key: string) => string | undefined;
13
15
  }): MathGateDiagnostic[];
14
16
 
15
17
  export { HEADING_LENGTH_LIMIT, gateHeadingLength };
@@ -1,5 +1,6 @@
1
1
  import { EQ_CAPTION_PATTERN, stripFencedCode } from "./equations.mjs";
2
2
  import { stripHeadingMarker } from "./rehype-heading-anchors.mjs";
3
+ import { headingDisplayText } from "./sections.mjs";
3
4
  import { lessonLine } from "./gate-prose-notation.js";
4
5
  const HEADING_LENGTH_LIMIT = 65;
5
6
  const HEADING_LINE = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm;
@@ -20,13 +21,13 @@ function readAttribute(attributesSource, name) {
20
21
  function overLimitMessage(kind, text, limit) {
21
22
  return `${kind} ${JSON.stringify(text)} is ${text.length} characters, over the ${limit}-character limit (rule S1, rules.S1.maxLength in the course settings). Shorten it and move the rest into the paragraph or caption that follows.`;
22
23
  }
23
- function headingFindings(text, limit) {
24
+ function headingFindings(text, limit, labelFor) {
24
25
  const out = [];
25
26
  const re = new RegExp(HEADING_LINE.source, HEADING_LINE.flags);
26
27
  let match;
27
28
  while (match = re.exec(text)) {
28
29
  const level = match[1]?.length ?? 1;
29
- const title = stripHeadingMarker(match[2] ?? "").trim();
30
+ const title = headingDisplayText(stripHeadingMarker(match[2] ?? ""), { labelFor });
30
31
  if (title.length > limit) {
31
32
  out.push({
32
33
  index: match.index,
@@ -71,9 +72,9 @@ function equationCaptionFindings(text, limit) {
71
72
  }
72
73
  return out;
73
74
  }
74
- function findingsIn(text, limit) {
75
+ function findingsIn(text, limit, labelFor) {
75
76
  return [
76
- ...headingFindings(text, limit),
77
+ ...headingFindings(text, limit, labelFor),
77
78
  ...floatTitleFindings(text, limit),
78
79
  ...equationCaptionFindings(text, limit)
79
80
  ].sort((a, b) => a.index - b.index);
@@ -83,7 +84,8 @@ function gateHeadingLength(lessons, options = {}) {
83
84
  const limit = options.maxLength ?? HEADING_LENGTH_LIMIT;
84
85
  for (const lesson of lessons) {
85
86
  const text = stripFencedCode(lesson.body);
86
- for (const finding of findingsIn(text, limit)) {
87
+ const labelFor = options.labelFor ? (key) => options.labelFor?.(lesson.lessonId, key) : void 0;
88
+ for (const finding of findingsIn(text, limit, labelFor)) {
87
89
  diagnostics.push({
88
90
  context: "structure",
89
91
  code: "title-length",
@@ -67,6 +67,31 @@ function skipFindings(blocks) {
67
67
  }
68
68
  return out;
69
69
  }
70
+ const HOSTS = ["Memo", "ShortNote"];
71
+ function nestedFindings(body) {
72
+ const blank = (match) => match.replace(/[^\n]/g, " ");
73
+ const text = body.replace(/```[\s\S]*?```/g, blank).replace(/\{\/\*[\s\S]*?\*\/\}/g, blank);
74
+ const out = [];
75
+ for (const host of HOSTS) {
76
+ const opening = new RegExp(`<${host}\\b(?:[^>"']|"[^"]*"|'[^']*')*(?<!/)>`, "g");
77
+ for (const open of text.matchAll(opening)) {
78
+ const start = (open.index ?? 0) + open[0].length;
79
+ const end = text.indexOf(`</${host}>`, start);
80
+ if (end === -1) continue;
81
+ for (const inner of text.slice(start, end).matchAll(/<(AssessmentSet|CompactExamples)\b/g)) {
82
+ const name = inner[1];
83
+ const what = name === "CompactExamples" ? "set of worked examples" : "knowledge check";
84
+ out.push({
85
+ code: "heading-nested",
86
+ index: start + (inner.index ?? 0),
87
+ token: `<${name}>`,
88
+ message: `\`<${name}>\` sits inside a \`<${host}>\`, ${TITLED_COMPONENTS[name].what}, ${host === "Memo" ? "under the memo\u2019s own `<h6>`" : "in the middle of the note"}: the heading order jumps, and "On this page" never lists it (rule S6). A ${what} is a section of the lesson: put it at the top level, after the ${host === "Memo" ? "memo" : "paragraph"}.`
89
+ });
90
+ }
91
+ }
92
+ }
93
+ return out;
94
+ }
70
95
  function adjacentFindings(blocks, lesson) {
71
96
  const out = [];
72
97
  blocks.forEach((block, position) => {
@@ -92,6 +117,7 @@ function gateHeadingStructure(lessons, options = {}) {
92
117
  const blocks = blocksOf(lesson.body);
93
118
  const findings = [
94
119
  ...levels ? skipFindings(blocks) : [],
120
+ ...levels ? nestedFindings(lesson.body) : [],
95
121
  ...adjacency ? adjacentFindings(blocks, lesson) : []
96
122
  ].sort((a, b) => a.index - b.index);
97
123
  for (const finding of findings) {
@@ -0,0 +1,35 @@
1
+ import { ScopeEntry, AssessmentLike, LessonAssessmentLink, MathGateDiagnostic } from './gate-math.js';
2
+ import { ResolvedGloss } from './gloss.js';
3
+ import { NotationLessonInput, DerivedUse, IndexedSymbol } from './types.js';
4
+
5
+ /** What the check needs of an entry besides its glyph-table row. */
6
+ interface IndexEntryFacts extends ScopeEntry {
7
+ readonly range?: string;
8
+ readonly formula?: string;
9
+ readonly glosses?: readonly ResolvedGloss[];
10
+ readonly source?: {
11
+ readonly file: string;
12
+ };
13
+ }
14
+ /**
15
+ * Run rule N23 over every lesson (and non-chapter page) body, every notation
16
+ * formula and every assessment, and collect the derived cards each body
17
+ * uses. `entries` holds every shared entry by key; a lesson's own come from
18
+ * its `localDefinitions`. The index-set check (`index-range`) reads lesson
19
+ * bodies only: a letter's set is stated on its card or on the page.
20
+ */
21
+ declare function gateIndices(lessons: readonly NotationLessonInput[], entries: ReadonlyMap<string, IndexEntryFacts>, extras?: {
22
+ readonly assessments?: readonly AssessmentLike[];
23
+ readonly lessonAssessments?: readonly LessonAssessmentLink[];
24
+ }): {
25
+ diagnostics: MathGateDiagnostic[];
26
+ derivedByLesson: Map<string, DerivedUse[]>;
27
+ };
28
+ /**
29
+ * Rule N23's findings in one math source rendered outside a lesson body — a
30
+ * lab's or a diagram's, which fail the build as they render — as one line
31
+ * each: the form, what explains none of it, and the fix.
32
+ */
33
+ declare function indexFindings(indices: readonly IndexedSymbol[]): string[];
34
+
35
+ export { DerivedUse, type IndexEntryFacts, gateIndices, indexFindings };