@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.
Files changed (68) hide show
  1. package/dist/incremental-dom.cjs +3 -2
  2. package/dist/incremental-dom.cjs.map +1 -1
  3. package/dist/incremental-dom.d.cts.map +1 -1
  4. package/dist/incremental-dom.d.mts.map +1 -1
  5. package/dist/incremental-dom.mjs +2 -1
  6. package/dist/incremental-dom.mjs.map +1 -1
  7. package/dist/index.cjs +1486 -99
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +8 -13
  10. package/dist/index.d.cts.map +1 -1
  11. package/dist/index.d.mts +8 -13
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +1429 -69
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/interop.cjs.map +1 -1
  16. package/dist/interop.mjs.map +1 -1
  17. package/dist/tabs.cjs +3 -4
  18. package/dist/tabs.cjs.map +1 -1
  19. package/dist/tabs.mjs +99 -2
  20. package/dist/tabs.mjs.map +1 -0
  21. package/dist/vitepress-cli.cjs +5 -4
  22. package/dist/vitepress-cli.cjs.map +1 -1
  23. package/dist/vitepress-cli.d.cts +1 -1
  24. package/dist/vitepress-cli.d.mts +1 -1
  25. package/dist/vitepress-cli.mjs +4 -2
  26. package/dist/vitepress-cli.mjs.map +1 -1
  27. package/dist/vitepress.cjs +106 -2
  28. package/dist/vitepress.cjs.map +1 -1
  29. package/dist/vitepress.mjs +32 -2
  30. package/dist/vitepress.mjs.map +1 -1
  31. package/package.json +19 -19
  32. package/dist/chunk.cjs +0 -57
  33. package/dist/github.cjs +0 -690
  34. package/dist/github.cjs.map +0 -1
  35. package/dist/github.mjs +0 -625
  36. package/dist/github.mjs.map +0 -1
  37. package/dist/github2.mjs +0 -2
  38. package/dist/media.cjs +0 -310
  39. package/dist/media.cjs.map +0 -1
  40. package/dist/media.mjs +0 -302
  41. package/dist/media.mjs.map +0 -1
  42. package/dist/media2.mjs +0 -2
  43. package/dist/mermaid.cjs +0 -114
  44. package/dist/mermaid.cjs.map +0 -1
  45. package/dist/mermaid.mjs +0 -2
  46. package/dist/mermaid2.mjs +0 -92
  47. package/dist/mermaid2.mjs.map +0 -1
  48. package/dist/napi.cjs +0 -45
  49. package/dist/napi.cjs.map +0 -1
  50. package/dist/napi.mjs +0 -34
  51. package/dist/napi.mjs.map +0 -1
  52. package/dist/ogp.cjs +0 -341
  53. package/dist/ogp.cjs.map +0 -1
  54. package/dist/ogp.mjs +0 -2
  55. package/dist/ogp2.mjs +0 -302
  56. package/dist/ogp2.mjs.map +0 -1
  57. package/dist/pm.cjs +0 -45
  58. package/dist/pm.cjs.map +0 -1
  59. package/dist/pm.mjs +0 -2
  60. package/dist/pm2.mjs +0 -38
  61. package/dist/pm2.mjs.map +0 -1
  62. package/dist/tabs2.mjs +0 -56
  63. package/dist/tabs2.mjs.map +0 -1
  64. package/dist/youtube.cjs +0 -57
  65. package/dist/youtube.cjs.map +0 -1
  66. package/dist/youtube.mjs +0 -2
  67. package/dist/youtube2.mjs +0 -35
  68. 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 { n as importNapiModuleSync, t as importNapiModule } from "./napi.mjs";
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$1 = interopDefault(rehypeParsePlugin);
142
- const rehypeStringify$1 = interopDefault(rehypeStringifyPlugin);
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$1, { fragment: true }).parse(highlighted);
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$1, { fragment: true }).parse(highlighted);
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 = [...new Set([
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$1, { fragment: true }).use(rehypeShikiHighlight, {
266
+ const result = await unified().use(rehypeParse$3, { fragment: true }).use(rehypeShikiHighlight, {
300
267
  theme,
301
268
  langs
302
- }).use(rehypeStringify$1).process(html);
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("`", "&#96;");
662
+ }
663
+ function escapeHtml$3(value) {
664
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
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 import("./pm.mjs");
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 import("./youtube.mjs");
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 import("./github2.mjs");
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 import("./ogp.mjs");
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 import("./media2.mjs");
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 import("./mermaid.mjs");
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 import("./github2.mjs");
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 import("./ogp.mjs");
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 import("./pm.mjs");
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 import("./media2.mjs");
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 file = join(temp, `snippet-${index}.${block.language.toLowerCase() === "tsx" ? "tsx" : "ts"}`);
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: 1024 * 1024 * 4
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 ? "cargo doc for JavaScript" : title;
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.129.0/helpers/usingCtx.js
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 html = await renderToString(createSSRApp(Component, props));
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: [...new Set([...options.exclude ?? DEFAULT_LINT_FILE_EXCLUDE, ...options.ignore ?? []])],
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",