explico 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1122 -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 +663 -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 +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 +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
|
@@ -35,9 +35,12 @@ export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
|
35
35
|
|
|
36
36
|
import {
|
|
37
37
|
GlyphResolutionError,
|
|
38
|
+
baseSymbolLatex,
|
|
39
|
+
derivedNotationLatex,
|
|
38
40
|
resolveMathGlyphs,
|
|
39
41
|
stripNotationMarkers,
|
|
40
42
|
} from './math-glyphs.mjs';
|
|
43
|
+
import { composeDerivedEntry } from './derived-notation.mjs';
|
|
41
44
|
import {
|
|
42
45
|
EQ_CAPTION_PATTERN,
|
|
43
46
|
EQ_LABEL_PATTERN,
|
|
@@ -234,6 +237,26 @@ function collectPageSharedKeys(tree) {
|
|
|
234
237
|
return keys;
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
/**
|
|
241
|
+
* A derived card (rule N23) as the remark pass records it: the frontmatter of
|
|
242
|
+
* its base and index entries composed (`derived-notation.mjs`). Only an
|
|
243
|
+
* authored `label` is known here, so the title is composed when both entries
|
|
244
|
+
* author one, which is what a table header's label needs.
|
|
245
|
+
*/
|
|
246
|
+
function derivedResolution(key, base, index) {
|
|
247
|
+
const entry = composeDerivedEntry({
|
|
248
|
+
key,
|
|
249
|
+
notation: derivedNotationLatex(base.notation, index.notation),
|
|
250
|
+
baseLatex: baseSymbolLatex(base.notation),
|
|
251
|
+
base: { ...base, label: base.label ?? '' },
|
|
252
|
+
index: { ...index, label: index.label ?? '', summary: index.meaning },
|
|
253
|
+
});
|
|
254
|
+
const labelled =
|
|
255
|
+
typeof base.label === 'string' && typeof index.label === 'string';
|
|
256
|
+
const { label, ...rest } = entry;
|
|
257
|
+
return { ...rest, ...(labelled ? { label } : {}), scope: 'derived' };
|
|
258
|
+
}
|
|
259
|
+
|
|
237
260
|
function pageGlyphScope(localDefinitions, sharedDefinitions, pageSharedKeys) {
|
|
238
261
|
return [
|
|
239
262
|
...localDefinitions.values(),
|
|
@@ -246,6 +269,8 @@ function pageGlyphScope(localDefinitions, sharedDefinitions, pageSharedKeys) {
|
|
|
246
269
|
].map((definition) => ({
|
|
247
270
|
key: definition.key,
|
|
248
271
|
notation: definition.notation,
|
|
272
|
+
// Rule N23: an entry whose index is a fixed label is no family.
|
|
273
|
+
...(definition.fixedIndex === true ? { fixedIndex: true } : {}),
|
|
249
274
|
}));
|
|
250
275
|
}
|
|
251
276
|
|
|
@@ -405,8 +430,9 @@ function inlineEquationNode(mathNode) {
|
|
|
405
430
|
* in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
|
|
406
431
|
* at its positional `#eq-<section>-<index>`. The visible number is a focusable
|
|
407
432
|
* `<a>` sibling — never authored, never inside KaTeX — so it survives print and
|
|
408
|
-
* screen readers name it "Equation 2.4". A "Copy LaTeX" button sits
|
|
409
|
-
* (`.keyed-
|
|
433
|
+
* screen readers name it "Equation 2.4". A "Copy LaTeX" button sits on the
|
|
434
|
+
* equation's bottom border, under the number (`.keyed-equation__copy`, styled
|
|
435
|
+
* in `global.css` and placed by `float-toolbar.ts`; titled "Copy LaTeX —
|
|
410
436
|
* ‹short›" when the equation has a `\caption{}`) — the equation's own
|
|
411
437
|
* copy affordance, distinct from the notation popup card's (`NotationLayer.astro`),
|
|
412
438
|
* which only exists for a symbol with a registry entry, not a bare display
|
|
@@ -451,19 +477,21 @@ function equationNode(mathNode, { key, number, short }) {
|
|
|
451
477
|
},
|
|
452
478
|
},
|
|
453
479
|
},
|
|
454
|
-
el(
|
|
455
|
-
'button',
|
|
456
|
-
{
|
|
457
|
-
type: 'button',
|
|
458
|
-
className: ['keyed-equation__copy'],
|
|
459
|
-
'data-eq-copy': '',
|
|
460
|
-
'data-latex': latex,
|
|
461
|
-
'aria-label': `Copy LaTeX for Equation ${number}`,
|
|
462
|
-
title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
|
|
463
|
-
},
|
|
464
|
-
[copyIconNode()],
|
|
465
|
-
),
|
|
466
480
|
]),
|
|
481
|
+
// On the block's border, not beside the number: the number's toolbar
|
|
482
|
+
// (`float-toolbar.ts`) takes the room around the number.
|
|
483
|
+
el(
|
|
484
|
+
'button',
|
|
485
|
+
{
|
|
486
|
+
type: 'button',
|
|
487
|
+
className: ['keyed-equation__copy'],
|
|
488
|
+
'data-eq-copy': '',
|
|
489
|
+
'data-latex': latex,
|
|
490
|
+
'aria-label': `Copy LaTeX for Equation ${number}`,
|
|
491
|
+
title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
|
|
492
|
+
},
|
|
493
|
+
[copyIconNode()],
|
|
494
|
+
),
|
|
467
495
|
],
|
|
468
496
|
};
|
|
469
497
|
}
|
|
@@ -581,7 +609,9 @@ function blockingComponent(tree, target) {
|
|
|
581
609
|
node.name &&
|
|
582
610
|
!TRANSPARENT_COMPONENTS.includes(node.name)
|
|
583
611
|
? node.name
|
|
584
|
-
:
|
|
612
|
+
: node.type === 'containerDirective'
|
|
613
|
+
? `:::${node.name ?? ''}`
|
|
614
|
+
: blocking;
|
|
585
615
|
for (const child of node.children) {
|
|
586
616
|
const found = search(child, here);
|
|
587
617
|
if (found !== undefined) return found;
|
|
@@ -593,11 +623,16 @@ function blockingComponent(tree, target) {
|
|
|
593
623
|
|
|
594
624
|
/**
|
|
595
625
|
* Why a `<Table>`/`<Figure>` on a lesson page got no number: it sits in a
|
|
596
|
-
* component other than the `TRANSPARENT_COMPONENTS`,
|
|
597
|
-
* illustrative, as an equation there is
|
|
626
|
+
* component other than the `TRANSPARENT_COMPONENTS`, or in a `:::note`
|
|
627
|
+
* aside, where a float is illustrative, as an equation there is
|
|
628
|
+
* (`keyed-jsx-floats.mjs`).
|
|
598
629
|
*/
|
|
599
630
|
function unnumberedFloatMessage(tagName, component) {
|
|
600
|
-
const where = component
|
|
631
|
+
const where = !component
|
|
632
|
+
? 'another component'
|
|
633
|
+
: component.startsWith(':::')
|
|
634
|
+
? `a ${component} aside`
|
|
635
|
+
: `<${component}>`;
|
|
601
636
|
const noun = tagName === 'Table' ? 'table' : 'figure';
|
|
602
637
|
const instead =
|
|
603
638
|
tagName === 'Table'
|
|
@@ -1305,6 +1340,16 @@ function assertManifestNumberParity({
|
|
|
1305
1340
|
|
|
1306
1341
|
for (const [key, number] of pageLabels) {
|
|
1307
1342
|
const expected = published[key];
|
|
1343
|
+
if (expected === undefined && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key)) {
|
|
1344
|
+
// The manifest left it out on purpose: `validate:content` reports the
|
|
1345
|
+
// id (`<prefix>-invalid-key`), and recompiling changes nothing.
|
|
1346
|
+
fail(
|
|
1347
|
+
file,
|
|
1348
|
+
`${slug}: the id in ${syntax(key)} is not a valid key — lowercase ` +
|
|
1349
|
+
`letters, digits and hyphens only — so the manifest leaves it unnumbered. ` +
|
|
1350
|
+
`Fix the id.`,
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1308
1353
|
if (expected === undefined) {
|
|
1309
1354
|
fail(
|
|
1310
1355
|
file,
|
|
@@ -1744,6 +1789,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1744
1789
|
// Suspension (rule N22) is lesson-body scope only.
|
|
1745
1790
|
result = resolveMathGlyphs(value, bindingScope, {
|
|
1746
1791
|
suspended: lessonId ? suspendedOperators : [],
|
|
1792
|
+
// Derived cards (rule N23) are a page's own, built by the
|
|
1793
|
+
// manifest for its body math — not the registry page's.
|
|
1794
|
+
derive: Boolean(lessonId || nonChapterPage),
|
|
1747
1795
|
});
|
|
1748
1796
|
} catch (error) {
|
|
1749
1797
|
const message =
|
|
@@ -1758,13 +1806,15 @@ export default function remarkNotation(options = {}) {
|
|
|
1758
1806
|
if (result.unresolved.length > 0) {
|
|
1759
1807
|
const unresolved = result.unresolved
|
|
1760
1808
|
.map(
|
|
1761
|
-
({ token, start, suspended, font }) =>
|
|
1809
|
+
({ token, start, suspended, font, lenders }) =>
|
|
1762
1810
|
`${tokenLocation(file, node, value, start)}: "${token}"${
|
|
1763
1811
|
suspended
|
|
1764
1812
|
? ` (${suspended} is suspended on this page, so this form needs its own entry)`
|
|
1765
1813
|
: font
|
|
1766
1814
|
? ' (a different symbol from the plain letter; it needs its own entry)'
|
|
1767
|
-
:
|
|
1815
|
+
: lenders
|
|
1816
|
+
? ` (${lenders.join(', ')} explains only its own instances with a subscript, never a bare "${token}", rule N4: give it its own entry, or \\explain{${lenders[0]}}{${token}} if it is that entry's shorthand)`
|
|
1817
|
+
: ''
|
|
1768
1818
|
}`,
|
|
1769
1819
|
)
|
|
1770
1820
|
.join(', ');
|
|
@@ -1779,9 +1829,23 @@ export default function remarkNotation(options = {}) {
|
|
|
1779
1829
|
|
|
1780
1830
|
for (const binding of result.bindings) {
|
|
1781
1831
|
if (binding.level === 'explicit') continue;
|
|
1832
|
+
if (binding.match === 'derived') continue;
|
|
1782
1833
|
resolve(binding.key, 'math', node);
|
|
1783
1834
|
references.push({ key: binding.key, kind: 'math' });
|
|
1784
1835
|
}
|
|
1836
|
+
// A derived card (rule N23) uses its base and its index entry; it
|
|
1837
|
+
// is recorded composed, so a table header reads its own label.
|
|
1838
|
+
for (const derived of result.derived) {
|
|
1839
|
+
const base = resolve(derived.base, 'math', node);
|
|
1840
|
+
const index = resolve(derived.index, 'math', node);
|
|
1841
|
+
references.push(
|
|
1842
|
+
{ key: derived.base, kind: 'math' },
|
|
1843
|
+
{ key: derived.index, kind: 'math' },
|
|
1844
|
+
);
|
|
1845
|
+
if (base && index) {
|
|
1846
|
+
resolved.set(derived.key, derivedResolution(derived.key, base, index));
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1785
1849
|
replaceMathSource(node, result.latex);
|
|
1786
1850
|
}
|
|
1787
1851
|
return;
|
|
@@ -4,6 +4,8 @@ export { slotPlaceholder } from './lab-math-slot.js';
|
|
|
4
4
|
interface LabMathScopeDefinition {
|
|
5
5
|
readonly key: string;
|
|
6
6
|
readonly notation: string;
|
|
7
|
+
/** Rule N23: the entry's index is a fixed label, so it is no family. */
|
|
8
|
+
readonly fixedIndex?: true;
|
|
7
9
|
}
|
|
8
10
|
interface RenderLabMathOptions {
|
|
9
11
|
/**
|
|
@@ -2,6 +2,8 @@ import katex from "./katex-delimiters.mjs";
|
|
|
2
2
|
import { resolveMathGlyphs, stripNotationMarkers } from "./math-glyphs.mjs";
|
|
3
3
|
import { createNotationKatexOptions } from "./katex-options.mjs";
|
|
4
4
|
import { slotPlaceholder } from "./lab-math-slot.js";
|
|
5
|
+
import { pageOfScope, registerPageDerivedNotation } from "./page-notation.js";
|
|
6
|
+
import { indexFindings } from "./gate-indices.js";
|
|
5
7
|
const SLOT_PATTERN = /\\slot\{([a-z][a-z0-9]*)\}\{((?:[^{}]|\{[^{}]*\})*)\}/g;
|
|
6
8
|
const sentinelFor = (index) => `909909${index}.909909`;
|
|
7
9
|
function renderLabMath(template, scope, { display = true } = {}) {
|
|
@@ -14,9 +16,12 @@ function renderLabMath(template, scope, { display = true } = {}) {
|
|
|
14
16
|
return ` ${sentinel} `;
|
|
15
17
|
}
|
|
16
18
|
);
|
|
19
|
+
const page = pageOfScope(scope);
|
|
17
20
|
let result;
|
|
18
21
|
try {
|
|
19
|
-
result = resolveMathGlyphs(forBinding, [...scope]
|
|
22
|
+
result = resolveMathGlyphs(forBinding, [...scope], {
|
|
23
|
+
derive: page !== void 0
|
|
24
|
+
});
|
|
20
25
|
} catch (error) {
|
|
21
26
|
throw new Error(
|
|
22
27
|
`Lab math failed to compile: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -32,6 +37,16 @@ function renderLabMath(template, scope, { display = true } = {}) {
|
|
|
32
37
|
template: ${template.trim()}`
|
|
33
38
|
);
|
|
34
39
|
}
|
|
40
|
+
if (page?.checkIndices) {
|
|
41
|
+
const findings = indexFindings(result.indices);
|
|
42
|
+
if (findings.length > 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Lab math has an index no card explains (rule N23, ${page.context}): ${findings.join("; ")}.
|
|
45
|
+
template: ${template.trim()}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (page) registerPageDerivedNotation(page.locals, result.derived, page.context);
|
|
35
50
|
let bound = result.latex;
|
|
36
51
|
let copyBound = result.latex;
|
|
37
52
|
slots.forEach(({ name, fallback }, index) => {
|
|
@@ -48,10 +63,7 @@ function renderLabMath(template, scope, { display = true } = {}) {
|
|
|
48
63
|
);
|
|
49
64
|
copyBound = copyBound.replace(sentinel, slotPlaceholder(name));
|
|
50
65
|
});
|
|
51
|
-
const html = katex.renderToString(
|
|
52
|
-
bound,
|
|
53
|
-
createNotationKatexOptions({ displayMode: display })
|
|
54
|
-
);
|
|
66
|
+
const html = katex.renderToString(bound, createNotationKatexOptions({ displayMode: display })).replaceAll('<span class="katex-mathml">', '<span class="katex-mathml" data-pagefind-ignore="">');
|
|
55
67
|
return {
|
|
56
68
|
html,
|
|
57
69
|
slots: slots.map(({ name }) => name),
|
|
@@ -21,7 +21,32 @@ interface SectionRefLessonInput {
|
|
|
21
21
|
/** The anchors of the lesson's top-level headings (`manifest.lessons[].headings`). */
|
|
22
22
|
readonly headingSlugs: readonly string[];
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* One `sec-ref-resolves` error per `[[sec-…]]` that names no heading.
|
|
26
|
+
* `pageSlugs` are the pages that are not chapters (`motion/index`), which a
|
|
27
|
+
* section reference cannot reach; the message says so instead of calling the
|
|
28
|
+
* page missing.
|
|
29
|
+
*/
|
|
30
|
+
declare function validateSectionRefs(lessons: readonly SectionRefLessonInput[], pageSlugs?: readonly string[]): MathGateDiagnostic[];
|
|
31
|
+
interface HeadingAnchorLessonInput {
|
|
32
|
+
readonly file: string;
|
|
33
|
+
readonly body: string;
|
|
34
|
+
readonly bodyLine?: number;
|
|
35
|
+
/** The lesson's outline headings in order (`manifest.lessons[].headings`). */
|
|
36
|
+
readonly headings: readonly {
|
|
37
|
+
readonly slug: string;
|
|
38
|
+
readonly text: string;
|
|
39
|
+
}[];
|
|
40
|
+
/** Every anchor the engine writes on the page, with what it belongs to. */
|
|
41
|
+
readonly anchors: ReadonlyMap<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One `heading-anchor-collision` error per heading whose anchor the engine
|
|
45
|
+
* already gives something else on the page — `## References` on a page that
|
|
46
|
+
* cites (the generated References section is `#references`), or `## Tab x`
|
|
47
|
+
* beside `<Table id="x">` (`#tab-x`). Two elements with one id send every link
|
|
48
|
+
* to the first, and the LaTeX export defines the label twice.
|
|
49
|
+
*/
|
|
50
|
+
declare function validateHeadingAnchors(lessons: readonly HeadingAnchorLessonInput[]): MathGateDiagnostic[];
|
|
26
51
|
|
|
27
|
-
export { type SectionRefLessonInput, validateSectionRefs };
|
|
52
|
+
export { type HeadingAnchorLessonInput, type SectionRefLessonInput, validateHeadingAnchors, validateSectionRefs };
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { lessonLine } from "./gate-prose-notation.js";
|
|
2
|
-
import { parseSectionRef } from "./sections.mjs";
|
|
2
|
+
import { parseSectionRef, scanOutlineBlocks } from "./sections.mjs";
|
|
3
3
|
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
4
4
|
function withoutCode(text) {
|
|
5
5
|
const blank = (block) => block.replace(/[^\n]/g, " ");
|
|
6
6
|
return text.replace(/```[\s\S]*?```/g, blank).replace(/~~~[\s\S]*?~~~/g, blank).replace(/`[^`\n]*`/g, blank);
|
|
7
7
|
}
|
|
8
|
-
function unresolved(ref, lessonExists) {
|
|
8
|
+
function unresolved(ref, lessonExists, pages) {
|
|
9
9
|
const written = ref.lesson ? `[[${ref.lesson}#sec-${ref.key}]]` : `[[sec-${ref.key}]]`;
|
|
10
|
+
const page = ref.lesson?.replace(/\/index$/, "");
|
|
11
|
+
if (page !== void 0 && !lessonExists && pages.has(page)) {
|
|
12
|
+
return `${written} names ${page}, a page that is not a chapter: a section reference reaches a chapter's top-level headings only (rule S10). Link the heading with a Markdown link instead, [its words](/${page}/#${ref.key}).`;
|
|
13
|
+
}
|
|
10
14
|
if (ref.lesson && !lessonExists) {
|
|
11
15
|
return `${written} names lesson ${ref.lesson}, which does not exist.`;
|
|
12
16
|
}
|
|
13
17
|
return `${written} names no heading: ${ref.lesson ? `lesson ${ref.lesson}` : "this lesson"} has no top-level heading anchored #${ref.key}. A heading's anchor is its text in lowercase with spaces as hyphens and punctuation dropped: \`## Two ways to name the same point\` is \`[[sec-two-ways-to-name-the-same-point]]\`.`;
|
|
14
18
|
}
|
|
15
|
-
function validateSectionRefs(lessons) {
|
|
19
|
+
function validateSectionRefs(lessons, pageSlugs = []) {
|
|
20
|
+
const pages = new Set(pageSlugs.map((slug) => slug.replace(/\/index$/, "")));
|
|
16
21
|
const headingsByLesson = new Map(
|
|
17
22
|
lessons.map((lesson) => [lesson.slug, new Set(lesson.headingSlugs)])
|
|
18
23
|
);
|
|
@@ -30,12 +35,34 @@ function validateSectionRefs(lessons) {
|
|
|
30
35
|
file: lesson.file,
|
|
31
36
|
line: lessonLine(lesson, match.index ?? 0),
|
|
32
37
|
token: match[0],
|
|
33
|
-
message: unresolved(ref, headings !== void 0)
|
|
38
|
+
message: unresolved(ref, headings !== void 0, pages)
|
|
34
39
|
});
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
return diagnostics;
|
|
38
43
|
}
|
|
44
|
+
function validateHeadingAnchors(lessons) {
|
|
45
|
+
const diagnostics = [];
|
|
46
|
+
for (const lesson of lessons) {
|
|
47
|
+
const offsets = scanOutlineBlocks(lesson.body).filter((block) => block.kind === "heading").map((block) => block.index);
|
|
48
|
+
lesson.headings.forEach((heading, index) => {
|
|
49
|
+
const owner = lesson.anchors.get(heading.slug);
|
|
50
|
+
if (owner === void 0) return;
|
|
51
|
+
const advice = heading.slug === "references" ? "Rename the heading, or drop it: the References section is generated from the page\u2019s citations." : `Rename the heading, or give ${owner} another id.`;
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
context: "structure",
|
|
54
|
+
code: "heading-anchor-collision",
|
|
55
|
+
severity: "error",
|
|
56
|
+
file: lesson.file,
|
|
57
|
+
line: lessonLine(lesson, offsets[index] ?? 0),
|
|
58
|
+
token: heading.text,
|
|
59
|
+
message: `The heading "${heading.text}" gets the anchor #${heading.slug}, which ${owner} already has on this page, so a link to either lands on the first. ${advice}`
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return diagnostics;
|
|
64
|
+
}
|
|
39
65
|
export {
|
|
66
|
+
validateHeadingAnchors,
|
|
40
67
|
validateSectionRefs
|
|
41
68
|
};
|
|
@@ -20,13 +20,26 @@ export function sectionNumbers(levels: any, { prefix, depth, unnumbered }?: {
|
|
|
20
20
|
unnumbered?: never[] | undefined;
|
|
21
21
|
}): any;
|
|
22
22
|
/**
|
|
23
|
-
* The text a heading's source
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* The text a heading's source slugs as, near enough to the way Astro would:
|
|
24
|
+
* math as it reads, a notation term its key. A heading's anchor is made from
|
|
25
|
+
* it, so a plain heading keeps the id it always had; math reads as it renders
|
|
26
|
+
* (`mathSlugText`), so `## The range $\explain{motion.range}{R}$` is
|
|
27
|
+
* `#the-range-r` and not `#the-range-explainmotionranger`.
|
|
28
28
|
*/
|
|
29
29
|
export function headingPlainText(source: any): string;
|
|
30
|
+
/**
|
|
31
|
+
* The text a reader sees for a math span: KaTeX's MathML without the LaTeX
|
|
32
|
+
* annotation, so `$\explain{motion.range}{R}$` reads `R` and `$v_0$` reads
|
|
33
|
+
* `v0`. Falls back to the source when KaTeX cannot render it.
|
|
34
|
+
*/
|
|
35
|
+
export function mathPlainText(latex: any): any;
|
|
36
|
+
/**
|
|
37
|
+
* The text a reader sees for a heading: math as it reads (`mathPlainText`)
|
|
38
|
+
* and a `[[key]]` term as its label, from `labelFor` (the page's notation),
|
|
39
|
+
* else as its key. "On this page", an unnumbered section's quoted title and a
|
|
40
|
+
* copied link name use it; the anchor is `headingPlainText`'s.
|
|
41
|
+
*/
|
|
42
|
+
export function headingDisplayText(source: any, { labelFor }?: {}): string;
|
|
30
43
|
/**
|
|
31
44
|
* Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
|
|
32
45
|
* run over their plain text: the same slugger with the same duplicate
|
|
@@ -36,9 +49,14 @@ export function slugHeadings(texts: any): any;
|
|
|
36
49
|
/**
|
|
37
50
|
* Every top-level heading of a lesson body in document order, as the
|
|
38
51
|
* manifest records it: `{ level, text, slug, number? }`. `prefix` is the
|
|
39
|
-
* chapter's `‹part›.‹chapter›`; without one nothing is numbered.
|
|
52
|
+
* chapter's `‹part›.‹chapter›`; without one nothing is numbered. `text` is
|
|
53
|
+
* what a reader sees (`headingDisplayText`, with `labelFor` naming a
|
|
54
|
+
* `[[key]]` term); the `slug` is made from `headingPlainText`.
|
|
55
|
+
* `math: true` marks a heading with inline math, whose text Astro's own
|
|
56
|
+
* heading list garbles (`LayoutPageSidebar.astro` relabels it).
|
|
40
57
|
*/
|
|
41
58
|
export function scanSectionHeadings(markdown: any, options?: {}): {
|
|
59
|
+
math?: boolean | undefined;
|
|
42
60
|
number?: any;
|
|
43
61
|
level: number;
|
|
44
62
|
text: string;
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
30
|
+
import katex from 'katex';
|
|
31
|
+
import { createNotationKatexOptions } from './katex-options.mjs';
|
|
30
32
|
import { headingCardTier, stripHeadingMarker } from './rehype-heading-anchors.mjs';
|
|
31
33
|
|
|
32
34
|
/** The deepest heading level numbered when a course sets nothing (`####`). */
|
|
@@ -113,19 +115,18 @@ const ATX_HEADING = /^[ \t]{0,3}(#{1,6})(?:[ \t]+([^\n]*?))?(?:[ \t]+#+)?[ \t]*$
|
|
|
113
115
|
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* source
|
|
119
|
-
* term keeps its key; a link keeps its text; an image, a citation and an MDX
|
|
118
|
+
* A heading source's text, with `math` and `term` deciding what a `$…$` span
|
|
119
|
+
* and a `[[key]]` contribute. Markup goes, the words stay; inline code keeps
|
|
120
|
+
* its source; a link keeps its text; an image, a citation and an MDX
|
|
120
121
|
* expression contribute nothing; the authoring marker is dropped.
|
|
121
122
|
*/
|
|
122
|
-
|
|
123
|
+
function headingText(source, { math, term }) {
|
|
123
124
|
const literals = [];
|
|
124
125
|
const keep = (value) => `\u0000${literals.push(value) - 1}\u0000`;
|
|
125
126
|
let text = stripHeadingMarker(source ?? '')
|
|
126
127
|
.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_, _ticks, code) => keep(code.trim()))
|
|
127
|
-
.replace(/\$([^$\n]+)\$/g, (_, latex) => keep(latex.trim()))
|
|
128
|
-
.replace(/\[\[\s*([^\][]+?)\s*\]\]/g, (_, key) => keep(key))
|
|
128
|
+
.replace(/\$([^$\n]+)\$/g, (_, latex) => keep(math(latex.trim())))
|
|
129
|
+
.replace(/\[\[\s*([^\][]+?)\s*\]\]/g, (_, key) => keep(term(key)))
|
|
129
130
|
.replace(/\[@[^\]]*\]/g, '')
|
|
130
131
|
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
131
132
|
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
@@ -138,17 +139,84 @@ export function headingPlainText(source) {
|
|
|
138
139
|
.replace(/(^|[^\w])_(?=\S)([\s\S]*?\S)_(?!\w)/g, '$1$2')
|
|
139
140
|
.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '$1')
|
|
140
141
|
.replace(/\\([!-/:-@[-`{-~])/g, '$1')
|
|
141
|
-
.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, name) =>
|
|
142
|
-
if (name[0] !== '#') return ENTITIES[name.toLowerCase()] ?? entity;
|
|
143
|
-
const code = name[1] === 'x' || name[1] === 'X'
|
|
144
|
-
? Number.parseInt(name.slice(2), 16)
|
|
145
|
-
: Number.parseInt(name.slice(1), 10);
|
|
146
|
-
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
147
|
-
});
|
|
142
|
+
.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, name) => decodeEntity(entity, name));
|
|
148
143
|
text = text.replace(/\u0000(\d+)\u0000/g, (_, index) => literals[Number(index)]);
|
|
149
144
|
return text.replace(/\s+/g, ' ').trim();
|
|
150
145
|
}
|
|
151
146
|
|
|
147
|
+
function decodeEntity(entity, name) {
|
|
148
|
+
if (name[0] !== '#') return ENTITIES[name.toLowerCase()] ?? entity;
|
|
149
|
+
const code = name[1] === 'x' || name[1] === 'X'
|
|
150
|
+
? Number.parseInt(name.slice(2), 16)
|
|
151
|
+
: Number.parseInt(name.slice(1), 10);
|
|
152
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The text a heading's source slugs as, near enough to the way Astro would:
|
|
157
|
+
* math as it reads, a notation term its key. A heading's anchor is made from
|
|
158
|
+
* it, so a plain heading keeps the id it always had; math reads as it renders
|
|
159
|
+
* (`mathSlugText`), so `## The range $\explain{motion.range}{R}$` is
|
|
160
|
+
* `#the-range-r` and not `#the-range-explainmotionranger`.
|
|
161
|
+
*/
|
|
162
|
+
export function headingPlainText(source) {
|
|
163
|
+
return headingText(source, { math: mathSlugText, term: (key) => key });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A Greek letter as LaTeX names it, for a slug that stays in ASCII. */
|
|
167
|
+
const GREEK_NAMES = {
|
|
168
|
+
α: 'alpha', β: 'beta', γ: 'gamma', δ: 'delta', ε: 'epsilon', ϵ: 'epsilon', ζ: 'zeta', η: 'eta',
|
|
169
|
+
θ: 'theta', ϑ: 'theta', ι: 'iota', κ: 'kappa', λ: 'lambda', μ: 'mu', ν: 'nu', ξ: 'xi', ο: 'omicron',
|
|
170
|
+
π: 'pi', ϖ: 'pi', ρ: 'rho', ϱ: 'rho', σ: 'sigma', ς: 'sigma', τ: 'tau', υ: 'upsilon', φ: 'phi',
|
|
171
|
+
ϕ: 'phi', χ: 'chi', ψ: 'psi', ω: 'omega', Γ: 'Gamma', Δ: 'Delta', Θ: 'Theta', Λ: 'Lambda',
|
|
172
|
+
Ξ: 'Xi', Π: 'Pi', Σ: 'Sigma', Υ: 'Upsilon', Φ: 'Phi', Ψ: 'Psi', Ω: 'Omega',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The text a heading's math slugs as: what it reads, each Greek letter spelled
|
|
177
|
+
* as LaTeX names it. `$e^{\lambda\cos(u)}$` reads `eλcos(u)` and slugs as
|
|
178
|
+
* `elambdacosu`, the anchor its source always gave it; what changes is the
|
|
179
|
+
* markup that never shows (`\explain{motion.range}{R}` is `R`).
|
|
180
|
+
*/
|
|
181
|
+
function mathSlugText(latex) {
|
|
182
|
+
return mathPlainText(latex).replace(/[\u0370-\u03FF]/g, (char) => GREEK_NAMES[char] ?? char);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The text a reader sees for a math span: KaTeX's MathML without the LaTeX
|
|
187
|
+
* annotation, so `$\explain{motion.range}{R}$` reads `R` and `$v_0$` reads
|
|
188
|
+
* `v0`. Falls back to the source when KaTeX cannot render it.
|
|
189
|
+
*/
|
|
190
|
+
export function mathPlainText(latex) {
|
|
191
|
+
let markup;
|
|
192
|
+
try {
|
|
193
|
+
markup = katex.renderToString(latex, createNotationKatexOptions());
|
|
194
|
+
} catch {
|
|
195
|
+
return latex;
|
|
196
|
+
}
|
|
197
|
+
const mathml = /<math[\s\S]*?<\/math>/.exec(markup)?.[0] ?? '';
|
|
198
|
+
return mathml
|
|
199
|
+
.replace(/<annotation[\s\S]*?<\/annotation>/g, '')
|
|
200
|
+
.replace(/<[^>]+>/g, '')
|
|
201
|
+
.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, name) => decodeEntity(entity, name))
|
|
202
|
+
.replace(/[\u200b\u2061-\u2064]/g, '')
|
|
203
|
+
.replace(/\s+/g, ' ')
|
|
204
|
+
.trim();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The text a reader sees for a heading: math as it reads (`mathPlainText`)
|
|
209
|
+
* and a `[[key]]` term as its label, from `labelFor` (the page's notation),
|
|
210
|
+
* else as its key. "On this page", an unnumbered section's quoted title and a
|
|
211
|
+
* copied link name use it; the anchor is `headingPlainText`'s.
|
|
212
|
+
*/
|
|
213
|
+
export function headingDisplayText(source, { labelFor } = {}) {
|
|
214
|
+
return headingText(source, {
|
|
215
|
+
math: mathPlainText,
|
|
216
|
+
term: (key) => labelFor?.(key) ?? key,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
152
220
|
/**
|
|
153
221
|
* Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
|
|
154
222
|
* run over their plain text: the same slugger with the same duplicate
|
|
@@ -171,18 +239,24 @@ export function slugHeadings(texts) {
|
|
|
171
239
|
/**
|
|
172
240
|
* Every top-level heading of a lesson body in document order, as the
|
|
173
241
|
* manifest records it: `{ level, text, slug, number? }`. `prefix` is the
|
|
174
|
-
* chapter's `‹part›.‹chapter›`; without one nothing is numbered.
|
|
242
|
+
* chapter's `‹part›.‹chapter›`; without one nothing is numbered. `text` is
|
|
243
|
+
* what a reader sees (`headingDisplayText`, with `labelFor` naming a
|
|
244
|
+
* `[[key]]` term); the `slug` is made from `headingPlainText`.
|
|
245
|
+
* `math: true` marks a heading with inline math, whose text Astro's own
|
|
246
|
+
* heading list garbles (`LayoutPageSidebar.astro` relabels it).
|
|
175
247
|
*/
|
|
176
248
|
export function scanSectionHeadings(markdown, options = {}) {
|
|
177
249
|
const found = [];
|
|
178
250
|
for (const match of outlineSource(markdown).matchAll(ATX_HEADING)) {
|
|
179
251
|
found.push({
|
|
180
252
|
level: match[1].length,
|
|
181
|
-
|
|
253
|
+
slugText: headingPlainText(match[2] ?? ''),
|
|
254
|
+
text: headingDisplayText(match[2] ?? '', options),
|
|
255
|
+
math: /\$[^$\n]+\$/.test(stripHeadingMarker(match[2] ?? '')),
|
|
182
256
|
meta: headingCardTier(match[2] ?? '') === 'meta',
|
|
183
257
|
});
|
|
184
258
|
}
|
|
185
|
-
const slugs = slugHeadings(found.map((heading) => heading.
|
|
259
|
+
const slugs = slugHeadings(found.map((heading) => heading.slugText));
|
|
186
260
|
const numbers = sectionNumbers(
|
|
187
261
|
found.map((heading) => heading.level),
|
|
188
262
|
{ ...options, unnumbered: found.map((heading) => heading.meta) },
|
|
@@ -192,6 +266,7 @@ export function scanSectionHeadings(markdown, options = {}) {
|
|
|
192
266
|
text: heading.text,
|
|
193
267
|
slug: slugs[index],
|
|
194
268
|
...(numbers[index] === undefined ? {} : { number: numbers[index] }),
|
|
269
|
+
...(heading.math ? { math: true } : {}),
|
|
195
270
|
}));
|
|
196
271
|
}
|
|
197
272
|
|
|
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
2
2
|
|
|
3
3
|
/** `validateKeyedFloats` for `<Table>` — see `keyed-float-validate.ts`. */
|
|
4
4
|
|
|
5
|
-
type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused' | 'tab-removed-prop';
|
|
5
|
+
type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused' | 'tab-removed-prop' | 'tab-unread-prop' | 'tab-several-tables';
|
|
6
6
|
interface TableDiagnostic {
|
|
7
7
|
readonly code: TableDiagnosticCode;
|
|
8
8
|
readonly severity: 'error' | 'warning' | 'info';
|
|
@@ -2,11 +2,34 @@ import {
|
|
|
2
2
|
validateKeyedFloats
|
|
3
3
|
} from "./keyed-float-validate.js";
|
|
4
4
|
function validateTables(lessons) {
|
|
5
|
-
|
|
5
|
+
const validation = validateKeyedFloats(lessons, {
|
|
6
6
|
tagName: "Table",
|
|
7
7
|
prefix: "tab",
|
|
8
8
|
noun: "table"
|
|
9
9
|
});
|
|
10
|
+
const several = lessons.flatMap(severalTables);
|
|
11
|
+
return several.length === 0 ? validation : { ...validation, diagnostics: [...validation.diagnostics, ...several] };
|
|
12
|
+
}
|
|
13
|
+
const DELIMITER_ROW = /^[ \t]*\|?(?:[ \t]*:?-+:?[ \t]*\|)+(?:[ \t]*:?-+:?[ \t]*)?$/gm;
|
|
14
|
+
function severalTables(lesson) {
|
|
15
|
+
const blank = (match) => match.replace(/[^\n]/g, " ");
|
|
16
|
+
const body = lesson.body.replace(/```[\s\S]*?```/g, blank).replace(/\{\/\*[\s\S]*?\*\/\}/g, blank);
|
|
17
|
+
const found = [];
|
|
18
|
+
for (const table of body.matchAll(/<Table\b((?:[^>"']|"[^"]*"|'[^']*')*?)(?<!\/)>([\s\S]*?)<\/Table>/g)) {
|
|
19
|
+
const inner = table[2] ?? "";
|
|
20
|
+
const count = (inner.match(DELIMITER_ROW)?.length ?? 0) + (inner.match(/<table\b/g)?.length ?? 0);
|
|
21
|
+
if (count < 2) continue;
|
|
22
|
+
const key = /\bid\s*=\s*["']([^"']*)["']/.exec(table[1] ?? "")?.[1];
|
|
23
|
+
found.push({
|
|
24
|
+
code: "tab-several-tables",
|
|
25
|
+
severity: "error",
|
|
26
|
+
message: `${lesson.lessonId} has a <Table${key ? ` id="${key}"` : ""}> holding ${count} tables: a <Table> holds one, with one number and one caption, and every export keeps only the first. Give each table its own <Table id>, or join them into one.`,
|
|
27
|
+
file: lesson.file,
|
|
28
|
+
lessonId: lesson.lessonId,
|
|
29
|
+
...key ? { key } : {}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
10
33
|
}
|
|
11
34
|
export {
|
|
12
35
|
validateTables
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The advance of `text` at `fontPx`, in px, rounded up to a whole pixel. */
|
|
2
|
+
declare function textWidth(text: string, fontPx: number): number;
|
|
3
|
+
/**
|
|
4
|
+
* Break `text` into lines no wider than `maxWidth` px, greedily at spaces,
|
|
5
|
+
* the way a browser wraps it. A word wider than `maxWidth` keeps a line of its
|
|
6
|
+
* own (the caller widens the box to it); a space never ends or starts a line.
|
|
7
|
+
*/
|
|
8
|
+
declare function wrapText(text: string, maxWidth: number, fontPx: number): string[];
|
|
9
|
+
|
|
10
|
+
export { textWidth, wrapText };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { UNITS_PER_EM, WIDTH_BLOCKS } from "./text-widths.js";
|
|
2
|
+
const WIDTHS = /* @__PURE__ */ new Map();
|
|
3
|
+
for (const [first, widths] of WIDTH_BLOCKS) {
|
|
4
|
+
widths.forEach((width, offset) => {
|
|
5
|
+
if (width >= 0) WIDTHS.set(first + offset, width / UNITS_PER_EM);
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
const FALLBACK_EM = 1;
|
|
9
|
+
const COMBINING_MARK = new RegExp("\\p{M}", "u");
|
|
10
|
+
function advanceEm(char) {
|
|
11
|
+
if (COMBINING_MARK.test(char)) return 0;
|
|
12
|
+
return WIDTHS.get(char.codePointAt(0) ?? 0) ?? FALLBACK_EM;
|
|
13
|
+
}
|
|
14
|
+
function textWidth(text, fontPx) {
|
|
15
|
+
let em = 0;
|
|
16
|
+
for (const char of text) em += advanceEm(char);
|
|
17
|
+
return Math.ceil(em * fontPx - 1e-6);
|
|
18
|
+
}
|
|
19
|
+
function wrapText(text, maxWidth, fontPx) {
|
|
20
|
+
const words = text.trim().split(/[ \t\r\n]+/u).filter(Boolean);
|
|
21
|
+
const space = textWidth(" ", fontPx);
|
|
22
|
+
const lines = [];
|
|
23
|
+
let line = "";
|
|
24
|
+
let lineWidth = 0;
|
|
25
|
+
for (const word of words) {
|
|
26
|
+
const wordWidth = textWidth(word, fontPx);
|
|
27
|
+
if (line && lineWidth + space + wordWidth > maxWidth) {
|
|
28
|
+
lines.push(line);
|
|
29
|
+
line = "";
|
|
30
|
+
lineWidth = 0;
|
|
31
|
+
}
|
|
32
|
+
line = line ? `${line} ${word}` : word;
|
|
33
|
+
lineWidth = lineWidth ? lineWidth + space + wordWidth : wordWidth;
|
|
34
|
+
}
|
|
35
|
+
if (line) lines.push(line);
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
textWidth,
|
|
40
|
+
wrapText
|
|
41
|
+
};
|