explico 0.4.0 → 0.5.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 +273 -1
- package/README.md +103 -8
- package/dist/compiler/astro.d.ts +9 -1
- package/dist/compiler/astro.js +182 -3
- package/dist/compiler/cli.d.ts +13 -0
- package/dist/compiler/cli.js +28 -4
- package/dist/compiler/collections.d.ts +3 -2
- package/dist/compiler/collections.js +28 -1
- package/dist/compiler/course-config.d.ts +58 -1
- package/dist/compiler/course-config.js +28 -1
- package/dist/compiler/exports/standalone-html.d.ts +34 -0
- package/dist/compiler/exports/standalone-html.js +803 -0
- package/dist/compiler/manifest.d.ts +90 -6
- package/dist/compiler/manifest.js +150 -17
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-abbreviations.d.mts +30 -0
- package/dist/reference/base-abbreviations.mjs +60 -0
- package/dist/reference/base-operators.d.mts +15 -0
- package/dist/reference/base-operators.mjs +40 -0
- package/dist/reference/base-roman-labels.d.mts +35 -0
- package/dist/reference/base-roman-labels.mjs +43 -0
- package/dist/reference/consistency.d.ts +8 -2
- package/dist/reference/consistency.js +50 -3
- package/dist/reference/diagrams-validate.d.ts +21 -0
- package/dist/reference/diagrams-validate.js +13 -0
- package/dist/reference/equations.d.mts +10 -15
- package/dist/reference/equations.mjs +67 -14
- package/dist/reference/gate-abbreviations.d.ts +17 -0
- package/dist/reference/gate-abbreviations.js +64 -0
- package/dist/reference/gate-math.d.ts +28 -5
- package/dist/reference/gate-math.js +5 -2
- package/dist/reference/gate-prose-notation.d.ts +60 -0
- package/dist/reference/gate-prose-notation.js +125 -0
- package/dist/reference/keyed-float-validate.d.ts +5 -5
- package/dist/reference/keyed-jsx-floats.d.mts +7 -12
- package/dist/reference/keyed-jsx-floats.mjs +50 -25
- package/dist/reference/knowledge-checks-validate.d.ts +35 -0
- package/dist/reference/knowledge-checks-validate.js +78 -0
- package/dist/reference/knowledge-checks.d.mts +25 -0
- package/dist/reference/knowledge-checks.mjs +45 -0
- package/dist/reference/lab-math-scope.d.ts +9 -1
- package/dist/reference/lab-math-scope.js +32 -4
- package/dist/reference/math-glyphs.d.mts +30 -0
- package/dist/reference/math-glyphs.mjs +367 -0
- package/dist/reference/mdx-attributes.d.mts +13 -0
- package/dist/reference/mdx-attributes.mjs +38 -0
- package/dist/reference/mixed-content.d.ts +17 -0
- package/dist/reference/mixed-content.js +29 -0
- package/dist/reference/page-notation.d.ts +52 -0
- package/dist/reference/page-notation.js +171 -0
- package/dist/reference/raw-floats-validate.d.ts +18 -0
- package/dist/reference/raw-floats-validate.js +53 -0
- package/dist/reference/raw-floats.d.mts +18 -0
- package/dist/reference/raw-floats.mjs +129 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -1
- package/dist/reference/rehype-heading-anchors.mjs +107 -1
- package/dist/reference/rehype-table-header-labels.d.mts +26 -0
- package/dist/reference/rehype-table-header-labels.mjs +152 -0
- package/dist/reference/remark-abbreviations.d.mts +7 -0
- package/dist/reference/remark-abbreviations.mjs +174 -0
- package/dist/reference/remark-memo.d.mts +16 -0
- package/dist/reference/remark-memo.mjs +339 -0
- package/dist/reference/remark-notation.d.mts +57 -9
- package/dist/reference/remark-notation.mjs +354 -42
- package/dist/reference/remark-short-note.d.mts +4 -0
- package/dist/reference/remark-short-note.mjs +237 -0
- package/dist/reference/types.d.ts +16 -1
- package/dist/reference/units-vocab.js +4 -0
- package/docs/VERSIONING.md +33 -4
- package/docs/authoring/README.md +8 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +135 -0
- package/docs/authoring/migrating.md +92 -7
- package/docs/authoring/notation.md +560 -1
- package/docs/authoring/structure.md +120 -8
- package/docs/authoring/visualizations.md +36 -3
- package/package.json +5 -2
- package/src/compiler/astro.ts +208 -3
- package/src/compiler/cli.ts +29 -2
- package/src/compiler/collections.ts +45 -4
- package/src/compiler/course-config.ts +92 -0
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/manifest.ts +268 -22
- package/src/components/HoverPanel.astro +45 -4
- package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
- package/src/components/assessments/AssessmentRunner.tsx +52 -17
- package/src/components/assessments/AssessmentSet.astro +98 -14
- package/src/components/assessments/RenderedContent.tsx +55 -0
- package/src/components/assessments/assessment-randomization.ts +16 -0
- package/src/components/diagram/Diagram.astro +444 -83
- package/src/components/disclosure/hover-preview.ts +202 -0
- package/src/components/disclosure/persisted-disclosure.ts +66 -0
- package/src/components/examples/CompactExamples.astro +2 -0
- package/src/components/figure/Figure.astro +279 -10
- package/src/components/memo/Memo.astro +453 -0
- package/src/components/memo/ShortNote.astro +223 -0
- package/src/components/notation/NotationFormula.astro +18 -1
- package/src/components/notation/NotationLayer.astro +105 -8
- package/src/components/notation/render-notation-math.ts +39 -2
- package/src/components/notation/safe-math-to-react.ts +50 -0
- package/src/components/reference/FloatRefLayer.astro +233 -15
- package/src/components/starlight/CollapsibleSections.astro +461 -0
- package/src/components/starlight/DiagramEnhancer.astro +385 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
- package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
- package/src/components/starlight/LayoutHeader.astro +50 -0
- package/src/components/starlight/LayoutSocialIcons.astro +41 -2
- package/src/components/starlight/LessonExportActions.astro +436 -0
- package/src/components/starlight/LessonFooter.astro +27 -2
- package/src/components/starlight/section-parts.ts +31 -0
- package/src/components/table/Table.astro +293 -7
- package/src/components/table/TableOverflowExpander.astro +489 -0
- package/src/content-config.ts +20 -0
- package/src/reference/base-abbreviations.mjs +60 -0
- package/src/reference/base-operators.mjs +40 -0
- package/src/reference/base-roman-labels.mjs +43 -0
- package/src/reference/consistency.ts +73 -2
- package/src/reference/diagrams-validate.ts +40 -0
- package/src/reference/equations.mjs +67 -14
- package/src/reference/gate-abbreviations.ts +128 -0
- package/src/reference/gate-math.ts +27 -7
- package/src/reference/gate-prose-notation.ts +224 -0
- package/src/reference/keyed-float-validate.ts +5 -5
- package/src/reference/keyed-jsx-floats.mjs +50 -25
- package/src/reference/knowledge-checks-validate.ts +130 -0
- package/src/reference/knowledge-checks.mjs +45 -0
- package/src/reference/lab-math-scope.ts +45 -3
- package/src/reference/math-glyphs.mjs +367 -0
- package/src/reference/mdx-attributes.mjs +38 -0
- package/src/reference/mixed-content.ts +69 -0
- package/src/reference/page-notation.ts +252 -0
- package/src/reference/raw-floats-validate.ts +103 -0
- package/src/reference/raw-floats.mjs +129 -0
- package/src/reference/rehype-heading-anchors.mjs +107 -1
- package/src/reference/rehype-table-header-labels.mjs +152 -0
- package/src/reference/remark-abbreviations.mjs +174 -0
- package/src/reference/remark-memo.mjs +339 -0
- package/src/reference/remark-notation.mjs +354 -42
- package/src/reference/remark-short-note.mjs +237 -0
- package/src/reference/types.ts +16 -0
- package/src/reference/units-vocab.ts +4 -0
- package/src/styles/global.css +996 -50
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import katex from 'katex';
|
|
2
2
|
|
|
3
3
|
import { BASE_LIBRARY } from './base-library.mjs';
|
|
4
|
+
import { PARENTHESIZED_FUNCTIONS } from './base-operators.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Page-level glyph-map resolver (cleanup plan, D1 decision 2026-09-02).
|
|
@@ -181,6 +182,52 @@ function readBracedGroup(value, start) {
|
|
|
181
182
|
return undefined;
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
const MARKER_COMMANDS = [EXPLAIN_COMMAND, '\\slot'];
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Strip Explico's own `\explain{key}{latex}` / `\slot{key}{latex}` markers
|
|
189
|
+
* (see `katex-options.mjs`) down to the LaTeX they wrap, recursively. Lesson
|
|
190
|
+
* math — both a notation card's `formula` and an equation written directly
|
|
191
|
+
* in prose — carries these markers inline so every symbol is hoverable; a
|
|
192
|
+
* reader copying the equation out of this system wants the plain math
|
|
193
|
+
* underneath, not internal notation-key plumbing that means nothing to
|
|
194
|
+
* KaTeX, MathJax, or LaTeX anywhere else. Shared by `render-notation-math.ts`
|
|
195
|
+
* (a notation card's copy button) and `remark-notation.mjs` (a numbered
|
|
196
|
+
* display equation's copy button) — kept here, rather than in either, so
|
|
197
|
+
* neither module has to import the other.
|
|
198
|
+
*/
|
|
199
|
+
export function stripNotationMarkers(latex) {
|
|
200
|
+
let result = '';
|
|
201
|
+
let cursor = 0;
|
|
202
|
+
|
|
203
|
+
while (cursor < latex.length) {
|
|
204
|
+
const command = MARKER_COMMANDS.find(
|
|
205
|
+
(name) =>
|
|
206
|
+
latex.startsWith(name, cursor) &&
|
|
207
|
+
!isEscaped(latex, cursor) &&
|
|
208
|
+
!/[A-Za-z@]/.test(latex[cursor + name.length] ?? ''),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const keyGroup = command
|
|
212
|
+
? readBracedGroup(latex, skipWhitespace(latex, cursor + command.length))
|
|
213
|
+
: undefined;
|
|
214
|
+
const contentGroup = keyGroup
|
|
215
|
+
? readBracedGroup(latex, skipWhitespace(latex, keyGroup.end))
|
|
216
|
+
: undefined;
|
|
217
|
+
|
|
218
|
+
if (contentGroup) {
|
|
219
|
+
result += stripNotationMarkers(contentGroup.content);
|
|
220
|
+
cursor = contentGroup.end;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
result += latex[cursor];
|
|
225
|
+
cursor += 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
184
231
|
/**
|
|
185
232
|
* Every `\explain{key}{latex}` in the source (the escape hatch). Each key must
|
|
186
233
|
* already be in the page glyph table. The returned span points at the wrapped
|
|
@@ -768,6 +815,243 @@ function unresolvedIdentifiers(latex, identifiers, bindings) {
|
|
|
768
815
|
.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
769
816
|
}
|
|
770
817
|
|
|
818
|
+
// --- N13: nonlinear functions take a parenthesized argument --------------
|
|
819
|
+
|
|
820
|
+
// Opening delimiters that visibly bracket a function's argument. A bare `{`
|
|
821
|
+
// (KaTeX `ordgroup`) does not count — it renders no visible mark at all, so
|
|
822
|
+
// `\sin{x}` reads identically to the bare `\sin x` this rule forbids.
|
|
823
|
+
const FUNCTION_ARG_OPENERS = new Set(['(', '[', '\\{']);
|
|
824
|
+
|
|
825
|
+
// Node types that plausibly start a bare (unparenthesized) argument right
|
|
826
|
+
// after a function name — a letter, a digit, an invisible `{…}` group,
|
|
827
|
+
// another function, an exponent/subscript structure, a fraction, a root, or
|
|
828
|
+
// a styled/labelled run (`\mathrm{…}`, `\color{…}{…}`, …). Anything else
|
|
829
|
+
// (a relation, a binary operator, closing punctuation, or nothing at all) is
|
|
830
|
+
// left alone: the function has no argument in this position to complain
|
|
831
|
+
// about.
|
|
832
|
+
const BARE_ARGUMENT_NODE_TYPES = new Set([
|
|
833
|
+
'mathord',
|
|
834
|
+
'textord',
|
|
835
|
+
'ordgroup',
|
|
836
|
+
'op',
|
|
837
|
+
'operatorname',
|
|
838
|
+
'supsub',
|
|
839
|
+
'genfrac',
|
|
840
|
+
'sqrt',
|
|
841
|
+
'font',
|
|
842
|
+
'accent',
|
|
843
|
+
'accentUnder',
|
|
844
|
+
'overline',
|
|
845
|
+
'underline',
|
|
846
|
+
'phantom',
|
|
847
|
+
'raisebox',
|
|
848
|
+
'href',
|
|
849
|
+
'color',
|
|
850
|
+
'styling',
|
|
851
|
+
'mclass',
|
|
852
|
+
]);
|
|
853
|
+
|
|
854
|
+
/** `\,`, `\;`, `\!`, `~`, … — spacing that sits between a function and its
|
|
855
|
+
* argument without being the argument itself. */
|
|
856
|
+
function isSpacingNode(node) {
|
|
857
|
+
return isNode(node) && node.type === 'kern';
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** True when `node` is a visible parenthesis/bracket group opening here. */
|
|
861
|
+
function opensFunctionArgument(node) {
|
|
862
|
+
if (!isNode(node)) return false;
|
|
863
|
+
if (node.type === 'leftright') return true;
|
|
864
|
+
return (
|
|
865
|
+
node.type === 'atom' &&
|
|
866
|
+
node.family === 'open' &&
|
|
867
|
+
FUNCTION_ARG_OPENERS.has(node.text)
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* The bare command name (`sin`, `log`, …) when `node` is a call to one of
|
|
873
|
+
* {@link PARENTHESIZED_FUNCTIONS} — either the `op` node itself, or one
|
|
874
|
+
* wrapped in a `supsub` for `\sin^2 x` / `\log_2 x`.
|
|
875
|
+
*/
|
|
876
|
+
function parenthesizedFunctionName(node) {
|
|
877
|
+
if (!isNode(node)) return null;
|
|
878
|
+
if (node.type === 'op' && typeof node.name === 'string') {
|
|
879
|
+
const name = node.name.replace(/^\\/, '');
|
|
880
|
+
return PARENTHESIZED_FUNCTIONS.has(name) ? name : null;
|
|
881
|
+
}
|
|
882
|
+
if (node.type === 'supsub') return parenthesizedFunctionName(node.base);
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Every `\sin x`-shaped violation of N13 in one already-parsed KaTeX tree: a
|
|
888
|
+
* {@link PARENTHESIZED_FUNCTIONS} name immediately followed, in its own
|
|
889
|
+
* sibling run, by something that reads as a bare argument rather than a
|
|
890
|
+
* parenthesized (or bracketed) one. Mirrors `collectOperators`'s tree walk,
|
|
891
|
+
* but needs sibling context a plain node visitor doesn't carry, so it walks
|
|
892
|
+
* array bodies explicitly instead of reusing that helper.
|
|
893
|
+
*
|
|
894
|
+
* @returns {string[]} one function name per violation, in document order.
|
|
895
|
+
*/
|
|
896
|
+
function findUnparenthesizedFunctionCalls(tree) {
|
|
897
|
+
const violations = [];
|
|
898
|
+
|
|
899
|
+
const visitSiblings = (nodes) => {
|
|
900
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
901
|
+
const name = parenthesizedFunctionName(nodes[i]);
|
|
902
|
+
if (!name) continue;
|
|
903
|
+
let j = i + 1;
|
|
904
|
+
while (j < nodes.length && isSpacingNode(nodes[j])) j += 1;
|
|
905
|
+
const next = j < nodes.length ? nodes[j] : undefined;
|
|
906
|
+
if (opensFunctionArgument(next)) continue;
|
|
907
|
+
if (isNode(next) && BARE_ARGUMENT_NODE_TYPES.has(next.type)) {
|
|
908
|
+
violations.push(name);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
const visit = (value) => {
|
|
914
|
+
if (Array.isArray(value)) {
|
|
915
|
+
visitSiblings(value);
|
|
916
|
+
for (const child of value) visit(child);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (!isNode(value)) return;
|
|
920
|
+
for (const key of Object.keys(value)) {
|
|
921
|
+
if (key === 'loc') continue;
|
|
922
|
+
visit(value[key]);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
visit(tree);
|
|
927
|
+
return violations;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// --- N15: nested visible delimiters need \left/\right sizing -------------
|
|
931
|
+
|
|
932
|
+
// The four visible delimiter glyphs this rule regulates, keyed by their
|
|
933
|
+
// KaTeX source text. `{`/`}` bare is invisible LaTeX grouping, not a visible
|
|
934
|
+
// mark, so only the escaped, visible `\{`/`\}` counts — the same distinction
|
|
935
|
+
// `FUNCTION_ARG_OPENERS` above already draws.
|
|
936
|
+
const NESTED_DELIM_OPEN = new Map([
|
|
937
|
+
['(', 'paren'],
|
|
938
|
+
['[', 'bracket'],
|
|
939
|
+
['\\{', 'brace'],
|
|
940
|
+
]);
|
|
941
|
+
const NESTED_DELIM_CLOSE = new Map([
|
|
942
|
+
[')', 'paren'],
|
|
943
|
+
[']', 'bracket'],
|
|
944
|
+
['\\}', 'brace'],
|
|
945
|
+
]);
|
|
946
|
+
// Recognised on either side of a `\left…\right` pair so a mixed pair like
|
|
947
|
+
// `\left.\right|` (an evaluation bar with an invisible left side) still
|
|
948
|
+
// counts as already-sized.
|
|
949
|
+
const NESTED_DELIM_CHARS = new Set(['(', ')', '[', ']', '\\{', '\\}', '|']);
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Every N15 violation in one already-parsed KaTeX tree: a bare `(`, `[`,
|
|
953
|
+
* `\{`, or `|` that either sits inside another of these four delimiters, or
|
|
954
|
+
* itself contains one, without either side switching to the `\left`/`\right`
|
|
955
|
+
* auto-sizing form. A `\left…\right` pair is never itself reported — it is
|
|
956
|
+
* the fix the rule asks for — but a bare delimiter one level inside it still
|
|
957
|
+
* needs its own `\left`/`\right`, since sizing one level of a nest does not
|
|
958
|
+
* size the rest.
|
|
959
|
+
*
|
|
960
|
+
* Mirrors {@link findUnparenthesizedFunctionCalls}'s tree walk, but needs one
|
|
961
|
+
* stack carried across the whole recursion (nesting here is "does an
|
|
962
|
+
* ancestor node in the tree still have an open delimiter", not sibling
|
|
963
|
+
* adjacency), so it walks the tree itself rather than reusing a sibling-only
|
|
964
|
+
* visitor.
|
|
965
|
+
*
|
|
966
|
+
* `|` has no distinct open/close glyph, so a bare `|` toggles: the first one
|
|
967
|
+
* at the current depth opens a pipe scope, the next one closes it — matching
|
|
968
|
+
* how `\lvert x \rvert`-less absolute value is actually written in this
|
|
969
|
+
* corpus (N9's `\lvert…\rvert` guidance is about *outside*-math prose; a bare
|
|
970
|
+
* `|` pair is unremarkable inside `$…$` on its own).
|
|
971
|
+
*
|
|
972
|
+
* @returns {{ type: string, char: string }[]} one entry per delimiter
|
|
973
|
+
* instance that must become `\left`/`\right`, in document order.
|
|
974
|
+
*/
|
|
975
|
+
function findNestedBareDelimiters(tree) {
|
|
976
|
+
const stack = [];
|
|
977
|
+
const violations = [];
|
|
978
|
+
|
|
979
|
+
const flag = (entry) => {
|
|
980
|
+
if (entry.reported) return;
|
|
981
|
+
entry.reported = true;
|
|
982
|
+
violations.push({ type: entry.type, char: entry.char });
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const pushOpen = (type, char, bare) => {
|
|
986
|
+
const entry = { type, char, bare, reported: false };
|
|
987
|
+
if (stack.length > 0) {
|
|
988
|
+
const parent = stack[stack.length - 1];
|
|
989
|
+
// The parent already contains a nested delimiter: if it is bare, it
|
|
990
|
+
// now needs sizing too, regardless of whether this new one is sized.
|
|
991
|
+
if (parent.bare) flag(parent);
|
|
992
|
+
// This one is nested inside another delimiter's span: if it is bare,
|
|
993
|
+
// it needs sizing, regardless of whether the parent is already sized.
|
|
994
|
+
if (bare) flag(entry);
|
|
995
|
+
}
|
|
996
|
+
stack.push(entry);
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const visit = (node) => {
|
|
1000
|
+
if (Array.isArray(node)) {
|
|
1001
|
+
for (const child of node) visit(child);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (!isNode(node)) return;
|
|
1005
|
+
|
|
1006
|
+
// A visible delimiter only counts in math mode — the same character
|
|
1007
|
+
// inside `\text{…}` is display text, not a sizing delimiter (it parses
|
|
1008
|
+
// to a plain `textord` in text mode, never reaching this branch).
|
|
1009
|
+
if (node.type === 'atom' && node.mode === 'math') {
|
|
1010
|
+
if (node.family === 'open' && NESTED_DELIM_OPEN.has(node.text)) {
|
|
1011
|
+
pushOpen(NESTED_DELIM_OPEN.get(node.text), node.text, true);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (node.family === 'close' && NESTED_DELIM_CLOSE.has(node.text)) {
|
|
1015
|
+
stack.pop();
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (node.type === 'textord' && node.mode === 'math' && node.text === '|') {
|
|
1020
|
+
const top = stack[stack.length - 1];
|
|
1021
|
+
if (top && top.type === 'pipe') {
|
|
1022
|
+
stack.pop();
|
|
1023
|
+
} else {
|
|
1024
|
+
pushOpen('pipe', '|', true);
|
|
1025
|
+
}
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (node.type === 'leftright') {
|
|
1029
|
+
// Already sized. Still an enclosing scope for whatever nests inside it
|
|
1030
|
+
// (a bare delimiter one level in still needs its own sizing), but only
|
|
1031
|
+
// when a tracked glyph is actually on one of its two sides — an
|
|
1032
|
+
// unrelated pair like `\left\langle…\right\rangle` is transparent to
|
|
1033
|
+
// this rule, matching the "with each other" scope of the four glyphs
|
|
1034
|
+
// it names.
|
|
1035
|
+
if (NESTED_DELIM_CHARS.has(node.left) || NESTED_DELIM_CHARS.has(node.right)) {
|
|
1036
|
+
pushOpen('sized', node.left, false);
|
|
1037
|
+
visit(node.body);
|
|
1038
|
+
stack.pop();
|
|
1039
|
+
} else {
|
|
1040
|
+
visit(node.body);
|
|
1041
|
+
}
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
for (const key of Object.keys(node)) {
|
|
1046
|
+
if (key === 'loc') continue;
|
|
1047
|
+
visit(node[key]);
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
visit(tree);
|
|
1052
|
+
return violations;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
771
1055
|
/**
|
|
772
1056
|
* Resolve and annotate one KaTeX expression against a page's flat glyph table.
|
|
773
1057
|
* Source offsets in the result always refer to the original, unmodified LaTeX.
|
|
@@ -786,6 +1070,33 @@ export function resolveMathGlyphs(latex, definitions) {
|
|
|
786
1070
|
sourceWithoutExplainPrefixes(latex, explicitCalls),
|
|
787
1071
|
'lesson math',
|
|
788
1072
|
);
|
|
1073
|
+
|
|
1074
|
+
const parenViolations = findUnparenthesizedFunctionCalls(tree);
|
|
1075
|
+
if (parenViolations.length > 0) {
|
|
1076
|
+
const names = [...new Set(parenViolations)].map((name) => `\\${name}`).join(', ');
|
|
1077
|
+
fail(
|
|
1078
|
+
'unparenthesized-function',
|
|
1079
|
+
`${names} used without a parenthesized argument in ${JSON.stringify(latex)}. ` +
|
|
1080
|
+
`A nonlinear function always takes a parenthesized argument, even a single ` +
|
|
1081
|
+
`symbol — write \\sin(x), not \\sin x (rule N13).`,
|
|
1082
|
+
{ violations: parenViolations },
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const nestingViolations = findNestedBareDelimiters(tree);
|
|
1086
|
+
if (nestingViolations.length > 0) {
|
|
1087
|
+
const shown = [...new Set(nestingViolations.map(({ char }) => char))]
|
|
1088
|
+
.map((char) => `\\left${char}`)
|
|
1089
|
+
.join(', ');
|
|
1090
|
+
fail(
|
|
1091
|
+
'nested-bare-delimiter',
|
|
1092
|
+
`${shown} nested with another (, [, {, or | in ${JSON.stringify(latex)}. ` +
|
|
1093
|
+
`(, [, { and | can be used bare only when not nested — once one of ` +
|
|
1094
|
+
`them is nested inside another, every level of the nest must use the ` +
|
|
1095
|
+
`sizing form (\\left(/\\right), \\left[/\\right], \\left\\{/\\right\\}, ` +
|
|
1096
|
+
`\\left|/\\right|) instead of the bare character (rule N15).`,
|
|
1097
|
+
{ violations: nestingViolations },
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
789
1100
|
const { containers, identifiers, scriptSpans } = collectSourceStructure(tree);
|
|
790
1101
|
|
|
791
1102
|
const canonical = matchCanonicalForms(latex, containers, glyphs);
|
|
@@ -920,3 +1231,59 @@ export function collectOperators(latex) {
|
|
|
920
1231
|
visit(tree);
|
|
921
1232
|
return operators;
|
|
922
1233
|
}
|
|
1234
|
+
|
|
1235
|
+
// --- named roman labels ----------------------------------------------
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Every multi-letter `\mathrm{…}` label in one KaTeX expression (`KE`, `PV`,
|
|
1239
|
+
* `opt`) — the upright-label counterpart to {@link collectOperators}. A
|
|
1240
|
+
* single-letter body (`\mathrm{d}`, the N7 differential) is not collected:
|
|
1241
|
+
* that one is a fixed typographic operator, not a named quantity a reader
|
|
1242
|
+
* would need explained.
|
|
1243
|
+
*
|
|
1244
|
+
* Pure read: nothing here rewrites the source. Used by the
|
|
1245
|
+
* `roman-label-explained` consistency check (rule N10); the glyph resolver
|
|
1246
|
+
* still skips `\mathrm{…}` bodies whole ([N3](../../docs/authoring/notation.md#n3)).
|
|
1247
|
+
*
|
|
1248
|
+
* @param {string} latex
|
|
1249
|
+
* @returns {Array<{ name: string }>}
|
|
1250
|
+
*/
|
|
1251
|
+
export function collectRomanLabels(latex) {
|
|
1252
|
+
if (typeof latex !== 'string' || latex.trim() === '') return [];
|
|
1253
|
+
|
|
1254
|
+
let tree;
|
|
1255
|
+
try {
|
|
1256
|
+
tree = parseMath(latex, 'roman-label scan');
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
if (error instanceof GlyphResolutionError && error.code === 'invalid-latex') {
|
|
1259
|
+
return [];
|
|
1260
|
+
}
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const seen = new Set();
|
|
1265
|
+
const labels = [];
|
|
1266
|
+
const visit = (value) => {
|
|
1267
|
+
if (Array.isArray(value)) {
|
|
1268
|
+
for (const child of value) visit(child);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (!isNode(value)) return;
|
|
1272
|
+
|
|
1273
|
+
if (value.type === 'font' && value.font === ROMAN_LABEL_FONT) {
|
|
1274
|
+
const name = fontBodyText(value.body);
|
|
1275
|
+
if (typeof name === 'string' && name.length >= 2 && !seen.has(name)) {
|
|
1276
|
+
seen.add(name);
|
|
1277
|
+
labels.push({ name });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
for (const key of Object.keys(value)) {
|
|
1282
|
+
if (key === 'loc') continue;
|
|
1283
|
+
visit(value[key]);
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
visit(tree);
|
|
1288
|
+
return labels;
|
|
1289
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace computed string attributes on an MDX JSX node without disturbing
|
|
3
|
+
* author-owned props. Remark adapters use this when the build, rather than the
|
|
4
|
+
* author, owns values such as a visible number or DOM anchor.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} node
|
|
7
|
+
* @param {Record<string, string>} values
|
|
8
|
+
*/
|
|
9
|
+
export function injectMdxStringAttributes(node, values) {
|
|
10
|
+
const names = new Set(Object.keys(values));
|
|
11
|
+
node.attributes = (node.attributes ?? []).filter(
|
|
12
|
+
(attribute) =>
|
|
13
|
+
!(
|
|
14
|
+
attribute?.type === 'mdxJsxAttribute' &&
|
|
15
|
+
typeof attribute.name === 'string' &&
|
|
16
|
+
names.has(attribute.name)
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
for (const [name, value] of Object.entries(values)) {
|
|
20
|
+
node.attributes.push({ type: 'mdxJsxAttribute', name, value });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Return every ordinary MDX JSX attribute with `name`. */
|
|
25
|
+
export function mdxAttributesNamed(node, name) {
|
|
26
|
+
return (node?.attributes ?? []).filter(
|
|
27
|
+
(attribute) =>
|
|
28
|
+
attribute?.type === 'mdxJsxAttribute' && attribute.name === name,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Remove an internal adapter-owned attribute from an MDX JSX node. */
|
|
33
|
+
export function removeMdxAttribute(node, name) {
|
|
34
|
+
node.attributes = (node.attributes ?? []).filter(
|
|
35
|
+
(attribute) =>
|
|
36
|
+
!(attribute?.type === 'mdxJsxAttribute' && attribute.name === name),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a plain string that mixes prose and `$…$` / `$$…$$` math into an
|
|
3
|
+
* ordered list of text and math segments, resolving each math span's glyphs
|
|
4
|
+
* against a page scope on the way — the shared step behind rendering an
|
|
5
|
+
* assessment `prompt` / `explanation` through the same KaTeX pipeline as
|
|
6
|
+
* lesson body math (`AssessmentSet.astro` calls `renderNotationMath` on each
|
|
7
|
+
* `math` segment's `latex`; nothing here touches KaTeX itself).
|
|
8
|
+
*
|
|
9
|
+
* Unlike `gate-math.ts`'s `mathSpans` (which only needs the math, for
|
|
10
|
+
* diagnostics), this keeps the surrounding text too, in order, since the
|
|
11
|
+
* caller has to reproduce the original sentence around the rendered math.
|
|
12
|
+
*
|
|
13
|
+
* Resolution failure is swallowed here, not thrown: a bad span falls back to
|
|
14
|
+
* its own raw source as a `math` segment KaTeX will very likely also reject,
|
|
15
|
+
* degrading the one span instead of crashing manifest compilation. The actual
|
|
16
|
+
* enforcement — failing the build — is `gate-math.ts`'s job (context
|
|
17
|
+
* `'assessment'`) and `gate-prose-notation.ts`'s (context `'prose'`); this
|
|
18
|
+
* module's contract is "never throw", so a manifest build never depends on
|
|
19
|
+
* content being clean to finish running.
|
|
20
|
+
*/
|
|
21
|
+
import { resolveMathGlyphs } from './math-glyphs.mjs';
|
|
22
|
+
import type { ScopeEntry } from './gate-math';
|
|
23
|
+
|
|
24
|
+
export type ContentSegment =
|
|
25
|
+
| { readonly type: 'text'; readonly value: string }
|
|
26
|
+
| { readonly type: 'math'; readonly latex: string };
|
|
27
|
+
|
|
28
|
+
// Byte-identical to `gate-math.ts`'s `DISPLAY_MATH` / `INLINE_MATH` — both
|
|
29
|
+
// scan the same author-facing `$…$` syntax and must agree on what counts as
|
|
30
|
+
// a span. Assessment prompt/explanation strings have no code fences or MDX
|
|
31
|
+
// to strip first, unlike a lesson body, so there is no shared `stripCode`
|
|
32
|
+
// step to reuse here.
|
|
33
|
+
const MATH_SPAN =
|
|
34
|
+
/\$\$([\s\S]+?)\$\$|(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Split `text` into ordered segments, resolving each math span's glyphs
|
|
38
|
+
* against `scope`. Never throws.
|
|
39
|
+
*/
|
|
40
|
+
export function splitMixedContent(
|
|
41
|
+
text: string,
|
|
42
|
+
scope: readonly ScopeEntry[],
|
|
43
|
+
): ContentSegment[] {
|
|
44
|
+
const segments: ContentSegment[] = [];
|
|
45
|
+
let cursor = 0;
|
|
46
|
+
const pattern = new RegExp(MATH_SPAN);
|
|
47
|
+
let match: RegExpExecArray | null;
|
|
48
|
+
|
|
49
|
+
while ((match = pattern.exec(text))) {
|
|
50
|
+
if (match.index > cursor) {
|
|
51
|
+
segments.push({ type: 'text', value: text.slice(cursor, match.index) });
|
|
52
|
+
}
|
|
53
|
+
const raw = match[1] ?? match[2] ?? '';
|
|
54
|
+
let latex = raw;
|
|
55
|
+
try {
|
|
56
|
+
latex = resolveMathGlyphs(raw, [...scope]).latex;
|
|
57
|
+
} catch {
|
|
58
|
+
latex = raw;
|
|
59
|
+
}
|
|
60
|
+
segments.push({ type: 'math', latex });
|
|
61
|
+
cursor = match.index + match[0].length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (cursor < text.length) {
|
|
65
|
+
segments.push({ type: 'text', value: text.slice(cursor) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return segments;
|
|
69
|
+
}
|