explico 0.11.1 → 0.13.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 (298) hide show
  1. package/CHANGELOG.md +1292 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +11 -4
  4. package/dist/compiler/astro.js +48 -15
  5. package/dist/compiler/cli.d.ts +27 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +33 -2
  8. package/dist/compiler/collections.js +91 -19
  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 +15 -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 +115 -8
  51. package/dist/compiler/manifest.js +221 -39
  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 +3 -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 +8 -2
  61. package/dist/reference/base-operators.mjs +8 -10
  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/figure-symbols.d.ts +32 -0
  88. package/dist/reference/figure-symbols.js +55 -0
  89. package/dist/reference/figures-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-abbreviations.js +1 -1
  92. package/dist/reference/gate-course-rules.d.ts +4 -3
  93. package/dist/reference/gate-course-rules.js +9 -4
  94. package/dist/reference/gate-heading-length.d.ts +2 -0
  95. package/dist/reference/gate-heading-length.js +7 -5
  96. package/dist/reference/gate-heading-structure.js +26 -0
  97. package/dist/reference/gate-indices.d.ts +35 -0
  98. package/dist/reference/gate-indices.js +299 -0
  99. package/dist/reference/gate-math.d.ts +32 -2
  100. package/dist/reference/gate-math.js +68 -12
  101. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  102. package/dist/reference/gate-mdx-expression.js +29 -0
  103. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  104. package/dist/reference/gate-non-chapter-pages.js +156 -0
  105. package/dist/reference/gate-part-pages.d.ts +4 -2
  106. package/dist/reference/gate-part-pages.js +2 -1
  107. package/dist/reference/gate-prop-text.d.ts +28 -0
  108. package/dist/reference/gate-prop-text.js +106 -0
  109. package/dist/reference/gate-prose-notation.d.ts +3 -1
  110. package/dist/reference/gate-prose-notation.js +4 -2
  111. package/dist/reference/gate-retired-keys.d.ts +35 -0
  112. package/dist/reference/gate-retired-keys.js +58 -0
  113. package/dist/reference/gloss.d.ts +2 -0
  114. package/dist/reference/katex-measure.d.ts +17 -0
  115. package/dist/reference/katex-measure.js +223 -0
  116. package/dist/reference/keyed-float-validate.js +12 -0
  117. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  118. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  119. package/dist/reference/lab-katex-validate.d.ts +43 -0
  120. package/dist/reference/lab-katex-validate.js +96 -0
  121. package/dist/reference/lab-math-scope.d.ts +1 -0
  122. package/dist/reference/lab-math-scope.js +13 -2
  123. package/dist/reference/lint-ignore.js +1 -1
  124. package/dist/reference/math-glyphs.d.mts +94 -2
  125. package/dist/reference/math-glyphs.mjs +851 -46
  126. package/dist/reference/mixed-content.d.ts +8 -3
  127. package/dist/reference/mixed-content.js +4 -2
  128. package/dist/reference/page-notation.d.ts +39 -7
  129. package/dist/reference/page-notation.js +62 -10
  130. package/dist/reference/raw-floats.mjs +37 -13
  131. package/dist/reference/references.d.ts +11 -1
  132. package/dist/reference/references.js +6 -0
  133. package/dist/reference/registry.js +115 -11
  134. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  135. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  136. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  137. package/dist/reference/remark-citation.mjs +0 -0
  138. package/dist/reference/remark-notation.mjs +275 -23
  139. package/dist/reference/render-lab-math.d.ts +13 -2
  140. package/dist/reference/render-lab-math.js +18 -6
  141. package/dist/reference/sections-validate.d.ts +28 -3
  142. package/dist/reference/sections-validate.js +31 -4
  143. package/dist/reference/sections.d.mts +24 -6
  144. package/dist/reference/sections.mjs +92 -17
  145. package/dist/reference/tables-validate.d.ts +1 -1
  146. package/dist/reference/tables-validate.js +24 -1
  147. package/dist/reference/text-measure.d.ts +10 -0
  148. package/dist/reference/text-measure.js +41 -0
  149. package/dist/reference/text-widths.d.ts +10 -0
  150. package/dist/reference/text-widths.js +1238 -0
  151. package/dist/reference/types.d.ts +64 -8
  152. package/dist/rules/catalog.js +223 -32
  153. package/dist/rules/in-effect.d.ts +1 -1
  154. package/dist/rules/rules.schema.json +100 -27
  155. package/dist/rules/settings.d.ts +1 -1
  156. package/dist/rules/settings.js +7 -0
  157. package/dist/session/PreferenceStore.d.ts +18 -1
  158. package/dist/session/PreferenceStore.js +29 -1
  159. package/docs/authoring/README.md +33 -21
  160. package/docs/authoring/agents.md +38 -7
  161. package/docs/authoring/components.md +115 -10
  162. package/docs/authoring/migrating.md +878 -0
  163. package/docs/authoring/notation.md +285 -30
  164. package/docs/authoring/retired.md +1 -0
  165. package/docs/authoring/structure-rules.md +84 -45
  166. package/docs/authoring/structure.md +67 -2
  167. package/docs/authoring/visualizations.md +167 -25
  168. package/package.json +12 -7
  169. package/src/compiler/astro.ts +72 -21
  170. package/src/compiler/cli.ts +143 -35
  171. package/src/compiler/collections.ts +158 -27
  172. package/src/compiler/content-schema.ts +108 -0
  173. package/src/compiler/content-schemas.ts +391 -0
  174. package/src/compiler/course-config.ts +86 -15
  175. package/src/compiler/exports/combined-html.ts +669 -0
  176. package/src/compiler/exports/download-index.ts +36 -0
  177. package/src/compiler/exports/export-chapters.ts +233 -0
  178. package/src/compiler/exports/export-pages.ts +44 -0
  179. package/src/compiler/exports/html-tree.ts +62 -0
  180. package/src/compiler/exports/latex/capture.ts +651 -0
  181. package/src/compiler/exports/latex/compile.ts +168 -0
  182. package/src/compiler/exports/latex/document.ts +479 -0
  183. package/src/compiler/exports/latex/explico.cls +415 -0
  184. package/src/compiler/exports/latex/numbering.ts +74 -0
  185. package/src/compiler/exports/latex/serialize.ts +787 -0
  186. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  187. package/src/compiler/exports/latex-export.ts +255 -0
  188. package/src/compiler/exports/markdown/serialize.ts +632 -0
  189. package/src/compiler/exports/markdown-export.ts +329 -0
  190. package/src/compiler/exports/standalone-html.ts +39 -70
  191. package/src/compiler/exports/static-tree.ts +944 -0
  192. package/src/compiler/exports/zip.ts +88 -0
  193. package/src/compiler/lesson-derivation.ts +13 -8
  194. package/src/compiler/manifest.ts +387 -40
  195. package/src/compiler/render-diagnostics.ts +111 -0
  196. package/src/components/CourseContents.astro +6 -0
  197. package/src/components/HoverPanel.astro +204 -6
  198. package/src/components/PartOverview.astro +18 -9
  199. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  200. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  201. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  202. package/src/components/assessments/AssessmentSet.astro +14 -1
  203. package/src/components/block-heading.ts +38 -0
  204. package/src/components/card-arrow.ts +18 -4
  205. package/src/components/card-carry.ts +406 -0
  206. package/src/components/card-drag.ts +1 -30
  207. package/src/components/card-fetch.ts +87 -0
  208. package/src/components/card-manager.ts +1222 -38
  209. package/src/components/card-memory.ts +5 -1
  210. package/src/components/card-placement.ts +211 -13
  211. package/src/components/card-side.ts +281 -31
  212. package/src/components/citation/CitationLayer.astro +45 -40
  213. package/src/components/diagram/Diagram.astro +123 -44
  214. package/src/components/disclosure/print-as-shown.ts +42 -6
  215. package/src/components/figure/Figure.astro +17 -1
  216. package/src/components/figure/FloatCaption.astro +4 -17
  217. package/src/components/figure/figure-state.ts +26 -17
  218. package/src/components/float-toolbar.ts +274 -0
  219. package/src/components/memo/Memo.astro +21 -10
  220. package/src/components/memo/ShortNote.astro +4 -0
  221. package/src/components/notation/NotationGlossary.astro +22 -17
  222. package/src/components/notation/NotationLayer.astro +300 -79
  223. package/src/components/notation/NotationMath.astro +8 -1
  224. package/src/components/notation/render-notation-math.ts +51 -5
  225. package/src/components/readable-text.ts +25 -0
  226. package/src/components/reference/FloatRefLayer.astro +162 -89
  227. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  228. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  229. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  230. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  231. package/src/components/starlight/LessonExportActions.astro +384 -310
  232. package/src/components/starlight/LessonFooter.astro +66 -8
  233. package/src/components/starlight/PrintController.astro +28 -0
  234. package/src/components/starlight/TextSizeControl.astro +137 -0
  235. package/src/components/starlight/print-controller.ts +380 -0
  236. package/src/components/starlight/section-summary.ts +13 -5
  237. package/src/components/table/Table.astro +14 -22
  238. package/src/components/table/table-truncation.ts +15 -0
  239. package/src/content-config.ts +20 -312
  240. package/src/curriculum/validation.ts +72 -24
  241. package/src/reference/base-abbreviations.mjs +2 -2
  242. package/src/reference/base-operators.mjs +8 -10
  243. package/src/reference/base-roman-labels.mjs +4 -3
  244. package/src/reference/citation-format.mjs +74 -0
  245. package/src/reference/consistency.ts +47 -61
  246. package/src/reference/curriculum-alignment.ts +4 -41
  247. package/src/reference/derived-entries.ts +68 -0
  248. package/src/reference/derived-notation.mjs +121 -0
  249. package/src/reference/diagram-fit.ts +104 -0
  250. package/src/reference/diagram-layout.ts +182 -51
  251. package/src/reference/diagrams-validate.ts +2 -1
  252. package/src/reference/equations-validate.ts +41 -2
  253. package/src/reference/equations.mjs +120 -22
  254. package/src/reference/examples-validate.ts +1 -0
  255. package/src/reference/figure-fit.ts +139 -0
  256. package/src/reference/figure-pictures.ts +96 -0
  257. package/src/reference/figure-symbols.ts +117 -0
  258. package/src/reference/figures-validate.ts +2 -1
  259. package/src/reference/gate-abbreviations.ts +1 -1
  260. package/src/reference/gate-course-rules.ts +22 -7
  261. package/src/reference/gate-heading-length.ts +25 -7
  262. package/src/reference/gate-heading-structure.ts +40 -1
  263. package/src/reference/gate-indices.ts +470 -0
  264. package/src/reference/gate-math.ts +102 -13
  265. package/src/reference/gate-mdx-expression.ts +45 -0
  266. package/src/reference/gate-non-chapter-pages.ts +241 -0
  267. package/src/reference/gate-part-pages.ts +1 -1
  268. package/src/reference/gate-prop-text.ts +145 -0
  269. package/src/reference/gate-prose-notation.ts +19 -5
  270. package/src/reference/gate-retired-keys.ts +101 -0
  271. package/src/reference/gloss.ts +2 -0
  272. package/src/reference/katex-measure.ts +384 -0
  273. package/src/reference/keyed-float-validate.ts +22 -0
  274. package/src/reference/keyed-jsx-floats.mjs +11 -14
  275. package/src/reference/lab-katex-validate.ts +156 -0
  276. package/src/reference/lab-math-scope.ts +15 -1
  277. package/src/reference/lint-ignore.ts +4 -1
  278. package/src/reference/math-glyphs.mjs +851 -46
  279. package/src/reference/mixed-content.ts +11 -2
  280. package/src/reference/page-notation.ts +114 -14
  281. package/src/reference/raw-floats.mjs +37 -13
  282. package/src/reference/references.ts +14 -0
  283. package/src/reference/registry.ts +170 -9
  284. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  285. package/src/reference/rehype-heading-anchors.mjs +11 -1
  286. package/src/reference/remark-citation.mjs +0 -0
  287. package/src/reference/remark-notation.mjs +275 -23
  288. package/src/reference/render-lab-math.ts +40 -5
  289. package/src/reference/sections-validate.ts +67 -3
  290. package/src/reference/sections.mjs +92 -17
  291. package/src/reference/tables-validate.ts +40 -2
  292. package/src/reference/text-measure.ts +63 -0
  293. package/src/reference/text-widths.ts +133 -0
  294. package/src/reference/types.ts +75 -8
  295. package/src/rules/catalog.ts +241 -31
  296. package/src/rules/settings.ts +10 -0
  297. package/src/session/PreferenceStore.ts +53 -0
  298. package/src/styles/global.css +322 -83
