explico 0.1.1 → 0.3.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 +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +17 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +31 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +87 -3
- package/dist/compiler/manifest.js +133 -20
- package/dist/compiler/resolution-report.d.ts +5 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +142 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +76 -18
- package/dist/reference/equations.mjs +118 -41
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/gate-math.d.ts +5 -4
- package/dist/reference/gate-math.js +73 -2
- package/dist/reference/gloss.d.ts +87 -0
- package/dist/reference/gloss.js +59 -0
- package/dist/reference/keyed-float-validate.d.ts +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/registry.js +25 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-notation.d.mts +12 -0
- package/dist/reference/remark-notation.mjs +228 -32
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/types.d.ts +25 -2
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +52 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +270 -17
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/hover-panel.ts +71 -5
- package/src/components/notation/NotationFormula.astro +154 -0
- package/src/components/notation/NotationGlossary.astro +60 -0
- package/src/components/notation/NotationLayer.astro +248 -103
- package/src/components/notation/render-notation-math.ts +66 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +8 -2
- package/src/components/table/Table.astro +104 -0
- package/src/content-config.ts +38 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +236 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +6 -1
- package/src/reference/equations.mjs +118 -41
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +228 -32
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/types.ts +26 -1
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +193 -3
- package/src/components/starlight/EquationEnhancer.astro +0 -72
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { effectiveOperatorAllowlist } from "./base-operators.mjs";
|
|
2
|
+
import { collectOperators } from "./math-glyphs.mjs";
|
|
1
3
|
import { offVocabularyTokens } from "./units-vocab.js";
|
|
2
4
|
const CONVENTION_RE = /\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;
|
|
3
5
|
function stripCode(text) {
|
|
@@ -200,13 +202,147 @@ function checkNotationSourceLocator(input, out) {
|
|
|
200
202
|
}
|
|
201
203
|
}
|
|
202
204
|
}
|
|
205
|
+
const GLOSS_PROMOTION_LIMIT = 3;
|
|
206
|
+
function checkGlossWantsPromoting(input, out) {
|
|
207
|
+
const owners = /* @__PURE__ */ new Map();
|
|
208
|
+
const record = (ownerKey, gloss) => {
|
|
209
|
+
const id = `${gloss.notation}\0${gloss.label.toLowerCase()}`;
|
|
210
|
+
const current = owners.get(id) ?? { label: gloss.label, keys: /* @__PURE__ */ new Set() };
|
|
211
|
+
current.keys.add(ownerKey);
|
|
212
|
+
owners.set(id, current);
|
|
213
|
+
};
|
|
214
|
+
for (const definition of input.notationInput.sharedDefinitions) {
|
|
215
|
+
for (const gloss of definition.glosses) record(definition.key, gloss);
|
|
216
|
+
}
|
|
217
|
+
for (const lesson of input.notationInput.lessons) {
|
|
218
|
+
for (const definition of lesson.localDefinitions) {
|
|
219
|
+
for (const gloss of definition.glosses) record(definition.key, gloss);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const [id, { label, keys }] of [...owners].sort(
|
|
223
|
+
([a], [b]) => a.localeCompare(b)
|
|
224
|
+
)) {
|
|
225
|
+
if (keys.size < GLOSS_PROMOTION_LIMIT) continue;
|
|
226
|
+
const glyph = id.split("\0")[0] ?? "";
|
|
227
|
+
if (input.lintIgnore.matches("gloss-wants-promoting", glyph)) continue;
|
|
228
|
+
out.push({
|
|
229
|
+
code: "gloss-wants-promoting",
|
|
230
|
+
severity: "warning",
|
|
231
|
+
glyph,
|
|
232
|
+
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`
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const CARD_BODY_MINIMUM_WORDS = 25;
|
|
237
|
+
function checkCardWantsDemoting(input, out) {
|
|
238
|
+
const used = new Set(
|
|
239
|
+
input.registry.backlinks.map((backlink) => backlink.key)
|
|
240
|
+
);
|
|
241
|
+
for (const definition of input.notationInput.sharedDefinitions) {
|
|
242
|
+
if (definition.formula !== void 0) continue;
|
|
243
|
+
if (definition.sources.length > 0) continue;
|
|
244
|
+
if (used.has(definition.key)) continue;
|
|
245
|
+
const words = stripCode(definition.body).split(/\s+/).filter((word) => word.length > 0).length;
|
|
246
|
+
if (words >= CARD_BODY_MINIMUM_WORDS) continue;
|
|
247
|
+
if (input.lintIgnore.matches("card-wants-demoting", definition.key)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
out.push({
|
|
251
|
+
code: "card-wants-demoting",
|
|
252
|
+
severity: "warning",
|
|
253
|
+
key: definition.key,
|
|
254
|
+
file: definition.source.file,
|
|
255
|
+
count: words,
|
|
256
|
+
message: `notation card ${definition.key} has no formula, no sources, a ${words}-word body, and no lesson uses it; it carries no more than a name \u2014 a gloss on the entry that needs it may be the right home`
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const GLOSS_NAME_WORD_LIMIT = 6;
|
|
261
|
+
const GLOSS_NAME_PROSE = /[.;]|\b(?:which|whose|that is|such that)\b/i;
|
|
262
|
+
function checkGlossNameShape(input, out) {
|
|
263
|
+
const check = (gloss, file, lessonId) => {
|
|
264
|
+
const words = gloss.label.trim().split(/\s+/).filter(Boolean);
|
|
265
|
+
const reason = words.length > GLOSS_NAME_WORD_LIMIT ? `${words.length} words` : GLOSS_NAME_PROSE.test(gloss.label) ? "sentence punctuation or a relative clause" : void 0;
|
|
266
|
+
if (reason === void 0) return;
|
|
267
|
+
if (input.lintIgnore.matches("gloss-name-shape", gloss.key)) return;
|
|
268
|
+
out.push({
|
|
269
|
+
code: "gloss-name-shape",
|
|
270
|
+
severity: "warning",
|
|
271
|
+
key: gloss.key,
|
|
272
|
+
file,
|
|
273
|
+
...lessonId ? { lessonId } : {},
|
|
274
|
+
message: `gloss ${gloss.key} is named ${JSON.stringify(gloss.label)} (${reason}); a gloss name is a short noun phrase naming the symbol, not a description of it`
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
for (const definition of input.notationInput.sharedDefinitions) {
|
|
278
|
+
for (const gloss of definition.glosses) {
|
|
279
|
+
check(gloss, definition.source.file);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const lesson of input.notationInput.lessons) {
|
|
283
|
+
for (const definition of lesson.localDefinitions) {
|
|
284
|
+
for (const gloss of definition.glosses) {
|
|
285
|
+
check(gloss, definition.source.file, lesson.lessonId);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function checkOperatorExplained(input, out) {
|
|
291
|
+
const allowlist = effectiveOperatorAllowlist(input.operators);
|
|
292
|
+
const byId = new Map(
|
|
293
|
+
input.registry.definitions.map((definition) => [definition.id, definition])
|
|
294
|
+
);
|
|
295
|
+
const bindingsByLesson = new Map(
|
|
296
|
+
input.registry.bundles.map((bundle) => [bundle.lessonId, bundle.bindings])
|
|
297
|
+
);
|
|
298
|
+
for (const lesson of input.notationInput.lessons) {
|
|
299
|
+
const explained = /* @__PURE__ */ new Set();
|
|
300
|
+
const noteEntryOperators = (latex) => {
|
|
301
|
+
for (const operator of collectOperators(latex)) {
|
|
302
|
+
explained.add(operator.name);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
for (const definition of lesson.localDefinitions) {
|
|
306
|
+
noteEntryOperators(definition.notation);
|
|
307
|
+
}
|
|
308
|
+
for (const binding of bindingsByLesson.get(lesson.lessonId) ?? []) {
|
|
309
|
+
const definition = byId.get(binding.definitionId);
|
|
310
|
+
if (definition) noteEntryOperators(definition.notation);
|
|
311
|
+
}
|
|
312
|
+
const reported = /* @__PURE__ */ new Set();
|
|
313
|
+
for (const span of mathSpans(lesson.body)) {
|
|
314
|
+
for (const operator of collectOperators(span.latex)) {
|
|
315
|
+
if (allowlist.has(operator.name)) continue;
|
|
316
|
+
if (explained.has(operator.name)) continue;
|
|
317
|
+
if (reported.has(operator.name)) continue;
|
|
318
|
+
reported.add(operator.name);
|
|
319
|
+
if (input.lintIgnore.matches("operator-explained", operator.name)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const display = operator.source === "operatorname" ? `\\operatorname{${operator.name}}` : `\\${operator.name}`;
|
|
323
|
+
out.push({
|
|
324
|
+
code: "operator-explained",
|
|
325
|
+
severity: "warning",
|
|
326
|
+
lessonId: lesson.lessonId,
|
|
327
|
+
file: lesson.source.file,
|
|
328
|
+
glyph: operator.name,
|
|
329
|
+
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 operators.allow in course.config.ts`
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
203
335
|
const CHECKS = [
|
|
204
336
|
checkGlyphUniqueness,
|
|
205
337
|
checkNotationUnits,
|
|
206
338
|
checkConventionSingleDefinition,
|
|
207
339
|
checkNumeralsTagged,
|
|
208
340
|
checkWeakLocal,
|
|
209
|
-
checkNotationSourceLocator
|
|
341
|
+
checkNotationSourceLocator,
|
|
342
|
+
checkGlossWantsPromoting,
|
|
343
|
+
checkCardWantsDemoting,
|
|
344
|
+
checkGlossNameShape,
|
|
345
|
+
checkOperatorExplained
|
|
210
346
|
];
|
|
211
347
|
function runConsistencyChecks(input) {
|
|
212
348
|
const diagnostics = [];
|
|
@@ -223,7 +359,11 @@ function consistencyCounts(diagnostics) {
|
|
|
223
359
|
"convention-single-definition": 0,
|
|
224
360
|
"numerals-tagged": 0,
|
|
225
361
|
"weak-local": 0,
|
|
226
|
-
"notation-source-locator": 0
|
|
362
|
+
"notation-source-locator": 0,
|
|
363
|
+
"gloss-wants-promoting": 0,
|
|
364
|
+
"card-wants-demoting": 0,
|
|
365
|
+
"gloss-name-shape": 0,
|
|
366
|
+
"operator-explained": 0
|
|
227
367
|
};
|
|
228
368
|
for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
|
|
229
369
|
return counts;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
interface DiagramNodeInput {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly height: number;
|
|
5
|
+
}
|
|
6
|
+
interface DiagramEdgeInput {
|
|
7
|
+
readonly from: string;
|
|
8
|
+
readonly to: string;
|
|
9
|
+
}
|
|
10
|
+
interface DiagramLayoutOptions {
|
|
11
|
+
/** Top-to-bottom (default) or left-to-right. */
|
|
12
|
+
readonly direction?: 'TB' | 'LR';
|
|
13
|
+
readonly nodeSeparation?: number;
|
|
14
|
+
readonly rankSeparation?: number;
|
|
15
|
+
/** Uniform padding added around the computed bounding box. */
|
|
16
|
+
readonly padding?: number;
|
|
17
|
+
}
|
|
18
|
+
interface Point {
|
|
19
|
+
readonly x: number;
|
|
20
|
+
readonly y: number;
|
|
21
|
+
}
|
|
22
|
+
interface LaidOutNode {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Top-left corner, not the center dagre itself works in. */
|
|
25
|
+
readonly x: number;
|
|
26
|
+
readonly y: number;
|
|
27
|
+
readonly width: number;
|
|
28
|
+
readonly height: number;
|
|
29
|
+
}
|
|
30
|
+
interface LaidOutEdge {
|
|
31
|
+
readonly from: string;
|
|
32
|
+
readonly to: string;
|
|
33
|
+
readonly points: readonly Point[];
|
|
34
|
+
}
|
|
35
|
+
interface DiagramLayout {
|
|
36
|
+
readonly nodes: readonly LaidOutNode[];
|
|
37
|
+
readonly edges: readonly LaidOutEdge[];
|
|
38
|
+
/** The SVG canvas size a caller should use as the `viewBox`. */
|
|
39
|
+
readonly width: number;
|
|
40
|
+
readonly height: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Lay out a small directed graph. Node ids must be unique; an edge naming an
|
|
44
|
+
* undeclared node fails loudly rather than silently dropping it (dagre itself
|
|
45
|
+
* would otherwise create a phantom zero-size node).
|
|
46
|
+
*/
|
|
47
|
+
declare function layoutDiagram(nodes: readonly DiagramNodeInput[], edges: readonly DiagramEdgeInput[], options?: DiagramLayoutOptions): DiagramLayout;
|
|
48
|
+
interface BoxSizeEstimate {
|
|
49
|
+
readonly width: number;
|
|
50
|
+
readonly height: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Estimate a diagram node's box from its label text (or raw, pre-resolution
|
|
54
|
+
* LaTeX for a formula node). Deliberately generous: it is meant to avoid
|
|
55
|
+
* clipping, not to match a real render pixel-for-pixel. Pass an explicit
|
|
56
|
+
* `width`/`height` on the node to override for a label this estimate gets
|
|
57
|
+
* badly wrong.
|
|
58
|
+
*/
|
|
59
|
+
declare function estimateBoxSize(content: string, kind: 'text' | 'formula', options?: {
|
|
60
|
+
readonly maxWidth?: number;
|
|
61
|
+
}): BoxSizeEstimate;
|
|
62
|
+
|
|
63
|
+
export { type BoxSizeEstimate, type DiagramEdgeInput, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodeInput, type LaidOutEdge, type LaidOutNode, type Point, estimateBoxSize, layoutDiagram };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import dagre from "@dagrejs/dagre";
|
|
2
|
+
function layoutDiagram(nodes, edges, options = {}) {
|
|
3
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4
|
+
for (const node of nodes) {
|
|
5
|
+
if (ids.has(node.id)) {
|
|
6
|
+
throw new Error(`Diagram declares node "${node.id}" more than once.`);
|
|
7
|
+
}
|
|
8
|
+
if (!(node.width > 0) || !(node.height > 0)) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`Diagram node "${node.id}" must have a positive width and height.`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
ids.add(node.id);
|
|
14
|
+
}
|
|
15
|
+
for (const edge of edges) {
|
|
16
|
+
if (!ids.has(edge.from)) {
|
|
17
|
+
throw new Error(`Diagram edge references unknown node "${edge.from}".`);
|
|
18
|
+
}
|
|
19
|
+
if (!ids.has(edge.to)) {
|
|
20
|
+
throw new Error(`Diagram edge references unknown node "${edge.to}".`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const graph = new dagre.graphlib.Graph();
|
|
24
|
+
graph.setGraph({
|
|
25
|
+
rankdir: options.direction ?? "TB",
|
|
26
|
+
nodesep: options.nodeSeparation ?? 24,
|
|
27
|
+
ranksep: options.rankSeparation ?? 48,
|
|
28
|
+
marginx: 0,
|
|
29
|
+
marginy: 0
|
|
30
|
+
});
|
|
31
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
graph.setNode(node.id, { width: node.width, height: node.height });
|
|
34
|
+
}
|
|
35
|
+
for (const edge of edges) {
|
|
36
|
+
graph.setEdge(edge.from, edge.to);
|
|
37
|
+
}
|
|
38
|
+
dagre.layout(graph);
|
|
39
|
+
const centeredNodes = nodes.map((node) => {
|
|
40
|
+
const label = graph.node(node.id);
|
|
41
|
+
const cx = label.x ?? 0;
|
|
42
|
+
const cy = label.y ?? 0;
|
|
43
|
+
return {
|
|
44
|
+
id: node.id,
|
|
45
|
+
x: cx - node.width / 2,
|
|
46
|
+
y: cy - node.height / 2,
|
|
47
|
+
width: node.width,
|
|
48
|
+
height: node.height
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const rawEdges = edges.map((edge) => {
|
|
52
|
+
const label = graph.edge(edge.from, edge.to) ?? {};
|
|
53
|
+
return {
|
|
54
|
+
from: edge.from,
|
|
55
|
+
to: edge.to,
|
|
56
|
+
points: (label.points ?? []).map((point) => ({
|
|
57
|
+
x: point.x,
|
|
58
|
+
y: point.y
|
|
59
|
+
}))
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
const padding = options.padding ?? 12;
|
|
63
|
+
const xs = [
|
|
64
|
+
...centeredNodes.flatMap((node) => [node.x, node.x + node.width]),
|
|
65
|
+
...rawEdges.flatMap((edge) => edge.points.map((point) => point.x))
|
|
66
|
+
];
|
|
67
|
+
const ys = [
|
|
68
|
+
...centeredNodes.flatMap((node) => [node.y, node.y + node.height]),
|
|
69
|
+
...rawEdges.flatMap((edge) => edge.points.map((point) => point.y))
|
|
70
|
+
];
|
|
71
|
+
const minX = xs.length > 0 ? Math.min(...xs) : 0;
|
|
72
|
+
const minY = ys.length > 0 ? Math.min(...ys) : 0;
|
|
73
|
+
const maxX = xs.length > 0 ? Math.max(...xs) : 0;
|
|
74
|
+
const maxY = ys.length > 0 ? Math.max(...ys) : 0;
|
|
75
|
+
const shiftX = padding - minX;
|
|
76
|
+
const shiftY = padding - minY;
|
|
77
|
+
return {
|
|
78
|
+
nodes: centeredNodes.map((node) => ({
|
|
79
|
+
...node,
|
|
80
|
+
x: node.x + shiftX,
|
|
81
|
+
y: node.y + shiftY
|
|
82
|
+
})),
|
|
83
|
+
edges: rawEdges.map((edge) => ({
|
|
84
|
+
...edge,
|
|
85
|
+
points: edge.points.map((point) => ({
|
|
86
|
+
x: point.x + shiftX,
|
|
87
|
+
y: point.y + shiftY
|
|
88
|
+
}))
|
|
89
|
+
})),
|
|
90
|
+
width: maxX - minX + padding * 2,
|
|
91
|
+
height: maxY - minY + padding * 2
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const DEFAULT_MAX_WIDTH = 220;
|
|
95
|
+
const MIN_WIDTH = 90;
|
|
96
|
+
const PADDING_X = 20;
|
|
97
|
+
const PADDING_Y = 16;
|
|
98
|
+
const LINE_HEIGHT = 22;
|
|
99
|
+
const CHAR_WIDTH = {
|
|
100
|
+
text: 7.2,
|
|
101
|
+
formula: 11
|
|
102
|
+
};
|
|
103
|
+
function formulaVisibleLength(latex) {
|
|
104
|
+
const collapsed = latex.replace(/\\[a-zA-Z]+/g, "x").replace(/[{}$]/g, "").trim();
|
|
105
|
+
return Math.max(1, collapsed.length);
|
|
106
|
+
}
|
|
107
|
+
function estimateBoxSize(content, kind, options = {}) {
|
|
108
|
+
const maxWidth = options.maxWidth ?? DEFAULT_MAX_WIDTH;
|
|
109
|
+
const visibleLength = kind === "formula" ? formulaVisibleLength(content) : content.trim().length || 1;
|
|
110
|
+
const charWidth = CHAR_WIDTH[kind];
|
|
111
|
+
const naturalWidth = visibleLength * charWidth + PADDING_X * 2;
|
|
112
|
+
const width = Math.min(maxWidth, Math.max(MIN_WIDTH, naturalWidth));
|
|
113
|
+
const usableWidth = width - PADDING_X * 2;
|
|
114
|
+
const charsPerLine = Math.max(1, Math.floor(usableWidth / charWidth));
|
|
115
|
+
const lines = Math.max(1, Math.ceil(visibleLength / charsPerLine));
|
|
116
|
+
const height = lines * LINE_HEIGHT + PADDING_Y * 2;
|
|
117
|
+
return { width: Math.round(width), height: Math.round(height) };
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
estimateBoxSize,
|
|
121
|
+
layoutDiagram
|
|
122
|
+
};
|
|
@@ -44,6 +44,11 @@ interface EquationLessonInput {
|
|
|
44
44
|
readonly slug: string;
|
|
45
45
|
readonly file: string;
|
|
46
46
|
readonly body: string;
|
|
47
|
+
/**
|
|
48
|
+
* The lesson's 1-based ordinal in the course. When set, equations are
|
|
49
|
+
* numbered `‹chapter›.‹n›`; when omitted, the legacy `##`-section scheme.
|
|
50
|
+
*/
|
|
51
|
+
readonly chapter?: number;
|
|
47
52
|
}
|
|
48
53
|
declare function validateEquations(lessons: readonly EquationLessonInput[]): EquationValidation;
|
|
49
54
|
|
|
@@ -25,7 +25,7 @@ function validateEquations(lessons) {
|
|
|
25
25
|
)) {
|
|
26
26
|
const seen = /* @__PURE__ */ new Set();
|
|
27
27
|
const table = {};
|
|
28
|
-
for (const raw of scanEquationLabels(lesson.body)) {
|
|
28
|
+
for (const raw of scanEquationLabels(lesson.body, lesson.chapter)) {
|
|
29
29
|
if (seen.has(raw.key)) {
|
|
30
30
|
diagnostics.push({
|
|
31
31
|
code: "eq-duplicate-key",
|
|
@@ -8,42 +8,100 @@ export function parseEquationRef(raw: any): {
|
|
|
8
8
|
} | undefined;
|
|
9
9
|
/** Remove every `\label{eq:…}` (and the whitespace it leaves) from a math source. */
|
|
10
10
|
export function stripEquationLabels(latex: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* The visible number for one display equation. With `chapter` (the lesson's
|
|
13
|
+
* course ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings
|
|
14
|
+
* are ignored; without it, the legacy `##`-section scheme.
|
|
15
|
+
*/
|
|
16
|
+
export function displayEquationNumber({ chapter, section, indexInSection, indexInLesson, }: {
|
|
17
|
+
chapter: any;
|
|
18
|
+
section: any;
|
|
19
|
+
indexInSection: any;
|
|
20
|
+
indexInLesson: any;
|
|
21
|
+
}): string;
|
|
22
|
+
/** The positional anchor id for an unkeyed equation, e.g. `2.4` → `eq-2-4`. */
|
|
23
|
+
export function equationAnchorId({ key, number }: {
|
|
24
|
+
key: any;
|
|
25
|
+
number: any;
|
|
26
|
+
}): string;
|
|
27
|
+
export function stripFencedCode(markdown: any): string;
|
|
28
|
+
/**
|
|
29
|
+
* Blank the body of every block-level MDX component (`<Capitalised …> … </…>`)
|
|
30
|
+
* so a `$$…$$` used to illustrate a worked example inside `<CompactExample>`
|
|
31
|
+
* (or an `<Aside>`, etc.) is not counted as a lesson-body equation. The
|
|
32
|
+
* outermost match wins, so nested components are blanked in one pass;
|
|
33
|
+
* newlines are kept so line/section structure is unchanged.
|
|
34
|
+
*
|
|
35
|
+
* `keep`, when given, leaves a component of that name untouched instead of
|
|
36
|
+
* blanking it — `keyed-jsx-floats.mjs` uses this to blank every *other*
|
|
37
|
+
* component while still finding `<Table>`/`<Figure>` tags at the top level.
|
|
38
|
+
*/
|
|
39
|
+
export function stripJsxComponents(markdown: any, keep: any): string;
|
|
11
40
|
/**
|
|
12
41
|
* Every keyed display equation in a Markdown body, in appearance order, with
|
|
13
|
-
* its assigned number. `$$…$$` blocks only
|
|
14
|
-
* the
|
|
42
|
+
* its assigned number. `$$…$$` blocks only. With `chapter` (the lesson's
|
|
43
|
+
* course ordinal) the number is `‹chapter›.‹n›`, `n` sequential over every
|
|
44
|
+
* display block in the lesson; without it, `##` headings advance a section
|
|
45
|
+
* counter and the number is `‹section›.‹indexInSection›`. Either way every
|
|
46
|
+
* display block advances the index, so a keyed equation's number reflects its
|
|
47
|
+
* position among *all* display equations, not only the keyed ones.
|
|
15
48
|
*/
|
|
16
|
-
export function scanEquationLabels(markdown: any): {
|
|
17
|
-
key: string
|
|
49
|
+
export function scanEquationLabels(markdown: any, chapter: any): {
|
|
50
|
+
key: string;
|
|
18
51
|
number: string;
|
|
19
52
|
section: number;
|
|
20
53
|
indexInSection: number;
|
|
21
54
|
}[];
|
|
22
55
|
/**
|
|
23
|
-
* The mdast equivalent of `scanEquationLabels
|
|
24
|
-
* order, advancing the section counter on every
|
|
25
|
-
*
|
|
56
|
+
* The mdast equivalent of `scanEquationLabels` for *every* display equation:
|
|
57
|
+
* walk the tree in document order, advancing the section counter on every
|
|
58
|
+
* depth-2 heading and emitting a record for every `math` node. `key` is the
|
|
59
|
+
* `\label{eq:…}` payload when present, else `null`. `chapter` is threaded the
|
|
60
|
+
* same way as in `scanEquationLabels`.
|
|
26
61
|
*/
|
|
27
|
-
export function
|
|
62
|
+
export function collectDisplayEquations(tree: any, chapter: any): {
|
|
63
|
+
key: string | null;
|
|
64
|
+
number: string;
|
|
65
|
+
section: number;
|
|
66
|
+
indexInSection: number;
|
|
67
|
+
node: any;
|
|
68
|
+
}[];
|
|
69
|
+
/**
|
|
70
|
+
* The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
|
|
71
|
+
* `scanEquationLabels`. Numbers reflect position among all display equations.
|
|
72
|
+
*/
|
|
73
|
+
export function collectEquationLabels(tree: any, chapter: any): {
|
|
74
|
+
key: string | null;
|
|
75
|
+
number: string;
|
|
76
|
+
section: number;
|
|
77
|
+
indexInSection: number;
|
|
78
|
+
node: any;
|
|
79
|
+
}[];
|
|
28
80
|
/**
|
|
29
|
-
* Equation identity (Step D7) — shared, framework-free helpers.
|
|
81
|
+
* Equation identity (Step D7, extended F3) — shared, framework-free helpers.
|
|
30
82
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
83
|
+
* Every display equation (`$$…$$`) in a lesson is numbered and given an `id`
|
|
84
|
+
* from appearance order. When the caller supplies a `chapter` (the lesson's
|
|
85
|
+
* ordinal position in the course), the number is `‹chapter›.‹n›` — `n` counts
|
|
86
|
+
* every display equation in the lesson and `##` headings do not affect it
|
|
87
|
+
* (`(2.4)` = fourth display equation of the second lesson). With no `chapter`
|
|
88
|
+
* the number falls back to the `##`-section scheme `‹section›.‹indexInSection›`
|
|
89
|
+
* (used by hand-built test trees and non-lesson pages). An author additionally
|
|
90
|
+
* marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
|
|
91
|
+
* block (inside math, so the `:` is safe and the form matches amsmath) when
|
|
92
|
+
* they want a *stable* anchor that survives edits and cross-page references:
|
|
39
93
|
*
|
|
40
94
|
* [[eq-discount-factor-def]] same page → "(2.4)" linked to #eq-…
|
|
41
95
|
* [[foundations/present-value#eq-pv]] cross page → number from the manifest
|
|
42
96
|
*
|
|
97
|
+
* An unkeyed equation still gets a positional anchor (`#eq-2-4`) so it can be
|
|
98
|
+
* deep-linked, but only a keyed equation is referenceable by name.
|
|
99
|
+
*
|
|
43
100
|
* `scanEquationLabels` (raw Markdown) and `collectEquationLabels` (mdast tree)
|
|
44
101
|
* MUST agree — `tests/notation/equations.test.ts` checks that on the real
|
|
45
102
|
* corpus. Both are here so `manifest.ts` and `remark-notation.mjs` share one
|
|
46
|
-
* numbering algorithm.
|
|
103
|
+
* numbering algorithm. `collectDisplayEquations` is the superset the render
|
|
104
|
+
* path uses to number and anchor every display equation, keyed or not.
|
|
47
105
|
*/
|
|
48
106
|
/** `eq:` key grammar — the notation-key shape without the dotted namespaces. */
|
|
49
107
|
export const EQ_KEY_PATTERN: RegExp;
|