explico 0.7.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +14 -1
  15. package/dist/compiler/logging.js +6 -2
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -1,32 +1,10 @@
1
+ import { RenderedLabMath } from './lab-math-slot.js';
2
+ export { slotPlaceholder } from './lab-math-slot.js';
3
+
1
4
  interface LabMathScopeDefinition {
2
5
  readonly key: string;
3
6
  readonly notation: string;
4
7
  }
5
- interface RenderedLabMath {
6
- /** KaTeX HTML+MathML with `data-notation-key` and `data-lab-slot` spans. */
7
- readonly html: string;
8
- /** Slot names, in template order, the island must supply at runtime. */
9
- readonly slots: readonly string[];
10
- /** Notation keys the template resolved to, for diagnostics. */
11
- readonly keys: readonly string[];
12
- /**
13
- * Plain, copyable LaTeX — every explained glyph resolved to its bare
14
- * symbol, every `\slot{name}{…}` replaced by `slotPlaceholder(name)` — so
15
- * the island can substitute each slot's *current* live value in and keep
16
- * `[data-notation-formula]`'s `data-latex` accurate as the reader drags a
17
- * control, the same "click the equation to copy it" gesture a static
18
- * lesson or glossary equation gets (`NotationLayer.astro`).
19
- */
20
- readonly plainLatexTemplate: string;
21
- }
22
- /**
23
- * A slot's placeholder inside `plainLatexTemplate` — distinct from the
24
- * numeric sentinel `sentinelFor` below uses internally (that one only has to
25
- * survive a single notation-binding pass at build time; this one is the
26
- * stable, name-keyed token an island substitutes with a live value at
27
- * runtime, so it's exported for that purpose).
28
- */
29
- declare function slotPlaceholder(name: string): string;
30
8
  /**
31
9
  * Compile a developer-authored lab math template the same way lesson `$$`
32
10
  * math is compiled: every identifier must resolve to a notation key in the
@@ -35,4 +13,4 @@ declare function slotPlaceholder(name: string): string;
35
13
  */
36
14
  declare function renderLabMath(template: string, scope: readonly LabMathScopeDefinition[]): RenderedLabMath;
37
15
 
38
- export { type LabMathScopeDefinition, type RenderedLabMath, renderLabMath, slotPlaceholder };
16
+ export { type LabMathScopeDefinition, RenderedLabMath, renderLabMath };
@@ -1,9 +1,7 @@
1
- import katex from "katex";
1
+ import katex from "./katex-delimiters.mjs";
2
2
  import { resolveMathGlyphs, stripNotationMarkers } from "./math-glyphs.mjs";
3
3
  import { createNotationKatexOptions } from "./katex-options.mjs";
4
- function slotPlaceholder(name) {
5
- return `\u27E6${name}\u27E7`;
6
- }
4
+ import { slotPlaceholder } from "./lab-math-slot.js";
7
5
  const SLOT_PATTERN = /\\slot\{([a-z][a-z0-9]*)\}\{((?:[^{}]|\{[^{}]*\})*)\}/g;
8
6
  const sentinelFor = (index) => `909909${index}.909909`;