@@ -1,18 +1,23 @@
1
1
  import { lessonLine } from "./gate-prose-notation.js";
2
- import { parseSectionRef } from "./sections.mjs";
2
+ import { parseSectionRef, scanOutlineBlocks } from "./sections.mjs";
3
3
  const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
4
4
  function withoutCode(text) {
5
5
  const blank = (block) => block.replace(/[^\n]/g, " ");
6
6
  return text.replace(/```[\s\S]*?```/g, blank).replace(/~~~[\s\S]*?~~~/g, blank).replace(/`[^`\n]*`/g, blank);
7
7
  }
8
- function unresolved(ref, lessonExists) {
8
+ function unresolved(ref, lessonExists, pages) {
9
9
  const written = ref.lesson ? `[[${ref.lesson}#sec-${ref.key}]]` : `[[sec-${ref.key}]]`;
10
+ const page = ref.lesson?.replace(/\/index$/, "");
11
+ if (page !== void 0 && !lessonExists && pages.has(page)) {
12
+ return `${written} names ${page}, a page that is not a chapter: a section reference reaches a chapter's top-level headings only (rule S10). Link the heading with a Markdown link instead, [its words](/${page}/#${ref.key}).`;
13
+ }
10
14
  if (ref.lesson && !lessonExists) {
11
15
  return `${written} names lesson ${ref.lesson}, which does not exist.`;
12
16
  }
13
17
  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
18
  }
15
- function validateSectionRefs(lessons) {
19
+ function validateSectionRefs(lessons, pageSlugs = []) {
20
+ const pages = new Set(pageSlugs.map((slug) => slug.replace(/\/index$/, "")));
16
21
  const headingsByLesson = new Map(
17
22
  lessons.map((lesson) => [lesson.slug, new Set(lesson.headingSlugs)])
18
23
  );
@@ -30,12 +35,34 @@ function validateSectionRefs(lessons) {
30
35
  file: lesson.file,
31
36
  line: lessonLine(lesson, match.index ?? 0),
32
37
  token: match[0],
33
- message: unresolved(ref, headings !== void 0)
38
+ message: unresolved(ref, headings !== void 0, pages)
34
39
  });
35
40
  }
36
41
  }
37
42
  return diagnostics;
38
43
  }
