@ox-content/code-play 3.0.0-alpha.8 → 3.0.0-beta.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/README.md +35 -6
- package/dist/browser.mjs +587 -68
- package/dist/client.mjs +192 -28
- package/dist/client.mjs.map +1 -1
- package/dist/config.d.mts +47 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/hydrate.d.mts +3 -1
- package/dist/hydrate.d.mts.map +1 -1
- package/dist/hydrate2.mjs +395 -43
- package/dist/hydrate2.mjs.map +1 -1
- package/dist/index.d.mts +49 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/payload.mjs +5 -1
- package/dist/payload.mjs.map +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin2.mjs +712 -162
- package/dist/plugin2.mjs.map +1 -1
- package/package.json +1 -1
package/dist/plugin2.mjs
CHANGED
|
@@ -1,28 +1,553 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
1
|
+
import { f as resolveCodePlayOptions, h as resolveLanguage, i as escapeAttribute, n as encodePayload, o as DEFAULT_ENDPOINTS, r as decodeHtml, s as DEFAULT_VIEWERS, t as decodePayload, u as DEV_TYPECHECK_PATH } from "./payload.mjs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/authoring.ts
|
|
7
|
+
function parsePlayMeta(meta) {
|
|
8
|
+
const options = emptyPlayOptions();
|
|
9
|
+
for (const token of splitPlayInfo(meta)) {
|
|
10
|
+
if (token === "typecheck") {
|
|
11
|
+
options.typecheck = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (token === "play-compact") {
|
|
15
|
+
options.ui = "compact";
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (token === "play-headless") {
|
|
19
|
+
options.ui = "headless";
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const pair = readTokenPair(token);
|
|
23
|
+
if (!pair) continue;
|
|
24
|
+
applyPlayOption(options, pair.name, pair.value);
|
|
25
|
+
}
|
|
26
|
+
return options;
|
|
27
|
+
}
|
|
28
|
+
function parseCodePlayAttributes(attrs) {
|
|
29
|
+
const options = emptyPlayOptions();
|
|
30
|
+
for (const [name, value] of readAttributes(attrs)) {
|
|
31
|
+
if (name === "typecheck") {
|
|
32
|
+
options.typecheck = value !== "false";
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (name === "title") {
|
|
36
|
+
options.title = value;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (name === "ui") {
|
|
40
|
+
options.ui = parseUi(value);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (name === "timeout" || name === "timeout-ms" || name === "timeoutms") {
|
|
44
|
+
options.timeoutMs = parsePositiveInt(value);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (name === "viewers") {
|
|
48
|
+
options.viewers = parseViewers(value);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (applyProjectAttribute(options, name, value)) continue;
|
|
52
|
+
if (name.startsWith("config-")) options.config[configKeyFromAttribute(name.slice(7))] = coerceOptionValue(value);
|
|
53
|
+
}
|
|
54
|
+
return options;
|
|
55
|
+
}
|
|
56
|
+
function readCodePlayAttribute(attrs, name) {
|
|
57
|
+
return readAttributes(attrs).get(name.toLowerCase());
|
|
58
|
+
}
|
|
59
|
+
function splitPlayInfo(info) {
|
|
60
|
+
const tokens = [];
|
|
61
|
+
let current = "";
|
|
62
|
+
let quote;
|
|
63
|
+
let escaped = false;
|
|
64
|
+
for (const char of info.trim()) {
|
|
65
|
+
if (escaped) {
|
|
66
|
+
current += `\\${char}`;
|
|
67
|
+
escaped = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (char === "\\") {
|
|
71
|
+
escaped = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (quote) {
|
|
75
|
+
if (char === quote) {
|
|
76
|
+
current += char;
|
|
77
|
+
quote = void 0;
|
|
78
|
+
} else current += char;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === "\"" || char === "'") {
|
|
82
|
+
current += char;
|
|
83
|
+
quote = char;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (/\s/.test(char)) {
|
|
87
|
+
if (current) {
|
|
88
|
+
tokens.push(current);
|
|
89
|
+
current = "";
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
current += char;
|
|
94
|
+
}
|
|
95
|
+
if (current) tokens.push(current);
|
|
96
|
+
return tokens;
|
|
97
|
+
}
|
|
98
|
+
function emptyPlayOptions() {
|
|
99
|
+
return {
|
|
100
|
+
typecheck: false,
|
|
101
|
+
config: {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function applyPlayOption(options, rawName, value) {
|
|
105
|
+
const name = rawName.toLowerCase();
|
|
106
|
+
if (name === "play-title") {
|
|
107
|
+
options.title = value;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (name === "play-ui") {
|
|
111
|
+
options.ui = parseUi(value);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (name === "play-timeout" || name === "play-timeout-ms" || name === "play-timeoutms") {
|
|
115
|
+
options.timeoutMs = parsePositiveInt(value);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (name === "play-viewers") {
|
|
119
|
+
options.viewers = parseViewers(value);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (applyProjectMeta(options, rawName, value)) return;
|
|
123
|
+
const configKey = name.startsWith("play-config:") || name.startsWith("play-config.") ? rawName.slice(12) : name.startsWith("play-") ? rawName.slice(5) : "";
|
|
124
|
+
if (configKey) options.config[configKey] = coerceOptionValue(value);
|
|
125
|
+
}
|
|
126
|
+
function applyProjectMeta(options, rawName, value) {
|
|
127
|
+
switch (rawName.toLowerCase()) {
|
|
128
|
+
case "play-project":
|
|
129
|
+
case "play-sandbox":
|
|
130
|
+
case "play-provider":
|
|
131
|
+
ensureProject(options, value);
|
|
132
|
+
return true;
|
|
133
|
+
case "play-entry":
|
|
134
|
+
ensureProject(options).entry = value;
|
|
135
|
+
return true;
|
|
136
|
+
case "play-file":
|
|
137
|
+
ensureProject(options).file = value;
|
|
138
|
+
return true;
|
|
139
|
+
case "play-files":
|
|
140
|
+
ensureProject(options).files.push(...splitList(value));
|
|
141
|
+
return true;
|
|
142
|
+
case "play-project-url":
|
|
143
|
+
case "play-open-url":
|
|
144
|
+
ensureProject(options).openUrl = value;
|
|
145
|
+
return true;
|
|
146
|
+
case "play-fallback-url":
|
|
147
|
+
ensureProject(options).fallbackUrl = value;
|
|
148
|
+
return true;
|
|
149
|
+
default: return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function applyProjectAttribute(options, name, value) {
|
|
153
|
+
switch (name) {
|
|
154
|
+
case "project":
|
|
155
|
+
case "sandbox":
|
|
156
|
+
case "provider":
|
|
157
|
+
ensureProject(options, value);
|
|
158
|
+
return true;
|
|
159
|
+
case "entry":
|
|
160
|
+
ensureProject(options).entry = value;
|
|
161
|
+
return true;
|
|
162
|
+
case "file":
|
|
163
|
+
ensureProject(options).file = value;
|
|
164
|
+
return true;
|
|
165
|
+
case "files":
|
|
166
|
+
ensureProject(options).files.push(...splitList(value));
|
|
167
|
+
return true;
|
|
168
|
+
case "project-url":
|
|
169
|
+
case "open-url":
|
|
170
|
+
ensureProject(options).openUrl = value;
|
|
171
|
+
return true;
|
|
172
|
+
case "fallback-url":
|
|
173
|
+
ensureProject(options).fallbackUrl = value;
|
|
174
|
+
return true;
|
|
175
|
+
default: return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function ensureProject(options, provider = "external") {
|
|
179
|
+
options.project ??= {
|
|
180
|
+
provider,
|
|
181
|
+
files: []
|
|
182
|
+
};
|
|
183
|
+
if (provider && options.project.provider === "external") options.project.provider = provider;
|
|
184
|
+
return options.project;
|
|
185
|
+
}
|
|
186
|
+
function readTokenPair(token) {
|
|
187
|
+
const index = token.indexOf("=");
|
|
188
|
+
if (index === -1) return;
|
|
189
|
+
return {
|
|
190
|
+
name: token.slice(0, index),
|
|
191
|
+
value: unquote(token.slice(index + 1))
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function readAttributes(attrs) {
|
|
195
|
+
const values = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const match of attrs.matchAll(/([:\w-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g)) {
|
|
197
|
+
const name = match[1]?.toLowerCase();
|
|
198
|
+
if (!name) continue;
|
|
199
|
+
values.set(name, match[2] ?? match[3] ?? match[4] ?? "true");
|
|
200
|
+
}
|
|
201
|
+
return values;
|
|
202
|
+
}
|
|
203
|
+
function parseUi(value) {
|
|
204
|
+
return value === "default" || value === "compact" || value === "headless" ? value : void 0;
|
|
205
|
+
}
|
|
206
|
+
function parsePositiveInt(value) {
|
|
207
|
+
const parsed = Number.parseInt(value, 10);
|
|
208
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
209
|
+
}
|
|
210
|
+
function parseViewers(value) {
|
|
211
|
+
const viewers = {};
|
|
212
|
+
for (const token of value.split(",")) {
|
|
213
|
+
const trimmed = token.trim();
|
|
214
|
+
const enabled = !trimmed.startsWith("-");
|
|
215
|
+
const key = enabled ? trimmed : trimmed.slice(1);
|
|
216
|
+
if (key === "config" || key === "stdio" || key === "stderr" || key === "provenance" || key === "timing") viewers[key] = enabled;
|
|
217
|
+
}
|
|
218
|
+
return Object.keys(viewers).length > 0 ? viewers : void 0;
|
|
219
|
+
}
|
|
220
|
+
function splitList(value) {
|
|
221
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
function coerceOptionValue(value) {
|
|
224
|
+
if (value === "true") return true;
|
|
225
|
+
if (value === "false") return false;
|
|
226
|
+
const numeric = Number(value);
|
|
227
|
+
return value.trim() !== "" && Number.isFinite(numeric) ? numeric : value;
|
|
228
|
+
}
|
|
229
|
+
function configKeyFromAttribute(value) {
|
|
230
|
+
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
231
|
+
}
|
|
232
|
+
function unquote(value) {
|
|
233
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/markdown.ts
|
|
238
|
+
const FENCE_OPEN = /^( {0,3})(`{3,}|~{3,})([^\n]*)$/;
|
|
239
|
+
function parseTopLevelFences(source) {
|
|
240
|
+
const lines = source.split("\n");
|
|
241
|
+
const fences = [];
|
|
242
|
+
let index = 0;
|
|
243
|
+
let offset = 0;
|
|
244
|
+
while (index < lines.length) {
|
|
245
|
+
const line = lines[index] ?? "";
|
|
246
|
+
const open = FENCE_OPEN.exec(line);
|
|
247
|
+
if (!open) {
|
|
248
|
+
offset += line.length + 1;
|
|
249
|
+
index += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const indent = open[1] ?? "";
|
|
253
|
+
const marker = open[2] ?? "```";
|
|
254
|
+
const { language, meta } = readFenceInfo((open[3] ?? "").trim());
|
|
255
|
+
const start = offset;
|
|
256
|
+
index += 1;
|
|
257
|
+
offset += line.length + 1;
|
|
258
|
+
const body = [];
|
|
259
|
+
while (index < lines.length) {
|
|
260
|
+
const candidate = lines[index] ?? "";
|
|
261
|
+
if (new RegExp(`^ {0,3}${escapeRegExp(marker)}[ \t]*$`).exec(candidate)) {
|
|
262
|
+
const raw = `${line}\n${body.join("\n")}${body.length > 0 ? "\n" : ""}${candidate}`;
|
|
263
|
+
fences.push({
|
|
264
|
+
language,
|
|
265
|
+
meta,
|
|
266
|
+
code: body.join("\n"),
|
|
267
|
+
raw,
|
|
268
|
+
start,
|
|
269
|
+
end: start + raw.length,
|
|
270
|
+
indent,
|
|
271
|
+
marker
|
|
272
|
+
});
|
|
273
|
+
offset += candidate.length + 1;
|
|
274
|
+
index += 1;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
body.push(candidate);
|
|
278
|
+
offset += candidate.length + 1;
|
|
279
|
+
index += 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return fences;
|
|
283
|
+
}
|
|
284
|
+
function parsePlayFences(source) {
|
|
285
|
+
return parseTopLevelFences(source).filter((fence) => hasToken(fence.meta, "play")).map((fence) => {
|
|
286
|
+
const options = parsePlayMeta(fence.meta);
|
|
287
|
+
return {
|
|
288
|
+
language: fence.language,
|
|
289
|
+
meta: fence.meta,
|
|
290
|
+
code: fence.code,
|
|
291
|
+
raw: fence.raw,
|
|
292
|
+
start: fence.start,
|
|
293
|
+
end: fence.end,
|
|
294
|
+
typecheck: options.typecheck,
|
|
295
|
+
title: options.title,
|
|
296
|
+
config: options.config,
|
|
297
|
+
ui: options.ui,
|
|
298
|
+
viewers: options.viewers,
|
|
299
|
+
timeoutMs: options.timeoutMs,
|
|
300
|
+
project: options.project
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function stripPlayMeta(meta) {
|
|
305
|
+
return splitPlayInfo(meta).filter((token) => token && token !== "play" && token !== "typecheck" && !token.startsWith("play-") && !token.startsWith("play:")).join(" ");
|
|
306
|
+
}
|
|
307
|
+
function rewritePlayFences(source, encode) {
|
|
308
|
+
const fences = parsePlayFences(source);
|
|
309
|
+
if (fences.length === 0) return source;
|
|
310
|
+
let cursor = 0;
|
|
311
|
+
let output = "";
|
|
312
|
+
for (const fence of fences) {
|
|
313
|
+
output += source.slice(cursor, fence.start);
|
|
314
|
+
const encoded = encode(fence);
|
|
315
|
+
if (encoded === null) output += source.slice(fence.start, fence.end);
|
|
316
|
+
else {
|
|
317
|
+
const cleanedMeta = stripPlayMeta(fence.meta);
|
|
318
|
+
const info = [fence.language, cleanedMeta].filter(Boolean).join(" ");
|
|
319
|
+
output += `<!--ox-code-play:${encoded}-->\n\`\`\`${info}\n${fence.code}\n\`\`\``;
|
|
320
|
+
}
|
|
321
|
+
cursor = fence.end;
|
|
322
|
+
}
|
|
323
|
+
output += source.slice(cursor);
|
|
324
|
+
return output;
|
|
325
|
+
}
|
|
326
|
+
function parseCodePlayTags(source) {
|
|
327
|
+
const tags = [];
|
|
328
|
+
for (const match of source.matchAll(/<CodePlay\b([^>]*)>([\s\S]*?)<\/CodePlay>/gi)) {
|
|
329
|
+
const attrs = match[1] ?? "";
|
|
330
|
+
const language = readCodePlayAttribute(attrs, "lang") ?? readCodePlayAttribute(attrs, "language") ?? "text";
|
|
331
|
+
const options = parseCodePlayAttributes(attrs);
|
|
332
|
+
tags.push({
|
|
333
|
+
language,
|
|
334
|
+
meta: "play",
|
|
335
|
+
code: stripIndent((match[2] ?? "").replace(/^\n/, "").replace(/\n$/, "")),
|
|
336
|
+
raw: match[0] ?? "",
|
|
337
|
+
start: match.index ?? 0,
|
|
338
|
+
end: (match.index ?? 0) + (match[0]?.length ?? 0),
|
|
339
|
+
typecheck: options.typecheck,
|
|
340
|
+
title: options.title,
|
|
341
|
+
config: options.config,
|
|
342
|
+
ui: options.ui,
|
|
343
|
+
viewers: options.viewers,
|
|
344
|
+
timeoutMs: options.timeoutMs,
|
|
345
|
+
project: options.project
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return tags;
|
|
349
|
+
}
|
|
350
|
+
function readFenceInfo(info) {
|
|
351
|
+
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(info);
|
|
352
|
+
return {
|
|
353
|
+
language: match?.[1] ?? "",
|
|
354
|
+
meta: match?.[2]?.trim() ?? ""
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function hasToken(meta, token) {
|
|
358
|
+
return splitPlayInfo(meta).includes(token);
|
|
359
|
+
}
|
|
360
|
+
function escapeRegExp(value) {
|
|
361
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
362
|
+
}
|
|
363
|
+
function stripIndent(value) {
|
|
364
|
+
const lines = value.split("\n");
|
|
365
|
+
const indents = lines.filter((line) => line.trim()).map((line) => line.match(/^ */)?.[0].length ?? 0);
|
|
366
|
+
const indent = indents.length > 0 ? Math.min(...indents) : 0;
|
|
367
|
+
return lines.map((line) => line.slice(indent)).join("\n");
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/project-sandbox.ts
|
|
371
|
+
const PROJECT_SANDBOX_ADAPTERS = {
|
|
372
|
+
stackblitz: adapter("stackblitz", "StackBlitz", "browser"),
|
|
373
|
+
codesandbox: adapter("codesandbox", "CodeSandbox", "browser"),
|
|
374
|
+
webcontainer: adapter("webcontainer", "WebContainer", "node"),
|
|
375
|
+
external: adapter("external", "External sandbox", "external")
|
|
376
|
+
};
|
|
377
|
+
function projectSandboxFromPayloadInput(input) {
|
|
378
|
+
if (!input.project) return;
|
|
379
|
+
const warnings = [...input.warnings ?? []];
|
|
380
|
+
const adapter = resolveProjectSandboxAdapter(input.project.provider, warnings);
|
|
381
|
+
const sourceFile = normalizeProjectPath(input.project.file, warnings, "source file");
|
|
382
|
+
const entry = normalizeProjectPath(input.project.entry, warnings, "entry path");
|
|
383
|
+
const primary = {
|
|
384
|
+
path: sourceFile ?? entry ?? defaultProjectFile(input.language, input.definition),
|
|
385
|
+
code: input.code
|
|
386
|
+
};
|
|
387
|
+
const files = mergeProjectFiles([primary, ...input.files ?? []]);
|
|
388
|
+
const openUrl = safeProjectUrl(input.project.openUrl, adapter.provider, warnings);
|
|
389
|
+
const fallbackUrl = safeProjectUrl(input.project.fallbackUrl, adapter.provider, warnings);
|
|
390
|
+
return adapter.resolve({
|
|
391
|
+
provider: input.project.provider,
|
|
392
|
+
entry: entry ?? primary.path,
|
|
393
|
+
files,
|
|
394
|
+
openUrl,
|
|
395
|
+
fallbackUrl,
|
|
396
|
+
warnings
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function normalizeProjectPath(value, warnings = [], label = "file path") {
|
|
400
|
+
const trimmed = value?.trim();
|
|
401
|
+
if (!trimmed) return;
|
|
402
|
+
const normalized = trimmed.replace(/^\.\//, "");
|
|
403
|
+
if (normalized.startsWith("/") || normalized.includes("\\") || normalized.includes("\0") || normalized.split("/").some((part) => part === "" || part === "." || part === "..") || /^[a-z]+:/i.test(normalized)) {
|
|
404
|
+
warnings.push(`Skipped unsafe project ${label}: ${trimmed}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (normalized.length > 256) {
|
|
408
|
+
warnings.push(`Skipped long project ${label}: ${trimmed}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
return normalized;
|
|
412
|
+
}
|
|
413
|
+
function projectSandboxProviderLabel(provider) {
|
|
414
|
+
return PROJECT_SANDBOX_ADAPTERS[provider].label;
|
|
415
|
+
}
|
|
416
|
+
function adapter(provider, label, target) {
|
|
417
|
+
return {
|
|
418
|
+
provider,
|
|
419
|
+
label,
|
|
420
|
+
target,
|
|
421
|
+
resolve(input) {
|
|
422
|
+
return compactProjectSandbox({
|
|
423
|
+
provider,
|
|
424
|
+
label,
|
|
425
|
+
target,
|
|
426
|
+
entry: input.entry,
|
|
427
|
+
files: input.files,
|
|
428
|
+
openUrl: input.openUrl,
|
|
429
|
+
fallbackUrl: input.fallbackUrl,
|
|
430
|
+
warnings: input.warnings
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function defaultProjectFile(language, definition) {
|
|
436
|
+
if (definition?.framework === "vue") return "src/App.vue";
|
|
437
|
+
if (definition?.framework === "react" || definition?.framework === "solid") return "src/App.tsx";
|
|
438
|
+
if (definition?.framework === "svelte") return "src/App.svelte";
|
|
439
|
+
switch (language) {
|
|
440
|
+
case "javascript": return "index.js";
|
|
441
|
+
case "typescript": return "index.ts";
|
|
442
|
+
case "go": return "main.go";
|
|
443
|
+
case "rust": return "src/main.rs";
|
|
444
|
+
default: return "snippet.txt";
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function resolveProjectSandboxAdapter(rawProvider, warnings) {
|
|
448
|
+
switch (rawProvider.trim().toLowerCase()) {
|
|
449
|
+
case "stackblitz":
|
|
450
|
+
case "stack-blitz":
|
|
451
|
+
case "sb": return PROJECT_SANDBOX_ADAPTERS.stackblitz;
|
|
452
|
+
case "codesandbox":
|
|
453
|
+
case "code-sandbox":
|
|
454
|
+
case "csb": return PROJECT_SANDBOX_ADAPTERS.codesandbox;
|
|
455
|
+
case "webcontainer":
|
|
456
|
+
case "web-container": return PROJECT_SANDBOX_ADAPTERS.webcontainer;
|
|
457
|
+
case "external":
|
|
458
|
+
case "": return PROJECT_SANDBOX_ADAPTERS.external;
|
|
459
|
+
default:
|
|
460
|
+
warnings.push(`Unknown project sandbox provider: ${rawProvider}`);
|
|
461
|
+
return PROJECT_SANDBOX_ADAPTERS.external;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function mergeProjectFiles(files) {
|
|
465
|
+
const merged = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const file of files) merged.set(file.path, file);
|
|
467
|
+
return [...merged.values()];
|
|
468
|
+
}
|
|
469
|
+
function safeProjectUrl(value, provider, warnings) {
|
|
470
|
+
if (!value) return;
|
|
471
|
+
let url;
|
|
472
|
+
try {
|
|
473
|
+
url = new URL(value);
|
|
474
|
+
} catch {
|
|
475
|
+
warnings.push(`Skipped invalid project URL: ${value}`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
479
|
+
warnings.push(`Skipped non-http project URL: ${value}`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (url.username || url.password) {
|
|
483
|
+
warnings.push(`Skipped project URL with credentials: ${url.origin}`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!providerAllowsHost(provider, url.hostname)) {
|
|
487
|
+
warnings.push(`Skipped ${provider} project URL on unexpected host: ${url.hostname}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
return url.href;
|
|
491
|
+
}
|
|
492
|
+
function providerAllowsHost(provider, host) {
|
|
493
|
+
if (provider === "external") return true;
|
|
494
|
+
const normalized = host.toLowerCase();
|
|
495
|
+
return {
|
|
496
|
+
stackblitz: ["stackblitz.com"],
|
|
497
|
+
codesandbox: ["codesandbox.io", "csb.app"],
|
|
498
|
+
webcontainer: ["webcontainers.io", "webcontainer.io"]
|
|
499
|
+
}[provider].some((suffix) => normalized === suffix || normalized.endsWith(`.${suffix}`));
|
|
500
|
+
}
|
|
501
|
+
function compactProjectSandbox(project) {
|
|
502
|
+
const next = {
|
|
503
|
+
provider: project.provider,
|
|
504
|
+
label: project.label,
|
|
505
|
+
target: project.target,
|
|
506
|
+
files: project.files
|
|
507
|
+
};
|
|
508
|
+
if (project.entry) next.entry = project.entry;
|
|
509
|
+
if (project.openUrl) next.openUrl = project.openUrl;
|
|
510
|
+
if (project.fallbackUrl) next.fallbackUrl = project.fallbackUrl;
|
|
511
|
+
if (project.warnings?.length) next.warnings = project.warnings;
|
|
512
|
+
return next;
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
6
515
|
//#region src/payload-factory.ts
|
|
7
|
-
function payloadFromFence(fence, options) {
|
|
516
|
+
function payloadFromFence(fence, options, context = {}) {
|
|
8
517
|
const definition = resolveLanguage(fence.language);
|
|
9
518
|
const enabled = definition ? options.languages.get(definition.id) : void 0;
|
|
10
|
-
|
|
519
|
+
const payload = {
|
|
11
520
|
language: definition?.id ?? fence.language,
|
|
12
521
|
code: fence.code,
|
|
522
|
+
title: fence.title,
|
|
13
523
|
capabilities: {
|
|
14
524
|
execute: enabled?.execute ?? Boolean(definition?.capabilities.execute),
|
|
15
525
|
typecheck: payloadTypecheckEnabled(fence.typecheck, enabled?.typecheck, definition, options.endpoints)
|
|
16
526
|
},
|
|
17
527
|
config: {
|
|
18
528
|
...definition?.defaultConfig,
|
|
19
|
-
...enabled?.config
|
|
529
|
+
...enabled?.config,
|
|
530
|
+
...fence.config
|
|
531
|
+
},
|
|
532
|
+
viewers: {
|
|
533
|
+
...options.viewers,
|
|
534
|
+
...fence.viewers
|
|
20
535
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
timeoutMs: options.timeoutMs,
|
|
536
|
+
ui: fence.ui ?? options.ui,
|
|
537
|
+
timeoutMs: fence.timeoutMs ?? options.timeoutMs,
|
|
24
538
|
endpoints: options.endpoints
|
|
25
539
|
};
|
|
540
|
+
if (enabled?.endpoint) payload.endpoint = enabled.endpoint;
|
|
541
|
+
const project = projectSandboxFromPayloadInput({
|
|
542
|
+
language: payload.language,
|
|
543
|
+
code: payload.code,
|
|
544
|
+
definition,
|
|
545
|
+
project: fence.project,
|
|
546
|
+
files: context.files,
|
|
547
|
+
warnings: context.warnings
|
|
548
|
+
});
|
|
549
|
+
if (project) payload.project = project;
|
|
550
|
+
return payload;
|
|
26
551
|
}
|
|
27
552
|
/** TypeScript typecheck in the browser needs a reachable endpoint; hide the dead button otherwise. */
|
|
28
553
|
function payloadTypecheckEnabled(fenceTypecheck, enabledTypecheck, definition, endpoints) {
|
|
@@ -60,23 +585,39 @@ function wrapCommentedBlocks(html, _options) {
|
|
|
60
585
|
function upgradeCodePlayTags(html, options) {
|
|
61
586
|
return html.replace(CODEPLAY_TAG_PATTERN, (all, attrs, body) => {
|
|
62
587
|
if (/\bdata-ox-code-play=/.test(all)) return all;
|
|
63
|
-
const language = readAttr
|
|
588
|
+
const language = readAttr(attrs, "lang") ?? readAttr(attrs, "language") ?? "text";
|
|
64
589
|
const code = decodeHtml(body).replace(/^\n/, "").replace(/\n$/, "");
|
|
65
590
|
const definition = resolveLanguage(language);
|
|
66
591
|
const endpoints = options.endpoints ?? DEFAULT_ENDPOINTS;
|
|
67
|
-
|
|
592
|
+
const playOptions = parseCodePlayAttributes(attrs);
|
|
593
|
+
const payloadValue = {
|
|
68
594
|
language: definition?.id ?? language,
|
|
69
595
|
code,
|
|
596
|
+
title: playOptions.title,
|
|
70
597
|
capabilities: {
|
|
71
598
|
execute: true,
|
|
72
|
-
typecheck: payloadTypecheckEnabled(
|
|
599
|
+
typecheck: payloadTypecheckEnabled(playOptions.typecheck, void 0, definition, endpoints)
|
|
600
|
+
},
|
|
601
|
+
config: {
|
|
602
|
+
...definition?.defaultConfig,
|
|
603
|
+
...playOptions.config
|
|
604
|
+
},
|
|
605
|
+
viewers: {
|
|
606
|
+
...DEFAULT_VIEWERS,
|
|
607
|
+
...playOptions.viewers
|
|
73
608
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
ui: "default",
|
|
77
|
-
timeoutMs: 1e4,
|
|
609
|
+
ui: playOptions.ui ?? "default",
|
|
610
|
+
timeoutMs: playOptions.timeoutMs ?? 1e4,
|
|
78
611
|
endpoints
|
|
79
|
-
}
|
|
612
|
+
};
|
|
613
|
+
const project = projectSandboxFromPayloadInput({
|
|
614
|
+
language: payloadValue.language,
|
|
615
|
+
code,
|
|
616
|
+
definition,
|
|
617
|
+
project: playOptions.project
|
|
618
|
+
});
|
|
619
|
+
if (project) payloadValue.project = project;
|
|
620
|
+
return wrapWidget(options.encodePayload(payloadValue), `<pre><code class="language-${escapeAttribute(language)}">${body}</code></pre>`);
|
|
80
621
|
});
|
|
81
622
|
}
|
|
82
623
|
function wrapMatchingFences(html, matches) {
|
|
@@ -85,7 +626,7 @@ function wrapMatchingFences(html, matches) {
|
|
|
85
626
|
used: false
|
|
86
627
|
}));
|
|
87
628
|
return html.replace(PRE_PATTERN, (all, attrs, body) => {
|
|
88
|
-
const language = (readAttr
|
|
629
|
+
const language = (readAttr(attrs, "class") ?? "").split(/\s+/).find((token) => token.startsWith("language-"))?.slice(9);
|
|
89
630
|
const code = normalizeCode(decodeHtml(stripTags(body)));
|
|
90
631
|
const match = unused.find((item) => !item.used && aliasesEqual(item.language, language) && normalizeCode(item.code) === code);
|
|
91
632
|
if (!match) return all;
|
|
@@ -94,7 +635,7 @@ function wrapMatchingFences(html, matches) {
|
|
|
94
635
|
});
|
|
95
636
|
}
|
|
96
637
|
function wrapWidget(payload, inner) {
|
|
97
|
-
return `<ox-code-play data-ox-code-play="${escapeAttribute(payload)}">${inner}</ox-code-play>`;
|
|
638
|
+
return `<ox-code-play data-ox-code-play="${escapeAttribute(payload)}" inert>${inner}</ox-code-play>`;
|
|
98
639
|
}
|
|
99
640
|
function readJsonString(source, start) {
|
|
100
641
|
try {
|
|
@@ -119,7 +660,7 @@ function sliceJsonString(source, start) {
|
|
|
119
660
|
}
|
|
120
661
|
throw new Error("Unterminated HTML JSON string.");
|
|
121
662
|
}
|
|
122
|
-
function readAttr
|
|
663
|
+
function readAttr(attrs, name) {
|
|
123
664
|
return new RegExp(`(?:^|\\s)${name}\\s*=\\s*"([^"]+)"`, "i").exec(attrs)?.[1];
|
|
124
665
|
}
|
|
125
666
|
function stripTags(value) {
|
|
@@ -133,120 +674,109 @@ function aliasesEqual(left, right) {
|
|
|
133
674
|
return left.toLowerCase() === right.toLowerCase();
|
|
134
675
|
}
|
|
135
676
|
//#endregion
|
|
136
|
-
//#region src/
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
677
|
+
//#region src/project-files.ts
|
|
678
|
+
const DEFAULT_MAX_FILES = 32;
|
|
679
|
+
const DEFAULT_MAX_BYTES = 262144;
|
|
680
|
+
function collectProjectFiles(project, context = {}) {
|
|
681
|
+
const warnings = [];
|
|
682
|
+
if (!project?.files.length) return {
|
|
683
|
+
files: [],
|
|
684
|
+
warnings
|
|
685
|
+
};
|
|
686
|
+
if (!context.documentPath) {
|
|
687
|
+
warnings.push("Skipped project files because the Markdown source path is unavailable.");
|
|
688
|
+
return {
|
|
689
|
+
files: [],
|
|
690
|
+
warnings
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const documentDir = path.dirname(context.documentPath);
|
|
694
|
+
const sourceRoot = path.resolve(context.sourceRoot ?? documentDir);
|
|
695
|
+
const realSourceRoot = realpathIfExists(sourceRoot) ?? sourceRoot;
|
|
696
|
+
const files = [];
|
|
697
|
+
for (const requested of project.files) {
|
|
698
|
+
if (files.length >= (context.maxFiles ?? DEFAULT_MAX_FILES)) {
|
|
699
|
+
warnings.push(`Skipped project file after ${files.length} files: ${requested}`);
|
|
149
700
|
continue;
|
|
150
701
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
body.push(candidate);
|
|
177
|
-
offset += candidate.length + 1;
|
|
178
|
-
index += 1;
|
|
702
|
+
const safePath = normalizeProjectPath(requested, warnings);
|
|
703
|
+
if (!safePath) continue;
|
|
704
|
+
const absolute = path.resolve(documentDir, safePath);
|
|
705
|
+
if (!pathInside(absolute, sourceRoot)) {
|
|
706
|
+
warnings.push(`Skipped project file outside source root: ${safePath}`);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const realAbsolute = realpathIfExists(absolute);
|
|
710
|
+
if (!realAbsolute) {
|
|
711
|
+
warnings.push(`Skipped missing project file: ${safePath}`);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (!pathInside(realAbsolute, realSourceRoot)) {
|
|
715
|
+
warnings.push(`Skipped project file outside real source root: ${safePath}`);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const stat = statSync(realAbsolute);
|
|
719
|
+
if (!stat.isFile()) {
|
|
720
|
+
warnings.push(`Skipped non-file project path: ${safePath}`);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (stat.size > (context.maxBytes ?? DEFAULT_MAX_BYTES)) {
|
|
724
|
+
warnings.push(`Skipped large project file: ${safePath}`);
|
|
725
|
+
continue;
|
|
179
726
|
}
|
|
727
|
+
files.push({
|
|
728
|
+
path: safePath,
|
|
729
|
+
code: readFileSync(realAbsolute, "utf8")
|
|
730
|
+
});
|
|
180
731
|
}
|
|
181
|
-
return
|
|
732
|
+
return {
|
|
733
|
+
files,
|
|
734
|
+
warnings
|
|
735
|
+
};
|
|
182
736
|
}
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
meta: fence.meta,
|
|
187
|
-
code: fence.code,
|
|
188
|
-
raw: fence.raw,
|
|
189
|
-
start: fence.start,
|
|
190
|
-
end: fence.end,
|
|
191
|
-
typecheck: hasToken(fence.meta, "typecheck")
|
|
192
|
-
}));
|
|
737
|
+
function realpathIfExists(file) {
|
|
738
|
+
if (!existsSync(file)) return;
|
|
739
|
+
return realpathSync(file);
|
|
193
740
|
}
|
|
194
|
-
function
|
|
195
|
-
|
|
741
|
+
function pathInside(file, root) {
|
|
742
|
+
const relative = path.relative(root, file);
|
|
743
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
196
744
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const encoded = encode(fence);
|
|
205
|
-
if (encoded === null) output += source.slice(fence.start, fence.end);
|
|
206
|
-
else {
|
|
207
|
-
const cleanedMeta = stripPlayMeta(fence.meta);
|
|
208
|
-
const info = [fence.language, cleanedMeta].filter(Boolean).join(" ");
|
|
209
|
-
output += `<!--ox-code-play:${encoded}-->\n\`\`\`${info}\n${fence.code}\n\`\`\``;
|
|
210
|
-
}
|
|
211
|
-
cursor = fence.end;
|
|
212
|
-
}
|
|
213
|
-
output += source.slice(cursor);
|
|
214
|
-
return output;
|
|
215
|
-
}
|
|
216
|
-
function parseCodePlayTags(source) {
|
|
217
|
-
const tags = [];
|
|
218
|
-
for (const match of source.matchAll(/<CodePlay\b([^>]*)>([\s\S]*?)<\/CodePlay>/gi)) {
|
|
219
|
-
const attrs = match[1] ?? "";
|
|
220
|
-
const language = readAttr(attrs, "lang") ?? readAttr(attrs, "language") ?? "text";
|
|
221
|
-
tags.push({
|
|
222
|
-
language,
|
|
223
|
-
meta: "play",
|
|
224
|
-
code: stripIndent((match[2] ?? "").replace(/^\n/, "").replace(/\n$/, "")),
|
|
225
|
-
raw: match[0] ?? "",
|
|
226
|
-
start: match.index ?? 0,
|
|
227
|
-
end: (match.index ?? 0) + (match[0]?.length ?? 0),
|
|
228
|
-
typecheck: /\btypecheck\b/i.test(attrs)
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
return tags;
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/plugin-paths.ts
|
|
747
|
+
const MARKDOWN_RE = /\.(?:md|markdown|mdx)(?:$|\?)/i;
|
|
748
|
+
function cleanMarkdownPath(id, root) {
|
|
749
|
+
const file = id.split("?")[0];
|
|
750
|
+
if (!file || file.startsWith("\0")) return;
|
|
751
|
+
return path.isAbsolute(file) ? file : path.resolve(root, file);
|
|
232
752
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
753
|
+
function guessHtmlPath(file, srcDir, outDir) {
|
|
754
|
+
const relative = path.relative(srcDir, file).replace(/\.(?:md|markdown|mdx)$/i, "");
|
|
755
|
+
return [path.join(outDir, `${relative}.html`), path.join(outDir, relative, "index.html")].find((candidate) => existsSync(candidate));
|
|
235
756
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
757
|
+
function normalizeBase(base) {
|
|
758
|
+
if (!base || base === "/") return "/";
|
|
759
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
238
760
|
}
|
|
239
|
-
function
|
|
240
|
-
return
|
|
761
|
+
function sourceRoot(root, resolved) {
|
|
762
|
+
return path.resolve(root, resolved.srcDir ?? "docs");
|
|
241
763
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
764
|
+
function urlToMarkdown(urlPath, root, srcDir, base) {
|
|
765
|
+
let relative = urlPath;
|
|
766
|
+
if (base !== "/" && relative.startsWith(base)) relative = relative.slice(base.length);
|
|
767
|
+
relative = relative.replace(/^\//, "").replace(/\.html$/, "");
|
|
768
|
+
if (!relative || relative.includes("..")) return;
|
|
769
|
+
return [path.resolve(root, srcDir, `${relative}.md`), path.resolve(root, srcDir, relative, "index.md")].find((candidate) => existsSync(candidate));
|
|
244
770
|
}
|
|
245
|
-
function
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
771
|
+
function walkFiles(dir) {
|
|
772
|
+
const entries = readdirSync(dir);
|
|
773
|
+
const files = [];
|
|
774
|
+
for (const entry of entries) {
|
|
775
|
+
const full = path.join(dir, entry);
|
|
776
|
+
if (statSync(full).isDirectory()) files.push(...walkFiles(full));
|
|
777
|
+
else files.push(full);
|
|
778
|
+
}
|
|
779
|
+
return files;
|
|
250
780
|
}
|
|
251
781
|
//#endregion
|
|
252
782
|
//#region src/plugin-client.ts
|
|
@@ -273,6 +803,13 @@ function assertBrowserClientSource(source) {
|
|
|
273
803
|
return source;
|
|
274
804
|
}
|
|
275
805
|
//#endregion
|
|
806
|
+
//#region src/plugin-assets.ts
|
|
807
|
+
async function writeClientAsset(outDir, source) {
|
|
808
|
+
if (!source) return;
|
|
809
|
+
await mkdir(outDir, { recursive: true });
|
|
810
|
+
await writeFile(path.join(outDir, "ox-code-play.js"), source);
|
|
811
|
+
}
|
|
812
|
+
//#endregion
|
|
276
813
|
//#region src/plugin-proxy.ts
|
|
277
814
|
const PROXY_MAX_BODY_BYTES = 262144;
|
|
278
815
|
var ProxyRequestError = class extends Error {
|
|
@@ -352,14 +889,20 @@ function writeJson(res, status, body) {
|
|
|
352
889
|
//#region src/plugin.ts
|
|
353
890
|
const VIRTUAL_ID = "virtual:ox-content/code-play";
|
|
354
891
|
const RESOLVED_VIRTUAL = `\0${VIRTUAL_ID}`;
|
|
355
|
-
const MARKDOWN_RE = /\.(?:md|markdown|mdx)(?:$|\?)/i;
|
|
356
892
|
function codePlay(options = {}) {
|
|
357
893
|
const resolved = resolveCodePlayOptions(options);
|
|
358
894
|
const explicitTypecheck = options.endpoints?.typecheck;
|
|
895
|
+
const proxyTargets = {
|
|
896
|
+
rust: resolved.endpoints.rust,
|
|
897
|
+
go: resolved.endpoints.go
|
|
898
|
+
};
|
|
359
899
|
let base = resolved.base;
|
|
360
900
|
let command = "serve";
|
|
361
901
|
let outDir = resolved.outDir;
|
|
362
902
|
let root = process.cwd();
|
|
903
|
+
let needsClientAsset = false;
|
|
904
|
+
let emittedClientAsset = false;
|
|
905
|
+
let clientSource;
|
|
363
906
|
const enhanceOptions = (matchFences) => ({
|
|
364
907
|
scriptSrc: `${base}ox-code-play.js`,
|
|
365
908
|
decodePayload,
|
|
@@ -374,10 +917,12 @@ function codePlay(options = {}) {
|
|
|
374
917
|
root = config.root;
|
|
375
918
|
base = resolved.base === "/" ? normalizeBase(config.base) : resolved.base;
|
|
376
919
|
outDir = resolved.outDir ?? config.build.outDir;
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
920
|
+
if (command === "serve" && resolved.proxy) {
|
|
921
|
+
resolved.endpoints.rust = options.endpoints?.rust ?? "/__ox-code-play/rust";
|
|
922
|
+
resolved.endpoints.go = options.endpoints?.go ?? "/__ox-code-play/go";
|
|
380
923
|
}
|
|
924
|
+
if (explicitTypecheck === void 0 && command === "serve" && resolved.proxy) resolved.endpoints.typecheck = DEV_TYPECHECK_PATH;
|
|
925
|
+
else if (explicitTypecheck === void 0) delete resolved.endpoints.typecheck;
|
|
381
926
|
},
|
|
382
927
|
resolveId(id) {
|
|
383
928
|
return id === VIRTUAL_ID ? RESOLVED_VIRTUAL : null;
|
|
@@ -388,18 +933,24 @@ function codePlay(options = {}) {
|
|
|
388
933
|
},
|
|
389
934
|
transform(code, id) {
|
|
390
935
|
if (!MARKDOWN_RE.test(id)) {
|
|
391
|
-
if (code.includes("export const html = ") && code.includes("ox-code-play:"))
|
|
936
|
+
if (code.includes("export const html = ") && code.includes("ox-code-play:")) {
|
|
937
|
+
const enhanced = enhanceGeneratedModule(code, enhanceOptions());
|
|
938
|
+
if (enhanced !== code) needsClientAsset = true;
|
|
939
|
+
return enhanced;
|
|
940
|
+
}
|
|
392
941
|
return null;
|
|
393
942
|
}
|
|
394
943
|
const rewritten = rewritePlayFences(code, (fence) => {
|
|
395
944
|
const definition = resolveLanguage(fence.language);
|
|
396
945
|
if (!definition || !resolved.languages.has(definition.id)) return null;
|
|
397
|
-
return encodePayload(payloadFromFence(fence, resolved));
|
|
946
|
+
return encodePayload(payloadFromFence(fence, resolved, projectContextForFence(fence, cleanMarkdownPath(id, root), sourceRoot(root, resolved))));
|
|
398
947
|
});
|
|
399
|
-
|
|
948
|
+
if (rewritten === code) return null;
|
|
949
|
+
needsClientAsset = true;
|
|
950
|
+
return rewritten;
|
|
400
951
|
},
|
|
401
952
|
configureServer(server) {
|
|
402
|
-
if (resolved.proxy) mountProxies(server,
|
|
953
|
+
if (resolved.proxy) mountProxies(server, proxyTargets.rust, proxyTargets.go);
|
|
403
954
|
server.middlewares.use(async (req, res, next) => {
|
|
404
955
|
const urlPath = req.url?.split("?")[0] ?? "";
|
|
405
956
|
if (urlPath === `${base}ox-code-play.js`.replace(/\/{2,}/g, "/")) {
|
|
@@ -414,22 +965,33 @@ function codePlay(options = {}) {
|
|
|
414
965
|
});
|
|
415
966
|
},
|
|
416
967
|
async generateBundle() {
|
|
417
|
-
|
|
418
|
-
|
|
968
|
+
if (command !== "build" || !needsClientAsset) return;
|
|
969
|
+
const source = await loadClientSource();
|
|
970
|
+
if (!source) return;
|
|
419
971
|
this.emitFile({
|
|
420
972
|
type: "asset",
|
|
421
973
|
fileName: "ox-code-play.js",
|
|
422
|
-
source
|
|
974
|
+
source
|
|
423
975
|
});
|
|
976
|
+
emittedClientAsset = true;
|
|
424
977
|
},
|
|
425
978
|
async closeBundle() {
|
|
426
979
|
if (command !== "build") return;
|
|
427
980
|
const srcDir = path.resolve(root, resolved.srcDir ?? "docs");
|
|
428
981
|
const destination = path.resolve(root, outDir ?? "dist");
|
|
429
982
|
if (!existsSync(srcDir) || !existsSync(destination)) return;
|
|
430
|
-
await enhanceWrittenPages(srcDir, destination, resolved, enhanceOptions);
|
|
983
|
+
if (!await enhanceWrittenPages(srcDir, destination, resolved, enhanceOptions)) return;
|
|
984
|
+
needsClientAsset = true;
|
|
985
|
+
if (!emittedClientAsset) await writeClientAsset(destination, await loadClientSource());
|
|
431
986
|
}
|
|
432
987
|
};
|
|
988
|
+
async function loadClientSource() {
|
|
989
|
+
if (clientSource) return clientSource;
|
|
990
|
+
const file = resolveClientFile();
|
|
991
|
+
if (!file) return;
|
|
992
|
+
clientSource = assertBrowserClientSource(await readFile(file, "utf8"));
|
|
993
|
+
return clientSource;
|
|
994
|
+
}
|
|
433
995
|
}
|
|
434
996
|
function interceptHtml(res, enhance) {
|
|
435
997
|
const originalEnd = res.end.bind(res);
|
|
@@ -451,6 +1013,7 @@ function enhanceHtmlForUrl(urlPath, html, root, resolved, enhance) {
|
|
|
451
1013
|
const markdownPath = urlToMarkdown(urlPath, root, resolved.srcDir ?? "docs", resolved.base);
|
|
452
1014
|
if (!markdownPath || !existsSync(markdownPath)) return;
|
|
453
1015
|
const source = readFileSync(markdownPath, "utf8");
|
|
1016
|
+
const srcRoot = sourceRoot(root, resolved);
|
|
454
1017
|
const fences = [...parsePlayFences(source), ...parseCodePlayTags(source)].filter((fence) => {
|
|
455
1018
|
const definition = resolveLanguage(fence.language);
|
|
456
1019
|
return Boolean(definition && resolved.languages.has(definition.id));
|
|
@@ -459,16 +1022,9 @@ function enhanceHtmlForUrl(urlPath, html, root, resolved, enhance) {
|
|
|
459
1022
|
return enhancePlayHtml(html, enhance(fences.map((fence) => ({
|
|
460
1023
|
language: fence.language,
|
|
461
1024
|
code: fence.code,
|
|
462
|
-
payload: encodePayload(payloadFromFence(fence, resolved))
|
|
1025
|
+
payload: encodePayload(payloadFromFence(fence, resolved, projectContextForFence(fence, markdownPath, srcRoot)))
|
|
463
1026
|
}))));
|
|
464
1027
|
}
|
|
465
|
-
function urlToMarkdown(urlPath, root, srcDir, base) {
|
|
466
|
-
let relative = urlPath;
|
|
467
|
-
if (base !== "/" && relative.startsWith(base)) relative = relative.slice(base.length);
|
|
468
|
-
relative = relative.replace(/^\//, "").replace(/\.html$/, "");
|
|
469
|
-
if (!relative || relative.includes("..")) return;
|
|
470
|
-
return [path.resolve(root, srcDir, `${relative}.md`), path.resolve(root, srcDir, relative, "index.md")].find((candidate) => existsSync(candidate));
|
|
471
|
-
}
|
|
472
1028
|
function mountProxies(server, rustUrl, goUrl) {
|
|
473
1029
|
server.middlewares.use("/__ox-code-play/rust", (req, res) => {
|
|
474
1030
|
proxy(req, res, rustUrl, "application/json");
|
|
@@ -481,6 +1037,7 @@ function mountProxies(server, rustUrl, goUrl) {
|
|
|
481
1037
|
});
|
|
482
1038
|
}
|
|
483
1039
|
async function enhanceWrittenPages(srcDir, outDir, resolved, enhance) {
|
|
1040
|
+
let enhancedAny = false;
|
|
484
1041
|
for (const file of walkFiles(srcDir)) {
|
|
485
1042
|
if (!MARKDOWN_RE.test(file)) continue;
|
|
486
1043
|
const source = await readFile(file, "utf8");
|
|
@@ -495,30 +1052,23 @@ async function enhanceWrittenPages(srcDir, outDir, resolved, enhance) {
|
|
|
495
1052
|
const enhanced = enhancePlayHtml(html, enhance(fences.map((fence) => ({
|
|
496
1053
|
language: fence.language,
|
|
497
1054
|
code: fence.code,
|
|
498
|
-
payload: encodePayload(payloadFromFence(fence, resolved))
|
|
1055
|
+
payload: encodePayload(payloadFromFence(fence, resolved, projectContextForFence(fence, file, srcDir)))
|
|
499
1056
|
}))));
|
|
500
|
-
if (enhanced !== html)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const relative = path.relative(srcDir, file).replace(/\.(?:md|markdown|mdx)$/i, "");
|
|
505
|
-
return [path.join(outDir, `${relative}.html`), path.join(outDir, relative, "index.html")].find((candidate) => existsSync(candidate));
|
|
506
|
-
}
|
|
507
|
-
function walkFiles(dir) {
|
|
508
|
-
const entries = readdirSync(dir);
|
|
509
|
-
const files = [];
|
|
510
|
-
for (const entry of entries) {
|
|
511
|
-
const full = path.join(dir, entry);
|
|
512
|
-
if (statSync(full).isDirectory()) files.push(...walkFiles(full));
|
|
513
|
-
else files.push(full);
|
|
1057
|
+
if (enhanced !== html) {
|
|
1058
|
+
await writeFile(htmlPath, enhanced);
|
|
1059
|
+
enhancedAny = true;
|
|
1060
|
+
}
|
|
514
1061
|
}
|
|
515
|
-
return
|
|
1062
|
+
return enhancedAny;
|
|
516
1063
|
}
|
|
517
|
-
function
|
|
518
|
-
if (!
|
|
519
|
-
return
|
|
1064
|
+
function projectContextForFence(fence, documentPath, srcRoot) {
|
|
1065
|
+
if (!fence.project) return {};
|
|
1066
|
+
return collectProjectFiles(fence.project, {
|
|
1067
|
+
documentPath,
|
|
1068
|
+
sourceRoot: srcRoot
|
|
1069
|
+
});
|
|
520
1070
|
}
|
|
521
1071
|
//#endregion
|
|
522
|
-
export {
|
|
1072
|
+
export { normalizeProjectPath as a, parseCodePlayTags as c, stripPlayMeta as d, PROJECT_SANDBOX_ADAPTERS as i, parsePlayFences as l, enhanceGeneratedModule as n, projectSandboxFromPayloadInput as o, enhancePlayHtml as r, projectSandboxProviderLabel as s, codePlay as t, rewritePlayFences as u };
|
|
523
1073
|
|
|
524
1074
|
//# sourceMappingURL=plugin2.mjs.map
|