@ox-content/code-play 3.0.0-alpha.9 → 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/dist/plugin2.mjs CHANGED
@@ -1,28 +1,553 @@
1
- import { c as DEV_TYPECHECK_PATH, i as escapeAttribute, n as encodePayload, o as DEFAULT_ENDPOINTS, p as resolveLanguage, r as decodeHtml, s as DEFAULT_VIEWERS, t as decodePayload, u as resolveCodePlayOptions } from "./payload.mjs";
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
- return {
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
- viewers: options.viewers,
22
- ui: options.ui,
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$1(attrs, "lang") ?? readAttr$1(attrs, "language") ?? "text";
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
- return wrapWidget(options.encodePayload({
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(/\btypecheck\b/i.test(attrs), void 0, definition, endpoints)
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
- config: {},
75
- viewers: { ...DEFAULT_VIEWERS },
76
- ui: "default",
77
- timeoutMs: 1e4,
609
+ ui: playOptions.ui ?? "default",
610
+ timeoutMs: playOptions.timeoutMs ?? 1e4,
78
611
  endpoints
79
- }), `<pre><code class="language-${escapeAttribute(language)}">${body}</code></pre>`);
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$1(attrs, "class") ?? "").split(/\s+/).find((token) => token.startsWith("language-"))?.slice(9);
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$1(attrs, name) {
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/markdown.ts
137
- const FENCE_OPEN = /^( {0,3})(`{3,}|~{3,})([^\n]*)$/;
138
- function parseTopLevelFences(source) {
139
- const lines = source.split("\n");
140
- const fences = [];
141
- let index = 0;
142
- let offset = 0;
143
- while (index < lines.length) {
144
- const line = lines[index] ?? "";
145
- const open = FENCE_OPEN.exec(line);
146
- if (!open) {
147
- offset += line.length + 1;
148
- index += 1;
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 indent = open[1] ?? "";
152
- const marker = open[2] ?? "```";
153
- const [language = "", ...metaParts] = splitInfo((open[3] ?? "").trim());
154
- const start = offset;
155
- index += 1;
156
- offset += line.length + 1;
157
- const body = [];
158
- while (index < lines.length) {
159
- const candidate = lines[index] ?? "";
160
- if (new RegExp(`^ {0,3}${escapeRegExp(marker)}[ \t]*$`).exec(candidate)) {
161
- const raw = `${line}\n${body.join("\n")}${body.length > 0 ? "\n" : ""}${candidate}`;
162
- fences.push({
163
- language,
164
- meta: metaParts.join(" "),
165
- code: body.join("\n"),
166
- raw,
167
- start,
168
- end: start + raw.length,
169
- indent,
170
- marker
171
- });
172
- offset += candidate.length + 1;
173
- index += 1;
174
- break;
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 fences;
732
+ return {
733
+ files,
734
+ warnings
735
+ };
182
736
  }
183
- function parsePlayFences(source) {
184
- return parseTopLevelFences(source).filter((fence) => hasToken(fence.meta, "play")).map((fence) => ({
185
- language: fence.language,
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 stripPlayMeta(meta) {
195
- return meta.split(/\s+/).filter((token) => token && token !== "play" && token !== "typecheck" && !token.startsWith("play-")).join(" ");
741
+ function pathInside(file, root) {
742
+ const relative = path.relative(root, file);
743
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
196
744
  }
197
- function rewritePlayFences(source, encode) {
198
- const fences = parsePlayFences(source);
199
- if (fences.length === 0) return source;
200
- let cursor = 0;
201
- let output = "";
202
- for (const fence of fences) {
203
- output += source.slice(cursor, fence.start);
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 splitInfo(info) {
234
- return info.trim().split(/\s+/).filter(Boolean);
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 hasToken(meta, token) {
237
- return meta.split(/\s+/).includes(token);
757
+ function normalizeBase(base) {
758
+ if (!base || base === "/") return "/";
759
+ return base.endsWith("/") ? base : `${base}/`;
238
760
  }
239
- function escapeRegExp(value) {
240
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
761
+ function sourceRoot(root, resolved) {
762
+ return path.resolve(root, resolved.srcDir ?? "docs");
241
763
  }
242
- function readAttr(attrs, name) {
243
- return new RegExp(`(?:^|\\s)${name}\\s*=\\s*"([^"]+)"`, "i").exec(attrs)?.[1];
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 stripIndent(value) {
246
- const lines = value.split("\n");
247
- const indents = lines.filter((line) => line.trim()).map((line) => line.match(/^ */)?.[0].length ?? 0);
248
- const indent = indents.length > 0 ? Math.min(...indents) : 0;
249
- return lines.map((line) => line.slice(indent)).join("\n");
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 (explicitTypecheck === void 0) {
378
- if (command === "serve" && resolved.proxy) resolved.endpoints.typecheck = DEV_TYPECHECK_PATH;
379
- else delete resolved.endpoints.typecheck;
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:")) return enhanceGeneratedModule(code, enhanceOptions());
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
- return rewritten === code ? null : rewritten;
948
+ if (rewritten === code) return null;
949
+ needsClientAsset = true;
950
+ return rewritten;
400
951
  },
401
952
  configureServer(server) {
402
- if (resolved.proxy) mountProxies(server, resolved.endpoints.rust, resolved.endpoints.go);
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
- const file = resolveClientFile();
418
- if (!file || command !== "build") return;
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: assertBrowserClientSource(await readFile(file, "utf8"))
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) await writeFile(htmlPath, enhanced);
501
- }
502
- }
503
- function guessHtmlPath(file, srcDir, outDir) {
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 files;
1062
+ return enhancedAny;
516
1063
  }
517
- function normalizeBase(base) {
518
- if (!base || base === "/") return "/";
519
- return base.endsWith("/") ? base : `${base}/`;
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 { stripPlayMeta as a, rewritePlayFences as i, parseCodePlayTags as n, enhanceGeneratedModule as o, parsePlayFences as r, enhancePlayHtml as s, codePlay as t };
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