44
+ function validateHeadingAnchors(lessons) {
45
+ const diagnostics = [];
46
+ for (const lesson of lessons) {
47
+ const offsets = scanOutlineBlocks(lesson.body).filter((block) => block.kind === "heading").map((block) => block.index);
48
+ lesson.headings.forEach((heading, index) => {
49
+ const owner = lesson.anchors.get(heading.slug);
50
+ if (owner === void 0) return;
51
+ const advice = heading.slug === "references" ? "Rename the heading, or drop it: the References section is generated from the page\u2019s citations." : `Rename the heading, or give ${owner} another id.`;
52
+ diagnostics.push({
53
+ context: "structure",
54
+ code: "heading-anchor-collision",
55
+ severity: "error",
56
+ file: lesson.file,
57
+ line: lessonLine(lesson, offsets[index] ?? 0),
58
+ token: heading.text,
59
+ message: `The heading "${heading.text}" gets the anchor #${heading.slug}, which ${owner} already has on this page, so a link to either lands on the first. ${advice}`
60
+ });
61
+ });
62
+ }
63
+ return diagnostics;
64
+ }
39
65
  export {
66
+ validateHeadingAnchors,
40
67
  validateSectionRefs
41
68
  };
@@ -20,13 +20,26 @@ export function sectionNumbers(levels: any, { prefix, depth, unnumbered }?: {
20
20
  unnumbered?: never[] | undefined;
21
21
  }): any;
