explico 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1292 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +11 -4
- package/dist/compiler/astro.js +48 -15
- package/dist/compiler/cli.d.ts +27 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +33 -2
- package/dist/compiler/collections.js +91 -19
- 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 +15 -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 +115 -8
- package/dist/compiler/manifest.js +221 -39
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +3 -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 +8 -2
- package/dist/reference/base-operators.mjs +8 -10
- 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/figure-symbols.d.ts +32 -0
- package/dist/reference/figure-symbols.js +55 -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-non-chapter-pages.d.ts +39 -0
- package/dist/reference/gate-non-chapter-pages.js +156 -0
- package/dist/reference/gate-part-pages.d.ts +4 -2
- package/dist/reference/gate-part-pages.js +2 -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-katex-validate.d.ts +43 -0
- package/dist/reference/lab-katex-validate.js +96 -0
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/lint-ignore.js +1 -1
- 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 +39 -7
- package/dist/reference/page-notation.js +62 -10
- package/dist/reference/raw-floats.mjs +37 -13
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +115 -11
- 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 +275 -23
- package/dist/reference/render-lab-math.d.ts +13 -2
- package/dist/reference/render-lab-math.js +18 -6
- 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 +223 -32
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +100 -27
- 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 +33 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +115 -10
- package/docs/authoring/migrating.md +878 -0
- package/docs/authoring/notation.md +285 -30
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +84 -45
- package/docs/authoring/structure.md +67 -2
- package/docs/authoring/visualizations.md +167 -25
- package/package.json +12 -7
- package/src/compiler/astro.ts +72 -21
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +158 -27
- 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 +387 -40
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/CourseContents.astro +6 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +18 -9
- package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
- 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/disclosure/print-as-shown.ts +42 -6
- package/src/components/figure/Figure.astro +17 -1
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/figure/figure-state.ts +26 -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 +300 -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 +66 -8
- 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 +14 -22
- 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 +8 -10
- 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/figure-symbols.ts +117 -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-non-chapter-pages.ts +241 -0
- package/src/reference/gate-part-pages.ts +1 -1
- 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-katex-validate.ts +156 -0
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/lint-ignore.ts +4 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +114 -14
- package/src/reference/raw-floats.mjs +37 -13
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +170 -9
- 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 +275 -23
- package/src/reference/render-lab-math.ts +40 -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 +241 -31
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +322 -83
|
@@ -45,19 +45,68 @@ interface DiagramLayout {
|
|
|
45
45
|
* would otherwise create a phantom zero-size node).
|
|
46
46
|
*/
|
|
47
47
|
declare function layoutDiagram(nodes: readonly DiagramNodeInput[], edges: readonly DiagramEdgeInput[], options?: DiagramLayoutOptions): DiagramLayout;
|
|
48
|
-
|
|
48
|
+
/** A node's text font size, in px. */
|
|
49
|
+
declare const NODE_FONT_PX = 13.6;
|
|
50
|
+
/**
|
|
51
|
+
* A node's formula font size, in px: KaTeX's own `.katex { font-size: 1.21em }`
|
|
52
|
+
* of the node's text. `katex.css` is unlayered, so no rule in the component's
|
|
53
|
+
* `@layer` can shrink it, and at that size KaTeX_Main's x-height matches the
|
|
54
|
+
* label's.
|
|
55
|
+
*/
|
|
56
|
+
declare const FORMULA_FONT_PX: number;
|
|
57
|
+
/** What a node holds, measured: its label, and its formula's box in px. */
|
|
58
|
+
interface NodeBoxContent {
|
|
59
|
+
readonly text?: string;
|
|
60
|
+
/** `measureFormula(latex, FORMULA_FONT_PX)`. */
|
|
61
|
+
readonly formula?: {
|
|
62
|
+
readonly width: number;
|
|
63
|
+
readonly height: number;
|
|
64
|
+
readonly depth: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
interface NodeBoxOptions {
|
|
68
|
+
/** The author's width: the label wraps inside it and the height follows. */
|
|
69
|
+
readonly width?: number;
|
|
70
|
+
/** The author's height. */
|
|
71
|
+
readonly height?: number;
|
|
72
|
+
/** The widest box a label is wrapped to fit before it takes a second line. */
|
|
73
|
+
readonly maxWidth?: number;
|
|
74
|
+
}
|
|
75
|
+
interface NodeBox {
|
|
49
76
|
readonly width: number;
|
|
50
77
|
readonly height: number;
|
|
78
|
+
/** The label as it is wrapped in DejaVu Sans. */
|
|
79
|
+
readonly lines: readonly string[];
|
|
51
80
|
}
|
|
52
81
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
82
|
+
* Size a node's box from its measured content: as wide as its widest line of
|
|
83
|
+
* text or its formula, whichever is wider, plus the inset, and as tall as its
|
|
84
|
+
* lines of text, the gap and its formula's line. A label wider than `maxWidth`
|
|
85
|
+
* wraps; a formula never does, so a wide formula widens the box instead and the
|
|
86
|
+
* label wraps to it. An author's `width` or `height` makes the box larger,
|
|
87
|
+
* never too small for its formula or its lines.
|
|
58
88
|
*/
|
|
59
|
-
declare function
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
declare function nodeBoxSize(content: NodeBoxContent, options?: NodeBoxOptions): NodeBox;
|
|
90
|
+
/** One node to fit: what it holds, and the author's size if any. */
|
|
91
|
+
interface FitNodeInput {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly content: NodeBoxContent;
|
|
94
|
+
readonly width?: number;
|
|
95
|
+
readonly height?: number;
|
|
96
|
+
}
|
|
97
|
+
interface FitOptions extends DiagramLayoutOptions {
|
|
98
|
+
/** The widest the diagram should lay out (rule V8's column); omit to skip fitting. */
|
|
99
|
+
readonly fitWidth?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Size every node from its content and lay the diagram out. When `fitWidth`
|
|
103
|
+
* is set and the diagram comes out wider, wrap the labels of the nodes without
|
|
104
|
+
* an author's `width` narrower, step by step, until it fits; a diagram that
|
|
105
|
+
* still does not fit keeps its narrowest layout, and the caller warns.
|
|
106
|
+
*/
|
|
107
|
+
declare function fitDiagramLayout(nodes: readonly FitNodeInput[], edges: readonly DiagramEdgeInput[], options?: FitOptions): {
|
|
108
|
+
readonly layout: DiagramLayout;
|
|
109
|
+
readonly boxes: ReadonlyMap<string, NodeBox>;
|
|
110
|
+
};
|
|
62
111
|
|
|
63
|
-
export { type
|
|
112
|
+
export { type DiagramEdgeInput, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodeInput, FORMULA_FONT_PX, type FitNodeInput, type FitOptions, type LaidOutEdge, type LaidOutNode, NODE_FONT_PX, type NodeBox, type NodeBoxContent, type NodeBoxOptions, type Point, fitDiagramLayout, layoutDiagram, nodeBoxSize };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dagre from "@dagrejs/dagre";
|
|
2
|
+
import { textWidth, wrapText } from "./text-measure.js";
|
|
2
3
|
function layoutDiagram(nodes, edges, options = {}) {
|
|
3
4
|
const ids = /* @__PURE__ */ new Set();
|
|
4
5
|
for (const node of nodes) {
|
|
@@ -19,6 +20,11 @@ function layoutDiagram(nodes, edges, options = {}) {
|
|
|
19
20
|
if (!ids.has(edge.to)) {
|
|
20
21
|
throw new Error(`Diagram edge references unknown node "${edge.to}".`);
|
|
21
22
|
}
|
|
23
|
+
if (edge.from === edge.to) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Diagram edge from "${edge.from}" to itself: a diagram draws no loop on one node. Name the repetition in the label ("Repeat until it converges"), or draw the step it returns to as its own node.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
const graph = new dagre.graphlib.Graph();
|
|
24
30
|
graph.setGraph({
|
|
@@ -91,32 +97,87 @@ function layoutDiagram(nodes, edges, options = {}) {
|
|
|
91
97
|
height: maxY - minY + padding * 2
|
|
92
98
|
};
|
|
93
99
|
}
|
|
94
|
-
const
|
|
100
|
+
const NODE_FONT_PX = 13.6;
|
|
101
|
+
const FORMULA_FONT_PX = NODE_FONT_PX * 1.21;
|
|
102
|
+
const LINE_PX = 17;
|
|
103
|
+
const CSS_PADDING_X = 8;
|
|
104
|
+
const INSET_X = 16;
|
|
105
|
+
const INSET_Y = 14;
|
|
106
|
+
const GAP_PX = 2.4;
|
|
107
|
+
const FORMULA_ASCENT_PX = 14;
|
|
108
|
+
const FORMULA_DESCENT_PX = 5.5;
|
|
95
109
|
const MIN_WIDTH = 90;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
const DEFAULT_MAX_WIDTH = 220;
|
|
111
|
+
function balancedLines(text, maxWidth) {
|
|
112
|
+
const greedy = wrapText(text, maxWidth, NODE_FONT_PX);
|
|
113
|
+
if (greedy.length <= 1) return greedy;
|
|
114
|
+
let low = 1;
|
|
115
|
+
let high = Math.ceil(maxWidth);
|
|
116
|
+
while (low < high) {
|
|
117
|
+
const middle = Math.floor((low + high) / 2);
|
|
118
|
+
if (wrapText(text, middle, NODE_FONT_PX).length <= greedy.length) high = middle;
|
|
119
|
+
else low = middle + 1;
|
|
120
|
+
}
|
|
121
|
+
return wrapText(text, high, NODE_FONT_PX);
|
|
122
|
+
}
|
|
123
|
+
function nodeBoxSize(content, options = {}) {
|
|
124
|
+
const text = content.text?.trim() ?? "";
|
|
125
|
+
const formula = content.formula;
|
|
126
|
+
const formulaWidth = formula?.width ?? 0;
|
|
127
|
+
let width;
|
|
128
|
+
let lines;
|
|
129
|
+
if (options.width !== void 0) {
|
|
130
|
+
width = Math.max(options.width, Math.ceil(formulaWidth + 2 * CSS_PADDING_X));
|
|
131
|
+
lines = text ? wrapText(text, width - 2 * CSS_PADDING_X, NODE_FONT_PX) : [];
|
|
132
|
+
} else {
|
|
133
|
+
const maxContent = Math.max(
|
|
134
|
+
(options.maxWidth ?? DEFAULT_MAX_WIDTH) - 2 * INSET_X,
|
|
135
|
+
formulaWidth
|
|
136
|
+
);
|
|
137
|
+
lines = text ? balancedLines(text, maxContent) : [];
|
|
138
|
+
const contentWidth = Math.max(
|
|
139
|
+
formulaWidth,
|
|
140
|
+
...lines.map((line) => textWidth(line, NODE_FONT_PX))
|
|
141
|
+
);
|
|
142
|
+
width = Math.max(MIN_WIDTH, Math.ceil(contentWidth + 2 * INSET_X));
|
|
143
|
+
}
|
|
144
|
+
const formulaHeight = formula ? Math.max(FORMULA_ASCENT_PX, formula.height) + Math.max(FORMULA_DESCENT_PX, formula.depth) : 0;
|
|
145
|
+
const gap = lines.length > 0 && formula ? GAP_PX : 0;
|
|
146
|
+
const contentHeight = Math.ceil(lines.length * LINE_PX + gap + formulaHeight + 2 * INSET_Y);
|
|
147
|
+
const height = Math.max(options.height ?? 0, contentHeight);
|
|
148
|
+
return { width, height, lines };
|
|
149
|
+
}
|
|
150
|
+
const FIT_MAX_WIDTHS = [DEFAULT_MAX_WIDTH, 200, 180, 160, 140, 120];
|
|
151
|
+
function fitDiagramLayout(nodes, edges, options = {}) {
|
|
152
|
+
let result;
|
|
153
|
+
for (const maxWidth of FIT_MAX_WIDTHS) {
|
|
154
|
+
const boxes = new Map(
|
|
155
|
+
nodes.map((node) => [
|
|
156
|
+
node.id,
|
|
157
|
+
nodeBoxSize(node.content, {
|
|
158
|
+
maxWidth,
|
|
159
|
+
...node.width !== void 0 ? { width: node.width } : {},
|
|
160
|
+
...node.height !== void 0 ? { height: node.height } : {}
|
|
161
|
+
})
|
|
162
|
+
])
|
|
163
|
+
);
|
|
164
|
+
const layout = layoutDiagram(
|
|
165
|
+
nodes.map((node) => ({ id: node.id, ...sizeOf(boxes.get(node.id)) })),
|
|
166
|
+
edges,
|
|
167
|
+
options
|
|
168
|
+
);
|
|
169
|
+
result = { layout, boxes };
|
|
170
|
+
if (options.fitWidth === void 0 || layout.width <= options.fitWidth) break;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
106
173
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
const visibleLength = kind === "formula" ? formulaVisibleLength(content) : content.trim().length || 1;
|
|
110
|
-
const charWidth = CHAR_WIDTH[kind];
|
|
111
|
-
const naturalWidth = visibleLength * charWidth + PADDING_X * 2;
|
|
112
|
-
const width = Math.min(maxWidth, Math.max(MIN_WIDTH, naturalWidth));
|
|
113
|
-
const usableWidth = width - PADDING_X * 2;
|
|
114
|
-
const charsPerLine = Math.max(1, Math.floor(usableWidth / charWidth));
|
|
115
|
-
const lines = Math.max(1, Math.ceil(visibleLength / charsPerLine));
|
|
116
|
-
const height = lines * LINE_HEIGHT + PADDING_Y * 2;
|
|
117
|
-
return { width: Math.round(width), height: Math.round(height) };
|
|
174
|
+
function sizeOf(box) {
|
|
175
|
+
return { width: box.width, height: box.height };
|
|
118
176
|
}
|
|
119
177
|
export {
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
FORMULA_FONT_PX,
|
|
179
|
+
NODE_FONT_PX,
|
|
180
|
+
fitDiagramLayout,
|
|
181
|
+
layoutDiagram,
|
|
182
|
+
nodeBoxSize
|
|
122
183
|
};
|
|
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
2
2
|
|
|
3
3
|
/** `validateKeyedFloats` for `<Diagram>` — see `keyed-float-validate.ts`. */
|
|
4
4
|
|
|
5
|
-
type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused' | 'dia-removed-prop';
|
|
5
|
+
type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused' | 'dia-removed-prop' | 'dia-unread-prop';
|
|
6
6
|
interface DiagramDiagnostic {
|
|
7
7
|
readonly code: DiagramDiagnosticCode;
|
|
8
8
|
readonly severity: 'error' | 'warning' | 'info';
|
|
@@ -25,7 +25,7 @@ interface EquationRefRecord {
|
|
|
25
25
|
/** Target lesson slug for a cross-page reference; `undefined` = same page. */
|
|
26
26
|
readonly targetSlug?: string;
|
|
27
27
|
}
|
|
28
|
-
type EquationDiagnosticCode = 'eq-duplicate-key' | 'eq-ref-resolves' | 'eq-key-unused';
|
|
28
|
+
type EquationDiagnosticCode = 'eq-duplicate-key' | 'eq-ref-resolves' | 'eq-key-unused' | 'eq-label-repeated' | 'eq-latex-ref';
|
|
29
29
|
interface EquationDiagnostic {
|
|
30
30
|
readonly code: EquationDiagnosticCode;
|
|
31
31
|
readonly severity: 'error' | 'warning' | 'info';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { parseEquationRef, scanEquationLabels } from "./equations.mjs";
|
|
1
|
+
import { parseEquationRef, repeatedEquationLabels, scanEquationLabels } from "./equations.mjs";
|
|
2
|
+
import { proseOnly } from "./gate-prose-notation.js";
|
|
2
3
|
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
3
4
|
function stripCode(text) {
|
|
4
5
|
return text.replace(/```[\s\S]*?```/g, " ").replace(/~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
|
|
@@ -16,6 +17,7 @@ function bodyEquationRefs(lesson) {
|
|
|
16
17
|
}
|
|
17
18
|
return refs;
|
|
18
19
|
}
|
|
20
|
+
const LATEX_REFERENCE = /\\(eqref|ref|autoref|cref|Cref)\s*\{([^{}\n]*)\}/g;
|
|
19
21
|
function validateEquations(lessons) {
|
|
20
22
|
const labels = [];
|
|
21
23
|
const diagnostics = [];
|
|
@@ -50,6 +52,29 @@ function validateEquations(lessons) {
|
|
|
50
52
|
});
|
|
51
53
|
}
|
|
52
54
|
numbersBySlug[lesson.slug] = table;
|
|
55
|
+
for (const match of proseOnly(lesson.body).matchAll(LATEX_REFERENCE)) {
|
|
56
|
+
const [, command, target = ""] = match;
|
|
57
|
+
const [prefix, key] = target.includes(":") ? target.split(":", 2) : ["eq", target];
|
|
58
|
+
diagnostics.push({
|
|
59
|
+
code: "eq-latex-ref",
|
|
60
|
+
severity: "error",
|
|
61
|
+
message: `${lesson.lessonId} writes \\${command}{${target}}, LaTeX's reference: MDX reads its {\u2026} as JavaScript, and the page fails to build. Write [[${prefix}-${key}]].`,
|
|
62
|
+
file: lesson.file,
|
|
63
|
+
lessonId: lesson.lessonId,
|
|
64
|
+
key: key ?? target
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const keys of repeatedEquationLabels(lesson.body)) {
|
|
68
|
+
const [first, ...rest] = keys;
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
code: "eq-label-repeated",
|
|
71
|
+
severity: "error",
|
|
72
|
+
message: `${lesson.lessonId} puts ${keys.map((key) => `\\label{eq:${key}}`).join(" and ")} in one display block, which is one numbered equation: eq:${first} names it, and ${rest.map((key) => `[[eq-${key}]]`).join(", ")} would point at nothing. Split the block into one $$\u2026$$ block per equation you refer to.`,
|
|
73
|
+
file: lesson.file,
|
|
74
|
+
lessonId: lesson.lessonId,
|
|
75
|
+
key: rest[0]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
53
78
|
}
|
|
54
79
|
const referenced = /* @__PURE__ */ new Set();
|
|
55
80
|
for (const lesson of lessons) {
|
|
@@ -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
|
+
};
|