9
7
  function renderLabMath(template, scope) {
@@ -27,8 +25,10 @@ function renderLabMath(template, scope) {
27
25
  }
28
26
  if (result.unresolved.length > 0) {
29
27
  const list = result.unresolved.map(({ token }) => `"${token}"`).join(", ");
28
+ const styled = result.unresolved.filter(({ font }) => font !== void 0);
29
+ const fontHint = styled.length === 0 ? "" : ` ${styled.map(({ token }) => token).join(", ")} ${styled.length === 1 ? "is a different symbol" : "are different symbols"} from the plain letter and need${styled.length === 1 ? "s" : ""} an entry of ${styled.length === 1 ? "its" : "their"} own.`;
30
30
  throw new Error(
31
- `Lab math uses undefined notation ${list}. Every symbol must resolve to a notation entry in this page's glyph scope (a [[key]] reference or notation.local).
31
+ `Lab math uses undefined notation ${list}. Every symbol must resolve to a notation entry in this page's glyph scope (a [[key]] reference or notation.local).${fontHint}
32
32
  template: ${template.trim()}`
33
33
  );
34
34
  }
@@ -0,0 +1,27 @@
1
+ import { MathGateDiagnostic } from './gate-math.js';
2
+ import './types.js';
3
+
4
+ /**
5
+ * `[[sec-…]]` references (docs/plans/titles-and-containers.md Part 1): every
6
+ * `[[sec-slug]]` and `[[lesson/slug#sec-slug]]` must name a top-level heading
7
+ * of that lesson — `sec-ref-resolves`, an error owned by the `keyed-floats`
8
+ * invariant, the same promise `[[eq-…]]` and `[[fig-…]]` make.
9
+ *
10
+ * The finding goes into `manifest.diagnostics.math` with context `structure`,
11
+ * like S1 and S2 (see `MathGateDiagnostic`), so every reporter that already
12
+ * reads that array fails the build on it without learning a new one.
13
+ */
14
+
15
+ interface SectionRefLessonInput {
16
+ /** Lesson slug, e.g. `motion/position-and-coordinates`. */
17
+ readonly slug: string;
18
+ readonly file: string;
19
+ readonly body: string;
20
+ readonly bodyLine?: number;
21
+ /** The anchors of the lesson's top-level headings (`manifest.lessons[].headings`). */
22
+ readonly headingSlugs: readonly string[];
23
+ }
24
+ /** One `sec-ref-resolves` error per `[[sec-…]]` that names no heading. */
25
+ declare function validateSectionRefs(lessons: readonly SectionRefLessonInput[]): MathGateDiagnostic[];
26
+
27
+ export { type SectionRefLessonInput, validateSectionRefs };
@@ -0,0 +1,41 @@
1
+ import { lessonLine } from "./gate-prose-notation.js";
2
+ import { parseSectionRef } from "./sections.mjs";
3
+ const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
4
+ function withoutCode(text) {
5
+ const blank = (block) => block.replace(/[^\n]/g, " ");
6
+ return text.replace(/```[\s\S]*?```/g, blank).replace(/~~~[\s\S]*?~~~/g, blank).replace(/`[^`\n]*`/g, blank);
7
+ }
8
+ function unresolved(ref, lessonExists) {
9
+ const written = ref.lesson ? `[[${ref.lesson}#sec-${ref.key}]]` : `[[sec-${ref.key}]]`;
10
+ if (ref.lesson && !lessonExists) {
11
+ return `${written} names lesson ${ref.lesson}, which does not exist.`;
12
+ }
13
+ return `${written} names no heading: ${ref.lesson ? `lesson ${ref.lesson}` : "this lesson"} has no top-level heading anchored #${ref.key}. A heading's anchor is its text in lowercase with spaces as hyphens and punctuation dropped: \`## Two ways to name the same point\` is \`[[sec-two-ways-to-name-the-same-point]]\`.`;
14
+ }
15
+ function validateSectionRefs(lessons) {
16
+ const headingsByLesson = new Map(
17
+ lessons.map((lesson) => [lesson.slug, new Set(lesson.headingSlugs)])
18
+ );
19
+ const diagnostics = [];
20
+ for (const lesson of lessons) {
21
+ for (const match of withoutCode(lesson.body).matchAll(BRACKET_REF)) {
22
+ const ref = parseSectionRef(match[1] ?? "");
23
+ if (!ref) continue;
24
+ const headings = headingsByLesson.get(ref.lesson ?? lesson.slug);
25
+ if (headings?.has(ref.key)) continue;
26
+ diagnostics.push({
27
+ context: "structure",
28
+ code: "sec-ref-resolves",
29
+ severity: "error",
30
+ file: lesson.file,
31
+ line: lessonLine(lesson, match.index ?? 0),
32
+ token: match[0],
33
+ message: unresolved(ref, headings !== void 0)
34
+ });
35
+ }
36
+ }
37
+ return diagnostics;
38
+ }
39
+ export {
40
+ validateSectionRefs
41
+ };
@@ -0,0 +1,97 @@
1
+ export function parseSectionRef(raw: any): {
2
+ lesson: string;
3
+ key: string;
4
+ } | {
5
+ key: string;
6
+ lesson?: undefined;
7
+ } | undefined;
8
+ /**
9
+ * The number each heading takes, from its level alone: `##` counts within the
10
+ * chapter, `###` within its `##`, and so on, each deeper counter restarting
11
+ * when a shallower heading appears. A level a heading skipped counts as 0
12
+ * (`##` then `####` is `…1.0.1`), the way LaTeX numbers a skipped level. An
13
+ * `#` in a lesson body is never numbered and resets nothing. Headings deeper
14
+ * than `depth`, or any heading when there is no `prefix`, get `undefined`.
15
+ * `unnumbered[i]` marks heading `i` as outside the outline (a meta section):
16
+ * it and the deeper headings under it get `undefined` and move no counter.
17
+ */
18
+ export function sectionNumbers(levels: any, { prefix, depth, unnumbered }?: {
19
+ depth?: number | undefined;
20
+ unnumbered?: never[] | undefined;
21
+ }): any;
22
+ /**
23
+ * The text a heading's source renders as, near enough to slug it the way
24
+ * Astro would: markup goes, the words stay. Inline code and math keep their
25
+ * source (math renders as KaTeX, whose text is no use as a slug); a notation
26
+ * term keeps its key; a link keeps its text; an image, a citation and an MDX
27
+ * expression contribute nothing; the authoring marker is dropped.
28
+ */
29
+ export function headingPlainText(source: any): string;
30
+ /**
31
+ * Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
32
+ * run over their plain text: the same slugger with the same duplicate
33
+ * counters (`foo`, `foo-1`) the page would otherwise get.
34
+ */
35
+ export function slugHeadings(texts: any): any;
36
+ /**
37
+ * Every top-level heading of a lesson body in document order, as the
38
+ * manifest records it: `{ level, text, slug, number? }`. `prefix` is the
39
+ * chapter's `‹part›.‹chapter›`; without one nothing is numbered.
40
+ */
41
+ export function scanSectionHeadings(markdown: any, options?: {}): {
42
+ number?: any;
43
+ level: number;
44
+ text: string;
45
+ slug: any;
46
+ }[];
47
+ /**
48
+ * The top-level blocks of a lesson body in document order, as the heading
49
+ * rules see them (S6, S7 — `gate-heading-structure.ts`): each outline heading
50
+ * `scanSectionHeadings` numbers, each JSX element by name, and every other
51
+ * line of content. An MDX comment and an `import` / `export` line render
52
+ * nothing, so they are not blocks. `index` is the offset in `markdown`; a
53
+ * heading's `text` is what the manifest records for it.
54
+ *
55
+ * @returns {Array<
56
+ * | { kind: 'heading', level: number, text: string, index: number }
57
+ * | { kind: 'element', name: string, index: number }
58
+ * | { kind: 'content', index: number }
59
+ * >}
60
+ */
61
+ export function scanOutlineBlocks(markdown: any): Array<{
62
+ kind: "heading";
63
+ level: number;
64
+ text: string;
65
+ index: number;
66
+ } | {
67
+ kind: "element";
68
+ name: string;
69
+ index: number;
70
+ } | {
71
+ kind: "content";
72
+ index: number;
73
+ }>;
74
+ /**
75
+ * The top-level blocks named in `names` — components that draw a heading of
76
+ * their own where their tag stands, a knowledge check's `<AssessmentSet />`
77
+ * and a worked-example set's `<CompactExamples>` — in document order: each
78
+ * with its tag `name`, the literal `id` its tag sets (if any), and `heading`,
79
+ * the position in `scanSectionHeadings`' list of the outline heading it
80
+ * follows (`-1` before the first). The manifest reads this to place the
81
+ * block's heading in "On this page".
82
+ *
83
+ * @returns {Array<{ name: string, heading: number, id?: string }>}
84
+ */
85
+ export function scanTitledBlocks(markdown: any, names: any): Array<{
86
+ name: string;
87
+ heading: number;
88
+ id?: string;
89
+ }>;
90
+ /**
91
+ * The mdast counterpart of `scanSectionHeadings`: the lesson root's own
92
+ * `heading` children, with the number each takes. Slugs are not computed here
93
+ * — the render path takes them from the manifest.
94
+ */
95
+ export function collectSectionHeadings(tree: any, options?: {}): any;
96
+ /** The deepest heading level numbered when a course sets nothing (`####`). */
97
+ export const DEFAULT_SECTION_DEPTH: 4;
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Outline numbering (docs/plans/titles-and-containers.md Part 1): every `##`
3
+ * to `######` heading at the top level of a lesson is numbered
4
+ * `‹part›.‹chapter›.‹section›[.‹subsection›…]`, down to the course's
5
+ * `numbering.sections.depth`, and is citable as `[[sec-‹slug›]]`. A
6
+ * `[card=meta]` section is logistics, not a step of the lesson, so it and
7
+ * every heading inside it stay unnumbered — LaTeX's `\section*`, and what
8
+ * References already is — and take no number from the sections after it.
9
+ *
10
+ * Numbers are for a reader to say and see; the anchor stays the heading's
11
+ * slug (`#two-ways-to-name-the-same-point`), never the number.
12
+ *
13
+ * Two walks, same contract as `equations.mjs` and `keyed-jsx-floats.mjs`:
14
+ * `scanSectionHeadings` reads raw Markdown for the manifest, and
15
+ * `collectSectionHeadings` walks the mdast for `remark-notation.mjs`. Both see
16
+ * the same headings — top level only; a heading inside a component (`<Memo>`,
17
+ * `<CompactExample>`, …) is part of that component, not of the outline — and
18
+ * number them with the one `sectionNumbers` below, so the two agree whenever
19
+ * they agree on the level sequence. `remark-notation.mjs` checks exactly that
20
+ * before it trusts the manifest's slugs and numbers.
21
+ *
22
+ * The manifest is where a heading's slug is decided: `headingPlainText` reads
23
+ * the heading's source text and `slugHeadings` slugs it with Astro's own
24
+ * `rehypeHeadingIds`, so a plain-text heading keeps the id it always had. The
25
+ * render path then gives each heading that id before Astro's pass would have
26
+ * picked one, so `[[sec-…]]`, the page, and the alias map cannot disagree.
27
+ */
28
+
29
+ import { rehypeHeadingIds } from '@astrojs/markdown-remark';
30
+ import { headingCardTier, stripHeadingMarker } from './rehype-heading-anchors.mjs';
31
+
32
+ /** The deepest heading level numbered when a course sets nothing (`####`). */
33
+ export const DEFAULT_SECTION_DEPTH = 4;
34
+
35
+ /**
36
+ * `[[sec-slug]]`, or `[[lesson/slug#sec-slug]]` across pages. The slug part
37
+ * is whatever the heading's anchor is: github-slugger keeps letters, digits,
38
+ * `_` and `-` from any script, and can double a hyphen.
39
+ */
40
+ const SECTION_REF =
41
+ /^(?:([a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*)#)?sec-([\p{L}\p{N}_-]+)$/u;
42
+
43
+ export function parseSectionRef(raw) {
44
+ const match = SECTION_REF.exec(String(raw).trim());
45
+ if (!match) return undefined;
46
+ return match[1] ? { lesson: match[1], key: match[2] } : { key: match[2] };
47
+ }
48
+
49
+ /**
50
+ * The number each heading takes, from its level alone: `##` counts within the
51
+ * chapter, `###` within its `##`, and so on, each deeper counter restarting
52
+ * when a shallower heading appears. A level a heading skipped counts as 0
53
+ * (`##` then `####` is `…1.0.1`), the way LaTeX numbers a skipped level. An
54
+ * `#` in a lesson body is never numbered and resets nothing. Headings deeper
55
+ * than `depth`, or any heading when there is no `prefix`, get `undefined`.
56
+ * `unnumbered[i]` marks heading `i` as outside the outline (a meta section):
57
+ * it and the deeper headings under it get `undefined` and move no counter.
58
+ */
59
+ export function sectionNumbers(
60
+ levels,
61
+ { prefix, depth = DEFAULT_SECTION_DEPTH, unnumbered = [] } = {},
62
+ ) {
63
+ const counters = [0, 0, 0, 0, 0, 0, 0];
64
+ // A heading deeper than this level sits inside an unnumbered section.
65
+ let outside = 7;
66
+ return levels.map((level, index) => {
67
+ if (level < 2) return undefined;
68
+ if (level <= outside) outside = 7;
69
+ if (unnumbered[index]) outside = level;
70
+ if (outside < 7) return undefined;
71
+ counters[level] += 1;
72
+ for (let deeper = level + 1; deeper <= 6; deeper++) counters[deeper] = 0;
73
+ if (!prefix || level > depth) return undefined;
74
+ return [prefix, ...counters.slice(2, level + 1)].join('.');
75
+ });
76
+ }
77
+
78
+ // --- raw Markdown ------------------------------------------------------------
79
+
80
+ const blank = (block) => block.replace(/[^\n]/g, ' ');
81
+
82
+ // An MDX attribute may be a quoted string, a brace expression (one level of
83
+ // nesting), or unquoted source — the same breadth `stripJsxComponents` allows.
84
+ const ATTRIBUTES = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'])*`;
85
+ const JSX_BLOCK = new RegExp(
86
+ String.raw`<([A-Za-z][\w.:-]*)(?:\s${ATTRIBUTES})?>[\s\S]*?<\/\1\s*>`,
87
+ 'g',
88
+ );
89
+
90
+ /**
91
+ * The body with everything that is not outline blanked, newlines kept: fenced
92
+ * code, MDX comments, and every JSX element's children (a heading in there is
93
+ * not an outline heading). A self-closing element has no children to blank.
94
+ * With `markers`, a code block keeps its opening backtick or tilde and an
95
+ * element its `<Name`, so `scanOutlineBlocks` can see that something stands
96
+ * there; neither can start a heading line, so both forms find the same
97
+ * headings.
98
+ */
99
+ function outlineSource(markdown, { markers = false } = {}) {
100
+ const fence = markers ? (block) => block[0] + blank(block.slice(1)) : blank;
101
+ const element = markers
102
+ ? (block, name) => `<${name}${blank(block.slice(name.length + 1))}`
103
+ : blank;
104
+ return String(markdown)
105
+ .replace(/```[\s\S]*?```/g, fence)
106
+ .replace(/~~~[\s\S]*?~~~/g, fence)
107
+ .replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
108
+ .replace(JSX_BLOCK, element);
109
+ }
110
+
111
+ const ATX_HEADING = /^[ \t]{0,3}(#{1,6})(?:[ \t]+([^\n]*?))?(?:[ \t]+#+)?[ \t]*$/gm;
112
+
113
+ const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
114
+
115
+ /**
116
+ * The text a heading's source renders as, near enough to slug it the way
117
+ * Astro would: markup goes, the words stay. Inline code and math keep their
118
+ * source (math renders as KaTeX, whose text is no use as a slug); a notation
119
+ * term keeps its key; a link keeps its text; an image, a citation and an MDX
120
+ * expression contribute nothing; the authoring marker is dropped.
121
+ */
122
+ export function headingPlainText(source) {
123
+ const literals = [];
124
+ const keep = (value) => `\u0000${literals.push(value) - 1}\u0000`;
125
+ let text = stripHeadingMarker(source ?? '')
126
+ .replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_, _ticks, code) => keep(code.trim()))
127
+ .replace(/\$([^$\n]+)\$/g, (_, latex) => keep(latex.trim()))
128
+ .replace(/\[\[\s*([^\][]+?)\s*\]\]/g, (_, key) => keep(key))
129
+ .replace(/\[@[^\]]*\]/g, '')
130
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
131
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
132
+ .replace(/\[([^\]]*)\]\[[^\]]*\]/g, '$1')
133
+ .replace(/<(https?:\/\/[^>\s]+)>/g, (_, url) => keep(url))
134
+ .replace(/\{[^{}]*\}/g, '')
135
+ .replace(/<\/?[A-Za-z][^>]*>/g, '')
136
+ .replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2')
137
+ .replace(/(^|[^\w*])\*(?=\S)([\s\S]*?\S)\*(?![\w*])/g, '$1$2')
138
+ .replace(/(^|[^\w])_(?=\S)([\s\S]*?\S)_(?!\w)/g, '$1$2')
139
+ .replace(/~~(?=\S)([\s\S]*?\S)~~/g, '$1')
140
+ .replace(/\\([!-/:-@[-`{-~])/g, '$1')
141
+ .replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, name) => {
142
+ if (name[0] !== '#') return ENTITIES[name.toLowerCase()] ?? entity;
143
+ const code = name[1] === 'x' || name[1] === 'X'
144
+ ? Number.parseInt(name.slice(2), 16)
145
+ : Number.parseInt(name.slice(1), 10);
146
+ return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
147
+ });
148
+ text = text.replace(/\u0000(\d+)\u0000/g, (_, index) => literals[Number(index)]);
149
+ return text.replace(/\s+/g, ' ').trim();
150
+ }
151
+
152
+ /**
153
+ * Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
154
+ * run over their plain text: the same slugger with the same duplicate
155
+ * counters (`foo`, `foo-1`) the page would otherwise get.
156
+ */
157
+ export function slugHeadings(texts) {
158
+ const tree = {
159
+ type: 'root',
160
+ children: texts.map((value) => ({
161
+ type: 'element',
162
+ tagName: 'h2',
163
+ properties: {},
164
+ children: [{ type: 'text', value }],
165
+ })),
166
+ };
167
+ rehypeHeadingIds()(tree, { history: ['headings.mdx'], data: {} });
168
+ return tree.children.map((node) => String(node.properties.id));
169
+ }
170
+
171
+ /**
172
+ * Every top-level heading of a lesson body in document order, as the
173
+ * manifest records it: `{ level, text, slug, number? }`. `prefix` is the
174
+ * chapter's `‹part›.‹chapter›`; without one nothing is numbered.
175
+ */
176
+ export function scanSectionHeadings(markdown, options = {}) {
177
+ const found = [];
178
+ for (const match of outlineSource(markdown).matchAll(ATX_HEADING)) {
179
+ found.push({
180
+ level: match[1].length,
181
+ text: headingPlainText(match[2] ?? ''),
182
+ meta: headingCardTier(match[2] ?? '') === 'meta',
183
+ });
184
+ }
185
+ const slugs = slugHeadings(found.map((heading) => heading.text));
186
+ const numbers = sectionNumbers(
187
+ found.map((heading) => heading.level),
188
+ { ...options, unnumbered: found.map((heading) => heading.meta) },
189
+ );
190
+ return found.map((heading, index) => ({
191
+ level: heading.level,
192
+ text: heading.text,
193
+ slug: slugs[index],
194
+ ...(numbers[index] === undefined ? {} : { number: numbers[index] }),
195
+ }));
196
+ }
197
+
198
+ const ELEMENT_START = /^[ \t]*<([A-Za-z][\w.:-]*)/;
199
+ const ESM_LINE = /^(?:import|export)\s/;
200
+
201
+ /**
202
+ * The top-level blocks of a lesson body in document order, as the heading
203
+ * rules see them (S6, S7 — `gate-heading-structure.ts`): each outline heading
204
+ * `scanSectionHeadings` numbers, each JSX element by name, and every other
205
+ * line of content. An MDX comment and an `import` / `export` line render
206
+ * nothing, so they are not blocks. `index` is the offset in `markdown`; a
207
+ * heading's `text` is what the manifest records for it.
208
+ *
209
+ * @returns {Array<
210
+ * | { kind: 'heading', level: number, text: string, index: number }
211
+ * | { kind: 'element', name: string, index: number }
212
+ * | { kind: 'content', index: number }
213
+ * >}
214
+ */
215
+ export function scanOutlineBlocks(markdown) {
216
+ // Same length, same lines: headings come from the form `scanSectionHeadings`
217
+ // reads, everything else from the one that keeps its markers.
218
+ const plain = outlineSource(markdown).split('\n');
219
+ const marked = outlineSource(markdown, { markers: true }).split('\n');
220
+ const heading = new RegExp(ATX_HEADING.source);
221
+ const blocks = [];
222
+ let index = 0;
223
+ marked.forEach((line, row) => {
224
+ const at = index;
225
+ index += line.length + 1;
226
+ const match = heading.exec(plain[row]);
227
+ if (match) {
228
+ blocks.push({
229
+ kind: 'heading',
230
+ level: match[1].length,
231
+ text: headingPlainText(match[2] ?? ''),
232
+ index: at,
233
+ });
234
+ } else if (line.trim() !== '' && !ESM_LINE.test(line)) {
235
+ const element = ELEMENT_START.exec(line);
236
+ blocks.push(element ? { kind: 'element', name: element[1], index: at } : { kind: 'content', index: at });
237
+ }
238
+ });
239
+ return blocks;
240
+ }
241
+
242
+ const OPENING_TAG = new RegExp(String.raw`^[ \t]*<[A-Za-z][\w.:-]*(${ATTRIBUTES})\/?>`);
243
+ const ID_ATTRIBUTE = /(?:^|\s)id\s*=\s*(?:"([^"]*)"|'([^']*)')/;
244
+
245
+ /**
246
+ * The top-level blocks named in `names` — components that draw a heading of
247
+ * their own where their tag stands, a knowledge check's `<AssessmentSet />`
248
+ * and a worked-example set's `<CompactExamples>` — in document order: each
249
+ * with its tag `name`, the literal `id` its tag sets (if any), and `heading`,
250
+ * the position in `scanSectionHeadings`' list of the outline heading it
251
+ * follows (`-1` before the first). The manifest reads this to place the
252
+ * block's heading in "On this page".
253
+ *
254
+ * @returns {Array<{ name: string, heading: number, id?: string }>}
255
+ */
256
+ export function scanTitledBlocks(markdown, names) {
257
+ const wanted = new Set(names);
258
+ const source = String(markdown);
259
+ const blocks = [];
260
+ let heading = -1;
261
+ for (const block of scanOutlineBlocks(source)) {
262
+ if (block.kind === 'heading') {
263
+ heading += 1;
264
+ } else if (block.kind === 'element' && wanted.has(block.name)) {
265
+ const attributes = OPENING_TAG.exec(source.slice(block.index))?.[1] ?? '';
266
+ const id = ID_ATTRIBUTE.exec(attributes);
267
+ blocks.push({
268
+ name: block.name,
269
+ heading,
270
+ ...(id ? { id: id[1] ?? id[2] } : {}),
271
+ });
272
+ }
273
+ }
274
+ return blocks;
275
+ }
276
+
277
+ // --- mdast -------------------------------------------------------------------
278
+
279
+ /** A node's literal text, markers included (`[card=meta]` is plain text here). */
280
+ function mdastText(node) {
281
+ if (typeof node?.value === 'string') return node.value;
282
+ return Array.isArray(node?.children) ? node.children.map(mdastText).join('') : '';
283
+ }
284
+
285
+ /**
286
+ * The mdast counterpart of `scanSectionHeadings`: the lesson root's own
287
+ * `heading` children, with the number each takes. Slugs are not computed here
288
+ * — the render path takes them from the manifest.
289
+ */
290
+ export function collectSectionHeadings(tree, options = {}) {
291
+ const nodes = (Array.isArray(tree?.children) ? tree.children : []).filter(
292
+ (node) => node?.type === 'heading',
293
+ );
294
+ const numbers = sectionNumbers(
295
+ nodes.map((node) => node.depth),
296
+ {
297
+ ...options,
298
+ unnumbered: nodes.map((node) => headingCardTier(mdastText(node)) === 'meta'),
299
+ },
300
+ );
301
+ return nodes.map((node, index) => ({
302
+ node,
303
+ level: node.depth,
304
+ ...(numbers[index] === undefined ? {} : { number: numbers[index] }),
305
+ }));
306
+ }
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
2
2
 
3
3
  /** `validateKeyedFloats` for `<Table>` — see `keyed-float-validate.ts`. */
4
4
 
5
- type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused';
5
+ type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused' | 'tab-removed-prop';
6
6
  interface TableDiagnostic {
7
7
  readonly code: TableDiagnosticCode;
8
8
  readonly severity: 'error' | 'warning' | 'info';
@@ -70,14 +70,37 @@ interface LocalNotationDefinitionInput {
70
70
  readonly glosses: readonly NotationGlossEntry[];
71
71
  readonly source: SourceSpan;
72
72
  }
73
+ /**
74
+ * One `notation.suspend` item: a decoration (`'`, `\hat`, …) this page says
75
+ * is not an operator (rule N22). Every decorated form of it on the page then
76
+ * needs its own entry.
77
+ */
78
+ interface NotationSuspension {
79
+ /** A canonical name from `DECORATION_NAMES` (`math-glyphs.mjs`). */
80
+ readonly operator: string;
81
+ /** Why; a suspension without one is an `operator-suspension-reason` warning. */
82
+ readonly reason?: string;
83
+ }
73
84
  interface NotationLessonInput {
74
85
  readonly lessonId: string;
75
86
  readonly status: EditorialStatus;
76
87
  readonly localDefinitions: readonly LocalNotationDefinitionInput[];
88
+ /**
89
+ * Decorations this page suspends (`notation.suspend`, rule N22). Optional
90
+ * so a hand-built input (a test, an adapter) stays valid.
91
+ */
92
+ readonly suspended?: readonly NotationSuspension[];
77
93
  /** References harvested from lesson prose and equations, excluding definitions. */
78
94
  readonly references: readonly NotationReferenceInput[];
79
95
  /** Original lesson body, retained for lossless loader adapters. */
80
96
  readonly body: string;
97
+ /**
98
+ * 1-based line in `source.file` where `body` starts. Gates scan the body, so
99
+ * every finding's index is body-relative; this is what turns one into a real
100
+ * location in the file. Optional so a hand-built input (a test, an adapter)
101
+ * stays valid — it then behaves as 1, the pre-0.7.1 behavior.
102
+ */
103
+ readonly bodyLine?: number;
81
104
  readonly source: SourceSpan;
82
105
  }
83
106
  interface NotationRegistryInput {
@@ -186,4 +209,4 @@ interface NotationRegistry {
186
209
  readonly diagnostics: readonly NotationDiagnostic[];
187
210
  }
188
211
 
189
- export type { AbbreviationDefinitionInput, DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
212
+ export type { AbbreviationDefinitionInput, DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, NotationSuspension, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Units written out as English words — "2 meters per second squared",
3
+ * "30 degrees", "$x = 3$ meters" — for rule N9's `spelled-unit` check
4
+ * (`gate-prose-notation.ts`). A unit is an upright symbol inside `$…$`
5
+ * (`$2\ \text{m/s}^2$`, `$30^{\circ}$`, `$x = 3\ \text{m}$`), so a spelled-out
6
+ * unit that quantifies something is the same mistake as `45 m/s` in plain
7
+ * text.
8
+ *
9
+ * A unit word on its own is not flagged — "measured in meters" names the
10
+ * unit, it does not state a quantity. What is flagged is a unit word
11
+ *
12
+ * - after a number, written in digits or as a word, in prose or typeset
13
+ * (`2 meters`, `two-meter`, `$x = 3$ meters`);
14
+ * - after `per`, or before it (`rotations per second`, `meters per unit`);
15
+ * - with `square` / `cubic` before it or `squared` / `cubed` after it.
16
+ *
17
+ * "Degrees of freedom" is not a unit. Calendar time (day, week, month, year)
18
+ * and currency are left out on purpose: they are ordinary words in a finance
19
+ * course's prose, not SI units. `notation` `units:` strings are a different
20
+ * vocabulary (`units-vocab.ts`, rule N19) and are never scanned here.
21
+ */
22
+ interface SpelledUnit {
23
+ readonly index: number;
24
+ readonly token: string;
25
+ /** What to write instead, as `$…$`. */
26
+ readonly suggestion: string;
27
+ }
28
+ /**
29
+ * Every spelled-out unit in a lesson or assessment text. `prose` has code,
30
+ * math, references, JSX tags and comments blanked (`proseOnly`); `withMath`
31
+ * is the same text with math left in, for the `$x = 3$ meters` shape. Both
32
+ * must share offsets. A phrase whose unit words are all in `allow`
33
+ * (`rules.N9.allow`, singular or plural) is skipped.
34
+ */
35
+ declare function findSpelledUnits(prose: string, withMath: string, allow?: readonly string[]): SpelledUnit[];
36
+
37
+ export { type SpelledUnit, findSpelledUnits };