@epic-web/workshop-utils 4.28.6 → 5.0.1
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/dist/esm/apps.server.d.ts.map +1 -1
- package/dist/esm/apps.server.js +126 -124
- package/dist/esm/apps.server.js.map +1 -1
- package/dist/esm/cache.server.d.ts +7 -9
- package/dist/esm/cache.server.d.ts.map +1 -1
- package/dist/esm/cache.server.js +38 -35
- package/dist/esm/cache.server.js.map +1 -1
- package/dist/esm/change-tracker.server.d.ts +3 -2
- package/dist/esm/change-tracker.server.d.ts.map +1 -1
- package/dist/esm/change-tracker.server.js +60 -12
- package/dist/esm/change-tracker.server.js.map +1 -1
- package/dist/esm/compile-mdx.server.d.ts +4 -3
- package/dist/esm/compile-mdx.server.d.ts.map +1 -1
- package/dist/esm/compile-mdx.server.js +26 -144
- package/dist/esm/compile-mdx.server.js.map +1 -1
- package/dist/esm/config.server.d.ts +9 -9
- package/package.json +2 -10
- package/dist/esm/codefile-mdx.server.d.ts +0 -16
- package/dist/esm/codefile-mdx.server.d.ts.map +0 -1
- package/dist/esm/codefile-mdx.server.js +0 -274
- package/dist/esm/codefile-mdx.server.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epic-web/workshop-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"./db.server": "./src/db.server.ts",
|
|
20
20
|
"./timing.server": "./src/timing.server.ts",
|
|
21
21
|
"./compile-mdx.server": "./src/compile-mdx.server.ts",
|
|
22
|
-
"./codefile-mdx.server": "./src/codefile-mdx.server.ts",
|
|
23
22
|
"./change-tracker.server": "./src/change-tracker.server.ts",
|
|
24
23
|
"./git.server": "./src/git.server.ts",
|
|
25
24
|
"./iframe-sync": "./src/iframe-sync.ts",
|
|
@@ -81,13 +80,6 @@
|
|
|
81
80
|
"default": "./dist/esm/compile-mdx.server.js"
|
|
82
81
|
}
|
|
83
82
|
},
|
|
84
|
-
"./codefile-mdx.server": {
|
|
85
|
-
"import": {
|
|
86
|
-
"source": "./src/codefile-mdx.server.ts",
|
|
87
|
-
"types": "./dist/esm/codefile-mdx.server.d.ts",
|
|
88
|
-
"default": "./dist/esm/codefile-mdx.server.js"
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
83
|
"./change-tracker.server": {
|
|
92
84
|
"import": {
|
|
93
85
|
"source": "./src/change-tracker.server.ts",
|
|
@@ -168,7 +160,7 @@
|
|
|
168
160
|
"chai": "^5.1.1",
|
|
169
161
|
"chai-dom": "^1.12.0",
|
|
170
162
|
"chalk": "^5.3.0",
|
|
171
|
-
"chokidar": "^
|
|
163
|
+
"chokidar": "^4.0.1",
|
|
172
164
|
"close-with-grace": "^1.3.0",
|
|
173
165
|
"cross-spawn": "^7.0.3",
|
|
174
166
|
"execa": "^9.1.0",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { type Root as MdastRoot } from 'mdast';
|
|
2
|
-
export type EmbeddedFile = {
|
|
3
|
-
error?: boolean;
|
|
4
|
-
file: string;
|
|
5
|
-
hash: string;
|
|
6
|
-
line?: number;
|
|
7
|
-
warning?: string;
|
|
8
|
-
};
|
|
9
|
-
export type CodeFileData = {
|
|
10
|
-
mdxFile: string;
|
|
11
|
-
cacheLocation: string;
|
|
12
|
-
cachedEmbeddedFiles: Map<string, EmbeddedFile>;
|
|
13
|
-
embeddedFiles: Map<string, EmbeddedFile>;
|
|
14
|
-
};
|
|
15
|
-
export declare function remarkCodeFile(data: CodeFileData): (tree: MdastRoot) => Promise<void>;
|
|
16
|
-
//# sourceMappingURL=codefile-mdx.server.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"codefile-mdx.server.d.ts","sourceRoot":"","sources":["../../src/codefile-mdx.server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,IAAI,IAAI,SAAS,EAAiC,MAAM,OAAO,CAAA;AAuB7E,MAAM,MAAM,YAAY,GAAG;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IAC9C,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;CACxC,CAAA;AA8LD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,UA4IA,SAAS,mBAczD"}
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { createProcessor } from '@mdx-js/mdx';
|
|
4
|
-
import md5 from 'md5-hex';
|
|
5
|
-
import { removePosition } from 'unist-util-remove-position';
|
|
6
|
-
import { visit } from 'unist-util-visit';
|
|
7
|
-
import * as z from 'zod';
|
|
8
|
-
const APP_TYPES = ['problem', 'solution', 'playground'];
|
|
9
|
-
const safePath = (s) => s.replace(/\\/g, '/');
|
|
10
|
-
const REG_EXP = /^(?:\d+(?:-\d+)?,)*\d+(?:-\d+)?$/;
|
|
11
|
-
const isValidRangeFormat = (value) => value ? REG_EXP.test(value) : true;
|
|
12
|
-
const transformRange = (value) => value?.split(',').map((range) => {
|
|
13
|
-
const [start, end] = range.split('-').map(Number);
|
|
14
|
-
return [start, end ?? start];
|
|
15
|
-
});
|
|
16
|
-
const isRangeBounded = (range, ctx, lines) => {
|
|
17
|
-
if (!lines || !Array.isArray(range))
|
|
18
|
-
return;
|
|
19
|
-
if (range.flat().some((r) => r < 1 || r > lines)) {
|
|
20
|
-
ctx.addIssue({
|
|
21
|
-
code: z.ZodIssueCode.custom,
|
|
22
|
-
message: `Range must be between 1 and ${lines}`,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
const isRangeInOrder = (range) => Array.isArray(range)
|
|
27
|
-
? range.every(([a, b]) => !isNaN(Number(a)) && !isNaN(Number(b)) && b >= a)
|
|
28
|
-
: true;
|
|
29
|
-
const isRangesNonOverlapping = (range) => {
|
|
30
|
-
if (!Array.isArray(range))
|
|
31
|
-
return true;
|
|
32
|
-
return range.every(([a], i) => i === 0 || (range[i - 1]?.[1] ?? 0) < a);
|
|
33
|
-
};
|
|
34
|
-
let fileContentCache = new Map();
|
|
35
|
-
async function getFileContent(filePath) {
|
|
36
|
-
if (fileContentCache.has(filePath)) {
|
|
37
|
-
return fileContentCache.get(filePath);
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
41
|
-
const fileContent = content.split('\n');
|
|
42
|
-
fileContentCache.set(filePath, fileContent);
|
|
43
|
-
return fileContent;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
console.warn(`@epic-web/workshop-app - invalid CodeFile.\nCould not read file: ${filePath}\n`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function validateProps(props, appDir) {
|
|
50
|
-
let validRange;
|
|
51
|
-
let linesCount = 0;
|
|
52
|
-
const BooleanSchema = z
|
|
53
|
-
.nullable(z.string())
|
|
54
|
-
.optional()
|
|
55
|
-
.refine((v) => ['true', 'false', null, undefined].includes(v), 'optional boolean key can be "true", "false", null or undefined')
|
|
56
|
-
.transform((v) => v === null || Boolean(v));
|
|
57
|
-
const RangeSchema = z
|
|
58
|
-
.string()
|
|
59
|
-
.optional()
|
|
60
|
-
.refine(isValidRangeFormat, 'Invalid range format')
|
|
61
|
-
.transform(transformRange)
|
|
62
|
-
.superRefine((val, ctx) => isRangeBounded(val, ctx, linesCount))
|
|
63
|
-
.refine(isRangeInOrder, 'Range must be in order low-high');
|
|
64
|
-
const inputSchema = z
|
|
65
|
-
.object({
|
|
66
|
-
file: z
|
|
67
|
-
.string()
|
|
68
|
-
.nonempty()
|
|
69
|
-
.transform(async (file, ctx) => {
|
|
70
|
-
const fullPath = path.join(appDir, file);
|
|
71
|
-
const content = await getFileContent(fullPath);
|
|
72
|
-
if (!content) {
|
|
73
|
-
ctx.addIssue({
|
|
74
|
-
code: z.ZodIssueCode.custom,
|
|
75
|
-
message: `Could not read file`,
|
|
76
|
-
fatal: true,
|
|
77
|
-
});
|
|
78
|
-
return z.NEVER;
|
|
79
|
-
}
|
|
80
|
-
linesCount = content.length;
|
|
81
|
-
// @mdx-js/mdx parser can NOT handle relative path with backslashes
|
|
82
|
-
return {
|
|
83
|
-
fullPath: safePath(fullPath),
|
|
84
|
-
filePath: safePath(file),
|
|
85
|
-
content,
|
|
86
|
-
};
|
|
87
|
-
}),
|
|
88
|
-
range: RangeSchema.refine((range) => {
|
|
89
|
-
const isValid = isRangesNonOverlapping(range);
|
|
90
|
-
// we use this value in highlight refine
|
|
91
|
-
validRange = isValid ? range : undefined;
|
|
92
|
-
return isValid;
|
|
93
|
-
}, 'Ranges must not overlap'),
|
|
94
|
-
highlight: RangeSchema.refine((highlight) => {
|
|
95
|
-
if (!Array.isArray(highlight) || !Array.isArray(validRange)) {
|
|
96
|
-
return z.NEVER;
|
|
97
|
-
}
|
|
98
|
-
return highlight.every(([hStart, hEnd]) => validRange?.some(([rStart, rEnd]) => hStart >= rStart && hEnd <= rEnd));
|
|
99
|
-
}, 'Highlight range must be within defined range')
|
|
100
|
-
.transform(() => props.highlight)
|
|
101
|
-
.optional(),
|
|
102
|
-
nonumber: BooleanSchema,
|
|
103
|
-
nocopy: BooleanSchema,
|
|
104
|
-
buttons: z
|
|
105
|
-
.string()
|
|
106
|
-
.optional()
|
|
107
|
-
.transform((str) => str ? str.split(',') : [])
|
|
108
|
-
.refine((arr) => arr.every((item) => APP_TYPES.includes(item)), {
|
|
109
|
-
message: `Buttons can only be any of ${APP_TYPES.join(',')}`,
|
|
110
|
-
}),
|
|
111
|
-
})
|
|
112
|
-
.strict();
|
|
113
|
-
return inputSchema.safeParseAsync(props);
|
|
114
|
-
}
|
|
115
|
-
async function createErrorNotification(node, errors, mdxFile, appType) {
|
|
116
|
-
const filename = path.basename(mdxFile);
|
|
117
|
-
const startLine = node.position?.start.line;
|
|
118
|
-
const endLine = node.position?.end.line;
|
|
119
|
-
const codeFence = async () => {
|
|
120
|
-
if (startLine && endLine) {
|
|
121
|
-
const contentStr = await getFileContent(mdxFile);
|
|
122
|
-
const content = contentStr?.slice(startLine - 1, endLine).join('\n');
|
|
123
|
-
if (content) {
|
|
124
|
-
return `
|
|
125
|
-
\`\`\`tsx filename=${filename} start=${startLine} nocopy
|
|
126
|
-
${content}
|
|
127
|
-
\`\`\``.trim();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return '';
|
|
131
|
-
};
|
|
132
|
-
const mdxSource = `
|
|
133
|
-
<CodeFileNotification variant="error" file="${filename}" line="${startLine}" type="${appType}">
|
|
134
|
-
<callout-danger class="notification">
|
|
135
|
-
<div className="title">CodeFile Error: invalid input</div>
|
|
136
|
-
${errors.map((error) => `<div>${error}</div>`).join('')}
|
|
137
|
-
${await codeFence()}
|
|
138
|
-
</callout-danger>
|
|
139
|
-
</CodeFileNotification>`;
|
|
140
|
-
return mdxToMdast(mdxSource);
|
|
141
|
-
}
|
|
142
|
-
// based on https://github.com/sindresorhus/strip-indent
|
|
143
|
-
function stripIndent(string) {
|
|
144
|
-
const match = string.match(/^[ \t]*(?=\S)/gm);
|
|
145
|
-
const indent = match?.reduce((r, a) => Math.min(r, a.length), Infinity) ?? 0;
|
|
146
|
-
if (indent === 0) {
|
|
147
|
-
return string;
|
|
148
|
-
}
|
|
149
|
-
const regex = new RegExp(`^[ \\t]{${indent}}`, 'gm');
|
|
150
|
-
return string.replace(regex, '');
|
|
151
|
-
}
|
|
152
|
-
function mdxToMdast(mdx) {
|
|
153
|
-
const processor = createProcessor();
|
|
154
|
-
const mdast = processor.parse(mdx.trim());
|
|
155
|
-
removePosition(mdast, { force: true });
|
|
156
|
-
return mdast.type === 'root' ? mdast.children : [mdast];
|
|
157
|
-
}
|
|
158
|
-
export function remarkCodeFile(data) {
|
|
159
|
-
fileContentCache = new Map();
|
|
160
|
-
const mdxFile = data.mdxFile;
|
|
161
|
-
const appDir = path.dirname(mdxFile);
|
|
162
|
-
const appType = mdxFile.includes('problem')
|
|
163
|
-
? 'problem'
|
|
164
|
-
: mdxFile.includes('solution')
|
|
165
|
-
? 'solution'
|
|
166
|
-
: 'other'; // not in exercise
|
|
167
|
-
async function replaceCodeFileNode({ node, parent, }) {
|
|
168
|
-
if (!parent) {
|
|
169
|
-
console.warn('Unexpected error: replaceCodeFileNode called without a Parent');
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const index = parent.children.indexOf(node);
|
|
173
|
-
if (index === -1) {
|
|
174
|
-
console.warn('Unexpected error: replaceCodeFileNode could not find node index in Parent');
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const attributes = node.attributes;
|
|
178
|
-
const props = {};
|
|
179
|
-
for (const { name, value } of attributes) {
|
|
180
|
-
props[name] = value;
|
|
181
|
-
}
|
|
182
|
-
const result = await validateProps(props, appDir);
|
|
183
|
-
if (!result.success) {
|
|
184
|
-
const errors = result.error.issues.map(({ message, path }) => path[0] ? `${message}: ${path[0]}="${props[path[0]]}"` : message);
|
|
185
|
-
const notification = await createErrorNotification(node, errors, mdxFile, appType);
|
|
186
|
-
parent.children.splice(index, 1, ...notification);
|
|
187
|
-
data.embeddedFiles.set('invalid input', {
|
|
188
|
-
error: true,
|
|
189
|
-
file: props.file,
|
|
190
|
-
hash: '',
|
|
191
|
-
line: node.position?.start.line ?? 1,
|
|
192
|
-
});
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const { file: { content, filePath, fullPath }, highlight, range, } = result.data;
|
|
196
|
-
const language = path.extname(filePath).substring(1);
|
|
197
|
-
const meta = [`filename=${filePath}`];
|
|
198
|
-
// nonumbers nocopy ....
|
|
199
|
-
Object.entries(result.data).forEach(([key, val]) => typeof val === 'boolean' && val && meta.push(`${key}=true`));
|
|
200
|
-
if (result.data.buttons) {
|
|
201
|
-
meta.push(`buttons=${result.data.buttons.join(',')}`);
|
|
202
|
-
meta.push(`type=${appType}`);
|
|
203
|
-
meta.push(`fullpath=${fullPath}`);
|
|
204
|
-
// Avoid the headache of finding the separator on the client side
|
|
205
|
-
// path.sep is always / on client side
|
|
206
|
-
meta.push(`sep=${path.sep}`);
|
|
207
|
-
}
|
|
208
|
-
if (highlight?.length) {
|
|
209
|
-
meta.push(`lines=${highlight}`);
|
|
210
|
-
}
|
|
211
|
-
const fileSections = range?.length ? range : [[1, content.length]];
|
|
212
|
-
const rangesContent = [];
|
|
213
|
-
const preNodes = [];
|
|
214
|
-
for (const [start, end] of fileSections) {
|
|
215
|
-
const rangeContent = stripIndent(content.slice(start ? start - 1 : 0, end).join('\n'));
|
|
216
|
-
rangesContent.push(rangeContent);
|
|
217
|
-
const mdxSource = `
|
|
218
|
-
\`\`\`${language} ${meta.concat(`start=${start}`).join(' ')}
|
|
219
|
-
${rangeContent}
|
|
220
|
-
\`\`\``;
|
|
221
|
-
preNodes.push(...mdxToMdast(mdxSource));
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Show a warning above the file content if the range we show changed in the
|
|
225
|
-
* embedded file and the range in <CodeFile range="a-b"> did not change.
|
|
226
|
-
* The warning will be removed automatically after the <CodeFile> range changes
|
|
227
|
-
* or by canceling it from the UI.
|
|
228
|
-
*/
|
|
229
|
-
const embeddedKey = md5(fullPath + JSON.stringify(range));
|
|
230
|
-
const contentHash = md5(rangesContent.join(','));
|
|
231
|
-
const newData = {
|
|
232
|
-
file: fullPath,
|
|
233
|
-
hash: contentHash,
|
|
234
|
-
};
|
|
235
|
-
const cachedData = data.cachedEmbeddedFiles.get(embeddedKey);
|
|
236
|
-
if (cachedData &&
|
|
237
|
-
// If a warning existed previously and its hash matched the current hash,
|
|
238
|
-
// then the changes were reverted and the warning will be remove
|
|
239
|
-
((cachedData.warning && cachedData.warning !== contentHash) ??
|
|
240
|
-
(!cachedData.warning && cachedData.hash !== contentHash))) {
|
|
241
|
-
// keep previously saved warning or previous hash
|
|
242
|
-
newData.warning = cachedData.warning ?? cachedData.hash;
|
|
243
|
-
const startLine = node.position?.start.line ?? 1;
|
|
244
|
-
newData.line = startLine;
|
|
245
|
-
const mdxFilename = path.basename(mdxFile);
|
|
246
|
-
const filename = path.basename(filePath);
|
|
247
|
-
const warning = `
|
|
248
|
-
<CodeFileNotification variant="warning" file="${mdxFilename}" line="${startLine}" type="${appType}"
|
|
249
|
-
cacheLocation="${data.cacheLocation}" embeddedKey="${embeddedKey}">
|
|
250
|
-
<callout-warning class="notification">
|
|
251
|
-
<div className="title">CodeFile Warning:</div>
|
|
252
|
-
<div>file ${filename} content was changed, review 'range' and 'highlight' inputs</div>
|
|
253
|
-
</callout-warning>
|
|
254
|
-
</CodeFileNotification>`;
|
|
255
|
-
preNodes.unshift(...mdxToMdast(warning));
|
|
256
|
-
}
|
|
257
|
-
data.embeddedFiles.set(embeddedKey, newData);
|
|
258
|
-
// replace <CodeFile> with embedded file content
|
|
259
|
-
parent.children.splice(index, 1, ...preNodes);
|
|
260
|
-
}
|
|
261
|
-
return async function codeFileTransformer(tree) {
|
|
262
|
-
const codeFiles = [];
|
|
263
|
-
const filter = { type: 'mdxJsxFlowElement', name: 'CodeFile' };
|
|
264
|
-
visit(tree, filter, ((node, _index, parent) => {
|
|
265
|
-
codeFiles.push({ node, parent });
|
|
266
|
-
}));
|
|
267
|
-
for (const props of codeFiles) {
|
|
268
|
-
await replaceCodeFileNode(props);
|
|
269
|
-
}
|
|
270
|
-
// cleanup
|
|
271
|
-
fileContentCache = new Map();
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
//# sourceMappingURL=codefile-mdx.server.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"codefile-mdx.server.js","sourceRoot":"","sources":["../../src/codefile-mdx.server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,GAAG,MAAM,SAAS,CAAA;AAMzB,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,KAAK,EAAgB,MAAM,kBAAkB,CAAA;AACtD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAaxB,MAAM,SAAS,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,YAAY,CAAU,CAAA;AAkBhE,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;AAErD,MAAM,OAAO,GAAG,kCAAkC,CAAA;AAElD,MAAM,kBAAkB,GAAG,CAAC,KAAyB,EAAE,EAAE,CACxD,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAEnC,MAAM,cAAc,GAAG,CAAC,KAAyB,EAAE,EAAE,CACpD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;IAC/B,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACjD,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI,KAAK,CAAqB,CAAA;AACjD,CAAC,CAAC,CAAA;AAEH,MAAM,cAAc,GAAG,CACtB,KAAiB,EACjB,GAAoB,EACpB,KAAa,EACZ,EAAE;IACH,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAM;IAC3C,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC;QAClD,GAAG,CAAC,QAAQ,CAAC;YACZ,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,OAAO,EAAE,+BAA+B,KAAK,EAAE;SAC/C,CAAC,CAAA;IACH,CAAC;AACF,CAAC,CAAA;AAED,MAAM,cAAc,GAAG,CAAC,KAAiB,EAAE,EAAE,CAC5C,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;IACnB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3E,CAAC,CAAC,IAAI,CAAA;AAER,MAAM,sBAAsB,GAAG,CAAC,KAAiB,EAAE,EAAE;IACpD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACtC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AACxE,CAAC,CAAA;AAED,IAAI,gBAAgB,GAAmB,IAAI,GAAG,EAAE,CAAA;AAChD,KAAK,UAAU,cAAc,CAAC,QAAgB;IAC7C,IAAI,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpC,OAAO,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACtC,CAAC;IACD,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC7D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACvC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;QAC3C,OAAO,WAAW,CAAA;IACnB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,CAAC,IAAI,CACX,oEAAoE,QAAQ,IAAI,CAChF,CAAA;IACF,CAAC;AACF,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,KAAoB,EAAE,MAAc;IAChE,IAAI,UAAsB,CAAA;IAC1B,IAAI,UAAU,GAAG,CAAC,CAAA;IAElB,MAAM,aAAa,GAAG,CAAC;SACrB,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACpB,QAAQ,EAAE;SACV,MAAM,CACN,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EACrD,gEAAgE,CAChE;SACA,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IAE5C,MAAM,WAAW,GAAG,CAAC;SACnB,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,MAAM,CAAC,kBAAkB,EAAE,sBAAsB,CAAC;SAClD,SAAS,CAAC,cAAc,CAAC;SACzB,WAAW,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC;SAC/D,MAAM,CAAC,cAAc,EAAE,iCAAiC,CAAC,CAAA;IAE3D,MAAM,WAAW,GAAG,CAAC;SACnB,MAAM,CAAC;QACP,IAAI,EAAE,CAAC;aACL,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YACxC,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,QAAQ,CAAC,CAAA;YAC9C,IAAI,CAAC,OAAO,EAAE,CAAC;gBACd,GAAG,CAAC,QAAQ,CAAC;oBACZ,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;oBAC3B,OAAO,EAAE,qBAAqB;oBAC9B,KAAK,EAAE,IAAI;iBACX,CAAC,CAAA;gBACF,OAAO,CAAC,CAAC,KAAK,CAAA;YACf,CAAC;YACD,UAAU,GAAG,OAAO,CAAC,MAAM,CAAA;YAC3B,mEAAmE;YACnE,OAAO;gBACN,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;gBAC5B,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC;gBACxB,OAAO;aACP,CAAA;QACF,CAAC,CAAC;QACH,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACnC,MAAM,OAAO,GAAG,sBAAsB,CAAC,KAAK,CAAC,CAAA;YAC7C,wCAAwC;YACxC,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;YACxC,OAAO,OAAO,CAAA;QACf,CAAC,EAAE,yBAAyB,CAAC;QAC7B,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE;YAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC7D,OAAO,CAAC,CAAC,KAAK,CAAA;YACf,CAAC;YACD,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CACzC,UAAU,EAAE,IAAI,CACf,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,MAAM,IAAI,MAAM,IAAI,IAAI,IAAI,IAAI,CACpD,CACD,CAAA;QACF,CAAC,EAAE,8CAA8C,CAAC;aAChD,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,SAA0B,CAAC;aACjD,QAAQ,EAAE;QACZ,QAAQ,EAAE,aAAa;QACvB,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE,CAAC;aACR,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAClB,GAAG,CAAC,CAAC,CAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC,CAAC,CAAC,EAAE,CAClD;aACA,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE;YAC/D,OAAO,EAAE,8BAA8B,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;SAC5D,CAAC;KACH,CAAC;SACD,MAAM,EAAE,CAAA;IAEV,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;AACzC,CAAC;AAED,KAAK,UAAU,uBAAuB,CACrC,IAAuB,EACvB,MAAgB,EAChB,OAAe,EACf,OAAe;IAEf,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAA;IAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAA;IAEvC,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE;QAC5B,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAA;YAChD,MAAM,OAAO,GAAG,UAAU,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACpE,IAAI,OAAO,EAAE,CAAC;gBACb,OAAO;qBACU,QAAQ,UAAU,SAAS;EAC9C,OAAO;OACF,CAAC,IAAI,EAAE,CAAA;YACX,CAAC;QACF,CAAC;QACD,OAAO,EAAE,CAAA;IACV,CAAC,CAAA;IAED,MAAM,SAAS,GAAG;8CAC2B,QAAQ,WAAW,SAAS,WAAW,OAAO;;;MAGtF,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;EACzD,MAAM,SAAS,EAAE;;wBAEK,CAAA;IAEvB,OAAO,UAAU,CAAC,SAAS,CAAC,CAAA;AAC7B,CAAC;AAED,wDAAwD;AACxD,SAAS,WAAW,CAAC,MAAc;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAA;IAC7C,MAAM,MAAM,GAAG,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC5E,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QAClB,OAAO,MAAM,CAAA;IACd,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,WAAW,MAAM,GAAG,EAAE,IAAI,CAAC,CAAA;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AACjC,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC9B,MAAM,SAAS,GAAG,eAAe,EAAE,CAAA;IACnC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAA;IACpE,cAAc,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACtC,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAkB;IAChD,gBAAgB,GAAG,IAAI,GAAG,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;IAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACpC,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;QAC1C,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;YAC7B,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,OAAO,CAAA,CAAC,kBAAkB;IAE9B,KAAK,UAAU,mBAAmB,CAAC,EAClC,IAAI,EACJ,MAAM,GACI;QACV,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CACX,+DAA+D,CAC/D,CAAA;YACD,OAAM;QACP,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CACX,2EAA2E,CAC3E,CAAA;YACD,OAAM;QACP,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,UAA+B,CAAA;QACvD,MAAM,KAAK,GAAkB,EAAE,CAAA;QAC/B,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,UAAU,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;QACpB,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACjD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAC5D,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAChE,CAAA;YACD,MAAM,YAAY,GAAG,MAAM,uBAAuB,CACjD,IAAI,EACJ,MAAM,EACN,OAAO,EACP,OAAO,CACP,CAAA;YACD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC,CAAA;YAEjD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,eAAe,EAAE;gBACvC,KAAK,EAAE,IAAI;gBACX,IAAI,EAAE,KAAK,CAAC,IAAc;gBAC1B,IAAI,EAAE,EAAE;gBACR,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC;aACpC,CAAC,CAAA;YACF,OAAM;QACP,CAAC;QAED,MAAM,EACL,IAAI,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EACrC,SAAS,EACT,KAAK,GACL,GAAG,MAAM,CAAC,IAAI,CAAA;QACf,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;QACpD,MAAM,IAAI,GAAG,CAAC,YAAY,QAAQ,EAAE,CAAC,CAAA;QAErC,wBAAwB;QACxB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAClC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE,CACd,OAAO,GAAG,KAAK,SAAS,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,CAC5D,CAAA;QAED,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,WAAW,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACrD,IAAI,CAAC,IAAI,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;YAC5B,IAAI,CAAC,IAAI,CAAC,YAAY,QAAQ,EAAE,CAAC,CAAA;YACjC,iEAAiE;YACjE,sCAAsC;YACtC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QAC7B,CAAC;QAED,IAAI,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,SAAS,SAAS,EAAE,CAAC,CAAA;QAChC,CAAC;QAED,MAAM,YAAY,GAAG,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;QAClE,MAAM,aAAa,GAAG,EAAE,CAAA;QACxB,MAAM,QAAQ,GAAG,EAAE,CAAA;QACnB,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,YAAY,EAAE,CAAC;YACzC,MAAM,YAAY,GAAG,WAAW,CAC/B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CACpD,CAAA;YACD,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YAChC,MAAM,SAAS,GAAG;QACb,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;EACzD,YAAY;OACP,CAAA;YACJ,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAA;QACxC,CAAC;QAED;;;;;WAKG;QACH,MAAM,WAAW,GAAG,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;QACzD,MAAM,WAAW,GAAG,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;QAChD,MAAM,OAAO,GAAiB;YAC7B,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,WAAW;SACjB,CAAA;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAE5D,IACC,UAAU;YACV,yEAAyE;YACzE,gEAAgE;YAChE,CAAC,CAAC,UAAU,CAAC,OAAO,IAAI,UAAU,CAAC,OAAO,KAAK,WAAW,CAAC;gBAC1D,CAAC,CAAC,UAAU,CAAC,OAAO,IAAI,UAAU,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,EACzD,CAAC;YACF,iDAAiD;YACjD,OAAO,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,IAAI,UAAU,CAAC,IAAI,CAAA;YACvD,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,CAAA;YAChD,OAAO,CAAC,IAAI,GAAG,SAAS,CAAA;YACxB,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;YAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;YACxC,MAAM,OAAO,GAAG;gDAC6B,WAAW,WAAW,SAAS,WAAW,OAAO;iBAChF,IAAI,CAAC,aAAa,kBAAkB,WAAW;;;gBAGhD,QAAQ;;wBAEA,CAAA;YACrB,QAAQ,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAA;QACzC,CAAC;QACD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;QAE5C,gDAAgD;QAChD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAA;IAC9C,CAAC;IAED,OAAO,KAAK,UAAU,mBAAmB,CAAC,IAAe;QACxD,MAAM,SAAS,GAAe,EAAE,CAAA;QAChC,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,UAAU,EAAW,CAAA;QACvE,KAAK,CAA2B,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACvE,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;QACjC,CAAC,CAAuC,CAAC,CAAA;QAEzC,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;YAC/B,MAAM,mBAAmB,CAAC,KAAK,CAAC,CAAA;QACjC,CAAC;QAED,UAAU;QACV,gBAAgB,GAAG,IAAI,GAAG,EAAE,CAAA;IAC7B,CAAC,CAAA;AACF,CAAC","sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport { createProcessor } from '@mdx-js/mdx'\nimport md5 from 'md5-hex'\nimport { type Root as MdastRoot, type Parent, type RootContent } from 'mdast'\nimport {\n\ttype MdxJsxAttribute,\n\ttype MdxJsxFlowElement,\n} from 'mdast-util-mdx-jsx'\nimport { removePosition } from 'unist-util-remove-position'\nimport { visit, type Visitor } from 'unist-util-visit'\nimport * as z from 'zod'\n\ntype CodeFile = {\n\tnode: MdxJsxFlowElement\n\tparent: Parent | null | undefined\n}\n\ntype RangeArray = [number, number][] | undefined\n\ntype CodeFileProps = Record<string, unknown>\n\ntype PathContentMap = Map<string, string[]>\n\nconst APP_TYPES = ['problem', 'solution', 'playground'] as const\ntype AppTypes = typeof APP_TYPES\n\nexport type EmbeddedFile = {\n\terror?: boolean\n\tfile: string\n\thash: string\n\tline?: number\n\twarning?: string\n}\n\nexport type CodeFileData = {\n\tmdxFile: string\n\tcacheLocation: string\n\tcachedEmbeddedFiles: Map<string, EmbeddedFile>\n\tembeddedFiles: Map<string, EmbeddedFile>\n}\n\nconst safePath = (s: string) => s.replace(/\\\\/g, '/')\n\nconst REG_EXP = /^(?:\\d+(?:-\\d+)?,)*\\d+(?:-\\d+)?$/\n\nconst isValidRangeFormat = (value: string | undefined) =>\n\tvalue ? REG_EXP.test(value) : true\n\nconst transformRange = (value: string | undefined) =>\n\tvalue?.split(',').map((range) => {\n\t\tconst [start, end] = range.split('-').map(Number)\n\t\treturn [start, end ?? start] as [number, number]\n\t})\n\nconst isRangeBounded = (\n\trange: RangeArray,\n\tctx: z.RefinementCtx,\n\tlines: number,\n) => {\n\tif (!lines || !Array.isArray(range)) return\n\tif (range.flat().some((r) => r < 1 || r > lines)) {\n\t\tctx.addIssue({\n\t\t\tcode: z.ZodIssueCode.custom,\n\t\t\tmessage: `Range must be between 1 and ${lines}`,\n\t\t})\n\t}\n}\n\nconst isRangeInOrder = (range: RangeArray) =>\n\tArray.isArray(range)\n\t\t? range.every(([a, b]) => !isNaN(Number(a)) && !isNaN(Number(b)) && b >= a)\n\t\t: true\n\nconst isRangesNonOverlapping = (range: RangeArray) => {\n\tif (!Array.isArray(range)) return true\n\treturn range.every(([a], i) => i === 0 || (range[i - 1]?.[1] ?? 0) < a)\n}\n\nlet fileContentCache: PathContentMap = new Map()\nasync function getFileContent(filePath: string) {\n\tif (fileContentCache.has(filePath)) {\n\t\treturn fileContentCache.get(filePath)\n\t}\n\ttry {\n\t\tconst content = await fs.promises.readFile(filePath, 'utf-8')\n\t\tconst fileContent = content.split('\\n')\n\t\tfileContentCache.set(filePath, fileContent)\n\t\treturn fileContent\n\t} catch {\n\t\tconsole.warn(\n\t\t\t`@epic-web/workshop-app - invalid CodeFile.\\nCould not read file: ${filePath}\\n`,\n\t\t)\n\t}\n}\n\nasync function validateProps(props: CodeFileProps, appDir: string) {\n\tlet validRange: RangeArray\n\tlet linesCount = 0\n\n\tconst BooleanSchema = z\n\t\t.nullable(z.string())\n\t\t.optional()\n\t\t.refine(\n\t\t\t(v) => ['true', 'false', null, undefined].includes(v),\n\t\t\t'optional boolean key can be \"true\", \"false\", null or undefined',\n\t\t)\n\t\t.transform((v) => v === null || Boolean(v))\n\n\tconst RangeSchema = z\n\t\t.string()\n\t\t.optional()\n\t\t.refine(isValidRangeFormat, 'Invalid range format')\n\t\t.transform(transformRange)\n\t\t.superRefine((val, ctx) => isRangeBounded(val, ctx, linesCount))\n\t\t.refine(isRangeInOrder, 'Range must be in order low-high')\n\n\tconst inputSchema = z\n\t\t.object({\n\t\t\tfile: z\n\t\t\t\t.string()\n\t\t\t\t.nonempty()\n\t\t\t\t.transform(async (file, ctx) => {\n\t\t\t\t\tconst fullPath = path.join(appDir, file)\n\t\t\t\t\tconst content = await getFileContent(fullPath)\n\t\t\t\t\tif (!content) {\n\t\t\t\t\t\tctx.addIssue({\n\t\t\t\t\t\t\tcode: z.ZodIssueCode.custom,\n\t\t\t\t\t\t\tmessage: `Could not read file`,\n\t\t\t\t\t\t\tfatal: true,\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn z.NEVER\n\t\t\t\t\t}\n\t\t\t\t\tlinesCount = content.length\n\t\t\t\t\t// @mdx-js/mdx parser can NOT handle relative path with backslashes\n\t\t\t\t\treturn {\n\t\t\t\t\t\tfullPath: safePath(fullPath),\n\t\t\t\t\t\tfilePath: safePath(file),\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\trange: RangeSchema.refine((range) => {\n\t\t\t\tconst isValid = isRangesNonOverlapping(range)\n\t\t\t\t// we use this value in highlight refine\n\t\t\t\tvalidRange = isValid ? range : undefined\n\t\t\t\treturn isValid\n\t\t\t}, 'Ranges must not overlap'),\n\t\t\thighlight: RangeSchema.refine((highlight) => {\n\t\t\t\tif (!Array.isArray(highlight) || !Array.isArray(validRange)) {\n\t\t\t\t\treturn z.NEVER\n\t\t\t\t}\n\t\t\t\treturn highlight.every(([hStart, hEnd]) =>\n\t\t\t\t\tvalidRange?.some(\n\t\t\t\t\t\t([rStart, rEnd]) => hStart >= rStart && hEnd <= rEnd,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}, 'Highlight range must be within defined range')\n\t\t\t\t.transform(() => props.highlight as Array<string>)\n\t\t\t\t.optional(),\n\t\t\tnonumber: BooleanSchema,\n\t\t\tnocopy: BooleanSchema,\n\t\t\tbuttons: z\n\t\t\t\t.string()\n\t\t\t\t.optional()\n\t\t\t\t.transform((str) =>\n\t\t\t\t\tstr ? (str.split(',') as unknown as AppTypes) : [],\n\t\t\t\t)\n\t\t\t\t.refine((arr) => arr.every((item) => APP_TYPES.includes(item)), {\n\t\t\t\t\tmessage: `Buttons can only be any of ${APP_TYPES.join(',')}`,\n\t\t\t\t}),\n\t\t})\n\t\t.strict()\n\n\treturn inputSchema.safeParseAsync(props)\n}\n\nasync function createErrorNotification(\n\tnode: MdxJsxFlowElement,\n\terrors: string[],\n\tmdxFile: string,\n\tappType: string,\n) {\n\tconst filename = path.basename(mdxFile)\n\tconst startLine = node.position?.start.line\n\tconst endLine = node.position?.end.line\n\n\tconst codeFence = async () => {\n\t\tif (startLine && endLine) {\n\t\t\tconst contentStr = await getFileContent(mdxFile)\n\t\t\tconst content = contentStr?.slice(startLine - 1, endLine).join('\\n')\n\t\t\tif (content) {\n\t\t\t\treturn `\n\\`\\`\\`tsx filename=${filename} start=${startLine} nocopy\n${content}\n\\`\\`\\``.trim()\n\t\t\t}\n\t\t}\n\t\treturn ''\n\t}\n\n\tconst mdxSource = `\n<CodeFileNotification variant=\"error\" file=\"${filename}\" line=\"${startLine}\" type=\"${appType}\">\n <callout-danger class=\"notification\">\n <div className=\"title\">CodeFile Error: invalid input</div>\n ${errors.map((error) => `<div>${error}</div>`).join('')}\n${await codeFence()}\n </callout-danger>\n</CodeFileNotification>`\n\n\treturn mdxToMdast(mdxSource)\n}\n\n// based on https://github.com/sindresorhus/strip-indent\nfunction stripIndent(string: string) {\n\tconst match = string.match(/^[ \\t]*(?=\\S)/gm)\n\tconst indent = match?.reduce((r, a) => Math.min(r, a.length), Infinity) ?? 0\n\tif (indent === 0) {\n\t\treturn string\n\t}\n\tconst regex = new RegExp(`^[ \\\\t]{${indent}}`, 'gm')\n\treturn string.replace(regex, '')\n}\n\nfunction mdxToMdast(mdx: string) {\n\tconst processor = createProcessor()\n\tconst mdast = processor.parse(mdx.trim()) as MdastRoot | RootContent\n\tremovePosition(mdast, { force: true })\n\treturn mdast.type === 'root' ? mdast.children : [mdast]\n}\n\nexport function remarkCodeFile(data: CodeFileData) {\n\tfileContentCache = new Map()\n\tconst mdxFile = data.mdxFile\n\tconst appDir = path.dirname(mdxFile)\n\tconst appType = mdxFile.includes('problem')\n\t\t? 'problem'\n\t\t: mdxFile.includes('solution')\n\t\t\t? 'solution'\n\t\t\t: 'other' // not in exercise\n\n\tasync function replaceCodeFileNode({\n\t\tnode,\n\t\tparent,\n\t}: CodeFile): Promise<void> {\n\t\tif (!parent) {\n\t\t\tconsole.warn(\n\t\t\t\t'Unexpected error: replaceCodeFileNode called without a Parent',\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t\tconst index = parent.children.indexOf(node)\n\t\tif (index === -1) {\n\t\t\tconsole.warn(\n\t\t\t\t'Unexpected error: replaceCodeFileNode could not find node index in Parent',\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t\tconst attributes = node.attributes as MdxJsxAttribute[]\n\t\tconst props: CodeFileProps = {}\n\t\tfor (const { name, value } of attributes) {\n\t\t\tprops[name] = value\n\t\t}\n\n\t\tconst result = await validateProps(props, appDir)\n\t\tif (!result.success) {\n\t\t\tconst errors = result.error.issues.map(({ message, path }) =>\n\t\t\t\tpath[0] ? `${message}: ${path[0]}=\"${props[path[0]]}\"` : message,\n\t\t\t)\n\t\t\tconst notification = await createErrorNotification(\n\t\t\t\tnode,\n\t\t\t\terrors,\n\t\t\t\tmdxFile,\n\t\t\t\tappType,\n\t\t\t)\n\t\t\tparent.children.splice(index, 1, ...notification)\n\n\t\t\tdata.embeddedFiles.set('invalid input', {\n\t\t\t\terror: true,\n\t\t\t\tfile: props.file as string,\n\t\t\t\thash: '',\n\t\t\t\tline: node.position?.start.line ?? 1,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tconst {\n\t\t\tfile: { content, filePath, fullPath },\n\t\t\thighlight,\n\t\t\trange,\n\t\t} = result.data\n\t\tconst language = path.extname(filePath).substring(1)\n\t\tconst meta = [`filename=${filePath}`]\n\n\t\t// nonumbers nocopy ....\n\t\tObject.entries(result.data).forEach(\n\t\t\t([key, val]) =>\n\t\t\t\ttypeof val === 'boolean' && val && meta.push(`${key}=true`),\n\t\t)\n\n\t\tif (result.data.buttons) {\n\t\t\tmeta.push(`buttons=${result.data.buttons.join(',')}`)\n\t\t\tmeta.push(`type=${appType}`)\n\t\t\tmeta.push(`fullpath=${fullPath}`)\n\t\t\t// Avoid the headache of finding the separator on the client side\n\t\t\t// path.sep is always / on client side\n\t\t\tmeta.push(`sep=${path.sep}`)\n\t\t}\n\n\t\tif (highlight?.length) {\n\t\t\tmeta.push(`lines=${highlight}`)\n\t\t}\n\n\t\tconst fileSections = range?.length ? range : [[1, content.length]]\n\t\tconst rangesContent = []\n\t\tconst preNodes = []\n\t\tfor (const [start, end] of fileSections) {\n\t\t\tconst rangeContent = stripIndent(\n\t\t\t\tcontent.slice(start ? start - 1 : 0, end).join('\\n'),\n\t\t\t)\n\t\t\trangesContent.push(rangeContent)\n\t\t\tconst mdxSource = `\n\\`\\`\\`${language} ${meta.concat(`start=${start}`).join(' ')}\n${rangeContent}\n\\`\\`\\``\n\t\t\tpreNodes.push(...mdxToMdast(mdxSource))\n\t\t}\n\n\t\t/**\n\t\t * Show a warning above the file content if the range we show changed in the\n\t\t * embedded file and the range in <CodeFile range=\"a-b\"> did not change.\n\t\t * The warning will be removed automatically after the <CodeFile> range changes\n\t\t * or by canceling it from the UI.\n\t\t */\n\t\tconst embeddedKey = md5(fullPath + JSON.stringify(range))\n\t\tconst contentHash = md5(rangesContent.join(','))\n\t\tconst newData: EmbeddedFile = {\n\t\t\tfile: fullPath,\n\t\t\thash: contentHash,\n\t\t}\n\t\tconst cachedData = data.cachedEmbeddedFiles.get(embeddedKey)\n\n\t\tif (\n\t\t\tcachedData &&\n\t\t\t// If a warning existed previously and its hash matched the current hash,\n\t\t\t// then the changes were reverted and the warning will be remove\n\t\t\t((cachedData.warning && cachedData.warning !== contentHash) ??\n\t\t\t\t(!cachedData.warning && cachedData.hash !== contentHash))\n\t\t) {\n\t\t\t// keep previously saved warning or previous hash\n\t\t\tnewData.warning = cachedData.warning ?? cachedData.hash\n\t\t\tconst startLine = node.position?.start.line ?? 1\n\t\t\tnewData.line = startLine\n\t\t\tconst mdxFilename = path.basename(mdxFile)\n\t\t\tconst filename = path.basename(filePath)\n\t\t\tconst warning = `\n<CodeFileNotification variant=\"warning\" file=\"${mdxFilename}\" line=\"${startLine}\" type=\"${appType}\"\ncacheLocation=\"${data.cacheLocation}\" embeddedKey=\"${embeddedKey}\">\n <callout-warning class=\"notification\">\n <div className=\"title\">CodeFile Warning:</div>\n <div>file ${filename} content was changed, review 'range' and 'highlight' inputs</div>\n </callout-warning>\n</CodeFileNotification>`\n\t\t\tpreNodes.unshift(...mdxToMdast(warning))\n\t\t}\n\t\tdata.embeddedFiles.set(embeddedKey, newData)\n\n\t\t// replace <CodeFile> with embedded file content\n\t\tparent.children.splice(index, 1, ...preNodes)\n\t}\n\n\treturn async function codeFileTransformer(tree: MdastRoot) {\n\t\tconst codeFiles: CodeFile[] = []\n\t\tconst filter = { type: 'mdxJsxFlowElement', name: 'CodeFile' } as const\n\t\tvisit<MdastRoot, typeof filter>(tree, filter, ((node, _index, parent) => {\n\t\t\tcodeFiles.push({ node, parent })\n\t\t}) as Visitor<MdxJsxFlowElement, Parent>)\n\n\t\tfor (const props of codeFiles) {\n\t\t\tawait replaceCodeFileNode(props)\n\t\t}\n\n\t\t// cleanup\n\t\tfileContentCache = new Map()\n\t}\n}\n"]}
|