explico 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1137 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +4 -3
- package/dist/compiler/astro.js +45 -15
- package/dist/compiler/cli.d.ts +26 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +9 -1
- package/dist/compiler/collections.js +55 -16
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +14 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +87 -6
- package/dist/compiler/manifest.js +174 -31
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +2 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +2 -2
- package/dist/reference/base-operators.mjs +2 -2
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +34 -4
- package/dist/reference/page-notation.js +54 -2
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +104 -10
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +84 -20
- package/dist/reference/render-lab-math.d.ts +2 -0
- package/dist/reference/render-lab-math.js +17 -5
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +191 -30
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +79 -24
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +32 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +111 -6
- package/docs/authoring/migrating.md +707 -0
- package/docs/authoring/notation.md +281 -29
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +10 -41
- package/docs/authoring/structure.md +53 -2
- package/docs/authoring/visualizations.md +144 -18
- package/package.json +12 -7
- package/src/compiler/astro.ts +57 -17
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +92 -19
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +299 -28
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +12 -9
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/figure/Figure.astro +11 -0
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +321 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +47 -9
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +13 -21
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +2 -2
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +100 -3
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +154 -8
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +84 -20
- package/src/reference/render-lab-math.ts +29 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +205 -29
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +291 -75
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* `content/abbreviations/*.md` table, not here. This baseline only holds
|
|
12
12
|
* abbreviations so common in ordinary English/academic writing that spelling
|
|
13
13
|
* them out on every course would be noise, not help. A course adds a real
|
|
14
|
-
* one via `
|
|
15
|
-
* `
|
|
14
|
+
* one via `rules.N14.allow` in its course config, the same shape as
|
|
15
|
+
* `rules.N8.allow` / `rules.N10.allow`.
|
|
16
16
|
*/
|
|
17
17
|
export const BASE_ABBREVIATIONS = Object.freeze(
|
|
18
18
|
new Set([
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*
|
|
22
22
|
* A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
|
|
23
23
|
* written `\operatorname{…}` and is NOT here: it earns a card, a
|
|
24
|
-
* `notation.local` entry, or an explicit line in
|
|
25
|
-
* (`
|
|
24
|
+
* `notation.local` entry, or an explicit line in the course config
|
|
25
|
+
* (`rules.N8.allow`).
|
|
26
26
|
*
|
|
27
27
|
* Entries are the KaTeX command without its backslash (`exp`, not `\exp`) for
|
|
28
28
|
* `op` nodes, and the verbatim body for `\operatorname{…}` nodes (`Var`).
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* always a course-specific named quantity (`KE`, `PV`, `Var`-as-a-label) that
|
|
16
16
|
* genuinely benefits from a hover card, so the shipped baseline holds only
|
|
17
17
|
* the qualifiers that function like an operator's extremum subscript
|
|
18
|
-
* (`x_{\mathrm{opt}}`) rather than naming a quantity of their own.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* (`x_{\mathrm{opt}}`) rather than naming a quantity of their own. That
|
|
19
|
+
* clears rule N10 only: the indexed symbol still needs a card (rule N23).
|
|
20
|
+
* A course adds a real one via `rules.N10.allow` in its course config, the
|
|
21
|
+
* same shape as `rules.N8.allow`.
|
|
21
22
|
*/
|
|
22
23
|
export const BASE_ROMAN_LABELS = Object.freeze(new Set(['opt']));
|
|
23
24
|
|
|
@@ -131,3 +131,77 @@ export function formatSourceLabel(record) {
|
|
|
131
131
|
.filter(Boolean)
|
|
132
132
|
.join(', ');
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* One `[@…]` bracket: `[@id]`, `[@id; locator]`, or several citations as
|
|
137
|
+
* Pandoc writes them, `[@a; @b; p. 7]`. Group 1 is the first id, group 2 the
|
|
138
|
+
* rest after the first `;`, read by {@link bracketCitations}.
|
|
139
|
+
*/
|
|
140
|
+
export const CITATION_BRACKET = /\[@([a-z0-9][a-z0-9.-]*)(?:;[ \t\r\n]*([^\]]*))?\]/g;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The citations one bracket holds, in order, as `{ id, locator }`. A
|
|
144
|
+
* `;`-separated part that is only `@id` starts another citation (Pandoc's
|
|
145
|
+
* `[@a; @b]`); any other part belongs to the locator of the citation before
|
|
146
|
+
* it, so a locator may itself contain `;` (`[@a; §1.2; eq. 3]`).
|
|
147
|
+
*
|
|
148
|
+
* @param {string} firstId
|
|
149
|
+
* @param {string | undefined} rest
|
|
150
|
+
* @returns {{ id: string, locator: string }[]}
|
|
151
|
+
*/
|
|
152
|
+
export function bracketCitations(firstId, rest) {
|
|
153
|
+
const citations = [{ id: String(firstId).trim(), parts: [] }];
|
|
154
|
+
for (const part of String(rest ?? '').split(';')) {
|
|
155
|
+
const next = /^\s*@([a-z0-9][a-z0-9.-]*)\s*$/.exec(part);
|
|
156
|
+
if (next) citations.push({ id: next[1], parts: [] });
|
|
157
|
+
else if (part.trim()) citations[citations.length - 1].parts.push(part);
|
|
158
|
+
}
|
|
159
|
+
return citations.map(({ id, parts }) => ({
|
|
160
|
+
id,
|
|
161
|
+
locator: parts.join(';').replace(/\s+/g, ' ').trim(),
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Every source id a text cites, `[@a; @b]` included. */
|
|
166
|
+
export function citedIds(text) {
|
|
167
|
+
const ids = new Set();
|
|
168
|
+
for (const match of String(text).matchAll(CITATION_BRACKET)) {
|
|
169
|
+
for (const { id } of bracketCitations(match[1] ?? '', match[2])) ids.add(id);
|
|
170
|
+
}
|
|
171
|
+
return ids;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A Markdown link whose text holds a citation, `[as measured in [@id]](url)`:
|
|
176
|
+
* remark-citation leaves link text alone (a link cannot hold the marker's
|
|
177
|
+
* link), so the citation would print as typed. Group 1 is the link text.
|
|
178
|
+
*/
|
|
179
|
+
const LINK_WITH_CITATION = /\[((?:[^[\]\n]|\[[^[\]\n]*\])*?\[@[^\]\n]*\](?:[^[\]\n]|\[[^[\]\n]*\])*)\]\(/g;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Each citation written inside a link's text, as `{ index, text }`: the
|
|
183
|
+
* offset of its `[@` in `text`, and the citation as written.
|
|
184
|
+
*/
|
|
185
|
+
export function citationsInLinks(text) {
|
|
186
|
+
const found = [];
|
|
187
|
+
for (const link of String(text).matchAll(LINK_WITH_CITATION)) {
|
|
188
|
+
const start = (link.index ?? 0) + 1;
|
|
189
|
+
for (const citation of (link[1] ?? '').matchAll(/\[@[^\]\n]*\]/g)) {
|
|
190
|
+
found.push({ index: start + (citation.index ?? 0), text: citation[0] });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return found;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `text` with every citation inside a link's text blanked, offsets kept: what
|
|
198
|
+
* the page cites, for a scan that counts a lesson's sources.
|
|
199
|
+
*/
|
|
200
|
+
export function withoutLinkCitations(text) {
|
|
201
|
+
let out = String(text);
|
|
202
|
+
for (const { index, text: citation } of citationsInLinks(out)) {
|
|
203
|
+
out = out.slice(0, index) + citation.replace(/[^\n]/g, ' ') + out.slice(index + citation.length);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
@@ -12,9 +12,7 @@
|
|
|
12
12
|
* (a) glyph-unique-in-corpus one rendered glyph → one meaning corpus-wide
|
|
13
13
|
* (b) notation-units / units-vocab every entry has units or `dimensionless`,
|
|
14
14
|
* unit strings drawn from a controlled vocab
|
|
15
|
-
* (
|
|
16
|
-
* lesson uses resolves to one declaration
|
|
17
|
-
* (f) weak-local a `notation.local` symbol used only in its
|
|
15
|
+
* (f) weak-local a `notation.local` symbol used only in its
|
|
18
16
|
* own defining equation and prose
|
|
19
17
|
* (g) notation-source-locator every notation `sources` entry has a locator
|
|
20
18
|
* (h) operator-explained a named operator a lesson uses is on the
|
|
@@ -39,18 +37,17 @@
|
|
|
39
37
|
|
|
40
38
|
import type {
|
|
41
39
|
CourseAccents,
|
|
42
|
-
CourseConventions,
|
|
43
40
|
CourseOperators,
|
|
44
41
|
CourseRomanLabels,
|
|
45
42
|
} from '../compiler/course-config';
|
|
46
|
-
import type { CurriculumCatalog } from '../curriculum/validation';
|
|
47
43
|
import type { NotationSpec } from '../compiler/collections';
|
|
48
44
|
import type { LintIgnore } from './lint-ignore';
|
|
49
45
|
import { effectiveAccentAllowlist } from './base-accents.mjs';
|
|
50
46
|
import { effectiveOperatorAllowlist } from './base-operators.mjs';
|
|
51
47
|
import { effectiveRomanLabelAllowlist } from './base-roman-labels.mjs';
|
|
52
48
|
import { stripEquationLabels } from './equations.mjs';
|
|
53
|
-
import { pageScope } from './gate-math';
|
|
49
|
+
import { pageScope, scopeEntry, type ScopeEntry } from './gate-math';
|
|
50
|
+
import { isIndexEntryLatex } from '../compiler/content-schemas';
|
|
54
51
|
import {
|
|
55
52
|
collectDecorations,
|
|
56
53
|
collectOperators,
|
|
@@ -65,12 +62,12 @@ import type {
|
|
|
65
62
|
NotationRegistry,
|
|
66
63
|
NotationRegistryInput,
|
|
67
64
|
} from './types';
|
|
65
|
+
import { blankPropStrings } from './gate-prop-text';
|
|
68
66
|
|
|
69
67
|
export type ConsistencyCode =
|
|
70
68
|
| 'glyph-unique-in-corpus'
|
|
71
69
|
| 'notation-units'
|
|
72
70
|
| 'units-vocab'
|
|
73
|
-
| 'convention-single-definition'
|
|
74
71
|
| 'weak-local'
|
|
75
72
|
| 'notation-source-locator'
|
|
76
73
|
| 'gloss-wants-promoting'
|
|
@@ -79,6 +76,7 @@ export type ConsistencyCode =
|
|
|
79
76
|
| 'operator-explained'
|
|
80
77
|
| 'roman-label-explained'
|
|
81
78
|
| 'accent-explained'
|
|
79
|
+
| 'accent-swallowed'
|
|
82
80
|
| 'operator-suspended'
|
|
83
81
|
| 'operator-suspension-reason'
|
|
84
82
|
| 'operator-shadowed';
|
|
@@ -100,10 +98,7 @@ export interface ConsistencyInput {
|
|
|
100
98
|
readonly registry: NotationRegistry;
|
|
101
99
|
readonly notationInput: NotationRegistryInput;
|
|
102
100
|
readonly specs: readonly NotationSpec[];
|
|
103
|
-
readonly catalog: CurriculumCatalog;
|
|
104
101
|
readonly lintIgnore: LintIgnore;
|
|
105
|
-
/** Convention keys this course anchors on (from `content/course.config.ts`). */
|
|
106
|
-
readonly conventions: CourseConventions;
|
|
107
102
|
/** This course's edits to the named-operator baseline (rule N8). */
|
|
108
103
|
readonly operators?: CourseOperators;
|
|
109
104
|
/** This course's edits to the roman-label baseline (rule N10). */
|
|
@@ -112,9 +107,6 @@ export interface ConsistencyInput {
|
|
|
112
107
|
readonly accents?: CourseAccents;
|
|
113
108
|
}
|
|
114
109
|
|
|
115
|
-
const CONVENTION_RE =
|
|
116
|
-
/\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
|
|
117
|
-
|
|
118
110
|
// --- shared text helpers ------------------------------------------------
|
|
119
111
|
|
|
120
112
|
function stripCode(text: string): string {
|
|
@@ -128,13 +120,14 @@ interface MathSpan {
|
|
|
128
120
|
}
|
|
129
121
|
|
|
130
122
|
/**
|
|
131
|
-
* Every `$$…$$` then `$…$` span in `body`, outside code
|
|
123
|
+
* Every `$$…$$` then `$…$` span in `body`, outside code and quoted JSX props
|
|
124
|
+
* (plain text; invariant `prop-text`). A `\label{eq:…}`
|
|
132
125
|
* (D7) is stripped: it is an identity marker KaTeX cannot parse, and the
|
|
133
126
|
* scans below treat an unparseable span as having nothing in it — so left in,
|
|
134
127
|
* every labelled display equation was invisible to rules N8, N10 and N22.
|
|
135
128
|
*/
|
|
136
129
|
function mathSpans(body: string): MathSpan[] {
|
|
137
|
-
const searchable = stripCode(body);
|
|
130
|
+
const searchable = stripCode(blankPropStrings(body));
|
|
138
131
|
const spans: MathSpan[] = [];
|
|
139
132
|
for (const match of searchable.matchAll(/\$\$([\s\S]+?)\$\$/g)) {
|
|
140
133
|
spans.push({ latex: stripEquationLabels(match[1] ?? '') });
|
|
@@ -217,6 +210,9 @@ function checkNotationUnits(
|
|
|
217
210
|
out: ConsistencyDiagnostic[],
|
|
218
211
|
): void {
|
|
219
212
|
for (const spec of input.specs) {
|
|
213
|
+
// An index entry (`{}_0`, rule N23) names a subscript, which has no
|
|
214
|
+
// unit; its derived cards take the indexed symbol's.
|
|
215
|
+
if (isIndexEntryLatex(spec.latex)) continue;
|
|
220
216
|
if (spec.units === undefined && !spec.dimensionless) {
|
|
221
217
|
if (input.lintIgnore.matches('notation-units', spec.key)) continue;
|
|
222
218
|
out.push({
|
|
@@ -244,42 +240,6 @@ function checkNotationUnits(
|
|
|
244
240
|
}
|
|
245
241
|
}
|
|
246
242
|
|
|
247
|
-
function checkConventionSingleDefinition(
|
|
248
|
-
input: ConsistencyInput,
|
|
249
|
-
out: ConsistencyDiagnostic[],
|
|
250
|
-
): void {
|
|
251
|
-
const lessonById = new Map(
|
|
252
|
-
input.catalog.lessons.map((lesson) => [lesson.id, lesson]),
|
|
253
|
-
);
|
|
254
|
-
for (const lesson of input.notationInput.lessons) {
|
|
255
|
-
const body = lesson.body;
|
|
256
|
-
if (!CONVENTION_RE.test(body)) continue;
|
|
257
|
-
const meta = lessonById.get(lesson.lessonId);
|
|
258
|
-
const { signConventionKey, cashFlowPerspectiveCompetency } =
|
|
259
|
-
input.conventions;
|
|
260
|
-
const anchoredByCompetency = [
|
|
261
|
-
...(meta?.teaches ?? []),
|
|
262
|
-
...(meta?.requires ?? []),
|
|
263
|
-
].includes(cashFlowPerspectiveCompetency);
|
|
264
|
-
const anchoredByTerm = new RegExp(
|
|
265
|
-
`\\[\\[\\s*${signConventionKey}\\s*\\]\\]`,
|
|
266
|
-
).test(body);
|
|
267
|
-
if (anchoredByCompetency || anchoredByTerm) continue;
|
|
268
|
-
if (
|
|
269
|
-
input.lintIgnore.matches('convention-single-definition', lesson.lessonId)
|
|
270
|
-
) {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
out.push({
|
|
274
|
-
code: 'convention-single-definition',
|
|
275
|
-
severity: 'warning',
|
|
276
|
-
lessonId: lesson.lessonId,
|
|
277
|
-
file: lesson.source.file,
|
|
278
|
-
message: `${lesson.lessonId} states a sign / cash-flow-perspective convention in prose but does not resolve it to a single declaration ([[${signConventionKey}]] or ${cashFlowPerspectiveCompetency})`,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
243
|
function checkWeakLocal(
|
|
284
244
|
input: ConsistencyInput,
|
|
285
245
|
out: ConsistencyDiagnostic[],
|
|
@@ -471,7 +431,7 @@ function checkGlossNameShape(
|
|
|
471
431
|
|
|
472
432
|
/**
|
|
473
433
|
* A named operator a lesson uses is either universal — on the shipped
|
|
474
|
-
* `BASE_OPERATORS` baseline plus this course's `
|
|
434
|
+
* `BASE_OPERATORS` baseline plus this course's `rules.N8.allow`, minus its
|
|
475
435
|
* `operators.deny` — or it is explained: a notation card or `notation.local`
|
|
476
436
|
* entry the lesson binds with `[[key]]` whose `latex` is that operator, so the
|
|
477
437
|
* reader can hover it. Anything else (`\operatorname{Var}` with no entry, or a
|
|
@@ -530,7 +490,7 @@ function checkOperatorExplained(
|
|
|
530
490
|
lessonId: lesson.lessonId,
|
|
531
491
|
file: lesson.source.file,
|
|
532
492
|
glyph: operator.name,
|
|
533
|
-
message: `${lesson.lessonId}: operator ${JSON.stringify(display)} is neither on the course operator allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(display)}) introduced with [[key]], or add ${JSON.stringify(operator.name)} to
|
|
493
|
+
message: `${lesson.lessonId}: operator ${JSON.stringify(display)} is neither on the course operator allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(display)}) introduced with [[key]], or add ${JSON.stringify(operator.name)} to rules.N8.allow in the course config (course.config.ts, or the course.settings.json it reads)`,
|
|
534
494
|
});
|
|
535
495
|
}
|
|
536
496
|
}
|
|
@@ -590,20 +550,18 @@ function checkRomanLabelExplained(
|
|
|
590
550
|
lessonId: lesson.lessonId,
|
|
591
551
|
file: lesson.source.file,
|
|
592
552
|
glyph: label.name,
|
|
593
|
-
message: `${lesson.lessonId}: roman label ${JSON.stringify(`\\mathrm{${label.name}}`)} is neither on the course roman-label allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(`\\mathrm{${label.name}}`)}) introduced with [[key]], or add ${JSON.stringify(label.name)} to
|
|
553
|
+
message: `${lesson.lessonId}: roman label ${JSON.stringify(`\\mathrm{${label.name}}`)} is neither on the course roman-label allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(`\\mathrm{${label.name}}`)}) introduced with [[key]], or add ${JSON.stringify(label.name)} to rules.N10.allow in the course config (course.config.ts, or the course.settings.json it reads)`,
|
|
594
554
|
});
|
|
595
555
|
}
|
|
596
556
|
}
|
|
597
557
|
}
|
|
598
558
|
}
|
|
599
559
|
|
|
600
|
-
function sharedScopeByKey(
|
|
601
|
-
input: ConsistencyInput,
|
|
602
|
-
): Map<string, { key: string; notation: string }> {
|
|
560
|
+
function sharedScopeByKey(input: ConsistencyInput): Map<string, ScopeEntry> {
|
|
603
561
|
return new Map(
|
|
604
562
|
input.notationInput.sharedDefinitions.map((definition) => [
|
|
605
563
|
definition.key,
|
|
606
|
-
|
|
564
|
+
scopeEntry(definition),
|
|
607
565
|
]),
|
|
608
566
|
);
|
|
609
567
|
}
|
|
@@ -633,11 +591,40 @@ function checkAccentExplained(
|
|
|
633
591
|
for (const span of mathSpans(lesson.body)) {
|
|
634
592
|
let decorations;
|
|
635
593
|
try {
|
|
636
|
-
({ decorations } = resolveMathGlyphs(span.latex, scope, {
|
|
594
|
+
({ decorations } = resolveMathGlyphs(span.latex, scope, {
|
|
595
|
+
suspended,
|
|
596
|
+
derive: true,
|
|
597
|
+
}));
|
|
637
598
|
} catch {
|
|
638
599
|
continue;
|
|
639
600
|
}
|
|
640
601
|
for (const decoration of decorations) {
|
|
602
|
+
// An author's `\explain{key}{\hat{\beta}}` counts as the whole form
|
|
603
|
+
// only when `key`'s own `latex` carries the decoration: the `beta`
|
|
604
|
+
// card says nothing about a hat (the twin of rule N23's
|
|
605
|
+
// `index-swallowed`).
|
|
606
|
+
if (decoration.wholeForm !== null && decoration.wholeFormExplicit) {
|
|
607
|
+
const notation =
|
|
608
|
+
scope.find((entry) => entry.key === decoration.wholeForm)?.notation ?? '';
|
|
609
|
+
const carries = collectDecorations(notation).some(
|
|
610
|
+
(item) => item.name === decoration.name,
|
|
611
|
+
);
|
|
612
|
+
const id = `swallowed:${decoration.token}`;
|
|
613
|
+
if (!carries && !reported.has(id)) {
|
|
614
|
+
reported.add(id);
|
|
615
|
+
if (!input.lintIgnore.matches('accent-swallowed', decoration.name)) {
|
|
616
|
+
out.push({
|
|
617
|
+
code: 'accent-swallowed',
|
|
618
|
+
severity: 'warning',
|
|
619
|
+
lessonId: lesson.lessonId,
|
|
620
|
+
file: lesson.source.file,
|
|
621
|
+
glyph: decoration.name,
|
|
622
|
+
message: `${lesson.lessonId}: \\explain{${decoration.wholeForm}}{${decoration.token}} wraps the decoration ${JSON.stringify(decoration.name)} in ${decoration.wholeForm}, whose latex (${notation}) has none, so its card cannot explain it; wrap only the symbol under it and give the decoration an operator entry (latex ${JSON.stringify(operatorEntryLatex(decoration.name))}), or name an entry for the decorated form (latex ${JSON.stringify(decoration.token)})`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
641
628
|
if (decoration.wholeForm !== null || decoration.operator !== null) continue;
|
|
642
629
|
// A suspended decoration never composes; the gate fails it instead.
|
|
643
630
|
if (suspended.includes(decoration.name)) continue;
|
|
@@ -737,7 +724,6 @@ const CHECKS: ((
|
|
|
737
724
|
) => void)[] = [
|
|
738
725
|
checkGlyphUniqueness,
|
|
739
726
|
checkNotationUnits,
|
|
740
|
-
checkConventionSingleDefinition,
|
|
741
727
|
checkWeakLocal,
|
|
742
728
|
checkNotationSourceLocator,
|
|
743
729
|
checkGlossWantsPromoting,
|
|
@@ -777,7 +763,6 @@ export function consistencyCounts(
|
|
|
777
763
|
'glyph-unique-in-corpus': 0,
|
|
778
764
|
'notation-units': 0,
|
|
779
765
|
'units-vocab': 0,
|
|
780
|
-
'convention-single-definition': 0,
|
|
781
766
|
'weak-local': 0,
|
|
782
767
|
'notation-source-locator': 0,
|
|
783
768
|
'gloss-wants-promoting': 0,
|
|
@@ -786,6 +771,7 @@ export function consistencyCounts(
|
|
|
786
771
|
'operator-explained': 0,
|
|
787
772
|
'roman-label-explained': 0,
|
|
788
773
|
'accent-explained': 0,
|
|
774
|
+
'accent-swallowed': 0,
|
|
789
775
|
'operator-suspended': 0,
|
|
790
776
|
'operator-suspension-reason': 0,
|
|
791
777
|
'operator-shadowed': 0,
|
|
@@ -6,24 +6,16 @@ import type {
|
|
|
6
6
|
NotationRegistry,
|
|
7
7
|
} from './types';
|
|
8
8
|
|
|
9
|
-
interface EditorialCompetency {
|
|
10
|
-
readonly id: string;
|
|
11
|
-
readonly editorialStatus?: 'draft' | 'in-review' | 'reviewed';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
export function validateNotationAlignment(
|
|
15
10
|
registry: NotationRegistry,
|
|
16
11
|
catalog: CurriculumCatalog,
|
|
17
12
|
): readonly NotationDiagnostic[] {
|
|
18
13
|
const diagnostics: NotationDiagnostic[] = [];
|
|
19
14
|
const competencies = new Map(
|
|
20
|
-
|
|
21
|
-
entry.id,
|
|
22
|
-
entry,
|
|
23
|
-
]),
|
|
15
|
+
catalog.competencies.map((entry) => [entry.id, entry]),
|
|
24
16
|
);
|
|
25
17
|
const lessons = new Map(catalog.lessons.map((entry) => [entry.id, entry]));
|
|
26
|
-
const sources = new
|
|
18
|
+
const sources = new Set(catalog.sources.map((entry) => entry.id));
|
|
27
19
|
const trackPositions = buildTrackPositions(catalog, diagnostics);
|
|
28
20
|
|
|
29
21
|
for (const definition of registry.definitions) {
|
|
@@ -82,20 +74,6 @@ export function validateNotationAlignment(
|
|
|
82
74
|
trackPositions,
|
|
83
75
|
diagnostics,
|
|
84
76
|
);
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
definition.status === 'reviewed' &&
|
|
88
|
-
(lesson.status !== 'reviewed' ||
|
|
89
|
-
competency?.editorialStatus !== 'reviewed')
|
|
90
|
-
) {
|
|
91
|
-
diagnostics.push(
|
|
92
|
-
error(
|
|
93
|
-
'alignment-review-state',
|
|
94
|
-
`${definition.id} is reviewed while its owning lesson or competency is not reviewed`,
|
|
95
|
-
definition,
|
|
96
|
-
),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
77
|
}
|
|
100
78
|
|
|
101
79
|
return diagnostics.sort(compareDiagnostics);
|
|
@@ -177,15 +155,11 @@ function validateIntroductionOrder(
|
|
|
177
155
|
|
|
178
156
|
function validateSources(
|
|
179
157
|
definition: NotationDefinitionRecord,
|
|
180
|
-
sources:
|
|
181
|
-
string,
|
|
182
|
-
{ readonly editorialStatus: 'draft' | 'in-review' | 'reviewed' }
|
|
183
|
-
>,
|
|
158
|
+
sources: ReadonlySet<string>,
|
|
184
159
|
diagnostics: NotationDiagnostic[],
|
|
185
160
|
): void {
|
|
186
161
|
for (const sourceId of definition.sources) {
|
|
187
|
-
|
|
188
|
-
if (!source) {
|
|
162
|
+
if (!sources.has(sourceId)) {
|
|
189
163
|
diagnostics.push(
|
|
190
164
|
error(
|
|
191
165
|
'alignment-unavailable',
|
|
@@ -193,17 +167,6 @@ function validateSources(
|
|
|
193
167
|
definition,
|
|
194
168
|
),
|
|
195
169
|
);
|
|
196
|
-
} else if (
|
|
197
|
-
definition.status === 'reviewed' &&
|
|
198
|
-
source.editorialStatus !== 'reviewed'
|
|
199
|
-
) {
|
|
200
|
-
diagnostics.push(
|
|
201
|
-
error(
|
|
202
|
-
'alignment-review-state',
|
|
203
|
-
`${definition.id} is reviewed while source ${sourceId} is ${source.editorialStatus}`,
|
|
204
|
-
definition,
|
|
205
|
-
),
|
|
206
|
-
);
|
|
207
170
|
}
|
|
208
171
|
}
|
|
209
172
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived cards (rule N23) as the page payload carries them: each derived
|
|
3
|
+
* card a page's math uses, composed from its base and index entries by
|
|
4
|
+
* `derived-notation.mjs`, with the glyph table its formula resolves against.
|
|
5
|
+
* The manifest builds the ones lesson-body and assessment math use; the page
|
|
6
|
+
* payload (`page-notation.ts`) builds the ones a lab or a diagram registers
|
|
7
|
+
* while it renders, the same way.
|
|
8
|
+
*/
|
|
9
|
+
import type { ManifestDerivedNotation } from '../compiler/manifest';
|
|
10
|
+
import { composeDerivedEntry } from './derived-notation.mjs';
|
|
11
|
+
import { scopeEntry } from './gate-math';
|
|
12
|
+
import { formulaExplainKeys, type GlyphScopeEntry } from './gloss';
|
|
13
|
+
import { baseSymbolLatex, derivedNotationLatex } from './math-glyphs.mjs';
|
|
14
|
+
import type { DerivedUse, NotationDefinitionRecord } from './types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The derived cards for `uses`, sorted by key. `records` holds the page's
|
|
18
|
+
* entries by key, local ones winning; a use whose base or index entry is not
|
|
19
|
+
* there is skipped. A card's formula resolves against the derived symbol,
|
|
20
|
+
* the index entry's glosses, the base, and the cards the formula names.
|
|
21
|
+
*/
|
|
22
|
+
export function derivedNotationEntries(
|
|
23
|
+
uses: readonly DerivedUse[],
|
|
24
|
+
records: ReadonlyMap<string, NotationDefinitionRecord>,
|
|
25
|
+
cardsByKey: ReadonlyMap<string, GlyphScopeEntry>,
|
|
26
|
+
): ManifestDerivedNotation[] {
|
|
27
|
+
const derived = new Map<string, ManifestDerivedNotation>();
|
|
28
|
+
for (const use of uses) {
|
|
29
|
+
if (derived.has(use.key)) continue;
|
|
30
|
+
const base = records.get(use.base);
|
|
31
|
+
const index = records.get(use.index);
|
|
32
|
+
if (!base || !index) continue;
|
|
33
|
+
const notation = derivedNotationLatex(base.notation, index.notation);
|
|
34
|
+
const entry = composeDerivedEntry({
|
|
35
|
+
key: use.key,
|
|
36
|
+
notation,
|
|
37
|
+
baseLatex: baseSymbolLatex(base.notation),
|
|
38
|
+
base,
|
|
39
|
+
index: {
|
|
40
|
+
...index,
|
|
41
|
+
...(index.formulaTemplate === undefined ? {} : { formula: index.formulaTemplate }),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const formulaScope: GlyphScopeEntry[] = [
|
|
45
|
+
{ key: use.key, notation },
|
|
46
|
+
...index.glosses.map((gloss) => ({ key: gloss.key, notation: gloss.notation })),
|
|
47
|
+
scopeEntry(base),
|
|
48
|
+
];
|
|
49
|
+
for (const key of formulaExplainKeys(entry.formula ?? '')) {
|
|
50
|
+
if (formulaScope.some((row) => row.key === key)) continue;
|
|
51
|
+
const card = records.get(key) ?? cardsByKey.get(key);
|
|
52
|
+
if (card) formulaScope.push(scopeEntry(card));
|
|
53
|
+
}
|
|
54
|
+
derived.set(use.key, {
|
|
55
|
+
key: entry.key,
|
|
56
|
+
base: entry.base,
|
|
57
|
+
index: entry.index,
|
|
58
|
+
notation: entry.notation,
|
|
59
|
+
label: entry.label,
|
|
60
|
+
summary: entry.summary,
|
|
61
|
+
glosses: [...index.glosses],
|
|
62
|
+
formulaScope,
|
|
63
|
+
...(entry.units === undefined ? {} : { units: entry.units }),
|
|
64
|
+
...(entry.formula === undefined ? {} : { formula: entry.formula }),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return [...derived.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
68
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived notation entries (rule N23): the card for a symbol with a subscript
|
|
3
|
+
* that an **index entry** explains — `\mathbf{v}_0` from the base entry
|
|
4
|
+
* `velocity-vector` (`\mathbf{v}`) and the index entry `motion.initial`
|
|
5
|
+
* (`{}_0`, "Initial value").
|
|
6
|
+
*
|
|
7
|
+
* The resolver writes one `\explain` around the whole indexed symbol, with
|
|
8
|
+
* the key `derivedNotationKey(base, index)` (`math-glyphs.mjs`); the build
|
|
9
|
+
* materializes the entry from the two, here, so that everything downstream
|
|
10
|
+
* treats it as one more card. What the reader sees first is about the
|
|
11
|
+
* indexed symbol; the base's own card is one level deeper, through the base
|
|
12
|
+
* symbol on the card's formula line:
|
|
13
|
+
*
|
|
14
|
+
* | Field | From |
|
|
15
|
+
* | ------- | ------------------------------------------- |
|
|
16
|
+
* | symbol | the base's symbol plus the index |
|
|
17
|
+
* | title | the index entry's `reads`, the base's label |
|
|
18
|
+
* | summary | the index entry's `meaning`, never the base's |
|
|
19
|
+
* | formula | the index entry's `formula`, `{base}` bound |
|
|
20
|
+
* | units | the base entry |
|
|
21
|
+
*
|
|
22
|
+
* Plain JavaScript with no parser, so the remark pass and the manifest share
|
|
23
|
+
* it; `derivedNotationLatex` in `math-glyphs.mjs` builds the symbol.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The indexed symbol, in an index entry's `reads` and `formula`. */
|
|
27
|
+
export const BASE_PLACEHOLDER = '{base}';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A label as it reads inside a sentence: its first letter lowercased unless
|
|
31
|
+
* the first word is an acronym (`OLS estimate`) — the same rule
|
|
32
|
+
* `composeHeaderLabel` applies to an operator's operand.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} label
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function inSentence(label) {
|
|
38
|
+
const [first = ''] = label.split(/\s+/u);
|
|
39
|
+
if (first.length > 1 && first === first.toUpperCase()) return label;
|
|
40
|
+
return label.charAt(0).toLowerCase() + label.slice(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A derived card's title: the index entry's `reads` with `{base}` replaced by
|
|
45
|
+
* the base's label (lowercased in mid-sentence), or `"<index label> of
|
|
46
|
+
* <base label>"` when the index entry gives no `reads`.
|
|
47
|
+
*
|
|
48
|
+
* @param {{ label: string, reads?: string }} index
|
|
49
|
+
* @param {string} baseLabel
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function derivedLabel(index, baseLabel) {
|
|
53
|
+
const reads = index.reads ?? `${index.label} of ${BASE_PLACEHOLDER}`;
|
|
54
|
+
const at = reads.indexOf(BASE_PLACEHOLDER);
|
|
55
|
+
const filled = reads
|
|
56
|
+
.split(BASE_PLACEHOLDER)
|
|
57
|
+
.join(at === 0 ? baseLabel : inSentence(baseLabel));
|
|
58
|
+
// `{base}` first in a later position, or a `reads` that starts lowercase.
|
|
59
|
+
return filled.charAt(0).toUpperCase() + filled.slice(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* An index entry's formula with `{base}` bound: to the base symbol under its
|
|
64
|
+
* own `\explain`, for a derived card, so the reader opens the base's card
|
|
65
|
+
* from it.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} formula
|
|
68
|
+
* @param {string} baseKey
|
|
69
|
+
* @param {string} baseLatex
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function derivedFormula(formula, baseKey, baseLatex) {
|
|
73
|
+
return formula.split(BASE_PLACEHOLDER).join(`\\explain{${baseKey}}{${baseLatex}}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* An index entry's own formula, shown on the index entry's card and checked
|
|
78
|
+
* by the formula gate: `{base}` becomes a placeholder square bound to the
|
|
79
|
+
* entry itself, as an operator entry is shown over a square.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} formula
|
|
82
|
+
* @param {string} ownKey
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function indexEntryFormula(formula, ownKey) {
|
|
86
|
+
return formula.split(BASE_PLACEHOLDER).join(`\\explain{${ownKey}}{\\square}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {object} DerivedSource
|
|
91
|
+
* @property {string} key
|
|
92
|
+
* @property {string} notation
|
|
93
|
+
* @property {string} label
|
|
94
|
+
* @property {string} [summary]
|
|
95
|
+
* @property {string} [units]
|
|
96
|
+
* @property {string} [reads]
|
|
97
|
+
* @property {string} [formula]
|
|
98
|
+
* @property {readonly { key: string, notation: string, label: string, units?: string, ownerKey: string }[]} [glosses]
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The derived entry for one (base, index) pair. `notation` is the indexed
|
|
103
|
+
* symbol, from `derivedNotationLatex`.
|
|
104
|
+
*
|
|
105
|
+
* @param {{ key: string, notation: string, baseLatex: string, base: DerivedSource, index: DerivedSource }} input
|
|
106
|
+
*/
|
|
107
|
+
export function composeDerivedEntry({ key, notation, baseLatex, base, index }) {
|
|
108
|
+
return {
|
|
109
|
+
key,
|
|
110
|
+
notation,
|
|
111
|
+
base: base.key,
|
|
112
|
+
index: index.key,
|
|
113
|
+
label: derivedLabel(index, base.label),
|
|
114
|
+
summary: index.summary ?? '',
|
|
115
|
+
glosses: [...(index.glosses ?? [])],
|
|
116
|
+
...(base.units === undefined ? {} : { units: base.units }),
|
|
117
|
+
...(index.formula === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: { formula: derivedFormula(index.formula, base.key, baseLatex) }),
|
|
120
|
+
};
|
|
121
|
+
}
|