22
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.
23
+ * The text a heading's source slugs as, near enough to the way Astro would:
24
+ * math as it reads, a notation term its key. A heading's anchor is made from
25
+ * it, so a plain heading keeps the id it always had; math reads as it renders
26
+ * (`mathSlugText`), so `## The range $\explain{motion.range}{R}$` is
27
+ * `#the-range-r` and not `#the-range-explainmotionranger`.
28
28
  */
29
29
  export function headingPlainText(source: any): string;
30
+ /**
31
+ * The text a reader sees for a math span: KaTeX's MathML without the LaTeX
32
+ * annotation, so `$\explain{motion.range}{R}$` reads `R` and `$v_0$` reads
33
+ * `v0`. Falls back to the source when KaTeX cannot render it.
34
+ */
35
+ export function mathPlainText(latex: any): any;
36
+ /**
37
+ * The text a reader sees for a heading: math as it reads (`mathPlainText`)
38
+ * and a `[[key]]` term as its label, from `labelFor` (the page's notation),
39
+ * else as its key. "On this page", an unnumbered section's quoted title and a
40
+ * copied link name use it; the anchor is `headingPlainText`'s.
41
+ */
42
+ export function headingDisplayText(source: any, { labelFor }?: {}): string;
30
43
  /**
31
44
  * Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
32
45
  * run over their plain text: the same slugger with the same duplicate
@@ -36,9 +49,14 @@ export function slugHeadings(texts: any): any;
36
49
  /**
37
50
  * Every top-level heading of a lesson body in document order, as the
38
51
  * manifest records it: `{ level, text, slug, number? }`. `prefix` is the
39
- * chapter's `‹part›.‹chapter›`; without one nothing is numbered.
52
+ * chapter's `‹part›.‹chapter›`; without one nothing is numbered. `text` is
53
+ * what a reader sees (`headingDisplayText`, with `labelFor` naming a
54
+ * `[[key]]` term); the `slug` is made from `headingPlainText`.
55
+ * `math: true` marks a heading with inline math, whose text Astro's own
56
+ * heading list garbles (`LayoutPageSidebar.astro` relabels it).
40
57
  */
