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,45 @@
1
+ /**
2
+ * The MDX-expression gate (invariant `prop-text`, with the prop gate). MDX
3
+ * reads `{…}` in a lesson's text as JavaScript: `The set {a} has one element` renders nothing
4
+ * and fails the build with "a is not defined", in a compiled chunk, with no
5
+ * file or line. `validate:content` and `content check` parse no expression, so
6
+ * the build was the first to notice. This gate finds a brace pair in the prose
7
+ * (code, math, JSX tags, comments, references and import/export lines blanked;
8
+ * a directive's attributes, `:::note{icon=""}`, kept out) and says what to
9
+ * write instead. See docs/authoring/components.md#braces-in-prose.
10
+ */
11
+ import type { MathGateDiagnostic } from './gate-math';
12
+ import { lessonLine, proseOnly } from './gate-prose-notation';
13
+ import type { NotationLessonInput } from './types';
14
+
15
+ /** `export const …` lines: MDX's own JavaScript, not prose. */
16
+ const EXPORT_LINE = /^[ \t]*export\b[^\n]*$/gm;
17
+ /** A directive and its attributes, `:::note{icon=""}` or `:::note[Title]{…}`. */
18
+ const DIRECTIVE = /:{1,3}[A-Za-z][\w-]*(?:\[[^\]\n]*\])?\{[^{}\n]*\}/g;
19
+
20
+ export function gateMdxExpressions(lessons: readonly NotationLessonInput[]): MathGateDiagnostic[] {
21
+ const blank = (match: string) => match.replace(/[^\n]/g, ' ');
22
+ const diagnostics: MathGateDiagnostic[] = [];
23
+ for (const lesson of lessons) {
24
+ if (!lesson.body.includes('{')) continue;
25
+ const prose = proseOnly(lesson.body).replace(EXPORT_LINE, blank).replace(DIRECTIVE, blank);
26
+ for (const match of prose.matchAll(/\{([^{}\n]*)\}/g)) {
27
+ const index = match.index ?? 0;
28
+ if (lesson.body[index - 1] === '\\') continue;
29
+ const inner = lesson.body.slice(index + 1, index + match[0].length - 1);
30
+ diagnostics.push({
31
+ context: 'prose',
32
+ code: 'mdx-expression',
33
+ severity: 'error',
34
+ file: lesson.source.file,
35
+ line: lessonLine(lesson, index),
36
+ token: `{${inner}}`,
37
+ message:
38
+ `{${inner}} in the prose is a JavaScript expression to MDX, and the build fails on it ` +
39
+ `("${inner.trim() || '…'} is not defined"). Write \\{${inner}\\} for braces in the text, ` +
40
+ `or $\\{${inner}\\}$ for a set.`,
41
+ });
42
+ }
43
+ }
44
+ return diagnostics;
45
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * The non-chapter page gate (rule S10). A page that is not a chapter — the
3
+ * landing page, a part's overview page, any other page outside a part
4
+ * directory — explains its symbols like a chapter does, but numbers nothing:
5
+ *
6
+ * - no numbered block: `<Table>`, `<Figure>`, `<Diagram>`, `<CompactExamples>`,
7
+ * `<AssessmentSet>`, and no lab (a component imported from a `labs/` module,
8
+ * or any hydrated `client:*` island) — code `non-chapter-float`;
9
+ * - no `\label{eq:…}` on a display equation — the same code;
10
+ * - no `notation.local` / `notation.suspend`: the page recalls symbols and
11
+ * defines none — code `non-chapter-local-notation`;
12
+ * - no `$…$` or `[[…]]` in the plain-text strings a page shows outside its
13
+ * Markdown body (`hero.title`, `hero.tagline`, a `<PartOverview chain>`
14
+ * step), where they would print literally — code `non-chapter-plain-text`.
15
+ *
16
+ * A float needs a chapter number, which such a page does not have; linking
17
+ * the chapter's float (`[[lesson#tab-key]]`) shows it in the reference card
18
+ * instead. The rule is locked: turning it off would not give the page a
19
+ * number, so each finding is an error with no waiver. See
20
+ * docs/authoring/structure-rules.md#s10.
21
+ */
22
+ import type { MathGateDiagnostic } from './gate-math';
23
+ import { scannable } from './gate-part-pages';
24
+ import { lessonLine } from './gate-prose-notation';
25
+
26
+ /** One non-chapter page, as the manifest compiler loads it. */
27
+ export interface NonChapterPageInput {
28
+ readonly file: string;
29
+ readonly data: Readonly<Record<string, unknown>>;
30
+ readonly body: string;
31
+ readonly bodyLine: number;
32
+ }
33
+
34
+ /** A numbered block, with the reference prefix of the float a chapter would give it. */
35
+ const NUMBERED_BLOCKS: Readonly<Record<string, { noun: string; ref: string }>> = {
36
+ Table: { noun: 'a table', ref: 'tab' },
37
+ Figure: { noun: 'a figure', ref: 'fig' },
38
+ Diagram: { noun: 'a diagram', ref: 'dia' },
39
+ CompactExamples: { noun: 'a worked-example set', ref: 'ex' },
40
+ AssessmentSet: { noun: 'a knowledge check', ref: 'check' },
41
+ };
42
+
43
+ const NUMBERED_BLOCK_TAG = new RegExp(
44
+ `<(${Object.keys(NUMBERED_BLOCKS).join('|')})(?=[\\s/>])`,
45
+ 'g',
46
+ );
47
+ const EQUATION_LABEL = /\\label\s*\{\s*eq:([^}]*)\}/g;
48
+ const CLIENT_DIRECTIVE = /<([A-Z][\w.]*)\b[^<>]*?\sclient:[a-z]+/g;
49
+ const IMPORT = /^\s*import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/gm;
50
+ const PLAIN_TEXT_MARKUP = /\$|\[\[/;
51
+
52
+ interface Finding {
53
+ readonly index: number;
54
+ readonly code: 'non-chapter-float' | 'non-chapter-local-notation' | 'non-chapter-plain-text';
55
+ readonly token: string;
56
+ readonly message: string;
57
+ /** Frontmatter findings have no body offset; they report the file's first line. */
58
+ readonly inFrontmatter?: true;
59
+ }
60
+
61
+ /** Local names an `import … from '<…>labs/…'` binds, e.g. `{ A, B as C }` → A, C. */
62
+ function labComponentNames(text: string): Set<string> {
63
+ const names = new Set<string>();
64
+ for (const match of text.matchAll(IMPORT)) {
65
+ const [, clause, source] = match;
66
+ if (!/(^|\/)labs\//.test(source)) continue;
67
+ const named = /\{([^}]*)\}/.exec(clause);
68
+ const defaultName = clause.replace(/\{[^}]*\}/, '').replace(/,/g, ' ').trim();
69
+ if (/^[A-Za-z_$][\w$]*$/.test(defaultName)) names.add(defaultName);
70
+ for (const part of named?.[1].split(',') ?? []) {
71
+ const alias = part.trim().split(/\s+as\s+/).pop()?.trim();
72
+ if (alias) names.add(alias);
73
+ }
74
+ }
75
+ return names;
76
+ }
77
+
78
+ function floatFindings(text: string): Finding[] {
79
+ const findings: Finding[] = [];
80
+
81
+ for (const match of text.matchAll(NUMBERED_BLOCK_TAG)) {
82
+ const tag = match[1];
83
+ const { noun, ref } = NUMBERED_BLOCKS[tag];
84
+ findings.push({
85
+ index: match.index ?? 0,
86
+ code: 'non-chapter-float',
87
+ token: `<${tag}>`,
88
+ message:
89
+ `<${tag}> on a page that is not a chapter (rule S10): only a chapter ` +
90
+ `numbers ${noun}. Move it into the chapter it belongs to, and link it ` +
91
+ `from here with [[<chapter>#${ref}-<key>]]: the link shows it in its card.`,
92
+ });
93
+ }
94
+
95
+ const labs = labComponentNames(text);
96
+ const flagged = new Set<number>();
97
+ for (const name of labs) {
98
+ // A lab bound to a numbered block's name (`import Diagram from
99
+ // '../labs/…'`) was reported above, under that name.
100
+ if (name in NUMBERED_BLOCKS) continue;
101
+ const usage = new RegExp(`<${name.replace(/\./g, '\\.')}(?=[\\s/>])`, 'g');
102
+ for (const match of text.matchAll(usage)) {
103
+ const index = match.index ?? 0;
104
+ flagged.add(index);
105
+ findings.push({
106
+ index,
107
+ code: 'non-chapter-float',
108
+ token: `<${name}>`,
109
+ message:
110
+ `<${name}> is a lab, on a page that is not a chapter (rule S10): a ` +
111
+ 'plot belongs in a chapter’s <Figure>. Move it there, and link it ' +
112
+ 'from here with [[<chapter>#fig-<key>]]: the link shows it in its card.',
113
+ });
114
+ }
115
+ }
116
+ for (const match of text.matchAll(CLIENT_DIRECTIVE)) {
117
+ const index = match.index ?? 0;
118
+ if (flagged.has(index) || match[1] in NUMBERED_BLOCKS) continue;
119
+ findings.push({
120
+ index,
121
+ code: 'non-chapter-float',
122
+ token: `<${match[1]}>`,
123
+ message:
124
+ `<${match[1]}> is an interactive island, on a page that is not a ` +
125
+ 'chapter (rule S10): an interactive figure belongs in a chapter’s ' +
126
+ '<Figure>. Move it there, and link it from here with ' +
127
+ '[[<chapter>#fig-<key>]]: the link shows it in its card.',
128
+ });
129
+ }
130
+
131
+ for (const match of text.matchAll(EQUATION_LABEL)) {
132
+ const key = match[1].trim();
133
+ findings.push({
134
+ index: match.index ?? 0,
135
+ code: 'non-chapter-float',
136
+ token: `\\label{eq:${key}}`,
137
+ message:
138
+ `\\label{eq:${key}} on a page that is not a chapter (rule S10): only a ` +
139
+ 'chapter numbers an equation. Drop the label to show the equation ' +
140
+ 'unnumbered, or link the chapter’s own with [[<chapter>#eq-<key>]].',
141
+ });
142
+ }
143
+
144
+ return findings;
145
+ }
146
+
147
+ function record(value: unknown): Record<string, unknown> | undefined {
148
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
149
+ ? (value as Record<string, unknown>)
150
+ : undefined;
151
+ }
152
+
153
+ function frontmatterFindings(data: Readonly<Record<string, unknown>>): Finding[] {
154
+ const findings: Finding[] = [];
155
+ const notation = record(data.notation);
156
+ for (const field of ['local', 'suspend'] as const) {
157
+ if (notation?.[field] === undefined) continue;
158
+ findings.push({
159
+ index: 0,
160
+ inFrontmatter: true,
161
+ code: 'non-chapter-local-notation',
162
+ token: `notation.${field}`,
163
+ message:
164
+ `notation.${field} on a page that is not a chapter (rule S10): the ` +
165
+ 'page recalls symbols and defines none. Move the entry to the chapter ' +
166
+ 'that introduces the symbol, or to shared notation, and name it here ' +
167
+ 'with [[key]].',
168
+ });
169
+ }
170
+
171
+ const hero = record(data.hero);
172
+ for (const field of ['title', 'tagline'] as const) {
173
+ const value = hero?.[field];
174
+ if (typeof value !== 'string' || !PLAIN_TEXT_MARKUP.test(value)) continue;
175
+ findings.push({
176
+ index: 0,
177
+ inFrontmatter: true,
178
+ code: 'non-chapter-plain-text',
179
+ token: `hero.${field}`,
180
+ message:
181
+ `hero.${field} is plain text, so its $…$ or [[…]] prints literally ` +
182
+ '(rule S10). Write the words here, and put the symbol in the page ' +
183
+ 'body, where it gets its card.',
184
+ });
185
+ }
186
+ return findings;
187
+ }
188
+
189
+ /** `<PartOverview chain={[ '…', "…" ]}>`: each quoted step is plain text. */
190
+ function chainFindings(text: string): Finding[] {
191
+ const findings: Finding[] = [];
192
+ const overview = /<PartOverview\b[\s\S]*?\bchain=\{\s*\[([\s\S]*?)\]\s*\}/g;
193
+ for (const match of text.matchAll(overview)) {
194
+ const listStart = (match.index ?? 0) + match[0].indexOf(match[1]);
195
+ const step = /'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"|`((?:[^`\\]|\\.)*)`/g;
196
+ for (const item of match[1].matchAll(step)) {
197
+ const value = item[1] ?? item[2] ?? item[3] ?? '';
198
+ if (!PLAIN_TEXT_MARKUP.test(value)) continue;
199
+ findings.push({
200
+ index: listStart + (item.index ?? 0),
201
+ code: 'non-chapter-plain-text',
202
+ token: 'chain',
203
+ message:
204
+ 'A <PartOverview chain> step is plain text, so its $…$ or [[…]] ' +
205
+ 'prints literally (rule S10). Write the step in words; a symbol ' +
206
+ 'with its card goes in <PartScope>.',
207
+ });
208
+ }
209
+ }
210
+ return findings;
211
+ }
212
+
213
+ /**
214
+ * Check every non-chapter page for what rule S10 keeps off it. Returns one
215
+ * `MathGateDiagnostic` (context `'structure'`, always an error) per finding.
216
+ */
217
+ export function gateNonChapterPages(
218
+ pages: readonly NonChapterPageInput[],
219
+ ): MathGateDiagnostic[] {
220
+ const diagnostics: MathGateDiagnostic[] = [];
221
+ for (const page of pages) {
222
+ const text = scannable(page.body);
223
+ const findings = [
224
+ ...frontmatterFindings(page.data),
225
+ ...floatFindings(text),
226
+ ...chainFindings(text),
227
+ ].sort((a, b) => a.index - b.index);
228
+ for (const finding of findings) {
229
+ diagnostics.push({
230
+ context: 'structure',
231
+ severity: 'error',
232
+ code: finding.code,
233
+ file: page.file,
234
+ line: finding.inFrontmatter ? 1 : lessonLine(page, finding.index),
235
+ token: finding.token,
236
+ message: finding.message,
237
+ });
238
+ }
239
+ }
240
+ return diagnostics;
241
+ }
@@ -52,7 +52,7 @@ interface Occurrence {
52
52
  }
53
53
 
54
54
  /** Blank MDX comments (`{/* … *\/}`) and code, keeping every newline. */
55
- function scannable(body: string): string {
55
+ export function scannable(body: string): string {
56
56
  return stripFencedCode(body).replace(/\{\/\*[\s\S]*?\*\/\}/g, (comment) =>
57
57
  comment.replace(/[^\n]/g, ' '),
58
58
  );
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The prop-text gate (invariant `prop-text`). A component's quoted prop —
3
+ * `caption="…"`, `short="…"`, `title="…"`, `label="…"` — reaches the page as
4
+ * plain text: KaTeX, the reference plugin and the citation plugin never run
5
+ * inside one. A `$…$`, `[[…]]` or `[@…]` there would print exactly as typed,
6
+ * dollars and brackets included, so it fails the build at its line with what
7
+ * to write instead. See docs/authoring/components.md#props-are-plain-text.
8
+ */
9
+ import type { CurriculumIssue } from '../curriculum/validation';
10
+ import type { ContentRecord } from './gate-retired-keys';
11
+
12
+ /** One quoted prop of a JSX opening tag: `name="value"`. */
13
+ export interface PropString {
14
+ readonly tag: string;
15
+ readonly name: string;
16
+ readonly value: string;
17
+ /** Index of `value` in the text it was read from. */
18
+ readonly offset: number;
19
+ }
20
+
21
+ const blank = (text: string) => text.replace(/[^\n]/g, ' ');
22
+
23
+ /** Code, MDX comments and a leading frontmatter, blanked so offsets hold. */
24
+ function blankNonJsx(text: string): string {
25
+ return text
26
+ .replace(/^---\r?\n[\s\S]*?\r?\n---(?=\r?\n|$)/, blank)
27
+ .replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
28
+ .replace(/```[\s\S]*?```/g, blank)
29
+ .replace(/(^|[^`])`[^`\n]*`/g, (match, lead: string) => lead + blank(match.slice(lead.length)));
30
+ }
31
+
32
+ /**
33
+ * The tag that opens at `start` (on its `<`), with every `{…}` expression
34
+ * blanked so a quoted string inside one is not read as a prop: its end and
35
+ * that copy, or `undefined` when the tag never closes.
36
+ */
37
+ function readTag(text: string, start: number): { readonly end: number; readonly props: string } | undefined {
38
+ let quote: string | undefined;
39
+ let depth = 0;
40
+ let props = '';
41
+ for (let index = start; index < text.length; index += 1) {
42
+ const char = text[index]!;
43
+ const kept = depth === 0 || char === '\n' ? char : ' ';
44
+ if (quote !== undefined) {
45
+ if (char === quote) quote = undefined;
46
+ props += kept;
47
+ continue;
48
+ }
49
+ if (char === '"' || char === "'" || (char === '`' && depth > 0)) {
50
+ quote = char;
51
+ } else if (char === '{') {
52
+ depth += 1;
53
+ } else if (char === '}') {
54
+ depth = Math.max(0, depth - 1);
55
+ props += ' ';
56
+ continue;
57
+ } else if (char === '>' && depth === 0) {
58
+ return { end: index + 1, props: props + char };
59
+ }
60
+ props += depth === 0 || char === '\n' ? char : ' ';
61
+ }
62
+ return undefined;
63
+ }
64
+
65
+ /** Every quoted prop of every JSX opening tag in `text`, outside code, comments and frontmatter. */
66
+ export function propStrings(text: string): PropString[] {
67
+ const searchable = blankNonJsx(text);
68
+ const found: PropString[] = [];
69
+ const opening = /<([A-Za-z][\w.:-]*)/g;
70
+ let match: RegExpExecArray | null;
71
+ while ((match = opening.exec(searchable))) {
72
+ const tag = readTag(searchable, match.index);
73
+ if (tag === undefined) break;
74
+ for (const prop of tag.props.matchAll(/([A-Za-z_][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
75
+ const value = prop[2] ?? prop[3] ?? '';
76
+ const valueAt = match.index + (prop.index ?? 0) + prop[0].length - value.length - 1;
77
+ found.push({ tag: match[1]!, name: prop[1]!, value: text.slice(valueAt, valueAt + value.length), offset: valueAt });
78
+ }
79
+ opening.lastIndex = tag.end;
80
+ }
81
+ return found;
82
+ }
83
+
84
+ /** `text` with every quoted JSX prop value blanked: the scans for math and prose skip what never renders as either. */
85
+ export function blankPropStrings(text: string): string {
86
+ let out = text;
87
+ for (const { value, offset } of propStrings(text)) {
88
+ out = out.slice(0, offset) + blank(value) + out.slice(offset + value.length);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/;
94
+ const REFERENCE = /\[\[[^\]\n]*\]\]/;
95
+ const CITATION = /\[@[^\]\n]*\]/;
96
+
97
+ /** What a prop holds that only renders in prose, and what to write instead. */
98
+ function markupIn(value: string): { readonly found: string; readonly advice: string } | undefined {
99
+ const math = INLINE_MATH.exec(value);
100
+ if (math) {
101
+ return {
102
+ found: `math (\`${math[0]}\`)`,
103
+ advice:
104
+ 'Write the symbol in Unicode or words ("v₀", "the launch speed"); the prose next to it typesets the symbol.',
105
+ };
106
+ }
107
+ const reference = REFERENCE.exec(value);
108
+ if (reference) {
109
+ return {
110
+ found: `a reference (\`${reference[0]}\`)`,
111
+ advice: 'Name the target in words, and reference it from the prose next to the component.',
112
+ };
113
+ }
114
+ const citation = CITATION.exec(value);
115
+ if (citation) {
116
+ return {
117
+ found: `a citation (\`${citation[0]}\`)`,
118
+ advice: 'Cite the source in the prose next to the component.',
119
+ };
120
+ }
121
+ return undefined;
122
+ }
123
+
124
+ /** One `markup-in-prop` issue per quoted prop of a page's JSX that holds math, a reference or a citation. */
125
+ export function findMarkupInProps(records: readonly ContentRecord[]): CurriculumIssue[] {
126
+ const issues: CurriculumIssue[] = [];
127
+ for (const { file, text } of records) {
128
+ const path = file.replace(/\\/g, '/');
129
+ if (!path.startsWith('content/docs/') || !path.endsWith('.mdx')) continue;
130
+ for (const prop of propStrings(text)) {
131
+ const markup = markupIn(prop.value);
132
+ if (markup === undefined) continue;
133
+ const line = text.slice(0, prop.offset).split('\n').length;
134
+ issues.push({
135
+ kind: 'markup-in-prop',
136
+ message:
137
+ `${file}:${line}: <${prop.tag}> \`${prop.name}\` holds ${markup.found}, but a prop ` +
138
+ `is plain text: it would print as typed. ${markup.advice}`,
139
+ file,
140
+ line,
141
+ });
142
+ }
143
+ }
144
+ return issues;
145
+ }
@@ -49,6 +49,12 @@ const KEY_REFERENCE = /\[\[[^\]\n]*\]\]/g;
49
49
  // does not apply inside one — enforcing "use $…$" somewhere it can't render
50
50
  // would just break the caption instead of fixing it.
51
51
  const JSX_TAG = /<\/?[A-Za-z][^<>]*>/g;
52
+ // A link's destination, `[text](https://…/basically/ "title")`: an address,
53
+ // not prose a reader sees; its text stays. A definition line,
54
+ // `[text]: https://…`, is all address. An autolink `<https://…>` is a JSX-tag
55
+ // shape and goes with them.
56
+ const LINK_DESTINATION = /\]\((?:[^()\n]|\([^()\n]*\))*\)/g;
57
+ const LINK_DEFINITION = /^[ \t]{0,3}\[[^\]\n]+\]:[ \t]*\S[^\n]*$/gm;
52
58
 
53
59
  /**
54
60
  * Blank code, math, reference, and JSX-tag spans so offsets don't shift.
@@ -62,9 +68,13 @@ const MDX_COMMENT = /\{\/\*[\s\S]*?\*\/\}/g;
62
68
  /**
63
69
  * `keepMath` leaves `$…$` / `$$…$$` in place (everything else still blanked,
64
70
  * offsets unchanged) — for a check that needs to see what a math span sits
65
- * next to, such as `$x = 3$ meters`.
71
+ * next to, such as `$x = 3$ meters`. `keepReferences` leaves `[[…]]` in
72
+ * place, for the check that a reference is well formed.
66
73
  */
67
- export function proseOnly(body: string, options: { readonly keepMath?: boolean } = {}): string {
74
+ export function proseOnly(
75
+ body: string,
76
+ options: { readonly keepMath?: boolean; readonly keepReferences?: boolean } = {},
77
+ ): string {
68
78
  const blank = (match: string) => match.replace(/[^\n]/g, ' ');
69
79
  const code = body
70
80
  .replace(MDX_COMMENT, blank)
@@ -73,7 +83,9 @@ export function proseOnly(body: string, options: { readonly keepMath?: boolean }
73
83
  const math = options.keepMath ? code : code.replace(DISPLAY_MATH, blank).replace(INLINE_MATH, blank);
74
84
  return math
75
85
  .replace(IMPORT_LINE, blank)
76
- .replace(KEY_REFERENCE, blank)
86
+ .replace(KEY_REFERENCE, (match) => (options.keepReferences ? match : blank(match)))
87
+ .replace(LINK_DESTINATION, (match) => `]${blank(match.slice(1))}`)
88
+ .replace(LINK_DEFINITION, blank)
77
89
  .replace(JSX_TAG, blank);
78
90
  }
79
91
 
@@ -130,8 +142,10 @@ const SLASH_IDIOMS = new Set([
130
142
  const SLASH_TOKENS = /\b([A-Za-z]{1,6})\/([A-Za-z]{1,6})\b/g;
131
143
 
132
144
  // A `_` or `^` sitting directly between two alphanumerics reads as a
133
- // hand-typed sub/superscript that was never wrapped in `$…$`.
134
- const FAKE_SCRIPT = /[A-Za-z0-9][_^][A-Za-z0-9{]/g;
145
+ // hand-typed sub/superscript that was never wrapped in `$…$`. The match takes
146
+ // the whole runs on both sides (or a braced group), so the message quotes
147
+ // `2^10`, not `2^1`.
148
+ const FAKE_SCRIPT = /[A-Za-z0-9]+[_^](?:\{[^{}\n]{0,24}\}?|[A-Za-z0-9]+)/g;
135
149
 
136
150
  /** Every N9 finding that is not a spelled-out unit. */
137
151
  const PROSE_NOTATION_CODE = 'prose-notation' as const;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The retired-key gate (invariant `retired-keys`). A content file that still
3
+ * sets a key the engine no longer reads fails the build, once per key and
4
+ * file, so a course stops maintaining a field that does nothing. The finding
5
+ * points at the key's line, and deleting that line clears it. See
6
+ * docs/authoring/structure.md#retired-keys.
7
+ */
8
+ import type { CurriculumIssue } from '../curriculum/validation';
9
+
10
+ /** One content file as `loadContentRecords` (`compiler/collections.ts`) reads it. */
11
+ export interface ContentRecord {
12
+ /** Repository-relative path, e.g. `content/sources/hull.json`. */
13
+ readonly file: string;
14
+ /** Top-level fields: a page's frontmatter, or a JSON record's object. */
15
+ readonly data: Readonly<Record<string, unknown>>;
16
+ readonly text: string;
17
+ }
18
+
19
+ /** Each retired key, with what an author does about it. Keys are plain identifiers. */
20
+ export const RETIRED_KEYS: Readonly<Record<string, string>> = {
21
+ editorialStatus:
22
+ 'explico no longer tracks a draft / in-review / reviewed status, because lessons are reviewed continuously. Delete the line.',
23
+ };
24
+
25
+ /** One `retired-key` issue per retired key a record sets, in record order. */
26
+ export function findRetiredKeys(
27
+ records: readonly ContentRecord[],
28
+ ): CurriculumIssue[] {
29
+ const issues: CurriculumIssue[] = [];
30
+ for (const { file, data, text } of records) {
31
+ for (const [key, advice] of Object.entries(RETIRED_KEYS)) {
32
+ if (!(key in data)) continue;
33
+ const line = keyLine(text, key);
34
+ const at = line === undefined ? file : `${file}:${line}`;
35
+ issues.push({
36
+ kind: 'retired-key',
37
+ message: `${at}: \`${key}\` is retired. ${advice}`,
38
+ file,
39
+ ...(line === undefined ? {} : { line }),
40
+ });
41
+ }
42
+ }
43
+ return issues;
44
+ }
45
+
46
+ /** The 1-based line a `key:` (YAML) or `"key":` (JSON) first starts on. */
47
+ export function keyLine(text: string, key: string): number | undefined {
48
+ const match = new RegExp(`^[ \\t]*(?:"${key}"|${key})[ \\t]*:`, 'm').exec(text);
49
+ return match === null
50
+ ? undefined
51
+ : text.slice(0, match.index).split('\n').length;
52
+ }
53
+
54
+ /**
55
+ * Starlight frontmatter a lesson may set but explico cannot honour: the
56
+ * manifest, not Starlight, decides which pages a course lists and in what
57
+ * order (invariant `retired-keys`). `draft: true` is an error — Starlight
58
+ * drops the page from a production build while the sidebar still names it, so
59
+ * the build fails — and `sidebar.order` a warning, since chapters follow the
60
+ * tracks and the key does nothing.
61
+ */
62
+ export function findUnsupportedKeys(
63
+ records: readonly ContentRecord[],
64
+ ): CurriculumIssue[] {
65
+ const issues: CurriculumIssue[] = [];
66
+ for (const { file, data, text } of records) {
67
+ if (!file.replace(/\\/g, '/').startsWith('content/docs/')) continue;
68
+ if (data.draft === true) {
69
+ const line = keyLine(text, 'draft');
70
+ issues.push({
71
+ kind: 'unsupported-key',
72
+ message:
73
+ `${line === undefined ? file : `${file}:${line}`}: \`draft: true\` is not supported in a ` +
74
+ 'course: Starlight leaves a draft out of a production build, but explico still ' +
75
+ 'lists and numbers it, so the build fails ("The slug … specified in the Starlight ' +
76
+ 'sidebar config does not exist"). Keep an unfinished lesson outside content/docs/ ' +
77
+ 'until it is ready.',
78
+ file,
79
+ ...(line === undefined ? {} : { line }),
80
+ });
81
+ }
82
+ // A chapter's order: a page outside a part (the glossary, the curriculum
83
+ // map) is placed by the engine and has no tracks to point to.
84
+ const sidebar = data.sidebar;
85
+ const chapter = /^content\/docs\/[^/]+\/(?!index\.mdx?$)[^/]+\.mdx?$/.test(file.replace(/\\/g, '/'));
86
+ if (chapter && sidebar !== null && typeof sidebar === 'object' && 'order' in sidebar) {
87
+ const line = keyLine(text, 'order');
88
+ issues.push({
89
+ kind: 'ignored-key',
90
+ severity: 'warning',
91
+ message:
92
+ `${line === undefined ? file : `${file}:${line}`}: \`sidebar.order\` does nothing in a ` +
93
+ 'course: chapters follow the order of the tracks (content/tracks/*.json), then their ' +
94
+ 'slugs. Order them there and delete the line.',
95
+ file,
96
+ ...(line === undefined ? {} : { line }),
97
+ });
98
+ }
99
+ }
100
+ return issues;
101
+ }
@@ -43,6 +43,8 @@ export interface ResolvedGloss {
43
43
  export interface GlyphScopeEntry {
44
44
  readonly key: string;
45
45
  readonly notation: string;
46
+ /** Rule N23: the entry's index is a fixed label, so it is no family. */
47
+ readonly fixedIndex?: true;
46
48
  }
47
49
 
48
50
  /**