explico 0.12.0 → 0.13.1
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.
- package/CHANGELOG.md +1137 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +4 -3
- package/dist/compiler/astro.js +45 -15
- package/dist/compiler/cli.d.ts +26 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +9 -1
- package/dist/compiler/collections.js +55 -16
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +14 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +87 -6
- package/dist/compiler/manifest.js +174 -31
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +2 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +2 -2
- package/dist/reference/base-operators.mjs +2 -2
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +34 -4
- package/dist/reference/page-notation.js +54 -2
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +104 -10
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +84 -20
- package/dist/reference/render-lab-math.d.ts +2 -0
- package/dist/reference/render-lab-math.js +17 -5
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +191 -30
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +79 -24
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +32 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +111 -6
- package/docs/authoring/migrating.md +707 -0
- package/docs/authoring/notation.md +281 -29
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +10 -41
- package/docs/authoring/structure.md +53 -2
- package/docs/authoring/visualizations.md +144 -18
- package/package.json +12 -7
- package/src/compiler/astro.ts +57 -17
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +92 -19
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +299 -28
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +12 -9
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/figure/Figure.astro +11 -0
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +321 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +47 -9
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +13 -21
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +2 -2
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +100 -3
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +154 -8
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +84 -20
- package/src/reference/render-lab-math.ts +29 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +205 -29
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +291 -75
|
@@ -74,6 +74,27 @@ export function stripFencedCode(markdown: any): string;
|
|
|
74
74
|
* a transparent body do not change the surrounding page's section counter.
|
|
75
75
|
*/
|
|
76
76
|
export function stripJsxComponents(markdown: any, keep: any, transparent?: any[]): string;
|
|
77
|
+
/**
|
|
78
|
+
* Blank the body of every `:::name … :::` container directive — a Starlight
|
|
79
|
+
* aside (`:::note`, `:::tip`, …) — keeping newlines, the text-scan twin of a
|
|
80
|
+
* tree walk that never enters a `containerDirective`. An aside is a callout,
|
|
81
|
+
* like `<Aside>`: what it holds is illustrative and numbers nothing. A fence
|
|
82
|
+
* of more colons closes only on as many; one left open runs to the end.
|
|
83
|
+
*/
|
|
84
|
+
export function stripContainerDirectives(markdown: any): string;
|
|
85
|
+
/**
|
|
86
|
+
* Every `$$` display block in a Markdown text the way remark-math reads one:
|
|
87
|
+
* an opening fence of two or more `$` that starts a line — inside a list item
|
|
88
|
+
* or a blockquote too — with nothing but a `$`-free meta after it, up to a
|
|
89
|
+
* closing fence of at least as many `$` on a line of its own (or the end of
|
|
90
|
+
* the text). `$$x$$` inside a paragraph, or on one line, is inline math and
|
|
91
|
+
* is not a display block. Each block is `{ index, body }`: the offset of its
|
|
92
|
+
* opening line and the text between the fences.
|
|
93
|
+
*/
|
|
94
|
+
export function displayMathBlocks(markdown: any): {
|
|
95
|
+
index: number;
|
|
96
|
+
body: string;
|
|
97
|
+
}[];
|
|
77
98
|
/**
|
|
78
99
|
* Every display equation in a Markdown body, keyed or not, in appearance order
|
|
79
100
|
* with its assigned number and its `\caption{}` short caption (`short`, or
|
|
@@ -94,6 +115,12 @@ export function scanDisplayEquations(markdown: any, numbering: any): {
|
|
|
94
115
|
section: number;
|
|
95
116
|
indexInSection: number;
|
|
96
117
|
}[];
|
|
118
|
+
/**
|
|
119
|
+
* The keys of each display block that carries more than one `\label{eq:…}`,
|
|
120
|
+
* in order. A block is one numbered equation: its first label names it, and
|
|
121
|
+
* a later one would name nothing.
|
|
122
|
+
*/
|
|
123
|
+
export function repeatedEquationLabels(markdown: any): string[][];
|
|
97
124
|
/** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
|
|
98
125
|
export function scanEquationLabels(markdown: any, numbering: any): {
|
|
99
126
|
key: string | null | undefined;
|
|
@@ -204,6 +204,87 @@ export function stripJsxComponents(markdown, keep, transparent = []) {
|
|
|
204
204
|
*/
|
|
205
205
|
export const TRANSPARENT_COMPONENTS = Object.freeze(['Memo', 'ShortNote', 'Fold']);
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Blank the body of every `:::name … :::` container directive — a Starlight
|
|
209
|
+
* aside (`:::note`, `:::tip`, …) — keeping newlines, the text-scan twin of a
|
|
210
|
+
* tree walk that never enters a `containerDirective`. An aside is a callout,
|
|
211
|
+
* like `<Aside>`: what it holds is illustrative and numbers nothing. A fence
|
|
212
|
+
* of more colons closes only on as many; one left open runs to the end.
|
|
213
|
+
*/
|
|
214
|
+
export function stripContainerDirectives(markdown) {
|
|
215
|
+
const lines = String(markdown).split('\n');
|
|
216
|
+
let fence = 0;
|
|
217
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
218
|
+
const line = lines[index] ?? '';
|
|
219
|
+
if (fence === 0) {
|
|
220
|
+
const open = /^[ \t]{0,3}(:{3,})[A-Za-z]/.exec(line);
|
|
221
|
+
if (!open) continue;
|
|
222
|
+
fence = open[1].length;
|
|
223
|
+
lines[index] = line.replace(/[^\n]/g, ' ');
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const close = /^[ \t]{0,3}(:{3,})[ \t]*$/.exec(line);
|
|
227
|
+
if (close && close[1].length >= fence) fence = 0;
|
|
228
|
+
lines[index] = line.replace(/[^\n]/g, ' ');
|
|
229
|
+
}
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A line with its blockquote markers, list marker and indentation removed. */
|
|
234
|
+
function containerContent(line) {
|
|
235
|
+
let rest = String(line);
|
|
236
|
+
for (;;) {
|
|
237
|
+
const next = rest
|
|
238
|
+
.replace(/^[ \t]*>[ \t]?/, '')
|
|
239
|
+
.replace(/^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]+/, '');
|
|
240
|
+
if (next === rest) return rest.replace(/^[ \t]+/, '');
|
|
241
|
+
rest = next;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Every `$$` display block in a Markdown text the way remark-math reads one:
|
|
247
|
+
* an opening fence of two or more `$` that starts a line — inside a list item
|
|
248
|
+
* or a blockquote too — with nothing but a `$`-free meta after it, up to a
|
|
249
|
+
* closing fence of at least as many `$` on a line of its own (or the end of
|
|
250
|
+
* the text). `$$x$$` inside a paragraph, or on one line, is inline math and
|
|
251
|
+
* is not a display block. Each block is `{ index, body }`: the offset of its
|
|
252
|
+
* opening line and the text between the fences.
|
|
253
|
+
*/
|
|
254
|
+
export function displayMathBlocks(markdown) {
|
|
255
|
+
const text = String(markdown);
|
|
256
|
+
const lines = text.split('\n');
|
|
257
|
+
const starts = [];
|
|
258
|
+
let offset = 0;
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
starts.push(offset);
|
|
261
|
+
offset += line.length + 1;
|
|
262
|
+
}
|
|
263
|
+
const blocks = [];
|
|
264
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
265
|
+
const open = /^(\${2,})[ \t]*[^$]*$/.exec(containerContent(lines[index]));
|
|
266
|
+
if (!open) continue;
|
|
267
|
+
const size = open[1].length;
|
|
268
|
+
let end = lines.length;
|
|
269
|
+
for (let next = index + 1; next < lines.length; next += 1) {
|
|
270
|
+
const close = /^(\${2,})[ \t]*$/.exec(containerContent(lines[next]));
|
|
271
|
+
if (close && close[1].length >= size) {
|
|
272
|
+
end = next;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
blocks.push({
|
|
277
|
+
index: starts[index],
|
|
278
|
+
body: lines
|
|
279
|
+
.slice(index + 1, end)
|
|
280
|
+
.map((line) => containerContent(line))
|
|
281
|
+
.join('\n'),
|
|
282
|
+
});
|
|
283
|
+
index = end;
|
|
284
|
+
}
|
|
285
|
+
return blocks;
|
|
286
|
+
}
|
|
287
|
+
|
|
207
288
|
/**
|
|
208
289
|
* Every display equation in a Markdown body, keyed or not, in appearance order
|
|
209
290
|
* with its assigned number and its `\caption{}` short caption (`short`, or
|
|
@@ -219,25 +300,20 @@ export const TRANSPARENT_COMPONENTS = Object.freeze(['Memo', 'ShortNote', 'Fold'
|
|
|
219
300
|
*/
|
|
220
301
|
export function scanDisplayEquations(markdown, numbering) {
|
|
221
302
|
const { prefix, offset } = resolveNumbering(numbering);
|
|
222
|
-
const text =
|
|
223
|
-
stripFencedCode(markdown),
|
|
224
|
-
undefined,
|
|
225
|
-
TRANSPARENT_COMPONENTS,
|
|
226
|
-
);
|
|
303
|
+
const text = equationText(markdown);
|
|
227
304
|
const tokens = [];
|
|
228
305
|
const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
|
|
229
|
-
const display = /\$\$([\s\S]+?)\$\$/g;
|
|
230
306
|
for (const match of text.matchAll(heading)) {
|
|
231
307
|
tokens.push({ kind: 'section', index: match.index });
|
|
232
308
|
}
|
|
233
|
-
for (const
|
|
309
|
+
for (const block of displayMathBlocks(text)) {
|
|
234
310
|
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
235
|
-
const label = EQ_LABEL_PATTERN.exec(
|
|
311
|
+
const label = EQ_LABEL_PATTERN.exec(block.body);
|
|
236
312
|
tokens.push({
|
|
237
313
|
kind: 'equation',
|
|
238
|
-
index:
|
|
314
|
+
index: block.index,
|
|
239
315
|
key: label ? label[1] : null,
|
|
240
|
-
short: equationCaption(
|
|
316
|
+
short: equationCaption(block.body),
|
|
241
317
|
});
|
|
242
318
|
}
|
|
243
319
|
tokens.sort((a, b) => a.index - b.index);
|
|
@@ -271,6 +347,31 @@ export function scanDisplayEquations(markdown, numbering) {
|
|
|
271
347
|
return labels;
|
|
272
348
|
}
|
|
273
349
|
|
|
350
|
+
/**
|
|
351
|
+
* A lesson body as the equation scans read it: no fenced code, no component
|
|
352
|
+
* but the transparent ones, no `:::` directive body.
|
|
353
|
+
*/
|
|
354
|
+
function equationText(markdown) {
|
|
355
|
+
return stripContainerDirectives(
|
|
356
|
+
stripJsxComponents(stripFencedCode(markdown), undefined, TRANSPARENT_COMPONENTS),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The keys of each display block that carries more than one `\label{eq:…}`,
|
|
362
|
+
* in order. A block is one numbered equation: its first label names it, and
|
|
363
|
+
* a later one would name nothing.
|
|
364
|
+
*/
|
|
365
|
+
export function repeatedEquationLabels(markdown) {
|
|
366
|
+
return displayMathBlocks(equationText(markdown))
|
|
367
|
+
// A copy: `matchAll` starts at the shared pattern's `lastIndex`, which
|
|
368
|
+
// `scanDisplayEquations`' `exec` leaves past the label it found.
|
|
369
|
+
.map((block) =>
|
|
370
|
+
[...block.body.matchAll(new RegExp(EQ_LABEL_PATTERN.source, 'g'))].map((match) => match[1]),
|
|
371
|
+
)
|
|
372
|
+
.filter((keys) => keys.length > 1);
|
|
373
|
+
}
|
|
374
|
+
|
|
274
375
|
/** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
|
|
275
376
|
export function scanEquationLabels(markdown, numbering) {
|
|
276
377
|
return scanDisplayEquations(markdown, numbering).filter(
|
|
@@ -301,13 +402,16 @@ export function collectDisplayEquations(tree, numbering) {
|
|
|
301
402
|
let indexInSection = 0;
|
|
302
403
|
let indexInLesson = 0;
|
|
303
404
|
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
405
|
+
// The lesson's flow — top-level blocks, list items, blockquotes — plus the
|
|
406
|
+
// bodies of the `TRANSPARENT_COMPONENTS`: other MDX components (a
|
|
407
|
+
// `<CompactExample>` worked example, an `<Aside>`) and `:::note`-style
|
|
408
|
+
// asides remain illustrative and outside the lesson's official equation
|
|
409
|
+
// sequence. The transparent ones occupy an exact point in the reading
|
|
410
|
+
// order, even while visually collapsed. `scanDisplayEquations` reads the
|
|
411
|
+
// same set from the text.
|
|
309
412
|
const collectFromDetail = (node) => {
|
|
310
413
|
if (!node || typeof node !== 'object') return;
|
|
414
|
+
if (node.type === 'containerDirective') return;
|
|
311
415
|
if (node.type === 'math') {
|
|
312
416
|
indexInSection += 1;
|
|
313
417
|
indexInLesson += 1;
|
|
@@ -347,13 +451,7 @@ export function collectDisplayEquations(tree, numbering) {
|
|
|
347
451
|
indexInSection = 0;
|
|
348
452
|
continue;
|
|
349
453
|
}
|
|
350
|
-
|
|
351
|
-
else if (
|
|
352
|
-
node.type === 'mdxJsxFlowElement' &&
|
|
353
|
-
TRANSPARENT_COMPONENTS.includes(node.name)
|
|
354
|
-
) {
|
|
355
|
-
collectFromDetail(node);
|
|
356
|
-
}
|
|
454
|
+
collectFromDetail(node);
|
|
357
455
|
}
|
|
358
456
|
return equations;
|
|
359
457
|
}
|
|
@@ -12,7 +12,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
12
12
|
* no default title to fall back on.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
type ExampleDiagnosticCode = 'ex-invalid-key' | 'ex-duplicate-key' | 'ex-ref-resolves' | 'ex-key-unused' | 'ex-removed-prop' | 'ex-missing-prop';
|
|
15
|
+
type ExampleDiagnosticCode = 'ex-invalid-key' | 'ex-duplicate-key' | 'ex-ref-resolves' | 'ex-key-unused' | 'ex-removed-prop' | 'ex-unread-prop' | 'ex-missing-prop';
|
|
16
16
|
interface ExampleDiagnostic {
|
|
17
17
|
readonly code: ExampleDiagnosticCode;
|
|
18
18
|
readonly severity: 'error' | 'warning' | 'info';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule V7 — a figure's picture fits on one screen.
|
|
3
|
+
*
|
|
4
|
+
* A picture's height is only known once the browser has laid the page out and
|
|
5
|
+
* hydrated its islands, so this check runs in a course's Playwright suite, not
|
|
6
|
+
* in the build: open each lesson that has a figure, call
|
|
7
|
+
* {@link checkFigureFit}, and expect no findings. The pure half,
|
|
8
|
+
* {@link figureFitFindings}, turns measured heights into findings and is what
|
|
9
|
+
* the unit tests pin.
|
|
10
|
+
*/
|
|
11
|
+
/** The share of the screen height a picture may take unless the course sets `rules.V7.maxPercent`. */
|
|
12
|
+
declare const FIGURE_FIT_MAX_PERCENT = 80;
|
|
13
|
+
/** One `<Figure>` as measured on a rendered page. */
|
|
14
|
+
interface FigureMeasurement {
|
|
15
|
+
/** The figure's anchor, `fig-<key>`. */
|
|
16
|
+
readonly anchor: string;
|
|
17
|
+
/** Its number, `2.1.3`. */
|
|
18
|
+
readonly number: string;
|
|
19
|
+
/** Height of the picture — the figure's default slot — in CSS pixels. */
|
|
20
|
+
readonly height: number;
|
|
21
|
+
}
|
|
22
|
+
interface FigureFitFinding {
|
|
23
|
+
readonly code: 'figure-too-tall';
|
|
24
|
+
readonly severity: 'warning';
|
|
25
|
+
readonly rule: 'V7';
|
|
26
|
+
readonly anchor: string;
|
|
27
|
+
readonly number: string;
|
|
28
|
+
readonly height: number;
|
|
29
|
+
/** The tallest picture the budget allows at this screen height, in CSS pixels. */
|
|
30
|
+
readonly limit: number;
|
|
31
|
+
readonly message: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The pictures taller than `maxPercent` of a `screenHeight`-pixel screen.
|
|
35
|
+
* A picture of height zero is skipped: nothing was drawn to judge.
|
|
36
|
+
*/
|
|
37
|
+
declare function figureFitFindings(measured: readonly FigureMeasurement[], screenHeight: number, maxPercent?: number): FigureFitFinding[];
|
|
38
|
+
/** The part of a Playwright `Page` the check uses. */
|
|
39
|
+
interface FigureFitPage {
|
|
40
|
+
evaluate<R>(pageFunction: () => R | Promise<R>): Promise<R>;
|
|
41
|
+
viewportSize(): {
|
|
42
|
+
readonly width: number;
|
|
43
|
+
readonly height: number;
|
|
44
|
+
} | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Measure every `<Figure>` on the open page and report the pictures that break
|
|
48
|
+
* rule V7 at the page's current viewport. A figure inside a closed `<Fold>` or
|
|
49
|
+
* `<details>` is opened first, since a reader can open it; each figure is
|
|
50
|
+
* scrolled into view and waits for its islands to hydrate and its height to
|
|
51
|
+
* settle before it is measured.
|
|
52
|
+
*/
|
|
53
|
+
declare function checkFigureFit(page: FigureFitPage, options?: {
|
|
54
|
+
readonly maxPercent?: number;
|
|
55
|
+
/** Figures exempt from the budget, by anchor (`fig-<key>`), each with the reason it may be taller. */
|
|
56
|
+
readonly allow?: Readonly<Record<string, string>>;
|
|
57
|
+
}): Promise<FigureFitFinding[]>;
|
|
58
|
+
/**
|
|
59
|
+
* The lesson paths whose manifest entry has at least one figure — the pages a
|
|
60
|
+
* course's V7 test opens. Pass the manifest the build wrote
|
|
61
|
+
* (`build/manifest.json`).
|
|
62
|
+
*/
|
|
63
|
+
declare function lessonsWithFigures(manifest: {
|
|
64
|
+
readonly lessons: readonly {
|
|
65
|
+
readonly slug: string;
|
|
66
|
+
readonly figures: readonly unknown[];
|
|
67
|
+
}[];
|
|
68
|
+
}): string[];
|
|
69
|
+
|
|
70
|
+
export { FIGURE_FIT_MAX_PERCENT, type FigureFitFinding, type FigureFitPage, type FigureMeasurement, checkFigureFit, figureFitFindings, lessonsWithFigures };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const FIGURE_FIT_MAX_PERCENT = 80;
|
|
2
|
+
function figureFitFindings(measured, screenHeight, maxPercent = FIGURE_FIT_MAX_PERCENT) {
|
|
3
|
+
const limit = Math.floor(screenHeight * maxPercent / 100);
|
|
4
|
+
return measured.filter((figure) => figure.height > limit).map((figure) => ({
|
|
5
|
+
code: "figure-too-tall",
|
|
6
|
+
severity: "warning",
|
|
7
|
+
rule: "V7",
|
|
8
|
+
anchor: figure.anchor,
|
|
9
|
+
number: figure.number,
|
|
10
|
+
height: figure.height,
|
|
11
|
+
limit,
|
|
12
|
+
message: `Figure ${figure.number} (#${figure.anchor}): its picture is ${figure.height}px tall, ${Math.round(figure.height / screenHeight * 100)}% of a ${screenHeight}px screen; rule V7 allows ${maxPercent}% (${limit}px). Give a square or tall plot a fixed width instead of the full column, or split stacked panels.`
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
async function checkFigureFit(page, options = {}) {
|
|
16
|
+
const viewport = page.viewportSize();
|
|
17
|
+
if (!viewport) throw new Error("checkFigureFit needs a page with a fixed viewport size.");
|
|
18
|
+
const measured = await page.evaluate(measureFigures);
|
|
19
|
+
const allow = options.allow ?? {};
|
|
20
|
+
for (const [anchor, reason] of Object.entries(allow)) {
|
|
21
|
+
if (!reason.trim()) throw new Error(`checkFigureFit: the exemption for #${anchor} needs a reason.`);
|
|
22
|
+
}
|
|
23
|
+
return figureFitFindings(
|
|
24
|
+
measured.filter((figure) => !(figure.anchor in allow)),
|
|
25
|
+
viewport.height,
|
|
26
|
+
options.maxPercent
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function lessonsWithFigures(manifest) {
|
|
30
|
+
return manifest.lessons.filter((lesson) => lesson.figures.length > 0).map((lesson) => `/${lesson.slug}/`);
|
|
31
|
+
}
|
|
32
|
+
async function measureFigures() {
|
|
33
|
+
const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const figure of document.querySelectorAll("figure.keyed-figure")) {
|
|
36
|
+
for (let fold = figure.parentElement?.closest("details"); fold; fold = fold.parentElement?.closest("details")) {
|
|
37
|
+
fold.open = true;
|
|
38
|
+
}
|
|
39
|
+
const body = figure.querySelector(":scope > .keyed-figure__body");
|
|
40
|
+
if (!body) continue;
|
|
41
|
+
figure.scrollIntoView({ block: "start" });
|
|
42
|
+
const deadline = performance.now() + 5e3;
|
|
43
|
+
while (body.querySelector("astro-island[ssr]") && performance.now() < deadline) await nextFrame();
|
|
44
|
+
let height = -1;
|
|
45
|
+
for (let still = 0, frames = 0; still < 5 && frames < 120; frames++) {
|
|
46
|
+
await nextFrame();
|
|
47
|
+
const now = body.getBoundingClientRect().height;
|
|
48
|
+
still = Math.abs(now - height) < 0.5 ? still + 1 : 0;
|
|
49
|
+
height = now;
|
|
50
|
+
}
|
|
51
|
+
out.push({ anchor: figure.id, number: figure.dataset.figNumber ?? "", height: Math.round(height) });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
FIGURE_FIT_MAX_PERCENT,
|
|
57
|
+
checkFigureFit,
|
|
58
|
+
figureFitFindings,
|
|
59
|
+
lessonsWithFigures
|
|
60
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every figure has its picture in the built page (rule V3). A lab draws its
|
|
3
|
+
* plot in the browser, after the page loads, so without a copy drawn at
|
|
4
|
+
* build time the page holds an empty box until then: a reference card made
|
|
5
|
+
* from the page (from another page, or before the figure scrolls into view)
|
|
6
|
+
* has only the caption, and so do print without JavaScript and the
|
|
7
|
+
* standalone HTML export. A lab draws its default picture at build time and
|
|
8
|
+
* hands it to its island as children (`still-plot.ts` in the playground).
|
|
9
|
+
*
|
|
10
|
+
* `undrawnFigurePictures` reads rendered HTML: a `figure.keyed-figure` whose
|
|
11
|
+
* picture (`.keyed-figure__body`) holds an `<astro-island>` but nothing drawn
|
|
12
|
+
* (`svg`, `img`, `picture`, `canvas`, `video` or `table`) is reported.
|
|
13
|
+
* `compiler/astro.ts` runs it on every built page and warns; it is a
|
|
14
|
+
* warning, not an error, since the figure still works once hydrated.
|
|
15
|
+
*/
|
|
16
|
+
/** The diagnostic a built page with an undrawn picture gets. */
|
|
17
|
+
interface UndrawnFigurePicture {
|
|
18
|
+
readonly code: 'lab-picture-undrawn';
|
|
19
|
+
/** The figure's anchor id, e.g. `fig-launch-angle-explorer`. */
|
|
20
|
+
readonly figure: string;
|
|
21
|
+
}
|
|
22
|
+
/** The figures in `html` whose picture is an island with nothing drawn. */
|
|
23
|
+
declare function undrawnFigurePictures(html: string): UndrawnFigurePicture[];
|
|
24
|
+
/** The build's warning for one page, naming each figure and what to do. */
|
|
25
|
+
declare function undrawnFigureWarning(page: string, figures: readonly UndrawnFigurePicture[]): string;
|
|
26
|
+
|
|
27
|
+
export { type UndrawnFigurePicture, undrawnFigurePictures, undrawnFigureWarning };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parse } from "parse5";
|
|
2
|
+
const DRAWN = /* @__PURE__ */ new Set(["svg", "img", "picture", "canvas", "video", "table"]);
|
|
3
|
+
function isElement(node) {
|
|
4
|
+
return "tagName" in node;
|
|
5
|
+
}
|
|
6
|
+
function attribute(node, name) {
|
|
7
|
+
return node.attrs.find((attr) => attr.name === name)?.value;
|
|
8
|
+
}
|
|
9
|
+
function hasClass(node, name) {
|
|
10
|
+
return (attribute(node, "class") ?? "").split(/\s+/u).includes(name);
|
|
11
|
+
}
|
|
12
|
+
function childrenOf(node) {
|
|
13
|
+
if (isElement(node) && node.tagName === "template") return [];
|
|
14
|
+
return "childNodes" in node ? node.childNodes : [];
|
|
15
|
+
}
|
|
16
|
+
function find(node, test) {
|
|
17
|
+
for (const child of childrenOf(node)) {
|
|
18
|
+
if (!isElement(child)) continue;
|
|
19
|
+
if (test(child)) return child;
|
|
20
|
+
const inner = find(child, test);
|
|
21
|
+
if (inner) return inner;
|
|
22
|
+
}
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
function undrawnFigurePictures(html) {
|
|
26
|
+
if (!html.includes("keyed-figure") || !html.includes("astro-island")) return [];
|
|
27
|
+
const found = [];
|
|
28
|
+
const visit = (node) => {
|
|
29
|
+
for (const child of childrenOf(node)) {
|
|
30
|
+
if (!isElement(child)) continue;
|
|
31
|
+
if (child.tagName === "figure" && hasClass(child, "keyed-figure")) {
|
|
32
|
+
const body = find(child, (el) => hasClass(el, "keyed-figure__body"));
|
|
33
|
+
if (body && find(body, (el) => el.tagName === "astro-island") && !find(body, (el) => DRAWN.has(el.tagName))) {
|
|
34
|
+
found.push({ code: "lab-picture-undrawn", figure: attribute(child, "id") ?? "" });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
visit(child);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
visit(parse(html));
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
function undrawnFigureWarning(page, figures) {
|
|
44
|
+
return `lab-picture-undrawn (rule V3): on rendered page "${page}", ` + figures.map(({ figure }) => `#${figure}`).join(", ") + " draw nothing until the browser runs the island, so a preview card, print without JavaScript and the HTML export show no picture. Draw the default picture at build time and pass it to the island as its children.";
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
undrawnFigurePictures,
|
|
48
|
+
undrawnFigureWarning
|
|
49
|
+
};
|
|
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
2
2
|
|
|
3
3
|
/** `validateKeyedFloats` for `<Figure>` — see `keyed-float-validate.ts`. */
|
|
4
4
|
|
|
5
|
-
type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused' | 'fig-removed-prop';
|
|
5
|
+
type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused' | 'fig-removed-prop' | 'fig-unread-prop';
|
|
6
6
|
interface FigureDiagnostic {
|
|
7
7
|
readonly code: FigureDiagnosticCode;
|
|
8
8
|
readonly severity: 'error' | 'warning' | 'info';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
2
|
import { NotationRegistryInput, AbbreviationDefinitionInput } from './types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { h as CourseAbbreviations } from '../course-config-rRtME5UV.js';
|
|
4
4
|
import '../rules/catalog.js';
|
|
5
5
|
import '../rules/course-rules.js';
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ function findAbbreviationLikeTokens(text) {
|
|
|
14
14
|
}
|
|
15
15
|
return out.sort((a, b) => a.index - b.index);
|
|
16
16
|
}
|
|
17
|
-
const FIX = "Add it to an abbreviations-table entry (content/abbreviations/*.md), allowlist it in
|
|
17
|
+
const FIX = "Add it to an abbreviations-table entry (content/abbreviations/*.md), allowlist it in rules.N14.allow in the course config (course.config.ts, or the course.settings.json it reads) if it is common, non-technical usage, or \u2014 if it names a math quantity \u2014 introduce it as \\mathrm{\u2026} (or \\operatorname{\u2026}) bound to a notation entry the reader can hover. See docs/authoring/notation.md#n14.";
|
|
18
18
|
function gateAbbreviations(input) {
|
|
19
19
|
const { notationInput, abbreviations, courseAbbreviations } = input;
|
|
20
20
|
const tableTokens = /* @__PURE__ */ new Set();
|
|
@@ -9,9 +9,10 @@ import '../rules/catalog.js';
|
|
|
9
9
|
*
|
|
10
10
|
* One loop over every enabled course rule's `forbid` patterns, matched against
|
|
11
11
|
* the same "is this really prose" surface rule N9 uses — `proseOnly` blanks
|
|
12
|
-
* code fences, inline code, `$…$` / `$$…$$`, `[[key]]` references,
|
|
13
|
-
* lines, JSX tags and MDX comments to spaces, so offsets
|
|
14
|
-
*
|
|
12
|
+
* code fences, inline code, `$…$` / `$$…$$`, `[[key]]` references, link
|
|
13
|
+
* addresses, `import` lines, JSX tags and MDX comments to spaces, so offsets
|
|
14
|
+
* still point at the source — plus the text of each visible prop (`caption`,
|
|
15
|
+
* `short`, `title`, `label`) as a paragraph of its own. A match becomes a `MathGateDiagnostic` with the rule's own ID as its
|
|
15
16
|
* code and a lesson `file` + `line`, which is what lets `{/* explico-allow X1:
|
|
16
17
|
* reason *\/}` waive one place.
|
|
17
18
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lessonLine, proseOnly } from "./gate-prose-notation.js";
|
|
2
|
+
import { propStrings } from "./gate-prop-text.js";
|
|
2
3
|
const HEADING = /^[ \t]{0,3}(#{1,6})[ \t]+(.*)$/;
|
|
3
4
|
function paragraphs(text) {
|
|
4
5
|
const units = [];
|
|
@@ -32,14 +33,18 @@ function headings(text) {
|
|
|
32
33
|
}
|
|
33
34
|
return units;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
const VISIBLE_PROPS = /* @__PURE__ */ new Set(["caption", "short", "title", "label"]);
|
|
37
|
+
function visibleProps(body) {
|
|
38
|
+
return propStrings(body).filter((prop) => VISIBLE_PROPS.has(prop.name) && prop.value.trim() !== "").map((prop) => ({ text: prop.value, index: prop.offset }));
|
|
39
|
+
}
|
|
40
|
+
function unitsFor(scope, prose, body) {
|
|
36
41
|
switch (scope) {
|
|
37
42
|
case "prose":
|
|
38
|
-
return [{ text: prose, index: 0 }];
|
|
43
|
+
return [{ text: prose, index: 0 }, ...visibleProps(body)];
|
|
39
44
|
case "heading":
|
|
40
45
|
return headings(prose);
|
|
41
46
|
case "paragraph":
|
|
42
|
-
return paragraphs(prose);
|
|
47
|
+
return [...paragraphs(prose), ...visibleProps(body)];
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
function* matches(pattern, unit) {
|
|
@@ -59,7 +64,7 @@ function gateCourseRules(lessons, rules) {
|
|
|
59
64
|
const cache = /* @__PURE__ */ new Map();
|
|
60
65
|
for (const rule of active) {
|
|
61
66
|
for (const pattern of rule.forbid) {
|
|
62
|
-
if (!cache.has(pattern.scope)) cache.set(pattern.scope, unitsFor(pattern.scope, prose));
|
|
67
|
+
if (!cache.has(pattern.scope)) cache.set(pattern.scope, unitsFor(pattern.scope, prose, lesson.body));
|
|
63
68
|
for (const unit of cache.get(pattern.scope)) {
|
|
64
69
|
for (const [index, token] of matches(pattern, unit)) {
|
|
65
70
|
diagnostics.push({
|
|
@@ -10,6 +10,8 @@ declare const HEADING_LENGTH_LIMIT = 65;
|
|
|
10
10
|
*/
|
|
11
11
|
declare function gateHeadingLength(lessons: readonly NotationLessonInput[], options?: {
|
|
12
12
|
readonly maxLength?: number;
|
|
13
|
+
/** A `[[key]]` term's label on a lesson's page, as a heading shows it. */
|
|
14
|
+
readonly labelFor?: (lessonId: string, key: string) => string | undefined;
|
|
13
15
|
}): MathGateDiagnostic[];
|
|
14
16
|
|
|
15
17
|
export { HEADING_LENGTH_LIMIT, gateHeadingLength };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EQ_CAPTION_PATTERN, stripFencedCode } from "./equations.mjs";
|
|
2
2
|
import { stripHeadingMarker } from "./rehype-heading-anchors.mjs";
|
|
3
|
+
import { headingDisplayText } from "./sections.mjs";
|
|
3
4
|
import { lessonLine } from "./gate-prose-notation.js";
|
|
4
5
|
const HEADING_LENGTH_LIMIT = 65;
|
|
5
6
|
const HEADING_LINE = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm;
|
|
@@ -20,13 +21,13 @@ function readAttribute(attributesSource, name) {
|
|
|
20
21
|
function overLimitMessage(kind, text, limit) {
|
|
21
22
|
return `${kind} ${JSON.stringify(text)} is ${text.length} characters, over the ${limit}-character limit (rule S1, rules.S1.maxLength in the course settings). Shorten it and move the rest into the paragraph or caption that follows.`;
|
|
22
23
|
}
|
|
23
|
-
function headingFindings(text, limit) {
|
|
24
|
+
function headingFindings(text, limit, labelFor) {
|
|
24
25
|
const out = [];
|
|
25
26
|
const re = new RegExp(HEADING_LINE.source, HEADING_LINE.flags);
|
|
26
27
|
let match;
|
|
27
28
|
while (match = re.exec(text)) {
|
|
28
29
|
const level = match[1]?.length ?? 1;
|
|
29
|
-
const title = stripHeadingMarker(match[2] ?? "")
|
|
30
|
+
const title = headingDisplayText(stripHeadingMarker(match[2] ?? ""), { labelFor });
|
|
30
31
|
if (title.length > limit) {
|
|
31
32
|
out.push({
|
|
32
33
|
index: match.index,
|
|
@@ -71,9 +72,9 @@ function equationCaptionFindings(text, limit) {
|
|
|
71
72
|
}
|
|
72
73
|
return out;
|
|
73
74
|
}
|
|
74
|
-
function findingsIn(text, limit) {
|
|
75
|
+
function findingsIn(text, limit, labelFor) {
|
|
75
76
|
return [
|
|
76
|
-
...headingFindings(text, limit),
|
|
77
|
+
...headingFindings(text, limit, labelFor),
|
|
77
78
|
...floatTitleFindings(text, limit),
|
|
78
79
|
...equationCaptionFindings(text, limit)
|
|
79
80
|
].sort((a, b) => a.index - b.index);
|
|
@@ -83,7 +84,8 @@ function gateHeadingLength(lessons, options = {}) {
|
|
|
83
84
|
const limit = options.maxLength ?? HEADING_LENGTH_LIMIT;
|
|
84
85
|
for (const lesson of lessons) {
|
|
85
86
|
const text = stripFencedCode(lesson.body);
|
|
86
|
-
|
|
87
|
+
const labelFor = options.labelFor ? (key) => options.labelFor?.(lesson.lessonId, key) : void 0;
|
|
88
|
+
for (const finding of findingsIn(text, limit, labelFor)) {
|
|
87
89
|
diagnostics.push({
|
|
88
90
|
context: "structure",
|
|
89
91
|
code: "title-length",
|
|
@@ -67,6 +67,31 @@ function skipFindings(blocks) {
|
|
|
67
67
|
}
|
|
68
68
|
return out;
|
|
69
69
|
}
|
|
70
|
+
const HOSTS = ["Memo", "ShortNote"];
|
|
71
|
+
function nestedFindings(body) {
|
|
72
|
+
const blank = (match) => match.replace(/[^\n]/g, " ");
|
|
73
|
+
const text = body.replace(/```[\s\S]*?```/g, blank).replace(/\{\/\*[\s\S]*?\*\/\}/g, blank);
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const host of HOSTS) {
|
|
76
|
+
const opening = new RegExp(`<${host}\\b(?:[^>"']|"[^"]*"|'[^']*')*(?<!/)>`, "g");
|
|
77
|
+
for (const open of text.matchAll(opening)) {
|
|
78
|
+
const start = (open.index ?? 0) + open[0].length;
|
|
79
|
+
const end = text.indexOf(`</${host}>`, start);
|
|
80
|
+
if (end === -1) continue;
|
|
81
|
+
for (const inner of text.slice(start, end).matchAll(/<(AssessmentSet|CompactExamples)\b/g)) {
|
|
82
|
+
const name = inner[1];
|
|
83
|
+
const what = name === "CompactExamples" ? "set of worked examples" : "knowledge check";
|
|
84
|
+
out.push({
|
|
85
|
+
code: "heading-nested",
|
|
86
|
+
index: start + (inner.index ?? 0),
|
|
87
|
+
token: `<${name}>`,
|
|
88
|
+
message: `\`<${name}>\` sits inside a \`<${host}>\`, ${TITLED_COMPONENTS[name].what}, ${host === "Memo" ? "under the memo\u2019s own `<h6>`" : "in the middle of the note"}: the heading order jumps, and "On this page" never lists it (rule S6). A ${what} is a section of the lesson: put it at the top level, after the ${host === "Memo" ? "memo" : "paragraph"}.`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
70
95
|
function adjacentFindings(blocks, lesson) {
|
|
71
96
|
const out = [];
|
|
72
97
|
blocks.forEach((block, position) => {
|
|
@@ -92,6 +117,7 @@ function gateHeadingStructure(lessons, options = {}) {
|
|
|
92
117
|
const blocks = blocksOf(lesson.body);
|
|
93
118
|
const findings = [
|
|
94
119
|
...levels ? skipFindings(blocks) : [],
|
|
120
|
+
...levels ? nestedFindings(lesson.body) : [],
|
|
95
121
|
...adjacency ? adjacentFindings(blocks, lesson) : []
|
|
96
122
|
].sort((a, b) => a.index - b.index);
|
|
97
123
|
for (const finding of findings) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ScopeEntry, AssessmentLike, LessonAssessmentLink, MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import { ResolvedGloss } from './gloss.js';
|
|
3
|
+
import { NotationLessonInput, DerivedUse, IndexedSymbol } from './types.js';
|
|
4
|
+
|
|
5
|
+
/** What the check needs of an entry besides its glyph-table row. */
|
|
6
|
+
interface IndexEntryFacts extends ScopeEntry {
|
|
7
|
+
readonly range?: string;
|
|
8
|
+
readonly formula?: string;
|
|
9
|
+
readonly glosses?: readonly ResolvedGloss[];
|
|
10
|
+
readonly source?: {
|
|
11
|
+
readonly file: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Run rule N23 over every lesson (and non-chapter page) body, every notation
|
|
16
|
+
* formula and every assessment, and collect the derived cards each body
|
|
17
|
+
* uses. `entries` holds every shared entry by key; a lesson's own come from
|
|
18
|
+
* its `localDefinitions`. The index-set check (`index-range`) reads lesson
|
|
19
|
+
* bodies only: a letter's set is stated on its card or on the page.
|
|
20
|
+
*/
|
|
21
|
+
declare function gateIndices(lessons: readonly NotationLessonInput[], entries: ReadonlyMap<string, IndexEntryFacts>, extras?: {
|
|
22
|
+
readonly assessments?: readonly AssessmentLike[];
|
|
23
|
+
readonly lessonAssessments?: readonly LessonAssessmentLink[];
|
|
24
|
+
}): {
|
|
25
|
+
diagnostics: MathGateDiagnostic[];
|
|
26
|
+
derivedByLesson: Map<string, DerivedUse[]>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Rule N23's findings in one math source rendered outside a lesson body — a
|
|
30
|
+
* lab's or a diagram's, which fail the build as they render — as one line
|
|
31
|
+
* each: the form, what explains none of it, and the fix.
|
|
32
|
+
*/
|
|
33
|
+
declare function indexFindings(indices: readonly IndexedSymbol[]): string[];
|
|
34
|
+
|
|
35
|
+
export { DerivedUse, type IndexEntryFacts, gateIndices, indexFindings };
|