41
58
  export function scanSectionHeadings(markdown: any, options?: {}): {
59
+ math?: boolean | undefined;
42
60
  number?: any;
43
61
  level: number;
44
62
  text: string;
@@ -27,6 +27,8 @@
27
27
  */
28
28
 
29
29
  import { rehypeHeadingIds } from '@astrojs/markdown-remark';
30
+ import katex from 'katex';
31
+ import { createNotationKatexOptions } from './katex-options.mjs';
30
32
  import { headingCardTier, stripHeadingMarker } from './rehype-heading-anchors.mjs';
31
33
 
32
34
  /** The deepest heading level numbered when a course sets nothing (`####`). */
@@ -113,19 +115,18 @@ const ATX_HEADING = /^[ \t]{0,3}(#{1,6})(?:[ \t]+([^\n]*?))?(?:[ \t]+#+)?[ \t]*$
113
115
  const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
114
116
 
115
117
  /**
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
118
+ * A heading source's text, with `math` and `term` deciding what a `$…$` span
119
+ * and a `[[key]]` contribute. Markup goes, the words stay; inline code keeps
120
+ * its source; a link keeps its text; an image, a citation and an MDX
120
121
  * expression contribute nothing; the authoring marker is dropped.
121
122
  */
122
- export function headingPlainText(source) {
123
+ function headingText(source, { math, term }) {
123
124
  const literals = [];
124
125
  const keep = (value) => `\u0000${literals.push(value) - 1}\u0000`;
125
126
  let text = stripHeadingMarker(source ?? '')
126
127
  .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))
128
+ .replace(/\$([^$\n]+)\$/g, (_, latex) => keep(math(latex.trim())))
129
+ .replace(/\[\[\s*([^\][]+?)\s*\]\]/g, (_, key) => keep(term(key)))
129
130
  .replace(/\[@[^\]]*\]/g, '')
130
131
  .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
131
132
  .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
@@ -138,17 +139,84 @@ export function headingPlainText(source) {
138
139
  .replace(/(^|[^\w])_(?=\S)([\s\S]*?\S)_(?!\w)/g, '$1$2')
139
140
  .replace(/~~(?=\S)([\s\S]*?\S)~~/g, '$1')
140
141
  .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
- });
142
+ .replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, name) => decodeEntity(entity, name));
148
143
  text = text.replace(/\u0000(\d+)\u0000/g, (_, index) => literals[Number(index)]);
