explico 0.6.0 → 0.7.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 +180 -1
- package/README.md +16 -2
- package/dist/compiler/astro.d.ts +13 -2
- package/dist/compiler/astro.js +39 -0
- package/dist/compiler/cli.d.ts +22 -3
- package/dist/compiler/cli.js +71 -10
- package/dist/compiler/exports/standalone-html.js +13 -0
- package/dist/compiler/logging.d.ts +109 -0
- package/dist/compiler/logging.js +107 -0
- package/dist/compiler/manifest.d.ts +7 -3
- package/dist/compiler/manifest.js +1 -1
- package/dist/reference/base-abbreviations.mjs +8 -2
- package/dist/reference/consistency.d.ts +8 -10
- package/dist/reference/consistency.js +3 -47
- package/dist/reference/delimiter-size.d.ts +39 -0
- package/dist/reference/delimiter-size.js +177 -0
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +2 -2
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-math.d.ts +2 -0
- package/dist/reference/gate-math.js +28 -6
- package/dist/reference/keyed-float-validate.d.ts +2 -2
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/references.js +2 -1
- package/dist/reference/registry.js +1 -1
- package/dist/reference/rehype-base-links.d.mts +23 -0
- package/dist/reference/rehype-base-links.mjs +40 -0
- package/dist/reference/remark-abbreviations.mjs +10 -12
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/render-lab-math.d.ts +18 -1
- package/dist/reference/render-lab-math.js +10 -3
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +1 -1
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +12 -2
- package/docs/authoring/agents.md +7 -3
- package/docs/authoring/migrating.md +286 -0
- package/docs/authoring/notation.md +139 -7
- package/docs/authoring/prose.md +64 -0
- package/package.json +1 -1
- package/src/compiler/astro.ts +89 -0
- package/src/compiler/cli.ts +144 -12
- package/src/compiler/exports/standalone-html.ts +15 -0
- package/src/compiler/logging.ts +248 -0
- package/src/compiler/manifest.ts +7 -3
- package/src/components/memo/Memo.astro +2 -5
- package/src/components/memo/ShortNote.astro +2 -5
- package/src/components/notation/NotationFormula.astro +199 -10
- package/src/components/notation/NotationGlossary.astro +89 -58
- package/src/components/notation/NotationLayer.astro +83 -81
- package/src/components/scroll-animation.ts +160 -0
- package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
- package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
- package/src/components/starlight/LessonFooter.astro +2 -0
- package/src/reference/base-abbreviations.mjs +8 -2
- package/src/reference/consistency.ts +10 -66
- package/src/reference/delimiter-size.ts +301 -0
- package/src/reference/diagrams-validate.ts +1 -1
- package/src/reference/equations-validate.ts +3 -3
- package/src/reference/figures-validate.ts +1 -1
- package/src/reference/gate-math.ts +41 -6
- package/src/reference/keyed-float-validate.ts +3 -3
- package/src/reference/references.ts +2 -1
- package/src/reference/registry.ts +1 -1
- package/src/reference/rehype-base-links.mjs +40 -0
- package/src/reference/remark-abbreviations.mjs +10 -12
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/render-lab-math.ts +24 -1
- package/src/reference/tables-validate.ts +1 -1
- package/src/reference/types.ts +1 -1
- package/src/styles/global.css +119 -14
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single, site-wide motion profile for "jump to an in-page anchor":
|
|
3
|
+
* `DURATION_MS`, constant-magnitude acceleration — speeding up through the
|
|
4
|
+
* first half of the trip, slowing down through the second, symmetric about
|
|
5
|
+
* the midpoint (the standard `easeInOutQuad` curve, which is exactly a
|
|
6
|
+
* constant |acceleration| profile since position is piecewise quadratic in
|
|
7
|
+
* time).
|
|
8
|
+
*
|
|
9
|
+
* The browser's own fragment navigation (a plain `<a href="#id">` click, with
|
|
10
|
+
* no `scroll-behavior: smooth` set anywhere in this site's CSS) jumps
|
|
11
|
+
* instantly — and CSS's `scroll-behavior: smooth` can't be tuned to a given
|
|
12
|
+
* duration or easing anyway. Getting an exact, consistent curve everywhere
|
|
13
|
+
* means animating the scroll ourselves. `AnchorScrollEnhancer.astro` (the
|
|
14
|
+
* generic click case, covering every same-page link on every page) and the
|
|
15
|
+
* handful of enhancers with their own reasons to move the viewport
|
|
16
|
+
* (`HeadingAnchorEnhancer.astro`, `KeyedFloatEnhancer.astro`, footnote/memo
|
|
17
|
+
* expansion) all render through this module, so a click that touches two of
|
|
18
|
+
* them at once — e.g. a heading anchor click, animated here *and* re-scrolled
|
|
19
|
+
* by that enhancer's own `hashchange` listener — collapses to a single
|
|
20
|
+
* animation via `activeAnimation` below, rather than two fighting for the
|
|
21
|
+
* scroll position.
|
|
22
|
+
*
|
|
23
|
+
* An earlier version of `AnchorScrollEnhancer.astro` let the browser's own
|
|
24
|
+
* click-triggered jump run, then tried to catch it a frame later and replay
|
|
25
|
+
* it as an animation. The trouble: the HTML spec gives no guarantee about
|
|
26
|
+
* *when*, relative to our own script, that jump actually lands — under load
|
|
27
|
+
* it can take several frames — so a single-frame check could conclude
|
|
28
|
+
* "nothing moved, there's nothing to animate" and let the browser's own
|
|
29
|
+
* un-animated jump land moments later, unopposed (this was the original,
|
|
30
|
+
* confirmed bug reports came in about). Polling for more frames narrows that
|
|
31
|
+
* window but never closes it — there's always some delay past which it gives
|
|
32
|
+
* up. `animatedScrollIntoView` below removes the dependency on that timing
|
|
33
|
+
* entirely: it measures the destination itself, synchronously, before
|
|
34
|
+
* anything has had a chance to move, so there's nothing to race.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const DURATION_MS = 200;
|
|
38
|
+
function easeInOutQuad(t: number): number {
|
|
39
|
+
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type SettleReason = 'complete' | 'interrupted';
|
|
43
|
+
|
|
44
|
+
let activeAnimation: {
|
|
45
|
+
readonly target: Element;
|
|
46
|
+
readonly cancel: (reason: SettleReason) => void;
|
|
47
|
+
} | null = null;
|
|
48
|
+
|
|
49
|
+
function prefersReducedMotion(): boolean {
|
|
50
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Cancel on the reader's own scroll intent — a short ride is unlikely to
|
|
54
|
+
// outlast one, but fighting the reader's wheel/touch/keyboard input, even
|
|
55
|
+
// briefly, is exactly the kind of jank this exists to avoid.
|
|
56
|
+
const CANCEL_EVENTS = ['wheel', 'touchstart', 'keydown'] as const;
|
|
57
|
+
|
|
58
|
+
function runAnimation(
|
|
59
|
+
target: Element,
|
|
60
|
+
startX: number,
|
|
61
|
+
startY: number,
|
|
62
|
+
endX: number,
|
|
63
|
+
endY: number,
|
|
64
|
+
onSettle?: (reason: SettleReason) => void,
|
|
65
|
+
): void {
|
|
66
|
+
if (activeAnimation?.target === target) return;
|
|
67
|
+
activeAnimation?.cancel('interrupted');
|
|
68
|
+
|
|
69
|
+
if (prefersReducedMotion() || (startX === endX && startY === endY)) {
|
|
70
|
+
window.scrollTo(endX, endY);
|
|
71
|
+
activeAnimation = null;
|
|
72
|
+
onSettle?.('complete');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
window.scrollTo(startX, startY);
|
|
77
|
+
|
|
78
|
+
let rafId = 0;
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
const startTime = performance.now();
|
|
81
|
+
|
|
82
|
+
const onCancelEvent = () => cancel('interrupted');
|
|
83
|
+
|
|
84
|
+
const cleanup = () => {
|
|
85
|
+
for (const type of CANCEL_EVENTS) {
|
|
86
|
+
window.removeEventListener(type, onCancelEvent);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const finish = (reason: SettleReason) => {
|
|
91
|
+
cleanup();
|
|
92
|
+
if (activeAnimation?.target === target) activeAnimation = null;
|
|
93
|
+
onSettle?.(reason);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const cancel = (reason: SettleReason) => {
|
|
97
|
+
if (cancelled) return;
|
|
98
|
+
cancelled = true;
|
|
99
|
+
cancelAnimationFrame(rafId);
|
|
100
|
+
finish(reason);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const step = (now: number) => {
|
|
104
|
+
if (cancelled) return;
|
|
105
|
+
const t = Math.min(1, (now - startTime) / DURATION_MS);
|
|
106
|
+
const eased = easeInOutQuad(t);
|
|
107
|
+
window.scrollTo(
|
|
108
|
+
startX + (endX - startX) * eased,
|
|
109
|
+
startY + (endY - startY) * eased,
|
|
110
|
+
);
|
|
111
|
+
if (t < 1) {
|
|
112
|
+
rafId = requestAnimationFrame(step);
|
|
113
|
+
} else {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
finish('complete');
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const type of CANCEL_EVENTS) {
|
|
120
|
+
window.addEventListener(type, onCancelEvent, { passive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
activeAnimation = { target, cancel };
|
|
124
|
+
rafId = requestAnimationFrame(step);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Drop-in replacement for `target.scrollIntoView({ behavior: 'smooth', ... })`.
|
|
129
|
+
* Measures the destination itself — instantly, synchronously, before
|
|
130
|
+
* anything has had a chance to move — via `target.scrollIntoView({behavior:
|
|
131
|
+
* 'auto', ...})` (this respects `scroll-margin-*`, sticky headers, etc.
|
|
132
|
+
* exactly as the browser's own fragment navigation would), then jumps back
|
|
133
|
+
* and animates the trip.
|
|
134
|
+
*
|
|
135
|
+
* `onSettle`, when given, fires once: `'complete'` if the animation ran to
|
|
136
|
+
* the destination (or reduced motion meant an instant jump straight there),
|
|
137
|
+
* `'interrupted'` if the reader's own scroll/keyboard input cut it short, or
|
|
138
|
+
* — a rare, intentional no-op — never, if a newer call for the *same*
|
|
139
|
+
* target was already in flight (this one is simply dropped; the one already
|
|
140
|
+
* running owns the eventual settle).
|
|
141
|
+
*/
|
|
142
|
+
export function animatedScrollIntoView(
|
|
143
|
+
target: Element,
|
|
144
|
+
options: ScrollIntoViewOptions = { block: 'start' },
|
|
145
|
+
onSettle?: (reason: SettleReason) => void,
|
|
146
|
+
): void {
|
|
147
|
+
if (prefersReducedMotion()) {
|
|
148
|
+
target.scrollIntoView({ ...options, behavior: 'auto' });
|
|
149
|
+
onSettle?.('complete');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const startX = window.scrollX;
|
|
154
|
+
const startY = window.scrollY;
|
|
155
|
+
target.scrollIntoView({ ...options, behavior: 'auto' });
|
|
156
|
+
const endX = window.scrollX;
|
|
157
|
+
const endY = window.scrollY;
|
|
158
|
+
|
|
159
|
+
runAnimation(target, startX, startY, endX, endY, onSettle);
|
|
160
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Site-wide "jump to an in-page anchor" animation (Step F3 follow-up). Loads
|
|
4
|
+
* unconditionally, on every page — not just lessons — since it has to catch
|
|
5
|
+
* every same-page `<a href="#…">`: a heading, a keyed equation/table/figure
|
|
6
|
+
* number, a footnote/memo marker, and plain content links with no dedicated
|
|
7
|
+
* enhancer of their own at all (the notation glossary's term/anchor links,
|
|
8
|
+
* its "see also" list, a page's table of contents, …).
|
|
9
|
+
*
|
|
10
|
+
* This calls `preventDefault()` on a qualifying click — which looks like the
|
|
11
|
+
* one thing `HeadingAnchorEnhancer.astro` and `KeyedFloatEnhancer.astro` both
|
|
12
|
+
* carry a comment warning against, since `:target`-driven CSS (the
|
|
13
|
+
* heading/equation "flash") and the sticky-header `scroll-margin` offset only
|
|
14
|
+
* fire on a genuine same-document fragment navigation — never on a
|
|
15
|
+
* `history.pushState`. That warning is about *silently* replacing the
|
|
16
|
+
* navigation with `pushState` and never actually re-triggering it. This does
|
|
17
|
+
* something safer: it measures the destination and animates there itself
|
|
18
|
+
* (`animatedScrollIntoView`, `scroll-animation.ts` — the same call
|
|
19
|
+
* `HeadingAnchorEnhancer`/`KeyedFloatEnhancer` use for their own supplementary
|
|
20
|
+
* scrolls), and only once the viewport is actually *at* that destination does
|
|
21
|
+
* it assign `location.hash` — a real fragment navigation, not `pushState`,
|
|
22
|
+
* so `:target` matches and `hashchange` fires exactly as a plain click's
|
|
23
|
+
* default action would have. Because the assignment happens after arrival,
|
|
24
|
+
* the browser's own resulting "scroll to the fragment" pass finds nothing
|
|
25
|
+
* left to move and is an invisible no-op, whenever it happens to run.
|
|
26
|
+
*
|
|
27
|
+
* An earlier version instead let the native click run and tried to catch its
|
|
28
|
+
* jump a frame later — see the note atop `scroll-animation.ts` for why that
|
|
29
|
+
* was racy in a way no amount of polling fixed, and had to be replaced by
|
|
30
|
+
* this preventDefault-and-replay approach instead.
|
|
31
|
+
*
|
|
32
|
+
* If the reader scrolls away mid-animation (`'interrupted'` below), the URL
|
|
33
|
+
* still gets a plain `pushState` — reflecting the click without dragging
|
|
34
|
+
* them back to a destination they've already scrolled past, and without the
|
|
35
|
+
* `:target` flash firing for a section they're no longer looking at.
|
|
36
|
+
*/
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
import { animatedScrollIntoView } from '../scroll-animation';
|
|
41
|
+
|
|
42
|
+
function sameDocumentFragmentId(link: HTMLAnchorElement): string | null {
|
|
43
|
+
if (link.target && link.target !== '_self') return null;
|
|
44
|
+
let url: URL;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(link.href, location.href);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
url.origin !== location.origin ||
|
|
52
|
+
url.pathname !== location.pathname ||
|
|
53
|
+
url.search !== location.search ||
|
|
54
|
+
!url.hash
|
|
55
|
+
) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return url.hash.slice(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
document.addEventListener('click', (event) => {
|
|
62
|
+
if (
|
|
63
|
+
event.defaultPrevented ||
|
|
64
|
+
event.button !== 0 ||
|
|
65
|
+
event.metaKey ||
|
|
66
|
+
event.ctrlKey ||
|
|
67
|
+
event.shiftKey ||
|
|
68
|
+
event.altKey
|
|
69
|
+
) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const origin = event.target instanceof Element ? event.target : null;
|
|
74
|
+
const link = origin?.closest('a[href]');
|
|
75
|
+
if (!(link instanceof HTMLAnchorElement)) return;
|
|
76
|
+
|
|
77
|
+
const rawId = sameDocumentFragmentId(link);
|
|
78
|
+
if (!rawId) return;
|
|
79
|
+
|
|
80
|
+
let id = rawId;
|
|
81
|
+
try {
|
|
82
|
+
id = decodeURIComponent(rawId);
|
|
83
|
+
} catch {
|
|
84
|
+
/* keep the raw fragment */
|
|
85
|
+
}
|
|
86
|
+
const target = document.getElementById(id);
|
|
87
|
+
if (!target) return; // no matching element: let the browser's own no-op stand
|
|
88
|
+
|
|
89
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
90
|
+
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
animatedScrollIntoView(target, { block: 'start' }, (reason) => {
|
|
93
|
+
const hash = `#${id}`;
|
|
94
|
+
if (location.hash === hash) return;
|
|
95
|
+
if (reason === 'complete') {
|
|
96
|
+
location.hash = id;
|
|
97
|
+
} else {
|
|
98
|
+
history.pushState(null, '', hash);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
<script>
|
|
39
39
|
import { HEADINGS, sectionParts } from './section-parts';
|
|
40
|
+
import { animatedScrollIntoView } from '../scroll-animation';
|
|
40
41
|
|
|
41
42
|
(() => {
|
|
42
43
|
const root = document.querySelector('.sl-markdown-content');
|
|
@@ -78,10 +79,7 @@
|
|
|
78
79
|
if (!heading) return;
|
|
79
80
|
const parts = sectionParts(heading);
|
|
80
81
|
if (doScroll) {
|
|
81
|
-
parts[0]
|
|
82
|
-
behavior: reduce.matches ? 'auto' : 'smooth',
|
|
83
|
-
block: 'start',
|
|
84
|
-
});
|
|
82
|
+
animatedScrollIntoView(parts[0], { block: 'start' });
|
|
85
83
|
}
|
|
86
84
|
for (const part of parts) flash(part);
|
|
87
85
|
heading.setAttribute('tabindex', '-1');
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
42
|
<script>
|
|
43
|
+
import { animatedScrollIntoView } from '../scroll-animation';
|
|
44
|
+
|
|
43
45
|
(() => {
|
|
44
46
|
const root = document.querySelector('.sl-markdown-content');
|
|
45
47
|
if (!root) return;
|
|
@@ -97,10 +99,7 @@
|
|
|
97
99
|
target.style.removeProperty('animation');
|
|
98
100
|
}
|
|
99
101
|
if (doScroll) {
|
|
100
|
-
target
|
|
101
|
-
behavior: reduce.matches ? 'auto' : 'smooth',
|
|
102
|
-
block: 'start',
|
|
103
|
-
});
|
|
102
|
+
animatedScrollIntoView(target, { block: 'start' });
|
|
104
103
|
}
|
|
105
104
|
target.setAttribute('tabindex', '-1');
|
|
106
105
|
try {
|
|
@@ -132,13 +131,15 @@
|
|
|
132
131
|
|
|
133
132
|
// "Copy LaTeX" for an equation, two ways: its own button (only a numbered
|
|
134
133
|
// display equation has one — `data-eq-copy`, `remark-notation.mjs`'s
|
|
135
|
-
// `equationNode`), or a click anywhere else in the
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
134
|
+
// `equationNode`), or a click anywhere else in the box — including the
|
|
135
|
+
// reserved gutter between the equation and its number/button, not just
|
|
136
|
+
// the rendered `.katex-html` — that isn't the number link or a symbol
|
|
137
|
+
// with its own notation card. The same gesture works on an inline
|
|
138
|
+
// equation too (`inlineEquationNode`), which has no room for a
|
|
139
|
+
// persistent button of its own without breaking the reading line. A
|
|
140
|
+
// notation-registry symbol keeps its existing click-to-pin behavior
|
|
141
|
+
// either way (`NotationLayer.astro`), so this skips anything under
|
|
142
|
+
// `[data-notation-key]`.
|
|
142
143
|
root.addEventListener('click', (event) => {
|
|
143
144
|
const origin = event.target instanceof Element ? event.target : null;
|
|
144
145
|
if (!origin) return;
|
|
@@ -150,9 +151,7 @@
|
|
|
150
151
|
const container = ownButton
|
|
151
152
|
? null
|
|
152
153
|
: origin.closest<HTMLElement>('.keyed-equation, .inline-equation');
|
|
153
|
-
if (!ownButton &&
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
154
|
+
if (!ownButton && !container) return;
|
|
156
155
|
|
|
157
156
|
const feedbackTarget =
|
|
158
157
|
ownButton ??
|
|
@@ -185,12 +184,14 @@
|
|
|
185
184
|
root.addEventListener('mouseover', (event) => {
|
|
186
185
|
const origin = event.target instanceof Element ? event.target : null;
|
|
187
186
|
if (!origin || origin.getAttribute('title') === 'Copy LaTeX') return;
|
|
188
|
-
if (origin.closest('[data-notation-key], [data-eq-copy]')) return;
|
|
189
187
|
if (
|
|
190
|
-
|
|
188
|
+
origin.closest(
|
|
189
|
+
'[data-notation-key], [data-eq-copy], a.keyed-equation__number',
|
|
190
|
+
)
|
|
191
191
|
) {
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
|
+
if (!origin.closest('.keyed-equation, .inline-equation')) return;
|
|
194
195
|
origin.setAttribute('title', 'Copy LaTeX');
|
|
195
196
|
});
|
|
196
197
|
|
|
@@ -23,6 +23,7 @@ import AbbreviationLayer from '../abbreviation/AbbreviationLayer.astro';
|
|
|
23
23
|
import KeyedFloatEnhancer from './KeyedFloatEnhancer.astro';
|
|
24
24
|
import FloatRefLayer from '../reference/FloatRefLayer.astro';
|
|
25
25
|
import HeadingAnchorEnhancer from './HeadingAnchorEnhancer.astro';
|
|
26
|
+
import AnchorScrollEnhancer from './AnchorScrollEnhancer.astro';
|
|
26
27
|
import CollapsibleSections from './CollapsibleSections.astro';
|
|
27
28
|
import TableOverflowExpander from '../table/TableOverflowExpander.astro';
|
|
28
29
|
import DiagramEnhancer from './DiagramEnhancer.astro';
|
|
@@ -83,6 +84,7 @@ if (lessonId) {
|
|
|
83
84
|
---
|
|
84
85
|
|
|
85
86
|
<NotationLayer definitions={definitions} defaultOpen={notationDefaultOpen} />
|
|
87
|
+
<AnchorScrollEnhancer />
|
|
86
88
|
{lessonId && <CitationLayer />}
|
|
87
89
|
{lessonId && <AbbreviationLayer />}
|
|
88
90
|
{lessonId && <KeyedFloatEnhancer />}
|
|
@@ -20,12 +20,18 @@ export const BASE_ABBREVIATIONS = Object.freeze(
|
|
|
20
20
|
'e.g.',
|
|
21
21
|
'i.e.',
|
|
22
22
|
'etc.',
|
|
23
|
+
'e.t.c.',
|
|
23
24
|
'cf.',
|
|
24
25
|
'vs.',
|
|
25
|
-
'a.m.',
|
|
26
|
-
'p.m.',
|
|
27
26
|
'a.k.a.',
|
|
28
27
|
'et al.',
|
|
28
|
+
// time of day, in each casing/punctuation style a lesson might use
|
|
29
|
+
'a.m.',
|
|
30
|
+
'p.m.',
|
|
31
|
+
'A.M.',
|
|
32
|
+
'P.M.',
|
|
33
|
+
'AM',
|
|
34
|
+
'PM',
|
|
29
35
|
// generic acronyms with no domain-specific meaning in this engine
|
|
30
36
|
'AI',
|
|
31
37
|
'ID',
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Corpus consistency checks (Step D6) — the Tier 1–2 checks from the cleanup
|
|
3
3
|
* plan's "Enforced checks" section that no earlier step covers.
|
|
4
4
|
*
|
|
5
|
-
* Every check here
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Every check here is non-blocking Tier 3: repairable violations are warnings
|
|
6
|
+
* and optional restructuring advice is info. `pnpm validate:content` surfaces
|
|
7
|
+
* warning counts, `manifest.diagnostics.consistency` records every detail, and
|
|
8
|
+
* `resolution/consistency-report.json` is the committed, diff-checkable
|
|
9
|
+
* snapshot. Nothing here changes content. A finding is silenced only by a
|
|
10
10
|
* reviewed line in `lint-ignore.yml` (see `./lint-ignore`).
|
|
11
11
|
*
|
|
12
12
|
* (a) glyph-unique-in-corpus one rendered glyph → one meaning corpus-wide
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
* unit strings drawn from a controlled vocab
|
|
15
15
|
* (c) convention-single-definition a sign / cash-flow-perspective convention a
|
|
16
16
|
* lesson uses resolves to one declaration
|
|
17
|
-
* (e) numerals-tagged numerals in `$…$` / result tables are tagged
|
|
18
|
-
* to a `src/domain/` call or a source locator
|
|
19
17
|
* (f) weak-local a `notation.local` symbol used only in its
|
|
20
18
|
* own defining equation and prose
|
|
21
19
|
* (g) notation-source-locator every notation `sources` entry has a locator
|
|
@@ -54,7 +52,6 @@ export type ConsistencyCode =
|
|
|
54
52
|
| 'notation-units'
|
|
55
53
|
| 'units-vocab'
|
|
56
54
|
| 'convention-single-definition'
|
|
57
|
-
| 'numerals-tagged'
|
|
58
55
|
| 'weak-local'
|
|
59
56
|
| 'notation-source-locator'
|
|
60
57
|
| 'gloss-wants-promoting'
|
|
@@ -65,8 +62,8 @@ export type ConsistencyCode =
|
|
|
65
62
|
|
|
66
63
|
export interface ConsistencyDiagnostic {
|
|
67
64
|
readonly code: ConsistencyCode;
|
|
68
|
-
/** Tier 3 —
|
|
69
|
-
readonly severity: 'warning';
|
|
65
|
+
/** Tier 3 — repairable violations are warnings; optional advice is info. */
|
|
66
|
+
readonly severity: 'warning' | 'info';
|
|
70
67
|
readonly message: string;
|
|
71
68
|
readonly file?: string;
|
|
72
69
|
readonly lessonId?: string;
|
|
@@ -119,28 +116,6 @@ function mathSpans(body: string): MathSpan[] {
|
|
|
119
116
|
return spans;
|
|
120
117
|
}
|
|
121
118
|
|
|
122
|
-
function tableRows(body: string): string[] {
|
|
123
|
-
const rows: string[] = [];
|
|
124
|
-
for (const line of stripCode(body).split('\n')) {
|
|
125
|
-
const trimmed = line.trim();
|
|
126
|
-
if (!trimmed.startsWith('|') || !trimmed.slice(1).includes('|')) continue;
|
|
127
|
-
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) continue;
|
|
128
|
-
rows.push(line);
|
|
129
|
-
}
|
|
130
|
-
return rows;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const NUMERAL_RE = /(?<![\w.])-?\d+(?:\.\d+)?/g;
|
|
134
|
-
|
|
135
|
-
function numeralCount(text: string): number {
|
|
136
|
-
return (text.replace(/\\[a-zA-Z]+/g, ' ').match(NUMERAL_RE) ?? []).length;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** A math span / table row is "tagged" when it carries a citation or a domain marker. */
|
|
140
|
-
function isTagged(text: string): boolean {
|
|
141
|
-
return /\[@[a-z0-9]/i.test(text) || /<!--\s*domain:/i.test(text);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
119
|
// --- glyph regex for weak-local --------------------------------------
|
|
145
120
|
|
|
146
121
|
function glyphMatcher(latex: string): RegExp | undefined {
|
|
@@ -274,35 +249,6 @@ function checkConventionSingleDefinition(
|
|
|
274
249
|
}
|
|
275
250
|
}
|
|
276
251
|
|
|
277
|
-
function checkNumeralsTagged(
|
|
278
|
-
input: ConsistencyInput,
|
|
279
|
-
out: ConsistencyDiagnostic[],
|
|
280
|
-
): void {
|
|
281
|
-
for (const lesson of input.notationInput.lessons) {
|
|
282
|
-
if (input.lintIgnore.matches('numerals-tagged', lesson.lessonId)) continue;
|
|
283
|
-
let math = 0;
|
|
284
|
-
for (const span of mathSpans(lesson.body)) {
|
|
285
|
-
if (isTagged(span.latex)) continue;
|
|
286
|
-
math += numeralCount(span.latex);
|
|
287
|
-
}
|
|
288
|
-
let table = 0;
|
|
289
|
-
for (const row of tableRows(lesson.body)) {
|
|
290
|
-
if (isTagged(row)) continue;
|
|
291
|
-
table += numeralCount(row.replace(/\$[^$\n]*\$/g, ' '));
|
|
292
|
-
}
|
|
293
|
-
const count = math + table;
|
|
294
|
-
if (count === 0) continue;
|
|
295
|
-
out.push({
|
|
296
|
-
code: 'numerals-tagged',
|
|
297
|
-
severity: 'warning',
|
|
298
|
-
lessonId: lesson.lessonId,
|
|
299
|
-
file: lesson.source.file,
|
|
300
|
-
count,
|
|
301
|
-
message: `${lesson.lessonId}: ${count} numeral(s) in $…$ / result tables (${math} in math, ${table} in tables) are not tagged to a src/domain/ call or a source locator`,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
252
|
function checkWeakLocal(
|
|
307
253
|
input: ConsistencyInput,
|
|
308
254
|
out: ConsistencyDiagnostic[],
|
|
@@ -317,7 +263,7 @@ function checkWeakLocal(
|
|
|
317
263
|
if (uses > 1) continue;
|
|
318
264
|
out.push({
|
|
319
265
|
code: 'weak-local',
|
|
320
|
-
severity: '
|
|
266
|
+
severity: 'info',
|
|
321
267
|
lessonId: lesson.lessonId,
|
|
322
268
|
file: lesson.source.file,
|
|
323
269
|
key: definition.key,
|
|
@@ -390,7 +336,7 @@ function checkGlossWantsPromoting(
|
|
|
390
336
|
if (input.lintIgnore.matches('gloss-wants-promoting', glyph)) continue;
|
|
391
337
|
out.push({
|
|
392
338
|
code: 'gloss-wants-promoting',
|
|
393
|
-
severity: '
|
|
339
|
+
severity: 'info',
|
|
394
340
|
glyph,
|
|
395
341
|
message: `gloss ${JSON.stringify(glyph)} (${label}) is declared on ${keys.size} entries (${[...keys].sort().join(', ')}); a meaning reused this widely wants promoting to a card`,
|
|
396
342
|
});
|
|
@@ -425,7 +371,7 @@ function checkCardWantsDemoting(
|
|
|
425
371
|
}
|
|
426
372
|
out.push({
|
|
427
373
|
code: 'card-wants-demoting',
|
|
428
|
-
severity: '
|
|
374
|
+
severity: 'info',
|
|
429
375
|
key: definition.key,
|
|
430
376
|
file: definition.source.file,
|
|
431
377
|
count: words,
|
|
@@ -627,7 +573,6 @@ const CHECKS: ((
|
|
|
627
573
|
checkGlyphUniqueness,
|
|
628
574
|
checkNotationUnits,
|
|
629
575
|
checkConventionSingleDefinition,
|
|
630
|
-
checkNumeralsTagged,
|
|
631
576
|
checkWeakLocal,
|
|
632
577
|
checkNotationSourceLocator,
|
|
633
578
|
checkGlossWantsPromoting,
|
|
@@ -666,7 +611,6 @@ export function consistencyCounts(
|
|
|
666
611
|
'notation-units': 0,
|
|
667
612
|
'units-vocab': 0,
|
|
668
613
|
'convention-single-definition': 0,
|
|
669
|
-
'numerals-tagged': 0,
|
|
670
614
|
'weak-local': 0,
|
|
671
615
|
'notation-source-locator': 0,
|
|
672
616
|
'gloss-wants-promoting': 0,
|