explico 0.12.0 → 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 (279) hide show
  1. package/CHANGELOG.md +1122 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +663 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +300 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -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 };
@@ -0,0 +1,223 @@
1
+ import katex, {} from "katex";
2
+ import fontMetricsData from "katex/src/fontMetricsData.js";
3
+ import { createNotationKatexOptions } from "./katex-options.mjs";
4
+ const SIZE_MULTIPLIERS = [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2, 1.44, 1.728, 2.074, 2.488];
5
+ const CLASS_PADDING = {
6
+ "x-arrow-pad": [0.5, 0.5],
7
+ "cd-arrow-pad": [0.27778, 0.55556],
8
+ boxpad: [0.3, 0.3],
9
+ "cancel-pad": [0.2, 0.2],
10
+ anglpad: [0.03889, 0.03889]
11
+ };
12
+ const CLASS_MARGIN = {
13
+ "cancel-lap": [-0.2, -0.2],
14
+ angl: [0, 0.03889]
15
+ };
16
+ const CLASS_BORDER = {
17
+ fbox: [0.04, 0.04],
18
+ fcolorbox: [0.04, 0.04],
19
+ angl: [0, 0.049]
20
+ };
21
+ const ZERO_WIDTH = /* @__PURE__ */ new Set(["llap", "rlap", "clap", "pstrut", "vlist-s"]);
22
+ function has(node, name) {
23
+ return node.classes?.includes(name) ?? false;
24
+ }
25
+ function em(value) {
26
+ if (!value) return 0;
27
+ const match = /^(-?\d*\.?\d+)em$/.exec(value.trim());
28
+ return match ? Number(match[1]) : 0;
29
+ }
30
+ function sizing(node) {
31
+ if (!has(node, "sizing") && !has(node, "fontsize-ensurer")) return 1;
32
+ let from = 0;
33
+ let to = 0;
34
+ for (const name of node.classes ?? []) {
35
+ const reset = /^reset-size(\d+)$/.exec(name);
36
+ if (reset) from = Number(reset[1]);
37
+ const size = /^size(\d+)$/.exec(name);
38
+ if (size) to = Number(size[1]);
39
+ }
40
+ const fromMultiplier = SIZE_MULTIPLIERS[from - 1];
41
+ const toMultiplier = SIZE_MULTIPLIERS[to - 1];
42
+ return fromMultiplier && toMultiplier ? toMultiplier / fromMultiplier : 1;
43
+ }
44
+ function pairOf(table, node) {
45
+ let total = 0;
46
+ for (const name of node.classes ?? []) {
47
+ const pair = table[name];
48
+ if (pair) total += pair[0] + pair[1];
49
+ }
50
+ return total;
51
+ }
52
+ const ROOT_FONT = { family: "Main", bold: false, italic: false };
53
+ const FONT_CLASSES = {
54
+ textbf: { bold: true },
55
+ textit: { italic: true },
56
+ textrm: { family: "Main" },
57
+ texttt: { family: "Typewriter" },
58
+ mathnormal: { family: "Math", italic: true },
59
+ mathit: { family: "Main", italic: true },
60
+ mathrm: { italic: false },
61
+ mathbf: { family: "Main", bold: true },
62
+ boldsymbol: { family: "Math", bold: true, italic: true },
63
+ amsrm: { family: "AMS" },
64
+ mathbb: { family: "AMS" },
65
+ textbb: { family: "AMS" },
66
+ mathcal: { family: "Caligraphic" },
67
+ mathfrak: { family: "Fraktur" },
68
+ textfrak: { family: "Fraktur" },
69
+ mathboldfrak: { family: "Fraktur", bold: true },
70
+ textboldfrak: { family: "Fraktur", bold: true },
71
+ mathtt: { family: "Typewriter" },
72
+ mathscr: { family: "Script" },
73
+ textscr: { family: "Script" },
74
+ mathsf: { family: "SansSerif" },
75
+ textsf: { family: "SansSerif" },
76
+ mathboldsf: { family: "SansSerif", bold: true },
77
+ textboldsf: { family: "SansSerif", bold: true },
78
+ mathsfit: { family: "SansSerif", italic: true },
79
+ mathitsf: { family: "SansSerif", italic: true },
80
+ textitsf: { family: "SansSerif", italic: true },
81
+ mainrm: { family: "Main", italic: false }
82
+ };
83
+ function fontOf(node, inherited) {
84
+ let font = inherited;
85
+ for (const name of node.classes ?? []) {
86
+ const rule = FONT_CLASSES[name];
87
+ if (rule) font = { ...font, ...rule };
88
+ }
89
+ if (has(node, "delimsizing")) {
90
+ for (const size of [1, 2, 3, 4]) {
91
+ if (has(node, `size${size}`)) font = { ...font, family: `Size${size}` };
92
+ }
93
+ }
94
+ if (has(node, "op-symbol")) {
95
+ if (has(node, "small-op")) font = { ...font, family: "Size1" };
96
+ if (has(node, "large-op")) font = { ...font, family: "Size2" };
97
+ }
98
+ return font;
99
+ }
100
+ const METRICS = fontMetricsData;
101
+ function metricsFor(font) {
102
+ const variants = [
103
+ font.bold && font.italic ? "BoldItalic" : font.bold ? "Bold" : font.italic ? "Italic" : "Regular",
104
+ font.bold ? "Bold" : void 0,
105
+ font.italic ? "Italic" : void 0,
106
+ "Regular"
107
+ ];
108
+ for (const variant of variants) {
109
+ const table = variant && METRICS[`${font.family}-${variant}`];
110
+ if (table) return table;
111
+ }
112
+ return METRICS["Main-Regular"] ?? {};
113
+ }
114
+ const WIDE_GLYPHS = /* @__PURE__ */ new Map([
115
+ [8748, 0.85],
116
+ // ∬ \iint
117
+ [8749, 1.22],
118
+ // ∭ \iiint
119
+ [8751, 0.85],
120
+ // ∯ \oiint
121
+ [8752, 1.22],
122
+ // ∰ \oiiint
123
+ [10764, 1.6]
124
+ // ⨌ \iiiint
125
+ ]);
126
+ function glyphWidth(table, char) {
127
+ if (char === "\u200B") return 0;
128
+ const metrics = table[String(char.codePointAt(0))] ?? table["77"];
129
+ return metrics?.[4] ?? 1;
130
+ }
131
+ function glyphAdvances(node, font) {
132
+ const [first, ...rest] = [...node.text ?? ""];
133
+ if (first === void 0) return [];
134
+ const table = metricsFor(font);
135
+ const wide = (char) => WIDE_GLYPHS.get(char.codePointAt(0) ?? 0);
136
+ const firstWidth = wide(first) ?? (node.width && node.width > 0 ? node.width : glyphWidth(table, first));
137
+ return [firstWidth, ...rest.map((char) => wide(char) ?? glyphWidth(table, char))];
138
+ }
139
+ function glyphPx(advanceEm, fontPx) {
140
+ return Math.max(Math.round(advanceEm * Math.round(fontPx)), advanceEm * fontPx);
141
+ }
142
+ function advance(node, inherited, context, parent) {
143
+ const frame = {
144
+ scale: inherited.scale * sizing(node),
145
+ font: fontOf(node, inherited.font)
146
+ };
147
+ const scale = frame.scale * context.fontPx;
148
+ const style = node.style ?? {};
149
+ if (typeof node.text === "string") {
150
+ const marginRight = style.marginRight !== void 0 ? em(style.marginRight) : Math.max(0, node.italic ?? 0);
151
+ const glyphs = glyphAdvances(node, frame.font).reduce(
152
+ (sum, width) => sum + glyphPx(width, scale),
153
+ 0
154
+ );
155
+ return glyphs + (marginRight + em(style.marginLeft)) * scale;
156
+ }
157
+ if (!node.classes) return 0;
158
+ const margins = (em(style.marginLeft) + em(style.marginRight) + pairOf(CLASS_MARGIN, node) + rootMargin(node, parent)) * scale;
159
+ if (node.classes.some((name) => ZERO_WIDTH.has(name))) return margins;
160
+ if (has(node, "accent-body") && !has(node, "accent-full")) return margins;
161
+ let content;
162
+ if (has(node, "nulldelimiter")) {
163
+ content = 0.12 * scale;
164
+ } else if (em(style.width) > 0) {
165
+ content = em(style.width) * scale;
166
+ } else if (has(node, "vlist-t")) {
167
+ content = tableWidth(node, frame, context);
168
+ } else if (has(node, "vlist")) {
169
+ content = Math.max(
170
+ 0,
171
+ ...(node.children ?? []).map((row) => advance(row, frame, context, node))
172
+ );
173
+ } else {
174
+ content = (node.children ?? []).reduce(
175
+ (sum, child) => sum + advance(child, frame, context, node),
176
+ 0
177
+ );
178
+ }
179
+ content = Math.max(content, em(style.minWidth) * scale);
180
+ if (has(node, "vertical-separator")) content = Math.max(content, 1);
181
+ const padding = (em(style.paddingLeft) + pairOf(CLASS_PADDING, node)) * scale;
182
+ const border = (em(style.borderRightWidth) + pairOf(CLASS_BORDER, node)) * scale;
183
+ return content + padding + border + margins;
184
+ }
185
+ function rootMargin(node, parent) {
186
+ return has(node, "root") && parent !== void 0 && has(parent, "sqrt") ? 0.2777777778 - 0.5555555556 : 0;
187
+ }
188
+ function tableWidth(node, frame, context) {
189
+ const columns = [];
190
+ for (const row of node.children ?? []) {
191
+ let column = 0;
192
+ for (const cell of row.children ?? []) {
193
+ if (has(cell, "vlist-s")) continue;
194
+ columns[column] = Math.max(columns[column] ?? 0, advance(cell, frame, context, row));
195
+ column += 1;
196
+ }
197
+ }
198
+ return columns.reduce((sum, width) => sum + width, 0);
199
+ }
200
+ function collectBases(node, out) {
201
+ if (has(node, "base")) {
202
+ out.push(node);
203
+ return out;
204
+ }
205
+ for (const child of node.children ?? []) collectBases(child, out);
206
+ return out;
207
+ }
208
+ function measureFormula(latex, fontPx) {
209
+ const { __renderToHTMLTree } = katex;
210
+ const tree = __renderToHTMLTree(latex, {
211
+ ...createNotationKatexOptions(),
212
+ displayMode: false
213
+ });
214
+ const bases = collectBases(tree, []);
215
+ return {
216
+ width: advance(tree, { scale: 1, font: ROOT_FONT }, { fontPx }),
217
+ height: Math.max(0, ...bases.map((base) => base.height ?? 0)) * fontPx,
218
+ depth: Math.max(0, ...bases.map((base) => base.depth ?? 0)) * fontPx
219
+ };
220
+ }
221
+ export {
222
+ measureFormula
223
+ };
@@ -46,6 +46,18 @@ function validateKeyedFloats(lessons, kind) {
46
46
  ...raw.key ? { key: raw.key } : {}
47
47
  });