149
144
  return text.replace(/\s+/g, ' ').trim();
150
145
  }
151
146
 
147
+ function decodeEntity(entity, name) {
148
+ if (name[0] !== '#') return ENTITIES[name.toLowerCase()] ?? entity;
149
+ const code = name[1] === 'x' || name[1] === 'X'
150
+ ? Number.parseInt(name.slice(2), 16)
151
+ : Number.parseInt(name.slice(1), 10);
152
+ return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
153
+ }
154
+
155
+ /**
156
+ * The text a heading's source slugs as, near enough to the way Astro would:
157
+ * math as it reads, a notation term its key. A heading's anchor is made from
158
+ * it, so a plain heading keeps the id it always had; math reads as it renders
159
+ * (`mathSlugText`), so `## The range $\explain{motion.range}{R}$` is
160
+ * `#the-range-r` and not `#the-range-explainmotionranger`.
161
+ */
162
+ export function headingPlainText(source) {
163
+ return headingText(source, { math: mathSlugText, term: (key) => key });
164
+ }
165
+
166
+ /** A Greek letter as LaTeX names it, for a slug that stays in ASCII. */
167
+ const GREEK_NAMES = {
168
+ α: 'alpha', β: 'beta', γ: 'gamma', δ: 'delta', ε: 'epsilon', ϵ: 'epsilon', ζ: 'zeta', η: 'eta',
169
+ θ: 'theta', ϑ: 'theta', ι: 'iota', κ: 'kappa', λ: 'lambda', μ: 'mu', ν: 'nu', ξ: 'xi', ο: 'omicron',
170
+ π: 'pi', ϖ: 'pi', ρ: 'rho', ϱ: 'rho', σ: 'sigma', ς: 'sigma', τ: 'tau', υ: 'upsilon', φ: 'phi',
171
+ ϕ: 'phi', χ: 'chi', ψ: 'psi', ω: 'omega', Γ: 'Gamma', Δ: 'Delta', Θ: 'Theta', Λ: 'Lambda',
172
+ Ξ: 'Xi', Π: 'Pi', Σ: 'Sigma', Υ: 'Upsilon', Φ: 'Phi', Ψ: 'Psi', Ω: 'Omega',
173
+ };
174
+
175
+ /**
176
+ * The text a heading's math slugs as: what it reads, each Greek letter spelled
177
+ * as LaTeX names it. `$e^{\lambda\cos(u)}$` reads `eλcos(u)` and slugs as
178
+ * `elambdacosu`, the anchor its source always gave it; what changes is the
179
+ * markup that never shows (`\explain{motion.range}{R}` is `R`).
180
+ */
181
+ function mathSlugText(latex) {
182
+ return mathPlainText(latex).replace(/[\u0370-\u03FF]/g, (char) => GREEK_NAMES[char] ?? char);
183
+ }
184
+
185
+ /**
186
+ * The text a reader sees for a math span: KaTeX's MathML without the LaTeX
187
+ * annotation, so `$\explain{motion.range}{R}$` reads `R` and `$v_0$` reads
188
+ * `v0`. Falls back to the source when KaTeX cannot render it.
189
+ */
190
+ export function mathPlainText(latex) {
191
+ let markup;
192
+ try {
193
+ markup = katex.renderToString(latex, createNotationKatexOptions());
194
+ } catch {
195
+ return latex;
196
+ }
197
+ const mathml = /<math[\s\S]*?<\/math>/.exec(markup)?.[0] ?? '';
198
+ return mathml
199
+ .replace(/<annotation[\s\S]*?<\/annotation>/g, '')
200
+ .replace(/<[^>]+>/g, '')
201
+ .replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, name) => decodeEntity(entity, name))
202
+ .replace(/[\u200b\u2061-\u2064]/g, '')
203
+ .replace(/\s+/g, ' ')
204
+ .trim();
205
+ }
206
+
207
+ /**
208
+ * The text a reader sees for a heading: math as it reads (`mathPlainText`)
209
+ * and a `[[key]]` term as its label, from `labelFor` (the page's notation),
210
+ * else as its key. "On this page", an unnumbered section's quoted title and a
211
+ * copied link name use it; the anchor is `headingPlainText`'s.
212
+ */
213
+ export function headingDisplayText(source, { labelFor } = {}) {
214
+ return headingText(source, {
215
+ math: mathPlainText,
216
+ term: (key) => labelFor?.(key) ?? key,
217
+ });
218
+ }
219
+
152
220
  /**
153
221
  * Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
154
222
  * run over their plain text: the same slugger with the same duplicate
@@ -171,18 +239,24 @@ export function slugHeadings(texts) {
171
239
  /**
172
240
  * Every top-level heading of a lesson body in document order, as the
173
241
  * manifest records it: `{ level, text, slug, number? }`. `prefix` is the
174
- * chapter's `‹part›.‹chapter›`; without one nothing is numbered.
242
+ * chapter's `‹part›.‹chapter›`; without one nothing is numbered. `text` is
243
+ * what a reader sees (`headingDisplayText`, with `labelFor` naming a
244
+ * `[[key]]` term); the `slug` is made from `headingPlainText`.
245
+ * `math: true` marks a heading with inline math, whose text Astro's own
246
+ * heading list garbles (`LayoutPageSidebar.astro` relabels it).
175
247
  */
