explico 0.4.0 → 0.6.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 +382 -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 +88 -1
- package/dist/compiler/course-config.js +35 -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 +98 -6
- package/dist/compiler/manifest.js +154 -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-heading-length.d.ts +12 -0
- package/dist/reference/gate-heading-length.js +79 -0
- package/dist/reference/gate-math.d.ts +32 -5
- package/dist/reference/gate-math.js +6 -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 +42 -0
- package/dist/reference/math-glyphs.mjs +548 -40
- 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-citation.d.mts +5 -1
- package/dist/reference/remark-citation.mjs +0 -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 +53 -4
- package/docs/authoring/README.md +10 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +138 -0
- package/docs/authoring/migrating.md +275 -7
- package/docs/authoring/notation.md +615 -1
- package/docs/authoring/structure-rules.md +69 -0
- 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 +132 -0
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/manifest.ts +281 -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 +409 -103
- 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 +260 -83
- 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 +30 -3
- 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 +36 -1
- 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-heading-length.ts +131 -0
- package/src/reference/gate-math.ts +32 -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 +548 -40
- 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-citation.mjs +0 -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,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remark adapter for substantial, referenceable memo disclosures.
|
|
3
|
+
*
|
|
4
|
+
* Authors place a memo as a normal flow block and cite it from earlier prose:
|
|
5
|
+
*
|
|
6
|
+
* The complete calculation is recorded in [[memo-derivation]].
|
|
7
|
+
*
|
|
8
|
+
* <Memo id="derivation" title="Derivation" description="The algebra.">
|
|
9
|
+
* Several paragraphs, equations, tables, or figures.
|
|
10
|
+
*
|
|
11
|
+
* </Memo>
|
|
12
|
+
*
|
|
13
|
+
* MDX normally rejects this before remark transforms run: an inline JSX node
|
|
14
|
+
* cannot contain block Markdown. The attacher therefore wraps remark-parse's
|
|
15
|
+
* parser and tags each memo before parsing. Legacy inline placements are still
|
|
16
|
+
* split into a temporary marker plus a flow `<Memo>`; standalone blocks do not
|
|
17
|
+
* emit a marker at their authored location. The transformer resolves every
|
|
18
|
+
* `[[memo-id]]` to a numbered link and injects the memo's per-page number and
|
|
19
|
+
* anchor into the real component. Temporary attributes never reach output.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { stripFencedCode } from './equations.mjs';
|
|
23
|
+
import { KEYED_FLOAT_KEY_PATTERN } from './keyed-jsx-floats.mjs';
|
|
24
|
+
import {
|
|
25
|
+
injectMdxStringAttributes,
|
|
26
|
+
mdxAttributesNamed,
|
|
27
|
+
removeMdxAttribute,
|
|
28
|
+
} from './mdx-attributes.mjs';
|
|
29
|
+
|
|
30
|
+
const BLOCK_ATTRIBUTE = 'data-explico-memo-block';
|
|
31
|
+
const MARKER_URL_PREFIX = '#explico-internal-memo-marker-';
|
|
32
|
+
const MEMO_REFERENCE_PATTERN =
|
|
33
|
+
/\[\[\s*memo-([a-z0-9]+(?:-[a-z0-9]+)*)\s*\]\]/g;
|
|
34
|
+
|
|
35
|
+
/** A same-page memo reference payload, e.g. `memo-full-derivation`. */
|
|
36
|
+
export function parseMemoRef(raw) {
|
|
37
|
+
const match = /^memo-([a-z0-9]+(?:-[a-z0-9]+)*)$/.exec(String(raw).trim());
|
|
38
|
+
return match ? { key: match[1] } : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// An MDX attribute may be a quoted string, a brace expression (including one
|
|
42
|
+
// level of nested braces), or ordinary unquoted source. Required memo props are
|
|
43
|
+
// validated as literal strings after parsing; this breadth is for preserving
|
|
44
|
+
// unrelated props such as `rememberState={condition}` intact.
|
|
45
|
+
const ATTRIBUTE_SOURCE = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'])*`;
|
|
46
|
+
const MEMO_PAIR = new RegExp(
|
|
47
|
+
String.raw`<Memo\b(${ATTRIBUTE_SOURCE})>([\s\S]*?)<\/Memo\s*>`,
|
|
48
|
+
'g',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
function maskBlock(block) {
|
|
52
|
+
return block.replace(/[^\n]/g, ' ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Protect examples/comments whose literal `<Memo>` text is not authoring. */
|
|
56
|
+
function searchableMemoSource(source) {
|
|
57
|
+
return stripFencedCode(source)
|
|
58
|
+
.replace(/<!--[\s\S]*?-->/g, maskBlock)
|
|
59
|
+
.replace(/\{\/\*[\s\S]*?\*\/\}/g, maskBlock);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isStandaloneBlock(source, start, end) {
|
|
63
|
+
const lineStart = source.lastIndexOf('\n', start - 1) + 1;
|
|
64
|
+
const followingNewline = source.indexOf('\n', end);
|
|
65
|
+
const lineEnd = followingNewline === -1 ? source.length : followingNewline;
|
|
66
|
+
return (
|
|
67
|
+
source.slice(lineStart, start).trim().length === 0 &&
|
|
68
|
+
source.slice(end, lineEnd).trim().length === 0
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Turn otherwise-unparseable inline/block memo syntax into ordinary MDX while
|
|
74
|
+
* retaining author attributes and body source verbatim.
|
|
75
|
+
*/
|
|
76
|
+
export function preprocessMemos(source) {
|
|
77
|
+
const input = String(source);
|
|
78
|
+
const searchable = searchableMemoSource(input);
|
|
79
|
+
const matches = [...searchable.matchAll(MEMO_PAIR)];
|
|
80
|
+
if (matches.length === 0) return input;
|
|
81
|
+
|
|
82
|
+
let cursor = 0;
|
|
83
|
+
let output = '';
|
|
84
|
+
for (let index = 0; index < matches.length; index++) {
|
|
85
|
+
const match = matches[index];
|
|
86
|
+
const start = match.index ?? 0;
|
|
87
|
+
if (start < cursor) continue;
|
|
88
|
+
|
|
89
|
+
const whole = match[0];
|
|
90
|
+
const maskedAttributes = match[1] ?? '';
|
|
91
|
+
const openLength = '<Memo'.length + maskedAttributes.length + 1;
|
|
92
|
+
const closeOffset = whole.lastIndexOf('</Memo');
|
|
93
|
+
const bodyStart = start + openLength;
|
|
94
|
+
const closeStart = start + closeOffset;
|
|
95
|
+
const attributes = input.slice(start + '<Memo'.length, bodyStart - 1);
|
|
96
|
+
const body = input.slice(bodyStart, closeStart);
|
|
97
|
+
const searchableBody = searchable.slice(bodyStart, closeStart);
|
|
98
|
+
if (/<Memo\b/.test(searchableBody)) {
|
|
99
|
+
throw new Error('A <Memo> cannot be nested inside another <Memo>.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const closingTag = input.slice(closeStart, start + whole.length);
|
|
103
|
+
const end = start + whole.length;
|
|
104
|
+
output += input.slice(cursor, start);
|
|
105
|
+
if (isStandaloneBlock(input, start, end)) {
|
|
106
|
+
output += `<Memo ${BLOCK_ATTRIBUTE}="${index}"${attributes}>\n\n`;
|
|
107
|
+
output += body;
|
|
108
|
+
output += `\n\n${closingTag}`;
|
|
109
|
+
} else {
|
|
110
|
+
output += `[memo](${MARKER_URL_PREFIX}${index})\n\n`;
|
|
111
|
+
output += `<Memo ${BLOCK_ATTRIBUTE}="${index}"${attributes}>\n\n`;
|
|
112
|
+
output += body;
|
|
113
|
+
output += `\n\n${closingTag}\n\n`;
|
|
114
|
+
}
|
|
115
|
+
cursor = end;
|
|
116
|
+
}
|
|
117
|
+
output += input.slice(cursor);
|
|
118
|
+
return output;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fail(file, message, node) {
|
|
122
|
+
if (typeof file?.fail === 'function') file.fail(message, node);
|
|
123
|
+
const location = file?.path ? `${file.path}: ` : '';
|
|
124
|
+
throw new Error(`${location}${message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function literalAttribute(node, name, file, { optional = false } = {}) {
|
|
128
|
+
const matches = mdxAttributesNamed(node, name);
|
|
129
|
+
if (matches.length > 1) {
|
|
130
|
+
fail(file, `<Memo> has more than one \`${name}\` prop.`, node);
|
|
131
|
+
}
|
|
132
|
+
if (matches.length === 0) {
|
|
133
|
+
if (optional) return undefined;
|
|
134
|
+
fail(file, `<Memo> requires a non-empty literal \`${name}\` prop.`, node);
|
|
135
|
+
}
|
|
136
|
+
const value = matches[0]?.value;
|
|
137
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
138
|
+
const qualifier = optional ? 'when provided' : '';
|
|
139
|
+
fail(
|
|
140
|
+
file,
|
|
141
|
+
`<Memo> \`${name}\` must be a non-empty literal string${qualifier ? ` ${qualifier}` : ''}; expressions are not supported for build-time memo markers.`,
|
|
142
|
+
matches[0] ?? node,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function markerNode(memo) {
|
|
149
|
+
const link = {
|
|
150
|
+
type: 'link',
|
|
151
|
+
url: `#${memo.anchorId}`,
|
|
152
|
+
children: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
value: memo.label ?? `[${memo.number}]`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
data: {
|
|
159
|
+
hProperties: {
|
|
160
|
+
className: ['memo-marker'],
|
|
161
|
+
'data-memo-key': memo.id,
|
|
162
|
+
'data-memo-number': memo.number,
|
|
163
|
+
'aria-controls': memo.anchorId,
|
|
164
|
+
'aria-expanded': 'false',
|
|
165
|
+
'aria-label': memo.label
|
|
166
|
+
? `${memo.label}: ${memo.title}`
|
|
167
|
+
: `Memo ${memo.number}: ${memo.title}`,
|
|
168
|
+
title: memo.title,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
if (memo.label) return link;
|
|
173
|
+
return {
|
|
174
|
+
type: 'emphasis',
|
|
175
|
+
children: [link],
|
|
176
|
+
data: {
|
|
177
|
+
hName: 'sup',
|
|
178
|
+
hProperties: { className: ['memo-marker-wrap'] },
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function visit(node, callback, parent) {
|
|
184
|
+
if (!node || typeof node !== 'object') return;
|
|
185
|
+
callback(node, parent);
|
|
186
|
+
if (!Array.isArray(node.children)) return;
|
|
187
|
+
for (const child of node.children) visit(child, callback, node);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function rewriteMemoReferences(node, memos, file) {
|
|
191
|
+
if (!node || typeof node !== 'object' || !Array.isArray(node.children)) return;
|
|
192
|
+
if (node.type === 'link' || node.type === 'linkReference') return;
|
|
193
|
+
|
|
194
|
+
for (let index = 0; index < node.children.length; index++) {
|
|
195
|
+
const child = node.children[index];
|
|
196
|
+
if (child?.type !== 'text' || typeof child.value !== 'string') {
|
|
197
|
+
rewriteMemoReferences(child, memos, file);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const replacements = [];
|
|
202
|
+
let cursor = 0;
|
|
203
|
+
let match;
|
|
204
|
+
MEMO_REFERENCE_PATTERN.lastIndex = 0;
|
|
205
|
+
while ((match = MEMO_REFERENCE_PATTERN.exec(child.value))) {
|
|
206
|
+
if (match.index > cursor) {
|
|
207
|
+
replacements.push({ type: 'text', value: child.value.slice(cursor, match.index) });
|
|
208
|
+
}
|
|
209
|
+
const memo = memos.get(match[1]);
|
|
210
|
+
if (!memo) {
|
|
211
|
+
fail(
|
|
212
|
+
file,
|
|
213
|
+
`Unknown memo reference [[memo-${match[1]}]]: no <Memo> on this page carries id="${match[1]}".`,
|
|
214
|
+
child,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
replacements.push(markerNode({ ...memo, label: undefined }));
|
|
218
|
+
cursor = match.index + match[0].length;
|
|
219
|
+
}
|
|
220
|
+
if (replacements.length === 0) continue;
|
|
221
|
+
if (cursor < child.value.length) {
|
|
222
|
+
replacements.push({ type: 'text', value: child.value.slice(cursor) });
|
|
223
|
+
}
|
|
224
|
+
node.children.splice(index, 1, ...replacements);
|
|
225
|
+
index += replacements.length - 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Number memo blocks from one on every page and resolve same-page
|
|
231
|
+
* `[[memo-id]]` references. Numbering is intentionally page-local.
|
|
232
|
+
*
|
|
233
|
+
* @this {any}
|
|
234
|
+
*/
|
|
235
|
+
export default function remarkMemo() {
|
|
236
|
+
const processor = this;
|
|
237
|
+
const parse = processor.parser;
|
|
238
|
+
if (typeof parse !== 'function') {
|
|
239
|
+
throw new Error(
|
|
240
|
+
'remarkMemo must be registered after the Markdown parser (and after remark-mdx when configured explicitly).',
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
Reflect.set(processor, 'parser', (document, file) =>
|
|
244
|
+
parse(preprocessMemos(document), file),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return function transform(tree, file) {
|
|
248
|
+
const blocks = new Map();
|
|
249
|
+
const markerLocations = new Map();
|
|
250
|
+
const unprocessed = [];
|
|
251
|
+
|
|
252
|
+
visit(tree, (node, parent) => {
|
|
253
|
+
if (
|
|
254
|
+
node.type === 'link' &&
|
|
255
|
+
typeof node.url === 'string' &&
|
|
256
|
+
node.url.startsWith(MARKER_URL_PREFIX)
|
|
257
|
+
) {
|
|
258
|
+
const token = node.url.slice(MARKER_URL_PREFIX.length);
|
|
259
|
+
if (markerLocations.has(token)) {
|
|
260
|
+
fail(file, 'Internal memo marker collision.', node);
|
|
261
|
+
}
|
|
262
|
+
markerLocations.set(token, { node, parent });
|
|
263
|
+
}
|
|
264
|
+
if (node.name !== 'Memo') return;
|
|
265
|
+
const internal = mdxAttributesNamed(node, BLOCK_ATTRIBUTE);
|
|
266
|
+
if (internal.length === 0) {
|
|
267
|
+
unprocessed.push(node);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const token = literalAttribute(node, BLOCK_ATTRIBUTE, file);
|
|
271
|
+
if (blocks.has(token)) fail(file, 'Internal memo block collision.', node);
|
|
272
|
+
blocks.set(token, node);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (unprocessed.length > 0) {
|
|
276
|
+
fail(
|
|
277
|
+
file,
|
|
278
|
+
'<Memo> must use a paired opening and closing tag so remarkMemo can place its inline marker and block body.',
|
|
279
|
+
unprocessed[0],
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
for (const token of markerLocations.keys()) {
|
|
283
|
+
if (!blocks.has(token)) fail(file, 'Internal memo marker/block pairing failed.', tree);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const seenIds = new Set();
|
|
287
|
+
const memos = new Map();
|
|
288
|
+
let number = 0;
|
|
289
|
+
for (const [token, node] of blocks) {
|
|
290
|
+
const marker = markerLocations.get(token);
|
|
291
|
+
if (marker && (!marker.parent || !Array.isArray(marker.parent.children))) {
|
|
292
|
+
fail(file, 'Internal memo marker has no paragraph parent.', marker.node);
|
|
293
|
+
}
|
|
294
|
+
if (node.type !== 'mdxJsxFlowElement') {
|
|
295
|
+
fail(file, '<Memo> did not become a block-level MDX element.', node);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const id = literalAttribute(node, 'id', file);
|
|
299
|
+
const title = literalAttribute(node, 'title', file);
|
|
300
|
+
literalAttribute(node, 'description', file);
|
|
301
|
+
const label = literalAttribute(node, 'label', file, { optional: true });
|
|
302
|
+
if (!KEYED_FLOAT_KEY_PATTERN.test(id)) {
|
|
303
|
+
fail(
|
|
304
|
+
file,
|
|
305
|
+
`Invalid <Memo id="${id}">; use lowercase letters/numbers separated by single hyphens.`,
|
|
306
|
+
node,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (seenIds.has(id)) {
|
|
310
|
+
fail(file, `Duplicate <Memo id="${id}"> on this page.`, node);
|
|
311
|
+
}
|
|
312
|
+
seenIds.add(id);
|
|
313
|
+
|
|
314
|
+
number += 1;
|
|
315
|
+
const anchorId = `memo-${id}`;
|
|
316
|
+
removeMdxAttribute(node, BLOCK_ATTRIBUTE);
|
|
317
|
+
injectMdxStringAttributes(node, {
|
|
318
|
+
number: String(number),
|
|
319
|
+
anchorId,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const memo = {
|
|
323
|
+
id,
|
|
324
|
+
title,
|
|
325
|
+
label,
|
|
326
|
+
number: String(number),
|
|
327
|
+
anchorId,
|
|
328
|
+
};
|
|
329
|
+
memos.set(id, memo);
|
|
330
|
+
|
|
331
|
+
if (marker) {
|
|
332
|
+
const markerIndex = marker.parent.children.indexOf(marker.node);
|
|
333
|
+
marker.parent.children[markerIndex] = markerNode(memo);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
rewriteMemoReferences(tree, memos, file);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -3,53 +3,88 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every number on a page comes from two places that MUST agree: the page's own
|
|
5
5
|
* counters (`numbering`) and the manifest tables a cross-page `[[slug#eq-key]]`
|
|
6
|
-
* is resolved against (`equations` / `tables` / `figures`
|
|
7
|
-
*
|
|
6
|
+
* is resolved against (`equations` / `tables` / `figures` / `diagrams` /
|
|
7
|
+
* `checks`).
|
|
8
|
+
* Deriving every input from the same manifest in one step stops a course config from
|
|
8
9
|
* supplying one without the other — the omission that produced page-local
|
|
9
10
|
* `(2.2)` and cross-page `(4.5)` for the same equation.
|
|
10
11
|
*
|
|
11
|
-
* `numbering` maps a lesson slug to
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* `numbering` maps a lesson slug to
|
|
13
|
+
* `{ part, equations, tables, figures, diagrams, checks }`: the part's ordinal
|
|
14
|
+
* (the number before the dot) and, per numbered kind, how many earlier lessons
|
|
15
|
+
* of that part already used.
|
|
14
16
|
*/
|
|
15
17
|
export function numberingFromManifest(manifest: any): {
|
|
16
18
|
equations?: undefined;
|
|
17
19
|
tables?: undefined;
|
|
18
20
|
figures?: undefined;
|
|
21
|
+
diagrams?: undefined;
|
|
22
|
+
checks?: undefined;
|
|
19
23
|
numbering?: undefined;
|
|
20
24
|
} | {
|
|
21
25
|
equations: any;
|
|
22
26
|
tables: any;
|
|
23
27
|
figures: any;
|
|
28
|
+
diagrams: any;
|
|
29
|
+
checks: any;
|
|
24
30
|
numbering: {
|
|
25
31
|
[k: string]: any;
|
|
26
32
|
};
|
|
27
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The options object `remarkNotation` needs for one manifest, in one call.
|
|
36
|
+
*
|
|
37
|
+
* A consumer used to hand-assemble `definitions`, `equations`, `tables`,
|
|
38
|
+
* `figures`, and `chapters` separately — easy to do partially, which is how a
|
|
39
|
+
* page ends up numbered under the legacy `##`-section scheme while the
|
|
40
|
+
* manifest (and every cross-page reference to that page) uses the part
|
|
41
|
+
* scheme. This does not return that literal four-map-plus-chapters shape:
|
|
42
|
+
* `chapters` is the superseded per-lesson form (see `pageNumbering` below),
|
|
43
|
+
* and passing `manifest` through already makes `numberingFromManifest()`
|
|
44
|
+
* derive `numbering` plus every `equations`/`tables`/`figures`/`diagrams`/
|
|
45
|
+
* `checks` map from the same source in one step — strictly more correct than
|
|
46
|
+
* reconstructing `chapters` by hand.
|
|
47
|
+
*
|
|
48
|
+
* `overrides` is spread last, so a caller can add `strict`, `unknown`,
|
|
49
|
+
* `resolve`, `base`, or override anything above:
|
|
50
|
+
*
|
|
51
|
+
* [remarkNotation, createRemarkNotationOptions(manifest, { strict: true })]
|
|
52
|
+
*
|
|
53
|
+
* @param {object} manifest - a compiled Explico manifest (`compileManifest()`).
|
|
54
|
+
* @param {object} [overrides] - additional/overriding `remarkNotation` options.
|
|
55
|
+
*/
|
|
56
|
+
export function createRemarkNotationOptions(manifest: object, overrides?: object): {
|
|
57
|
+
definitions: any;
|
|
58
|
+
manifest: object;
|
|
59
|
+
};
|
|
28
60
|
/**
|
|
29
61
|
* @param {{
|
|
30
62
|
* definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>),
|
|
31
63
|
* resolve?: (key: string, context: object) => object | undefined,
|
|
32
64
|
* unknown?: 'error' | 'warn' | 'ignore',
|
|
65
|
+
* strict?: boolean,
|
|
33
66
|
* base?: string,
|
|
34
67
|
* manifest?: object,
|
|
35
68
|
* equations?: Record<string, Record<string, string>>,
|
|
36
69
|
* tables?: Record<string, Record<string, string>>,
|
|
37
70
|
* figures?: Record<string, Record<string, string>>,
|
|
38
|
-
*
|
|
71
|
+
* diagrams?: Record<string, Record<string, string>>,
|
|
72
|
+
* checks?: Record<string, Record<string, string>>,
|
|
73
|
+
* numbering?: Record<string, { part: number, equations: number, tables: number, figures: number, diagrams: number, checks: number }>,
|
|
39
74
|
* chapters?: Record<string, number>,
|
|
40
75
|
* }} [options]
|
|
41
76
|
*
|
|
42
|
-
* `manifest` is the preferred wiring: pass the compiled manifest and
|
|
77
|
+
* `manifest` is the preferred wiring: pass the compiled manifest and all
|
|
43
78
|
* numbering inputs below are read off it together, so they cannot drift apart.
|
|
44
79
|
* They stay available individually for hand-built trees, and an explicitly
|
|
45
80
|
* supplied one still wins over the manifest.
|
|
46
81
|
*
|
|
47
82
|
* `numbering` maps a lesson slug to its **part** (the number before the dot)
|
|
48
83
|
* and, per kind of float, how many the earlier lessons of that part already
|
|
49
|
-
* used. Every display equation / `<Table>` / `<Figure>` on the page is then
|
|
84
|
+
* used. Every display equation / `<Table>` / `<Figure>` / `<Diagram>` on the page is then
|
|
50
85
|
* numbered `‹part›.‹offset + n›`, with `##` and `###` headings ignored; with no
|
|
51
86
|
* entry the legacy `##`-section scheme applies. It must agree with
|
|
52
|
-
* `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`
|
|
87
|
+
* `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`/`diagrams`
|
|
53
88
|
* above — supplying the tables without `numbering` numbers the same equation
|
|
54
89
|
* twice, and `assertManifestNumberParity` turns that into a build error rather
|
|
55
90
|
* than a page whose references disagree with their targets.
|
|
@@ -57,21 +92,34 @@ export function numberingFromManifest(manifest: any): {
|
|
|
57
92
|
* `chapters` is the superseded per-lesson form (slug → the lesson's course
|
|
58
93
|
* ordinal, no running offset). Still read when `numbering` has no entry, so a
|
|
59
94
|
* config written against the old option keeps building.
|
|
95
|
+
*
|
|
96
|
+
* `strict` (default `false`) additionally fails the build when a catalogued
|
|
97
|
+
* lesson contains an equation/`<Table>`/`<Figure>` — keyed or not — but has no
|
|
98
|
+
* `numbering` placement or no cross-page number table entry, instead of
|
|
99
|
+
* silently rendering it under the legacy `##`-section scheme. Off by default
|
|
100
|
+
* for backward compatibility; `createRemarkNotationOptions(manifest, { strict:
|
|
101
|
+
* true })` is the recommended way to turn it on for a course whose manifest
|
|
102
|
+
* covers every catalogued lesson.
|
|
60
103
|
*/
|
|
61
104
|
export default function remarkNotation(options?: {
|
|
62
105
|
definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>);
|
|
63
106
|
resolve?: (key: string, context: object) => object | undefined;
|
|
64
107
|
unknown?: "error" | "warn" | "ignore";
|
|
108
|
+
strict?: boolean;
|
|
65
109
|
base?: string;
|
|
66
110
|
manifest?: object;
|
|
67
111
|
equations?: Record<string, Record<string, string>>;
|
|
68
112
|
tables?: Record<string, Record<string, string>>;
|
|
69
113
|
figures?: Record<string, Record<string, string>>;
|
|
114
|
+
diagrams?: Record<string, Record<string, string>>;
|
|
115
|
+
checks?: Record<string, Record<string, string>>;
|
|
70
116
|
numbering?: Record<string, {
|
|
71
117
|
part: number;
|
|
72
118
|
equations: number;
|
|
73
119
|
tables: number;
|
|
74
120
|
figures: number;
|
|
121
|
+
diagrams: number;
|
|
122
|
+
checks: number;
|
|
75
123
|
}>;
|
|
76
124
|
chapters?: Record<string, number>;
|
|
77
125
|
}): (tree: any, file: any) => void;
|