48
48
  }
49
+ for (const prop of ["short", "caption"]) {
50
+ if (!raw.props.includes(prop) || raw[prop] != null) continue;
51
+ const places = prefix === "ex" ? `its [[${prefix}-\u2026]] card and a copied link` : `its [[${prefix}-\u2026]] card, a copied link, a folded section's line and the "Figures and tables" page`;
52
+ diagnostics.push({
53
+ code: `${prefix}-unread-prop`,
54
+ severity: "warning",
55
+ message: `${lesson.lessonId} has a <${tagName} ${prop}={\u2026}>${raw.key ? ` (id="${raw.key}")` : ""}, but the manifest reads only a quoted \`${prop}\`, so ${places} name this ${noun} without it. Write ${prop}="\u2026", with typographic quotes (\u2018\u2019 \u201C\u201D) inside.`,
56
+ file: lesson.file,
57
+ lessonId: lesson.lessonId,
58
+ ...raw.key ? { key: raw.key } : {}
59
+ });
60
+ }
49
61
  if (raw.key == null) continue;
50
62
  if (!KEYED_FLOAT_KEY_PATTERN.test(raw.key)) {
51
63
  diagnostics.push({
@@ -50,8 +50,8 @@ export function countKeyedJsxFloats(markdown: any, { tagName }: {
50
50
  tagName: any;
51
51
  }): number;
52
52
  /**
53
- * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
54
- * children (and through the `TRANSPARENT_COMPONENTS`' bodies) in document order, advancing the
53
+ * The mdast equivalent of `scanKeyedJsxFloats`: walk the lesson's flow (and
54
+ * through the `TRANSPARENT_COMPONENTS`' bodies) in document order, advancing the
55
55
  * section counter on every top-level depth-2 heading and emitting a record for
56
56
  * every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
57
57
  * `collectDisplayEquations`. `numbering` is threaded the same way as in
@@ -25,6 +25,7 @@
25
25
 
26
26
  import {
27
27
  resolveNumbering,
28
+ stripContainerDirectives,
28
29
  stripFencedCode,
29
30
  stripJsxComponents,
30
31
  TRANSPARENT_COMPONENTS,
@@ -131,10 +132,9 @@ function mdxStringAttribute(node, name) {
131
132
  */
132
133
  export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
133
134
  const { prefix, offset } = resolveNumbering(numbering);
134
- const text = stripJsxComponents(
135
- stripFencedCode(markdown),
136
- tagName,
137
- TRANSPARENT_COMPONENTS,
135
+ // A `:::note` aside is a callout, like `<Aside>`: nothing in it numbers.
136
+ const text = stripContainerDirectives(
137
+ stripJsxComponents(stripFencedCode(markdown), tagName, TRANSPARENT_COMPONENTS),
138
138
  );
139
139
  const tokens = [];
140
140
  const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
@@ -196,8 +196,8 @@ export function countKeyedJsxFloats(markdown, { tagName }) {
196
196
  }
197
197
 
198
198
  /**
199
- * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
200
- * children (and through the `TRANSPARENT_COMPONENTS`' bodies) in document order, advancing the
199
+ * The mdast equivalent of `scanKeyedJsxFloats`: walk the lesson's flow (and
200
+ * through the `TRANSPARENT_COMPONENTS`' bodies) in document order, advancing the
201
201
  * section counter on every top-level depth-2 heading and emitting a record for
202
202
  * every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
203
203
  * `collectDisplayEquations`. `numbering` is threaded the same way as in
@@ -210,8 +210,12 @@ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
210
210
  let indexInSection = 0;
211
211
  let indexInLesson = 0;
212
212
 
213
+ // The lesson's flow — top-level blocks, list items, blockquotes — and the
214
+ // `TRANSPARENT_COMPONENTS`' bodies; never another component's body or a
215
+ // `:::note` aside. `scanKeyedJsxFloats` reads the same set from the text.
213
216
  const collectFromDetail = (node) => {
214
217
  if (!node || typeof node !== 'object') return;
218
+ if (node.type === 'containerDirective') return;
215
219
  if (node.type === 'mdxJsxFlowElement' && node.name === tagName) {
216
220
  indexInSection += 1;
217
221
  indexInLesson += 1;
@@ -256,14 +260,7 @@ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
256
260
  indexInSection = 0;
257
261
  continue;
258
262
  }
259
- if (node.type === 'mdxJsxFlowElement' && node.name === tagName) {
260
- collectFromDetail(node);
261
- } else if (
262
- node.type === 'mdxJsxFlowElement' &&
263
- TRANSPARENT_COMPONENTS.includes(node.name)
264
- ) {
265
- collectFromDetail(node);
266
- }
263
+ collectFromDetail(node);
267
264
  }
268
265
  return floats;
269
266
  }
@@ -5,6 +5,7 @@ interface LessonNotationFrontmatter {
5
5
  readonly local?: ReadonlyArray<{
6
6
  readonly key: string;
7
7
  readonly latex: string;
8
+ readonly fixedIndex?: boolean;
8
9
  }>;
9
10
  }
10
11
  /**
@@ -1,5 +1,6 @@
1
1
  import { loadManifest } from "../compiler/manifest.js";
2
2
  import {
3
+ bindScopeToPage,
3
4
  registerPageNotationDependencies,
4
5
  routeIdFromLocals
5
6
  } from "./page-notation.js";
@@ -9,7 +10,8 @@ async function labMathScope(notation, body = "", component) {
9
10
  const local = notation?.local ?? [];
10
11
  const scope = local.map((definition) => ({
11
12
  key: definition.key,
12
- notation: definition.latex
13
+ notation: definition.latex,
14
+ ...definition.fixedIndex === true ? { fixedIndex: true } : {}
13
15
  }));
14
16
  const referenced = /* @__PURE__ */ new Set();
15
17
  const explicitlyExplained = /* @__PURE__ */ new Set();
@@ -32,7 +34,11 @@ async function labMathScope(notation, body = "", component) {
32
34
  }
33
35
  const shared = sharedByKey.get(key);
34
36
  if (shared) {
35
- scope.push({ key: shared.key, notation: shared.notation });
37
+ scope.push({
38
+ key: shared.key,
39
+ notation: shared.notation,
40
+ ...shared.fixedIndex ? { fixedIndex: true } : {}
41
+ });
36
42
  resolvedKeys.add(key);
37
43
  continue;
38
44
  }
@@ -48,6 +54,11 @@ async function labMathScope(notation, body = "", component) {
48
54
  resolvedKeys,
49
55
  component.context
50
56
  );
57
+ bindScopeToPage(scope, {
58
+ locals: component.locals,
59
+ context: component.context,
60
+ checkIndices: !manifest.rules.disabled.includes("N23")
61
+ });
51
62
  }
52
63
  return scope;
53
64
  }