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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formulaExplainKeys,
|
|
3
|
+
formulaGlyphScope
|
|
4
|
+
} from "./gloss.js";
|
|
5
|
+
import { resolveReferenceId } from "./references.js";
|
|
6
|
+
const dependenciesByLocals = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function registerPageNotationDependencies(locals, keys, context) {
|
|
8
|
+
let byKey = dependenciesByLocals.get(locals);
|
|
9
|
+
if (!byKey) {
|
|
10
|
+
byKey = /* @__PURE__ */ new Map();
|
|
11
|
+
dependenciesByLocals.set(locals, byKey);
|
|
12
|
+
}
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
let contexts = byKey.get(key);
|
|
15
|
+
if (!contexts) {
|
|
16
|
+
contexts = /* @__PURE__ */ new Set();
|
|
17
|
+
byKey.set(key, contexts);
|
|
18
|
+
}
|
|
19
|
+
contexts.add(context);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function pageNotationDependencies(locals) {
|
|
23
|
+
const byKey = dependenciesByLocals.get(locals);
|
|
24
|
+
if (!byKey) return [];
|
|
25
|
+
return [...byKey].flatMap(
|
|
26
|
+
([key, contexts]) => [...contexts].map((context) => ({ key, context }))
|
|
27
|
+
).sort(
|
|
28
|
+
(left, right) => left.key.localeCompare(right.key) || left.context.localeCompare(right.context)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function routeIdFromLocals(locals) {
|
|
32
|
+
const id = locals.starlightRoute?.entry?.id;
|
|
33
|
+
return typeof id === "string" ? id : void 0;
|
|
34
|
+
}
|
|
35
|
+
function notationKeysInMathNodes(nodes) {
|
|
36
|
+
const keys = /* @__PURE__ */ new Set();
|
|
37
|
+
const visit = (node) => {
|
|
38
|
+
if (!node || typeof node !== "object") return;
|
|
39
|
+
const candidate = node;
|
|
40
|
+
const key = candidate.properties?.["data-notation-key"];
|
|
41
|
+
if (key) keys.add(key);
|
|
42
|
+
for (const child of candidate.children ?? []) visit(child);
|
|
43
|
+
};
|
|
44
|
+
for (const node of nodes) visit(node);
|
|
45
|
+
return [...keys].sort();
|
|
46
|
+
}
|
|
47
|
+
function manifestDefinition(definition, cardsByKey) {
|
|
48
|
+
return {
|
|
49
|
+
id: definition.id,
|
|
50
|
+
key: definition.key,
|
|
51
|
+
scope: definition.kind === "shared" ? "shared" : "local",
|
|
52
|
+
notation: definition.notation,
|
|
53
|
+
label: definition.label,
|
|
54
|
+
summary: definition.summary,
|
|
55
|
+
editorialStatus: definition.status,
|
|
56
|
+
seeAlso: [...definition.seeAlso],
|
|
57
|
+
glosses: [...definition.glosses],
|
|
58
|
+
formulaScope: formulaGlyphScope({
|
|
59
|
+
key: definition.key,
|
|
60
|
+
notation: definition.notation,
|
|
61
|
+
glosses: definition.glosses,
|
|
62
|
+
cardsByKey,
|
|
63
|
+
...definition.formula === void 0 ? {} : { formula: definition.formula }
|
|
64
|
+
}),
|
|
65
|
+
...definition.units === void 0 ? {} : { units: definition.units },
|
|
66
|
+
...definition.formula === void 0 ? {} : { formula: definition.formula }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function notationDefinitionsForRenderedPage(manifest, lessonId, dependencies) {
|
|
70
|
+
const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
|
|
71
|
+
if (!lesson) return [];
|
|
72
|
+
const definitionsById = new Map(
|
|
73
|
+
manifest.notation.definitions.map((definition) => [definition.id, definition])
|
|
74
|
+
);
|
|
75
|
+
const availableIds = new Set(definitionsById.keys());
|
|
76
|
+
const ids = new Set(lesson.notation.definitionIds);
|
|
77
|
+
const contextsByKey = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const dependency of dependencies) {
|
|
79
|
+
let contexts = contextsByKey.get(dependency.key);
|
|
80
|
+
if (!contexts) {
|
|
81
|
+
contexts = /* @__PURE__ */ new Set();
|
|
82
|
+
contextsByKey.set(dependency.key, contexts);
|
|
83
|
+
}
|
|
84
|
+
contexts.add(dependency.context);
|
|
85
|
+
const definitionId = resolveReferenceId(
|
|
86
|
+
dependency.key,
|
|
87
|
+
{ kind: "page", lessonId },
|
|
88
|
+
availableIds
|
|
89
|
+
);
|
|
90
|
+
if (!definitionId) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Notation payload error in lesson "${lessonId}": ${dependency.context} emitted data-notation-key="${dependency.key}", but that key is not defined in this lesson or the shared notation registry.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
ids.add(definitionId);
|
|
96
|
+
}
|
|
97
|
+
const pending = [...ids];
|
|
98
|
+
while (pending.length > 0) {
|
|
99
|
+
const id = pending.pop();
|
|
100
|
+
if (!id) continue;
|
|
101
|
+
const definition = definitionsById.get(id);
|
|
102
|
+
for (const reference of definition?.resolvedReferences ?? []) {
|
|
103
|
+
if (ids.has(reference.definitionId)) continue;
|
|
104
|
+
ids.add(reference.definitionId);
|
|
105
|
+
pending.push(reference.definitionId);
|
|
106
|
+
}
|
|
107
|
+
for (const key of formulaExplainKeys(definition?.formula ?? "")) {
|
|
108
|
+
if (!definition) continue;
|
|
109
|
+
const formulaDependencyId = resolveReferenceId(
|
|
110
|
+
key,
|
|
111
|
+
definition.scope,
|
|
112
|
+
availableIds
|
|
113
|
+
);
|
|
114
|
+
if (!formulaDependencyId || ids.has(formulaDependencyId)) continue;
|
|
115
|
+
ids.add(formulaDependencyId);
|
|
116
|
+
pending.push(formulaDependencyId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const definitionIdByKey = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const id of ids) {
|
|
121
|
+
const definition = definitionsById.get(id);
|
|
122
|
+
if (!definition) continue;
|
|
123
|
+
const previous = definitionIdByKey.get(definition.key);
|
|
124
|
+
if (previous && previous !== id) {
|
|
125
|
+
const context = [...contextsByKey.get(definition.key) ?? []].join(", ");
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Notation payload error in lesson "${lessonId}": key "${definition.key}" would have two meanings (${previous} and ${id})` + (context ? ` after ${context} emitted it.` : ".")
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
definitionIdByKey.set(definition.key, id);
|
|
131
|
+
}
|
|
132
|
+
const cardsByKey = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const definition of manifest.notation.definitions) {
|
|
134
|
+
if (definition.kind !== "shared") continue;
|
|
135
|
+
cardsByKey.set(definition.key, {
|
|
136
|
+
key: definition.key,
|
|
137
|
+
notation: definition.notation
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const existingById = new Map(
|
|
141
|
+
lesson.notation.definitions.map((definition) => [definition.id, definition])
|
|
142
|
+
);
|
|
143
|
+
return [...ids].sort().flatMap((id) => {
|
|
144
|
+
const existing = existingById.get(id);
|
|
145
|
+
if (existing) return [existing];
|
|
146
|
+
const definition = definitionsById.get(id);
|
|
147
|
+
return definition ? [manifestDefinition(definition, cardsByKey)] : [];
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function missingNotationPayloadKeys(html) {
|
|
151
|
+
const attributeValues = (name) => {
|
|
152
|
+
const values = /* @__PURE__ */ new Set();
|
|
153
|
+
const pattern = new RegExp(`${name}=(?:"([^"]+)"|'([^']+)')`, "g");
|
|
154
|
+
for (const match of html.matchAll(pattern)) {
|
|
155
|
+
const value = match[1] ?? match[2];
|
|
156
|
+
if (value) values.add(value);
|
|
157
|
+
}
|
|
158
|
+
return values;
|
|
159
|
+
};
|
|
160
|
+
const triggers = attributeValues("data-notation-key");
|
|
161
|
+
const entries = attributeValues("data-notation-entry");
|
|
162
|
+
return [...triggers].filter((key) => !entries.has(key)).sort();
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
missingNotationPayloadKeys,
|
|
166
|
+
notationDefinitionsForRenderedPage,
|
|
167
|
+
notationKeysInMathNodes,
|
|
168
|
+
pageNotationDependencies,
|
|
169
|
+
registerPageNotationDependencies,
|
|
170
|
+
routeIdFromLocals
|
|
171
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Diagnostics for raw/unwrapped floats — see `raw-floats.mjs`. */
|
|
2
|
+
type RawFloatDiagnosticCode = 'raw-table-markdown' | 'raw-table-html' | 'diagram-unkeyed' | 'diagram-nested';
|
|
3
|
+
interface RawFloatDiagnostic {
|
|
4
|
+
readonly code: RawFloatDiagnosticCode;
|
|
5
|
+
readonly severity: 'warning';
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly file?: string;
|
|
8
|
+
readonly lessonId?: string;
|
|
9
|
+
}
|
|
10
|
+
interface RawFloatLessonInput {
|
|
11
|
+
readonly lessonId: string;
|
|
12
|
+
readonly slug: string;
|
|
13
|
+
readonly file: string;
|
|
14
|
+
readonly body: string;
|
|
15
|
+
}
|
|
16
|
+
declare function validateRawFloats(lessons: readonly RawFloatLessonInput[]): readonly RawFloatDiagnostic[];
|
|
17
|
+
|
|
18
|
+
export { type RawFloatDiagnostic, type RawFloatDiagnosticCode, type RawFloatLessonInput, validateRawFloats };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { scanRawFloats } from "./raw-floats.mjs";
|
|
2
|
+
function plural(count, noun) {
|
|
3
|
+
return count === 1 ? noun : `${noun}s`;
|
|
4
|
+
}
|
|
5
|
+
function validateRawFloats(lessons) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
const push = (lesson, code, count, message) => {
|
|
8
|
+
if (count === 0) return;
|
|
9
|
+
diagnostics.push({
|
|
10
|
+
code,
|
|
11
|
+
severity: "warning",
|
|
12
|
+
message: message(count),
|
|
13
|
+
file: lesson.file,
|
|
14
|
+
lessonId: lesson.lessonId
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
for (const lesson of [...lessons].sort(
|
|
18
|
+
(a, b) => a.lessonId.localeCompare(b.lessonId)
|
|
19
|
+
)) {
|
|
20
|
+
const found = scanRawFloats(lesson.body);
|
|
21
|
+
push(
|
|
22
|
+
lesson,
|
|
23
|
+
"raw-table-markdown",
|
|
24
|
+
found.markdownTableCount,
|
|
25
|
+
(count) => `${lesson.lessonId} has ${count} raw Markdown ${plural(count, "table")} not wrapped in <Table>; ${count === 1 ? "it has" : "they have"} no stable number and cannot be referenced with [[tab-\u2026]]. Wrap it in <Table id="\u2026">\u2026</Table> to give it one.`
|
|
26
|
+
);
|
|
27
|
+
push(
|
|
28
|
+
lesson,
|
|
29
|
+
"raw-table-html",
|
|
30
|
+
found.htmlTableCount,
|
|
31
|
+
(count) => `${lesson.lessonId} has ${count} raw HTML <table> ${plural(count, "element")} not wrapped in <Table>; ${count === 1 ? "it has" : "they have"} no stable number and cannot be referenced with [[tab-\u2026]]. Wrap it in <Table id="\u2026">\u2026</Table> to give it one.`
|
|
32
|
+
);
|
|
33
|
+
push(
|
|
34
|
+
lesson,
|
|
35
|
+
"diagram-unkeyed",
|
|
36
|
+
found.unkeyedDiagramCount,
|
|
37
|
+
(count) => `${lesson.lessonId} has ${count} <Diagram> ${plural(count, "component")} with no id; ${count === 1 ? "it" : "they"} cannot be referenced with [[dia-\u2026]]. Add an id to give it a stable reference.`
|
|
38
|
+
);
|
|
39
|
+
push(
|
|
40
|
+
lesson,
|
|
41
|
+
"diagram-nested",
|
|
42
|
+
found.nestedDiagramCount,
|
|
43
|
+
(count) => `${lesson.lessonId} has ${count} <Diagram> ${plural(count, "component")} nested inside another component; ${count === 1 ? "it is" : "they are"} invisible to diagram numbering. Move it to the top level (or inside <Memo>/<ShortNote>), or wrap it in <Figure> to give it identity.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
diagnostics.sort(
|
|
47
|
+
(a, b) => a.code.localeCompare(b.code) || (a.lessonId ?? "").localeCompare(b.lessonId ?? "")
|
|
48
|
+
);
|
|
49
|
+
return diagnostics;
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
validateRawFloats
|
|
53
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts of raw/unwrapped content in one lesson body. See the module doc for
|
|
3
|
+
* what each count means and why `<Diagram>` needs the extra visible/nested
|
|
4
|
+
* split that raw tables don't.
|
|
5
|
+
*/
|
|
6
|
+
export function scanRawFloats(markdown: any): {
|
|
7
|
+
markdownTableCount: number;
|
|
8
|
+
htmlTableCount: number;
|
|
9
|
+
nestedDiagramCount: number;
|
|
10
|
+
unkeyedDiagramCount: number;
|
|
11
|
+
};
|
|
12
|
+
/** The mdast equivalent of `scanRawFloats` — same counts, plus the nodes. */
|
|
13
|
+
export function collectRawFloats(tree: any): {
|
|
14
|
+
markdownTableNodes: any[];
|
|
15
|
+
htmlTableNodes: any[];
|
|
16
|
+
nestedDiagramNodes: any[];
|
|
17
|
+
unkeyedDiagramNodes: any[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection for content invisible to keyed-float validation:
|
|
3
|
+
*
|
|
4
|
+
* - a raw Markdown pipe table or raw HTML `<table>` that was never wrapped in
|
|
5
|
+
* `<Table>`/`<Figure>`, so it can never get a stable number or be
|
|
6
|
+
* referenced with `[[tab-…]]`;
|
|
7
|
+
* - an Explico `<Diagram>` with no `id` (can't be referenced with
|
|
8
|
+
* `[[dia-…]]` even though it participates in diagram numbering);
|
|
9
|
+
* - an Explico `<Diagram>` nested inside some other, non-transparent
|
|
10
|
+
* component (anything but `<Memo>`/`<ShortNote>`, or `<Table>`/`<Figure>`
|
|
11
|
+
* which legitimately gives it identity) — invisible to
|
|
12
|
+
* `keyed-jsx-floats.mjs`'s top-level-only scan entirely, so it never even
|
|
13
|
+
* reaches diagram numbering.
|
|
14
|
+
*
|
|
15
|
+
* A raw table nested in `<Table>`/`<Figure>` is the intended, wrapped case
|
|
16
|
+
* and is not flagged; nesting inside anything else (`<CompactExample>`,
|
|
17
|
+
* `<Aside>`, `<Memo>`, …) still is, because a raw table has no numbering
|
|
18
|
+
* scheme of its own to opt into the way a `<Diagram>`'s top-level/`<Memo>`-
|
|
19
|
+
* transparent scan does.
|
|
20
|
+
*
|
|
21
|
+
* `scanRawFloats` (raw Markdown, for the manifest builder) and
|
|
22
|
+
* `collectRawFloats` (mdast, for `remark-notation.mjs`) MUST agree on counts
|
|
23
|
+
* — `tests/notation/raw-floats.test.ts` checks that, the same contract
|
|
24
|
+
* `keyed-jsx-floats.mjs` holds `scanKeyedJsxFloats`/`collectKeyedJsxFloats` to.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { stripFencedCode } from './equations.mjs';
|
|
28
|
+
import { collectKeyedJsxFloats, scanKeyedJsxFloats } from './keyed-jsx-floats.mjs';
|
|
29
|
+
|
|
30
|
+
/** Blank `<Table>…</Table>` and `<Figure>…</Figure>` bodies — legitimately wrapped, not raw. */
|
|
31
|
+
function blankFloatWrappers(text) {
|
|
32
|
+
return text.replace(
|
|
33
|
+
/<(Table|Figure)\b(?:\s[^>]*)?>[\s\S]*?<\/\1\s*>/g,
|
|
34
|
+
(block) => block.replace(/[^\n]/g, ' '),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Line index of every GFM pipe-table header + delimiter-row pair. */
|
|
39
|
+
function findMarkdownTableStarts(text) {
|
|
40
|
+
const lines = text.split('\n');
|
|
41
|
+
const delimiterRow =
|
|
42
|
+
/^[ \t]{0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
|
|
43
|
+
const hits = [];
|
|
44
|
+
for (let index = 0; index < lines.length - 1; index++) {
|
|
45
|
+
const header = lines[index];
|
|
46
|
+
const delimiter = lines[index + 1];
|
|
47
|
+
if (!header.includes('|')) continue;
|
|
48
|
+
if (!delimiter.includes('-') || !delimiterRow.test(delimiter)) continue;
|
|
49
|
+
hits.push(index);
|
|
50
|
+
}
|
|
51
|
+
return hits;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findHtmlTableTags(text) {
|
|
55
|
+
return [...text.matchAll(/<table\b/gi)];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findDiagramTags(text) {
|
|
59
|
+
return [...text.matchAll(/<Diagram\b/g)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Counts of raw/unwrapped content in one lesson body. See the module doc for
|
|
64
|
+
* what each count means and why `<Diagram>` needs the extra visible/nested
|
|
65
|
+
* split that raw tables don't.
|
|
66
|
+
*/
|
|
67
|
+
export function scanRawFloats(markdown) {
|
|
68
|
+
const codeStripped = stripFencedCode(String(markdown));
|
|
69
|
+
const withoutWrappedFloats = blankFloatWrappers(codeStripped);
|
|
70
|
+
|
|
71
|
+
const markdownTableCount = findMarkdownTableStarts(withoutWrappedFloats).length;
|
|
72
|
+
const htmlTableCount = findHtmlTableTags(withoutWrappedFloats).length;
|
|
73
|
+
|
|
74
|
+
const diagramsOutsideWrappers = findDiagramTags(withoutWrappedFloats).length;
|
|
75
|
+
const visibleDiagrams = scanKeyedJsxFloats(markdown, { tagName: 'Diagram' });
|
|
76
|
+
const nestedDiagramCount = Math.max(
|
|
77
|
+
0,
|
|
78
|
+
diagramsOutsideWrappers - visibleDiagrams.length,
|
|
79
|
+
);
|
|
80
|
+
const unkeyedDiagramCount = visibleDiagrams.filter(
|
|
81
|
+
(diagram) => diagram.key == null,
|
|
82
|
+
).length;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
markdownTableCount,
|
|
86
|
+
htmlTableCount,
|
|
87
|
+
nestedDiagramCount,
|
|
88
|
+
unkeyedDiagramCount,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The mdast equivalent of `scanRawFloats` — same counts, plus the nodes. */
|
|
93
|
+
export function collectRawFloats(tree) {
|
|
94
|
+
const markdownTableNodes = [];
|
|
95
|
+
const htmlTableNodes = [];
|
|
96
|
+
const allDiagramNodes = [];
|
|
97
|
+
|
|
98
|
+
const walk = (node) => {
|
|
99
|
+
if (!node || typeof node !== 'object') return;
|
|
100
|
+
if (
|
|
101
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
102
|
+
(node.name === 'Table' || node.name === 'Figure')
|
|
103
|
+
) {
|
|
104
|
+
return; // legitimately wrapped — don't descend
|
|
105
|
+
}
|
|
106
|
+
if (node.type === 'table') markdownTableNodes.push(node);
|
|
107
|
+
if (node.type === 'html' && /<table\b/i.test(String(node.value ?? ''))) {
|
|
108
|
+
htmlTableNodes.push(node);
|
|
109
|
+
}
|
|
110
|
+
if (node.type === 'mdxJsxFlowElement' && node.name === 'Diagram') {
|
|
111
|
+
allDiagramNodes.push(node);
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(node.children)) {
|
|
114
|
+
for (const child of node.children) walk(child);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
walk(tree);
|
|
118
|
+
|
|
119
|
+
const visibleDiagrams = collectKeyedJsxFloats(tree, { tagName: 'Diagram' });
|
|
120
|
+
const visibleNodes = new Set(visibleDiagrams.map((diagram) => diagram.node));
|
|
121
|
+
const nestedDiagramNodes = allDiagramNodes.filter(
|
|
122
|
+
(node) => !visibleNodes.has(node),
|
|
123
|
+
);
|
|
124
|
+
const unkeyedDiagramNodes = visibleDiagrams
|
|
125
|
+
.filter((diagram) => diagram.key == null)
|
|
126
|
+
.map((diagram) => diagram.node);
|
|
127
|
+
|
|
128
|
+
return { markdownTableNodes, htmlTableNodes, nestedDiagramNodes, unkeyedDiagramNodes };
|
|
129
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export default function rehypeHeadingAnchors(): (tree: any, file: any) => void;
|
|
1
|
+
export default function rehypeHeadingAnchors(options?: {}): (tree: any, file: any) => void;
|
|
@@ -22,6 +22,27 @@
|
|
|
22
22
|
* idempotent (only assigns a missing `id`, and rebuilds `file.data.astro.headings`
|
|
23
23
|
* from an unchanged document order), so this plugin runs it first and Astro's
|
|
24
24
|
* later pass becomes a no-op for these headings.
|
|
25
|
+
*
|
|
26
|
+
* Collapsibility (docs/plans/collapsible-disclosure.md Part 3) has a
|
|
27
|
+
* site-wide default — `defaultCollapsible: { sections, subsections }`, an
|
|
28
|
+
* option to this plugin (see `astro.config.mjs`), each of `{ collapsible,
|
|
29
|
+
* defaultOpen }` — plus a per-heading marker an author can append to
|
|
30
|
+
* override it for one heading:
|
|
31
|
+
*
|
|
32
|
+
* - `[collapsible]` — collapsible, starts open, regardless of the default.
|
|
33
|
+
* - `[collapsed]` — collapsible, starts *closed*, regardless of the default.
|
|
34
|
+
* - `[no-collapsible]` — never collapsible, regardless of the default.
|
|
35
|
+
* - no marker — falls back to the site default for this heading's rank:
|
|
36
|
+
* `sections` governs `h2`, `subsections` governs `h3`-`h6`. `collapsible`
|
|
37
|
+
* decides whether it's wrapped at all; `defaultOpen` decides its initial
|
|
38
|
+
* state when it is (ignored, like the rest of the category, when
|
|
39
|
+
* `collapsible` is `false`).
|
|
40
|
+
*
|
|
41
|
+
* The marker is stripped from the rendered text and the *resolved* decision
|
|
42
|
+
* (not the raw marker) becomes `data-collapsible="true"` +
|
|
43
|
+
* `data-default-open="true"|"false"` on the wrapper — `CollapsibleSections.astro`
|
|
44
|
+
* only wraps headings carrying `data-collapsible` and never needs to know
|
|
45
|
+
* about the config or the marker syntax itself.
|
|
25
46
|
*/
|
|
26
47
|
|
|
27
48
|
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
@@ -47,6 +68,50 @@ function headingText(node) {
|
|
|
47
68
|
return text.replace(/\s+/g, ' ').trim();
|
|
48
69
|
}
|
|
49
70
|
|
|
71
|
+
// A heading's marker, e.g. `## Some title [collapsed]`, stripped from the
|
|
72
|
+
// rendered text. Square brackets, not curly braces (`{collapsed}`) — every
|
|
73
|
+
// lesson here is authored as `.mdx`, where `{...}` is a JS expression
|
|
74
|
+
// container (MDX tries to evaluate `collapsed` as a variable and the build
|
|
75
|
+
// fails), not literal text the way it is in plain Markdown. An unmatched
|
|
76
|
+
// `[collapsed]` with no link/reference definition of that name is just
|
|
77
|
+
// literal text in both dialects. Deliberately not a general attribute syntax
|
|
78
|
+
// (e.g. `{#id .class}`) — this codebase has no remark-attr-style plugin, and
|
|
79
|
+
// three purpose-built tokens are enough for what an author needs to say
|
|
80
|
+
// about a heading here.
|
|
81
|
+
const COLLAPSIBLE_MARKER = /\s*\[(collapsible|collapsed|no-collapsible)\]\s*$/i;
|
|
82
|
+
|
|
83
|
+
function stripCollapsibleMarker(headingNode) {
|
|
84
|
+
const children = headingNode.children;
|
|
85
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
86
|
+
const node = children[index];
|
|
87
|
+
if (node.type !== 'text') return null; // trailing non-text node: no marker
|
|
88
|
+
const match = COLLAPSIBLE_MARKER.exec(node.value);
|
|
89
|
+
if (match) {
|
|
90
|
+
node.value = node.value.slice(0, match.index);
|
|
91
|
+
if (!node.value) children.splice(index, 1);
|
|
92
|
+
return match[1].toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
if (node.value.trim()) return null; // real trailing text, no marker present
|
|
95
|
+
// trailing whitespace-only text node: keep looking further back
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a heading's marker (or lack of one) plus the site-wide default for
|
|
102
|
+
* its rank into a final `{ collapsible, defaultOpen }` decision.
|
|
103
|
+
*/
|
|
104
|
+
function resolveCollapsible(marker, tagName, defaultCollapsible) {
|
|
105
|
+
if (marker === 'no-collapsible') return { collapsible: false };
|
|
106
|
+
if (marker === 'collapsed') return { collapsible: true, defaultOpen: false };
|
|
107
|
+
if (marker === 'collapsible') return { collapsible: true, defaultOpen: true };
|
|
108
|
+
const rankDefault =
|
|
109
|
+
tagName === 'h2' ? defaultCollapsible.sections : defaultCollapsible.subsections;
|
|
110
|
+
return rankDefault.collapsible
|
|
111
|
+
? { collapsible: true, defaultOpen: rankDefault.defaultOpen }
|
|
112
|
+
: { collapsible: false };
|
|
113
|
+
}
|
|
114
|
+
|
|
50
115
|
function isHeadingWrapper(node) {
|
|
51
116
|
const className = node?.properties?.className;
|
|
52
117
|
return (
|
|
@@ -97,10 +162,44 @@ function anchorLink(id, label) {
|
|
|
97
162
|
};
|
|
98
163
|
}
|
|
99
164
|
|
|
100
|
-
|
|
165
|
+
const RANK_DEFAULT = { collapsible: false, defaultOpen: true };
|
|
166
|
+
|
|
167
|
+
export default function rehypeHeadingAnchors(options = {}) {
|
|
168
|
+
const defaultCollapsible = {
|
|
169
|
+
sections: { ...RANK_DEFAULT, ...options.defaultCollapsible?.sections },
|
|
170
|
+
subsections: { ...RANK_DEFAULT, ...options.defaultCollapsible?.subsections },
|
|
171
|
+
};
|
|
101
172
|
const assignHeadingIds = rehypeHeadingIds();
|
|
102
173
|
|
|
103
174
|
return function transform(tree, file) {
|
|
175
|
+
// A marker must be stripped *before* `rehypeHeadingIds` runs — otherwise
|
|
176
|
+
// the generated slug is built from text that still contains it (e.g.
|
|
177
|
+
// `two-ways-to-name-the-same-point-collapsed` instead of the clean
|
|
178
|
+
// title). `collapsibleHeadings` records each heading node's *resolved*
|
|
179
|
+
// decision, since the marker text is gone by the time the main wrapping
|
|
180
|
+
// walk below runs.
|
|
181
|
+
const collapsibleHeadings = new WeakMap();
|
|
182
|
+
const stripMarkers = (node) => {
|
|
183
|
+
if (!node || typeof node !== 'object' || !Array.isArray(node.children)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const child of node.children) {
|
|
187
|
+
if (
|
|
188
|
+
child?.type === 'element' &&
|
|
189
|
+
typeof child.tagName === 'string' &&
|
|
190
|
+
HEADING_TAG.test(child.tagName)
|
|
191
|
+
) {
|
|
192
|
+
const marker = stripCollapsibleMarker(child);
|
|
193
|
+
collapsibleHeadings.set(
|
|
194
|
+
child,
|
|
195
|
+
resolveCollapsible(marker, child.tagName, defaultCollapsible),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
stripMarkers(child);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
stripMarkers(tree);
|
|
202
|
+
|
|
104
203
|
assignHeadingIds(tree, file);
|
|
105
204
|
|
|
106
205
|
const walk = (node) => {
|
|
@@ -123,12 +222,19 @@ export default function rehypeHeadingAnchors() {
|
|
|
123
222
|
child.properties.id.length > 0
|
|
124
223
|
) {
|
|
125
224
|
const id = child.properties.id;
|
|
225
|
+
const decision = collapsibleHeadings.get(child) ?? { collapsible: false };
|
|
126
226
|
const label = `Section titled “${headingText(child)}”`;
|
|
127
227
|
node.children[index] = {
|
|
128
228
|
type: 'element',
|
|
129
229
|
tagName: 'div',
|
|
130
230
|
properties: {
|
|
131
231
|
className: ['sl-heading-wrapper', `level-${child.tagName}`],
|
|
232
|
+
...(decision.collapsible
|
|
233
|
+
? {
|
|
234
|
+
'data-collapsible': 'true',
|
|
235
|
+
'data-default-open': String(decision.defaultOpen),
|
|
236
|
+
}
|
|
237
|
+
: {}),
|
|
132
238
|
},
|
|
133
239
|
children: [child, anchorLink(id, label)],
|
|
134
240
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `node` already has real, visible text a screen reader would read
|
|
3
|
+
* as this `<th>`'s accessible name — anything outside a `.katex` subtree. A
|
|
4
|
+
* `.katex` span is treated as opaque here on purpose: its `.katex-mathml`
|
|
5
|
+
* child holds the MathML `<math>` axe won't read, and its `.katex-html`
|
|
6
|
+
* child is `aria-hidden="true"`, so neither counts either way — a header
|
|
7
|
+
* that already carries a descriptive word (`Launch speed $\text{(m/s)}$`)
|
|
8
|
+
* sees that word as a normal text-node sibling and is left alone.
|
|
9
|
+
*/
|
|
10
|
+
export function hasVisibleText(node: any): any;
|
|
11
|
+
/** Every `application/x-tex` annotation's raw source inside `node`, in order. */
|
|
12
|
+
export function collectMathSource(node: any, out?: any[]): any[];
|
|
13
|
+
/** Every `\explain{key}{…}` key named across one or more LaTeX sources, in order, deduped. */
|
|
14
|
+
export function explainKeysIn(sources: any): string[];
|
|
15
|
+
/**
|
|
16
|
+
* `"Label (units)"` per key, joined with `" / "` for a header that binds
|
|
17
|
+
* more than one (a ratio like `\mathrm{KE}/m`, explain-wrapped on both
|
|
18
|
+
* sides). `undefined` when nothing resolved, so the caller leaves the
|
|
19
|
+
* header exactly as it was rather than inserting an empty/placeholder label.
|
|
20
|
+
*/
|
|
21
|
+
export function composeHeaderLabel(keys: any, notationResolved: any): any;
|
|
22
|
+
/**
|
|
23
|
+
* See the module doc above. No plugin options — everything it needs is
|
|
24
|
+
* already on `file.data.notationResolved` by the time it runs.
|
|
25
|
+
*/
|
|
26
|
+
export default function rehypeTableHeaderLabels(): (tree: any, file: any) => void;
|