explico 0.8.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1257 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +4 -1
- package/dist/compiler/logging.js +4 -1
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +3 -2
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +60 -31
- package/dist/reference/equations.mjs +83 -43
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +1 -0
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +45 -21
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +22 -6
- package/dist/reference/gate-math.js +42 -8
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +572 -37
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +4 -4
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +17 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +5 -3
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1152 -2
- package/docs/authoring/notation.md +576 -322
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +8 -2
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +572 -37
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +16 -31
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +17 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1467 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -46,7 +46,8 @@ const IGNORED_MATH_TOKENS = new Set([
|
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
48
|
// KaTeX font ids mapped back to their LaTeX command, so a single-letter font
|
|
49
|
-
// node can be reconstructed as its source token (`\mathbb{N}`).
|
|
49
|
+
// node can be reconstructed as its source token (`\mathbb{N}`). `\bm{x}`
|
|
50
|
+
// parses as `boldsymbol`, so it is rebuilt as `\boldsymbol{x}`.
|
|
50
51
|
const FONT_COMMANDS = new Map([
|
|
51
52
|
['mathbb', '\\mathbb'],
|
|
52
53
|
['mathcal', '\\mathcal'],
|
|
@@ -55,8 +56,73 @@ const FONT_COMMANDS = new Map([
|
|
|
55
56
|
['mathrm', '\\mathrm'],
|
|
56
57
|
['mathbf', '\\mathbf'],
|
|
57
58
|
['mathit', '\\mathit'],
|
|
59
|
+
['mathnormal', '\\mathnormal'],
|
|
60
|
+
['mathsf', '\\mathsf'],
|
|
61
|
+
['mathtt', '\\mathtt'],
|
|
62
|
+
['boldsymbol', '\\boldsymbol'],
|
|
58
63
|
]);
|
|
59
64
|
|
|
65
|
+
// A font that makes a different symbol: `\mathcal{N}` is not `N`, and
|
|
66
|
+
// `\mathbf{x}` is not `x`, so a letter in one of these never borrows the
|
|
67
|
+
// plain letter's card (rule N4). `\mathit` / `\mathnormal` render the plain
|
|
68
|
+
// italic letter and fall back as before; `\mathrm` is a roman label, skipped
|
|
69
|
+
// whole (rule N10).
|
|
70
|
+
const DISTINCT_FONTS = new Set([
|
|
71
|
+
'mathbf',
|
|
72
|
+
'boldsymbol',
|
|
73
|
+
'mathcal',
|
|
74
|
+
'mathscr',
|
|
75
|
+
'mathfrak',
|
|
76
|
+
'mathbb',
|
|
77
|
+
'mathsf',
|
|
78
|
+
'mathtt',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// Every math-mode accent KaTeX 0.16 parses, over and under (`accent` /
|
|
82
|
+
// `accentUnder` nodes, named by their `label`). Text-mode accents (`\'`, `\^`,
|
|
83
|
+
// …) fail the strict parse, so they never reach the resolver.
|
|
84
|
+
const ACCENT_LABELS = [
|
|
85
|
+
'\\acute',
|
|
86
|
+
'\\grave',
|
|
87
|
+
'\\ddot',
|
|
88
|
+
'\\tilde',
|
|
89
|
+
'\\bar',
|
|
90
|
+
'\\breve',
|
|
91
|
+
'\\check',
|
|
92
|
+
'\\hat',
|
|
93
|
+
'\\vec',
|
|
94
|
+
'\\dot',
|
|
95
|
+
'\\mathring',
|
|
96
|
+
'\\widecheck',
|
|
97
|
+
'\\widehat',
|
|
98
|
+
'\\widetilde',
|
|
99
|
+
'\\overrightarrow',
|
|
100
|
+
'\\overleftarrow',
|
|
101
|
+
'\\Overrightarrow',
|
|
102
|
+
'\\overleftrightarrow',
|
|
103
|
+
'\\overgroup',
|
|
104
|
+
'\\overlinesegment',
|
|
105
|
+
'\\overleftharpoon',
|
|
106
|
+
'\\overrightharpoon',
|
|
107
|
+
'\\underleftarrow',
|
|
108
|
+
'\\underrightarrow',
|
|
109
|
+
'\\underleftrightarrow',
|
|
110
|
+
'\\undergroup',
|
|
111
|
+
'\\underlinesegment',
|
|
112
|
+
'\\utilde',
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Every decoration rule N22 treats as an operator, by canonical name: each
|
|
117
|
+
* accent (`\hat`, `\widehat`, … — `\widehat` is not `\hat`), `\overline`,
|
|
118
|
+
* `\underline`, and the superscript marks `'` (any run of primes), `*` (`*`
|
|
119
|
+
* or `\ast`) and `\dagger`. `^\top`, `^{-1}`, powers, subscripts and fonts
|
|
120
|
+
* are not decorations.
|
|
121
|
+
*/
|
|
122
|
+
export const DECORATION_NAMES = Object.freeze(
|
|
123
|
+
new Set([...ACCENT_LABELS, '\\overline', '\\underline', "'", '*', '\\dagger']),
|
|
124
|
+
);
|
|
125
|
+
|
|
60
126
|
function fontBodyText(value) {
|
|
61
127
|
if (Array.isArray(value)) {
|
|
62
128
|
const parts = value.map(fontBodyText);
|
|
@@ -451,6 +517,9 @@ function isIdentifierNode(node) {
|
|
|
451
517
|
if (node.type !== 'mathord' && node.type !== 'textord') return false;
|
|
452
518
|
if (typeof node.text !== 'string') return false;
|
|
453
519
|
if (/^[0-9.,]+$/u.test(node.text)) return false;
|
|
520
|
+
// A prime is a mark on the symbol before it (rule N22), never a symbol of
|
|
521
|
+
// its own — whether typed `'` or spelled `^\prime`.
|
|
522
|
+
if (node.text === '\\prime') return false;
|
|
454
523
|
return /^(?:\p{L}|\\[A-Za-z]+)$/u.test(node.text);
|
|
455
524
|
}
|
|
456
525
|
|
|
@@ -461,16 +530,198 @@ const sameSpan = (left, right) =>
|
|
|
461
530
|
const overlaps = (left, right) =>
|
|
462
531
|
left.start < right.end && right.start < left.end;
|
|
463
532
|
|
|
533
|
+
// --- decorations (rule N22) ---------------------------------------------
|
|
534
|
+
|
|
535
|
+
/** The nodes of a script or group, with `{…}` groups flattened. */
|
|
536
|
+
function flattenGroup(value) {
|
|
537
|
+
if (Array.isArray(value)) return value.flatMap(flattenGroup);
|
|
538
|
+
if (!isNode(value)) return [];
|
|
539
|
+
if (value.type === 'ordgroup') return flattenGroup(value.body);
|
|
540
|
+
return [value];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const isPrimeNode = (node) =>
|
|
544
|
+
node.type === 'textord' && node.text === '\\prime';
|
|
545
|
+
|
|
546
|
+
/** The mark a superscript is, if any: a run of primes, `*` / `\ast`, or `\dagger`. */
|
|
547
|
+
function scriptMarkName(sup) {
|
|
548
|
+
const items = flattenGroup(sup);
|
|
549
|
+
if (items.length === 0) return undefined;
|
|
550
|
+
if (isPrimeNode(items[0])) return "'";
|
|
551
|
+
if (items.length !== 1 || items[0].type !== 'atom') return undefined;
|
|
552
|
+
if (items[0].text === '*' || items[0].text === '\\ast') return '*';
|
|
553
|
+
if (items[0].text === '\\dagger') return '\\dagger';
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The decoration a node is, as `{ name, part, command? }` — `part` is the child
|
|
559
|
+
* key holding what it decorates, `command` the source text before it — or
|
|
560
|
+
* `undefined` for any other node.
|
|
561
|
+
*/
|
|
562
|
+
function decorationOf(node) {
|
|
563
|
+
if (!isNode(node)) return undefined;
|
|
564
|
+
if (node.type === 'accent' || node.type === 'accentUnder') {
|
|
565
|
+
return DECORATION_NAMES.has(node.label)
|
|
566
|
+
? { name: node.label, part: 'base', command: node.label }
|
|
567
|
+
: undefined;
|
|
568
|
+
}
|
|
569
|
+
if (node.type === 'overline' || node.type === 'underline') {
|
|
570
|
+
const name = `\\${node.type}`;
|
|
571
|
+
return { name, part: 'body', command: name };
|
|
572
|
+
}
|
|
573
|
+
if (node.type === 'supsub' && isNode(node.base)) {
|
|
574
|
+
const name = scriptMarkName(node.sup);
|
|
575
|
+
return name === undefined ? undefined : { name, part: 'base' };
|
|
576
|
+
}
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function mergeSpans(spans) {
|
|
581
|
+
const present = spans.filter(Boolean);
|
|
582
|
+
if (present.length === 0) return undefined;
|
|
583
|
+
return {
|
|
584
|
+
start: Math.min(...present.map((span) => span.start)),
|
|
585
|
+
end: Math.max(...present.map((span) => span.end)),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Every source spelling of a KaTeX font id, for widening a font node's span
|
|
590
|
+
// over its command (`\bm{x}` and `\boldsymbol{x}` are one node).
|
|
591
|
+
const FONT_SOURCE_COMMANDS = new Map(
|
|
592
|
+
[...FONT_COMMANDS].map(([font, command]) => [
|
|
593
|
+
font,
|
|
594
|
+
{
|
|
595
|
+
mathbf: ['\\mathbf', '\\bold'],
|
|
596
|
+
boldsymbol: ['\\boldsymbol', '\\bm'],
|
|
597
|
+
mathbb: ['\\mathbb', '\\Bbb'],
|
|
598
|
+
mathfrak: ['\\mathfrak', '\\frak'],
|
|
599
|
+
}[font] ?? [command],
|
|
600
|
+
]),
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Widen the span of an accent's (or `\overline`'s, or a font's) argument over
|
|
605
|
+
* the command that precedes it and the braces around it. `undefined` when no
|
|
606
|
+
* command in `commands` is there — a node a macro produced, whose source is
|
|
607
|
+
* elsewhere.
|
|
608
|
+
*/
|
|
609
|
+
function widenOverCommand(latex, span, commands) {
|
|
610
|
+
let cursor = span.start;
|
|
611
|
+
for (;;) {
|
|
612
|
+
while (cursor > 0 && /\s/.test(latex[cursor - 1])) cursor -= 1;
|
|
613
|
+
if (latex[cursor - 1] === '{' && !isEscaped(latex, cursor - 1)) {
|
|
614
|
+
cursor -= 1;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
const command = commands.find(
|
|
620
|
+
(name) => latex.slice(cursor - name.length, cursor) === name,
|
|
621
|
+
);
|
|
622
|
+
if (command === undefined) return undefined;
|
|
623
|
+
const start = cursor - command.length;
|
|
624
|
+
const argument = skipWhitespace(latex, cursor);
|
|
625
|
+
const group =
|
|
626
|
+
latex[argument] === '{' ? readBracedGroup(latex, argument) : undefined;
|
|
627
|
+
return { start, end: Math.max(span.end, group?.end ?? span.end) };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** Extend a span over the `'` primes KaTeX leaves without a `loc`. */
|
|
631
|
+
function extendOverPrimes(latex, end) {
|
|
632
|
+
let cursor = skipWhitespace(latex, end);
|
|
633
|
+
if (latex[cursor] !== "'") return end;
|
|
634
|
+
while (latex[cursor] === "'") cursor += 1;
|
|
635
|
+
return cursor;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* A node's full extent in the source. {@link nodeSpan} reads only the `loc`s
|
|
640
|
+
* KaTeX records, and KaTeX records none on an accent, an `\overline`, a
|
|
641
|
+
* `supsub` or a `'` prime — so `\hat{\beta}` spans only `\beta`, and `b''`
|
|
642
|
+
* only `b`. This widens each decoration over its command text or its trailing
|
|
643
|
+
* primes, so a binding over a decorated form wraps the whole of it.
|
|
644
|
+
*/
|
|
645
|
+
function sourceSpan(value, latex) {
|
|
646
|
+
if (Array.isArray(value)) {
|
|
647
|
+
return mergeSpans(value.map((child) => sourceSpan(child, latex)));
|
|
648
|
+
}
|
|
649
|
+
if (!isNode(value)) return undefined;
|
|
650
|
+
const own =
|
|
651
|
+
value.loc &&
|
|
652
|
+
Number.isInteger(value.loc.start) &&
|
|
653
|
+
Number.isInteger(value.loc.end) &&
|
|
654
|
+
isAuthoredLocation(value.loc, latex)
|
|
655
|
+
? { start: value.loc.start, end: value.loc.end }
|
|
656
|
+
: undefined;
|
|
657
|
+
let span = mergeSpans([
|
|
658
|
+
own,
|
|
659
|
+
...childValues(value).map((child) => sourceSpan(child, latex)),
|
|
660
|
+
]);
|
|
661
|
+
if (!span) return undefined;
|
|
662
|
+
|
|
663
|
+
const commands =
|
|
664
|
+
value.type === 'font'
|
|
665
|
+
? FONT_SOURCE_COMMANDS.get(value.font)
|
|
666
|
+
: decorationOf(value)?.command;
|
|
667
|
+
if (commands !== undefined) {
|
|
668
|
+
span = widenOverCommand(latex, span, [commands].flat()) ?? span;
|
|
669
|
+
} else if (
|
|
670
|
+
value.type === 'supsub' &&
|
|
671
|
+
flattenGroup(value.sup).some((node) => isPrimeNode(node) && !node.loc)
|
|
672
|
+
) {
|
|
673
|
+
span = { start: span.start, end: extendOverPrimes(latex, span.end) };
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
start: span.start,
|
|
677
|
+
end: trimTrailingWhitespace(latex, span.start, span.end),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** A decoration as the resolver records it, or `undefined` if its source text is not found. */
|
|
682
|
+
function decorationRecord(node, latex) {
|
|
683
|
+
const decoration = decorationOf(node);
|
|
684
|
+
if (decoration === undefined) return undefined;
|
|
685
|
+
const span = sourceSpan(node, latex);
|
|
686
|
+
const partSpan = sourceSpan(node[decoration.part], latex);
|
|
687
|
+
if (!span || !partSpan) return undefined;
|
|
688
|
+
// The decoration's own text (`\hat{`, `'`) was not found: nothing to wrap.
|
|
689
|
+
if (span.start === partSpan.start && span.end === partSpan.end) {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
const inner = flattenGroup(node[decoration.part]);
|
|
693
|
+
const single =
|
|
694
|
+
inner.length === 1 &&
|
|
695
|
+
(isIdentifierNode(inner[0]) ||
|
|
696
|
+
(inner[0].type === 'font' && FONT_COMMANDS.has(inner[0].font)));
|
|
697
|
+
const baseSpan = unwrapEnclosingGroupSpan(latex, partSpan);
|
|
698
|
+
return {
|
|
699
|
+
name: decoration.name,
|
|
700
|
+
start: span.start,
|
|
701
|
+
end: span.end,
|
|
702
|
+
part: partSpan,
|
|
703
|
+
base: single ? latex.slice(baseSpan.start, baseSpan.end).trim() : undefined,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const ROOT_CONTEXT = Object.freeze({ decorations: [], font: undefined });
|
|
708
|
+
|
|
464
709
|
/**
|
|
465
710
|
* Walk the parse tree once, collecting: every maximal run of sibling nodes
|
|
466
711
|
* (`containers`, where a canonical form is matched), every identifier atom with
|
|
467
|
-
* its source span,
|
|
468
|
-
*
|
|
712
|
+
* its source span, the spans of every sub/superscript (so a symbol nested in a
|
|
713
|
+
* script can still be marked), and every decoration (rule N22).
|
|
714
|
+
*
|
|
715
|
+
* Each identifier also carries the decorations it sits under (outermost first;
|
|
716
|
+
* the letter in `\hat{x}'` is under both) and the distinct font it is set in
|
|
717
|
+
* (`font: 'mathbf'`), if any. No container is recorded inside a distinct
|
|
718
|
+
* font's body, so the `N` of `\mathcal{N}` can never match an `N` entry.
|
|
469
719
|
*/
|
|
470
720
|
function collectSourceStructure(tree, latex) {
|
|
471
721
|
const containers = [];
|
|
472
722
|
const identifiers = [];
|
|
473
723
|
const scriptSpans = [];
|
|
724
|
+
const decorations = [];
|
|
474
725
|
const seenContainers = new Set();
|
|
475
726
|
const seenIdentifiers = new Set();
|
|
476
727
|
|
|
@@ -485,10 +736,12 @@ function collectSourceStructure(tree, latex) {
|
|
|
485
736
|
containers.push(usable);
|
|
486
737
|
}
|
|
487
738
|
|
|
488
|
-
function visitValue(value, ignored = false) {
|
|
739
|
+
function visitValue(value, ignored = false, context = ROOT_CONTEXT) {
|
|
489
740
|
if (Array.isArray(value)) {
|
|
490
|
-
if (!ignored && value.every(isNode))
|
|
491
|
-
|
|
741
|
+
if (!ignored && context.font === undefined && value.every(isNode)) {
|
|
742
|
+
addContainer(value);
|
|
743
|
+
}
|
|
744
|
+
for (const child of value) visitValue(child, ignored, context);
|
|
492
745
|
return;
|
|
493
746
|
}
|
|
494
747
|
if (!isNode(value)) return;
|
|
@@ -509,6 +762,8 @@ function collectSourceStructure(tree, latex) {
|
|
|
509
762
|
node: value,
|
|
510
763
|
signature: sequenceSignature([value]),
|
|
511
764
|
...span,
|
|
765
|
+
decorations: context.decorations,
|
|
766
|
+
font: context.font,
|
|
512
767
|
});
|
|
513
768
|
}
|
|
514
769
|
}
|
|
@@ -520,32 +775,126 @@ function collectSourceStructure(tree, latex) {
|
|
|
520
775
|
}
|
|
521
776
|
}
|
|
522
777
|
|
|
523
|
-
|
|
778
|
+
const decoration = ignored ? undefined : decorationRecord(value, latex);
|
|
779
|
+
if (decoration) decorations.push(decoration);
|
|
780
|
+
const decoratedPart = decoration ? decorationOf(value).part : undefined;
|
|
781
|
+
const fontContext =
|
|
782
|
+
value.type === 'font' && DISTINCT_FONTS.has(value.font)
|
|
783
|
+
? { ...context, font: value.font }
|
|
784
|
+
: context;
|
|
785
|
+
|
|
786
|
+
for (const key of SOURCE_CHILD_KEYS) {
|
|
787
|
+
const child = value[key];
|
|
788
|
+
if (child === undefined || child === null) continue;
|
|
789
|
+
const childContext =
|
|
790
|
+
key === decoratedPart
|
|
791
|
+
? {
|
|
792
|
+
...fontContext,
|
|
793
|
+
decorations: [...fontContext.decorations, decoration],
|
|
794
|
+
}
|
|
795
|
+
: fontContext;
|
|
796
|
+
const recordContainers = !ignoredBody && childContext.font === undefined;
|
|
524
797
|
if (Array.isArray(child)) {
|
|
525
|
-
if (
|
|
526
|
-
for (const nested of child)
|
|
798
|
+
if (recordContainers && child.every(isNode)) addContainer(child);
|
|
799
|
+
for (const nested of child) {
|
|
800
|
+
visitValue(nested, ignoredBody, childContext);
|
|
801
|
+
}
|
|
527
802
|
} else if (isNode(child)) {
|
|
528
|
-
if (
|
|
529
|
-
visitValue(child, ignoredBody);
|
|
803
|
+
if (recordContainers) addContainer([child]);
|
|
804
|
+
visitValue(child, ignoredBody, childContext);
|
|
530
805
|
}
|
|
531
806
|
}
|
|
532
807
|
}
|
|
533
808
|
|
|
534
809
|
addContainer(tree);
|
|
535
810
|
visitValue(tree);
|
|
536
|
-
return { containers, identifiers, scriptSpans };
|
|
811
|
+
return { containers, identifiers, scriptSpans, decorations };
|
|
537
812
|
}
|
|
538
813
|
|
|
539
814
|
// --- base-glyph signatures ------------------------------------------
|
|
540
815
|
|
|
541
816
|
function identifierBaseSignature(node) {
|
|
542
817
|
if (isIdentifierNode(node)) return sequenceSignature([node]);
|
|
543
|
-
|
|
818
|
+
// `t_k` lends its card to a bare `t`, but `b'` or `\theta^{*}` does not lend
|
|
819
|
+
// its card to `b` or `\theta`: a mark makes a different quantity (N22).
|
|
820
|
+
if (
|
|
821
|
+
node?.type === 'supsub' &&
|
|
822
|
+
isIdentifierNode(node.base) &&
|
|
823
|
+
scriptMarkName(node.sup) === undefined
|
|
824
|
+
) {
|
|
544
825
|
return sequenceSignature([node.base]);
|
|
545
826
|
}
|
|
546
827
|
return undefined;
|
|
547
828
|
}
|
|
548
829
|
|
|
830
|
+
/**
|
|
831
|
+
* The decoration a notation entry is an **operator entry** for: exactly one
|
|
832
|
+
* decoration over an empty group (`\hat{}`, `\overline{}`, `{}'`, `{}^{*}`,
|
|
833
|
+
* `{}^{\dagger}`), else `undefined`. Such an entry explains the decoration
|
|
834
|
+
* wherever it composes (rule N22); it never binds a letter.
|
|
835
|
+
*/
|
|
836
|
+
function decorationOfTree(tree) {
|
|
837
|
+
if (!Array.isArray(tree) || tree.length !== 1) return undefined;
|
|
838
|
+
const [node] = tree;
|
|
839
|
+
const decoration = decorationOf(node);
|
|
840
|
+
if (decoration === undefined) return undefined;
|
|
841
|
+
if (node.type === 'supsub' && node.sub) return undefined;
|
|
842
|
+
const part = node[decoration.part];
|
|
843
|
+
return isNode(part) && part.type === 'ordgroup' && part.body.length === 0
|
|
844
|
+
? decoration.name
|
|
845
|
+
: undefined;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The operator-entry `latex` for a decoration name — the decoration over an
|
|
850
|
+
* empty group: `\hat{}`, `\overline{}`, `{}'`, `{}^{*}`, `{}^{\dagger}`. The
|
|
851
|
+
* inverse of {@link decorationOfEntry}, and how the decoration alone is shown.
|
|
852
|
+
*
|
|
853
|
+
* @param {string} name
|
|
854
|
+
* @returns {string}
|
|
855
|
+
*/
|
|
856
|
+
export function operatorEntryLatex(name) {
|
|
857
|
+
if (name === "'") return "{}'";
|
|
858
|
+
if (name === '*') return '{}^{*}';
|
|
859
|
+
if (name === '\\dagger') return '{}^{\\dagger}';
|
|
860
|
+
return `${name}{}`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* How a decoration is shown on its own — over a placeholder square, so a
|
|
865
|
+
* reader sees where it goes: `\hat{\square}`, `\square'`, `\square^{*}`.
|
|
866
|
+
* A lone `\dot{}` renders as a speck. Display only: the entry's `latex` stays
|
|
867
|
+
* the empty-group form the resolver keys on.
|
|
868
|
+
*
|
|
869
|
+
* @param {string} name
|
|
870
|
+
* @returns {string}
|
|
871
|
+
*/
|
|
872
|
+
export function operatorDisplayLatex(name) {
|
|
873
|
+
return operatorEntryLatex(name).replace(/^\{\}/, '\\square').replace(/\{\}$/, '{\\square}');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* The decoration a notation entry's `latex` is an operator entry for (see
|
|
878
|
+
* {@link decorationOfTree}), or `undefined`. `\hat{\beta}` is a whole form,
|
|
879
|
+
* not an operator entry.
|
|
880
|
+
*
|
|
881
|
+
* @param {string} latex
|
|
882
|
+
* @returns {string | undefined}
|
|
883
|
+
*/
|
|
884
|
+
export function decorationOfEntry(latex) {
|
|
885
|
+
if (typeof latex !== 'string' || latex.trim() === '') return undefined;
|
|
886
|
+
try {
|
|
887
|
+
return decorationOfTree(
|
|
888
|
+
parseMath(stripNotationMarkers(latex), 'operator entry'),
|
|
889
|
+
);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
if (error instanceof GlyphResolutionError && error.code === 'invalid-latex') {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
549
898
|
/**
|
|
550
899
|
* The shorter page form a parameterized symbol also permits: the bare base
|
|
551
900
|
* glyph of `t_k` / `r_m` / `j^{(m)}` (one significant node) or the head of a
|
|
@@ -623,7 +972,7 @@ function unwrapEnclosingGroupSpan(latex, span) {
|
|
|
623
972
|
}
|
|
624
973
|
|
|
625
974
|
const FONT_WRAPPER_OPEN =
|
|
626
|
-
/\\(?:mathbb|mathcal|mathfrak|mathscr|mathrm|mathbf|mathit)\{$/;
|
|
975
|
+
/\\(?:mathbb|mathcal|mathfrak|mathscr|mathrm|mathbf|mathit|mathnormal|mathsf|mathtt|boldsymbol|bm|bold|Bbb|frak)\{$/;
|
|
627
976
|
|
|
628
977
|
/**
|
|
629
978
|
* KaTeX puts no `loc` on a font node (`\mathbb{E}`, `\mathrm{SST}`, …), so a
|
|
@@ -666,7 +1015,7 @@ function isSafeScriptNesting(inner, outer, scriptSpans) {
|
|
|
666
1015
|
* registered. This is deliberately *not* `collectSourceStructure`'s
|
|
667
1016
|
* `identifiers` list: that list is built for scanning *page* math, where a
|
|
668
1017
|
* `\mathrm{…}` body is correctly treated as opaque (never a bindable atom on
|
|
669
|
-
* its own, [
|
|
1018
|
+
* its own, [N10](../../docs/authoring/notation.md#n10)) — but reusing it here to
|
|
670
1019
|
* gate the *definition's own* registration wrongly excluded every glyph whose
|
|
671
1020
|
* whole declared form is a roman label (`\mathrm{SSR}`, `\mathrm{SST}`, …):
|
|
672
1021
|
* such a definition produced zero `identifiers`, so it never entered
|
|
@@ -703,6 +1052,7 @@ function buildGlyphIndex(definitions) {
|
|
|
703
1052
|
const bySignature = new Map();
|
|
704
1053
|
const byBase = new Map();
|
|
705
1054
|
const ambiguousBase = new Set();
|
|
1055
|
+
const operators = new Map();
|
|
706
1056
|
let maxNodeLength = 0;
|
|
707
1057
|
|
|
708
1058
|
for (const [index, value] of definitions.entries()) {
|
|
@@ -739,6 +1089,22 @@ function buildGlyphIndex(definitions) {
|
|
|
739
1089
|
const entry = { key, notation };
|
|
740
1090
|
byKey.set(key, entry);
|
|
741
1091
|
|
|
1092
|
+
// An operator entry (`\hat{}`) binds no letter; it explains its
|
|
1093
|
+
// decoration wherever that composes (rule N22).
|
|
1094
|
+
const operator = decorationOfTree(tree);
|
|
1095
|
+
if (operator !== undefined) {
|
|
1096
|
+
const clash = operators.get(operator);
|
|
1097
|
+
if (clash && clash !== key) {
|
|
1098
|
+
fail(
|
|
1099
|
+
'ambiguous-canonical-notation',
|
|
1100
|
+
`Page glyphs "${clash}" and "${key}" are both operator entries for ${operator}.`,
|
|
1101
|
+
{ left: clash, right: key },
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
operators.set(operator, key);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
742
1108
|
const signature = sequenceSignature(tree);
|
|
743
1109
|
if (hasBindableGlyph(tree) && tree.length > 0) {
|
|
744
1110
|
const clash = bySignature.get(signature);
|
|
@@ -763,7 +1129,7 @@ function buildGlyphIndex(definitions) {
|
|
|
763
1129
|
}
|
|
764
1130
|
}
|
|
765
1131
|
|
|
766
|
-
return { byKey, bySignature, byBase, ambiguousBase, maxNodeLength };
|
|
1132
|
+
return { byKey, bySignature, byBase, ambiguousBase, operators, maxNodeLength };
|
|
767
1133
|
}
|
|
768
1134
|
|
|
769
1135
|
/** Bind whole canonical forms: greedy longest match, left to right, per run. */
|
|
@@ -783,7 +1149,9 @@ function matchCanonicalForms(latex, strippedSource, containers, glyphs) {
|
|
|
783
1149
|
const nodes = container.slice(cursor, cursor + length);
|
|
784
1150
|
const key = glyphs.bySignature.get(sequenceSignature(nodes));
|
|
785
1151
|
if (key === undefined) continue;
|
|
786
|
-
|
|
1152
|
+
// `sourceSpan`, not `sequenceSpan`: a whole-form match on `\hat{\beta}`
|
|
1153
|
+
// or `b'` wraps the decoration too, not just the letter under it.
|
|
1154
|
+
const rawSpan = sourceSpan(nodes, strippedSource);
|
|
787
1155
|
if (!rawSpan) continue;
|
|
788
1156
|
const span = expandFontWrapperSpan(
|
|
789
1157
|
latex,
|
|
@@ -837,6 +1205,9 @@ function matchBaseGlyphs(latex, identifiers, canonical, glyphs, scriptSpans) {
|
|
|
837
1205
|
|
|
838
1206
|
const cover = canonical.find((binding) => contains(binding, identifier));
|
|
839
1207
|
if (cover && !isSafeScriptNesting(identifier, cover, scriptSpans)) continue;
|
|
1208
|
+
// `\mathbf{x}` is a different symbol from `x` (rule N4): it binds only
|
|
1209
|
+
// through its own entry or an `\explain`, never the plain letter's card.
|
|
1210
|
+
if (identifier.font !== undefined) continue;
|
|
840
1211
|
|
|
841
1212
|
const baseSignature = identifier.signature;
|
|
842
1213
|
if (glyphs.ambiguousBase.has(baseSignature)) {
|
|
@@ -899,26 +1270,61 @@ function injectScopeBindings(latex, bindings) {
|
|
|
899
1270
|
return output;
|
|
900
1271
|
}
|
|
901
1272
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1273
|
+
/**
|
|
1274
|
+
* Every identifier no binding covers, plus every decorated form whose
|
|
1275
|
+
* decoration is suspended on this page and that no whole form covers (rule
|
|
1276
|
+
* N22). A letter under such a decoration is reported once, as the decorated
|
|
1277
|
+
* form (`b''''`, not `b`): that whole form is what needs the entry.
|
|
1278
|
+
*/
|
|
1279
|
+
function unresolvedIdentifiers(latex, identifiers, bindings, suspended) {
|
|
1280
|
+
const letters = identifiers
|
|
1281
|
+
.filter(
|
|
1282
|
+
(identifier) =>
|
|
1283
|
+
!bindings.some((binding) => contains(binding, identifier)) &&
|
|
1284
|
+
!suspended.some((decoration) => contains(decoration.part, identifier)),
|
|
1285
|
+
)
|
|
1286
|
+
.map(({ start, end, font }) => {
|
|
912
1287
|
// Report `\mathbb{E}`, not a bare `E`, so the fix hint points at the
|
|
913
1288
|
// whole token the author has to wrap in `\explain`.
|
|
914
1289
|
const wrapped = expandFontWrapperSpan(latex, { start, end });
|
|
915
1290
|
return {
|
|
916
1291
|
token: latex.slice(wrapped.start, wrapped.end),
|
|
1292
|
+
// A styled base-library letter (`\mathbf{e}`) is not Euler's number.
|
|
1293
|
+
libraryToken: latex.slice(
|
|
1294
|
+
font === undefined ? start : wrapped.start,
|
|
1295
|
+
font === undefined ? end : wrapped.end,
|
|
1296
|
+
),
|
|
917
1297
|
start: wrapped.start,
|
|
918
1298
|
end: wrapped.end,
|
|
1299
|
+
...(font === undefined ? {} : { font }),
|
|
919
1300
|
};
|
|
920
1301
|
})
|
|
921
|
-
.
|
|
1302
|
+
.filter(({ libraryToken }) => !BASE_LIBRARY.has(libraryToken))
|
|
1303
|
+
.map(({ libraryToken: _libraryToken, ...item }) => item);
|
|
1304
|
+
const decorated = suspended.map(({ name, start, end }) => ({
|
|
1305
|
+
token: latex.slice(start, end),
|
|
1306
|
+
start,
|
|
1307
|
+
end,
|
|
1308
|
+
suspended: name,
|
|
1309
|
+
}));
|
|
1310
|
+
return [...letters, ...decorated].sort(
|
|
1311
|
+
(left, right) => left.start - right.start || left.end - right.end,
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* A decoration over an `\explain{key}{…}` call starts at that call's content
|
|
1317
|
+
* group in the blanked source; widen it to the call itself, so a marker
|
|
1318
|
+
* injected around the decoration never splits `\explain{key}` from its `{…}`.
|
|
1319
|
+
*/
|
|
1320
|
+
function widenOverExplainPrefix(span, explicitCalls) {
|
|
1321
|
+
let { start } = span;
|
|
1322
|
+
for (;;) {
|
|
1323
|
+
const call = explicitCalls.find((candidate) => candidate.prefixEnd === start);
|
|
1324
|
+
if (!call) break;
|
|
1325
|
+
start = call.callStart;
|
|
1326
|
+
}
|
|
1327
|
+
return { start, end: span.end };
|
|
922
1328
|
}
|
|
923
1329
|
|
|
924
1330
|
// --- N13: nonlinear functions take a parenthesized argument --------------
|
|
@@ -1162,14 +1568,24 @@ function findNestedBareDelimiters(tree) {
|
|
|
1162
1568
|
* Resolve and annotate one KaTeX expression against a page's flat glyph table.
|
|
1163
1569
|
* Source offsets in the result always refer to the original, unmodified LaTeX.
|
|
1164
1570
|
*
|
|
1571
|
+
* Decorations (rule N22) are reported in `decorations`: `wholeForm` is the key
|
|
1572
|
+
* of the binding that covers the decorated form (`\hat{\beta}` has its own
|
|
1573
|
+
* entry), else `null`; `operator` is the key of the operator entry
|
|
1574
|
+
* (`latex: \hat{}`) whose `\explain` now wraps the decorated form, else
|
|
1575
|
+
* `null`. A decoration named in `options.suspended` never composes: a
|
|
1576
|
+
* decorated form no whole form covers is reported in `unresolved`, marked
|
|
1577
|
+
* `suspended`.
|
|
1578
|
+
*
|
|
1165
1579
|
* @param {string} latex
|
|
1166
1580
|
* @param {Array<{key: string, notation: string}>} definitions
|
|
1581
|
+
* @param {{ suspended?: readonly string[] }} [options]
|
|
1167
1582
|
*/
|
|
1168
|
-
export function resolveMathGlyphs(latex, definitions) {
|
|
1583
|
+
export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
1169
1584
|
if (typeof latex !== 'string') {
|
|
1170
1585
|
fail('invalid-latex-input', 'Math source must be a string.');
|
|
1171
1586
|
}
|
|
1172
1587
|
|
|
1588
|
+
const suspended = new Set(options.suspended ?? []);
|
|
1173
1589
|
const glyphs = buildGlyphIndex(definitions);
|
|
1174
1590
|
const explicitCalls = findExplainCalls(latex, glyphs.byKey);
|
|
1175
1591
|
// The source KaTeX actually parses — `\explain{key}{` prefixes blanked to
|
|
@@ -1206,10 +1622,8 @@ export function resolveMathGlyphs(latex, definitions) {
|
|
|
1206
1622
|
{ violations: nestingViolations },
|
|
1207
1623
|
);
|
|
1208
1624
|
}
|
|
1209
|
-
const { containers, identifiers, scriptSpans } =
|
|
1210
|
-
tree,
|
|
1211
|
-
strippedSource,
|
|
1212
|
-
);
|
|
1625
|
+
const { containers, identifiers, scriptSpans, decorations } =
|
|
1626
|
+
collectSourceStructure(tree, strippedSource);
|
|
1213
1627
|
|
|
1214
1628
|
// A canonical container match can be found by seeing through transparent
|
|
1215
1629
|
// ordgroup/font wrapping into a structural sibling of an explicit
|
|
@@ -1237,7 +1651,48 @@ export function resolveMathGlyphs(latex, definitions) {
|
|
|
1237
1651
|
scriptSpans,
|
|
1238
1652
|
);
|
|
1239
1653
|
|
|
1240
|
-
|
|
1654
|
+
// Rule N22: a decoration a whole form covers is part of that symbol. One
|
|
1655
|
+
// that composes is wrapped in its operator entry's `\explain`, around the
|
|
1656
|
+
// letter's own — unless it is suspended on this page, when the decorated
|
|
1657
|
+
// form is unresolved.
|
|
1658
|
+
const letterBindings = [...canonical, ...base, ...explicitCalls];
|
|
1659
|
+
const operatorBindings = [];
|
|
1660
|
+
const suspendedDecorations = [];
|
|
1661
|
+
const decorationResults = decorations.map((decoration) => {
|
|
1662
|
+
const span = widenOverExplainPrefix(decoration, explicitCalls);
|
|
1663
|
+
const wholeForm =
|
|
1664
|
+
covered.find((binding) => contains(binding, span))?.key ?? null;
|
|
1665
|
+
let operator = null;
|
|
1666
|
+
if (wholeForm === null && suspended.has(decoration.name)) {
|
|
1667
|
+
suspendedDecorations.push({ ...decoration, ...span });
|
|
1668
|
+
} else if (wholeForm === null && glyphs.operators.has(decoration.name)) {
|
|
1669
|
+
const straddles = [...letterBindings, ...operatorBindings].some(
|
|
1670
|
+
(binding) =>
|
|
1671
|
+
overlaps(binding, span) &&
|
|
1672
|
+
!contains(span, binding) &&
|
|
1673
|
+
!contains(binding, span),
|
|
1674
|
+
);
|
|
1675
|
+
if (!straddles) {
|
|
1676
|
+
operator = glyphs.operators.get(decoration.name);
|
|
1677
|
+
operatorBindings.push({
|
|
1678
|
+
key: operator,
|
|
1679
|
+
...span,
|
|
1680
|
+
token: latex.slice(span.start, span.end),
|
|
1681
|
+
level: 'scope',
|
|
1682
|
+
match: 'operator',
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
name: decoration.name,
|
|
1688
|
+
...span,
|
|
1689
|
+
token: latex.slice(span.start, span.end),
|
|
1690
|
+
wholeForm,
|
|
1691
|
+
operator,
|
|
1692
|
+
};
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
const bindings = [...letterBindings, ...operatorBindings]
|
|
1241
1696
|
.sort(
|
|
1242
1697
|
(left, right) =>
|
|
1243
1698
|
left.start - right.start ||
|
|
@@ -1279,7 +1734,13 @@ export function resolveMathGlyphs(latex, definitions) {
|
|
|
1279
1734
|
match,
|
|
1280
1735
|
token,
|
|
1281
1736
|
})),
|
|
1282
|
-
unresolved: unresolvedIdentifiers(
|
|
1737
|
+
unresolved: unresolvedIdentifiers(
|
|
1738
|
+
latex,
|
|
1739
|
+
identifiers,
|
|
1740
|
+
bindings,
|
|
1741
|
+
suspendedDecorations,
|
|
1742
|
+
),
|
|
1743
|
+
decorations: decorationResults,
|
|
1283
1744
|
};
|
|
1284
1745
|
}
|
|
1285
1746
|
|
|
@@ -1383,18 +1844,92 @@ export function collectOperators(latex) {
|
|
|
1383
1844
|
return operators;
|
|
1384
1845
|
}
|
|
1385
1846
|
|
|
1847
|
+
// --- decorations -----------------------------------------------------
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* Blank every `\explain{key}{` / `\slot{key}{` prefix to spaces, keeping the
|
|
1851
|
+
* source's length, so KaTeX parses what the markers wrap and every offset
|
|
1852
|
+
* still points into the original. Unlike `findExplainCalls`, no key is
|
|
1853
|
+
* checked: a scan has no glyph table.
|
|
1854
|
+
*/
|
|
1855
|
+
function blankMarkerPrefixes(latex) {
|
|
1856
|
+
let source = latex;
|
|
1857
|
+
const calls = [];
|
|
1858
|
+
for (let cursor = 0; cursor < latex.length; cursor++) {
|
|
1859
|
+
const command = MARKER_COMMANDS.find(
|
|
1860
|
+
(name) =>
|
|
1861
|
+
latex.startsWith(name, cursor) &&
|
|
1862
|
+
!isEscaped(latex, cursor) &&
|
|
1863
|
+
!/[A-Za-z@]/.test(latex[cursor + name.length] ?? ''),
|
|
1864
|
+
);
|
|
1865
|
+
if (!command) continue;
|
|
1866
|
+
const keyGroup = readBracedGroup(
|
|
1867
|
+
latex,
|
|
1868
|
+
skipWhitespace(latex, cursor + command.length),
|
|
1869
|
+
);
|
|
1870
|
+
if (!keyGroup) continue;
|
|
1871
|
+
const prefixEnd = skipWhitespace(latex, keyGroup.end);
|
|
1872
|
+
if (latex[prefixEnd] !== '{') continue;
|
|
1873
|
+
source =
|
|
1874
|
+
source.slice(0, cursor) +
|
|
1875
|
+
' '.repeat(prefixEnd - cursor) +
|
|
1876
|
+
source.slice(prefixEnd);
|
|
1877
|
+
calls.push({ callStart: cursor, prefixEnd });
|
|
1878
|
+
cursor = prefixEnd;
|
|
1879
|
+
}
|
|
1880
|
+
return { source, calls };
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* Every decoration in one KaTeX expression (rule N22): an accent (`\hat`,
|
|
1885
|
+
* `\widehat`, …), `\overline` / `\underline`, or a superscript mark (`'` for
|
|
1886
|
+
* any run of primes, `*` for `*` / `\ast`, `\dagger`). `name` is the
|
|
1887
|
+
* canonical name in {@link DECORATION_NAMES}; `start`/`end` span the whole
|
|
1888
|
+
* decorated form in `latex`; `base` is the source of what it decorates when
|
|
1889
|
+
* that is one symbol (`\beta`, `b`, `\mathbf{x}`), else `undefined`.
|
|
1890
|
+
*
|
|
1891
|
+
* Pure read, the twin of {@link collectOperators}: `\text{}`, `\mathrm{}` and
|
|
1892
|
+
* operator bodies are skipped, and a span that will not parse returns `[]`
|
|
1893
|
+
* (the math gate owns that error).
|
|
1894
|
+
*
|
|
1895
|
+
* @param {string} latex
|
|
1896
|
+
* @returns {Array<{ name: string, start: number, end: number, base: string | undefined }>}
|
|
1897
|
+
*/
|
|
1898
|
+
export function collectDecorations(latex) {
|
|
1899
|
+
if (typeof latex !== 'string' || latex.trim() === '') return [];
|
|
1900
|
+
|
|
1901
|
+
const { source, calls } = blankMarkerPrefixes(latex);
|
|
1902
|
+
let tree;
|
|
1903
|
+
try {
|
|
1904
|
+
tree = parseMath(source, 'decoration scan');
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
if (error instanceof GlyphResolutionError && error.code === 'invalid-latex') {
|
|
1907
|
+
return [];
|
|
1908
|
+
}
|
|
1909
|
+
throw error;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
return collectSourceStructure(tree, source).decorations.map(
|
|
1913
|
+
({ name, start, end, base }) => ({
|
|
1914
|
+
name,
|
|
1915
|
+
...widenOverExplainPrefix({ start, end }, calls),
|
|
1916
|
+
base,
|
|
1917
|
+
}),
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1386
1921
|
// --- named roman labels ----------------------------------------------
|
|
1387
1922
|
|
|
1388
1923
|
/**
|
|
1389
1924
|
* Every multi-letter `\mathrm{…}` label in one KaTeX expression (`KE`, `PV`,
|
|
1390
1925
|
* `opt`) — the upright-label counterpart to {@link collectOperators}. A
|
|
1391
|
-
* single-letter body (`\mathrm{d}`, the
|
|
1926
|
+
* single-letter body (`\mathrm{d}`, the rule-N2 differential) is not collected:
|
|
1392
1927
|
* that one is a fixed typographic operator, not a named quantity a reader
|
|
1393
1928
|
* would need explained.
|
|
1394
1929
|
*
|
|
1395
1930
|
* Pure read: nothing here rewrites the source. Used by the
|
|
1396
1931
|
* `roman-label-explained` consistency check (rule N10); the glyph resolver
|
|
1397
|
-
* still skips `\mathrm{…}` bodies whole ([
|
|
1932
|
+
* still skips `\mathrm{…}` bodies whole ([N10](../../docs/authoring/notation.md#n10)).
|
|
1398
1933
|
*
|
|
1399
1934
|
* Same `stripNotationMarkers` preprocessing as {@link collectOperators}, and
|
|
1400
1935
|
* for the same reason: an unstripped `\explain{…}{…}` elsewhere in the span
|