@ox-content/code-play 3.0.0-alpha.1 → 3.0.0-alpha.10
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 +19 -6
- package/dist/browser.mjs +481 -57
- package/dist/client.mjs +189 -28
- package/dist/client.mjs.map +1 -1
- package/dist/config.d.mts +15 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/hydrate.d.mts +3 -2
- package/dist/hydrate.d.mts.map +1 -1
- package/dist/hydrate.mjs +2 -2
- package/dist/hydrate2.d.mts +2 -2
- package/dist/hydrate2.mjs +295 -32
- package/dist/hydrate2.mjs.map +1 -1
- package/dist/index.d.mts +24 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/payload.mjs +10 -2
- package/dist/payload.mjs.map +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin2.mjs +378 -144
- package/dist/plugin2.mjs.map +1 -1
- package/package.json +1 -1
package/dist/plugin2.mjs
CHANGED
|
@@ -1,28 +1,332 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
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
3
|
import { existsSync, readFileSync, readdirSync, 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 (name.startsWith("config-")) options.config[configKeyFromAttribute(name.slice(7))] = coerceOptionValue(value);
|
|
52
|
+
}
|
|
53
|
+
return options;
|
|
54
|
+
}
|
|
55
|
+
function readCodePlayAttribute(attrs, name) {
|
|
56
|
+
return readAttributes(attrs).get(name.toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
function splitPlayInfo(info) {
|
|
59
|
+
const tokens = [];
|
|
60
|
+
let current = "";
|
|
61
|
+
let quote;
|
|
62
|
+
let escaped = false;
|
|
63
|
+
for (const char of info.trim()) {
|
|
64
|
+
if (escaped) {
|
|
65
|
+
current += `\\${char}`;
|
|
66
|
+
escaped = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (char === "\\") {
|
|
70
|
+
escaped = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (quote) {
|
|
74
|
+
if (char === quote) {
|
|
75
|
+
current += char;
|
|
76
|
+
quote = void 0;
|
|
77
|
+
} else current += char;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (char === "\"" || char === "'") {
|
|
81
|
+
current += char;
|
|
82
|
+
quote = char;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (/\s/.test(char)) {
|
|
86
|
+
if (current) {
|
|
87
|
+
tokens.push(current);
|
|
88
|
+
current = "";
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
current += char;
|
|
93
|
+
}
|
|
94
|
+
if (current) tokens.push(current);
|
|
95
|
+
return tokens;
|
|
96
|
+
}
|
|
97
|
+
function emptyPlayOptions() {
|
|
98
|
+
return {
|
|
99
|
+
typecheck: false,
|
|
100
|
+
config: {}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function applyPlayOption(options, rawName, value) {
|
|
104
|
+
const name = rawName.toLowerCase();
|
|
105
|
+
if (name === "play-title") {
|
|
106
|
+
options.title = value;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (name === "play-ui") {
|
|
110
|
+
options.ui = parseUi(value);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (name === "play-timeout" || name === "play-timeout-ms" || name === "play-timeoutms") {
|
|
114
|
+
options.timeoutMs = parsePositiveInt(value);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (name === "play-viewers") {
|
|
118
|
+
options.viewers = parseViewers(value);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const configKey = name.startsWith("play-config:") || name.startsWith("play-config.") ? rawName.slice(12) : name.startsWith("play-") ? rawName.slice(5) : "";
|
|
122
|
+
if (configKey) options.config[configKey] = coerceOptionValue(value);
|
|
123
|
+
}
|
|
124
|
+
function readTokenPair(token) {
|
|
125
|
+
const index = token.indexOf("=");
|
|
126
|
+
if (index === -1) return;
|
|
127
|
+
return {
|
|
128
|
+
name: token.slice(0, index),
|
|
129
|
+
value: unquote(token.slice(index + 1))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function readAttributes(attrs) {
|
|
133
|
+
const values = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const match of attrs.matchAll(/([:\w-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g)) {
|
|
135
|
+
const name = match[1]?.toLowerCase();
|
|
136
|
+
if (!name) continue;
|
|
137
|
+
values.set(name, match[2] ?? match[3] ?? match[4] ?? "true");
|
|
138
|
+
}
|
|
139
|
+
return values;
|
|
140
|
+
}
|
|
141
|
+
function parseUi(value) {
|
|
142
|
+
return value === "default" || value === "compact" || value === "headless" ? value : void 0;
|
|
143
|
+
}
|
|
144
|
+
function parsePositiveInt(value) {
|
|
145
|
+
const parsed = Number.parseInt(value, 10);
|
|
146
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
147
|
+
}
|
|
148
|
+
function parseViewers(value) {
|
|
149
|
+
const viewers = {};
|
|
150
|
+
for (const token of value.split(",")) {
|
|
151
|
+
const trimmed = token.trim();
|
|
152
|
+
const enabled = !trimmed.startsWith("-");
|
|
153
|
+
const key = enabled ? trimmed : trimmed.slice(1);
|
|
154
|
+
if (key === "config" || key === "stdio" || key === "stderr" || key === "provenance" || key === "timing") viewers[key] = enabled;
|
|
155
|
+
}
|
|
156
|
+
return Object.keys(viewers).length > 0 ? viewers : void 0;
|
|
157
|
+
}
|
|
158
|
+
function coerceOptionValue(value) {
|
|
159
|
+
if (value === "true") return true;
|
|
160
|
+
if (value === "false") return false;
|
|
161
|
+
const numeric = Number(value);
|
|
162
|
+
return value.trim() !== "" && Number.isFinite(numeric) ? numeric : value;
|
|
163
|
+
}
|
|
164
|
+
function configKeyFromAttribute(value) {
|
|
165
|
+
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
166
|
+
}
|
|
167
|
+
function unquote(value) {
|
|
168
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/markdown.ts
|
|
173
|
+
const FENCE_OPEN = /^( {0,3})(`{3,}|~{3,})([^\n]*)$/;
|
|
174
|
+
function parseTopLevelFences(source) {
|
|
175
|
+
const lines = source.split("\n");
|
|
176
|
+
const fences = [];
|
|
177
|
+
let index = 0;
|
|
178
|
+
let offset = 0;
|
|
179
|
+
while (index < lines.length) {
|
|
180
|
+
const line = lines[index] ?? "";
|
|
181
|
+
const open = FENCE_OPEN.exec(line);
|
|
182
|
+
if (!open) {
|
|
183
|
+
offset += line.length + 1;
|
|
184
|
+
index += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const indent = open[1] ?? "";
|
|
188
|
+
const marker = open[2] ?? "```";
|
|
189
|
+
const { language, meta } = readFenceInfo((open[3] ?? "").trim());
|
|
190
|
+
const start = offset;
|
|
191
|
+
index += 1;
|
|
192
|
+
offset += line.length + 1;
|
|
193
|
+
const body = [];
|
|
194
|
+
while (index < lines.length) {
|
|
195
|
+
const candidate = lines[index] ?? "";
|
|
196
|
+
if (new RegExp(`^ {0,3}${escapeRegExp(marker)}[ \t]*$`).exec(candidate)) {
|
|
197
|
+
const raw = `${line}\n${body.join("\n")}${body.length > 0 ? "\n" : ""}${candidate}`;
|
|
198
|
+
fences.push({
|
|
199
|
+
language,
|
|
200
|
+
meta,
|
|
201
|
+
code: body.join("\n"),
|
|
202
|
+
raw,
|
|
203
|
+
start,
|
|
204
|
+
end: start + raw.length,
|
|
205
|
+
indent,
|
|
206
|
+
marker
|
|
207
|
+
});
|
|
208
|
+
offset += candidate.length + 1;
|
|
209
|
+
index += 1;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
body.push(candidate);
|
|
213
|
+
offset += candidate.length + 1;
|
|
214
|
+
index += 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return fences;
|
|
218
|
+
}
|
|
219
|
+
function parsePlayFences(source) {
|
|
220
|
+
return parseTopLevelFences(source).filter((fence) => hasToken(fence.meta, "play")).map((fence) => {
|
|
221
|
+
const options = parsePlayMeta(fence.meta);
|
|
222
|
+
return {
|
|
223
|
+
language: fence.language,
|
|
224
|
+
meta: fence.meta,
|
|
225
|
+
code: fence.code,
|
|
226
|
+
raw: fence.raw,
|
|
227
|
+
start: fence.start,
|
|
228
|
+
end: fence.end,
|
|
229
|
+
typecheck: options.typecheck,
|
|
230
|
+
title: options.title,
|
|
231
|
+
config: options.config,
|
|
232
|
+
ui: options.ui,
|
|
233
|
+
viewers: options.viewers,
|
|
234
|
+
timeoutMs: options.timeoutMs
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function stripPlayMeta(meta) {
|
|
239
|
+
return splitPlayInfo(meta).filter((token) => token && token !== "play" && token !== "typecheck" && !token.startsWith("play-") && !token.startsWith("play:")).join(" ");
|
|
240
|
+
}
|
|
241
|
+
function rewritePlayFences(source, encode) {
|
|
242
|
+
const fences = parsePlayFences(source);
|
|
243
|
+
if (fences.length === 0) return source;
|
|
244
|
+
let cursor = 0;
|
|
245
|
+
let output = "";
|
|
246
|
+
for (const fence of fences) {
|
|
247
|
+
output += source.slice(cursor, fence.start);
|
|
248
|
+
const encoded = encode(fence);
|
|
249
|
+
if (encoded === null) output += source.slice(fence.start, fence.end);
|
|
250
|
+
else {
|
|
251
|
+
const cleanedMeta = stripPlayMeta(fence.meta);
|
|
252
|
+
const info = [fence.language, cleanedMeta].filter(Boolean).join(" ");
|
|
253
|
+
output += `<!--ox-code-play:${encoded}-->\n\`\`\`${info}\n${fence.code}\n\`\`\``;
|
|
254
|
+
}
|
|
255
|
+
cursor = fence.end;
|
|
256
|
+
}
|
|
257
|
+
output += source.slice(cursor);
|
|
258
|
+
return output;
|
|
259
|
+
}
|
|
260
|
+
function parseCodePlayTags(source) {
|
|
261
|
+
const tags = [];
|
|
262
|
+
for (const match of source.matchAll(/<CodePlay\b([^>]*)>([\s\S]*?)<\/CodePlay>/gi)) {
|
|
263
|
+
const attrs = match[1] ?? "";
|
|
264
|
+
const language = readCodePlayAttribute(attrs, "lang") ?? readCodePlayAttribute(attrs, "language") ?? "text";
|
|
265
|
+
const options = parseCodePlayAttributes(attrs);
|
|
266
|
+
tags.push({
|
|
267
|
+
language,
|
|
268
|
+
meta: "play",
|
|
269
|
+
code: stripIndent((match[2] ?? "").replace(/^\n/, "").replace(/\n$/, "")),
|
|
270
|
+
raw: match[0] ?? "",
|
|
271
|
+
start: match.index ?? 0,
|
|
272
|
+
end: (match.index ?? 0) + (match[0]?.length ?? 0),
|
|
273
|
+
typecheck: options.typecheck,
|
|
274
|
+
title: options.title,
|
|
275
|
+
config: options.config,
|
|
276
|
+
ui: options.ui,
|
|
277
|
+
viewers: options.viewers,
|
|
278
|
+
timeoutMs: options.timeoutMs
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return tags;
|
|
282
|
+
}
|
|
283
|
+
function readFenceInfo(info) {
|
|
284
|
+
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(info);
|
|
285
|
+
return {
|
|
286
|
+
language: match?.[1] ?? "",
|
|
287
|
+
meta: match?.[2]?.trim() ?? ""
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function hasToken(meta, token) {
|
|
291
|
+
return splitPlayInfo(meta).includes(token);
|
|
292
|
+
}
|
|
293
|
+
function escapeRegExp(value) {
|
|
294
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
295
|
+
}
|
|
296
|
+
function stripIndent(value) {
|
|
297
|
+
const lines = value.split("\n");
|
|
298
|
+
const indents = lines.filter((line) => line.trim()).map((line) => line.match(/^ */)?.[0].length ?? 0);
|
|
299
|
+
const indent = indents.length > 0 ? Math.min(...indents) : 0;
|
|
300
|
+
return lines.map((line) => line.slice(indent)).join("\n");
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
6
303
|
//#region src/payload-factory.ts
|
|
7
304
|
function payloadFromFence(fence, options) {
|
|
8
305
|
const definition = resolveLanguage(fence.language);
|
|
9
306
|
const enabled = definition ? options.languages.get(definition.id) : void 0;
|
|
10
|
-
|
|
307
|
+
const payload = {
|
|
11
308
|
language: definition?.id ?? fence.language,
|
|
12
309
|
code: fence.code,
|
|
310
|
+
title: fence.title,
|
|
13
311
|
capabilities: {
|
|
14
312
|
execute: enabled?.execute ?? Boolean(definition?.capabilities.execute),
|
|
15
313
|
typecheck: payloadTypecheckEnabled(fence.typecheck, enabled?.typecheck, definition, options.endpoints)
|
|
16
314
|
},
|
|
17
315
|
config: {
|
|
18
316
|
...definition?.defaultConfig,
|
|
19
|
-
...enabled?.config
|
|
317
|
+
...enabled?.config,
|
|
318
|
+
...fence.config
|
|
20
319
|
},
|
|
21
|
-
viewers:
|
|
22
|
-
|
|
23
|
-
|
|
320
|
+
viewers: {
|
|
321
|
+
...options.viewers,
|
|
322
|
+
...fence.viewers
|
|
323
|
+
},
|
|
324
|
+
ui: fence.ui ?? options.ui,
|
|
325
|
+
timeoutMs: fence.timeoutMs ?? options.timeoutMs,
|
|
24
326
|
endpoints: options.endpoints
|
|
25
327
|
};
|
|
328
|
+
if (enabled?.endpoint) payload.endpoint = enabled.endpoint;
|
|
329
|
+
return payload;
|
|
26
330
|
}
|
|
27
331
|
/** TypeScript typecheck in the browser needs a reachable endpoint; hide the dead button otherwise. */
|
|
28
332
|
function payloadTypecheckEnabled(fenceTypecheck, enabledTypecheck, definition, endpoints) {
|
|
@@ -60,21 +364,29 @@ function wrapCommentedBlocks(html, _options) {
|
|
|
60
364
|
function upgradeCodePlayTags(html, options) {
|
|
61
365
|
return html.replace(CODEPLAY_TAG_PATTERN, (all, attrs, body) => {
|
|
62
366
|
if (/\bdata-ox-code-play=/.test(all)) return all;
|
|
63
|
-
const language = readAttr
|
|
367
|
+
const language = readAttr(attrs, "lang") ?? readAttr(attrs, "language") ?? "text";
|
|
64
368
|
const code = decodeHtml(body).replace(/^\n/, "").replace(/\n$/, "");
|
|
65
369
|
const definition = resolveLanguage(language);
|
|
66
370
|
const endpoints = options.endpoints ?? DEFAULT_ENDPOINTS;
|
|
371
|
+
const playOptions = parseCodePlayAttributes(attrs);
|
|
67
372
|
return wrapWidget(options.encodePayload({
|
|
68
373
|
language: definition?.id ?? language,
|
|
69
374
|
code,
|
|
375
|
+
title: playOptions.title,
|
|
70
376
|
capabilities: {
|
|
71
377
|
execute: true,
|
|
72
|
-
typecheck: payloadTypecheckEnabled(
|
|
378
|
+
typecheck: payloadTypecheckEnabled(playOptions.typecheck, void 0, definition, endpoints)
|
|
379
|
+
},
|
|
380
|
+
config: {
|
|
381
|
+
...definition?.defaultConfig,
|
|
382
|
+
...playOptions.config
|
|
383
|
+
},
|
|
384
|
+
viewers: {
|
|
385
|
+
...DEFAULT_VIEWERS,
|
|
386
|
+
...playOptions.viewers
|
|
73
387
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
ui: "default",
|
|
77
|
-
timeoutMs: 1e4,
|
|
388
|
+
ui: playOptions.ui ?? "default",
|
|
389
|
+
timeoutMs: playOptions.timeoutMs ?? 1e4,
|
|
78
390
|
endpoints
|
|
79
391
|
}), `<pre><code class="language-${escapeAttribute(language)}">${body}</code></pre>`);
|
|
80
392
|
});
|
|
@@ -85,7 +397,7 @@ function wrapMatchingFences(html, matches) {
|
|
|
85
397
|
used: false
|
|
86
398
|
}));
|
|
87
399
|
return html.replace(PRE_PATTERN, (all, attrs, body) => {
|
|
88
|
-
const language = (readAttr
|
|
400
|
+
const language = (readAttr(attrs, "class") ?? "").split(/\s+/).find((token) => token.startsWith("language-"))?.slice(9);
|
|
89
401
|
const code = normalizeCode(decodeHtml(stripTags(body)));
|
|
90
402
|
const match = unused.find((item) => !item.used && aliasesEqual(item.language, language) && normalizeCode(item.code) === code);
|
|
91
403
|
if (!match) return all;
|
|
@@ -94,7 +406,7 @@ function wrapMatchingFences(html, matches) {
|
|
|
94
406
|
});
|
|
95
407
|
}
|
|
96
408
|
function wrapWidget(payload, inner) {
|
|
97
|
-
return `<ox-code-play data-ox-code-play="${escapeAttribute(payload)}">${inner}</ox-code-play>`;
|
|
409
|
+
return `<ox-code-play data-ox-code-play="${escapeAttribute(payload)}" inert>${inner}</ox-code-play>`;
|
|
98
410
|
}
|
|
99
411
|
function readJsonString(source, start) {
|
|
100
412
|
try {
|
|
@@ -119,7 +431,7 @@ function sliceJsonString(source, start) {
|
|
|
119
431
|
}
|
|
120
432
|
throw new Error("Unterminated HTML JSON string.");
|
|
121
433
|
}
|
|
122
|
-
function readAttr
|
|
434
|
+
function readAttr(attrs, name) {
|
|
123
435
|
return new RegExp(`(?:^|\\s)${name}\\s*=\\s*"([^"]+)"`, "i").exec(attrs)?.[1];
|
|
124
436
|
}
|
|
125
437
|
function stripTags(value) {
|
|
@@ -133,122 +445,6 @@ function aliasesEqual(left, right) {
|
|
|
133
445
|
return left.toLowerCase() === right.toLowerCase();
|
|
134
446
|
}
|
|
135
447
|
//#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;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
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;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return fences;
|
|
182
|
-
}
|
|
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
|
-
}));
|
|
193
|
-
}
|
|
194
|
-
function stripPlayMeta(meta) {
|
|
195
|
-
return meta.split(/\s+/).filter((token) => token && token !== "play" && token !== "typecheck" && !token.startsWith("play-")).join(" ");
|
|
196
|
-
}
|
|
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;
|
|
232
|
-
}
|
|
233
|
-
function splitInfo(info) {
|
|
234
|
-
return info.trim().split(/\s+/).filter(Boolean);
|
|
235
|
-
}
|
|
236
|
-
function hasToken(meta, token) {
|
|
237
|
-
return meta.split(/\s+/).includes(token);
|
|
238
|
-
}
|
|
239
|
-
function escapeRegExp(value) {
|
|
240
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
241
|
-
}
|
|
242
|
-
function readAttr(attrs, name) {
|
|
243
|
-
return new RegExp(`(?:^|\\s)${name}\\s*=\\s*"([^"]+)"`, "i").exec(attrs)?.[1];
|
|
244
|
-
}
|
|
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");
|
|
250
|
-
}
|
|
251
|
-
//#endregion
|
|
252
448
|
//#region src/plugin-client.ts
|
|
253
449
|
const CLIENT_FILE_CANDIDATES = [
|
|
254
450
|
"browser.mjs",
|
|
@@ -273,6 +469,13 @@ function assertBrowserClientSource(source) {
|
|
|
273
469
|
return source;
|
|
274
470
|
}
|
|
275
471
|
//#endregion
|
|
472
|
+
//#region src/plugin-assets.ts
|
|
473
|
+
async function writeClientAsset(outDir, source) {
|
|
474
|
+
if (!source) return;
|
|
475
|
+
await mkdir(outDir, { recursive: true });
|
|
476
|
+
await writeFile(path.join(outDir, "ox-code-play.js"), source);
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
276
479
|
//#region src/plugin-proxy.ts
|
|
277
480
|
const PROXY_MAX_BODY_BYTES = 262144;
|
|
278
481
|
var ProxyRequestError = class extends Error {
|
|
@@ -356,10 +559,17 @@ const MARKDOWN_RE = /\.(?:md|markdown|mdx)(?:$|\?)/i;
|
|
|
356
559
|
function codePlay(options = {}) {
|
|
357
560
|
const resolved = resolveCodePlayOptions(options);
|
|
358
561
|
const explicitTypecheck = options.endpoints?.typecheck;
|
|
562
|
+
const proxyTargets = {
|
|
563
|
+
rust: resolved.endpoints.rust,
|
|
564
|
+
go: resolved.endpoints.go
|
|
565
|
+
};
|
|
359
566
|
let base = resolved.base;
|
|
360
567
|
let command = "serve";
|
|
361
568
|
let outDir = resolved.outDir;
|
|
362
569
|
let root = process.cwd();
|
|
570
|
+
let needsClientAsset = false;
|
|
571
|
+
let emittedClientAsset = false;
|
|
572
|
+
let clientSource;
|
|
363
573
|
const enhanceOptions = (matchFences) => ({
|
|
364
574
|
scriptSrc: `${base}ox-code-play.js`,
|
|
365
575
|
decodePayload,
|
|
@@ -374,10 +584,12 @@ function codePlay(options = {}) {
|
|
|
374
584
|
root = config.root;
|
|
375
585
|
base = resolved.base === "/" ? normalizeBase(config.base) : resolved.base;
|
|
376
586
|
outDir = resolved.outDir ?? config.build.outDir;
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
587
|
+
if (command === "serve" && resolved.proxy) {
|
|
588
|
+
resolved.endpoints.rust = options.endpoints?.rust ?? "/__ox-code-play/rust";
|
|
589
|
+
resolved.endpoints.go = options.endpoints?.go ?? "/__ox-code-play/go";
|
|
380
590
|
}
|
|
591
|
+
if (explicitTypecheck === void 0 && command === "serve" && resolved.proxy) resolved.endpoints.typecheck = DEV_TYPECHECK_PATH;
|
|
592
|
+
else if (explicitTypecheck === void 0) delete resolved.endpoints.typecheck;
|
|
381
593
|
},
|
|
382
594
|
resolveId(id) {
|
|
383
595
|
return id === VIRTUAL_ID ? RESOLVED_VIRTUAL : null;
|
|
@@ -388,7 +600,11 @@ function codePlay(options = {}) {
|
|
|
388
600
|
},
|
|
389
601
|
transform(code, id) {
|
|
390
602
|
if (!MARKDOWN_RE.test(id)) {
|
|
391
|
-
if (code.includes("export const html = ") && code.includes("ox-code-play:"))
|
|
603
|
+
if (code.includes("export const html = ") && code.includes("ox-code-play:")) {
|
|
604
|
+
const enhanced = enhanceGeneratedModule(code, enhanceOptions());
|
|
605
|
+
if (enhanced !== code) needsClientAsset = true;
|
|
606
|
+
return enhanced;
|
|
607
|
+
}
|
|
392
608
|
return null;
|
|
393
609
|
}
|
|
394
610
|
const rewritten = rewritePlayFences(code, (fence) => {
|
|
@@ -396,10 +612,12 @@ function codePlay(options = {}) {
|
|
|
396
612
|
if (!definition || !resolved.languages.has(definition.id)) return null;
|
|
397
613
|
return encodePayload(payloadFromFence(fence, resolved));
|
|
398
614
|
});
|
|
399
|
-
|
|
615
|
+
if (rewritten === code) return null;
|
|
616
|
+
needsClientAsset = true;
|
|
617
|
+
return rewritten;
|
|
400
618
|
},
|
|
401
619
|
configureServer(server) {
|
|
402
|
-
if (resolved.proxy) mountProxies(server,
|
|
620
|
+
if (resolved.proxy) mountProxies(server, proxyTargets.rust, proxyTargets.go);
|
|
403
621
|
server.middlewares.use(async (req, res, next) => {
|
|
404
622
|
const urlPath = req.url?.split("?")[0] ?? "";
|
|
405
623
|
if (urlPath === `${base}ox-code-play.js`.replace(/\/{2,}/g, "/")) {
|
|
@@ -414,22 +632,33 @@ function codePlay(options = {}) {
|
|
|
414
632
|
});
|
|
415
633
|
},
|
|
416
634
|
async generateBundle() {
|
|
417
|
-
|
|
418
|
-
|
|
635
|
+
if (command !== "build" || !needsClientAsset) return;
|
|
636
|
+
const source = await loadClientSource();
|
|
637
|
+
if (!source) return;
|
|
419
638
|
this.emitFile({
|
|
420
639
|
type: "asset",
|
|
421
640
|
fileName: "ox-code-play.js",
|
|
422
|
-
source
|
|
641
|
+
source
|
|
423
642
|
});
|
|
643
|
+
emittedClientAsset = true;
|
|
424
644
|
},
|
|
425
645
|
async closeBundle() {
|
|
426
646
|
if (command !== "build") return;
|
|
427
647
|
const srcDir = path.resolve(root, resolved.srcDir ?? "docs");
|
|
428
648
|
const destination = path.resolve(root, outDir ?? "dist");
|
|
429
649
|
if (!existsSync(srcDir) || !existsSync(destination)) return;
|
|
430
|
-
await enhanceWrittenPages(srcDir, destination, resolved, enhanceOptions);
|
|
650
|
+
if (!await enhanceWrittenPages(srcDir, destination, resolved, enhanceOptions)) return;
|
|
651
|
+
needsClientAsset = true;
|
|
652
|
+
if (!emittedClientAsset) await writeClientAsset(destination, await loadClientSource());
|
|
431
653
|
}
|
|
432
654
|
};
|
|
655
|
+
async function loadClientSource() {
|
|
656
|
+
if (clientSource) return clientSource;
|
|
657
|
+
const file = resolveClientFile();
|
|
658
|
+
if (!file) return;
|
|
659
|
+
clientSource = assertBrowserClientSource(await readFile(file, "utf8"));
|
|
660
|
+
return clientSource;
|
|
661
|
+
}
|
|
433
662
|
}
|
|
434
663
|
function interceptHtml(res, enhance) {
|
|
435
664
|
const originalEnd = res.end.bind(res);
|
|
@@ -481,6 +710,7 @@ function mountProxies(server, rustUrl, goUrl) {
|
|
|
481
710
|
});
|
|
482
711
|
}
|
|
483
712
|
async function enhanceWrittenPages(srcDir, outDir, resolved, enhance) {
|
|
713
|
+
let enhancedAny = false;
|
|
484
714
|
for (const file of walkFiles(srcDir)) {
|
|
485
715
|
if (!MARKDOWN_RE.test(file)) continue;
|
|
486
716
|
const source = await readFile(file, "utf8");
|
|
@@ -497,8 +727,12 @@ async function enhanceWrittenPages(srcDir, outDir, resolved, enhance) {
|
|
|
497
727
|
code: fence.code,
|
|
498
728
|
payload: encodePayload(payloadFromFence(fence, resolved))
|
|
499
729
|
}))));
|
|
500
|
-
if (enhanced !== html)
|
|
730
|
+
if (enhanced !== html) {
|
|
731
|
+
await writeFile(htmlPath, enhanced);
|
|
732
|
+
enhancedAny = true;
|
|
733
|
+
}
|
|
501
734
|
}
|
|
735
|
+
return enhancedAny;
|
|
502
736
|
}
|
|
503
737
|
function guessHtmlPath(file, srcDir, outDir) {
|
|
504
738
|
const relative = path.relative(srcDir, file).replace(/\.(?:md|markdown|mdx)$/i, "");
|
|
@@ -519,6 +753,6 @@ function normalizeBase(base) {
|
|
|
519
753
|
return base.endsWith("/") ? base : `${base}/`;
|
|
520
754
|
}
|
|
521
755
|
//#endregion
|
|
522
|
-
export {
|
|
756
|
+
export { parsePlayFences as a, parseCodePlayTags as i, enhanceGeneratedModule as n, rewritePlayFences as o, enhancePlayHtml as r, stripPlayMeta as s, codePlay as t };
|
|
523
757
|
|
|
524
758
|
//# sourceMappingURL=plugin2.mjs.map
|