176
248
  export function scanSectionHeadings(markdown, options = {}) {
177
249
  const found = [];
178
250
  for (const match of outlineSource(markdown).matchAll(ATX_HEADING)) {
179
251
  found.push({
180
252
  level: match[1].length,
181
- text: headingPlainText(match[2] ?? ''),
253
+ slugText: headingPlainText(match[2] ?? ''),
254
+ text: headingDisplayText(match[2] ?? '', options),
255
+ math: /\$[^$\n]+\$/.test(stripHeadingMarker(match[2] ?? '')),
182
256
  meta: headingCardTier(match[2] ?? '') === 'meta',
183
257
  });
184
258
  }
185
- const slugs = slugHeadings(found.map((heading) => heading.text));
259
+ const slugs = slugHeadings(found.map((heading) => heading.slugText));
186
260
  const numbers = sectionNumbers(
187
261
  found.map((heading) => heading.level),
188
262
  { ...options, unnumbered: found.map((heading) => heading.meta) },
@@ -192,6 +266,7 @@ export function scanSectionHeadings(markdown, options = {}) {
192
266
  text: heading.text,
193
267
  slug: slugs[index],
194
268
  ...(numbers[index] === undefined ? {} : { number: numbers[index] }),
269
+ ...(heading.math ? { math: true } : {}),
195
270
  }));
196
271
  }
197
272
 
