@ox-content/vite-plugin 2.80.0 → 2.82.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/incremental-dom.cjs +3 -2
- package/dist/incremental-dom.cjs.map +1 -1
- package/dist/incremental-dom.d.cts.map +1 -1
- package/dist/incremental-dom.d.mts.map +1 -1
- package/dist/incremental-dom.mjs +2 -1
- package/dist/incremental-dom.mjs.map +1 -1
- package/dist/index.cjs +1486 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -13
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +8 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1429 -69
- package/dist/index.mjs.map +1 -1
- package/dist/interop.cjs.map +1 -1
- package/dist/interop.mjs.map +1 -1
- package/dist/tabs.cjs +3 -4
- package/dist/tabs.cjs.map +1 -1
- package/dist/tabs.mjs +99 -2
- package/dist/tabs.mjs.map +1 -0
- package/dist/vitepress-cli.cjs +5 -4
- package/dist/vitepress-cli.cjs.map +1 -1
- package/dist/vitepress-cli.d.cts +1 -1
- package/dist/vitepress-cli.d.mts +1 -1
- package/dist/vitepress-cli.mjs +4 -2
- package/dist/vitepress-cli.mjs.map +1 -1
- package/dist/vitepress.cjs +106 -2
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.mjs +32 -2
- package/dist/vitepress.mjs.map +1 -1
- package/package.json +19 -19
- package/dist/chunk.cjs +0 -57
- package/dist/github.cjs +0 -690
- package/dist/github.cjs.map +0 -1
- package/dist/github.mjs +0 -625
- package/dist/github.mjs.map +0 -1
- package/dist/github2.mjs +0 -2
- package/dist/media.cjs +0 -310
- package/dist/media.cjs.map +0 -1
- package/dist/media.mjs +0 -302
- package/dist/media.mjs.map +0 -1
- package/dist/media2.mjs +0 -2
- package/dist/mermaid.cjs +0 -114
- package/dist/mermaid.cjs.map +0 -1
- package/dist/mermaid.mjs +0 -2
- package/dist/mermaid2.mjs +0 -92
- package/dist/mermaid2.mjs.map +0 -1
- package/dist/napi.cjs +0 -45
- package/dist/napi.cjs.map +0 -1
- package/dist/napi.mjs +0 -34
- package/dist/napi.mjs.map +0 -1
- package/dist/ogp.cjs +0 -341
- package/dist/ogp.cjs.map +0 -1
- package/dist/ogp.mjs +0 -2
- package/dist/ogp2.mjs +0 -302
- package/dist/ogp2.mjs.map +0 -1
- package/dist/pm.cjs +0 -45
- package/dist/pm.cjs.map +0 -1
- package/dist/pm.mjs +0 -2
- package/dist/pm2.mjs +0 -38
- package/dist/pm2.mjs.map +0 -1
- package/dist/tabs2.mjs +0 -56
- package/dist/tabs2.mjs.map +0 -1
- package/dist/youtube.cjs +0 -57
- package/dist/youtube.cjs.map +0 -1
- package/dist/youtube.mjs +0 -2
- package/dist/youtube2.mjs +0 -35
- package/dist/youtube2.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
|
+
import { c as __exportAll, i as setTabGroupCounter, l as __require, n as getTabGroupCounter, o as transformTabs, r as resetTabGroupCounter, s as __esmMin, t as generateTabsCSS, u as __toCommonJS } from "./tabs.mjs";
|
|
1
2
|
import { t as interopDefault } from "./interop.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as transformMermaidStatic, t as mermaidClientScript } from "./mermaid2.mjs";
|
|
4
|
-
import { a as transformTabs, r as resetTabGroupCounter, t as generateTabsCSS } from "./tabs2.mjs";
|
|
5
|
-
import "./pm2.mjs";
|
|
6
|
-
import { n as transformYouTube, t as extractVideoId } from "./youtube2.mjs";
|
|
7
|
-
import "./media.mjs";
|
|
8
|
-
import { a as fetchRepoData, i as fetchGitHubSource, l as parseGitHubLineRange, n as collectGitHubRepos, o as prefetchGitHubRepos, r as collectGitHubSources, s as prefetchGitHubSources, t as transformGitHub, u as parseGitHubPermalink } from "./github.mjs";
|
|
9
|
-
import { a as transformOgp, i as prefetchOgpData, n as fetchOgpData, t as collectOgpUrls } from "./ogp2.mjs";
|
|
10
|
-
import { a as normalizeVitePressFrontmatter, c as mergeThemes, i as generateVitePressMigrationConfig, l as resolveTheme, n as convertVitePressSidebar, o as defaultTheme, r as fromVitePressConfig, s as defineTheme, t as convertVitePressNav, u as themeToNapi } from "./vitepress.mjs";
|
|
3
|
+
import { a as normalizeVitePressFrontmatter, c as mergeThemes, d as importNapiModule, f as importNapiModuleSync, i as generateVitePressMigrationConfig, l as resolveTheme, n as convertVitePressSidebar, o as defaultTheme, r as fromVitePressConfig, s as defineTheme, t as convertVitePressNav, u as themeToNapi } from "./vitepress.mjs";
|
|
11
4
|
import { createRequire } from "node:module";
|
|
12
5
|
import * as path$2 from "path";
|
|
13
6
|
import { unified } from "unified";
|
|
14
7
|
import rehypeParsePlugin from "rehype-parse";
|
|
15
8
|
import rehypeStringifyPlugin from "rehype-stringify";
|
|
16
9
|
import { createHighlighter } from "shiki";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
17
11
|
import * as path$1 from "node:path";
|
|
18
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import path, { dirname, join } from "node:path";
|
|
19
13
|
import * as fs from "node:fs/promises";
|
|
20
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
14
|
+
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
21
16
|
import { tmpdir } from "node:os";
|
|
22
17
|
import { promisify } from "node:util";
|
|
23
18
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -25,34 +20,6 @@ import * as fs$2 from "fs/promises";
|
|
|
25
20
|
import * as crypto from "crypto";
|
|
26
21
|
import * as fs$1 from "fs";
|
|
27
22
|
import { glob } from "glob";
|
|
28
|
-
//#region \0rolldown/runtime.js
|
|
29
|
-
var __defProp = Object.defineProperty;
|
|
30
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
31
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
32
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
33
|
-
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
34
|
-
var __exportAll = (all, no_symbols) => {
|
|
35
|
-
let target = {};
|
|
36
|
-
for (var name in all) __defProp(target, name, {
|
|
37
|
-
get: all[name],
|
|
38
|
-
enumerable: true
|
|
39
|
-
});
|
|
40
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
41
|
-
return target;
|
|
42
|
-
};
|
|
43
|
-
var __copyProps = (to, from, except, desc) => {
|
|
44
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
45
|
-
key = keys[i];
|
|
46
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
47
|
-
get: ((k) => from[k]).bind(null, key),
|
|
48
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return to;
|
|
52
|
-
};
|
|
53
|
-
var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
54
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
55
|
-
//#endregion
|
|
56
23
|
//#region src/markdown.ts
|
|
57
24
|
const DEFAULT_MARKDOWN_EXTENSIONS = [
|
|
58
25
|
".md",
|
|
@@ -138,8 +105,8 @@ function createMarkdownEnvironment(options) {
|
|
|
138
105
|
/**
|
|
139
106
|
* Syntax highlighting with Shiki via rehype.
|
|
140
107
|
*/
|
|
141
|
-
const rehypeParse$
|
|
142
|
-
const rehypeStringify$
|
|
108
|
+
const rehypeParse$3 = interopDefault(rehypeParsePlugin);
|
|
109
|
+
const rehypeStringify$3 = interopDefault(rehypeStringifyPlugin);
|
|
143
110
|
const BUILTIN_LANGS = [
|
|
144
111
|
"javascript",
|
|
145
112
|
"typescript",
|
|
@@ -218,7 +185,7 @@ function rehypeShikiHighlight(options) {
|
|
|
218
185
|
lang,
|
|
219
186
|
theme: themeName
|
|
220
187
|
});
|
|
221
|
-
const parsed = unified().use(rehypeParse$
|
|
188
|
+
const parsed = unified().use(rehypeParse$3, { fragment: true }).parse(highlighted);
|
|
222
189
|
if (parsed.children[0]?.type === "element") {
|
|
223
190
|
const highlightedPre = parsed.children[0];
|
|
224
191
|
highlightedPre.properties ??= {};
|
|
@@ -240,13 +207,13 @@ function rehypeShikiHighlight(options) {
|
|
|
240
207
|
lang,
|
|
241
208
|
theme: themeName
|
|
242
209
|
});
|
|
243
|
-
const parsed = unified().use(rehypeParse$
|
|
210
|
+
const parsed = unified().use(rehypeParse$3, { fragment: true }).parse(highlighted);
|
|
244
211
|
if (parsed.children[0]?.type === "element") {
|
|
245
212
|
const highlightedCode = parsed.children[0].children.find((child) => child.type === "element" && child.tagName === "code");
|
|
246
213
|
if (highlightedCode) {
|
|
247
214
|
highlightedCode.properties ??= {};
|
|
248
215
|
const highlightedClasses = normalizeClassName(highlightedCode.properties.className);
|
|
249
|
-
highlightedCode.properties.className = [
|
|
216
|
+
highlightedCode.properties.className = [.../* @__PURE__ */ new Set([
|
|
250
217
|
...originalCodeClasses,
|
|
251
218
|
...highlightedClasses,
|
|
252
219
|
"shiki-inline"
|
|
@@ -296,42 +263,1433 @@ function normalizeClassName(className) {
|
|
|
296
263
|
* Apply syntax highlighting to HTML using Shiki.
|
|
297
264
|
*/
|
|
298
265
|
async function highlightCode(html, theme = "github-dark", langs = []) {
|
|
299
|
-
const result = await unified().use(rehypeParse$
|
|
266
|
+
const result = await unified().use(rehypeParse$3, { fragment: true }).use(rehypeShikiHighlight, {
|
|
300
267
|
theme,
|
|
301
268
|
langs
|
|
302
|
-
}).use(rehypeStringify$
|
|
269
|
+
}).use(rehypeStringify$3).process(html);
|
|
270
|
+
return String(result);
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/plugins/mermaid.ts
|
|
274
|
+
/**
|
|
275
|
+
* Mermaid Plugin - Native Rust renderer via NAPI
|
|
276
|
+
*
|
|
277
|
+
* Renders mermaid code blocks to SVG using the native Rust renderer
|
|
278
|
+
* via NAPI. Delegates to the NAPI `transformMermaid` function which
|
|
279
|
+
* extracts mermaid code blocks from HTML and renders them using mmdc.
|
|
280
|
+
*/
|
|
281
|
+
var mermaid_exports = /* @__PURE__ */ __exportAll({
|
|
282
|
+
mermaidClientScript: () => "",
|
|
283
|
+
transformMermaidStatic: () => transformMermaidStatic
|
|
284
|
+
});
|
|
285
|
+
/** Cached NAPI bindings */
|
|
286
|
+
let napiBindings$1 = null;
|
|
287
|
+
let napiLoadAttempted$1 = false;
|
|
288
|
+
async function loadNapi() {
|
|
289
|
+
if (napiLoadAttempted$1) return napiBindings$1;
|
|
290
|
+
napiLoadAttempted$1 = true;
|
|
291
|
+
try {
|
|
292
|
+
const binding = await importNapiModule();
|
|
293
|
+
if (typeof binding.transformMermaid !== "function") {
|
|
294
|
+
napiBindings$1 = null;
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
napiBindings$1 = binding;
|
|
298
|
+
return binding;
|
|
299
|
+
} catch {
|
|
300
|
+
napiBindings$1 = null;
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let cachedMmdcPath;
|
|
305
|
+
let missingMmdcWarned = false;
|
|
306
|
+
function resolveMmdcPath() {
|
|
307
|
+
if (cachedMmdcPath !== void 0) return cachedMmdcPath;
|
|
308
|
+
for (const resolver of createMmdcResolvers()) try {
|
|
309
|
+
const entry = resolver.resolve("@mermaid-js/mermaid-cli");
|
|
310
|
+
const cliPath = join(dirname(entry), "cli.js");
|
|
311
|
+
if (existsSync(cliPath)) {
|
|
312
|
+
cachedMmdcPath = cliPath;
|
|
313
|
+
return cachedMmdcPath;
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
const binPath = join(process.cwd(), "node_modules", ".bin", "mmdc");
|
|
317
|
+
if (existsSync(binPath)) {
|
|
318
|
+
cachedMmdcPath = binPath;
|
|
319
|
+
return cachedMmdcPath;
|
|
320
|
+
}
|
|
321
|
+
cachedMmdcPath = null;
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function createMmdcResolvers() {
|
|
325
|
+
const consumerRequire = createRequire(join(process.cwd(), "noop.js"));
|
|
326
|
+
const resolvers = [consumerRequire];
|
|
327
|
+
try {
|
|
328
|
+
resolvers.push(createRequire(consumerRequire.resolve("@ox-content/vite-plugin")));
|
|
329
|
+
} catch {}
|
|
330
|
+
return resolvers;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Transforms mermaid code blocks in HTML to rendered SVG diagrams.
|
|
334
|
+
* Uses the native Rust NAPI transformMermaid function.
|
|
335
|
+
*/
|
|
336
|
+
async function transformMermaidStatic(html, _options) {
|
|
337
|
+
const napi = await loadNapi();
|
|
338
|
+
if (!napi) return html;
|
|
339
|
+
const mmdcPath = resolveMmdcPath();
|
|
340
|
+
if (!mmdcPath) {
|
|
341
|
+
warnMissingMmdcOnce();
|
|
342
|
+
return html;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const result = napi.transformMermaid(html, mmdcPath);
|
|
346
|
+
for (const error of result.errors) console.warn("[ox-content] Mermaid render error:", error);
|
|
347
|
+
return result.html;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
console.warn("[ox-content] Mermaid transform error:", err);
|
|
350
|
+
return html;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function warnMissingMmdcOnce() {
|
|
354
|
+
if (missingMmdcWarned) return;
|
|
355
|
+
missingMmdcWarned = true;
|
|
356
|
+
console.warn("[ox-content] mmdc not found; skipping Mermaid rendering.");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* @deprecated No longer used. Mermaid rendering is now done at build time via NAPI.
|
|
360
|
+
*/
|
|
361
|
+
const mermaidClientScript = "";
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/plugins/pm.ts
|
|
364
|
+
/**
|
|
365
|
+
* Package Manager Tabs Plugin
|
|
366
|
+
*
|
|
367
|
+
* Transforms <pm>npm install …</pm> blocks into a tab group with one tab per
|
|
368
|
+
* package manager (npm/pnpm/yarn/bun). The single npm-style command is converted
|
|
369
|
+
* to each package manager's equivalent natively in Rust (`transformPmEmbeds` in
|
|
370
|
+
* @ox-content/napi), and the result reuses the same `ox-tabs` widget markup as
|
|
371
|
+
* the generic `<tabs>` plugin so styling and keyboard navigation are consistent.
|
|
372
|
+
*
|
|
373
|
+
* Syncing is opt-in (off by default): when enabled, the rendered group carries a
|
|
374
|
+
* `data-ox-tab-group="pkg-manager"` attribute so the client runtime can keep
|
|
375
|
+
* every package-manager group on the page in sync via localStorage.
|
|
376
|
+
*
|
|
377
|
+
* Package-manager groups share the tab-group counter with the `<tabs>` plugin so
|
|
378
|
+
* `data-group` ids (and the CSS produced by `generateTabsCSS`) stay unique.
|
|
379
|
+
*/
|
|
380
|
+
var pm_exports = /* @__PURE__ */ __exportAll({ transformPm: () => transformPm });
|
|
381
|
+
/**
|
|
382
|
+
* Transform `<pm>` package-manager blocks in HTML into install tabs.
|
|
383
|
+
*
|
|
384
|
+
* @param html - Rendered HTML potentially containing `<pm>` blocks.
|
|
385
|
+
* @param options - Package-manager tab options (syncing is opt-in).
|
|
386
|
+
* @returns The rewritten HTML.
|
|
387
|
+
*/
|
|
388
|
+
async function transformPm(html, options) {
|
|
389
|
+
if (!/<pm[\s/>]/i.test(html)) return html;
|
|
390
|
+
const mod = await importNapiModule();
|
|
391
|
+
const startGroup = getTabGroupCounter();
|
|
392
|
+
const result = mod.transformPmEmbeds(html, startGroup, { sync: options?.sync ?? false });
|
|
393
|
+
setTabGroupCounter(startGroup + result.groupCount);
|
|
394
|
+
return result.html;
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/plugins/youtube.ts
|
|
398
|
+
/**
|
|
399
|
+
* YouTube Plugin - Privacy-enhanced iframe embedding
|
|
400
|
+
*
|
|
401
|
+
* Transforms <YouTube> components into responsive iframe embeds using
|
|
402
|
+
* youtube-nocookie.com for enhanced privacy.
|
|
403
|
+
*
|
|
404
|
+
* The HTML rewrite is performed in Rust (`transformYoutubeEmbeds` in
|
|
405
|
+
* @ox-content/napi), replacing the previous rehype parse/stringify
|
|
406
|
+
* round-trip. This module keeps the public TS surface and a cheap marker
|
|
407
|
+
* check so pages without a `<youtube>` element never cross the NAPI boundary.
|
|
408
|
+
*/
|
|
409
|
+
var youtube_exports = /* @__PURE__ */ __exportAll({
|
|
410
|
+
extractVideoId: () => extractVideoId,
|
|
411
|
+
transformYouTube: () => transformYouTube
|
|
412
|
+
});
|
|
413
|
+
/**
|
|
414
|
+
* Extract YouTube video ID from various URL formats.
|
|
415
|
+
*/
|
|
416
|
+
function extractVideoId(input) {
|
|
417
|
+
if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;
|
|
418
|
+
for (const pattern of [/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/)([a-zA-Z0-9_-]{11})/, /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/]) {
|
|
419
|
+
const match = input.match(pattern);
|
|
420
|
+
if (match) return match[1];
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Transform YouTube components in HTML.
|
|
426
|
+
*/
|
|
427
|
+
async function transformYouTube(html, options) {
|
|
428
|
+
if (!/<youtube/i.test(html)) return html;
|
|
429
|
+
return (await importNapiModule()).transformYoutubeEmbeds(html, options);
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/plugins/twitter/url.ts
|
|
433
|
+
const STATUS_PATH = /^\/(?:[^/]+|i\/web)\/status\/(\d+)(?:\/.*)?$/;
|
|
434
|
+
function createSyndicationToken(id) {
|
|
435
|
+
return (Number(id) / 0x38d7ea4c68000 * Math.PI).toString(36).replaceAll(/(0+|\.)/g, "");
|
|
436
|
+
}
|
|
437
|
+
function parseTweetReference(value) {
|
|
438
|
+
const trimmed = value.trim();
|
|
439
|
+
if (/^\d+$/.test(trimmed)) return {
|
|
440
|
+
id: trimmed,
|
|
441
|
+
url: `https://x.com/i/web/status/${trimmed}`
|
|
442
|
+
};
|
|
443
|
+
try {
|
|
444
|
+
const url = new URL(trimmed);
|
|
445
|
+
const hostname = url.hostname.toLowerCase().replace(/^(?:www\.|mobile\.)/, "");
|
|
446
|
+
if (url.protocol !== "https:" || hostname !== "x.com" && hostname !== "twitter.com") return null;
|
|
447
|
+
const match = url.pathname.match(STATUS_PATH);
|
|
448
|
+
if (!match) return null;
|
|
449
|
+
const screenName = url.pathname.startsWith("/i/web/status/") ? "i/web" : url.pathname.split("/")[1];
|
|
450
|
+
return {
|
|
451
|
+
id: match[1],
|
|
452
|
+
url: `https://x.com/${screenName}/status/${match[1]}`
|
|
453
|
+
};
|
|
454
|
+
} catch {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function referenceFromAttributes(attributes) {
|
|
459
|
+
const values = /* @__PURE__ */ new Map();
|
|
460
|
+
for (const match of attributes.matchAll(/\b(url|href|id)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) values.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? "");
|
|
461
|
+
return parseTweetReference(values.get("url") ?? values.get("href") ?? values.get("id") ?? "");
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/plugins/twitter/fetch.ts
|
|
465
|
+
const tweetCache = /* @__PURE__ */ new Map();
|
|
466
|
+
async function fetchTweetData(id, options) {
|
|
467
|
+
const key = `${id}-${sanitizeSegment(options.lang)}`;
|
|
468
|
+
if (options.cache) {
|
|
469
|
+
const memory = tweetCache.get(key);
|
|
470
|
+
if (memory) return memory;
|
|
471
|
+
const disk = await readCachedTweet(key, options.cacheDir);
|
|
472
|
+
if (disk) {
|
|
473
|
+
tweetCache.set(key, disk);
|
|
474
|
+
return disk;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const controller = new AbortController();
|
|
478
|
+
const timeout = setTimeout(() => controller.abort(), options.timeout);
|
|
479
|
+
const endpoint = new URL("https://cdn.syndication.twimg.com/tweet-result");
|
|
480
|
+
endpoint.searchParams.set("id", id);
|
|
481
|
+
endpoint.searchParams.set("lang", options.lang);
|
|
482
|
+
endpoint.searchParams.set("token", createSyndicationToken(id));
|
|
483
|
+
try {
|
|
484
|
+
const response = await fetch(endpoint, {
|
|
485
|
+
headers: { Accept: "application/json" },
|
|
486
|
+
signal: controller.signal
|
|
487
|
+
});
|
|
488
|
+
if (!response.ok) return null;
|
|
489
|
+
const data = await response.json();
|
|
490
|
+
if (!isTweetData(data)) return null;
|
|
491
|
+
if (options.cache) {
|
|
492
|
+
tweetCache.set(key, data);
|
|
493
|
+
await writeCachedTweet(key, data, options.cacheDir);
|
|
494
|
+
}
|
|
495
|
+
return data;
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
} finally {
|
|
499
|
+
clearTimeout(timeout);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function materializeTweetAssets(id, data, options) {
|
|
503
|
+
const assets = { media: [] };
|
|
504
|
+
const avatarUrl = data.user.profile_image_url_https?.replace(/_normal(?=\.[^.]+$)/, "_bigger");
|
|
505
|
+
if (avatarUrl) assets.avatar = await downloadAsset(avatarUrl, `${id}-avatar`, options);
|
|
506
|
+
const media = data.mediaDetails ?? data.entities?.media ?? [];
|
|
507
|
+
for (const [index, item] of media.entries()) {
|
|
508
|
+
if (item.type && item.type !== "photo") continue;
|
|
509
|
+
if (!item.media_url_https) continue;
|
|
510
|
+
const src = await downloadAsset(item.media_url_https, `${id}-media-${index + 1}`, options);
|
|
511
|
+
if (src) assets.media.push(assetRecord(src, item));
|
|
512
|
+
}
|
|
513
|
+
return assets;
|
|
514
|
+
}
|
|
515
|
+
async function downloadAsset(source, basename, options) {
|
|
516
|
+
let url;
|
|
517
|
+
try {
|
|
518
|
+
url = new URL(source);
|
|
519
|
+
} catch {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "pbs.twimg.com") return;
|
|
523
|
+
const filename = `${basename}${extensionFromUrl(url)}`;
|
|
524
|
+
const output = path.join(options.mediaOutputDir, filename);
|
|
525
|
+
try {
|
|
526
|
+
await access(output);
|
|
527
|
+
return joinPublicPath(options.mediaPublicPath, filename);
|
|
528
|
+
} catch {}
|
|
529
|
+
try {
|
|
530
|
+
const response = await fetch(url, { headers: { Accept: "image/*" } });
|
|
531
|
+
if (!response.ok) return void 0;
|
|
532
|
+
await mkdir(options.mediaOutputDir, { recursive: true });
|
|
533
|
+
await writeFile(output, new Uint8Array(await response.arrayBuffer()));
|
|
534
|
+
return joinPublicPath(options.mediaPublicPath, filename);
|
|
535
|
+
} catch {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async function readCachedTweet(key, directory) {
|
|
540
|
+
try {
|
|
541
|
+
const data = JSON.parse(await readFile(path.join(directory, `${key}.json`), "utf8"));
|
|
542
|
+
return isTweetData(data) ? data : null;
|
|
543
|
+
} catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function writeCachedTweet(key, data, directory) {
|
|
548
|
+
try {
|
|
549
|
+
await mkdir(directory, { recursive: true });
|
|
550
|
+
await writeFile(path.join(directory, `${key}.json`), `${JSON.stringify(data)}\n`);
|
|
551
|
+
} catch {}
|
|
552
|
+
}
|
|
553
|
+
function isTweetData(data) {
|
|
554
|
+
if (!data || typeof data !== "object") return false;
|
|
555
|
+
const value = data;
|
|
556
|
+
return typeof value.text === "string" && Boolean(value.user) && typeof value.user?.name === "string" && typeof value.user.screen_name === "string";
|
|
557
|
+
}
|
|
558
|
+
function extensionFromUrl(url) {
|
|
559
|
+
const match = url.pathname.match(/\.(jpe?g|png|webp|gif)$/i);
|
|
560
|
+
return match ? `.${match[1].toLowerCase().replace("jpeg", "jpg")}` : ".jpg";
|
|
561
|
+
}
|
|
562
|
+
function joinPublicPath(prefix, filename) {
|
|
563
|
+
return `${prefix.replace(/\/$/, "")}/${filename}`;
|
|
564
|
+
}
|
|
565
|
+
function sanitizeSegment(value) {
|
|
566
|
+
return value.replaceAll(/[^a-zA-Z0-9_-]/g, "-");
|
|
567
|
+
}
|
|
568
|
+
function assetRecord(src, media) {
|
|
569
|
+
return {
|
|
570
|
+
src,
|
|
571
|
+
alt: media.ext_alt_text,
|
|
572
|
+
width: media.original_info?.width,
|
|
573
|
+
height: media.original_info?.height
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/plugins/twitter/render.ts
|
|
578
|
+
function renderFetchedTweet(permalink, data, assets, options) {
|
|
579
|
+
const profile = `https://x.com/${encodeURIComponent(data.user.screen_name)}`;
|
|
580
|
+
const author = escapeHtml$3(data.user.name);
|
|
581
|
+
const handle = escapeHtml$3(data.user.screen_name);
|
|
582
|
+
const avatar = assets.avatar ? `<img class="ox-tweet__avatar" src="${escapeAttribute(assets.avatar)}" alt="" width="48" height="48" loading="lazy" decoding="async">` : "";
|
|
583
|
+
const media = renderMedia(assets);
|
|
584
|
+
const footer = renderFooter(permalink, data.created_at, options.lang);
|
|
585
|
+
return [
|
|
586
|
+
"<figure class=\"ox-tweet ox-tweet--fetched\">",
|
|
587
|
+
"<header class=\"ox-tweet__header\">",
|
|
588
|
+
`<a class="ox-tweet__profile" href="${escapeAttribute(profile)}" target="_blank" rel="noopener noreferrer">`,
|
|
589
|
+
avatar,
|
|
590
|
+
`<span class="ox-tweet__author-name">${author}</span>`,
|
|
591
|
+
`<span class="ox-tweet__author-handle">@${handle}</span>`,
|
|
592
|
+
"</a></header>",
|
|
593
|
+
`<div class="ox-tweet__body">${renderTweetText(data)}</div>`,
|
|
594
|
+
media,
|
|
595
|
+
footer,
|
|
596
|
+
"</figure>"
|
|
597
|
+
].join("");
|
|
598
|
+
}
|
|
599
|
+
function renderTweetText(data) {
|
|
600
|
+
const [start, end] = data.display_text_range ?? [0, data.text.length];
|
|
601
|
+
const entities = collectEntities(data).filter((entity) => validRange(entity.indices, start, end)).sort((left, right) => left.indices[0] - right.indices[0]);
|
|
602
|
+
let cursor = start;
|
|
603
|
+
let output = "";
|
|
604
|
+
for (const entity of entities) {
|
|
605
|
+
const [entityStart, entityEnd] = entity.indices;
|
|
606
|
+
if (entityStart < cursor) continue;
|
|
607
|
+
output += escapeText(data.text.slice(cursor, entityStart));
|
|
608
|
+
if (entity.kind === "url") {
|
|
609
|
+
const href = entity.expanded_url ?? entity.url;
|
|
610
|
+
const label = entity.display_url ?? href;
|
|
611
|
+
output += `<a href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$3(label)}</a>`;
|
|
612
|
+
}
|
|
613
|
+
cursor = entityEnd;
|
|
614
|
+
}
|
|
615
|
+
output += escapeText(data.text.slice(cursor, end));
|
|
616
|
+
return output.trim();
|
|
617
|
+
}
|
|
618
|
+
function collectEntities(data) {
|
|
619
|
+
return [...(data.entities?.urls ?? []).map((entity) => ({
|
|
620
|
+
...entity,
|
|
621
|
+
kind: "url"
|
|
622
|
+
})), ...(data.entities?.media ?? []).map((entity) => ({
|
|
623
|
+
...entity,
|
|
624
|
+
kind: "media"
|
|
625
|
+
}))];
|
|
626
|
+
}
|
|
627
|
+
function validRange(indices, start, end) {
|
|
628
|
+
return Boolean(indices && indices[0] >= start && indices[1] <= end && indices[0] < indices[1]);
|
|
629
|
+
}
|
|
630
|
+
function renderMedia(assets) {
|
|
631
|
+
if (assets.media.length === 0) return "";
|
|
632
|
+
const images = assets.media.map((item) => {
|
|
633
|
+
const size = [item.width ? ` width="${item.width}"` : "", item.height ? ` height="${item.height}"` : ""].join("");
|
|
634
|
+
return `<img class="ox-tweet__media-item" src="${escapeAttribute(item.src)}" alt="${escapeAttribute(item.alt ?? "")}"${size} loading="lazy" decoding="async">`;
|
|
635
|
+
}).join("");
|
|
636
|
+
return `<div class="ox-tweet__media" data-count="${assets.media.length}">${images}</div>`;
|
|
637
|
+
}
|
|
638
|
+
function renderFooter(permalink, createdAt, lang) {
|
|
639
|
+
if (!createdAt) return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute(permalink)}" target="_blank" rel="noopener noreferrer">View on X</a></footer>`;
|
|
640
|
+
const date = new Date(createdAt);
|
|
641
|
+
if (Number.isNaN(date.valueOf())) return renderFooter(permalink, void 0, lang);
|
|
642
|
+
const iso = date.toISOString();
|
|
643
|
+
let label;
|
|
644
|
+
try {
|
|
645
|
+
label = new Intl.DateTimeFormat(lang, {
|
|
646
|
+
dateStyle: "medium",
|
|
647
|
+
timeZone: "UTC"
|
|
648
|
+
}).format(date);
|
|
649
|
+
} catch {
|
|
650
|
+
label = new Intl.DateTimeFormat("en", {
|
|
651
|
+
dateStyle: "medium",
|
|
652
|
+
timeZone: "UTC"
|
|
653
|
+
}).format(date);
|
|
654
|
+
}
|
|
655
|
+
return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute(permalink)}" target="_blank" rel="noopener noreferrer"><time datetime="${iso}">${escapeHtml$3(label)}</time></a></footer>`;
|
|
656
|
+
}
|
|
657
|
+
function escapeText(value) {
|
|
658
|
+
return escapeHtml$3(value).replaceAll("\n", "<br>");
|
|
659
|
+
}
|
|
660
|
+
function escapeAttribute(value) {
|
|
661
|
+
return escapeHtml$3(value).replaceAll("`", "`");
|
|
662
|
+
}
|
|
663
|
+
function escapeHtml$3(value) {
|
|
664
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/plugins/twitter/transform.ts
|
|
668
|
+
const TWEET_ELEMENT = /<(tweet|xpost)\b([^>]*?)(?:\/\s*>|>[\s\S]*?<\/\1\s*>)/gi;
|
|
669
|
+
function resolveTwitterEmbedOptions$1(options) {
|
|
670
|
+
return {
|
|
671
|
+
fetch: options.fetch ?? false,
|
|
672
|
+
lang: options.lang ?? "en",
|
|
673
|
+
timeout: options.timeout ?? 1e4,
|
|
674
|
+
cache: options.cache ?? true,
|
|
675
|
+
cacheDir: path.resolve(options.cacheDir ?? ".cache/ox-content/twitter"),
|
|
676
|
+
mediaOutputDir: path.resolve(options.mediaOutputDir ?? "public/ox-content/twitter"),
|
|
677
|
+
mediaPublicPath: options.mediaPublicPath ?? "/ox-content/twitter"
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async function transformFetchedTweets(html, options) {
|
|
681
|
+
const resolved = resolveTwitterEmbedOptions$1(options);
|
|
682
|
+
if (!resolved.fetch) return html;
|
|
683
|
+
let output = "";
|
|
684
|
+
let cursor = 0;
|
|
685
|
+
for (const match of html.matchAll(TWEET_ELEMENT)) {
|
|
686
|
+
const index = match.index ?? 0;
|
|
687
|
+
output += html.slice(cursor, index);
|
|
688
|
+
const reference = referenceFromAttributes(match[2]);
|
|
689
|
+
if (!reference) {
|
|
690
|
+
output += match[0];
|
|
691
|
+
cursor = index + match[0].length;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const data = await fetchTweetData(reference.id, resolved);
|
|
695
|
+
if (!data) {
|
|
696
|
+
output += match[0];
|
|
697
|
+
cursor = index + match[0].length;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const assets = await materializeTweetAssets(reference.id, data, resolved);
|
|
701
|
+
output += renderFetchedTweet(reference.url, data, assets, resolved);
|
|
702
|
+
cursor = index + match[0].length;
|
|
703
|
+
}
|
|
704
|
+
return output + html.slice(cursor);
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/plugins/media.ts
|
|
708
|
+
var media_exports = /* @__PURE__ */ __exportAll({ transformMediaEmbeds: () => transformMediaEmbeds });
|
|
709
|
+
async function transformMediaEmbeds(html, options) {
|
|
710
|
+
if (!hasEnabledMediaEmbed(options) || !hasMediaMarker(html)) return html;
|
|
711
|
+
let result = html;
|
|
712
|
+
if (typeof options.twitter === "object") result = await transformFetchedTweets(result, options.twitter);
|
|
713
|
+
if (!hasMediaMarker(result)) return result;
|
|
714
|
+
return (await importNapiModule()).transformMediaEmbeds(result, {
|
|
715
|
+
spotify: options.spotify,
|
|
716
|
+
stackBlitz: options.stackBlitz,
|
|
717
|
+
twitter: Boolean(options.twitter),
|
|
718
|
+
bluesky: options.bluesky,
|
|
719
|
+
webContainer: options.webContainer
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
function hasEnabledMediaEmbed(options) {
|
|
723
|
+
return Boolean(options.spotify || options.stackBlitz || options.twitter || options.bluesky || options.webContainer);
|
|
724
|
+
}
|
|
725
|
+
function hasMediaMarker(html) {
|
|
726
|
+
return /<(spotify|stackblitz|tweet|xpost|bluesky|webcontainer)[\s/>]/i.test(html);
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/plugins/github/validation.ts
|
|
730
|
+
const GITHUB_REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
731
|
+
function hasControlChar(value) {
|
|
732
|
+
for (let index = 0; index < value.length; index++) {
|
|
733
|
+
const code = value.charCodeAt(index);
|
|
734
|
+
if (code <= 31 || code === 127) return true;
|
|
735
|
+
}
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
function isSafeGitHubRepo(repo) {
|
|
739
|
+
return GITHUB_REPO_RE.test(repo) && !repo.split("/").some((part) => part === "." || part === "..");
|
|
740
|
+
}
|
|
741
|
+
function isSafeGitHubRef(ref) {
|
|
742
|
+
return Boolean(ref) && !hasControlChar(ref) && !hasUnsafePathSegment(ref);
|
|
743
|
+
}
|
|
744
|
+
function isSafeGitHubPath(path) {
|
|
745
|
+
return Boolean(path) && !hasControlChar(path) && !hasUnsafePathSegment(path);
|
|
746
|
+
}
|
|
747
|
+
function hasUnsafePathSegment(value) {
|
|
748
|
+
return value.split("/").some((part) => !part || part === "." || part === ".." || part.includes("\\"));
|
|
749
|
+
}
|
|
750
|
+
function encodePath(path) {
|
|
751
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
752
|
+
}
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/plugins/github/source.ts
|
|
755
|
+
const EXTENSION_LANGUAGE_MAP = /* @__PURE__ */ new Map([
|
|
756
|
+
["cjs", "javascript"],
|
|
757
|
+
["css", "css"],
|
|
758
|
+
["go", "go"],
|
|
759
|
+
["html", "html"],
|
|
760
|
+
["js", "javascript"],
|
|
761
|
+
["json", "json"],
|
|
762
|
+
["jsx", "jsx"],
|
|
763
|
+
["md", "markdown"],
|
|
764
|
+
["mdx", "mdx"],
|
|
765
|
+
["mjs", "javascript"],
|
|
766
|
+
["py", "python"],
|
|
767
|
+
["rb", "ruby"],
|
|
768
|
+
["rs", "rust"],
|
|
769
|
+
["sh", "shell"],
|
|
770
|
+
["svelte", "svelte"],
|
|
771
|
+
["toml", "toml"],
|
|
772
|
+
["ts", "typescript"],
|
|
773
|
+
["tsx", "tsx"],
|
|
774
|
+
["vue", "vue"],
|
|
775
|
+
["yaml", "yaml"],
|
|
776
|
+
["yml", "yaml"]
|
|
777
|
+
]);
|
|
778
|
+
function sourceKey(source) {
|
|
779
|
+
return `${source.repo}@${source.ref}:${source.path}`;
|
|
780
|
+
}
|
|
781
|
+
function formatLineRange(lines) {
|
|
782
|
+
return lines.start === lines.end ? `L${lines.start}` : `L${lines.start}-L${lines.end}`;
|
|
783
|
+
}
|
|
784
|
+
function parseGitHubLineRange(value) {
|
|
785
|
+
if (!value) return void 0;
|
|
786
|
+
const match = value.trim().match(/^#?L?(\d+)(?:-L?(\d+))?$/i);
|
|
787
|
+
if (!match) return void 0;
|
|
788
|
+
const start = Number.parseInt(match[1], 10);
|
|
789
|
+
const end = match[2] ? Number.parseInt(match[2], 10) : start;
|
|
790
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) return;
|
|
791
|
+
return {
|
|
792
|
+
start,
|
|
793
|
+
end
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function createGitHubPermalink(source) {
|
|
797
|
+
const fragment = source.lines ? `#${formatLineRange(source.lines)}` : "";
|
|
798
|
+
return `https://github.com/${source.repo}/blob/${encodeURIComponent(source.ref)}/${encodePath(source.path)}${fragment}`;
|
|
799
|
+
}
|
|
800
|
+
function parseGitHubPermalink(value) {
|
|
801
|
+
let url;
|
|
802
|
+
try {
|
|
803
|
+
url = new URL(value);
|
|
804
|
+
} catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com") return null;
|
|
808
|
+
let parts;
|
|
809
|
+
try {
|
|
810
|
+
parts = url.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
|
|
811
|
+
} catch {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
if (parts.length < 5 || parts[2] !== "blob") return null;
|
|
815
|
+
const repo = `${parts[0]}/${parts[1]}`;
|
|
816
|
+
const ref = parts[3];
|
|
817
|
+
const path = parts.slice(4).join("/");
|
|
818
|
+
if (!isSafeGitHubRepo(repo) || !isSafeGitHubRef(ref) || !isSafeGitHubPath(path)) return null;
|
|
819
|
+
const source = {
|
|
820
|
+
repo,
|
|
821
|
+
ref,
|
|
822
|
+
path,
|
|
823
|
+
lines: parseGitHubLineRange(url.hash)
|
|
824
|
+
};
|
|
825
|
+
return {
|
|
826
|
+
...source,
|
|
827
|
+
permalink: createGitHubPermalink(source)
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function inferLanguage(path) {
|
|
831
|
+
const fileName = path.split("/").at(-1)?.toLowerCase() ?? "";
|
|
832
|
+
if (fileName === "dockerfile") return "dockerfile";
|
|
833
|
+
if (fileName === "makefile") return "makefile";
|
|
834
|
+
const extension = fileName.includes(".") ? fileName.split(".").at(-1) : void 0;
|
|
835
|
+
return extension ? EXTENSION_LANGUAGE_MAP.get(extension) ?? extension : null;
|
|
836
|
+
}
|
|
837
|
+
//#endregion
|
|
838
|
+
//#region src/plugins/github/types.ts
|
|
839
|
+
const defaultOptions$1 = {
|
|
840
|
+
token: "",
|
|
841
|
+
cache: true,
|
|
842
|
+
cacheTTL: 36e5,
|
|
843
|
+
maxSourceBytes: 2e5,
|
|
844
|
+
maxSourceLines: 120
|
|
845
|
+
};
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/plugins/github/api.ts
|
|
848
|
+
const repoCache = /* @__PURE__ */ new Map();
|
|
849
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
850
|
+
function githubHeaders(options) {
|
|
851
|
+
const headers = {
|
|
852
|
+
Accept: "application/vnd.github.v3+json",
|
|
853
|
+
"User-Agent": "ox-content-github-plugin"
|
|
854
|
+
};
|
|
855
|
+
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
856
|
+
return headers;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Fetch repository data from GitHub API.
|
|
860
|
+
*/
|
|
861
|
+
async function fetchRepoData(repo, options) {
|
|
862
|
+
if (!isSafeGitHubRepo(repo)) return null;
|
|
863
|
+
if (options.cache) {
|
|
864
|
+
const cached = repoCache.get(repo);
|
|
865
|
+
if (cached && Date.now() - cached.timestamp < options.cacheTTL) return cached.data;
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
const response = await fetch(`https://api.github.com/repos/${repo}`, { headers: githubHeaders(options) });
|
|
869
|
+
if (!response.ok) {
|
|
870
|
+
console.warn(`Failed to fetch GitHub repo ${repo}: ${response.status}`);
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
const data = await response.json();
|
|
874
|
+
if (options.cache) repoCache.set(repo, {
|
|
875
|
+
data,
|
|
876
|
+
timestamp: Date.now()
|
|
877
|
+
});
|
|
878
|
+
return data;
|
|
879
|
+
} catch (error) {
|
|
880
|
+
console.warn(`Error fetching GitHub repo ${repo}:`, error);
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Fetch source file data from GitHub API.
|
|
886
|
+
*/
|
|
887
|
+
async function fetchGitHubSource(source, options) {
|
|
888
|
+
if (!isSafeGitHubRepo(source.repo) || !isSafeGitHubRef(source.ref) || !isSafeGitHubPath(source.path)) return null;
|
|
889
|
+
const key = sourceKey(source);
|
|
890
|
+
if (options.cache) {
|
|
891
|
+
const cached = sourceCache.get(key);
|
|
892
|
+
if (cached && Date.now() - cached.timestamp < options.cacheTTL) return cached.data;
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
const apiUrl = `https://api.github.com/repos/${source.repo}/contents/${encodePath(source.path)}?ref=${encodeURIComponent(source.ref)}`;
|
|
896
|
+
const response = await fetch(apiUrl, { headers: githubHeaders(options) });
|
|
897
|
+
if (!response.ok) {
|
|
898
|
+
console.warn(`Failed to fetch GitHub source ${source.permalink}: ${response.status}`);
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
const data = await response.json();
|
|
902
|
+
if (data.type !== "file" || data.encoding !== "base64" || !data.content || (data.size ?? 0) > options.maxSourceBytes) return null;
|
|
903
|
+
const content = Buffer$1.from(data.content.replace(/\s/g, ""), "base64").toString("utf8");
|
|
904
|
+
if (Buffer$1.byteLength(content) > options.maxSourceBytes) return null;
|
|
905
|
+
const sourceData = {
|
|
906
|
+
repo: source.repo,
|
|
907
|
+
ref: source.ref,
|
|
908
|
+
path: source.path,
|
|
909
|
+
permalink: source.permalink,
|
|
910
|
+
content,
|
|
911
|
+
size: data.size ?? Buffer$1.byteLength(content),
|
|
912
|
+
html_url: data.html_url ?? source.permalink,
|
|
913
|
+
language: inferLanguage(source.path)
|
|
914
|
+
};
|
|
915
|
+
if (options.cache) sourceCache.set(key, {
|
|
916
|
+
data: sourceData,
|
|
917
|
+
timestamp: Date.now()
|
|
918
|
+
});
|
|
919
|
+
return sourceData;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
console.warn(`Error fetching GitHub source ${source.permalink}:`, error);
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Pre-fetch all GitHub repos data.
|
|
927
|
+
*/
|
|
928
|
+
async function prefetchGitHubRepos(repos, options) {
|
|
929
|
+
const mergedOptions = {
|
|
930
|
+
...defaultOptions$1,
|
|
931
|
+
...options
|
|
932
|
+
};
|
|
933
|
+
const results = /* @__PURE__ */ new Map();
|
|
934
|
+
await Promise.all(Array.from(new Set(repos)).map(async (repo) => {
|
|
935
|
+
const data = await fetchRepoData(repo, mergedOptions);
|
|
936
|
+
results.set(repo, data);
|
|
937
|
+
}));
|
|
938
|
+
return results;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Pre-fetch all GitHub source files.
|
|
942
|
+
*/
|
|
943
|
+
async function prefetchGitHubSources(sources, options) {
|
|
944
|
+
const mergedOptions = {
|
|
945
|
+
...defaultOptions$1,
|
|
946
|
+
...options
|
|
947
|
+
};
|
|
948
|
+
const results = /* @__PURE__ */ new Map();
|
|
949
|
+
const uniqueSources = Array.from(new Map(sources.map((source) => [sourceKey(source), source])).values());
|
|
950
|
+
await Promise.all(uniqueSources.map(async (source) => {
|
|
951
|
+
const data = await fetchGitHubSource(source, mergedOptions);
|
|
952
|
+
results.set(sourceKey(source), data);
|
|
953
|
+
}));
|
|
954
|
+
return results;
|
|
955
|
+
}
|
|
956
|
+
//#endregion
|
|
957
|
+
//#region src/plugins/github/attributes.ts
|
|
958
|
+
const GITHUB_COMPONENT_RE = /<github\b([^>]*)>/gi;
|
|
959
|
+
const ATTRIBUTE_RE = /([:\w-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>/]+)))?/g;
|
|
960
|
+
/**
|
|
961
|
+
* Collect all GitHub repos from HTML for pre-fetching.
|
|
962
|
+
*/
|
|
963
|
+
async function collectGitHubRepos(html) {
|
|
964
|
+
const repos = [];
|
|
965
|
+
GITHUB_COMPONENT_RE.lastIndex = 0;
|
|
966
|
+
let match;
|
|
967
|
+
while ((match = GITHUB_COMPONENT_RE.exec(html)) !== null) {
|
|
968
|
+
const attrs = parseAttributes(match[1]);
|
|
969
|
+
if (attrs.path || attrs.file || attrs.permalink || attrs.url || attrs.href) continue;
|
|
970
|
+
const repo = attrs.repo;
|
|
971
|
+
if (repo && isSafeGitHubRepo(repo)) repos.push(repo);
|
|
972
|
+
}
|
|
973
|
+
return repos;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Collect all GitHub source references from HTML for pre-fetching.
|
|
977
|
+
*/
|
|
978
|
+
async function collectGitHubSources(html) {
|
|
979
|
+
const sources = [];
|
|
980
|
+
GITHUB_COMPONENT_RE.lastIndex = 0;
|
|
981
|
+
let match;
|
|
982
|
+
while ((match = GITHUB_COMPONENT_RE.exec(html)) !== null) {
|
|
983
|
+
const source = sourceRefFromAttributes(parseAttributes(match[1]));
|
|
984
|
+
if (source) sources.push(source);
|
|
985
|
+
}
|
|
986
|
+
return sources;
|
|
987
|
+
}
|
|
988
|
+
function parseAttributes(raw) {
|
|
989
|
+
const attrs = {};
|
|
990
|
+
ATTRIBUTE_RE.lastIndex = 0;
|
|
991
|
+
let match;
|
|
992
|
+
while ((match = ATTRIBUTE_RE.exec(raw)) !== null) attrs[match[1].toLowerCase()] = match[2] ?? match[3] ?? match[4] ?? "";
|
|
993
|
+
return attrs;
|
|
994
|
+
}
|
|
995
|
+
function attributesFromElement(el) {
|
|
996
|
+
const attrs = {};
|
|
997
|
+
for (const name of [
|
|
998
|
+
"permalink",
|
|
999
|
+
"url",
|
|
1000
|
+
"href",
|
|
1001
|
+
"repo",
|
|
1002
|
+
"path",
|
|
1003
|
+
"file",
|
|
1004
|
+
"ref",
|
|
1005
|
+
"sha",
|
|
1006
|
+
"branch",
|
|
1007
|
+
"loc",
|
|
1008
|
+
"lines",
|
|
1009
|
+
"line"
|
|
1010
|
+
]) {
|
|
1011
|
+
const value = getAttribute$2(el, name);
|
|
1012
|
+
if (value !== void 0) attrs[name] = value;
|
|
1013
|
+
}
|
|
1014
|
+
return attrs;
|
|
1015
|
+
}
|
|
1016
|
+
function getAttribute$2(el, name) {
|
|
1017
|
+
const value = el.properties?.[name];
|
|
1018
|
+
if (typeof value === "string") return value;
|
|
1019
|
+
if (Array.isArray(value)) return value.join(" ");
|
|
1020
|
+
}
|
|
1021
|
+
function sourceRefFromAttributes(attrs) {
|
|
1022
|
+
const permalink = attrs.permalink ?? attrs.url ?? attrs.href;
|
|
1023
|
+
if (permalink) return parseGitHubPermalink(permalink);
|
|
1024
|
+
const repo = attrs.repo;
|
|
1025
|
+
const path = attrs.path ?? attrs.file;
|
|
1026
|
+
if (!repo || !path || !isSafeGitHubRepo(repo) || !isSafeGitHubPath(path)) return null;
|
|
1027
|
+
const ref = attrs.ref ?? attrs.sha ?? attrs.branch ?? "main";
|
|
1028
|
+
if (!isSafeGitHubRef(ref)) return null;
|
|
1029
|
+
const source = {
|
|
1030
|
+
repo,
|
|
1031
|
+
ref,
|
|
1032
|
+
path,
|
|
1033
|
+
lines: parseGitHubLineRange(attrs.loc ?? attrs.lines ?? attrs.line)
|
|
1034
|
+
};
|
|
1035
|
+
return {
|
|
1036
|
+
...source,
|
|
1037
|
+
permalink: createGitHubPermalink(source)
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/plugins/github/fallback-card.ts
|
|
1042
|
+
/**
|
|
1043
|
+
* Create fallback element when repo data is unavailable.
|
|
1044
|
+
*/
|
|
1045
|
+
function createFallbackCard$1(repo) {
|
|
1046
|
+
return {
|
|
1047
|
+
type: "element",
|
|
1048
|
+
tagName: "a",
|
|
1049
|
+
properties: {
|
|
1050
|
+
className: ["ox-github-card", "error"],
|
|
1051
|
+
href: isSafeGitHubRepo(repo) ? `https://github.com/${repo}` : "#",
|
|
1052
|
+
target: "_blank",
|
|
1053
|
+
rel: "noopener noreferrer"
|
|
1054
|
+
},
|
|
1055
|
+
children: [{
|
|
1056
|
+
type: "element",
|
|
1057
|
+
tagName: "div",
|
|
1058
|
+
properties: { className: ["ox-github-header"] },
|
|
1059
|
+
children: [{
|
|
1060
|
+
type: "element",
|
|
1061
|
+
tagName: "svg",
|
|
1062
|
+
properties: {
|
|
1063
|
+
className: ["ox-github-icon"],
|
|
1064
|
+
viewBox: "0 0 16 16",
|
|
1065
|
+
fill: "currentColor"
|
|
1066
|
+
},
|
|
1067
|
+
children: [{
|
|
1068
|
+
type: "element",
|
|
1069
|
+
tagName: "path",
|
|
1070
|
+
properties: { d: "M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z" },
|
|
1071
|
+
children: []
|
|
1072
|
+
}]
|
|
1073
|
+
}, {
|
|
1074
|
+
type: "element",
|
|
1075
|
+
tagName: "span",
|
|
1076
|
+
properties: { className: ["ox-github-repo"] },
|
|
1077
|
+
children: [{
|
|
1078
|
+
type: "text",
|
|
1079
|
+
value: repo
|
|
1080
|
+
}]
|
|
1081
|
+
}]
|
|
1082
|
+
}]
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/plugins/github/repo-card.ts
|
|
1087
|
+
function formatNumber(num) {
|
|
1088
|
+
if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
|
|
1089
|
+
if (num >= 1e3) return `${(num / 1e3).toFixed(1)}k`;
|
|
1090
|
+
return String(num);
|
|
1091
|
+
}
|
|
1092
|
+
function iconPath(d) {
|
|
1093
|
+
return {
|
|
1094
|
+
type: "element",
|
|
1095
|
+
tagName: "svg",
|
|
1096
|
+
properties: {
|
|
1097
|
+
viewBox: "0 0 16 16",
|
|
1098
|
+
fill: "currentColor"
|
|
1099
|
+
},
|
|
1100
|
+
children: [{
|
|
1101
|
+
type: "element",
|
|
1102
|
+
tagName: "path",
|
|
1103
|
+
properties: { d },
|
|
1104
|
+
children: []
|
|
1105
|
+
}]
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function createStatsChildren(repoData) {
|
|
1109
|
+
const statsChildren = [];
|
|
1110
|
+
if (repoData.language) statsChildren.push({
|
|
1111
|
+
type: "element",
|
|
1112
|
+
tagName: "span",
|
|
1113
|
+
properties: { className: ["ox-github-language"] },
|
|
1114
|
+
children: [{
|
|
1115
|
+
type: "element",
|
|
1116
|
+
tagName: "span",
|
|
1117
|
+
properties: {
|
|
1118
|
+
className: ["ox-github-language-color"],
|
|
1119
|
+
"data-lang": repoData.language.toLowerCase()
|
|
1120
|
+
},
|
|
1121
|
+
children: []
|
|
1122
|
+
}, {
|
|
1123
|
+
type: "text",
|
|
1124
|
+
value: repoData.language
|
|
1125
|
+
}]
|
|
1126
|
+
});
|
|
1127
|
+
statsChildren.push({
|
|
1128
|
+
type: "element",
|
|
1129
|
+
tagName: "span",
|
|
1130
|
+
properties: { className: ["ox-github-stat"] },
|
|
1131
|
+
children: [iconPath("M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"), {
|
|
1132
|
+
type: "text",
|
|
1133
|
+
value: formatNumber(repoData.stargazers_count)
|
|
1134
|
+
}]
|
|
1135
|
+
});
|
|
1136
|
+
statsChildren.push({
|
|
1137
|
+
type: "element",
|
|
1138
|
+
tagName: "span",
|
|
1139
|
+
properties: { className: ["ox-github-stat"] },
|
|
1140
|
+
children: [iconPath("M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"), {
|
|
1141
|
+
type: "text",
|
|
1142
|
+
value: formatNumber(repoData.forks_count)
|
|
1143
|
+
}]
|
|
1144
|
+
});
|
|
1145
|
+
return statsChildren;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Create GitHub card element from repo data.
|
|
1149
|
+
*/
|
|
1150
|
+
function createGitHubCard(repoData) {
|
|
1151
|
+
return {
|
|
1152
|
+
type: "element",
|
|
1153
|
+
tagName: "a",
|
|
1154
|
+
properties: {
|
|
1155
|
+
className: ["ox-github-card"],
|
|
1156
|
+
href: repoData.html_url,
|
|
1157
|
+
target: "_blank",
|
|
1158
|
+
rel: "noopener noreferrer"
|
|
1159
|
+
},
|
|
1160
|
+
children: [
|
|
1161
|
+
{
|
|
1162
|
+
type: "element",
|
|
1163
|
+
tagName: "div",
|
|
1164
|
+
properties: { className: ["ox-github-header"] },
|
|
1165
|
+
children: [{
|
|
1166
|
+
...iconPath("M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"),
|
|
1167
|
+
properties: {
|
|
1168
|
+
className: ["ox-github-icon"],
|
|
1169
|
+
viewBox: "0 0 16 16",
|
|
1170
|
+
fill: "currentColor"
|
|
1171
|
+
}
|
|
1172
|
+
}, {
|
|
1173
|
+
type: "element",
|
|
1174
|
+
tagName: "span",
|
|
1175
|
+
properties: { className: ["ox-github-repo"] },
|
|
1176
|
+
children: [{
|
|
1177
|
+
type: "text",
|
|
1178
|
+
value: repoData.full_name
|
|
1179
|
+
}]
|
|
1180
|
+
}]
|
|
1181
|
+
},
|
|
1182
|
+
...repoData.description ? [{
|
|
1183
|
+
type: "element",
|
|
1184
|
+
tagName: "p",
|
|
1185
|
+
properties: { className: ["ox-github-description"] },
|
|
1186
|
+
children: [{
|
|
1187
|
+
type: "text",
|
|
1188
|
+
value: repoData.description
|
|
1189
|
+
}]
|
|
1190
|
+
}] : [],
|
|
1191
|
+
{
|
|
1192
|
+
type: "element",
|
|
1193
|
+
tagName: "div",
|
|
1194
|
+
properties: { className: ["ox-github-stats"] },
|
|
1195
|
+
children: createStatsChildren(repoData)
|
|
1196
|
+
}
|
|
1197
|
+
]
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region src/plugins/github/source-card.ts
|
|
1202
|
+
function normalizeSourceLines(content) {
|
|
1203
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
1204
|
+
if (lines.length > 1 && lines.at(-1) === "") lines.pop();
|
|
1205
|
+
return lines.length > 0 ? lines : [""];
|
|
1206
|
+
}
|
|
1207
|
+
function createGitHubSourceCard(source, lines, options) {
|
|
1208
|
+
const allLines = normalizeSourceLines(source.content);
|
|
1209
|
+
const start = Math.min(lines?.start ?? 1, allLines.length);
|
|
1210
|
+
const end = lines ? Math.min(lines.end, allLines.length) : Math.min(allLines.length, options.maxSourceLines);
|
|
1211
|
+
const selectedLines = allLines.slice(start - 1, end);
|
|
1212
|
+
const lineRange = {
|
|
1213
|
+
start,
|
|
1214
|
+
end
|
|
1215
|
+
};
|
|
1216
|
+
const loc = selectedLines.length;
|
|
1217
|
+
const rangeLabel = formatLineRange(lineRange);
|
|
1218
|
+
const locLabel = !lines && end < allLines.length ? `${rangeLabel} of ${allLines.length} LOC` : `${rangeLabel} - ${loc} LOC`;
|
|
1219
|
+
const languageClass = source.language ? [`language-${source.language}`] : [];
|
|
1220
|
+
return {
|
|
1221
|
+
type: "element",
|
|
1222
|
+
tagName: "figure",
|
|
1223
|
+
properties: {
|
|
1224
|
+
className: ["ox-github-code"],
|
|
1225
|
+
"data-loc": String(loc),
|
|
1226
|
+
"data-source": source.permalink
|
|
1227
|
+
},
|
|
1228
|
+
children: [{
|
|
1229
|
+
type: "element",
|
|
1230
|
+
tagName: "figcaption",
|
|
1231
|
+
properties: { className: ["ox-github-code-header"] },
|
|
1232
|
+
children: [{
|
|
1233
|
+
type: "element",
|
|
1234
|
+
tagName: "a",
|
|
1235
|
+
properties: {
|
|
1236
|
+
className: ["ox-github-code-title"],
|
|
1237
|
+
href: source.permalink,
|
|
1238
|
+
target: "_blank",
|
|
1239
|
+
rel: "noopener noreferrer"
|
|
1240
|
+
},
|
|
1241
|
+
children: [{
|
|
1242
|
+
type: "text",
|
|
1243
|
+
value: `${source.repo}/${source.path}`
|
|
1244
|
+
}]
|
|
1245
|
+
}, {
|
|
1246
|
+
type: "element",
|
|
1247
|
+
tagName: "span",
|
|
1248
|
+
properties: { className: ["ox-github-code-loc"] },
|
|
1249
|
+
children: [{
|
|
1250
|
+
type: "text",
|
|
1251
|
+
value: locLabel
|
|
1252
|
+
}]
|
|
1253
|
+
}]
|
|
1254
|
+
}, {
|
|
1255
|
+
type: "element",
|
|
1256
|
+
tagName: "pre",
|
|
1257
|
+
properties: {
|
|
1258
|
+
className: ["ox-github-code-block", ...languageClass],
|
|
1259
|
+
...source.language ? { "data-language": source.language } : {}
|
|
1260
|
+
},
|
|
1261
|
+
children: [{
|
|
1262
|
+
type: "element",
|
|
1263
|
+
tagName: "code",
|
|
1264
|
+
properties: { className: languageClass },
|
|
1265
|
+
children: selectedLines.map((line, index) => {
|
|
1266
|
+
const lineNumber = start + index;
|
|
1267
|
+
return {
|
|
1268
|
+
type: "element",
|
|
1269
|
+
tagName: "span",
|
|
1270
|
+
properties: {
|
|
1271
|
+
className: ["line", "ox-github-code-line"],
|
|
1272
|
+
"data-line": String(lineNumber)
|
|
1273
|
+
},
|
|
1274
|
+
children: [{
|
|
1275
|
+
type: "element",
|
|
1276
|
+
tagName: "span",
|
|
1277
|
+
properties: { className: ["ox-github-code-line-number"] },
|
|
1278
|
+
children: [{
|
|
1279
|
+
type: "text",
|
|
1280
|
+
value: String(lineNumber)
|
|
1281
|
+
}]
|
|
1282
|
+
}, {
|
|
1283
|
+
type: "element",
|
|
1284
|
+
tagName: "span",
|
|
1285
|
+
properties: { className: ["ox-github-code-line-content"] },
|
|
1286
|
+
children: [{
|
|
1287
|
+
type: "text",
|
|
1288
|
+
value: line || " "
|
|
1289
|
+
}]
|
|
1290
|
+
}]
|
|
1291
|
+
};
|
|
1292
|
+
})
|
|
1293
|
+
}]
|
|
1294
|
+
}]
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/plugins/github/transform.ts
|
|
1299
|
+
const rehypeParse$2 = interopDefault(rehypeParsePlugin);
|
|
1300
|
+
const rehypeStringify$2 = interopDefault(rehypeStringifyPlugin);
|
|
1301
|
+
/**
|
|
1302
|
+
* Rehype plugin to transform GitHub components.
|
|
1303
|
+
*/
|
|
1304
|
+
function rehypeGitHub(repoDataMap, sourceDataMap, options) {
|
|
1305
|
+
return (tree) => {
|
|
1306
|
+
const visit = (node) => {
|
|
1307
|
+
if ("children" in node) for (let i = 0; i < node.children.length; i++) {
|
|
1308
|
+
const child = node.children[i];
|
|
1309
|
+
if (child.type !== "element") continue;
|
|
1310
|
+
if (child.tagName.toLowerCase() !== "github") {
|
|
1311
|
+
visit(child);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const attrs = attributesFromElement(child);
|
|
1315
|
+
const source = sourceRefFromAttributes(attrs);
|
|
1316
|
+
if (source) {
|
|
1317
|
+
const sourceData = sourceDataMap.get(sourceKey(source));
|
|
1318
|
+
node.children[i] = sourceData ? createGitHubSourceCard(sourceData, source.lines, options) : createFallbackCard$1(source.permalink);
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const repo = attrs.repo;
|
|
1322
|
+
if (repo) {
|
|
1323
|
+
const repoData = repoDataMap.get(repo);
|
|
1324
|
+
node.children[i] = repoData ? createGitHubCard(repoData) : createFallbackCard$1(repo);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
visit(tree);
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Transform GitHub components in HTML.
|
|
1333
|
+
*/
|
|
1334
|
+
async function transformGitHub(html, repoDataMap, options) {
|
|
1335
|
+
const mergedOptions = {
|
|
1336
|
+
...defaultOptions$1,
|
|
1337
|
+
...options
|
|
1338
|
+
};
|
|
1339
|
+
let dataMap = repoDataMap;
|
|
1340
|
+
if (!dataMap) dataMap = await prefetchGitHubRepos(await collectGitHubRepos(html), mergedOptions);
|
|
1341
|
+
const sourceDataMap = await prefetchGitHubSources(await collectGitHubSources(html), mergedOptions);
|
|
1342
|
+
const result = await unified().use(rehypeParse$2, { fragment: true }).use(rehypeGitHub, dataMap, sourceDataMap, mergedOptions).use(rehypeStringify$2).process(html);
|
|
1343
|
+
return String(result);
|
|
1344
|
+
}
|
|
1345
|
+
//#endregion
|
|
1346
|
+
//#region src/plugins/github.ts
|
|
1347
|
+
var github_exports = /* @__PURE__ */ __exportAll({ transformGitHub: () => transformGitHub });
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region src/plugins/ogp.ts
|
|
1350
|
+
/**
|
|
1351
|
+
* OGP Card Plugin - Link card embedding
|
|
1352
|
+
*
|
|
1353
|
+
* Transforms <OgCard> components into static link preview cards
|
|
1354
|
+
* by fetching OGP metadata at build time.
|
|
1355
|
+
*/
|
|
1356
|
+
var ogp_exports = /* @__PURE__ */ __exportAll({
|
|
1357
|
+
collectOgpUrls: () => collectOgpUrls,
|
|
1358
|
+
fetchOgpData: () => fetchOgpData,
|
|
1359
|
+
isSafeOgpUrl: () => isSafeOgpUrl,
|
|
1360
|
+
prefetchOgpData: () => prefetchOgpData,
|
|
1361
|
+
transformOgp: () => transformOgp
|
|
1362
|
+
});
|
|
1363
|
+
const rehypeParse$1 = interopDefault(rehypeParsePlugin);
|
|
1364
|
+
const rehypeStringify$1 = interopDefault(rehypeStringifyPlugin);
|
|
1365
|
+
const defaultOptions = {
|
|
1366
|
+
timeout: 1e4,
|
|
1367
|
+
cache: true,
|
|
1368
|
+
cacheTTL: 36e5,
|
|
1369
|
+
userAgent: "ox-content-ogp-bot/1.0 (compatible; +https://github.com/ubugeeei-prod/ox-content)"
|
|
1370
|
+
};
|
|
1371
|
+
const ogpCache = /* @__PURE__ */ new Map();
|
|
1372
|
+
function isPrivateIPv4(hostname) {
|
|
1373
|
+
const parts = hostname.split(".").map(Number);
|
|
1374
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
1375
|
+
const [a, b] = parts;
|
|
1376
|
+
return a === 10 || a === 127 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
1377
|
+
}
|
|
1378
|
+
function isSafeOgpUrl(value) {
|
|
1379
|
+
try {
|
|
1380
|
+
const url = new URL(value);
|
|
1381
|
+
const host = url.hostname.toLowerCase();
|
|
1382
|
+
const ipv6 = host.replace(/^\[|\]$/g, "");
|
|
1383
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1384
|
+
if (host === "localhost" || host.endsWith(".localhost")) return false;
|
|
1385
|
+
if (ipv6.includes(":") && (ipv6 === "::1" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80"))) return false;
|
|
1386
|
+
return !isPrivateIPv4(host);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Get element attribute value.
|
|
1393
|
+
*/
|
|
1394
|
+
function getAttribute$1(el, name) {
|
|
1395
|
+
const value = el.properties?.[name];
|
|
1396
|
+
if (typeof value === "string") return value;
|
|
1397
|
+
if (Array.isArray(value)) return value.join(" ");
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Extract domain from URL.
|
|
1401
|
+
*/
|
|
1402
|
+
function extractDomain(url) {
|
|
1403
|
+
try {
|
|
1404
|
+
return new URL(url).hostname;
|
|
1405
|
+
} catch {
|
|
1406
|
+
return url;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Get favicon URL for a domain.
|
|
1411
|
+
*/
|
|
1412
|
+
function getFaviconUrl(url) {
|
|
1413
|
+
try {
|
|
1414
|
+
return `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return "";
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Parse OGP metadata from HTML.
|
|
1421
|
+
*/
|
|
1422
|
+
function parseOgpFromHtml(html, url) {
|
|
1423
|
+
const result = {
|
|
1424
|
+
url,
|
|
1425
|
+
title: ""
|
|
1426
|
+
};
|
|
1427
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
1428
|
+
result.title = (html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:title["']/i))?.[1] || titleMatch?.[1] || extractDomain(url);
|
|
1429
|
+
const descMatch = html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:description["']/i) || html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*name=["']description["']/i);
|
|
1430
|
+
if (descMatch) result.description = descMatch[1];
|
|
1431
|
+
const imageMatch = html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:image["']/i);
|
|
1432
|
+
if (imageMatch) {
|
|
1433
|
+
let imageUrl = imageMatch[1];
|
|
1434
|
+
if (imageUrl.startsWith("/")) try {
|
|
1435
|
+
const urlObj = new URL(url);
|
|
1436
|
+
imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;
|
|
1437
|
+
} catch {}
|
|
1438
|
+
result.image = imageUrl;
|
|
1439
|
+
}
|
|
1440
|
+
const siteNameMatch = html.match(/<meta[^>]*property=["']og:site_name["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:site_name["']/i);
|
|
1441
|
+
if (siteNameMatch) result.siteName = siteNameMatch[1];
|
|
1442
|
+
result.favicon = getFaviconUrl(url);
|
|
1443
|
+
return result;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Fetch OGP data for a URL.
|
|
1447
|
+
*/
|
|
1448
|
+
async function fetchOgpData(url, options) {
|
|
1449
|
+
if (!isSafeOgpUrl(url)) return null;
|
|
1450
|
+
if (options.cache) {
|
|
1451
|
+
const cached = ogpCache.get(url);
|
|
1452
|
+
if (cached && Date.now() - cached.timestamp < options.cacheTTL) return cached.data;
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
const controller = new AbortController();
|
|
1456
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
1457
|
+
const response = await fetch(url, {
|
|
1458
|
+
headers: {
|
|
1459
|
+
"User-Agent": options.userAgent,
|
|
1460
|
+
Accept: "text/html,application/xhtml+xml"
|
|
1461
|
+
},
|
|
1462
|
+
signal: controller.signal
|
|
1463
|
+
});
|
|
1464
|
+
clearTimeout(timeoutId);
|
|
1465
|
+
if (!response.ok) {
|
|
1466
|
+
console.warn(`Failed to fetch OGP for ${url}: ${response.status}`);
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
const data = parseOgpFromHtml(await response.text(), url);
|
|
1470
|
+
if (options.cache) ogpCache.set(url, {
|
|
1471
|
+
data,
|
|
1472
|
+
timestamp: Date.now()
|
|
1473
|
+
});
|
|
1474
|
+
return data;
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
if (error instanceof Error && error.name === "AbortError") console.warn(`Timeout fetching OGP for ${url}`);
|
|
1477
|
+
else console.warn(`Error fetching OGP for ${url}:`, error);
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Create OGP card element.
|
|
1483
|
+
*/
|
|
1484
|
+
function createOgpCard(data) {
|
|
1485
|
+
const children = [];
|
|
1486
|
+
const contentChildren = [];
|
|
1487
|
+
contentChildren.push({
|
|
1488
|
+
type: "element",
|
|
1489
|
+
tagName: "div",
|
|
1490
|
+
properties: { className: ["ox-ogp-title"] },
|
|
1491
|
+
children: [{
|
|
1492
|
+
type: "text",
|
|
1493
|
+
value: data.title
|
|
1494
|
+
}]
|
|
1495
|
+
});
|
|
1496
|
+
if (data.description) contentChildren.push({
|
|
1497
|
+
type: "element",
|
|
1498
|
+
tagName: "div",
|
|
1499
|
+
properties: { className: ["ox-ogp-description"] },
|
|
1500
|
+
children: [{
|
|
1501
|
+
type: "text",
|
|
1502
|
+
value: data.description
|
|
1503
|
+
}]
|
|
1504
|
+
});
|
|
1505
|
+
const metaChildren = [];
|
|
1506
|
+
if (data.favicon) metaChildren.push({
|
|
1507
|
+
type: "element",
|
|
1508
|
+
tagName: "img",
|
|
1509
|
+
properties: {
|
|
1510
|
+
className: ["ox-ogp-favicon"],
|
|
1511
|
+
src: data.favicon,
|
|
1512
|
+
alt: "",
|
|
1513
|
+
loading: "lazy"
|
|
1514
|
+
},
|
|
1515
|
+
children: []
|
|
1516
|
+
});
|
|
1517
|
+
metaChildren.push({
|
|
1518
|
+
type: "element",
|
|
1519
|
+
tagName: "span",
|
|
1520
|
+
properties: { className: ["ox-ogp-domain"] },
|
|
1521
|
+
children: [{
|
|
1522
|
+
type: "text",
|
|
1523
|
+
value: data.siteName || extractDomain(data.url)
|
|
1524
|
+
}]
|
|
1525
|
+
});
|
|
1526
|
+
contentChildren.push({
|
|
1527
|
+
type: "element",
|
|
1528
|
+
tagName: "div",
|
|
1529
|
+
properties: { className: ["ox-ogp-meta"] },
|
|
1530
|
+
children: metaChildren
|
|
1531
|
+
});
|
|
1532
|
+
children.push({
|
|
1533
|
+
type: "element",
|
|
1534
|
+
tagName: "div",
|
|
1535
|
+
properties: { className: ["ox-ogp-content"] },
|
|
1536
|
+
children: contentChildren
|
|
1537
|
+
});
|
|
1538
|
+
if (data.image) children.push({
|
|
1539
|
+
type: "element",
|
|
1540
|
+
tagName: "img",
|
|
1541
|
+
properties: {
|
|
1542
|
+
className: ["ox-ogp-image"],
|
|
1543
|
+
src: data.image,
|
|
1544
|
+
alt: "",
|
|
1545
|
+
loading: "lazy"
|
|
1546
|
+
},
|
|
1547
|
+
children: []
|
|
1548
|
+
});
|
|
1549
|
+
return {
|
|
1550
|
+
type: "element",
|
|
1551
|
+
tagName: "a",
|
|
1552
|
+
properties: {
|
|
1553
|
+
className: ["ox-ogp-card"],
|
|
1554
|
+
href: isSafeOgpUrl(data.url) ? data.url : "#",
|
|
1555
|
+
target: "_blank",
|
|
1556
|
+
rel: "noopener noreferrer"
|
|
1557
|
+
},
|
|
1558
|
+
children
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Create fallback element when OGP data is unavailable.
|
|
1563
|
+
*/
|
|
1564
|
+
function createFallbackCard(url) {
|
|
1565
|
+
return {
|
|
1566
|
+
type: "element",
|
|
1567
|
+
tagName: "a",
|
|
1568
|
+
properties: {
|
|
1569
|
+
className: ["ox-ogp-simple"],
|
|
1570
|
+
href: isSafeOgpUrl(url) ? url : "#",
|
|
1571
|
+
target: "_blank",
|
|
1572
|
+
rel: "noopener noreferrer"
|
|
1573
|
+
},
|
|
1574
|
+
children: [{
|
|
1575
|
+
type: "element",
|
|
1576
|
+
tagName: "svg",
|
|
1577
|
+
properties: {
|
|
1578
|
+
viewBox: "0 0 24 24",
|
|
1579
|
+
fill: "none",
|
|
1580
|
+
stroke: "currentColor",
|
|
1581
|
+
"stroke-width": "2"
|
|
1582
|
+
},
|
|
1583
|
+
children: [{
|
|
1584
|
+
type: "element",
|
|
1585
|
+
tagName: "path",
|
|
1586
|
+
properties: { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" },
|
|
1587
|
+
children: []
|
|
1588
|
+
}]
|
|
1589
|
+
}, {
|
|
1590
|
+
type: "text",
|
|
1591
|
+
value: extractDomain(url)
|
|
1592
|
+
}]
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Collect all OGP URLs from HTML for pre-fetching.
|
|
1597
|
+
*/
|
|
1598
|
+
async function collectOgpUrls(html) {
|
|
1599
|
+
const urls = [];
|
|
1600
|
+
const urlPattern = /<ogcard[^>]*\s+url=["']([^"']+)["']/gi;
|
|
1601
|
+
let match;
|
|
1602
|
+
while ((match = urlPattern.exec(html)) !== null) if (isSafeOgpUrl(match[1])) urls.push(match[1]);
|
|
1603
|
+
return urls;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Pre-fetch all OGP data.
|
|
1607
|
+
*/
|
|
1608
|
+
async function prefetchOgpData(urls, options) {
|
|
1609
|
+
const mergedOptions = {
|
|
1610
|
+
...defaultOptions,
|
|
1611
|
+
...options
|
|
1612
|
+
};
|
|
1613
|
+
const results = /* @__PURE__ */ new Map();
|
|
1614
|
+
await Promise.all(urls.map(async (url) => {
|
|
1615
|
+
const data = await fetchOgpData(url, mergedOptions);
|
|
1616
|
+
results.set(url, data);
|
|
1617
|
+
}));
|
|
1618
|
+
return results;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Rehype plugin to transform OgCard components.
|
|
1622
|
+
*/
|
|
1623
|
+
function rehypeOgp(ogpDataMap) {
|
|
1624
|
+
return (tree) => {
|
|
1625
|
+
const visit = (node) => {
|
|
1626
|
+
if ("children" in node) for (let i = 0; i < node.children.length; i++) {
|
|
1627
|
+
const child = node.children[i];
|
|
1628
|
+
if (child.type === "element") if (child.tagName.toLowerCase() === "ogcard") {
|
|
1629
|
+
const url = getAttribute$1(child, "url");
|
|
1630
|
+
if (url) {
|
|
1631
|
+
const ogpData = ogpDataMap.get(url);
|
|
1632
|
+
const cardElement = ogpData ? createOgpCard(ogpData) : createFallbackCard(url);
|
|
1633
|
+
node.children[i] = cardElement;
|
|
1634
|
+
}
|
|
1635
|
+
} else visit(child);
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
visit(tree);
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Transform OgCard components in HTML.
|
|
1643
|
+
*/
|
|
1644
|
+
async function transformOgp(html, ogpDataMap, options) {
|
|
1645
|
+
let dataMap = ogpDataMap;
|
|
1646
|
+
if (!dataMap) dataMap = await prefetchOgpData(await collectOgpUrls(html), options);
|
|
1647
|
+
const result = await unified().use(rehypeParse$1, { fragment: true }).use(rehypeOgp, dataMap).use(rehypeStringify$1).process(html);
|
|
303
1648
|
return String(result);
|
|
304
1649
|
}
|
|
305
1650
|
//#endregion
|
|
306
1651
|
//#region src/plugins/index.ts
|
|
1652
|
+
const SELF_CLOSING_EMBED_TAG = /<(GitHub|OgCard|Tweet|XPost|Bluesky|Spotify|StackBlitz|WebContainer|YouTube)((?:[^>"']|"[^"]*"|'[^']*')*?)\s*\/>/gi;
|
|
1653
|
+
/**
|
|
1654
|
+
* Custom embed tags are not HTML void elements, so a self-closing authoring
|
|
1655
|
+
* form like `<GitHub ... />` reaches the HTML re-parsers (Shiki highlighting,
|
|
1656
|
+
* embed transforms) as an unclosed element that swallows the rest of the
|
|
1657
|
+
* document. Normalize to an explicit open/close pair before any rehype pass
|
|
1658
|
+
* runs.
|
|
1659
|
+
*/
|
|
1660
|
+
function normalizeSelfClosingEmbeds(html) {
|
|
1661
|
+
return html.replace(SELF_CLOSING_EMBED_TAG, (_match, tag, attrs) => {
|
|
1662
|
+
return `<${tag}${attrs}></${tag}>`;
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
307
1665
|
/**
|
|
308
1666
|
* Transform all enabled plugins in HTML content.
|
|
309
1667
|
*/
|
|
310
1668
|
async function transformAllPlugins(html, options = {}) {
|
|
311
1669
|
const { tabs = true, pm = false, youtube = true, github = true, ogp, openGraph, mermaid = true, githubToken, spotify = false, stackBlitz = false, twitter = false, bluesky = false, webContainer = false } = options;
|
|
312
|
-
let result = html;
|
|
1670
|
+
let result = normalizeSelfClosingEmbeds(html);
|
|
313
1671
|
const ogpOptions = openGraph ?? ogp ?? true;
|
|
314
1672
|
if (tabs) {
|
|
315
|
-
const { transformTabs } = await import("./tabs.mjs");
|
|
1673
|
+
const { transformTabs } = await import("./tabs.mjs").then((n) => n.a);
|
|
316
1674
|
result = await transformTabs(result);
|
|
317
1675
|
}
|
|
318
1676
|
if (pm) {
|
|
319
|
-
const { transformPm } = await
|
|
1677
|
+
const { transformPm } = await Promise.resolve().then(() => pm_exports);
|
|
320
1678
|
result = await transformPm(result, typeof pm === "object" ? pm : {});
|
|
321
1679
|
}
|
|
322
1680
|
if (youtube) {
|
|
323
|
-
const { transformYouTube } = await
|
|
1681
|
+
const { transformYouTube } = await Promise.resolve().then(() => youtube_exports);
|
|
324
1682
|
result = await transformYouTube(result);
|
|
325
1683
|
}
|
|
326
1684
|
if (github !== false) {
|
|
327
|
-
const { transformGitHub } = await
|
|
1685
|
+
const { transformGitHub } = await Promise.resolve().then(() => github_exports);
|
|
328
1686
|
result = await transformGitHub(result, void 0, {
|
|
329
1687
|
token: githubToken,
|
|
330
1688
|
...typeof github === "object" ? github : {}
|
|
331
1689
|
});
|
|
332
1690
|
}
|
|
333
1691
|
if (ogpOptions !== false) {
|
|
334
|
-
const { transformOgp } = await
|
|
1692
|
+
const { transformOgp } = await Promise.resolve().then(() => ogp_exports);
|
|
335
1693
|
result = await transformOgp(result, void 0, typeof ogpOptions === "object" ? ogpOptions : {});
|
|
336
1694
|
}
|
|
337
1695
|
const mediaOptions = {
|
|
@@ -342,11 +1700,11 @@ async function transformAllPlugins(html, options = {}) {
|
|
|
342
1700
|
webContainer
|
|
343
1701
|
};
|
|
344
1702
|
if (Object.values(mediaOptions).some(Boolean)) {
|
|
345
|
-
const { transformMediaEmbeds } = await
|
|
1703
|
+
const { transformMediaEmbeds } = await Promise.resolve().then(() => media_exports);
|
|
346
1704
|
result = await transformMediaEmbeds(result, mediaOptions);
|
|
347
1705
|
}
|
|
348
1706
|
if (mermaid) {
|
|
349
|
-
const { transformMermaidStatic } = await
|
|
1707
|
+
const { transformMermaidStatic } = await Promise.resolve().then(() => mermaid_exports);
|
|
350
1708
|
result = await transformMermaidStatic(result);
|
|
351
1709
|
}
|
|
352
1710
|
return result;
|
|
@@ -355,20 +1713,20 @@ async function transformAllPlugins(html, options = {}) {
|
|
|
355
1713
|
* Transform built-in embed components in HTML content.
|
|
356
1714
|
*/
|
|
357
1715
|
async function transformBuiltinEmbeds(html, options) {
|
|
358
|
-
let result = html;
|
|
1716
|
+
let result = normalizeSelfClosingEmbeds(html);
|
|
359
1717
|
if (options.github) {
|
|
360
|
-
const { transformGitHub } = await
|
|
1718
|
+
const { transformGitHub } = await Promise.resolve().then(() => github_exports);
|
|
361
1719
|
result = await transformGitHub(result, void 0, {
|
|
362
1720
|
token: process.env.GITHUB_TOKEN,
|
|
363
1721
|
...options.github
|
|
364
1722
|
});
|
|
365
1723
|
}
|
|
366
1724
|
if (options.openGraph) {
|
|
367
|
-
const { transformOgp } = await
|
|
1725
|
+
const { transformOgp } = await Promise.resolve().then(() => ogp_exports);
|
|
368
1726
|
result = await transformOgp(result, void 0, options.openGraph);
|
|
369
1727
|
}
|
|
370
1728
|
if (options.pm) {
|
|
371
|
-
const { transformPm } = await
|
|
1729
|
+
const { transformPm } = await Promise.resolve().then(() => pm_exports);
|
|
372
1730
|
result = await transformPm(result, typeof options.pm === "object" ? options.pm : {});
|
|
373
1731
|
}
|
|
374
1732
|
const mediaOptions = {
|
|
@@ -379,7 +1737,7 @@ async function transformBuiltinEmbeds(html, options) {
|
|
|
379
1737
|
webContainer: options.webContainer
|
|
380
1738
|
};
|
|
381
1739
|
if (Object.values(mediaOptions).some(Boolean)) {
|
|
382
|
-
const { transformMediaEmbeds } = await
|
|
1740
|
+
const { transformMediaEmbeds } = await Promise.resolve().then(() => media_exports);
|
|
383
1741
|
result = await transformMediaEmbeds(result, mediaOptions);
|
|
384
1742
|
}
|
|
385
1743
|
return result;
|
|
@@ -471,7 +1829,8 @@ async function typecheckCodeBlocks(source, options = {}) {
|
|
|
471
1829
|
try {
|
|
472
1830
|
const files = [];
|
|
473
1831
|
await Promise.all(blocks.map(async (block, index) => {
|
|
474
|
-
const
|
|
1832
|
+
const extension = block.language.toLowerCase() === "tsx" ? "tsx" : "ts";
|
|
1833
|
+
const file = join(temp, `snippet-${index}.${extension}`);
|
|
475
1834
|
files.push(file);
|
|
476
1835
|
await writeFile(file, block.code);
|
|
477
1836
|
}));
|
|
@@ -483,7 +1842,7 @@ async function typecheckCodeBlocks(source, options = {}) {
|
|
|
483
1842
|
...files
|
|
484
1843
|
], {
|
|
485
1844
|
cwd: process.cwd(),
|
|
486
|
-
maxBuffer:
|
|
1845
|
+
maxBuffer: 4194304
|
|
487
1846
|
});
|
|
488
1847
|
return [];
|
|
489
1848
|
} catch (error) {
|
|
@@ -636,7 +1995,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
636
1995
|
} : void 0
|
|
637
1996
|
});
|
|
638
1997
|
if (result.errors.length > 0) console.warn("[ox-content] Transform warnings:", result.errors);
|
|
639
|
-
let html = result.html;
|
|
1998
|
+
let html = normalizeSelfClosingEmbeds(result.html);
|
|
640
1999
|
const frontmatter = parseFrontmatterJson(result.frontmatter);
|
|
641
2000
|
const toc = options.toc ? result.toc.map(normalizeTocEntry) : [];
|
|
642
2001
|
if (options.mermaid) html = await transformMermaidStatic(html);
|
|
@@ -1191,7 +2550,7 @@ function getDefaultTemplate() {
|
|
|
1191
2550
|
const { title, description, siteName } = props;
|
|
1192
2551
|
const rawBrand = siteName?.trim() ? siteName : "Ox Content";
|
|
1193
2552
|
const isBrandCard = normalizeBrandValue(title) === normalizeBrandValue(rawBrand);
|
|
1194
|
-
const heroTitle = isBrandCard ? "
|
|
2553
|
+
const heroTitle = isBrandCard ? "High-performance Markdown toolkit" : title;
|
|
1195
2554
|
const heroDescription = isBrandCard ? "Rust-powered docs and high-performance Markdown tooling." : description && description.trim().length > 0 ? description : "Rust-powered docs and Markdown tooling.";
|
|
1196
2555
|
const descriptionHtml = heroDescription.trim().length > 0 ? `<p style="max-width:760px;font-size:28px;color:#93a4c3;line-height:1.45;margin:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${escapeHtml$2(heroDescription)}</p>` : "";
|
|
1197
2556
|
return `<div style="width:100%;height:100%;position:relative;overflow:hidden;box-sizing:border-box;padding:56px 64px 52px;background:#0b1220;font-family:'IBM Plex Sans','Avenir Next','Segoe UI',system-ui,sans-serif;color:#eff6ff;border:1px solid #223252;border-top:4px solid #4f6fae;">
|
|
@@ -1246,7 +2605,7 @@ async function writeCache(cacheDir, key, png) {
|
|
|
1246
2605
|
await fs$2.writeFile(filePath, png);
|
|
1247
2606
|
}
|
|
1248
2607
|
//#endregion
|
|
1249
|
-
//#region \0@oxc-project+runtime@0.
|
|
2608
|
+
//#region \0@oxc-project+runtime@0.142.0/helpers/esm/usingCtx.js
|
|
1250
2609
|
function _usingCtx() {
|
|
1251
2610
|
var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
|
|
1252
2611
|
var n = Error();
|
|
@@ -1413,7 +2772,8 @@ async function resolveVueTemplate(templatePath, options, root) {
|
|
|
1413
2772
|
const { createSSRApp } = await import("vue");
|
|
1414
2773
|
const { renderToString } = await import("vue/server-renderer");
|
|
1415
2774
|
return async (props) => {
|
|
1416
|
-
const
|
|
2775
|
+
const app = createSSRApp(Component, props);
|
|
2776
|
+
const html = await renderToString(app);
|
|
1417
2777
|
if (extractedCss) return `<style>${extractedCss}</style>${html}`;
|
|
1418
2778
|
return html;
|
|
1419
2779
|
};
|
|
@@ -2406,7 +3766,7 @@ function generateSearchModule(options, indexPath) {
|
|
|
2406
3766
|
* during `vite dev`, matching the SSG build output.
|
|
2407
3767
|
*/
|
|
2408
3768
|
/** File extensions to skip in the middleware. */
|
|
2409
|
-
const SKIP_EXTENSIONS = new Set([
|
|
3769
|
+
const SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2410
3770
|
".js",
|
|
2411
3771
|
".ts",
|
|
2412
3772
|
".css",
|
|
@@ -4257,7 +5617,7 @@ async function lintMarkdownFiles(options = {}) {
|
|
|
4257
5617
|
function resolveMarkdownLintFileOptions(options) {
|
|
4258
5618
|
return {
|
|
4259
5619
|
cwd: path$1.resolve(options.cwd ?? process.cwd()),
|
|
4260
|
-
exclude: [
|
|
5620
|
+
exclude: [.../* @__PURE__ */ new Set([...options.exclude ?? DEFAULT_LINT_FILE_EXCLUDE, ...options.ignore ?? []])],
|
|
4261
5621
|
include: [...new Set(options.include ?? DEFAULT_LINT_FILE_INCLUDE)],
|
|
4262
5622
|
lintOptions: {
|
|
4263
5623
|
dictionary: options.dictionary,
|
|
@@ -4349,7 +5709,7 @@ function createEmptyLintResult() {
|
|
|
4349
5709
|
* }
|
|
4350
5710
|
* ```
|
|
4351
5711
|
*/
|
|
4352
|
-
const VOID_ELEMENTS = new Set([
|
|
5712
|
+
const VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
4353
5713
|
"area",
|
|
4354
5714
|
"base",
|
|
4355
5715
|
"br",
|
|
@@ -4365,7 +5725,7 @@ const VOID_ELEMENTS = new Set([
|
|
|
4365
5725
|
"track",
|
|
4366
5726
|
"wbr"
|
|
4367
5727
|
]);
|
|
4368
|
-
const BOOLEAN_ATTRS = new Set([
|
|
5728
|
+
const BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
4369
5729
|
"allowfullscreen",
|
|
4370
5730
|
"async",
|
|
4371
5731
|
"autofocus",
|