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
@@ -0,0 +1,156 @@
1
+ import { scannable } from "./gate-part-pages.js";
2
+ import { lessonLine } from "./gate-prose-notation.js";
3
+ const NUMBERED_BLOCKS = {
4
+ Table: { noun: "a table", ref: "tab" },
5
+ Figure: { noun: "a figure", ref: "fig" },
6
+ Diagram: { noun: "a diagram", ref: "dia" },
7
+ CompactExamples: { noun: "a worked-example set", ref: "ex" },
8
+ AssessmentSet: { noun: "a knowledge check", ref: "check" }
9
+ };
10
+ const NUMBERED_BLOCK_TAG = new RegExp(
11
+ `<(${Object.keys(NUMBERED_BLOCKS).join("|")})(?=[\\s/>])`,
12
+ "g"
13
+ );
14
+ const EQUATION_LABEL = /\\label\s*\{\s*eq:([^}]*)\}/g;
15
+ const CLIENT_DIRECTIVE = /<([A-Z][\w.]*)\b[^<>]*?\sclient:[a-z]+/g;
16
+ const IMPORT = /^\s*import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/gm;
17
+ const PLAIN_TEXT_MARKUP = /\$|\[\[/;
18
+ function labComponentNames(text) {
19
+ const names = /* @__PURE__ */ new Set();
20
+ for (const match of text.matchAll(IMPORT)) {
21
+ const [, clause, source] = match;
22
+ if (!/(^|\/)labs\//.test(source)) continue;
23
+ const named = /\{([^}]*)\}/.exec(clause);
24
+ const defaultName = clause.replace(/\{[^}]*\}/, "").replace(/,/g, " ").trim();
25
+ if (/^[A-Za-z_$][\w$]*$/.test(defaultName)) names.add(defaultName);
26
+ for (const part of named?.[1].split(",") ?? []) {
27
+ const alias = part.trim().split(/\s+as\s+/).pop()?.trim();
28
+ if (alias) names.add(alias);
29
+ }
30
+ }
31
+ return names;
32
+ }
33
+ function floatFindings(text) {
34
+ const findings = [];
35
+ for (const match of text.matchAll(NUMBERED_BLOCK_TAG)) {
36
+ const tag = match[1];
37
+ const { noun, ref } = NUMBERED_BLOCKS[tag];
38
+ findings.push({
39
+ index: match.index ?? 0,
40
+ code: "non-chapter-float",
41
+ token: `<${tag}>`,
42
+ message: `<${tag}> on a page that is not a chapter (rule S10): only a chapter numbers ${noun}. Move it into the chapter it belongs to, and link it from here with [[<chapter>#${ref}-<key>]]: the link shows it in its card.`
43
+ });
44
+ }
45
+ const labs = labComponentNames(text);
46
+ const flagged = /* @__PURE__ */ new Set();
47
+ for (const name of labs) {
48
+ if (name in NUMBERED_BLOCKS) continue;
49
+ const usage = new RegExp(`<${name.replace(/\./g, "\\.")}(?=[\\s/>])`, "g");
50
+ for (const match of text.matchAll(usage)) {
51
+ const index = match.index ?? 0;
52
+ flagged.add(index);
53
+ findings.push({
54
+ index,
55
+ code: "non-chapter-float",
56
+ token: `<${name}>`,
57
+ message: `<${name}> is a lab, on a page that is not a chapter (rule S10): a plot belongs in a chapter\u2019s <Figure>. Move it there, and link it from here with [[<chapter>#fig-<key>]]: the link shows it in its card.`
58
+ });
59
+ }
60
+ }
61
+ for (const match of text.matchAll(CLIENT_DIRECTIVE)) {
62
+ const index = match.index ?? 0;
63
+ if (flagged.has(index) || match[1] in NUMBERED_BLOCKS) continue;
64
+ findings.push({
65
+ index,
66
+ code: "non-chapter-float",
67
+ token: `<${match[1]}>`,
68
+ message: `<${match[1]}> is an interactive island, on a page that is not a chapter (rule S10): an interactive figure belongs in a chapter\u2019s <Figure>. Move it there, and link it from here with [[<chapter>#fig-<key>]]: the link shows it in its card.`
69
+ });
70
+ }
71
+ for (const match of text.matchAll(EQUATION_LABEL)) {
72
+ const key = match[1].trim();
73
+ findings.push({
74
+ index: match.index ?? 0,
75
+ code: "non-chapter-float",
76
+ token: `\\label{eq:${key}}`,
77
+ message: `\\label{eq:${key}} on a page that is not a chapter (rule S10): only a chapter numbers an equation. Drop the label to show the equation unnumbered, or link the chapter\u2019s own with [[<chapter>#eq-<key>]].`
78
+ });
79
+ }
80
+ return findings;
81
+ }
82
+ function record(value) {
83
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
84
+ }
85
+ function frontmatterFindings(data) {
86
+ const findings = [];
87
+ const notation = record(data.notation);
88
+ for (const field of ["local", "suspend"]) {
89
+ if (notation?.[field] === void 0) continue;
90
+ findings.push({
91
+ index: 0,
92
+ inFrontmatter: true,
93
+ code: "non-chapter-local-notation",
94
+ token: `notation.${field}`,
95
+ message: `notation.${field} on a page that is not a chapter (rule S10): the page recalls symbols and defines none. Move the entry to the chapter that introduces the symbol, or to shared notation, and name it here with [[key]].`
96
+ });
97
+ }
98
+ const hero = record(data.hero);
99
+ for (const field of ["title", "tagline"]) {
100
+ const value = hero?.[field];
101
+ if (typeof value !== "string" || !PLAIN_TEXT_MARKUP.test(value)) continue;
102
+ findings.push({
103
+ index: 0,
104
+ inFrontmatter: true,
105
+ code: "non-chapter-plain-text",
106
+ token: `hero.${field}`,
107
+ message: `hero.${field} is plain text, so its $\u2026$ or [[\u2026]] prints literally (rule S10). Write the words here, and put the symbol in the page body, where it gets its card.`
108
+ });
109
+ }
110
+ return findings;
111
+ }
112
+ function chainFindings(text) {
113
+ const findings = [];
114
+ const overview = /<PartOverview\b[\s\S]*?\bchain=\{\s*\[([\s\S]*?)\]\s*\}/g;
115
+ for (const match of text.matchAll(overview)) {
116
+ const listStart = (match.index ?? 0) + match[0].indexOf(match[1]);
117
+ const step = /'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"|`((?:[^`\\]|\\.)*)`/g;
118
+ for (const item of match[1].matchAll(step)) {
119
+ const value = item[1] ?? item[2] ?? item[3] ?? "";
120
+ if (!PLAIN_TEXT_MARKUP.test(value)) continue;
121
+ findings.push({
122
+ index: listStart + (item.index ?? 0),
123
+ code: "non-chapter-plain-text",
124
+ token: "chain",
125
+ message: "A <PartOverview chain> step is plain text, so its $\u2026$ or [[\u2026]] prints literally (rule S10). Write the step in words; a symbol with its card goes in <PartScope>."
126
+ });
127
+ }
128
+ }
129
+ return findings;
130
+ }
131
+ function gateNonChapterPages(pages) {
132
+ const diagnostics = [];
133
+ for (const page of pages) {
134
+ const text = scannable(page.body);
135
+ const findings = [
136
+ ...frontmatterFindings(page.data),
137
+ ...floatFindings(text),
138
+ ...chainFindings(text)
139
+ ].sort((a, b) => a.index - b.index);
140
+ for (const finding of findings) {
141
+ diagnostics.push({
142
+ context: "structure",
143
+ severity: "error",
144
+ code: finding.code,
145
+ file: page.file,
146
+ line: finding.inFrontmatter ? 1 : lessonLine(page, finding.index),
147
+ token: finding.token,
148
+ message: finding.message
149
+ });
150
+ }
151
+ }
152
+ return diagnostics;
153
+ }
154
+ export {
155
+ gateNonChapterPages
156
+ };
@@ -1,4 +1,4 @@
1
- import { N as NormalizedPartPages } from '../course-config-Dsnri9Tk.js';
1
+ import { i as NormalizedPartPages } from '../course-config-rRtME5UV.js';
2
2
  import { MathGateDiagnostic } from './gate-math.js';
3
3
  import '../rules/catalog.js';
4
4
  import '../rules/course-rules.js';
@@ -34,6 +34,8 @@ interface PartPageInput {
34
34
  readonly body: string;
35
35
  readonly bodyLine: number;
36
36
  }
37
+ /** Blank MDX comments (`{/* … *\/}`) and code, keeping every newline. */
38
+ declare function scannable(body: string): string;
37
39
  /**
38
40
  * Words a reader reads in a paragraph of MDX prose. A math span, a `[[key]]`
39
41
  * reference, or a link each count by what they render as: one token for the
@@ -47,4 +49,4 @@ declare function proseWordCount(markdown: string): number;
47
49
  */
48
50
  declare function gatePartPages(pages: readonly PartPageInput[], limits: NormalizedPartPages): MathGateDiagnostic[];
49
51
 
50
- export { type PartPageInput, gatePartPages, proseWordCount };
52
+ export { type PartPageInput, gatePartPages, proseWordCount, scannable };
@@ -114,5 +114,6 @@ function gatePartPages(pages, limits) {
114
114
  }
115
115
  export {
116
116
  gatePartPages,
117
- proseWordCount
117
+ proseWordCount,
118
+ scannable
118
119
  };
@@ -0,0 +1,28 @@
1
+ import { CurriculumIssue } from '../curriculum/validation.js';
2
+ import { ContentRecord } from './gate-retired-keys.js';
3
+
4
+ /**
5
+ * The prop-text gate (invariant `prop-text`). A component's quoted prop —
6
+ * `caption="…"`, `short="…"`, `title="…"`, `label="…"` — reaches the page as
7
+ * plain text: KaTeX, the reference plugin and the citation plugin never run
8
+ * inside one. A `$…$`, `[[…]]` or `[@…]` there would print exactly as typed,
9
+ * dollars and brackets included, so it fails the build at its line with what
10
+ * to write instead. See docs/authoring/components.md#props-are-plain-text.
11
+ */
12
+
13
+ /** One quoted prop of a JSX opening tag: `name="value"`. */
14
+ interface PropString {
15
+ readonly tag: string;
16
+ readonly name: string;
17
+ readonly value: string;
18
+ /** Index of `value` in the text it was read from. */
19
+ readonly offset: number;
20
+ }
21
+ /** Every quoted prop of every JSX opening tag in `text`, outside code, comments and frontmatter. */
22
+ declare function propStrings(text: string): PropString[];
23
+ /** `text` with every quoted JSX prop value blanked: the scans for math and prose skip what never renders as either. */
24
+ declare function blankPropStrings(text: string): string;
25
+ /** One `markup-in-prop` issue per quoted prop of a page's JSX that holds math, a reference or a citation. */
26
+ declare function findMarkupInProps(records: readonly ContentRecord[]): CurriculumIssue[];
27
+
28
+ export { type PropString, blankPropStrings, findMarkupInProps, propStrings };
@@ -0,0 +1,106 @@
1
+ const blank = (text) => text.replace(/[^\n]/g, " ");
2
+ function blankNonJsx(text) {
3
+ return text.replace(/^---\r?\n[\s\S]*?\r?\n---(?=\r?\n|$)/, blank).replace(/\{\/\*[\s\S]*?\*\/\}/g, blank).replace(/```[\s\S]*?```/g, blank).replace(/(^|[^`])`[^`\n]*`/g, (match, lead) => lead + blank(match.slice(lead.length)));
4
+ }
5
+ function readTag(text, start) {
6
+ let quote;
7
+ let depth = 0;
8
+ let props = "";
9
+ for (let index = start; index < text.length; index += 1) {
10
+ const char = text[index];
11
+ const kept = depth === 0 || char === "\n" ? char : " ";
12
+ if (quote !== void 0) {
13
+ if (char === quote) quote = void 0;
14
+ props += kept;
15
+ continue;
16
+ }
17
+ if (char === '"' || char === "'" || char === "`" && depth > 0) {
18
+ quote = char;
19
+ } else if (char === "{") {
20
+ depth += 1;
21
+ } else if (char === "}") {
22
+ depth = Math.max(0, depth - 1);
23
+ props += " ";
24
+ continue;
25
+ } else if (char === ">" && depth === 0) {
26
+ return { end: index + 1, props: props + char };
27
+ }
28
+ props += depth === 0 || char === "\n" ? char : " ";
29
+ }
30
+ return void 0;
31
+ }
32
+ function propStrings(text) {
33
+ const searchable = blankNonJsx(text);
34
+ const found = [];
35
+ const opening = /<([A-Za-z][\w.:-]*)/g;
36
+ let match;
37
+ while (match = opening.exec(searchable)) {
38
+ const tag = readTag(searchable, match.index);
39
+ if (tag === void 0) break;
40
+ for (const prop of tag.props.matchAll(/([A-Za-z_][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
41
+ const value = prop[2] ?? prop[3] ?? "";
42
+ const valueAt = match.index + (prop.index ?? 0) + prop[0].length - value.length - 1;
43
+ found.push({ tag: match[1], name: prop[1], value: text.slice(valueAt, valueAt + value.length), offset: valueAt });
44
+ }
45
+ opening.lastIndex = tag.end;
46
+ }
47
+ return found;
48
+ }
49
+ function blankPropStrings(text) {
50
+ let out = text;
51
+ for (const { value, offset } of propStrings(text)) {
52
+ out = out.slice(0, offset) + blank(value) + out.slice(offset + value.length);
53
+ }
54
+ return out;
55
+ }
56
+ const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/;
57
+ const REFERENCE = /\[\[[^\]\n]*\]\]/;
58
+ const CITATION = /\[@[^\]\n]*\]/;
59
+ function markupIn(value) {
60
+ const math = INLINE_MATH.exec(value);
61
+ if (math) {
62
+ return {
63
+ found: `math (\`${math[0]}\`)`,
64
+ advice: 'Write the symbol in Unicode or words ("v\u2080", "the launch speed"); the prose next to it typesets the symbol.'
65
+ };
66
+ }
67
+ const reference = REFERENCE.exec(value);
68
+ if (reference) {
69
+ return {
70
+ found: `a reference (\`${reference[0]}\`)`,
71
+ advice: "Name the target in words, and reference it from the prose next to the component."
72
+ };
73
+ }
74
+ const citation = CITATION.exec(value);
75
+ if (citation) {
76
+ return {
77
+ found: `a citation (\`${citation[0]}\`)`,
78
+ advice: "Cite the source in the prose next to the component."
79
+ };
80
+ }
81
+ return void 0;
82
+ }
83
+ function findMarkupInProps(records) {
84
+ const issues = [];
85
+ for (const { file, text } of records) {
86
+ const path = file.replace(/\\/g, "/");
87
+ if (!path.startsWith("content/docs/") || !path.endsWith(".mdx")) continue;
88
+ for (const prop of propStrings(text)) {
89
+ const markup = markupIn(prop.value);
90
+ if (markup === void 0) continue;
91
+ const line = text.slice(0, prop.offset).split("\n").length;
92
+ issues.push({
93
+ kind: "markup-in-prop",
94
+ message: `${file}:${line}: <${prop.tag}> \`${prop.name}\` holds ${markup.found}, but a prop is plain text: it would print as typed. ${markup.advice}`,
95
+ file,
96
+ line
97
+ });
98
+ }
99
+ }
100
+ return issues;
101
+ }
102
+ export {
103
+ blankPropStrings,
104
+ findMarkupInProps,
105
+ propStrings
106
+ };
@@ -34,10 +34,12 @@ interface AssessmentLike {
34
34
  /**
35
35
  * `keepMath` leaves `$…$` / `$$…$$` in place (everything else still blanked,
36
36
  * offsets unchanged) — for a check that needs to see what a math span sits
37
- * next to, such as `$x = 3$ meters`.
37
+ * next to, such as `$x = 3$ meters`. `keepReferences` leaves `[[…]]` in
38
+ * place, for the check that a reference is well formed.
38
39
  */
39
40
  declare function proseOnly(body: string, options?: {
40
41
  readonly keepMath?: boolean;
42
+ readonly keepReferences?: boolean;
41
43
  }): string;
42
44
  /** Exported for the same reason as {@link proseOnly}. */
43
45
  declare function lineOf(text: string, index: number): number;
@@ -6,12 +6,14 @@ const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g;
6
6
  const IMPORT_LINE = /^[ \t]*import\b[^\n]*$/gm;
7
7
  const KEY_REFERENCE = /\[\[[^\]\n]*\]\]/g;
8
8
  const JSX_TAG = /<\/?[A-Za-z][^<>]*>/g;
9
+ const LINK_DESTINATION = /\]\((?:[^()\n]|\([^()\n]*\))*\)/g;
10
+ const LINK_DEFINITION = /^[ \t]{0,3}\[[^\]\n]+\]:[ \t]*\S[^\n]*$/gm;
9
11
  const MDX_COMMENT = /\{\/\*[\s\S]*?\*\/\}/g;
10
12
  function proseOnly(body, options = {}) {
11
13
  const blank = (match) => match.replace(/[^\n]/g, " ");
12
14
  const code = body.replace(MDX_COMMENT, blank).replace(CODE_FENCE, blank).replace(INLINE_CODE, (match, lead) => lead + blank(match.slice(lead.length)));
13
15
  const math = options.keepMath ? code : code.replace(DISPLAY_MATH, blank).replace(INLINE_MATH, blank);
14
- return math.replace(IMPORT_LINE, blank).replace(KEY_REFERENCE, blank).replace(JSX_TAG, blank);
16
+ return math.replace(IMPORT_LINE, blank).replace(KEY_REFERENCE, (match) => options.keepReferences ? match : blank(match)).replace(LINK_DESTINATION, (match) => `]${blank(match.slice(1))}`).replace(LINK_DEFINITION, blank).replace(JSX_TAG, blank);
15
17
  }
16
18
  function lineOf(text, index) {
17
19
  return text.slice(0, index).split("\n").length;
@@ -43,7 +45,7 @@ const SLASH_IDIOMS = /* @__PURE__ */ new Set([
43
45
  "a/b"
44
46
  ]);
45
47
  const SLASH_TOKENS = /\b([A-Za-z]{1,6})\/([A-Za-z]{1,6})\b/g;
46
- const FAKE_SCRIPT = /[A-Za-z0-9][_^][A-Za-z0-9{]/g;
48
+ const FAKE_SCRIPT = /[A-Za-z0-9]+[_^](?:\{[^{}\n]{0,24}\}?|[A-Za-z0-9]+)/g;
47
49
  const PROSE_NOTATION_CODE = "prose-notation";
48
50
  function findAll(text, pattern, toFinding) {
49
51
  const out = [];
@@ -0,0 +1,35 @@
1
+ import { CurriculumIssue } from '../curriculum/validation.js';
2
+
3
+ /**
4
+ * The retired-key gate (invariant `retired-keys`). A content file that still
5
+ * sets a key the engine no longer reads fails the build, once per key and
6
+ * file, so a course stops maintaining a field that does nothing. The finding
7
+ * points at the key's line, and deleting that line clears it. See
8
+ * docs/authoring/structure.md#retired-keys.
9
+ */
10
+
11
+ /** One content file as `loadContentRecords` (`compiler/collections.ts`) reads it. */
12
+ interface ContentRecord {
13
+ /** Repository-relative path, e.g. `content/sources/hull.json`. */
14
+ readonly file: string;
15
+ /** Top-level fields: a page's frontmatter, or a JSON record's object. */
16
+ readonly data: Readonly<Record<string, unknown>>;
17
+ readonly text: string;
18
+ }
19
+ /** Each retired key, with what an author does about it. Keys are plain identifiers. */
20
+ declare const RETIRED_KEYS: Readonly<Record<string, string>>;
21
+ /** One `retired-key` issue per retired key a record sets, in record order. */
22
+ declare function findRetiredKeys(records: readonly ContentRecord[]): CurriculumIssue[];
23
+ /** The 1-based line a `key:` (YAML) or `"key":` (JSON) first starts on. */
24
+ declare function keyLine(text: string, key: string): number | undefined;
25
+ /**
26
+ * Starlight frontmatter a lesson may set but explico cannot honour: the
27
+ * manifest, not Starlight, decides which pages a course lists and in what
28
+ * order (invariant `retired-keys`). `draft: true` is an error — Starlight
29
+ * drops the page from a production build while the sidebar still names it, so
30
+ * the build fails — and `sidebar.order` a warning, since chapters follow the
31
+ * tracks and the key does nothing.
32
+ */
33
+ declare function findUnsupportedKeys(records: readonly ContentRecord[]): CurriculumIssue[];
34
+
35
+ export { type ContentRecord, RETIRED_KEYS, findRetiredKeys, findUnsupportedKeys, keyLine };
@@ -0,0 +1,58 @@
1
+ const RETIRED_KEYS = {
2
+ editorialStatus: "explico no longer tracks a draft / in-review / reviewed status, because lessons are reviewed continuously. Delete the line."
3
+ };
4
+ function findRetiredKeys(records) {
5
+ const issues = [];
6
+ for (const { file, data, text } of records) {
7
+ for (const [key, advice] of Object.entries(RETIRED_KEYS)) {
8
+ if (!(key in data)) continue;
9
+ const line = keyLine(text, key);
10
+ const at = line === void 0 ? file : `${file}:${line}`;
11
+ issues.push({
12
+ kind: "retired-key",
13
+ message: `${at}: \`${key}\` is retired. ${advice}`,
14
+ file,
15
+ ...line === void 0 ? {} : { line }
16
+ });
17
+ }
18
+ }
19
+ return issues;
20
+ }
21
+ function keyLine(text, key) {
22
+ const match = new RegExp(`^[ \\t]*(?:"${key}"|${key})[ \\t]*:`, "m").exec(text);
23
+ return match === null ? void 0 : text.slice(0, match.index).split("\n").length;
24
+ }
25
+ function findUnsupportedKeys(records) {
26
+ const issues = [];
27
+ for (const { file, data, text } of records) {
28
+ if (!file.replace(/\\/g, "/").startsWith("content/docs/")) continue;
29
+ if (data.draft === true) {
30
+ const line = keyLine(text, "draft");
31
+ issues.push({
32
+ kind: "unsupported-key",
33
+ message: `${line === void 0 ? file : `${file}:${line}`}: \`draft: true\` is not supported in a course: Starlight leaves a draft out of a production build, but explico still lists and numbers it, so the build fails ("The slug \u2026 specified in the Starlight sidebar config does not exist"). Keep an unfinished lesson outside content/docs/ until it is ready.`,
34
+ file,
35
+ ...line === void 0 ? {} : { line }
36
+ });
37
+ }
38
+ const sidebar = data.sidebar;
39
+ const chapter = /^content\/docs\/[^/]+\/(?!index\.mdx?$)[^/]+\.mdx?$/.test(file.replace(/\\/g, "/"));
40
+ if (chapter && sidebar !== null && typeof sidebar === "object" && "order" in sidebar) {
41
+ const line = keyLine(text, "order");
42
+ issues.push({
43
+ kind: "ignored-key",
44
+ severity: "warning",
45
+ message: `${line === void 0 ? file : `${file}:${line}`}: \`sidebar.order\` does nothing in a course: chapters follow the order of the tracks (content/tracks/*.json), then their slugs. Order them there and delete the line.`,
46
+ file,
47
+ ...line === void 0 ? {} : { line }
48
+ });
49
+ }
50
+ }
51
+ return issues;
52
+ }
53
+ export {
54
+ RETIRED_KEYS,
55
+ findRetiredKeys,
56
+ findUnsupportedKeys,
57
+ keyLine
58
+ };
@@ -41,6 +41,8 @@ interface ResolvedGloss {
41
41
  interface GlyphScopeEntry {
42
42
  readonly key: string;
43
43
  readonly notation: string;
44
+ /** Rule N23: the entry's index is a fixed label, so it is no family. */
45
+ readonly fixedIndex?: true;
44
46
  }
45
47
  /**
46
48
  * The key segment derived from a gloss name: lower-cased, runs of anything
@@ -0,0 +1,17 @@
1
+ /** A formula's box at one font size, in px. */
2
+ interface FormulaMetrics {
3
+ /** Advance width. */
4
+ readonly width: number;
5
+ /** Height above the baseline. */
6
+ readonly height: number;
7
+ /** Depth below the baseline. */
8
+ readonly depth: number;
9
+ }
10
+ /**
11
+ * Measure `latex` as the notation KaTeX renders it — the same macros
12
+ * (`\explain`, `\slot`), trust boundary and delimiter sizing as lesson math —
13
+ * at a font size of `fontPx`. Throws on LaTeX KaTeX cannot render.
14
+ */
15
+ declare function measureFormula(latex: string, fontPx: number): FormulaMetrics;
16
+
17
+ export { type FormulaMetrics, measureFormula };