explico 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1292 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +11 -4
- package/dist/compiler/astro.js +48 -15
- package/dist/compiler/cli.d.ts +27 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +33 -2
- package/dist/compiler/collections.js +91 -19
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +15 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +115 -8
- package/dist/compiler/manifest.js +221 -39
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +3 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +8 -2
- package/dist/reference/base-operators.mjs +8 -10
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figure-symbols.d.ts +32 -0
- package/dist/reference/figure-symbols.js +55 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
- package/dist/reference/gate-non-chapter-pages.js +156 -0
- package/dist/reference/gate-part-pages.d.ts +4 -2
- package/dist/reference/gate-part-pages.js +2 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-katex-validate.d.ts +43 -0
- package/dist/reference/lab-katex-validate.js +96 -0
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/lint-ignore.js +1 -1
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +39 -7
- package/dist/reference/page-notation.js +62 -10
- package/dist/reference/raw-floats.mjs +37 -13
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +115 -11
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +275 -23
- package/dist/reference/render-lab-math.d.ts +13 -2
- package/dist/reference/render-lab-math.js +18 -6
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +223 -32
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +100 -27
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +33 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +115 -10
- package/docs/authoring/migrating.md +878 -0
- package/docs/authoring/notation.md +285 -30
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +84 -45
- package/docs/authoring/structure.md +67 -2
- package/docs/authoring/visualizations.md +167 -25
- package/package.json +12 -7
- package/src/compiler/astro.ts +72 -21
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +158 -27
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +387 -40
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/CourseContents.astro +6 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +18 -9
- package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/disclosure/print-as-shown.ts +42 -6
- package/src/components/figure/Figure.astro +17 -1
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/figure/figure-state.ts +26 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +300 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +66 -8
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +14 -22
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +8 -10
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figure-symbols.ts +117 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-non-chapter-pages.ts +241 -0
- package/src/reference/gate-part-pages.ts +1 -1
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-katex-validate.ts +156 -0
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/lint-ignore.ts +4 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +114 -14
- package/src/reference/raw-floats.mjs +37 -13
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +170 -9
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +275 -23
- package/src/reference/render-lab-math.ts +40 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +241 -31
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +322 -83
|
@@ -185,14 +185,71 @@ function assertPinnedKatex() {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/** The inner environment that takes a top-level LaTeX one's rows inside `$$…$$`. */
|
|
189
|
+
const INNER_ENVIRONMENT = {
|
|
190
|
+
align: 'aligned',
|
|
191
|
+
flalign: 'aligned',
|
|
192
|
+
split: 'aligned',
|
|
193
|
+
alignat: 'alignedat',
|
|
194
|
+
gather: 'gathered',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* What to write instead of a LaTeX habit KaTeX rejects, or `undefined`.
|
|
199
|
+
* `replace` is set when KaTeX's own words would mislead: every span is parsed
|
|
200
|
+
* as inline math, so in a `$$…$$` block KaTeX says a display environment or
|
|
201
|
+
* `\tag` "can be used only in display mode" — but a block already is the
|
|
202
|
+
* display, and explico numbers it from its `\label{eq:key}`.
|
|
203
|
+
*/
|
|
204
|
+
function latexHabitAdvice(message, latex) {
|
|
205
|
+
const NUMBERED = 'a $$…$$ block already is the display, and explico numbers it from its \\label{eq:key}';
|
|
206
|
+
const display = /\{(\w+)(\*?)\} can be used only in display mode/.exec(message);
|
|
207
|
+
if (display) {
|
|
208
|
+
const [, name, star] = display;
|
|
209
|
+
const inner = INNER_ENVIRONMENT[name];
|
|
210
|
+
return {
|
|
211
|
+
replace: true,
|
|
212
|
+
text: inner
|
|
213
|
+
? `\\begin{${name}${star}} is a LaTeX display environment of its own; ${NUMBERED}. Write the rows in \\begin{${inner}}…\\end{${inner}}, one numbered equation per block.`
|
|
214
|
+
: `\\begin{${name}${star}} is a LaTeX display environment of its own; ${NUMBERED}. Drop \\begin{${name}${star}} and \\end{${name}${star}}.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (/\\tag works only in display equations/.test(message)) {
|
|
218
|
+
return {
|
|
219
|
+
replace: true,
|
|
220
|
+
text: `\\tag prints a number of its own, but ${NUMBERED}. Drop the \\tag, and refer to the equation with [[eq-key]].`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const command = /Undefined control sequence: \\(label|eqref|ref|autoref|cref|cite[a-z]*)\b/.exec(message)?.[1];
|
|
224
|
+
if (command === 'label') {
|
|
225
|
+
const label = /\\label\s*\{([^}]*)\}/.exec(latex)?.[1] ?? '';
|
|
226
|
+
return {
|
|
227
|
+
text: `An equation's label is \\label{eq:key}, its key lowercase kebab-case, in a $$…$$ block: \\label{${label}} is not one.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (command !== undefined && command.startsWith('cite')) {
|
|
231
|
+
return { text: 'Math holds no citation: cite in the prose with [@source-id].' };
|
|
232
|
+
}
|
|
233
|
+
if (command !== undefined) {
|
|
234
|
+
const target = new RegExp(`\\\\${command}\\s*\\{([a-z]+):([^}]*)\\}`).exec(latex);
|
|
235
|
+
const reference = target ? `[[${target[1]}-${target[2]}]]` : '[[eq-key]]';
|
|
236
|
+
return { text: `Math holds no reference: write ${reference} in the prose.` };
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
188
241
|
function parseMath(latex, label) {
|
|
189
242
|
assertPinnedKatex();
|
|
190
243
|
try {
|
|
191
244
|
return katex.__parse(latex, { throwOnError: true, strict: 'error' });
|
|
192
245
|
} catch (error) {
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
247
|
+
const advice = latexHabitAdvice(message, latex);
|
|
193
248
|
fail(
|
|
194
249
|
'invalid-latex',
|
|
195
|
-
|
|
250
|
+
advice?.replace
|
|
251
|
+
? `KaTeX could not parse ${label}: ${advice.text}`
|
|
252
|
+
: `KaTeX could not parse ${label}: ${message}${advice ? ` ${advice.text}` : ''}`,
|
|
196
253
|
{ label, latex },
|
|
197
254
|
);
|
|
198
255
|
}
|
|
@@ -265,6 +322,8 @@ const MARKER_COMMANDS = [EXPLAIN_COMMAND, '\\slot'];
|
|
|
265
322
|
export function stripNotationMarkers(latex) {
|
|
266
323
|
let result = '';
|
|
267
324
|
let cursor = 0;
|
|
325
|
+
// True right after a stripped marker, whose edge the next character meets.
|
|
326
|
+
let atMarkerEdge = false;
|
|
268
327
|
|
|
269
328
|
while (cursor < latex.length) {
|
|
270
329
|
const command = MARKER_COMMANDS.find(
|
|
@@ -282,18 +341,38 @@ export function stripNotationMarkers(latex) {
|
|
|
282
341
|
: undefined;
|
|
283
342
|
|
|
284
343
|
if (contentGroup) {
|
|
285
|
-
result
|
|
344
|
+
result = joinAtMarkerEdge(
|
|
345
|
+
result,
|
|
346
|
+
stripNotationMarkers(contentGroup.content),
|
|
347
|
+
);
|
|
286
348
|
cursor = contentGroup.end;
|
|
349
|
+
atMarkerEdge = true;
|
|
287
350
|
continue;
|
|
288
351
|
}
|
|
289
352
|
|
|
290
|
-
result
|
|
353
|
+
result = atMarkerEdge
|
|
354
|
+
? joinAtMarkerEdge(result, latex[cursor])
|
|
355
|
+
: result + latex[cursor];
|
|
356
|
+
atMarkerEdge = false;
|
|
291
357
|
cursor += 1;
|
|
292
358
|
}
|
|
293
359
|
|
|
294
360
|
return result;
|
|
295
361
|
}
|
|
296
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Join the two sides of a stripped marker's edge. TeX reads a control word up
|
|
365
|
+
* to its first non-letter, so `\times` and `m` must not become `\timesm`, nor
|
|
366
|
+
* `\lambda` and `u` become `\lambdau`. The space is neutral: TeX skips spaces
|
|
367
|
+
* after a control word in every mode.
|
|
368
|
+
*/
|
|
369
|
+
function joinAtMarkerEdge(before, after) {
|
|
370
|
+
const word = /\\[A-Za-z]+$/.exec(before);
|
|
371
|
+
return word && !isEscaped(before, word.index) && /^[A-Za-z]/.test(after)
|
|
372
|
+
? `${before} ${after}`
|
|
373
|
+
: before + after;
|
|
374
|
+
}
|
|
375
|
+
|
|
297
376
|
/**
|
|
298
377
|
* Every `\explain{key}{latex}` in the source (the escape hatch). Each key must
|
|
299
378
|
* already be in the page glyph table. The returned span points at the wrapped
|
|
@@ -385,6 +464,18 @@ function sourceWithoutExplainPrefixes(latex, calls) {
|
|
|
385
464
|
|
|
386
465
|
// --- structural signatures --------------------------------------------
|
|
387
466
|
|
|
467
|
+
/**
|
|
468
|
+
* The letters of a declared operator's body, spacing dropped, so
|
|
469
|
+
* `\operatorname{arg\,max}` and `\operatorname{argmax}` sign alike — the
|
|
470
|
+
* same name rule N8 keys its allow list by.
|
|
471
|
+
*/
|
|
472
|
+
function operatorLetters(value) {
|
|
473
|
+
if (Array.isArray(value)) return value.map(operatorLetters).join('');
|
|
474
|
+
if (!isNode(value)) return '';
|
|
475
|
+
if (value.type === 'mathord' || value.type === 'textord') return value.text ?? '';
|
|
476
|
+
return operatorLetters(value.body);
|
|
477
|
+
}
|
|
478
|
+
|
|
388
479
|
function signatureParts(node) {
|
|
389
480
|
if (!isNode(node)) return [];
|
|
390
481
|
|
|
@@ -399,7 +490,15 @@ function signatureParts(node) {
|
|
|
399
490
|
return [`atom:${node.family ?? ''}:${node.text ?? ''}`];
|
|
400
491
|
if (node.type === 'spacing') return [`space:${node.text ?? ''}`];
|
|
401
492
|
if (node.type === 'op' || node.type === 'operatorname') {
|
|
402
|
-
return [`operator:${node.name
|
|
493
|
+
if (typeof node.name === 'string') return [`operator:${node.name}`];
|
|
494
|
+
// An `\operatorname{…}` (or a `\mathop{…}`) carries its name in `body`,
|
|
495
|
+
// not `name`. Signed by its node type alone, every declared operator
|
|
496
|
+
// looked alike: a page's one operator card claimed every other declared
|
|
497
|
+
// operator on the page, and two operator cards could not share a page.
|
|
498
|
+
// A body with no letters (`\mathop{\sum}`) falls through to the full
|
|
499
|
+
// structural signature below.
|
|
500
|
+
const letters = operatorLetters(node.body);
|
|
501
|
+
if (letters !== '') return [`operator:${node.type}:${letters}`];
|
|
403
502
|
}
|
|
404
503
|
if (node.type === 'text')
|
|
405
504
|
return [`text:${signatureValue(node.body).join('|')}`];
|
|
@@ -722,6 +821,7 @@ function collectSourceStructure(tree, latex) {
|
|
|
722
821
|
const identifiers = [];
|
|
723
822
|
const scriptSpans = [];
|
|
724
823
|
const decorations = [];
|
|
824
|
+
const indexed = [];
|
|
725
825
|
const seenContainers = new Set();
|
|
726
826
|
const seenIdentifiers = new Set();
|
|
727
827
|
|
|
@@ -773,6 +873,11 @@ function collectSourceStructure(tree, latex) {
|
|
|
773
873
|
const span = nodeSpan(script, latex);
|
|
774
874
|
if (span) scriptSpans.push(span);
|
|
775
875
|
}
|
|
876
|
+
// A symbol with a subscript (rule N23). Not one in a distinct font's
|
|
877
|
+
// body: `\mathbf{x_1}` is a styled whole, like a container there.
|
|
878
|
+
if (!ignored && context.font === undefined && indexedForm(value)) {
|
|
879
|
+
indexed.push(value);
|
|
880
|
+
}
|
|
776
881
|
}
|
|
777
882
|
|
|
778
883
|
const decoration = ignored ? undefined : decorationRecord(value, latex);
|
|
@@ -808,7 +913,7 @@ function collectSourceStructure(tree, latex) {
|
|
|
808
913
|
|
|
809
914
|
addContainer(tree);
|
|
810
915
|
visitValue(tree);
|
|
811
|
-
return { containers, identifiers, scriptSpans, decorations };
|
|
916
|
+
return { containers, identifiers, scriptSpans, decorations, indexed };
|
|
812
917
|
}
|
|
813
918
|
|
|
814
919
|
// --- base-glyph signatures ------------------------------------------
|
|
@@ -896,21 +1001,319 @@ export function decorationOfEntry(latex) {
|
|
|
896
1001
|
}
|
|
897
1002
|
|
|
898
1003
|
/**
|
|
899
|
-
* The
|
|
900
|
-
*
|
|
901
|
-
*
|
|
1004
|
+
* The bare glyph an entry lends its card to: a plain letter's own, the base of
|
|
1005
|
+
* a superscript form `j^{(m)}`, or the head of a function form `D(0,t)`.
|
|
1006
|
+
*
|
|
1007
|
+
* An index is part of a symbol's name (rule N23), so an entry with a
|
|
1008
|
+
* subscript lends to no bare glyph: `v_0` (a fixed label) is not `v`, and a
|
|
1009
|
+
* family `\alpha_i` gives its card to its own instances (`\alpha_1`, see
|
|
1010
|
+
* {@link resolveIndexedNode}) but never to a bare `\alpha`, which is a
|
|
1011
|
+
* different object. A function form whose head is indexed (`V_0(1_t)`) lends
|
|
1012
|
+
* by that head, as a whole — see {@link indexedFunctionHead}.
|
|
902
1013
|
*/
|
|
903
1014
|
function canonicalBaseSignature(tree) {
|
|
1015
|
+
const head = functionFormHead(tree);
|
|
1016
|
+
if (head === null) return undefined;
|
|
1017
|
+
if (head?.type === 'supsub' && head.sub) return undefined;
|
|
1018
|
+
return identifierBaseSignature(head);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* The one significant node of an entry, or the head of a function form
|
|
1023
|
+
* (`D(0,t)`, `V_0(1_t)`: a head followed by `(`); `null` when the entry is
|
|
1024
|
+
* neither.
|
|
1025
|
+
*/
|
|
1026
|
+
function functionFormHead(tree) {
|
|
904
1027
|
const significant = tree.filter(
|
|
905
1028
|
(node) => node.type !== 'spacing' && node.type !== 'ordgroup',
|
|
906
1029
|
);
|
|
907
|
-
if (significant.length === 1) return
|
|
908
|
-
if (significant.length < 2) return
|
|
1030
|
+
if (significant.length === 1) return significant[0];
|
|
1031
|
+
if (significant.length < 2) return null;
|
|
909
1032
|
const [head, open] = significant;
|
|
910
1033
|
if (open.type !== 'atom' || open.family !== 'open' || open.text !== '(') {
|
|
911
|
-
return
|
|
1034
|
+
return null;
|
|
912
1035
|
}
|
|
913
|
-
return
|
|
1036
|
+
return head;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/** The indexed head of a function form, `V_0` in `V_0(1_t)`, else `undefined`. */
|
|
1040
|
+
function indexedFunctionHead(tree) {
|
|
1041
|
+
const significant = tree.filter(
|
|
1042
|
+
(node) => node.type !== 'spacing' && node.type !== 'ordgroup',
|
|
1043
|
+
);
|
|
1044
|
+
if (significant.length < 2) return undefined;
|
|
1045
|
+
const head = functionFormHead(tree);
|
|
1046
|
+
return head && indexedForm(head) ? head : undefined;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// --- indexed symbols (rule N23) -------------------------------------
|
|
1050
|
+
|
|
1051
|
+
/** A top-level comma in a subscript separates two index slots: `x_{i,j}`. */
|
|
1052
|
+
const isSlotComma = (node) =>
|
|
1053
|
+
node.type === 'atom' && node.family === 'punct' && node.text === ',';
|
|
1054
|
+
|
|
1055
|
+
/** The slots of a subscript, `{…}` groups flattened. */
|
|
1056
|
+
function indexSlots(sub) {
|
|
1057
|
+
const slots = [[]];
|
|
1058
|
+
for (const node of flattenGroup(sub)) {
|
|
1059
|
+
if (isSlotComma(node)) slots.push([]);
|
|
1060
|
+
else slots[slots.length - 1].push(node);
|
|
1061
|
+
}
|
|
1062
|
+
return slots;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const isDigitNode = (node) =>
|
|
1066
|
+
(node.type === 'textord' || node.type === 'mathord') &&
|
|
1067
|
+
/^[0-9.]+$/u.test(node.text ?? '');
|
|
1068
|
+
|
|
1069
|
+
const isLabelNode = (node) =>
|
|
1070
|
+
node.type === 'text' ||
|
|
1071
|
+
(node.type === 'font' && node.font === ROMAN_LABEL_FONT);
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* A running index is a lowercase Latin letter: `i`, `k`, `t`. An uppercase
|
|
1075
|
+
* letter in a subscript is a size (`X_N`) or another symbol (`p_X`), and a
|
|
1076
|
+
* Greek one a parameter (`p_\theta`), so neither runs over a set.
|
|
1077
|
+
*/
|
|
1078
|
+
const RUNNING_INDEX_LETTER = /^[a-z]$/u;
|
|
1079
|
+
|
|
1080
|
+
/** True for a letter that is a running index wherever it is a whole slot. */
|
|
1081
|
+
export function isRunningIndexLetter(text) {
|
|
1082
|
+
return RUNNING_INDEX_LETTER.test(text);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* What one index slot is: `running` (a lowercase letter), `digits`, `named`
|
|
1087
|
+
* (a `\mathrm{…}` / `\text{…}` label, or a subscript the page declares fixed:
|
|
1088
|
+
* an index entry `{}_x`, a letter card with `fixedIndex`), `symbol` (anything
|
|
1089
|
+
* else: `N`, `\theta`, `T_{\mathrm{fwd}}`, `t-1`), or `empty`.
|
|
1090
|
+
*/
|
|
1091
|
+
function slotKind(slot, fixedSlots) {
|
|
1092
|
+
if (slot.length === 0) return 'empty';
|
|
1093
|
+
if (fixedSlots.has(sequenceSignature(slot))) return 'named';
|
|
1094
|
+
if (slot.every(isDigitNode)) return 'digits';
|
|
1095
|
+
if (slot.length === 1 && isLabelNode(slot[0])) return 'named';
|
|
1096
|
+
if (
|
|
1097
|
+
slot.length === 1 &&
|
|
1098
|
+
slot[0].type === 'mathord' &&
|
|
1099
|
+
RUNNING_INDEX_LETTER.test(slot[0].text ?? '')
|
|
1100
|
+
) {
|
|
1101
|
+
return 'running';
|
|
1102
|
+
}
|
|
1103
|
+
return 'symbol';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Whether a subscript's base is a symbol an index belongs to: a letter
|
|
1108
|
+
* (`x`, `\theta`), a font-styled symbol (`\mathbf{v}`, `\mathbb{E}`), a roman
|
|
1109
|
+
* label (`\mathrm{RV}`), or a decorated one (`\hat{\beta}`, `b'`). A big
|
|
1110
|
+
* operator (`\sum_{i=1}`), a bracket (`\lVert x\rVert_2`), a number set
|
|
1111
|
+
* (`\mathbb{R}_+`) and a brace (`\underbrace{…}_{…}`) are not.
|
|
1112
|
+
*/
|
|
1113
|
+
function isSymbolBase(base) {
|
|
1114
|
+
const items = flattenGroup(base);
|
|
1115
|
+
if (items.length !== 1) return false;
|
|
1116
|
+
const [node] = items;
|
|
1117
|
+
if (IGNORED_MATH_TOKENS.has(fontTokenText(node))) return false;
|
|
1118
|
+
if (isIdentifierNode(node)) return true;
|
|
1119
|
+
if (node.type === 'font') return hasBindableGlyph(node) || isLabelNode(node);
|
|
1120
|
+
const decoration = decorationOf(node);
|
|
1121
|
+
return decoration !== undefined && isSymbolBase(node[decoration.part]);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* The parts of a symbol with a subscript, or `undefined` for any other node.
|
|
1126
|
+
* `sup` says what rides on top: nothing, a mark (`'`, `*`, `\dagger`: rule
|
|
1127
|
+
* N22) or anything else, a power (`^{2}`, `^{(m)}`).
|
|
1128
|
+
*/
|
|
1129
|
+
function indexedForm(node) {
|
|
1130
|
+
if (!isNode(node) || node.type !== 'supsub' || !node.sub) return undefined;
|
|
1131
|
+
if (!isSymbolBase(node.base)) return undefined;
|
|
1132
|
+
return {
|
|
1133
|
+
baseSignature: signatureValue(node.base).join('|'),
|
|
1134
|
+
subSignature: signatureValue(node.sub).join('|'),
|
|
1135
|
+
supSignature: node.sup ? signatureValue(node.sup).join('|') : '',
|
|
1136
|
+
sup: !node.sup
|
|
1137
|
+
? 'none'
|
|
1138
|
+
: scriptMarkName(node.sup) === undefined
|
|
1139
|
+
? 'power'
|
|
1140
|
+
: 'mark',
|
|
1141
|
+
slots: indexSlots(node.sub),
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** The signature of a symbol with its subscript and without what rides on top. */
|
|
1146
|
+
const coreSignature = (form) =>
|
|
1147
|
+
`supsub(${form.baseSignature})_(${form.subSignature})^()`;
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* The source span of a symbol and its subscript, without a power or mark
|
|
1151
|
+
* written after them: `v_0` in `v_0^{2}`. When the superscript sits between
|
|
1152
|
+
* them (`v^2_0`, `v'_0`) the two cannot be wrapped apart, so the whole node
|
|
1153
|
+
* is the span.
|
|
1154
|
+
*/
|
|
1155
|
+
function coreSourceSpan(node, latex) {
|
|
1156
|
+
const whole = sourceSpan(node, latex);
|
|
1157
|
+
if (!node.sup) return whole;
|
|
1158
|
+
const base = sourceSpan(node.base, latex);
|
|
1159
|
+
const sub = sourceSpan(node.sub, latex);
|
|
1160
|
+
if (!whole || !base || !sub) return whole;
|
|
1161
|
+
return /^\s*_\s*$/u.test(latex.slice(base.end, sub.start))
|
|
1162
|
+
? { start: base.start, end: sub.end }
|
|
1163
|
+
: whole;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Whether an occurrence is an instance of a family: same base, same number of
|
|
1168
|
+
* slots, the same superscript or none on the family, and each slot fits. A
|
|
1169
|
+
* running slot takes anything but a fixed label (`x_1`, `x_{k+1}`, `x_N`; not
|
|
1170
|
+
* `x_{\mathrm{e}}`), a `symbol` slot any symbol (`T_{\mathrm{opt}}` in place
|
|
1171
|
+
* of `T_{\mathrm{fwd}}`), and a fixed slot only itself.
|
|
1172
|
+
*/
|
|
1173
|
+
function isFamilyInstance(family, form, fixedSlots) {
|
|
1174
|
+
if (family.form.baseSignature !== form.baseSignature) return false;
|
|
1175
|
+
if (family.form.sup !== 'none' && family.form.supSignature !== form.supSignature) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
if (family.form.slots.length !== form.slots.length) return false;
|
|
1179
|
+
return family.form.slots.every((slot, index) => {
|
|
1180
|
+
const kind = family.kinds[index];
|
|
1181
|
+
const filler = form.slots[index];
|
|
1182
|
+
const fillerKind = slotKind(filler, fixedSlots);
|
|
1183
|
+
if (kind === 'running' || kind === 'symbol') {
|
|
1184
|
+
return fillerKind !== 'named' && fillerKind !== 'empty';
|
|
1185
|
+
}
|
|
1186
|
+
return sequenceSignature(slot) === sequenceSignature(filler);
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* The key of a derived entry: a base entry composed with an index entry
|
|
1192
|
+
* (`\mathbf{v}_0` from `velocity-vector` and `{}_0`). The build materializes
|
|
1193
|
+
* it from the two (`derived-notation.mjs`); it is never authored, and an
|
|
1194
|
+
* authored key that equals one fails the build.
|
|
1195
|
+
*
|
|
1196
|
+
* @param {string} baseKey
|
|
1197
|
+
* @param {string} indexKey
|
|
1198
|
+
* @returns {string}
|
|
1199
|
+
*/
|
|
1200
|
+
export function derivedNotationKey(baseKey, indexKey) {
|
|
1201
|
+
return `${baseKey}.sub.${indexKey}`;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* The subscript an **index entry** explains — an empty group with a subscript:
|
|
1206
|
+
* `{}_0`, `{}_x`, `{}_{\mathrm{max}}` — as the source after `{}`, else
|
|
1207
|
+
* `undefined`. Such an entry binds no symbol of its own; composed with the
|
|
1208
|
+
* entry of the symbol it indexes, it makes a derived card (rule N23).
|
|
1209
|
+
*
|
|
1210
|
+
* @param {string} latex
|
|
1211
|
+
* @returns {string | undefined}
|
|
1212
|
+
*/
|
|
1213
|
+
export function indexOfEntry(latex) {
|
|
1214
|
+
if (typeof latex !== 'string' || latex.trim() === '') return undefined;
|
|
1215
|
+
let tree;
|
|
1216
|
+
try {
|
|
1217
|
+
tree = parseMath(stripNotationMarkers(latex), 'index entry');
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
if (error instanceof GlyphResolutionError && error.code === 'invalid-latex') {
|
|
1220
|
+
return undefined;
|
|
1221
|
+
}
|
|
1222
|
+
throw error;
|
|
1223
|
+
}
|
|
1224
|
+
if (!isIndexEntryTree(tree)) return undefined;
|
|
1225
|
+
const match = /^\s*\{\s*\}\s*(_[\s\S]*)$/u.exec(latex);
|
|
1226
|
+
return match ? match[1].trim() : undefined;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function isIndexEntryTree(tree) {
|
|
1230
|
+
if (!Array.isArray(tree) || tree.length !== 1) return false;
|
|
1231
|
+
const [node] = tree;
|
|
1232
|
+
return (
|
|
1233
|
+
node.type === 'supsub' &&
|
|
1234
|
+
Boolean(node.sub) &&
|
|
1235
|
+
!node.sup &&
|
|
1236
|
+
isNode(node.base) &&
|
|
1237
|
+
node.base.type === 'ordgroup' &&
|
|
1238
|
+
node.base.body.length === 0
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* The symbol a base entry gives a derived card: its whole `latex`, or the
|
|
1244
|
+
* glyph it lends to — the head of a function form (`v` of `v(t)`), the base
|
|
1245
|
+
* of a superscript form (`j` of `j^{(m)}`).
|
|
1246
|
+
*
|
|
1247
|
+
* @param {string} notation
|
|
1248
|
+
* @returns {string}
|
|
1249
|
+
*/
|
|
1250
|
+
export function baseSymbolLatex(notation) {
|
|
1251
|
+
const source = stripNotationMarkers(notation);
|
|
1252
|
+
let tree;
|
|
1253
|
+
try {
|
|
1254
|
+
tree = parseMath(source, 'base symbol');
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
if (error instanceof GlyphResolutionError) return source.trim();
|
|
1257
|
+
throw error;
|
|
1258
|
+
}
|
|
1259
|
+
let node = functionFormHead(tree);
|
|
1260
|
+
if (!node) return source.trim();
|
|
1261
|
+
if (node.type === 'supsub' && !node.sub && scriptMarkName(node.sup) === undefined) {
|
|
1262
|
+
node = node.base;
|
|
1263
|
+
}
|
|
1264
|
+
const span = sourceSpan(node, source);
|
|
1265
|
+
return span ? source.slice(span.start, span.end) : source.trim();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* The symbol of a derived card: the base's symbol with the index entry's
|
|
1270
|
+
* subscript — `\mathbf{v}` and `{}_0` make `\mathbf{v}_0`.
|
|
1271
|
+
*
|
|
1272
|
+
* @param {string} baseNotation
|
|
1273
|
+
* @param {string} indexNotation
|
|
1274
|
+
* @returns {string}
|
|
1275
|
+
*/
|
|
1276
|
+
export function derivedNotationLatex(baseNotation, indexNotation) {
|
|
1277
|
+
return `${baseSymbolLatex(baseNotation)}${indexOfEntry(indexNotation) ?? ''}`;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* How an index entry is shown on its own — over a placeholder square, as an
|
|
1282
|
+
* operator entry is (`\square_0`). Display only.
|
|
1283
|
+
*
|
|
1284
|
+
* @param {string} latex
|
|
1285
|
+
* @returns {string}
|
|
1286
|
+
*/
|
|
1287
|
+
export function indexDisplayLatex(latex) {
|
|
1288
|
+
return latex.replace(/^\s*\{\s*\}/u, '\\square');
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Whether an entry's `latex` has a subscript on a symbol anywhere — what an
|
|
1293
|
+
* `\explain{key}{X_i}` wrap needs from `key` before its card can explain the
|
|
1294
|
+
* index (rule N23, `index-swallowed`).
|
|
1295
|
+
*
|
|
1296
|
+
* @param {string} latex
|
|
1297
|
+
* @returns {boolean}
|
|
1298
|
+
*/
|
|
1299
|
+
export function entryHasIndex(latex) {
|
|
1300
|
+
if (typeof latex !== 'string' || latex.trim() === '') return false;
|
|
1301
|
+
let tree;
|
|
1302
|
+
try {
|
|
1303
|
+
tree = parseMath(stripNotationMarkers(latex), 'indexed entry');
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (error instanceof GlyphResolutionError && error.code === 'invalid-latex') {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
const visit = (value) => {
|
|
1311
|
+
if (Array.isArray(value)) return value.some(visit);
|
|
1312
|
+
if (!isNode(value)) return false;
|
|
1313
|
+
if (value.type === 'supsub' && value.sub) return true;
|
|
1314
|
+
return [...SOURCE_CHILD_KEYS].some((key) => visit(value[key]));
|
|
1315
|
+
};
|
|
1316
|
+
return visit(tree);
|
|
914
1317
|
}
|
|
915
1318
|
|
|
916
1319
|
/**
|
|
@@ -1053,6 +1456,21 @@ function buildGlyphIndex(definitions) {
|
|
|
1053
1456
|
const byBase = new Map();
|
|
1054
1457
|
const ambiguousBase = new Set();
|
|
1055
1458
|
const operators = new Map();
|
|
1459
|
+
// Rule N23: index entries by the subscript they explain, indexed function
|
|
1460
|
+
// heads (`V_0` of `V_0(1_t)`) by their signature, the subscripts the page
|
|
1461
|
+
// declares fixed, and the indexed whole forms that may be families.
|
|
1462
|
+
const indexEntries = new Map();
|
|
1463
|
+
const headForms = new Map();
|
|
1464
|
+
const fixedSlots = new Set();
|
|
1465
|
+
const indexedEntries = [];
|
|
1466
|
+
// What each bare glyph's indexed entries are, for the message of a symbol
|
|
1467
|
+
// that no longer borrows one's card (rule N4).
|
|
1468
|
+
const indexedBases = new Map();
|
|
1469
|
+
const noteIndexedBase = (signature, key) => {
|
|
1470
|
+
const keys = indexedBases.get(signature) ?? [];
|
|
1471
|
+
if (!keys.includes(key)) keys.push(key);
|
|
1472
|
+
indexedBases.set(signature, keys);
|
|
1473
|
+
};
|
|
1056
1474
|
let maxNodeLength = 0;
|
|
1057
1475
|
|
|
1058
1476
|
for (const [index, value] of definitions.entries()) {
|
|
@@ -1105,13 +1523,52 @@ function buildGlyphIndex(definitions) {
|
|
|
1105
1523
|
continue;
|
|
1106
1524
|
}
|
|
1107
1525
|
|
|
1526
|
+
// An index entry (`{}_0`) binds no symbol; it explains its subscript
|
|
1527
|
+
// wherever that composes with an explained symbol (rule N23).
|
|
1528
|
+
if (isIndexEntryTree(tree)) {
|
|
1529
|
+
const subscript = signatureValue(tree[0].sub).join('|');
|
|
1530
|
+
const clash = indexEntries.get(subscript);
|
|
1531
|
+
if (clash && clash !== key) {
|
|
1532
|
+
fail(
|
|
1533
|
+
'ambiguous-canonical-notation',
|
|
1534
|
+
`Page glyphs "${clash}" and "${key}" are both index entries for the subscript in "${notation}": an index has one meaning per page (rule N4).`,
|
|
1535
|
+
{ left: clash, right: key },
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
indexEntries.set(subscript, key);
|
|
1539
|
+
fixedSlots.add(subscript);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const fixedIndex = value?.fixedIndex === true;
|
|
1544
|
+
const significant = tree.filter((node) => node.type !== 'spacing');
|
|
1545
|
+
if (significant.length === 1 && indexedForm(significant[0])) {
|
|
1546
|
+
const form = indexedForm(significant[0]);
|
|
1547
|
+
indexedEntries.push({ key, form, fixedIndex });
|
|
1548
|
+
noteIndexedBase(form.baseSignature, key);
|
|
1549
|
+
}
|
|
1550
|
+
if (fixedIndex && significant.length === 1 && isIdentifierNode(significant[0])) {
|
|
1551
|
+
fixedSlots.add(sequenceSignature(significant));
|
|
1552
|
+
}
|
|
1553
|
+
const indexedHead = indexedFunctionHead(tree);
|
|
1554
|
+
if (indexedHead) {
|
|
1555
|
+
noteIndexedBase(indexedForm(indexedHead).baseSignature, key);
|
|
1556
|
+
const head = sequenceSignature([indexedHead]);
|
|
1557
|
+
const clash = headForms.get(head);
|
|
1558
|
+
if (clash && clash !== key) ambiguousBase.add(head);
|
|
1559
|
+
else headForms.set(head, key);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1108
1562
|
const signature = sequenceSignature(tree);
|
|
1109
1563
|
if (hasBindableGlyph(tree) && tree.length > 0) {
|
|
1110
1564
|
const clash = bySignature.get(signature);
|
|
1111
1565
|
if (clash && clash !== key) {
|
|
1112
1566
|
fail(
|
|
1113
1567
|
'ambiguous-canonical-notation',
|
|
1114
|
-
`Page glyphs "${clash}" and "${key}" have structurally identical LaTeX ("${notation}")
|
|
1568
|
+
`Page glyphs "${clash}" and "${key}" have structurally identical LaTeX ("${notation}"), ` +
|
|
1569
|
+
'so a reader hovering it could not tell which is meant: a glyph has one meaning per ' +
|
|
1570
|
+
'page (rule N4), and \\explain does not lift that. Write one of them differently on ' +
|
|
1571
|
+
'this page (another letter, a subscript), or keep one of them off it.',
|
|
1115
1572
|
{ left: clash, right: key },
|
|
1116
1573
|
);
|
|
1117
1574
|
}
|
|
@@ -1129,18 +1586,163 @@ function buildGlyphIndex(definitions) {
|
|
|
1129
1586
|
}
|
|
1130
1587
|
}
|
|
1131
1588
|
|
|
1132
|
-
|
|
1589
|
+
// A family is an indexed entry with a running slot (`x_k`, `F_{t,T}`); one
|
|
1590
|
+
// whose every slot is fixed (`v_0`, `T_{\mathrm{opt}}`, or `fixedIndex`)
|
|
1591
|
+
// is a single symbol and lends its card to nothing.
|
|
1592
|
+
const families = new Map();
|
|
1593
|
+
for (const { key, form, fixedIndex } of indexedEntries) {
|
|
1594
|
+
if (fixedIndex) continue;
|
|
1595
|
+
const kinds = form.slots.map((slot) => slotKind(slot, fixedSlots));
|
|
1596
|
+
if (!kinds.includes('running')) continue;
|
|
1597
|
+
const list = families.get(form.baseSignature) ?? [];
|
|
1598
|
+
list.push({ key, form, kinds });
|
|
1599
|
+
families.set(form.baseSignature, list);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
return {
|
|
1603
|
+
byKey,
|
|
1604
|
+
bySignature,
|
|
1605
|
+
byBase,
|
|
1606
|
+
ambiguousBase,
|
|
1607
|
+
operators,
|
|
1608
|
+
indexEntries,
|
|
1609
|
+
headForms,
|
|
1610
|
+
fixedSlots,
|
|
1611
|
+
families,
|
|
1612
|
+
indexedBases,
|
|
1613
|
+
maxNodeLength,
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* The entry that explains a symbol on its own — its whole form, or a bare
|
|
1619
|
+
* glyph's unique card — so that an index entry can compose with it into a
|
|
1620
|
+
* derived card. `undefined` when there is none, or more than one.
|
|
1621
|
+
*/
|
|
1622
|
+
function baseEntryKey(base, glyphs) {
|
|
1623
|
+
const signature = signatureValue(base).join('|');
|
|
1624
|
+
const whole = glyphs.bySignature.get(signature);
|
|
1625
|
+
if (whole !== undefined) return whole;
|
|
1626
|
+
const items = flattenGroup(base);
|
|
1627
|
+
if (items.length !== 1 || !isIdentifierNode(items[0])) return undefined;
|
|
1628
|
+
if (glyphs.ambiguousBase.has(signature)) return undefined;
|
|
1629
|
+
return glyphs.byBase.get(signature);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Resolve a symbol with a subscript that no whole form matched exactly
|
|
1634
|
+
* (rule N23), taking the first of these that exists:
|
|
1635
|
+
*
|
|
1636
|
+
* 1. the head of a function form (`V_0` of `V_0(1_t)`), or a whole form
|
|
1637
|
+
* under a power or a mark — `v_0^{2}` is `v_0` squared;
|
|
1638
|
+
* 2. a family whose index has the same shape — `x_1` under `x_k`;
|
|
1639
|
+
* 3. with `derive`, an index entry for its whole subscript and an entry for
|
|
1640
|
+
* its base: a derived card (`\mathbf{v}_0` from `\mathbf{v}` and `{}_0`).
|
|
1641
|
+
*
|
|
1642
|
+
* Returns `{ key, span, match, derived? }`, or `undefined` to leave the
|
|
1643
|
+
* symbol to its base's card and its index's own.
|
|
1644
|
+
*/
|
|
1645
|
+
function resolveIndexedNode(node, glyphs, source, derive) {
|
|
1646
|
+
const form = indexedForm(node);
|
|
1647
|
+
if (!form) return undefined;
|
|
1648
|
+
const whole = sourceSpan(node, source);
|
|
1649
|
+
const core = coreSourceSpan(node, source);
|
|
1650
|
+
if (!whole || !core) return undefined;
|
|
1651
|
+
|
|
1652
|
+
const head = sequenceSignature([node]);
|
|
1653
|
+
if (glyphs.headForms.has(head) && !glyphs.ambiguousBase.has(head)) {
|
|
1654
|
+
return { key: glyphs.headForms.get(head), span: whole, match: 'canonical' };
|
|
1655
|
+
}
|
|
1656
|
+
if (form.sup !== 'none') {
|
|
1657
|
+
const signature = coreSignature(form);
|
|
1658
|
+
const key =
|
|
1659
|
+
glyphs.bySignature.get(signature) ??
|
|
1660
|
+
(glyphs.ambiguousBase.has(signature)
|
|
1661
|
+
? undefined
|
|
1662
|
+
: glyphs.headForms.get(signature));
|
|
1663
|
+
if (key !== undefined) return { key, span: core, match: 'canonical' };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const families = (glyphs.families.get(form.baseSignature) ?? []).filter(
|
|
1667
|
+
(family) => isFamilyInstance(family, form, glyphs.fixedSlots),
|
|
1668
|
+
);
|
|
1669
|
+
if (families.length > 1) {
|
|
1670
|
+
const token = source.slice(whole.start, whole.end);
|
|
1671
|
+
fail(
|
|
1672
|
+
'ambiguous-base-fallback',
|
|
1673
|
+
`"${token}" at ${whole.start}:${whole.end} is an instance of more than one page symbol (${families
|
|
1674
|
+
.map((family) => `"${family.key}"`)
|
|
1675
|
+
.join(', ')}); wrap it in \\explain{key}{${token}}.`,
|
|
1676
|
+
{ token, start: whole.start, end: whole.end },
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
if (families.length === 1) {
|
|
1680
|
+
const [family] = families;
|
|
1681
|
+
return {
|
|
1682
|
+
key: family.key,
|
|
1683
|
+
span: family.form.sup !== 'none' ? whole : core,
|
|
1684
|
+
match: 'family',
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (!derive) return undefined;
|
|
1689
|
+
const indexKey = glyphs.indexEntries.get(form.subSignature);
|
|
1690
|
+
if (indexKey === undefined) return undefined;
|
|
1691
|
+
const baseKey = baseEntryKey(node.base, glyphs);
|
|
1692
|
+
if (baseKey === undefined) return undefined;
|
|
1693
|
+
return {
|
|
1694
|
+
key: derivedNotationKey(baseKey, indexKey),
|
|
1695
|
+
span: core,
|
|
1696
|
+
match: 'derived',
|
|
1697
|
+
derived: { base: baseKey, index: indexKey },
|
|
1698
|
+
};
|
|
1133
1699
|
}
|
|
1134
1700
|
|
|
1135
|
-
/**
|
|
1136
|
-
|
|
1701
|
+
/**
|
|
1702
|
+
* Bind whole canonical forms: greedy longest match, left to right, per run.
|
|
1703
|
+
* A symbol with a subscript that no whole form matches exactly is then
|
|
1704
|
+
* resolved as an indexed symbol ({@link resolveIndexedNode}).
|
|
1705
|
+
*
|
|
1706
|
+
* A match inside another one's script still binds — `T_{\mathrm{opt}}` in
|
|
1707
|
+
* `B_{T_{\mathrm{opt}}}` keeps its own card — unless it is a lone letter
|
|
1708
|
+
* (`matchBaseGlyphs` binds those) or the outer match is a derived card, whose
|
|
1709
|
+
* index is explained by the index entry as a whole.
|
|
1710
|
+
*/
|
|
1711
|
+
function matchCanonicalForms(
|
|
1712
|
+
latex,
|
|
1713
|
+
strippedSource,
|
|
1714
|
+
containers,
|
|
1715
|
+
glyphs,
|
|
1716
|
+
scriptSpans,
|
|
1717
|
+
derive,
|
|
1718
|
+
explicitCalls,
|
|
1719
|
+
) {
|
|
1137
1720
|
const bindings = [];
|
|
1138
1721
|
const claimed = [];
|
|
1722
|
+
// An author's `\explain` claims what it wraps: a symbol inside one is not
|
|
1723
|
+
// resolved as an indexed symbol, so two families never fail there.
|
|
1724
|
+
const insideExplicit = (node) => {
|
|
1725
|
+
const span = sourceSpan(node, strippedSource);
|
|
1726
|
+
return (
|
|
1727
|
+
span !== undefined && explicitCalls.some((call) => contains(call, span))
|
|
1728
|
+
);
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
const blocked = (span, nodes) =>
|
|
1732
|
+
claimed.some(
|
|
1733
|
+
(existing) =>
|
|
1734
|
+
sameSpan(existing, span) ||
|
|
1735
|
+
(contains(existing, span) &&
|
|
1736
|
+
(existing.match === 'derived' ||
|
|
1737
|
+
(nodes.length === 1 && isIdentifierNode(nodes[0])) ||
|
|
1738
|
+
!isSafeScriptNesting(span, existing, scriptSpans))),
|
|
1739
|
+
);
|
|
1139
1740
|
|
|
1140
1741
|
for (const container of containers) {
|
|
1141
1742
|
let cursor = 0;
|
|
1142
1743
|
while (cursor < container.length) {
|
|
1143
1744
|
let step = 1;
|
|
1745
|
+
let found;
|
|
1144
1746
|
const maxLength = Math.min(
|
|
1145
1747
|
glyphs.maxNodeLength,
|
|
1146
1748
|
container.length - cursor,
|
|
@@ -1153,27 +1755,42 @@ function matchCanonicalForms(latex, strippedSource, containers, glyphs) {
|
|
|
1153
1755
|
// or `b'` wraps the decoration too, not just the letter under it.
|
|
1154
1756
|
const rawSpan = sourceSpan(nodes, strippedSource);
|
|
1155
1757
|
if (!rawSpan) continue;
|
|
1758
|
+
found = { key, nodes, rawSpan, match: 'canonical' };
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
// A group around one node (an `aligned` cell's) shares that node's
|
|
1762
|
+
// container, which is recorded once: look through it.
|
|
1763
|
+
let single = container[cursor];
|
|
1764
|
+
while (single.type === 'ordgroup' && single.body.length === 1) {
|
|
1765
|
+
[single] = single.body;
|
|
1766
|
+
}
|
|
1767
|
+
if (!found && !insideExplicit(single)) {
|
|
1768
|
+
const indexed = resolveIndexedNode(single, glyphs, strippedSource, derive);
|
|
1769
|
+
if (indexed) {
|
|
1770
|
+
found = {
|
|
1771
|
+
...indexed,
|
|
1772
|
+
nodes: [container[cursor]],
|
|
1773
|
+
rawSpan: indexed.span,
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
if (found) {
|
|
1156
1778
|
const span = expandFontWrapperSpan(
|
|
1157
1779
|
latex,
|
|
1158
|
-
unwrapEnclosingGroupSpan(latex, rawSpan),
|
|
1780
|
+
unwrapEnclosingGroupSpan(latex, found.rawSpan),
|
|
1159
1781
|
);
|
|
1160
|
-
step = length;
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1782
|
+
step = found.nodes.length;
|
|
1783
|
+
if (!blocked(span, found.nodes)) {
|
|
1784
|
+
bindings.push({
|
|
1785
|
+
key: found.key,
|
|
1786
|
+
...span,
|
|
1787
|
+
token: latex.slice(span.start, span.end),
|
|
1788
|
+
level: 'scope',
|
|
1789
|
+
match: found.match,
|
|
1790
|
+
...(found.derived ? { derived: found.derived } : {}),
|
|
1791
|
+
});
|
|
1792
|
+
claimed.push({ ...span, match: found.match });
|
|
1167
1793
|
}
|
|
1168
|
-
bindings.push({
|
|
1169
|
-
key,
|
|
1170
|
-
...span,
|
|
1171
|
-
token: latex.slice(span.start, span.end),
|
|
1172
|
-
level: 'scope',
|
|
1173
|
-
match: 'canonical',
|
|
1174
|
-
});
|
|
1175
|
-
claimed.push(span);
|
|
1176
|
-
break;
|
|
1177
1794
|
}
|
|
1178
1795
|
cursor += step;
|
|
1179
1796
|
}
|
|
@@ -1194,10 +1811,13 @@ function matchBaseGlyphs(latex, identifiers, canonical, glyphs, scriptSpans) {
|
|
|
1194
1811
|
// is already accounted for by that call; never re-flag or re-bind it, even
|
|
1195
1812
|
// when it also sits in a script inside a larger canonical match. (To bind a
|
|
1196
1813
|
// nested glyph separately, wrap it in its own nested `\explain`.)
|
|
1814
|
+
// A derived card (rule N23) explains its index as a whole, so a letter in
|
|
1815
|
+
// that index (`x` of `v_x` under `{}_x`) gets no card of its own either.
|
|
1197
1816
|
if (
|
|
1198
1817
|
canonical.some(
|
|
1199
1818
|
(binding) =>
|
|
1200
|
-
binding.level === 'explicit'
|
|
1819
|
+
(binding.level === 'explicit' || binding.match === 'derived') &&
|
|
1820
|
+
contains(binding, identifier),
|
|
1201
1821
|
)
|
|
1202
1822
|
) {
|
|
1203
1823
|
continue;
|
|
@@ -1276,17 +1896,20 @@ function injectScopeBindings(latex, bindings) {
|
|
|
1276
1896
|
* N22). A letter under such a decoration is reported once, as the decorated
|
|
1277
1897
|
* form (`b''''`, not `b`): that whole form is what needs the entry.
|
|
1278
1898
|
*/
|
|
1279
|
-
function unresolvedIdentifiers(latex, identifiers, bindings, suspended) {
|
|
1899
|
+
function unresolvedIdentifiers(latex, identifiers, bindings, suspended, indexedBases) {
|
|
1280
1900
|
const letters = identifiers
|
|
1281
1901
|
.filter(
|
|
1282
1902
|
(identifier) =>
|
|
1283
1903
|
!bindings.some((binding) => contains(binding, identifier)) &&
|
|
1284
1904
|
!suspended.some((decoration) => contains(decoration.part, identifier)),
|
|
1285
1905
|
)
|
|
1286
|
-
.map(({ start, end, font }) => {
|
|
1906
|
+
.map(({ start, end, font, signature }) => {
|
|
1287
1907
|
// Report `\mathbb{E}`, not a bare `E`, so the fix hint points at the
|
|
1288
1908
|
// whole token the author has to wrap in `\explain`.
|
|
1289
1909
|
const wrapped = expandFontWrapperSpan(latex, { start, end });
|
|
1910
|
+
// Rule N23: an indexed entry lends its card to no bare glyph, so name
|
|
1911
|
+
// the ones this letter used to borrow from.
|
|
1912
|
+
const lenders = font === undefined ? (indexedBases?.get(signature) ?? []) : [];
|
|
1290
1913
|
return {
|
|
1291
1914
|
token: latex.slice(wrapped.start, wrapped.end),
|
|
1292
1915
|
// A styled base-library letter (`\mathbf{e}`) is not Euler's number.
|
|
@@ -1297,6 +1920,7 @@ function unresolvedIdentifiers(latex, identifiers, bindings, suspended) {
|
|
|
1297
1920
|
start: wrapped.start,
|
|
1298
1921
|
end: wrapped.end,
|
|
1299
1922
|
...(font === undefined ? {} : { font }),
|
|
1923
|
+
...(lenders.length === 0 ? {} : { lenders: [...lenders] }),
|
|
1300
1924
|
};
|
|
1301
1925
|
})
|
|
1302
1926
|
.filter(({ libraryToken }) => !BASE_LIBRARY.has(libraryToken))
|
|
@@ -1312,6 +1936,151 @@ function unresolvedIdentifiers(latex, identifiers, bindings, suspended) {
|
|
|
1312
1936
|
);
|
|
1313
1937
|
}
|
|
1314
1938
|
|
|
1939
|
+
/**
|
|
1940
|
+
* How each symbol with a subscript is explained (rule N23), for the check in
|
|
1941
|
+
* `gate-indices.ts`. `how` is one of:
|
|
1942
|
+
*
|
|
1943
|
+
* - `whole-form`, `family`, `derived`: a binding of that kind is exactly the
|
|
1944
|
+
* symbol; `inside-form`: a larger whole form covers it (`F_k` in `CF_k`);
|
|
1945
|
+
* - `explicit`: an `\explain` around exactly it, whose key's `latex` has an
|
|
1946
|
+
* index; `swallowed`: the same, but the key's `latex` has none, so its card
|
|
1947
|
+
* cannot explain the index;
|
|
1948
|
+
* - `inside-wrap`: inside a larger `\explain`, which claims its content;
|
|
1949
|
+
* - `composed`: the base's card plus each slot's own — `explained` only when
|
|
1950
|
+
* the base has a card and every slot holds a symbol with one (`X_{t-1}`);
|
|
1951
|
+
* `unexplained` lists the parts that have none, the base first when
|
|
1952
|
+
* `baseExplained` is false;
|
|
1953
|
+
* - `unresolved`: a letter in it has no card at all, which rule N4 reports.
|
|
1954
|
+
*
|
|
1955
|
+
* `running` lists the running index letters (`i`, `k`, `t`) in its slots that
|
|
1956
|
+
* the page has not declared fixed, with the key of the letter's own card and
|
|
1957
|
+
* of the entry that covers the symbol, for the index-set check.
|
|
1958
|
+
*/
|
|
1959
|
+
function classifyIndexed(nodes, context) {
|
|
1960
|
+
const { latex, source, bindings, explicitCalls, glyphs, identifiers, unresolved } =
|
|
1961
|
+
context;
|
|
1962
|
+
const tight = (span) => (span ? unwrapEnclosingGroupSpan(latex, span) : span);
|
|
1963
|
+
const size = (span) => span.end - span.start;
|
|
1964
|
+
const cards = bindings.filter((binding) => binding.match !== 'operator');
|
|
1965
|
+
const isUnresolved = (span) => unresolved.some((item) => contains(span, item));
|
|
1966
|
+
const results = [];
|
|
1967
|
+
const seen = new Set();
|
|
1968
|
+
|
|
1969
|
+
for (const node of nodes) {
|
|
1970
|
+
const whole = tight(sourceSpan(node, source));
|
|
1971
|
+
const core = tight(coreSourceSpan(node, source));
|
|
1972
|
+
const base = tight(sourceSpan(node.base, source));
|
|
1973
|
+
const sub = tight(sourceSpan(node.sub, source));
|
|
1974
|
+
if (!whole || !core || !base || !sub) continue;
|
|
1975
|
+
if (seen.has(`${core.start}:${core.end}`)) continue;
|
|
1976
|
+
seen.add(`${core.start}:${core.end}`);
|
|
1977
|
+
|
|
1978
|
+
const slots = indexSlots(node.sub).map((slot) => ({
|
|
1979
|
+
span: tight(sourceSpan(slot, source)),
|
|
1980
|
+
kind: slotKind(slot, glyphs.fixedSlots),
|
|
1981
|
+
}));
|
|
1982
|
+
const exactly = (span) => sameSpan(span, core) || sameSpan(span, whole);
|
|
1983
|
+
|
|
1984
|
+
let how;
|
|
1985
|
+
let key = null;
|
|
1986
|
+
const wrap = explicitCalls
|
|
1987
|
+
.filter((call) => contains(call, core))
|
|
1988
|
+
.sort((left, right) => size(left) - size(right))[0];
|
|
1989
|
+
const cover = cards
|
|
1990
|
+
.filter(
|
|
1991
|
+
(binding) =>
|
|
1992
|
+
binding.level === 'scope' &&
|
|
1993
|
+
binding.match !== 'base' &&
|
|
1994
|
+
contains(binding, core),
|
|
1995
|
+
)
|
|
1996
|
+
.sort((left, right) => size(left) - size(right))[0];
|
|
1997
|
+
if (wrap && !exactly(wrap)) {
|
|
1998
|
+
how = 'inside-wrap';
|
|
1999
|
+
} else if (wrap) {
|
|
2000
|
+
key = wrap.key;
|
|
2001
|
+
how = entryHasIndex(glyphs.byKey.get(wrap.key)?.notation)
|
|
2002
|
+
? 'explicit'
|
|
2003
|
+
: 'swallowed';
|
|
2004
|
+
} else if (cover) {
|
|
2005
|
+
key = cover.key;
|
|
2006
|
+
how = !exactly(cover)
|
|
2007
|
+
? 'inside-form'
|
|
2008
|
+
: cover.match === 'canonical'
|
|
2009
|
+
? 'whole-form'
|
|
2010
|
+
: cover.match;
|
|
2011
|
+
} else if (isUnresolved(whole)) {
|
|
2012
|
+
how = 'unresolved';
|
|
2013
|
+
} else {
|
|
2014
|
+
how = 'composed';
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const unexplained = [];
|
|
2018
|
+
let baseExplained = true;
|
|
2019
|
+
if (how === 'composed') {
|
|
2020
|
+
// `\hat{\beta}` has a card when `\beta` does; the hat is rule N22's.
|
|
2021
|
+
if (
|
|
2022
|
+
!cards.some(
|
|
2023
|
+
(binding) => contains(binding, base) || contains(base, binding),
|
|
2024
|
+
)
|
|
2025
|
+
) {
|
|
2026
|
+
baseExplained = false;
|
|
2027
|
+
unexplained.push(latex.slice(base.start, base.end));
|
|
2028
|
+
}
|
|
2029
|
+
for (const slot of slots) {
|
|
2030
|
+
if (slot.kind === 'empty' || !slot.span) continue;
|
|
2031
|
+
const carded = cards.some((binding) => contains(slot.span, binding));
|
|
2032
|
+
if (!carded) unexplained.push(latex.slice(slot.span.start, slot.span.end));
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const running = [];
|
|
2037
|
+
// A swallowed index is already an error; its letters wait for the fix.
|
|
2038
|
+
if (!['inside-wrap', 'derived', 'unresolved', 'swallowed'].includes(how)) {
|
|
2039
|
+
for (const slot of slots) {
|
|
2040
|
+
if (slot.kind === 'named' || !slot.span) continue;
|
|
2041
|
+
for (const identifier of identifiers) {
|
|
2042
|
+
if (identifier.font !== undefined) continue;
|
|
2043
|
+
if (identifier.node.type !== 'mathord') continue;
|
|
2044
|
+
if (!RUNNING_INDEX_LETTER.test(identifier.node.text ?? '')) continue;
|
|
2045
|
+
if (!contains(slot.span, identifier)) continue;
|
|
2046
|
+
const own = cards
|
|
2047
|
+
.filter(
|
|
2048
|
+
(binding) =>
|
|
2049
|
+
contains(binding, identifier) && contains(slot.span, binding),
|
|
2050
|
+
)
|
|
2051
|
+
.sort((left, right) => size(left) - size(right))[0];
|
|
2052
|
+
running.push({
|
|
2053
|
+
letter: identifier.node.text,
|
|
2054
|
+
start: identifier.start,
|
|
2055
|
+
end: identifier.end,
|
|
2056
|
+
key: own?.key ?? null,
|
|
2057
|
+
cover: key,
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// The form as written, markers dropped: `\mathbf{v}_0`, not
|
|
2064
|
+
// `\explain{v}{\mathbf{v}}_0` or the blanked `{\mathbf{v}}_0`.
|
|
2065
|
+
const shown = widenOverExplainPrefix(core, explicitCalls);
|
|
2066
|
+
results.push({
|
|
2067
|
+
token: stripNotationMarkers(latex.slice(shown.start, core.end)).trim(),
|
|
2068
|
+
start: core.start,
|
|
2069
|
+
end: core.end,
|
|
2070
|
+
base: latex.slice(base.start, base.end),
|
|
2071
|
+
index: latex.slice(sub.start, sub.end),
|
|
2072
|
+
how,
|
|
2073
|
+
key,
|
|
2074
|
+
explained:
|
|
2075
|
+
how === 'composed' ? unexplained.length === 0 : how !== 'swallowed',
|
|
2076
|
+
baseExplained,
|
|
2077
|
+
unexplained,
|
|
2078
|
+
running,
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
return results.sort((left, right) => left.start - right.start);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1315
2084
|
/**
|
|
1316
2085
|
* A decoration over an `\explain{key}{…}` call starts at that call's content
|
|
1317
2086
|
* group in the blanked source; widen it to the call itself, so a marker
|
|
@@ -1576,9 +2345,20 @@ function findNestedBareDelimiters(tree) {
|
|
|
1576
2345
|
* decorated form no whole form covers is reported in `unresolved`, marked
|
|
1577
2346
|
* `suspended`.
|
|
1578
2347
|
*
|
|
2348
|
+
* A symbol with a subscript (rule N23) takes its exact whole form, else the
|
|
2349
|
+
* family whose index has its shape, else — with `options.derive`, which page
|
|
2350
|
+
* math sets (a lesson body, its assessments, labs and diagrams) — a derived
|
|
2351
|
+
* card from an index entry (`{}_0`) and the base's entry; each such card
|
|
2352
|
+
* wraps the whole indexed symbol. `derived`
|
|
2353
|
+
* lists the derived cards used, as `{ key, base, index }`, for the build to
|
|
2354
|
+
* materialize; `indices` says how each indexed symbol is explained (see
|
|
2355
|
+
* `classifyIndexed`). A definition may carry `fixedIndex: true`: its index
|
|
2356
|
+
* (or, on a letter, the letter as an index) is a fixed label, not a running
|
|
2357
|
+
* one, so it is no family.
|
|
2358
|
+
*
|
|
1579
2359
|
* @param {string} latex
|
|
1580
|
-
* @param {Array<{key: string, notation: string}>} definitions
|
|
1581
|
-
* @param {{ suspended?: readonly string[] }} [options]
|
|
2360
|
+
* @param {Array<{key: string, notation: string, fixedIndex?: boolean}>} definitions
|
|
2361
|
+
* @param {{ suspended?: readonly string[], derive?: boolean }} [options]
|
|
1582
2362
|
*/
|
|
1583
2363
|
export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
1584
2364
|
if (typeof latex !== 'string') {
|
|
@@ -1586,6 +2366,7 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
|
1586
2366
|
}
|
|
1587
2367
|
|
|
1588
2368
|
const suspended = new Set(options.suspended ?? []);
|
|
2369
|
+
const derive = options.derive === true;
|
|
1589
2370
|
const glyphs = buildGlyphIndex(definitions);
|
|
1590
2371
|
const explicitCalls = findExplainCalls(latex, glyphs.byKey);
|
|
1591
2372
|
// The source KaTeX actually parses — `\explain{key}{` prefixes blanked to
|
|
@@ -1622,7 +2403,7 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
|
1622
2403
|
{ violations: nestingViolations },
|
|
1623
2404
|
);
|
|
1624
2405
|
}
|
|
1625
|
-
const { containers, identifiers, scriptSpans, decorations } =
|
|
2406
|
+
const { containers, identifiers, scriptSpans, decorations, indexed } =
|
|
1626
2407
|
collectSourceStructure(tree, strippedSource);
|
|
1627
2408
|
|
|
1628
2409
|
// A canonical container match can be found by seeing through transparent
|
|
@@ -1636,6 +2417,9 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
|
1636
2417
|
strippedSource,
|
|
1637
2418
|
containers,
|
|
1638
2419
|
glyphs,
|
|
2420
|
+
scriptSpans,
|
|
2421
|
+
derive,
|
|
2422
|
+
explicitCalls,
|
|
1639
2423
|
).filter(
|
|
1640
2424
|
(binding) =>
|
|
1641
2425
|
!explicitCalls.some(
|
|
@@ -1660,8 +2444,8 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
|
1660
2444
|
const suspendedDecorations = [];
|
|
1661
2445
|
const decorationResults = decorations.map((decoration) => {
|
|
1662
2446
|
const span = widenOverExplainPrefix(decoration, explicitCalls);
|
|
1663
|
-
const
|
|
1664
|
-
|
|
2447
|
+
const cover = covered.find((binding) => contains(binding, span));
|
|
2448
|
+
const wholeForm = cover?.key ?? null;
|
|
1665
2449
|
let operator = null;
|
|
1666
2450
|
if (wholeForm === null && suspended.has(decoration.name)) {
|
|
1667
2451
|
suspendedDecorations.push({ ...decoration, ...span });
|
|
@@ -1688,6 +2472,9 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
|
1688
2472
|
...span,
|
|
1689
2473
|
token: latex.slice(span.start, span.end),
|
|
1690
2474
|
wholeForm,
|
|
2475
|
+
// An author's `\explain` names the whole form; rule N22 checks that
|
|
2476
|
+
// its key's `latex` carries the decoration.
|
|
2477
|
+
wholeFormExplicit: cover?.level === 'explicit',
|
|
1691
2478
|
operator,
|
|
1692
2479
|
};
|
|
1693
2480
|
});
|
|
@@ -1726,6 +2513,19 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
|
1726
2513
|
}
|
|
1727
2514
|
}
|
|
1728
2515
|
|
|
2516
|
+
const unresolved = unresolvedIdentifiers(
|
|
2517
|
+
latex,
|
|
2518
|
+
identifiers,
|
|
2519
|
+
bindings,
|
|
2520
|
+
suspendedDecorations,
|
|
2521
|
+
glyphs.indexedBases,
|
|
2522
|
+
);
|
|
2523
|
+
const derived = new Map();
|
|
2524
|
+
for (const binding of bindings) {
|
|
2525
|
+
if (binding.match !== 'derived' || derived.has(binding.key)) continue;
|
|
2526
|
+
derived.set(binding.key, { key: binding.key, ...binding.derived });
|
|
2527
|
+
}
|
|
2528
|
+
|
|
1729
2529
|
return {
|
|
1730
2530
|
latex: injectScopeBindings(latex, bindings),
|
|
1731
2531
|
bindings: bindings.map(({ key, level, match, token }) => ({
|
|
@@ -1734,13 +2534,18 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
|
|
|
1734
2534
|
match,
|
|
1735
2535
|
token,
|
|
1736
2536
|
})),
|
|
1737
|
-
unresolved
|
|
2537
|
+
unresolved,
|
|
2538
|
+
decorations: decorationResults,
|
|
2539
|
+
indices: classifyIndexed(indexed, {
|
|
1738
2540
|
latex,
|
|
1739
|
-
|
|
2541
|
+
source: strippedSource,
|
|
1740
2542
|
bindings,
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2543
|
+
explicitCalls,
|
|
2544
|
+
glyphs,
|
|
2545
|
+
identifiers,
|
|
2546
|
+
unresolved,
|
|
2547
|
+
}),
|
|
2548
|
+
derived: [...derived.values()],
|
|
1744
2549
|
};
|
|
1745
2550
|
}
|
|
1746
2551
|
|