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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule V8: a diagram is no wider than the lesson column.
|
|
3
|
+
*
|
|
4
|
+
* `Diagram.astro` lays a diagram out at build time and so knows its width, but
|
|
5
|
+
* not the page it is on or what the course allows, and a diagram drawn by a
|
|
6
|
+
* wrapper component (`<CourseDiagram kind="…">`) never shows its nodes to the
|
|
7
|
+
* manifest walk. So the check runs on what was rendered: the component marks
|
|
8
|
+
* a diagram wider than {@link diagramWidthLimit} with `data-diagram-too-wide`,
|
|
9
|
+
* `compiler/astro.ts` finds each mark with {@link oversizedDiagrams} on every
|
|
10
|
+
* built page and reports a `dia-too-wide` warning with the lesson file and the
|
|
11
|
+
* diagram id, and `content warnings` lists what the last build reported
|
|
12
|
+
* (`compiler/render-diagnostics.ts`). Under `astro dev` the component prints
|
|
13
|
+
* the same message as it renders.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parse, type DefaultTreeAdapterMap } from 'parse5';
|
|
17
|
+
|
|
18
|
+
import type { ManifestRules } from '../rules/settings';
|
|
19
|
+
|
|
20
|
+
type HtmlNode = DefaultTreeAdapterMap['node'];
|
|
21
|
+
type HtmlElement = DefaultTreeAdapterMap['element'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The lesson column at a 1366px-wide window, the most common laptop screen
|
|
25
|
+
* (632px at 1280, 832px at 1920), unless the course sets
|
|
26
|
+
* `rules.V8.columnWidth`.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_COLUMN_WIDTH = 707;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The widest a diagram may lay out, or `undefined` when nothing checks it: rule
|
|
32
|
+
* V8 is off, or the diagram says `warnAboveWidth={false}`. A number in
|
|
33
|
+
* `warnAboveWidth` replaces the course's column width for that diagram.
|
|
34
|
+
*/
|
|
35
|
+
export function diagramWidthLimit(
|
|
36
|
+
warnAboveWidth: number | false | undefined,
|
|
37
|
+
rules: Pick<ManifestRules, 'disabled' | 'settings'> | undefined,
|
|
38
|
+
): number | undefined {
|
|
39
|
+
if (warnAboveWidth === false || rules?.disabled.includes('V8')) return undefined;
|
|
40
|
+
if (warnAboveWidth !== undefined) return warnAboveWidth;
|
|
41
|
+
const columnWidth = rules?.settings.V8?.columnWidth;
|
|
42
|
+
return typeof columnWidth === 'number' ? columnWidth : DEFAULT_COLUMN_WIDTH;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What to tell the author about one diagram wider than its limit. */
|
|
46
|
+
export function diagramTooWideMessage(id: string, width: number, limit: number): string {
|
|
47
|
+
return (
|
|
48
|
+
`Diagram "${id}" lays out ${Math.round(width)}px wide, wider than the ${limit}px it may ` +
|
|
49
|
+
`take (rule V8), so a reader has to scroll it or widen it to see it whole. Try ` +
|
|
50
|
+
`direction="TB", a shorter label, or a narrower node \`width\` (its label wraps inside ` +
|
|
51
|
+
`it); set warnAboveWidth={false} only when the diagram is meant to scroll.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A diagram on a built page that is wider than its limit. */
|
|
56
|
+
export interface OversizedDiagram {
|
|
57
|
+
readonly code: 'dia-too-wide';
|
|
58
|
+
/** The diagram's `id`. */
|
|
59
|
+
readonly key: string;
|
|
60
|
+
readonly width: number;
|
|
61
|
+
readonly limit: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isElement(node: HtmlNode): node is HtmlElement {
|
|
65
|
+
return 'tagName' in node;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function attribute(node: HtmlElement, name: string): string | undefined {
|
|
69
|
+
return node.attrs.find((attr) => attr.name === name)?.value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function viewportKey(node: HtmlNode): string | undefined {
|
|
73
|
+
for (const child of 'childNodes' in node ? node.childNodes : []) {
|
|
74
|
+
if (!isElement(child)) continue;
|
|
75
|
+
const match = /^diagram-(.+)-viewport$/.exec(attribute(child, 'id') ?? '');
|
|
76
|
+
if (match) return match[1];
|
|
77
|
+
const inner = viewportKey(child);
|
|
78
|
+
if (inner) return inner;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The diagrams `Diagram.astro` marked too wide in `html`. */
|
|
84
|
+
export function oversizedDiagrams(html: string): OversizedDiagram[] {
|
|
85
|
+
if (!html.includes('data-diagram-too-wide')) return [];
|
|
86
|
+
const found: OversizedDiagram[] = [];
|
|
87
|
+
const visit = (node: HtmlNode) => {
|
|
88
|
+
for (const child of 'childNodes' in node ? node.childNodes : []) {
|
|
89
|
+
if (!isElement(child)) continue;
|
|
90
|
+
const limit = attribute(child, 'data-diagram-too-wide');
|
|
91
|
+
if (limit !== undefined) {
|
|
92
|
+
found.push({
|
|
93
|
+
code: 'dia-too-wide',
|
|
94
|
+
key: viewportKey(child) ?? attribute(child, 'id') ?? '',
|
|
95
|
+
width: Number(attribute(child, 'data-diagram-width')),
|
|
96
|
+
limit: Number(limit),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
visit(child);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
visit(parse(html));
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* SVG; this module only computes geometry and is deliberately framework-free
|
|
9
9
|
* so it is unit-testable without KaTeX, Astro, or a browser.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `width
|
|
11
|
+
* A node's box is sized from what it holds (`nodeBoxSize`): its label's
|
|
12
|
+
* width in DejaVu Sans, the widest common system font (`text-measure.ts`),
|
|
13
|
+
* wrapped to a line count that sets the height, and its formula's width as
|
|
14
|
+
* KaTeX lays it out (`katex-measure.ts`, measured by the caller). An explicit
|
|
15
|
+
* `width` or `height` on the node overrides either side.
|
|
16
16
|
*/
|
|
17
17
|
import dagre from '@dagrejs/dagre';
|
|
18
18
|
import type {
|
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
NodeLabel as DagreNodeLabel,
|
|
22
22
|
} from '@dagrejs/dagre';
|
|
23
23
|
|
|
24
|
+
import { textWidth, wrapText } from './text-measure';
|
|
25
|
+
|
|
24
26
|
export interface DiagramNodeInput {
|
|
25
27
|
readonly id: string;
|
|
26
28
|
readonly width: number;
|
|
@@ -98,6 +100,14 @@ export function layoutDiagram(
|
|
|
98
100
|
if (!ids.has(edge.to)) {
|
|
99
101
|
throw new Error(`Diagram edge references unknown node "${edge.to}".`);
|
|
100
102
|
}
|
|
103
|
+
// The layout draws a self-loop as a stray line out of the node.
|
|
104
|
+
if (edge.from === edge.to) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Diagram edge from "${edge.from}" to itself: a diagram draws no loop on one ` +
|
|
107
|
+
'node. Name the repetition in the label ("Repeat until it converges"), or ' +
|
|
108
|
+
'draw the step it returns to as its own node.',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
const graph = new dagre.graphlib.Graph<
|
|
@@ -185,62 +195,183 @@ export function layoutDiagram(
|
|
|
185
195
|
};
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
// ---
|
|
198
|
+
// --- node boxes -----------------------------------------------------------
|
|
199
|
+
//
|
|
200
|
+
// The constants mirror `Diagram.astro`'s `.diagram-node-content`, set in px
|
|
201
|
+
// so a reader's font size cannot outgrow a box: `font-size: 13.6px`,
|
|
202
|
+
// `line-height: 1.25`, `padding: 3.2px 8px`, `gap: 2.4px`.
|
|
189
203
|
|
|
190
|
-
|
|
204
|
+
/** A node's text font size, in px. */
|
|
205
|
+
export const NODE_FONT_PX = 13.6;
|
|
206
|
+
/**
|
|
207
|
+
* A node's formula font size, in px: KaTeX's own `.katex { font-size: 1.21em }`
|
|
208
|
+
* of the node's text. `katex.css` is unlayered, so no rule in the component's
|
|
209
|
+
* `@layer` can shrink it, and at that size KaTeX_Main's x-height matches the
|
|
210
|
+
* label's.
|
|
211
|
+
*/
|
|
212
|
+
export const FORMULA_FONT_PX = NODE_FONT_PX * 1.21;
|
|
213
|
+
/** One line of node text, in px. */
|
|
214
|
+
const LINE_PX = 17;
|
|
215
|
+
/** The horizontal CSS padding inside a node's box, per side. */
|
|
216
|
+
const CSS_PADDING_X = 8;
|
|
217
|
+
/** Space between the content and the box, per side: the CSS padding and some air. */
|
|
218
|
+
const INSET_X = 16;
|
|
219
|
+
const INSET_Y = 14;
|
|
220
|
+
/** The gap between a node's text and its formula. */
|
|
221
|
+
const GAP_PX = 2.4;
|
|
222
|
+
/**
|
|
223
|
+
* A formula's line reaches at least this far above and below its baseline:
|
|
224
|
+
* the strut of the line it sits in, measured in Chromium, with room for a
|
|
225
|
+
* system font that sits higher than DejaVu Sans.
|
|
226
|
+
*/
|
|
227
|
+
const FORMULA_ASCENT_PX = 14;
|
|
228
|
+
const FORMULA_DESCENT_PX = 5.5;
|
|
229
|
+
const MIN_WIDTH = 90;
|
|
230
|
+
const DEFAULT_MAX_WIDTH = 220;
|
|
231
|
+
|
|
232
|
+
/** What a node holds, measured: its label, and its formula's box in px. */
|
|
233
|
+
export interface NodeBoxContent {
|
|
234
|
+
readonly text?: string;
|
|
235
|
+
/** `measureFormula(latex, FORMULA_FONT_PX)`. */
|
|
236
|
+
readonly formula?: {
|
|
237
|
+
readonly width: number;
|
|
238
|
+
readonly height: number;
|
|
239
|
+
readonly depth: number;
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface NodeBoxOptions {
|
|
244
|
+
/** The author's width: the label wraps inside it and the height follows. */
|
|
245
|
+
readonly width?: number;
|
|
246
|
+
/** The author's height. */
|
|
247
|
+
readonly height?: number;
|
|
248
|
+
/** The widest box a label is wrapped to fit before it takes a second line. */
|
|
249
|
+
readonly maxWidth?: number;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface NodeBox {
|
|
191
253
|
readonly width: number;
|
|
192
254
|
readonly height: number;
|
|
255
|
+
/** The label as it is wrapped in DejaVu Sans. */
|
|
256
|
+
readonly lines: readonly string[];
|
|
193
257
|
}
|
|
194
258
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
/**
|
|
260
|
+
* The narrowest wrap of `text` that needs no more lines than it needs at
|
|
261
|
+
* `maxWidth`: a two-line label splits near its middle rather than leaving one
|
|
262
|
+
* word on the second line, and its box is no wider than that takes.
|
|
263
|
+
*/
|
|
264
|
+
function balancedLines(text: string, maxWidth: number): string[] {
|
|
265
|
+
const greedy = wrapText(text, maxWidth, NODE_FONT_PX);
|
|
266
|
+
if (greedy.length <= 1) return greedy;
|
|
267
|
+
let low = 1;
|
|
268
|
+
let high = Math.ceil(maxWidth);
|
|
269
|
+
while (low < high) {
|
|
270
|
+
const middle = Math.floor((low + high) / 2);
|
|
271
|
+
if (wrapText(text, middle, NODE_FONT_PX).length <= greedy.length) high = middle;
|
|
272
|
+
else low = middle + 1;
|
|
273
|
+
}
|
|
274
|
+
return wrapText(text, high, NODE_FONT_PX);
|
|
275
|
+
}
|
|
205
276
|
|
|
206
277
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
278
|
+
* Size a node's box from its measured content: as wide as its widest line of
|
|
279
|
+
* text or its formula, whichever is wider, plus the inset, and as tall as its
|
|
280
|
+
* lines of text, the gap and its formula's line. A label wider than `maxWidth`
|
|
281
|
+
* wraps; a formula never does, so a wide formula widens the box instead and the
|
|
282
|
+
* label wraps to it. An author's `width` or `height` makes the box larger,
|
|
283
|
+
* never too small for its formula or its lines.
|
|
211
284
|
*/
|
|
212
|
-
function
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
285
|
+
export function nodeBoxSize(content: NodeBoxContent, options: NodeBoxOptions = {}): NodeBox {
|
|
286
|
+
const text = content.text?.trim() ?? '';
|
|
287
|
+
const formula = content.formula;
|
|
288
|
+
const formulaWidth = formula?.width ?? 0;
|
|
289
|
+
|
|
290
|
+
let width: number;
|
|
291
|
+
let lines: string[];
|
|
292
|
+
if (options.width !== undefined) {
|
|
293
|
+
// A formula never wraps, so an author's width narrower than it would cut
|
|
294
|
+
// it off: the box takes the formula's width, and the label wraps to that.
|
|
295
|
+
width = Math.max(options.width, Math.ceil(formulaWidth + 2 * CSS_PADDING_X));
|
|
296
|
+
lines = text ? wrapText(text, width - 2 * CSS_PADDING_X, NODE_FONT_PX) : [];
|
|
297
|
+
} else {
|
|
298
|
+
const maxContent = Math.max(
|
|
299
|
+
(options.maxWidth ?? DEFAULT_MAX_WIDTH) - 2 * INSET_X,
|
|
300
|
+
formulaWidth,
|
|
301
|
+
);
|
|
302
|
+
lines = text ? balancedLines(text, maxContent) : [];
|
|
303
|
+
const contentWidth = Math.max(
|
|
304
|
+
formulaWidth,
|
|
305
|
+
...lines.map((line) => textWidth(line, NODE_FONT_PX)),
|
|
306
|
+
);
|
|
307
|
+
width = Math.max(MIN_WIDTH, Math.ceil(contentWidth + 2 * INSET_X));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const formulaHeight = formula
|
|
311
|
+
? Math.max(FORMULA_ASCENT_PX, formula.height) + Math.max(FORMULA_DESCENT_PX, formula.depth)
|
|
312
|
+
: 0;
|
|
313
|
+
const gap = lines.length > 0 && formula ? GAP_PX : 0;
|
|
314
|
+
// An author's height adds room; it never cuts what the box holds.
|
|
315
|
+
const contentHeight = Math.ceil(lines.length * LINE_PX + gap + formulaHeight + 2 * INSET_Y);
|
|
316
|
+
const height = Math.max(options.height ?? 0, contentHeight);
|
|
317
|
+
|
|
318
|
+
return { width, height, lines };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** One node to fit: what it holds, and the author's size if any. */
|
|
322
|
+
export interface FitNodeInput {
|
|
323
|
+
readonly id: string;
|
|
324
|
+
readonly content: NodeBoxContent;
|
|
325
|
+
readonly width?: number;
|
|
326
|
+
readonly height?: number;
|
|
218
327
|
}
|
|
219
328
|
|
|
329
|
+
export interface FitOptions extends DiagramLayoutOptions {
|
|
330
|
+
/** The widest the diagram should lay out (rule V8's column); omit to skip fitting. */
|
|
331
|
+
readonly fitWidth?: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The widest box a label wraps to fit, tried in turn while the diagram is
|
|
336
|
+
* still wider than its column: a left-to-right chain of four steps fits a
|
|
337
|
+
* 707px column once its labels wrap at 140px.
|
|
338
|
+
*/
|
|
339
|
+
const FIT_MAX_WIDTHS = [DEFAULT_MAX_WIDTH, 200, 180, 160, 140, 120];
|
|
340
|
+
|
|
220
341
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* badly wrong.
|
|
342
|
+
* Size every node from its content and lay the diagram out. When `fitWidth`
|
|
343
|
+
* is set and the diagram comes out wider, wrap the labels of the nodes without
|
|
344
|
+
* an author's `width` narrower, step by step, until it fits; a diagram that
|
|
345
|
+
* still does not fit keeps its narrowest layout, and the caller warns.
|
|
226
346
|
*/
|
|
227
|
-
export function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
options:
|
|
231
|
-
):
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
347
|
+
export function fitDiagramLayout(
|
|
348
|
+
nodes: readonly FitNodeInput[],
|
|
349
|
+
edges: readonly DiagramEdgeInput[],
|
|
350
|
+
options: FitOptions = {},
|
|
351
|
+
): { readonly layout: DiagramLayout; readonly boxes: ReadonlyMap<string, NodeBox> } {
|
|
352
|
+
let result: { layout: DiagramLayout; boxes: Map<string, NodeBox> } | undefined;
|
|
353
|
+
for (const maxWidth of FIT_MAX_WIDTHS) {
|
|
354
|
+
const boxes = new Map(
|
|
355
|
+
nodes.map((node) => [
|
|
356
|
+
node.id,
|
|
357
|
+
nodeBoxSize(node.content, {
|
|
358
|
+
maxWidth,
|
|
359
|
+
...(node.width !== undefined ? { width: node.width } : {}),
|
|
360
|
+
...(node.height !== undefined ? { height: node.height } : {}),
|
|
361
|
+
}),
|
|
362
|
+
]),
|
|
363
|
+
);
|
|
364
|
+
const layout = layoutDiagram(
|
|
365
|
+
nodes.map((node) => ({ id: node.id, ...sizeOf(boxes.get(node.id)!) })),
|
|
366
|
+
edges,
|
|
367
|
+
options,
|
|
368
|
+
);
|
|
369
|
+
result = { layout, boxes };
|
|
370
|
+
if (options.fitWidth === undefined || layout.width <= options.fitWidth) break;
|
|
371
|
+
}
|
|
372
|
+
return result!;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function sizeOf(box: NodeBox): { width: number; height: number } {
|
|
376
|
+
return { width: box.width, height: box.height };
|
|
246
377
|
}
|
|
@@ -12,7 +12,8 @@ export type DiagramDiagnosticCode =
|
|
|
12
12
|
| 'dia-duplicate-key'
|
|
13
13
|
| 'dia-ref-resolves'
|
|
14
14
|
| 'dia-key-unused'
|
|
15
|
-
| 'dia-removed-prop'
|
|
15
|
+
| 'dia-removed-prop'
|
|
16
|
+
| 'dia-unread-prop';
|
|
16
17
|
|
|
17
18
|
export interface DiagramDiagnostic {
|
|
18
19
|
readonly code: DiagramDiagnosticCode;
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* fast `pnpm validate:content` the same coverage and feeds the manifest.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { parseEquationRef, scanEquationLabels } from './equations.mjs';
|
|
13
|
+
import { parseEquationRef, repeatedEquationLabels, scanEquationLabels } from './equations.mjs';
|
|
14
|
+
import { proseOnly } from './gate-prose-notation';
|
|
14
15
|
|
|
15
16
|
export interface EquationLabelRecord {
|
|
16
17
|
readonly key: string;
|
|
@@ -31,7 +32,11 @@ export interface EquationRefRecord {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export type EquationDiagnosticCode =
|
|
34
|
-
|
|
35
|
+
| 'eq-duplicate-key'
|
|
36
|
+
| 'eq-ref-resolves'
|
|
37
|
+
| 'eq-key-unused'
|
|
38
|
+
| 'eq-label-repeated'
|
|
39
|
+
| 'eq-latex-ref';
|
|
35
40
|
|
|
36
41
|
export interface EquationDiagnostic {
|
|
37
42
|
readonly code: EquationDiagnosticCode;
|
|
@@ -88,6 +93,9 @@ function bodyEquationRefs(lesson: EquationLessonInput): EquationRefRecord[] {
|
|
|
88
93
|
return refs;
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
/** `\\eqref{eq:key}`, `\\ref{fig:key}` and their kin, as LaTeX writes a reference. */
|
|
97
|
+
const LATEX_REFERENCE = /\\(eqref|ref|autoref|cref|Cref)\s*\{([^{}\n]*)\}/g;
|
|
98
|
+
|
|
91
99
|
export function validateEquations(
|
|
92
100
|
lessons: readonly EquationLessonInput[],
|
|
93
101
|
): EquationValidation {
|
|
@@ -131,6 +139,37 @@ export function validateEquations(
|
|
|
131
139
|
});
|
|
132
140
|
}
|
|
133
141
|
numbersBySlug[lesson.slug] = table;
|
|
142
|
+
// LaTeX's own reference in prose: MDX reads its `{…}` as JavaScript, so
|
|
143
|
+
// the page fails to build with a parse error that names neither.
|
|
144
|
+
for (const match of proseOnly(lesson.body).matchAll(LATEX_REFERENCE)) {
|
|
145
|
+
const [, command, target = ''] = match;
|
|
146
|
+
const [prefix, key] = target.includes(':') ? target.split(':', 2) : ['eq', target];
|
|
147
|
+
diagnostics.push({
|
|
148
|
+
code: 'eq-latex-ref',
|
|
149
|
+
severity: 'error',
|
|
150
|
+
message:
|
|
151
|
+
`${lesson.lessonId} writes \\${command}{${target}}, LaTeX's reference: MDX reads its ` +
|
|
152
|
+
`{…} as JavaScript, and the page fails to build. Write [[${prefix}-${key}]].`,
|
|
153
|
+
file: lesson.file,
|
|
154
|
+
lessonId: lesson.lessonId,
|
|
155
|
+
key: key ?? target,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
for (const keys of repeatedEquationLabels(lesson.body) as string[][]) {
|
|
159
|
+
const [first, ...rest] = keys;
|
|
160
|
+
diagnostics.push({
|
|
161
|
+
code: 'eq-label-repeated',
|
|
162
|
+
severity: 'error',
|
|
163
|
+
message:
|
|
164
|
+
`${lesson.lessonId} puts ${keys.map((key) => `\\label{eq:${key}}`).join(' and ')} in one ` +
|
|
165
|
+
`display block, which is one numbered equation: eq:${first} names it, and ` +
|
|
166
|
+
`${rest.map((key) => `[[eq-${key}]]`).join(', ')} would point at nothing. Split the block ` +
|
|
167
|
+
'into one $$…$$ block per equation you refer to.',
|
|
168
|
+
file: lesson.file,
|
|
169
|
+
lessonId: lesson.lessonId,
|
|
170
|
+
key: rest[0]!,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
134
173
|
}
|
|
135
174
|
|
|
136
175
|
const referenced = new Set<string>();
|
|
@@ -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
|
}
|