@@ -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' | 'tab-removed-prop';
5
+ type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused' | 'tab-removed-prop' | 'tab-unread-prop' | 'tab-several-tables';
6
6
  interface TableDiagnostic {
7
7
  readonly code: TableDiagnosticCode;
8
8
  readonly severity: 'error' | 'warning' | 'info';
@@ -2,11 +2,34 @@ import {
2
2
  validateKeyedFloats
3
3
  } from "./keyed-float-validate.js";
4
4
  function validateTables(lessons) {
5
- return validateKeyedFloats(lessons, {
5
+ const validation = validateKeyedFloats(lessons, {
6
6
  tagName: "Table",
7
7
  prefix: "tab",
8
8
  noun: "table"
9
9
  });
10
+ const several = lessons.flatMap(severalTables);
11
+ return several.length === 0 ? validation : { ...validation, diagnostics: [...validation.diagnostics, ...several] };
12
+ }
13
+ const DELIMITER_ROW = /^[ \t]*\|?(?:[ \t]*:?-+:?[ \t]*\|)+(?:[ \t]*:?-+:?[ \t]*)?$/gm;
14
+ function severalTables(lesson) {
15
+ const blank = (match) => match.replace(/[^\n]/g, " ");
16
+ const body = lesson.body.replace(/```[\s\S]*?```/g, blank).replace(/\{\/\*[\s\S]*?\*\/\}/g, blank);
17
+ const found = [];
18
+ for (const table of body.matchAll(/<Table\b((?:[^>"']|"[^"]*"|'[^']*')*?)(?<!\/)>([\s\S]*?)<\/Table>/g)) {
19
+ const inner = table[2] ?? "";
20
+ const count = (inner.match(DELIMITER_ROW)?.length ?? 0) + (inner.match(/<table\b/g)?.length ?? 0);
21
+ if (count < 2) continue;
22
+ const key = /\bid\s*=\s*["']([^"']*)["']/.exec(table[1] ?? "")?.[1];
23
+ found.push({
24
+ code: "tab-several-tables",
25
+ severity: "error",
26
+ message: `${lesson.lessonId} has a <Table${key ? ` id="${key}"` : ""}> holding ${count} tables: a <Table> holds one, with one number and one caption, and every export keeps only the first. Give each table its own <Table id>, or join them into one.`,
27
+ file: lesson.file,
28
+ lessonId: lesson.lessonId,
29
+ ...key ? { key } : {}
30
+ });
31
+ }
32
+ return found;
10
33
  }
11
34
  export {
12
35
  validateTables
@@ -0,0 +1,10 @@
1
+ /** The advance of `text` at `fontPx`, in px, rounded up to a whole pixel. */
2
+ declare function textWidth(text: string, fontPx: number): number;
3
+ /**
4
+ * Break `text` into lines no wider than `maxWidth` px, greedily at spaces,
5
+ * the way a browser wraps it. A word wider than `maxWidth` keeps a line of its
6
+ * own (the caller widens the box to it); a space never ends or starts a line.
7
+ */
8
+ declare function wrapText(text: string, maxWidth: number, fontPx: number): string[];
9
+
10
+ export { textWidth, wrapText };
@@ -0,0 +1,41 @@
1
+ import { UNITS_PER_EM, WIDTH_BLOCKS } from "./text-widths.js";
2
+ const WIDTHS = /* @__PURE__ */ new Map();
3
+ for (const [first, widths] of WIDTH_BLOCKS) {
4
+ widths.forEach((width, offset) => {
5
+ if (width >= 0) WIDTHS.set(first + offset, width / UNITS_PER_EM);
6
+ });
7
+ }
8
+ const FALLBACK_EM = 1;
9
+ const COMBINING_MARK = new RegExp("\\p{M}", "u");
10
+ function advanceEm(char) {
11
+ if (COMBINING_MARK.test(char)) return 0;
12
+ return WIDTHS.get(char.codePointAt(0) ?? 0) ?? FALLBACK_EM;
13
+ }
14
+ function textWidth(text, fontPx) {
15
+ let em = 0;
16
+ for (const char of text) em += advanceEm(char);
17
+ return Math.ceil(em * fontPx - 1e-6);
18
+ }
19
+ function wrapText(text, maxWidth, fontPx) {
20
+ const words = text.trim().split(/[ \t\r\n]+/u).filter(Boolean);
21
+ const space = textWidth(" ", fontPx);
22
+ const lines = [];
23
+ let line = "";
24
+ let lineWidth = 0;
25
+ for (const word of words) {
26
+ const wordWidth = textWidth(word, fontPx);
27
+ if (line && lineWidth + space + wordWidth > maxWidth) {
28
+ lines.push(line);
29
+ line = "";
30
+ lineWidth = 0;
31
+ }
32
+ line = line ? `${line} ${word}` : word;
33
+ lineWidth = lineWidth ? lineWidth + space + wordWidth : wordWidth;
34
+ }
35
+ if (line) lines.push(line);
36
+ return lines;
37
+ }
38
+ export {
39
+ textWidth,
40
+ wrapText
41
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Advance widths of DejaVu Sans (Version 2.37), in font units.
3
+ * Generated by `scripts/gen-text-widths.mjs` — do not edit by hand.
4
+ * `-1` marks a code point the font has no glyph for.
5
+ */
6
+ declare const UNITS_PER_EM = 2048;
7
+ /** `[first code point, widths of it and the code points after it]`. */
8
+ declare const WIDTH_BLOCKS: readonly (readonly [number, readonly number[]])[];
9
+
10
+ export { UNITS_PER_EM, WIDTH_BLOCKS };