explico 0.7.0 → 0.11.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 +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- 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 +14 -1
- package/dist/compiler/logging.js +6 -2
- 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 +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- 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 +69 -34
- package/dist/reference/equations.mjs +90 -44
- 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 +3 -2
- 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 +47 -23
- 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 +44 -6
- package/dist/reference/gate-math.js +63 -10
- 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 +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +5 -5
- 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 +24 -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 +2 -2
- 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 +1324 -2
- package/docs/authoring/notation.md +616 -344
- 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/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- 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 +21 -4
- 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 +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- 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 +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- 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 +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +17 -32
- 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 +24 -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 +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import katex from 'katex';
|
|
1
|
+
import katex from './katex-delimiters.mjs';
|
|
2
2
|
|
|
3
3
|
import { BASE_LIBRARY } from './base-library.mjs';
|
|
4
4
|
import { PARENTHESIZED_FUNCTIONS } from './base-operators.mjs';
|
|
@@ -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);
|
|
@@ -386,6 +452,19 @@ function trimTrailingWhitespace(latex, start, end) {
|
|
|
386
452
|
return trimmed;
|
|
387
453
|
}
|
|
388
454
|
|
|
455
|
+
/**
|
|
456
|
+
* KaTeX gives a node it produced by expanding a macro a `loc` from the lexer
|
|
457
|
+
* over the *macro body*, not over the source being scanned: `\vdots` expands
|
|
458
|
+
* to `{\varvdots\rule{0pt}{15pt}}`, and its `\varvdots` atom arrives with
|
|
459
|
+
* `loc.start = 1`, `loc.end = 10` — offsets into that 27-character body.
|
|
460
|
+
* Those numbers are valid indices into any longer source, so they read as a
|
|
461
|
+
* real span pointing at unrelated text. Every `loc` therefore has to prove it
|
|
462
|
+
* belongs to the string this pass parsed before its offsets are believed.
|
|
463
|
+
*/
|
|
464
|
+
function isAuthoredLocation(loc, latex) {
|
|
465
|
+
return loc?.lexer?.input === latex;
|
|
466
|
+
}
|
|
467
|
+
|
|
389
468
|
function nodeSpan(node, latex) {
|
|
390
469
|
let start = Number.POSITIVE_INFINITY;
|
|
391
470
|
let end = Number.NEGATIVE_INFINITY;
|
|
@@ -397,7 +476,8 @@ function nodeSpan(node, latex) {
|
|
|
397
476
|
if (
|
|
398
477
|
value.loc &&
|
|
399
478
|
Number.isInteger(value.loc.start) &&
|
|
400
|
-
Number.isInteger(value.loc.end)
|
|
479
|
+
Number.isInteger(value.loc.end) &&
|
|
480
|
+
isAuthoredLocation(value.loc, latex)
|
|
401
481
|
) {
|
|
402
482
|
start = Math.min(start, value.loc.start);
|
|
403
483
|
end = Math.max(end, value.loc.end);
|
|
@@ -437,6 +517,9 @@ function isIdentifierNode(node) {
|
|
|
437
517
|
if (node.type !== 'mathord' && node.type !== 'textord') return false;
|
|
438
518
|
if (typeof node.text !== 'string') return false;
|
|
439
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;
|
|
440
523
|
return /^(?:\p{L}|\\[A-Za-z]+)$/u.test(node.text);
|
|
441
524
|
}
|
|
442
525
|
|
|
@@ -447,16 +530,198 @@ const sameSpan = (left, right) =>
|
|
|
447
530
|
const overlaps = (left, right) =>
|
|
448
531
|
left.start < right.end && right.start < left.end;
|
|
449
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
|
+
|
|
450
709
|
/**
|
|
451
710
|
* Walk the parse tree once, collecting: every maximal run of sibling nodes
|
|
452
711
|
* (`containers`, where a canonical form is matched), every identifier atom with
|
|
453
|
-
* its source span,
|
|
454
|
-
*
|
|
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.
|
|
455
719
|
*/
|
|
456
720
|
function collectSourceStructure(tree, latex) {
|
|
457
721
|
const containers = [];
|
|
458
722
|
const identifiers = [];
|
|
459
723
|
const scriptSpans = [];
|
|
724
|
+
const decorations = [];
|
|
460
725
|
const seenContainers = new Set();
|
|
461
726
|
const seenIdentifiers = new Set();
|
|
462
727
|
|
|
@@ -471,10 +736,12 @@ function collectSourceStructure(tree, latex) {
|
|
|
471
736
|
containers.push(usable);
|
|
472
737
|
}
|
|
473
738
|
|
|
474
|
-
function visitValue(value, ignored = false) {
|
|
739
|
+
function visitValue(value, ignored = false, context = ROOT_CONTEXT) {
|
|
475
740
|
if (Array.isArray(value)) {
|
|
476
|
-
if (!ignored && value.every(isNode))
|
|
477
|
-
|
|
741
|
+
if (!ignored && context.font === undefined && value.every(isNode)) {
|
|
742
|
+
addContainer(value);
|
|
743
|
+
}
|
|
744
|
+
for (const child of value) visitValue(child, ignored, context);
|
|
478
745
|
return;
|
|
479
746
|
}
|
|
480
747
|
if (!isNode(value)) return;
|
|
@@ -495,6 +762,8 @@ function collectSourceStructure(tree, latex) {
|
|
|
495
762
|
node: value,
|
|
496
763
|
signature: sequenceSignature([value]),
|
|
497
764
|
...span,
|
|
765
|
+
decorations: context.decorations,
|
|
766
|
+
font: context.font,
|
|
498
767
|
});
|
|
499
768
|
}
|
|
500
769
|
}
|
|
@@ -506,32 +775,126 @@ function collectSourceStructure(tree, latex) {
|
|
|
506
775
|
}
|
|
507
776
|
}
|
|
508
777
|
|
|
509
|
-
|
|
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;
|
|
510
797
|
if (Array.isArray(child)) {
|
|
511
|
-
if (
|
|
512
|
-
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
|
+
}
|
|
513
802
|
} else if (isNode(child)) {
|
|
514
|
-
if (
|
|
515
|
-
visitValue(child, ignoredBody);
|
|
803
|
+
if (recordContainers) addContainer([child]);
|
|
804
|
+
visitValue(child, ignoredBody, childContext);
|
|
516
805
|
}
|
|
517
806
|
}
|
|
518
807
|
}
|
|
519
808
|
|
|
520
809
|
addContainer(tree);
|
|
521
810
|
visitValue(tree);
|
|
522
|
-
return { containers, identifiers, scriptSpans };
|
|
811
|
+
return { containers, identifiers, scriptSpans, decorations };
|
|
523
812
|
}
|
|
524
813
|
|
|
525
814
|
// --- base-glyph signatures ------------------------------------------
|
|
526
815
|
|
|
527
816
|
function identifierBaseSignature(node) {
|
|
528
817
|
if (isIdentifierNode(node)) return sequenceSignature([node]);
|
|
529
|
-
|
|
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
|
+
) {
|
|
530
825
|
return sequenceSignature([node.base]);
|
|
531
826
|
}
|
|
532
827
|
return undefined;
|
|
533
828
|
}
|
|
534
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
|
+
|
|
535
898
|
/**
|
|
536
899
|
* The shorter page form a parameterized symbol also permits: the bare base
|
|
537
900
|
* glyph of `t_k` / `r_m` / `j^{(m)}` (one significant node) or the head of a
|
|
@@ -609,7 +972,7 @@ function unwrapEnclosingGroupSpan(latex, span) {
|
|
|
609
972
|
}
|
|
610
973
|
|
|
611
974
|
const FONT_WRAPPER_OPEN =
|
|
612
|
-
/\\(?:mathbb|mathcal|mathfrak|mathscr|mathrm|mathbf|mathit)\{$/;
|
|
975
|
+
/\\(?:mathbb|mathcal|mathfrak|mathscr|mathrm|mathbf|mathit|mathnormal|mathsf|mathtt|boldsymbol|bm|bold|Bbb|frak)\{$/;
|
|
613
976
|
|
|
614
977
|
/**
|
|
615
978
|
* KaTeX puts no `loc` on a font node (`\mathbb{E}`, `\mathrm{SST}`, …), so a
|
|
@@ -652,7 +1015,7 @@ function isSafeScriptNesting(inner, outer, scriptSpans) {
|
|
|
652
1015
|
* registered. This is deliberately *not* `collectSourceStructure`'s
|
|
653
1016
|
* `identifiers` list: that list is built for scanning *page* math, where a
|
|
654
1017
|
* `\mathrm{…}` body is correctly treated as opaque (never a bindable atom on
|
|
655
|
-
* its own, [
|
|
1018
|
+
* its own, [N10](../../docs/authoring/notation.md#n10)) — but reusing it here to
|
|
656
1019
|
* gate the *definition's own* registration wrongly excluded every glyph whose
|
|
657
1020
|
* whole declared form is a roman label (`\mathrm{SSR}`, `\mathrm{SST}`, …):
|
|
658
1021
|
* such a definition produced zero `identifiers`, so it never entered
|
|
@@ -689,6 +1052,7 @@ function buildGlyphIndex(definitions) {
|
|
|
689
1052
|
const bySignature = new Map();
|
|
690
1053
|
const byBase = new Map();
|
|
691
1054
|
const ambiguousBase = new Set();
|
|
1055
|
+
const operators = new Map();
|
|
692
1056
|
let maxNodeLength = 0;
|
|
693
1057
|
|
|
694
1058
|
for (const [index, value] of definitions.entries()) {
|
|
@@ -725,6 +1089,22 @@ function buildGlyphIndex(definitions) {
|
|
|
725
1089
|
const entry = { key, notation };
|
|
726
1090
|
byKey.set(key, entry);
|
|
727
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
|
+
|
|
728
1108
|
const signature = sequenceSignature(tree);
|
|
729
1109
|
if (hasBindableGlyph(tree) && tree.length > 0) {
|
|
730
1110
|
const clash = bySignature.get(signature);
|
|
@@ -749,7 +1129,7 @@ function buildGlyphIndex(definitions) {
|
|
|
749
1129
|
}
|
|
750
1130
|
}
|
|
751
1131
|
|
|
752
|
-
return { byKey, bySignature, byBase, ambiguousBase, maxNodeLength };
|
|
1132
|
+
return { byKey, bySignature, byBase, ambiguousBase, operators, maxNodeLength };
|
|
753
1133
|
}
|
|
754
1134
|
|
|
755
1135
|
/** Bind whole canonical forms: greedy longest match, left to right, per run. */
|
|
@@ -769,7 +1149,9 @@ function matchCanonicalForms(latex, strippedSource, containers, glyphs) {
|
|
|
769
1149
|
const nodes = container.slice(cursor, cursor + length);
|
|
770
1150
|
const key = glyphs.bySignature.get(sequenceSignature(nodes));
|
|
771
1151
|
if (key === undefined) continue;
|
|
772
|
-
|
|
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);
|
|
773
1155
|
if (!rawSpan) continue;
|
|
774
1156
|
const span = expandFontWrapperSpan(
|
|
775
1157
|
latex,
|
|
@@ -823,6 +1205,9 @@ function matchBaseGlyphs(latex, identifiers, canonical, glyphs, scriptSpans) {
|
|
|
823
1205
|
|
|
824
1206
|
const cover = canonical.find((binding) => contains(binding, identifier));
|
|
825
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;
|
|
826
1211
|
|
|
827
1212
|
const baseSignature = identifier.signature;
|
|
828
1213
|
if (glyphs.ambiguousBase.has(baseSignature)) {
|
|
@@ -885,26 +1270,61 @@ function injectScopeBindings(latex, bindings) {
|
|
|
885
1270
|
return output;
|
|
886
1271
|
}
|
|
887
1272
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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 }) => {
|
|
898
1287
|
// Report `\mathbb{E}`, not a bare `E`, so the fix hint points at the
|
|
899
1288
|
// whole token the author has to wrap in `\explain`.
|
|
900
1289
|
const wrapped = expandFontWrapperSpan(latex, { start, end });
|
|
901
1290
|
return {
|
|
902
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
|
+
),
|
|
903
1297
|
start: wrapped.start,
|
|
904
1298
|
end: wrapped.end,
|
|
1299
|
+
...(font === undefined ? {} : { font }),
|
|
905
1300
|
};
|
|
906
1301
|
})
|
|
907
|
-
.
|
|
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 };
|
|
908
1328
|
}
|
|
909
1329
|
|
|
910
1330
|
// --- N13: nonlinear functions take a parenthesized argument --------------
|
|
@@ -1148,14 +1568,24 @@ function findNestedBareDelimiters(tree) {
|
|
|
1148
1568
|
* Resolve and annotate one KaTeX expression against a page's flat glyph table.
|
|
1149
1569
|
* Source offsets in the result always refer to the original, unmodified LaTeX.
|
|
1150
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
|
+
*
|
|
1151
1579
|
* @param {string} latex
|
|
1152
1580
|
* @param {Array<{key: string, notation: string}>} definitions
|
|
1581
|
+
* @param {{ suspended?: readonly string[] }} [options]
|
|
1153
1582
|
*/
|
|
1154
|
-
export function resolveMathGlyphs(latex, definitions) {
|
|
1583
|
+
export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
1155
1584
|
if (typeof latex !== 'string') {
|
|
1156
1585
|
fail('invalid-latex-input', 'Math source must be a string.');
|
|
1157
1586
|
}
|
|
1158
1587
|
|
|
1588
|
+
const suspended = new Set(options.suspended ?? []);
|
|
1159
1589
|
const glyphs = buildGlyphIndex(definitions);
|
|
1160
1590
|
const explicitCalls = findExplainCalls(latex, glyphs.byKey);
|
|
1161
1591
|
// The source KaTeX actually parses — `\explain{key}{` prefixes blanked to
|
|
@@ -1192,10 +1622,8 @@ export function resolveMathGlyphs(latex, definitions) {
|
|
|
1192
1622
|
{ violations: nestingViolations },
|
|
1193
1623
|
);
|
|
1194
1624
|
}
|
|
1195
|
-
const { containers, identifiers, scriptSpans } =
|
|
1196
|
-
tree,
|
|
1197
|
-
strippedSource,
|
|
1198
|
-
);
|
|
1625
|
+
const { containers, identifiers, scriptSpans, decorations } =
|
|
1626
|
+
collectSourceStructure(tree, strippedSource);
|
|
1199
1627
|
|
|
1200
1628
|
// A canonical container match can be found by seeing through transparent
|
|
1201
1629
|
// ordgroup/font wrapping into a structural sibling of an explicit
|
|
@@ -1223,7 +1651,48 @@ export function resolveMathGlyphs(latex, definitions) {
|
|
|
1223
1651
|
scriptSpans,
|
|
1224
1652
|
);
|
|
1225
1653
|
|
|
1226
|
-
|
|
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]
|
|
1227
1696
|
.sort(
|
|
1228
1697
|
(left, right) =>
|
|
1229
1698
|
left.start - right.start ||
|
|
@@ -1265,7 +1734,13 @@ export function resolveMathGlyphs(latex, definitions) {
|
|
|
1265
1734
|
match,
|
|
1266
1735
|
token,
|
|
1267
1736
|
})),
|
|
1268
|
-
unresolved: unresolvedIdentifiers(
|
|
1737
|
+
unresolved: unresolvedIdentifiers(
|
|
1738
|
+
latex,
|
|
1739
|
+
identifiers,
|
|
1740
|
+
bindings,
|
|
1741
|
+
suspendedDecorations,
|
|
1742
|
+
),
|
|
1743
|
+
decorations: decorationResults,
|
|
1269
1744
|
};
|
|
1270
1745
|
}
|
|
1271
1746
|
|
|
@@ -1369,18 +1844,92 @@ export function collectOperators(latex) {
|
|
|
1369
1844
|
return operators;
|
|
1370
1845
|
}
|
|
1371
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
|
+
|
|
1372
1921
|
// --- named roman labels ----------------------------------------------
|
|
1373
1922
|
|
|
1374
1923
|
/**
|
|
1375
1924
|
* Every multi-letter `\mathrm{…}` label in one KaTeX expression (`KE`, `PV`,
|
|
1376
1925
|
* `opt`) — the upright-label counterpart to {@link collectOperators}. A
|
|
1377
|
-
* single-letter body (`\mathrm{d}`, the
|
|
1926
|
+
* single-letter body (`\mathrm{d}`, the rule-N2 differential) is not collected:
|
|
1378
1927
|
* that one is a fixed typographic operator, not a named quantity a reader
|
|
1379
1928
|
* would need explained.
|
|
1380
1929
|
*
|
|
1381
1930
|
* Pure read: nothing here rewrites the source. Used by the
|
|
1382
1931
|
* `roman-label-explained` consistency check (rule N10); the glyph resolver
|
|
1383
|
-
* still skips `\mathrm{…}` bodies whole ([
|
|
1932
|
+
* still skips `\mathrm{…}` bodies whole ([N10](../../docs/authoring/notation.md#n10)).
|
|
1384
1933
|
*
|
|
1385
1934
|
* Same `stripNotationMarkers` preprocessing as {@link collectOperators}, and
|
|
1386
1935
|
* for the same reason: an unstripped `\explain{…}{…}` elsewhere in the span
|