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
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
|
|
12
|
+
/** The share of the screen height a picture may take unless the course sets `rules.V7.maxPercent`. */
|
|
13
|
+
export const FIGURE_FIT_MAX_PERCENT = 80;
|
|
14
|
+
|
|
15
|
+
/** One `<Figure>` as measured on a rendered page. */
|
|
16
|
+
export interface FigureMeasurement {
|
|
17
|
+
/** The figure's anchor, `fig-<key>`. */
|
|
18
|
+
readonly anchor: string;
|
|
19
|
+
/** Its number, `2.1.3`. */
|
|
20
|
+
readonly number: string;
|
|
21
|
+
/** Height of the picture — the figure's default slot — in CSS pixels. */
|
|
22
|
+
readonly height: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FigureFitFinding {
|
|
26
|
+
readonly code: 'figure-too-tall';
|
|
27
|
+
readonly severity: 'warning';
|
|
28
|
+
readonly rule: 'V7';
|
|
29
|
+
readonly anchor: string;
|
|
30
|
+
readonly number: string;
|
|
31
|
+
readonly height: number;
|
|
32
|
+
/** The tallest picture the budget allows at this screen height, in CSS pixels. */
|
|
33
|
+
readonly limit: number;
|
|
34
|
+
readonly message: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The pictures taller than `maxPercent` of a `screenHeight`-pixel screen.
|
|
39
|
+
* A picture of height zero is skipped: nothing was drawn to judge.
|
|
40
|
+
*/
|
|
41
|
+
export function figureFitFindings(
|
|
42
|
+
measured: readonly FigureMeasurement[],
|
|
43
|
+
screenHeight: number,
|
|
44
|
+
maxPercent: number = FIGURE_FIT_MAX_PERCENT,
|
|
45
|
+
): FigureFitFinding[] {
|
|
46
|
+
const limit = Math.floor((screenHeight * maxPercent) / 100);
|
|
47
|
+
return measured
|
|
48
|
+
.filter((figure) => figure.height > limit)
|
|
49
|
+
.map((figure) => ({
|
|
50
|
+
code: 'figure-too-tall',
|
|
51
|
+
severity: 'warning',
|
|
52
|
+
rule: 'V7',
|
|
53
|
+
anchor: figure.anchor,
|
|
54
|
+
number: figure.number,
|
|
55
|
+
height: figure.height,
|
|
56
|
+
limit,
|
|
57
|
+
message:
|
|
58
|
+
`Figure ${figure.number} (#${figure.anchor}): its picture is ${figure.height}px tall, ` +
|
|
59
|
+
`${Math.round((figure.height / screenHeight) * 100)}% of a ${screenHeight}px screen; ` +
|
|
60
|
+
`rule V7 allows ${maxPercent}% (${limit}px). Give a square or tall plot a fixed width ` +
|
|
61
|
+
`instead of the full column, or split stacked panels.`,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The part of a Playwright `Page` the check uses. */
|
|
66
|
+
export interface FigureFitPage {
|
|
67
|
+
evaluate<R>(pageFunction: () => R | Promise<R>): Promise<R>;
|
|
68
|
+
viewportSize(): { readonly width: number; readonly height: number } | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Measure every `<Figure>` on the open page and report the pictures that break
|
|
73
|
+
* rule V7 at the page's current viewport. A figure inside a closed `<Fold>` or
|
|
74
|
+
* `<details>` is opened first, since a reader can open it; each figure is
|
|
75
|
+
* scrolled into view and waits for its islands to hydrate and its height to
|
|
76
|
+
* settle before it is measured.
|
|
77
|
+
*/
|
|
78
|
+
export async function checkFigureFit(
|
|
79
|
+
page: FigureFitPage,
|
|
80
|
+
options: {
|
|
81
|
+
readonly maxPercent?: number;
|
|
82
|
+
/** Figures exempt from the budget, by anchor (`fig-<key>`), each with the reason it may be taller. */
|
|
83
|
+
readonly allow?: Readonly<Record<string, string>>;
|
|
84
|
+
} = {},
|
|
85
|
+
): Promise<FigureFitFinding[]> {
|
|
86
|
+
const viewport = page.viewportSize();
|
|
87
|
+
if (!viewport) throw new Error('checkFigureFit needs a page with a fixed viewport size.');
|
|
88
|
+
const measured = await page.evaluate(measureFigures);
|
|
89
|
+
const allow = options.allow ?? {};
|
|
90
|
+
for (const [anchor, reason] of Object.entries(allow)) {
|
|
91
|
+
if (!reason.trim()) throw new Error(`checkFigureFit: the exemption for #${anchor} needs a reason.`);
|
|
92
|
+
}
|
|
93
|
+
return figureFitFindings(
|
|
94
|
+
measured.filter((figure) => !(figure.anchor in allow)),
|
|
95
|
+
viewport.height,
|
|
96
|
+
options.maxPercent,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The lesson paths whose manifest entry has at least one figure — the pages a
|
|
102
|
+
* course's V7 test opens. Pass the manifest the build wrote
|
|
103
|
+
* (`build/manifest.json`).
|
|
104
|
+
*/
|
|
105
|
+
export function lessonsWithFigures(manifest: {
|
|
106
|
+
readonly lessons: readonly { readonly slug: string; readonly figures: readonly unknown[] }[];
|
|
107
|
+
}): string[] {
|
|
108
|
+
return manifest.lessons.filter((lesson) => lesson.figures.length > 0).map((lesson) => `/${lesson.slug}/`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Runs in the browser, so it must not close over anything in this module.
|
|
113
|
+
*/
|
|
114
|
+
async function measureFigures(): Promise<FigureMeasurement[]> {
|
|
115
|
+
const nextFrame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
116
|
+
const out: FigureMeasurement[] = [];
|
|
117
|
+
for (const figure of document.querySelectorAll<HTMLElement>('figure.keyed-figure')) {
|
|
118
|
+
for (let fold = figure.parentElement?.closest('details'); fold; fold = fold.parentElement?.closest('details')) {
|
|
119
|
+
fold.open = true;
|
|
120
|
+
}
|
|
121
|
+
const body = figure.querySelector<HTMLElement>(':scope > .keyed-figure__body');
|
|
122
|
+
if (!body) continue;
|
|
123
|
+
figure.scrollIntoView({ block: 'start' });
|
|
124
|
+
// An island keeps its `ssr` attribute until it has hydrated.
|
|
125
|
+
const deadline = performance.now() + 5000;
|
|
126
|
+
while (body.querySelector('astro-island[ssr]') && performance.now() < deadline) await nextFrame();
|
|
127
|
+
// Then wait for the height to hold still for a few frames: a chart may
|
|
128
|
+
// size itself from its container after it mounts.
|
|
129
|
+
let height = -1;
|
|
130
|
+
for (let still = 0, frames = 0; still < 5 && frames < 120; frames++) {
|
|
131
|
+
await nextFrame();
|
|
132
|
+
const now = body.getBoundingClientRect().height;
|
|
133
|
+
still = Math.abs(now - height) < 0.5 ? still + 1 : 0;
|
|
134
|
+
height = now;
|
|
135
|
+
}
|
|
136
|
+
out.push({ anchor: figure.id, number: figure.dataset.figNumber ?? '', height: Math.round(height) });
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
|
|
17
|
+
import { parse, type DefaultTreeAdapterMap } from 'parse5';
|
|
18
|
+
|
|
19
|
+
type HtmlNode = DefaultTreeAdapterMap['node'];
|
|
20
|
+
type HtmlElement = DefaultTreeAdapterMap['element'];
|
|
21
|
+
|
|
22
|
+
/** The diagnostic a built page with an undrawn picture gets. */
|
|
23
|
+
export interface UndrawnFigurePicture {
|
|
24
|
+
readonly code: 'lab-picture-undrawn';
|
|
25
|
+
/** The figure's anchor id, e.g. `fig-launch-angle-explorer`. */
|
|
26
|
+
readonly figure: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DRAWN = new Set(['svg', 'img', 'picture', 'canvas', 'video', 'table']);
|
|
30
|
+
|
|
31
|
+
function isElement(node: HtmlNode): node is HtmlElement {
|
|
32
|
+
return 'tagName' in node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function attribute(node: HtmlElement, name: string): string | undefined {
|
|
36
|
+
return node.attrs.find((attr) => attr.name === name)?.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasClass(node: HtmlElement, name: string): boolean {
|
|
40
|
+
return (attribute(node, 'class') ?? '').split(/\s+/u).includes(name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Element children only; a `<template>`'s content is never drawn, so it is skipped. */
|
|
44
|
+
function childrenOf(node: HtmlNode): readonly HtmlNode[] {
|
|
45
|
+
if (isElement(node) && node.tagName === 'template') return [];
|
|
46
|
+
return 'childNodes' in node ? node.childNodes : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function find(node: HtmlNode, test: (el: HtmlElement) => boolean): HtmlElement | undefined {
|
|
50
|
+
for (const child of childrenOf(node)) {
|
|
51
|
+
if (!isElement(child)) continue;
|
|
52
|
+
if (test(child)) return child;
|
|
53
|
+
const inner = find(child, test);
|
|
54
|
+
if (inner) return inner;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The figures in `html` whose picture is an island with nothing drawn. */
|
|
60
|
+
export function undrawnFigurePictures(html: string): UndrawnFigurePicture[] {
|
|
61
|
+
// Most pages have no lab; they need no parse.
|
|
62
|
+
if (!html.includes('keyed-figure') || !html.includes('astro-island')) return [];
|
|
63
|
+
const found: UndrawnFigurePicture[] = [];
|
|
64
|
+
const visit = (node: HtmlNode) => {
|
|
65
|
+
for (const child of childrenOf(node)) {
|
|
66
|
+
if (!isElement(child)) continue;
|
|
67
|
+
if (child.tagName === 'figure' && hasClass(child, 'keyed-figure')) {
|
|
68
|
+
const body = find(child, (el) => hasClass(el, 'keyed-figure__body'));
|
|
69
|
+
if (
|
|
70
|
+
body &&
|
|
71
|
+
find(body, (el) => el.tagName === 'astro-island') &&
|
|
72
|
+
!find(body, (el) => DRAWN.has(el.tagName))
|
|
73
|
+
) {
|
|
74
|
+
found.push({ code: 'lab-picture-undrawn', figure: attribute(child, 'id') ?? '' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
visit(child);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
visit(parse(html));
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The build's warning for one page, naming each figure and what to do. */
|
|
85
|
+
export function undrawnFigureWarning(
|
|
86
|
+
page: string,
|
|
87
|
+
figures: readonly UndrawnFigurePicture[],
|
|
88
|
+
): string {
|
|
89
|
+
return (
|
|
90
|
+
`lab-picture-undrawn (rule V3): on rendered page "${page}", ` +
|
|
91
|
+
figures.map(({ figure }) => `#${figure}`).join(', ') +
|
|
92
|
+
' draw nothing until the browser runs the island, so a preview card, ' +
|
|
93
|
+
'print without JavaScript and the HTML export show no picture. Draw the ' +
|
|
94
|
+
'default picture at build time and pass it to the island as its children.'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -12,7 +12,8 @@ export type FigureDiagnosticCode =
|
|
|
12
12
|
| 'fig-duplicate-key'
|
|
13
13
|
| 'fig-ref-resolves'
|
|
14
14
|
| 'fig-key-unused'
|
|
15
|
-
| 'fig-removed-prop'
|
|
15
|
+
| 'fig-removed-prop'
|
|
16
|
+
| 'fig-unread-prop';
|
|
16
17
|
|
|
17
18
|
export interface FigureDiagnostic {
|
|
18
19
|
readonly code: FigureDiagnosticCode;
|
|
@@ -58,7 +58,7 @@ function findAbbreviationLikeTokens(text: string): AbbreviationMatch[] {
|
|
|
58
58
|
|
|
59
59
|
const FIX =
|
|
60
60
|
'Add it to an abbreviations-table entry (content/abbreviations/*.md), ' +
|
|
61
|
-
'allowlist it in
|
|
61
|
+
'allowlist it in rules.N14.allow in the course config (course.config.ts, or the course.settings.json it reads) if it is common, ' +
|
|
62
62
|
'non-technical usage, or — if it names a math quantity — introduce it as ' +
|
|
63
63
|
'\\mathrm{…} (or \\operatorname{…}) bound to a notation entry the reader ' +
|
|
64
64
|
'can hover. See docs/authoring/notation.md#n14.';
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* One loop over every enabled course rule's `forbid` patterns, matched against
|
|
6
6
|
* the same "is this really prose" surface rule N9 uses — `proseOnly` blanks
|
|
7
|
-
* code fences, inline code, `$…$` / `$$…$$`, `[[key]]` references,
|
|
8
|
-
* lines, JSX tags and MDX comments to spaces, so offsets
|
|
9
|
-
*
|
|
7
|
+
* code fences, inline code, `$…$` / `$$…$$`, `[[key]]` references, link
|
|
8
|
+
* addresses, `import` lines, JSX tags and MDX comments to spaces, so offsets
|
|
9
|
+
* still point at the source — plus the text of each visible prop (`caption`,
|
|
10
|
+
* `short`, `title`, `label`) as a paragraph of its own. A match becomes a `MathGateDiagnostic` with the rule's own ID as its
|
|
10
11
|
* code and a lesson `file` + `line`, which is what lets `{/* explico-allow X1:
|
|
11
12
|
* reason *\/}` waive one place.
|
|
12
13
|
*
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
*/
|
|
18
19
|
import type { MathGateDiagnostic } from './gate-math';
|
|
19
20
|
import { lessonLine, proseOnly } from './gate-prose-notation';
|
|
21
|
+
import { propStrings } from './gate-prop-text';
|
|
20
22
|
import type { NotationLessonInput } from './types';
|
|
21
23
|
import type { CourseRule, CourseRulePattern } from '../rules/course-rules';
|
|
22
24
|
|
|
@@ -69,14 +71,27 @@ function headings(text: string): Unit[] {
|
|
|
69
71
|
return units;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
/** The props whose text a reader sees: a float's or a memo's captions, a short note's title, a fold's bar. */
|
|
75
|
+
const VISIBLE_PROPS = new Set(['caption', 'short', 'title', 'label']);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Each visible prop's text, as a unit of its own: a caption is prose a reader
|
|
79
|
+
* sees, and a prop holds plain text (invariant `prop-text`).
|
|
80
|
+
*/
|
|
81
|
+
function visibleProps(body: string): Unit[] {
|
|
82
|
+
return propStrings(body)
|
|
83
|
+
.filter((prop) => VISIBLE_PROPS.has(prop.name) && prop.value.trim() !== '')
|
|
84
|
+
.map((prop) => ({ text: prop.value, index: prop.offset }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function unitsFor(scope: CourseRulePattern['scope'], prose: string, body: string): Unit[] {
|
|
73
88
|
switch (scope) {
|
|
74
89
|
case 'prose':
|
|
75
|
-
return [{ text: prose, index: 0 }];
|
|
90
|
+
return [{ text: prose, index: 0 }, ...visibleProps(body)];
|
|
76
91
|
case 'heading':
|
|
77
92
|
return headings(prose);
|
|
78
93
|
case 'paragraph':
|
|
79
|
-
return paragraphs(prose);
|
|
94
|
+
return [...paragraphs(prose), ...visibleProps(body)];
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
97
|
|
|
@@ -111,7 +126,7 @@ export function gateCourseRules(
|
|
|
111
126
|
const cache = new Map<CourseRulePattern['scope'], Unit[]>();
|
|
112
127
|
for (const rule of active) {
|
|
113
128
|
for (const pattern of rule.forbid) {
|
|
114
|
-
if (!cache.has(pattern.scope)) cache.set(pattern.scope, unitsFor(pattern.scope, prose));
|
|
129
|
+
if (!cache.has(pattern.scope)) cache.set(pattern.scope, unitsFor(pattern.scope, prose, lesson.body));
|
|
115
130
|
for (const unit of cache.get(pattern.scope)!) {
|
|
116
131
|
for (const [index, token] of matches(pattern, unit)) {
|
|
117
132
|
diagnostics.push({
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { EQ_CAPTION_PATTERN, stripFencedCode } from './equations.mjs';
|
|
23
23
|
import { stripHeadingMarker } from './rehype-heading-anchors.mjs';
|
|
24
|
+
import { headingDisplayText } from './sections.mjs';
|
|
24
25
|
import type { MathGateDiagnostic } from './gate-math';
|
|
25
26
|
import { lessonLine } from './gate-prose-notation';
|
|
26
27
|
import type { NotationLessonInput } from './types';
|
|
@@ -67,14 +68,20 @@ function overLimitMessage(kind: string, text: string, limit: number): string {
|
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function headingFindings(
|
|
71
|
+
function headingFindings(
|
|
72
|
+
text: string,
|
|
73
|
+
limit: number,
|
|
74
|
+
labelFor?: (key: string) => string | undefined,
|
|
75
|
+
): Finding[] {
|
|
71
76
|
const out: Finding[] = [];
|
|
72
77
|
const re = new RegExp(HEADING_LINE.source, HEADING_LINE.flags);
|
|
73
78
|
let match: RegExpExecArray | null;
|
|
74
79
|
while ((match = re.exec(text))) {
|
|
75
80
|
const level = match[1]?.length ?? 1;
|
|
76
|
-
// What the reader sees: `[collapsed]`, `[card]` and the like never
|
|
77
|
-
|
|
81
|
+
// What the reader sees: `[collapsed]`, `[card]` and the like never
|
|
82
|
+
// render, a formula reads as its glyphs (`\explain{k}{R}` is `R`) and a
|
|
83
|
+
// `[[key]]` term as its label.
|
|
84
|
+
const title = headingDisplayText(stripHeadingMarker(match[2] ?? ''), { labelFor });
|
|
78
85
|
if (title.length > limit) {
|
|
79
86
|
out.push({
|
|
80
87
|
index: match.index,
|
|
@@ -123,9 +130,13 @@ function equationCaptionFindings(text: string, limit: number): Finding[] {
|
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
/** Every S1 finding in one lesson body, in source order. */
|
|
126
|
-
function findingsIn(
|
|
133
|
+
function findingsIn(
|
|
134
|
+
text: string,
|
|
135
|
+
limit: number,
|
|
136
|
+
labelFor?: (key: string) => string | undefined,
|
|
137
|
+
): Finding[] {
|
|
127
138
|
return [
|
|
128
|
-
...headingFindings(text, limit),
|
|
139
|
+
...headingFindings(text, limit, labelFor),
|
|
129
140
|
...floatTitleFindings(text, limit),
|
|
130
141
|
...equationCaptionFindings(text, limit),
|
|
131
142
|
].sort((a, b) => a.index - b.index);
|
|
@@ -138,14 +149,21 @@ function findingsIn(text: string, limit: number): Finding[] {
|
|
|
138
149
|
*/
|
|
139
150
|
export function gateHeadingLength(
|
|
140
151
|
lessons: readonly NotationLessonInput[],
|
|
141
|
-
options: {
|
|
152
|
+
options: {
|
|
153
|
+
readonly maxLength?: number;
|
|
154
|
+
/** A `[[key]]` term's label on a lesson's page, as a heading shows it. */
|
|
155
|
+
readonly labelFor?: (lessonId: string, key: string) => string | undefined;
|
|
156
|
+
} = {},
|
|
142
157
|
): MathGateDiagnostic[] {
|
|
143
158
|
const diagnostics: MathGateDiagnostic[] = [];
|
|
144
159
|
const limit = options.maxLength ?? HEADING_LENGTH_LIMIT;
|
|
145
160
|
|
|
146
161
|
for (const lesson of lessons) {
|
|
147
162
|
const text = stripFencedCode(lesson.body);
|
|
148
|
-
|
|
163
|
+
const labelFor = options.labelFor
|
|
164
|
+
? (key: string) => options.labelFor?.(lesson.lessonId, key)
|
|
165
|
+
: undefined;
|
|
166
|
+
for (const finding of findingsIn(text, limit, labelFor)) {
|
|
149
167
|
diagnostics.push({
|
|
150
168
|
context: 'structure',
|
|
151
169
|
code: 'title-length',
|
|
@@ -83,7 +83,7 @@ function blocksOf(body: string): Block[] {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
interface Finding {
|
|
86
|
-
readonly code: 'heading-skip' | 'heading-adjacent';
|
|
86
|
+
readonly code: 'heading-skip' | 'heading-adjacent' | 'heading-nested';
|
|
87
87
|
readonly index: number;
|
|
88
88
|
readonly token: string;
|
|
89
89
|
readonly message: string;
|
|
@@ -129,6 +129,44 @@ function skipFindings(blocks: readonly Block[]): Finding[] {
|
|
|
129
129
|
return out;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** The components that title themselves below an `<h2>`: a memo (an `<h6>` bar) and a short note (inline). */
|
|
133
|
+
const HOSTS = ['Memo', 'ShortNote'] as const;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Rule S6: a component that draws an `<h2>` never sits inside a memo or a
|
|
137
|
+
* short note. Its heading would land under the host's own, the order would
|
|
138
|
+
* jump, and "On this page" would never list it.
|
|
139
|
+
*/
|
|
140
|
+
function nestedFindings(body: string): Finding[] {
|
|
141
|
+
const blank = (match: string) => match.replace(/[^\n]/g, ' ');
|
|
142
|
+
const text = body.replace(/```[\s\S]*?```/g, blank).replace(/\{\/\*[\s\S]*?\*\/\}/g, blank);
|
|
143
|
+
const out: Finding[] = [];
|
|
144
|
+
for (const host of HOSTS) {
|
|
145
|
+
const opening = new RegExp(`<${host}\\b(?:[^>"']|"[^"]*"|'[^']*')*(?<!/)>`, 'g');
|
|
146
|
+
for (const open of text.matchAll(opening)) {
|
|
147
|
+
const start = (open.index ?? 0) + open[0].length;
|
|
148
|
+
const end = text.indexOf(`</${host}>`, start);
|
|
149
|
+
if (end === -1) continue;
|
|
150
|
+
for (const inner of text.slice(start, end).matchAll(/<(AssessmentSet|CompactExamples)\b/g)) {
|
|
151
|
+
const name = inner[1]!;
|
|
152
|
+
const what = name === 'CompactExamples' ? 'set of worked examples' : 'knowledge check';
|
|
153
|
+
out.push({
|
|
154
|
+
code: 'heading-nested',
|
|
155
|
+
index: start + (inner.index ?? 0),
|
|
156
|
+
token: `<${name}>`,
|
|
157
|
+
message:
|
|
158
|
+
`\`<${name}>\` sits inside a \`<${host}>\`, ${TITLED_COMPONENTS[name]!.what}, ` +
|
|
159
|
+
`${host === 'Memo' ? 'under the memo’s own `<h6>`' : 'in the middle of the note'}: ` +
|
|
160
|
+
'the heading order jumps, and "On this page" never lists it (rule S6). A ' +
|
|
161
|
+
`${what} is a section of the lesson: put it at the top level, after the ` +
|
|
162
|
+
`${host === 'Memo' ? 'memo' : 'paragraph'}.`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
132
170
|
/** Rule S7: a heading is followed by body content before the next heading. */
|
|
133
171
|
function adjacentFindings(blocks: readonly Block[], lesson: NotationLessonInput): Finding[] {
|
|
134
172
|
const out: Finding[] = [];
|
|
@@ -174,6 +212,7 @@ export function gateHeadingStructure(
|
|
|
174
212
|
const blocks = blocksOf(lesson.body);
|
|
175
213
|
const findings = [
|
|
176
214
|
...(levels ? skipFindings(blocks) : []),
|
|
215
|
+
...(levels ? nestedFindings(lesson.body) : []),
|
|
177
216
|
...(adjacency ? adjacentFindings(blocks, lesson) : []),
|
|
178
217
|
].sort((a, b) => a.index - b.index);
|
|
179
218
|
for (const finding of findings) {
|