@ox-content/vite-plugin 3.0.0-alpha.5 → 3.0.0-alpha.7
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/index.cjs +2269 -589
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +269 -25
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +269 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2287 -610
- package/dist/index.mjs.map +1 -1
- package/dist/vitepress.cjs +3 -0
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.mjs +3 -0
- package/dist/vitepress.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -18,6 +18,7 @@ node_path = require_vitepress.__toESM(node_path, 1);
|
|
|
18
18
|
let node_fs_promises = require("node:fs/promises");
|
|
19
19
|
node_fs_promises = require_vitepress.__toESM(node_fs_promises, 1);
|
|
20
20
|
let node_buffer = require("node:buffer");
|
|
21
|
+
let node_crypto = require("node:crypto");
|
|
21
22
|
let node_os = require("node:os");
|
|
22
23
|
let node_util = require("node:util");
|
|
23
24
|
let node_child_process = require("node:child_process");
|
|
@@ -25,7 +26,7 @@ let fs_promises = require("fs/promises");
|
|
|
25
26
|
fs_promises = require_vitepress.__toESM(fs_promises, 1);
|
|
26
27
|
let crypto = require("crypto");
|
|
27
28
|
crypto = require_vitepress.__toESM(crypto, 1);
|
|
28
|
-
let
|
|
29
|
+
let node_dns_promises = require("node:dns/promises");
|
|
29
30
|
let node_zlib = require("node:zlib");
|
|
30
31
|
let fs = require("fs");
|
|
31
32
|
fs = require_vitepress.__toESM(fs, 1);
|
|
@@ -457,7 +458,8 @@ async function transformPm(html, options) {
|
|
|
457
458
|
* YouTube Plugin - Privacy-enhanced iframe embedding
|
|
458
459
|
*
|
|
459
460
|
* Transforms <YouTube> components into responsive iframe embeds using
|
|
460
|
-
* youtube-nocookie.com for enhanced privacy.
|
|
461
|
+
* youtube-nocookie.com for enhanced privacy. A digits-only `start` attribute
|
|
462
|
+
* becomes `?start=` on the iframe URL.
|
|
461
463
|
*
|
|
462
464
|
* The HTML rewrite is performed in Rust (`transformYoutubeEmbeds` in
|
|
463
465
|
* @ox-content/napi), replacing the previous rehype parse/stringify
|
|
@@ -488,7 +490,7 @@ async function transformYouTube(html, options) {
|
|
|
488
490
|
}
|
|
489
491
|
//#endregion
|
|
490
492
|
//#region src/plugins/twitter/url.ts
|
|
491
|
-
const STATUS_PATH = /^\/(?:[^/]+|i\/web)\/status\/(\d+)(?:\/.*)?$/;
|
|
493
|
+
const STATUS_PATH$1 = /^\/(?:[^/]+|i\/web)\/status\/(\d+)(?:\/.*)?$/;
|
|
492
494
|
function createSyndicationToken(id) {
|
|
493
495
|
return (Number(id) / 0x38d7ea4c68000 * Math.PI).toString(36).replaceAll(/(0+|\.)/g, "");
|
|
494
496
|
}
|
|
@@ -502,7 +504,7 @@ function parseTweetReference(value) {
|
|
|
502
504
|
const url = new URL(trimmed);
|
|
503
505
|
const hostname = url.hostname.toLowerCase().replace(/^(?:www\.|mobile\.)/, "");
|
|
504
506
|
if (url.protocol !== "https:" || hostname !== "x.com" && hostname !== "twitter.com") return null;
|
|
505
|
-
const match = url.pathname.match(STATUS_PATH);
|
|
507
|
+
const match = url.pathname.match(STATUS_PATH$1);
|
|
506
508
|
if (!match) return null;
|
|
507
509
|
const screenName = url.pathname.startsWith("/i/web/status/") ? "i/web" : url.pathname.split("/")[1];
|
|
508
510
|
return {
|
|
@@ -513,10 +515,164 @@ function parseTweetReference(value) {
|
|
|
513
515
|
return null;
|
|
514
516
|
}
|
|
515
517
|
}
|
|
516
|
-
function
|
|
518
|
+
function tweetElementAttributes(attributes) {
|
|
517
519
|
const values = /* @__PURE__ */ new Map();
|
|
518
|
-
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] ?? "");
|
|
519
|
-
|
|
520
|
+
for (const match of attributes.matchAll(/\b(url|href|id|appearance)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) values.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? "");
|
|
521
|
+
const appearance = values.get("appearance");
|
|
522
|
+
return {
|
|
523
|
+
reference: parseTweetReference(values.get("url") ?? values.get("href") ?? values.get("id") ?? ""),
|
|
524
|
+
appearance: appearance === "full" || appearance === "compact" ? appearance : void 0
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/plugins/twitter/validate.ts
|
|
529
|
+
const SCREEN_NAME = /^[A-Za-z0-9_]{1,15}$/;
|
|
530
|
+
const STATUS_ID = /^\d+$/;
|
|
531
|
+
const STATUS_PATH = /(?:x\.com|twitter\.com)\/(?:[^/]+|i\/web)\/status\/(\d+)/i;
|
|
532
|
+
const TCO = /^https?:\/\/t\.co\/[A-Za-z0-9]+$/i;
|
|
533
|
+
function isTweetData(data) {
|
|
534
|
+
return isTweetBodyData(data);
|
|
535
|
+
}
|
|
536
|
+
function parseTweetData(data) {
|
|
537
|
+
return isTweetData(data) ? normalizeTweetData(data) : null;
|
|
538
|
+
}
|
|
539
|
+
function isTweetBodyData(data) {
|
|
540
|
+
if (!data || typeof data !== "object") return false;
|
|
541
|
+
const value = data;
|
|
542
|
+
return typeof value.text === "string" && isTweetUser(value.user);
|
|
543
|
+
}
|
|
544
|
+
function isTweetUser(value) {
|
|
545
|
+
if (!value || typeof value !== "object") return false;
|
|
546
|
+
const user = value;
|
|
547
|
+
return typeof user.name === "string" && typeof user.screen_name === "string";
|
|
548
|
+
}
|
|
549
|
+
function normalizeTweetData(data) {
|
|
550
|
+
const handle = sanitizeScreenName(data.in_reply_to_screen_name);
|
|
551
|
+
return {
|
|
552
|
+
...data,
|
|
553
|
+
quoted_tweet: isTweetBodyData(data.quoted_tweet) ? stripNestedQuote(data.quoted_tweet) : void 0,
|
|
554
|
+
in_reply_to_screen_name: handle,
|
|
555
|
+
in_reply_to_status_id_str: handle ? sanitizeStatusId(data.in_reply_to_status_id_str) : void 0
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function stripNestedQuote(data) {
|
|
559
|
+
const { quoted_tweet: _nested, ...quoted } = data;
|
|
560
|
+
return quoted;
|
|
561
|
+
}
|
|
562
|
+
function sanitizeScreenName(value) {
|
|
563
|
+
return value && SCREEN_NAME.test(value) ? value : void 0;
|
|
564
|
+
}
|
|
565
|
+
function sanitizeStatusId(value) {
|
|
566
|
+
return value && STATUS_ID.test(value) ? value : void 0;
|
|
567
|
+
}
|
|
568
|
+
function quotedPermalink(quoted) {
|
|
569
|
+
const id = sanitizeStatusId(quoted.id_str);
|
|
570
|
+
if (!id) return void 0;
|
|
571
|
+
return `https://x.com/${sanitizeScreenName(quoted.user.screen_name) ?? "i/web"}/status/${id}`;
|
|
572
|
+
}
|
|
573
|
+
function replyPermalink(data) {
|
|
574
|
+
const handle = sanitizeScreenName(data.in_reply_to_screen_name);
|
|
575
|
+
if (!handle) return void 0;
|
|
576
|
+
const id = sanitizeStatusId(data.in_reply_to_status_id_str);
|
|
577
|
+
return id ? `https://x.com/${handle}/status/${id}` : `https://x.com/${handle}`;
|
|
578
|
+
}
|
|
579
|
+
function visibleTextRange(data, omitTrailingQuoteUrl = false) {
|
|
580
|
+
const start = Math.max(0, data.display_text_range?.[0] ?? 0);
|
|
581
|
+
let end = Math.min(data.text.length, data.display_text_range?.[1] ?? data.text.length);
|
|
582
|
+
if (!omitTrailingQuoteUrl || start >= end) return [start, end];
|
|
583
|
+
const quoted = "quoted_tweet" in data ? data.quoted_tweet : void 0;
|
|
584
|
+
for (const entity of data.entities?.urls ?? []) {
|
|
585
|
+
const indices = entity.indices;
|
|
586
|
+
if (!indices || !isQuoteUrlEntity(entity, quoted)) continue;
|
|
587
|
+
const [entityStart, entityEnd] = indices;
|
|
588
|
+
if (entityStart >= start && isTrailingEntity(entityEnd, end, data.text)) end = Math.min(end, entityStart);
|
|
589
|
+
}
|
|
590
|
+
while (end > start && isUtf16Space(data.text, end - 1)) end -= 1;
|
|
591
|
+
return [start, end];
|
|
592
|
+
}
|
|
593
|
+
function isQuoteUrlEntity(entity, quoted) {
|
|
594
|
+
for (const href of [
|
|
595
|
+
entity.expanded_url,
|
|
596
|
+
entity.url,
|
|
597
|
+
entity.display_url
|
|
598
|
+
]) {
|
|
599
|
+
if (!href) continue;
|
|
600
|
+
const match = href.match(STATUS_PATH);
|
|
601
|
+
if (match) return !quoted?.id_str || match[1] === quoted.id_str;
|
|
602
|
+
if (TCO.test(href)) return true;
|
|
603
|
+
}
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
function isTrailingEntity(entityEnd, rangeEnd, text) {
|
|
607
|
+
if (entityEnd >= rangeEnd || entityEnd === text.length) return true;
|
|
608
|
+
return entityEnd > 0 && /^[\t\n\r ]*$/.test(text.slice(entityEnd, rangeEnd));
|
|
609
|
+
}
|
|
610
|
+
function isUtf16Space(text, index) {
|
|
611
|
+
const char = text[index];
|
|
612
|
+
return char === " " || char === "\n" || char === " " || char === "\r";
|
|
613
|
+
}
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/plugins/twitter/video.ts
|
|
616
|
+
const VIDEO_HOSTS = /* @__PURE__ */ new Set(["pbs.twimg.com", "video.twimg.com"]);
|
|
617
|
+
function selectBestMp4Url(variants) {
|
|
618
|
+
const candidates = (variants ?? []).filter((variant) => isVideoMp4Type(variant.content_type) && isAllowedVideoUrl(variant.url));
|
|
619
|
+
if (candidates.length === 0) return void 0;
|
|
620
|
+
return candidates.reduce((best, variant) => {
|
|
621
|
+
const bestBitrate = best.bitrate ?? Number.NEGATIVE_INFINITY;
|
|
622
|
+
const nextBitrate = variant.bitrate ?? Number.NEGATIVE_INFINITY;
|
|
623
|
+
if (nextBitrate > bestBitrate) return variant;
|
|
624
|
+
if (nextBitrate === bestBitrate && variant.url < best.url) return variant;
|
|
625
|
+
return best;
|
|
626
|
+
}).url;
|
|
627
|
+
}
|
|
628
|
+
function isVideoMp4Type(value) {
|
|
629
|
+
return (value ?? "").split(";", 1)[0].trim().toLowerCase() === "video/mp4";
|
|
630
|
+
}
|
|
631
|
+
function isAllowedVideoUrl(value) {
|
|
632
|
+
if (!value) return false;
|
|
633
|
+
try {
|
|
634
|
+
const url = new URL(value);
|
|
635
|
+
return url.protocol === "https:" && VIDEO_HOSTS.has(url.hostname.toLowerCase());
|
|
636
|
+
} catch {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function downloadVideoAsset(source, basename, options) {
|
|
641
|
+
if (!isAllowedVideoUrl(source)) return void 0;
|
|
642
|
+
const filename = `${sanitizeFilename(basename)}.mp4`;
|
|
643
|
+
const output = node_path.default.join(options.mediaOutputDir, filename);
|
|
644
|
+
const publicPath = joinPublicPath$1(options.mediaPublicPath, filename);
|
|
645
|
+
try {
|
|
646
|
+
await (0, node_fs_promises.access)(output);
|
|
647
|
+
return publicPath;
|
|
648
|
+
} catch {}
|
|
649
|
+
const controller = new AbortController();
|
|
650
|
+
const timeout = setTimeout(() => controller.abort(), options.timeout);
|
|
651
|
+
try {
|
|
652
|
+
const response = await fetch(source, {
|
|
653
|
+
headers: { Accept: "video/mp4" },
|
|
654
|
+
signal: controller.signal
|
|
655
|
+
});
|
|
656
|
+
if (!response.ok) return void 0;
|
|
657
|
+
if (!isVideoMp4Type(response.headers?.get("content-type"))) return void 0;
|
|
658
|
+
const declared = Number(response.headers?.get("content-length"));
|
|
659
|
+
if (Number.isFinite(declared) && declared > options.maxVideoBytes) return void 0;
|
|
660
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
661
|
+
if (bytes.byteLength > options.maxVideoBytes) return void 0;
|
|
662
|
+
await (0, node_fs_promises.mkdir)(options.mediaOutputDir, { recursive: true });
|
|
663
|
+
await (0, node_fs_promises.writeFile)(output, bytes);
|
|
664
|
+
return publicPath;
|
|
665
|
+
} catch {
|
|
666
|
+
return;
|
|
667
|
+
} finally {
|
|
668
|
+
clearTimeout(timeout);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function sanitizeFilename(value) {
|
|
672
|
+
return value.replaceAll(/[^a-zA-Z0-9_-]/g, "-") || "video";
|
|
673
|
+
}
|
|
674
|
+
function joinPublicPath$1(prefix, filename) {
|
|
675
|
+
return `${prefix.replace(/\/$/, "")}/${filename}`;
|
|
520
676
|
}
|
|
521
677
|
//#endregion
|
|
522
678
|
//#region src/plugins/twitter/fetch.ts
|
|
@@ -544,8 +700,8 @@ async function fetchTweetData(id, options) {
|
|
|
544
700
|
signal: controller.signal
|
|
545
701
|
});
|
|
546
702
|
if (!response.ok) return null;
|
|
547
|
-
const data = await response.json();
|
|
548
|
-
if (!
|
|
703
|
+
const data = parseTweetData(await response.json());
|
|
704
|
+
if (!data) return null;
|
|
549
705
|
if (options.cache) {
|
|
550
706
|
tweetCache.set(key, data);
|
|
551
707
|
await writeCachedTweet(key, data, options.cacheDir);
|
|
@@ -558,15 +714,29 @@ async function fetchTweetData(id, options) {
|
|
|
558
714
|
}
|
|
559
715
|
}
|
|
560
716
|
async function materializeTweetAssets(id, data, options) {
|
|
717
|
+
const assets = await materializeBodyAssets(id, data, options);
|
|
718
|
+
if (data.quoted_tweet) assets.quoted = await materializeBodyAssets(`${id}-quoted`, data.quoted_tweet, options);
|
|
719
|
+
return assets;
|
|
720
|
+
}
|
|
721
|
+
async function materializeBodyAssets(id, data, options) {
|
|
561
722
|
const assets = { media: [] };
|
|
562
723
|
const avatarUrl = data.user.profile_image_url_https?.replace(/_normal(?=\.[^.]+$)/, "_bigger");
|
|
563
724
|
if (avatarUrl) assets.avatar = await downloadAsset(avatarUrl, `${id}-avatar`, options);
|
|
564
725
|
const media = data.mediaDetails ?? data.entities?.media ?? [];
|
|
565
726
|
for (const [index, item] of media.entries()) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
727
|
+
const kind = item.type === "video" || item.type === "animated_gif" ? item.type : "photo";
|
|
728
|
+
const basename = `${id}-media-${index + 1}`;
|
|
729
|
+
if (kind === "photo") {
|
|
730
|
+
if (item.type && item.type !== "photo") continue;
|
|
731
|
+
if (!item.media_url_https) continue;
|
|
732
|
+
const src = await downloadAsset(item.media_url_https, basename, options);
|
|
733
|
+
if (src) assets.media.push(assetRecord("photo", src, item));
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const poster = item.media_url_https ? await downloadAsset(item.media_url_https, `${basename}-poster`, options) : void 0;
|
|
737
|
+
const videoUrl = options.downloadVideo ? selectBestMp4Url(item.video_info?.variants) : void 0;
|
|
738
|
+
const src = videoUrl ? await downloadVideoAsset(videoUrl, basename, options) : void 0;
|
|
739
|
+
assets.media.push(assetRecord(kind, src, item, poster));
|
|
570
740
|
}
|
|
571
741
|
return assets;
|
|
572
742
|
}
|
|
@@ -596,8 +766,7 @@ async function downloadAsset(source, basename, options) {
|
|
|
596
766
|
}
|
|
597
767
|
async function readCachedTweet(key, directory) {
|
|
598
768
|
try {
|
|
599
|
-
|
|
600
|
-
return isTweetData(data) ? data : null;
|
|
769
|
+
return parseTweetData(JSON.parse(await (0, node_fs_promises.readFile)(node_path.default.join(directory, `${key}.json`), "utf8")));
|
|
601
770
|
} catch {
|
|
602
771
|
return null;
|
|
603
772
|
}
|
|
@@ -608,11 +777,6 @@ async function writeCachedTweet(key, data, directory) {
|
|
|
608
777
|
await (0, node_fs_promises.writeFile)(node_path.default.join(directory, `${key}.json`), `${JSON.stringify(data)}\n`);
|
|
609
778
|
} catch {}
|
|
610
779
|
}
|
|
611
|
-
function isTweetData(data) {
|
|
612
|
-
if (!data || typeof data !== "object") return false;
|
|
613
|
-
const value = data;
|
|
614
|
-
return typeof value.text === "string" && Boolean(value.user) && typeof value.user?.name === "string" && typeof value.user.screen_name === "string";
|
|
615
|
-
}
|
|
616
780
|
function extensionFromUrl(url) {
|
|
617
781
|
const match = url.pathname.match(/\.(jpe?g|png|webp|gif)$/i);
|
|
618
782
|
return match ? `.${match[1].toLowerCase().replace("jpeg", "jpg")}` : ".jpg";
|
|
@@ -623,39 +787,64 @@ function joinPublicPath(prefix, filename) {
|
|
|
623
787
|
function sanitizeSegment(value) {
|
|
624
788
|
return value.replaceAll(/[^a-zA-Z0-9_-]/g, "-");
|
|
625
789
|
}
|
|
626
|
-
function assetRecord(src, media) {
|
|
790
|
+
function assetRecord(kind, src, media, poster) {
|
|
627
791
|
return {
|
|
792
|
+
kind,
|
|
628
793
|
src,
|
|
794
|
+
poster,
|
|
629
795
|
alt: media.ext_alt_text,
|
|
630
796
|
width: media.original_info?.width,
|
|
631
797
|
height: media.original_info?.height
|
|
632
798
|
};
|
|
633
799
|
}
|
|
634
800
|
//#endregion
|
|
635
|
-
//#region src/plugins/twitter/
|
|
636
|
-
function
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
return
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
801
|
+
//#region src/plugins/twitter/html.ts
|
|
802
|
+
function escapeText(value) {
|
|
803
|
+
return escapeHtml$6(value).replaceAll("\n", "<br>");
|
|
804
|
+
}
|
|
805
|
+
function escapeAttribute$3(value) {
|
|
806
|
+
return escapeHtml$6(value).replaceAll("`", "`");
|
|
807
|
+
}
|
|
808
|
+
function escapeHtml$6(value) {
|
|
809
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/plugins/twitter/markup.ts
|
|
813
|
+
function renderMedia(assets, permalink) {
|
|
814
|
+
if (assets.media.length === 0) return "";
|
|
815
|
+
const items = assets.media.map((item) => renderMediaItem(item, permalink)).join("");
|
|
816
|
+
return `<div class="ox-tweet__media" data-count="${assets.media.length}">${items}</div>`;
|
|
817
|
+
}
|
|
818
|
+
function renderMediaItem(item, permalink) {
|
|
819
|
+
if (item.kind === "video" || item.kind === "animated_gif") return renderVideoItem(item, permalink);
|
|
820
|
+
const size = sizeAttributes(item);
|
|
821
|
+
return `<img class="ox-tweet__media-item" src="${escapeAttribute$3(item.src ?? "")}" alt="${escapeAttribute$3(item.alt ?? "")}"${size} loading="lazy" decoding="async">`;
|
|
656
822
|
}
|
|
657
|
-
function
|
|
658
|
-
const
|
|
823
|
+
function renderVideoItem(item, permalink) {
|
|
824
|
+
const watch = watchOnX(permalink);
|
|
825
|
+
const size = sizeAttributes(item);
|
|
826
|
+
const src = selfHostedMediaSrc(item.src);
|
|
827
|
+
if (src) {
|
|
828
|
+
const poster = item.poster ? ` poster="${escapeAttribute$3(item.poster)}"` : "";
|
|
829
|
+
const gif = item.kind === "animated_gif" ? " muted loop" : "";
|
|
830
|
+
return `<video class="ox-tweet__media-item" src="${escapeAttribute$3(src)}"${poster}${size} controls playsinline preload="none"${gif}>${watch}</video>`;
|
|
831
|
+
}
|
|
832
|
+
return `<div class="ox-tweet__media-item ox-tweet__media-fallback">${item.poster ? `<img src="${escapeAttribute$3(item.poster)}" alt="${escapeAttribute$3(item.alt ?? "")}"${size} loading="lazy" decoding="async">` : ""}${watch}</div>`;
|
|
833
|
+
}
|
|
834
|
+
function selfHostedMediaSrc(src) {
|
|
835
|
+
return src && !src.includes("video.twimg.com") ? src : void 0;
|
|
836
|
+
}
|
|
837
|
+
function sizeAttributes(item) {
|
|
838
|
+
return [item.width ? ` width="${item.width}"` : "", item.height ? ` height="${item.height}"` : ""].join("");
|
|
839
|
+
}
|
|
840
|
+
function watchOnX(permalink) {
|
|
841
|
+
if (!permalink) return "";
|
|
842
|
+
return `<a class="ox-tweet__watch" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer">Watch on X</a>`;
|
|
843
|
+
}
|
|
844
|
+
//#endregion
|
|
845
|
+
//#region src/plugins/twitter/text.ts
|
|
846
|
+
function renderTweetText(data, options) {
|
|
847
|
+
const [start, end] = visibleTextRange(data, options?.omitTrailingQuoteUrl === true);
|
|
659
848
|
const entities = collectEntities(data).filter((entity) => validRange(entity.indices, start, end)).sort((left, right) => left.indices[0] - right.indices[0]);
|
|
660
849
|
let cursor = start;
|
|
661
850
|
let output = "";
|
|
@@ -663,10 +852,9 @@ function renderTweetText(data) {
|
|
|
663
852
|
const [entityStart, entityEnd] = entity.indices;
|
|
664
853
|
if (entityStart < cursor) continue;
|
|
665
854
|
output += escapeText(data.text.slice(cursor, entityStart));
|
|
666
|
-
if (entity.
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
output += `<a href="${escapeAttribute$2(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(label)}</a>`;
|
|
855
|
+
if (entity.href) {
|
|
856
|
+
const label = entity.label ?? data.text.slice(entityStart, entityEnd);
|
|
857
|
+
output += `<a href="${escapeAttribute$3(entity.href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(label)}</a>`;
|
|
670
858
|
}
|
|
671
859
|
cursor = entityEnd;
|
|
672
860
|
}
|
|
@@ -674,27 +862,228 @@ function renderTweetText(data) {
|
|
|
674
862
|
return output.trim();
|
|
675
863
|
}
|
|
676
864
|
function collectEntities(data) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
kind: "url"
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
})
|
|
865
|
+
const collected = [];
|
|
866
|
+
for (const entity of data.entities?.urls ?? []) collected.push({
|
|
867
|
+
kind: "url",
|
|
868
|
+
indices: entity.indices,
|
|
869
|
+
href: entity.expanded_url ?? entity.url,
|
|
870
|
+
label: entity.display_url ?? entity.expanded_url ?? entity.url
|
|
871
|
+
});
|
|
872
|
+
for (const entity of data.entities?.media ?? []) collected.push({
|
|
873
|
+
kind: "media",
|
|
874
|
+
indices: entity.indices
|
|
875
|
+
});
|
|
876
|
+
for (const entity of data.entities?.hashtags ?? []) {
|
|
877
|
+
if (!entity.text) continue;
|
|
878
|
+
collected.push({
|
|
879
|
+
kind: "hashtag",
|
|
880
|
+
indices: entity.indices,
|
|
881
|
+
href: `https://x.com/hashtag/${encodeURIComponent(entity.text)}`
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
for (const entity of data.entities?.user_mentions ?? []) {
|
|
885
|
+
const screen = sanitizeScreenName(entity.screen_name);
|
|
886
|
+
if (!screen) continue;
|
|
887
|
+
collected.push({
|
|
888
|
+
kind: "mention",
|
|
889
|
+
indices: entity.indices,
|
|
890
|
+
href: `https://x.com/${encodeURIComponent(screen)}`
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
for (const entity of data.entities?.symbols ?? []) {
|
|
894
|
+
if (!entity.text) continue;
|
|
895
|
+
collected.push({
|
|
896
|
+
kind: "symbol",
|
|
897
|
+
indices: entity.indices,
|
|
898
|
+
href: `https://x.com/search?q=%24${encodeURIComponent(entity.text)}`
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return collected;
|
|
684
902
|
}
|
|
685
903
|
function validRange(indices, start, end) {
|
|
686
904
|
return Boolean(indices && indices[0] >= start && indices[1] <= end && indices[0] < indices[1]);
|
|
687
905
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/plugins/twitter/full.ts
|
|
908
|
+
const HELP_HREF = "https://help.x.com/en/x-for-websites-ads-info-and-privacy";
|
|
909
|
+
function renderFullTweet(permalink, data, assets) {
|
|
910
|
+
const quote = data.quoted_tweet ? renderFullQuote(data.quoted_tweet, assets.quoted) : "";
|
|
911
|
+
return [
|
|
912
|
+
"<figure class=\"ox-tweet ox-tweet--fetched ox-tweet--full\">",
|
|
913
|
+
renderFullHeader(data.user, assets.avatar, permalink),
|
|
914
|
+
renderReply$1(data),
|
|
915
|
+
`<div class="ox-tweet__body">${renderTweetText(data, { omitTrailingQuoteUrl: Boolean(quote) })}</div>`,
|
|
916
|
+
renderMedia(assets, permalink),
|
|
917
|
+
quote,
|
|
918
|
+
renderInfo(permalink, data.created_at),
|
|
919
|
+
renderActions(permalink, data),
|
|
920
|
+
renderReplies(permalink, data.conversation_count),
|
|
921
|
+
"</figure>"
|
|
922
|
+
].join("");
|
|
923
|
+
}
|
|
924
|
+
function renderFullQuote(data, assets) {
|
|
925
|
+
const permalink = quotedPermalink(data) ?? "";
|
|
926
|
+
return [
|
|
927
|
+
"<blockquote class=\"ox-tweet__quote\">",
|
|
928
|
+
renderQuoteHeader(data.user, assets?.avatar, permalink),
|
|
929
|
+
`<div class="ox-tweet__quote-body">${renderTweetText(data)}</div>`,
|
|
930
|
+
renderMedia(assets ?? { media: [] }, permalink),
|
|
931
|
+
"</blockquote>"
|
|
932
|
+
].join("");
|
|
933
|
+
}
|
|
934
|
+
function renderFullHeader(user, avatarSrc, permalink) {
|
|
935
|
+
const profile = profileHref(user);
|
|
936
|
+
const follow = followHref(user);
|
|
937
|
+
return [
|
|
938
|
+
"<header class=\"ox-tweet__header\">",
|
|
939
|
+
`<a class="ox-tweet__avatar-link" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">`,
|
|
940
|
+
avatar(avatarSrc, 48),
|
|
941
|
+
"</a>",
|
|
942
|
+
"<div class=\"ox-tweet__author\">",
|
|
943
|
+
`<a class="ox-tweet__author-name" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(user.name)}${verifiedBadge(user)}</a>`,
|
|
944
|
+
"<div class=\"ox-tweet__author-meta\">",
|
|
945
|
+
`<a class="ox-tweet__author-handle" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">@${escapeHtml$6(user.screen_name)}</a>`,
|
|
946
|
+
follow ? `<span class="ox-tweet__sep" aria-hidden="true">·</span><a class="ox-tweet__follow" href="${escapeAttribute$3(follow)}" target="_blank" rel="noopener noreferrer">Follow</a>` : "",
|
|
947
|
+
"</div></div>",
|
|
948
|
+
`<a class="ox-tweet__brand" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer" aria-label="View on X"><span class="ox-tweet__icon ox-tweet__icon--x"></span></a>`,
|
|
949
|
+
"</header>"
|
|
950
|
+
].join("");
|
|
951
|
+
}
|
|
952
|
+
function renderQuoteHeader(user, avatarSrc, permalink) {
|
|
953
|
+
return [
|
|
954
|
+
"<header class=\"ox-tweet__quote-header\">",
|
|
955
|
+
`<a class="ox-tweet__profile" href="${escapeAttribute$3(permalink || profileHref(user))}" target="_blank" rel="noopener noreferrer">`,
|
|
956
|
+
avatar(avatarSrc, 20),
|
|
957
|
+
`<span class="ox-tweet__author-name">${escapeHtml$6(user.name)}${verifiedBadge(user)}</span>`,
|
|
958
|
+
`<span class="ox-tweet__author-handle">@${escapeHtml$6(user.screen_name)}</span>`,
|
|
959
|
+
"</a></header>"
|
|
960
|
+
].join("");
|
|
961
|
+
}
|
|
962
|
+
function renderReply$1(data) {
|
|
963
|
+
const href = replyPermalink(data);
|
|
964
|
+
const handle = data.in_reply_to_screen_name;
|
|
965
|
+
if (!href || !handle) return "";
|
|
966
|
+
return `<p class="ox-tweet__reply"><a class="ox-tweet__reply-link" href="${escapeAttribute$3(href)}" target="_blank" rel="noopener noreferrer">Replying to @${escapeHtml$6(handle)}</a></p>`;
|
|
967
|
+
}
|
|
968
|
+
function renderInfo(permalink, createdAt) {
|
|
969
|
+
const formatted = formatFullDate(createdAt);
|
|
970
|
+
return `<div class="ox-tweet__info">${formatted ? `<a class="ox-tweet__permalink" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer"><time datetime="${formatted.iso}">${escapeHtml$6(formatted.label)}</time></a>` : `<a class="ox-tweet__permalink" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer">View on X</a>`}<a class="ox-tweet__info-help" href="${HELP_HREF}" target="_blank" rel="noopener noreferrer" aria-label="X for Websites, Ads Information and Privacy"><span class="ox-tweet__icon ox-tweet__icon--info"></span></a></div>`;
|
|
971
|
+
}
|
|
972
|
+
function renderActions(permalink, data) {
|
|
973
|
+
const id = statusId(data, permalink);
|
|
974
|
+
if (!id) return "";
|
|
975
|
+
return [
|
|
976
|
+
"<div class=\"ox-tweet__actions\">",
|
|
977
|
+
`<a class="ox-tweet__action ox-tweet__action--like" href="https://x.com/intent/like?tweet_id=${id}" target="_blank" rel="noopener noreferrer"><span class="ox-tweet__icon ox-tweet__icon--like"></span><span>${formatCount(data.favorite_count)}</span></a>`,
|
|
978
|
+
`<a class="ox-tweet__action ox-tweet__action--reply" href="https://x.com/intent/tweet?in_reply_to=${id}" target="_blank" rel="noopener noreferrer"><span class="ox-tweet__icon ox-tweet__icon--reply"></span>Reply</a>`,
|
|
979
|
+
"</div>"
|
|
980
|
+
].join("");
|
|
981
|
+
}
|
|
982
|
+
function renderReplies(permalink, conversationCount) {
|
|
983
|
+
return `<p class="ox-tweet__replies"><a class="ox-tweet__replies-link" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(repliesLabel(conversationCount))}</a></p>`;
|
|
984
|
+
}
|
|
985
|
+
function avatar(src, size) {
|
|
986
|
+
return src ? `<img class="ox-tweet__avatar" src="${escapeAttribute$3(src)}" alt="" width="${size}" height="${size}" loading="lazy" decoding="async">` : "";
|
|
987
|
+
}
|
|
988
|
+
function verifiedBadge(user) {
|
|
989
|
+
const kind = verifiedKind(user);
|
|
990
|
+
return kind ? `<span class="ox-tweet__badge ox-tweet__badge--${kind}" title="Verified"></span>` : "";
|
|
991
|
+
}
|
|
992
|
+
function verifiedKind(user) {
|
|
993
|
+
if (user.verified_type === "Government") return "gray";
|
|
994
|
+
if (user.verified_type === "Business") return "gold";
|
|
995
|
+
if (user.is_blue_verified) return "blue";
|
|
996
|
+
if (user.verified) return "gray";
|
|
997
|
+
}
|
|
998
|
+
function profileHref(user) {
|
|
999
|
+
const screen = sanitizeScreenName(user.screen_name) ?? user.screen_name;
|
|
1000
|
+
return `https://x.com/${encodeURIComponent(screen)}`;
|
|
1001
|
+
}
|
|
1002
|
+
function followHref(user) {
|
|
1003
|
+
const screen = sanitizeScreenName(user.screen_name);
|
|
1004
|
+
return screen ? `https://x.com/intent/follow?screen_name=${encodeURIComponent(screen)}` : void 0;
|
|
1005
|
+
}
|
|
1006
|
+
function statusId(data, permalink) {
|
|
1007
|
+
return sanitizeStatusId(data.id_str) ?? permalink.match(/\/status\/(\d+)/)?.[1];
|
|
1008
|
+
}
|
|
1009
|
+
function formatCount(value) {
|
|
1010
|
+
const n = typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0;
|
|
1011
|
+
if (n > 999999) return `${(n / 1e6).toFixed(1)}M`;
|
|
1012
|
+
if (n > 999) return `${(n / 1e3).toFixed(1)}K`;
|
|
1013
|
+
return String(n);
|
|
1014
|
+
}
|
|
1015
|
+
function repliesLabel(value) {
|
|
1016
|
+
const n = typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1017
|
+
if (n === 0) return "Read more on X";
|
|
1018
|
+
if (n === 1) return "Read 1 reply";
|
|
1019
|
+
return `Read ${formatCount(n)} replies`;
|
|
1020
|
+
}
|
|
1021
|
+
function formatFullDate(createdAt) {
|
|
1022
|
+
if (!createdAt) return void 0;
|
|
1023
|
+
const date = new Date(createdAt);
|
|
1024
|
+
if (Number.isNaN(date.valueOf())) return void 0;
|
|
1025
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
1026
|
+
hour: "numeric",
|
|
1027
|
+
minute: "2-digit",
|
|
1028
|
+
hour12: true,
|
|
1029
|
+
month: "short",
|
|
1030
|
+
day: "numeric",
|
|
1031
|
+
year: "numeric",
|
|
1032
|
+
timeZone: "UTC"
|
|
1033
|
+
}).formatToParts(date);
|
|
1034
|
+
const get = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
1035
|
+
return {
|
|
1036
|
+
iso: date.toISOString(),
|
|
1037
|
+
label: `${get("hour")}:${get("minute")} ${get("dayPeriod")} · ${get("month")} ${get("day")}, ${get("year")}`
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/plugins/twitter/render.ts
|
|
1042
|
+
function renderFetchedTweet(permalink, data, assets, options) {
|
|
1043
|
+
if (options.appearance === "full") return renderFullTweet(permalink, data, assets);
|
|
1044
|
+
const quote = data.quoted_tweet ? renderQuotedTweet(data.quoted_tweet, assets.quoted) : "";
|
|
1045
|
+
return [
|
|
1046
|
+
"<figure class=\"ox-tweet ox-tweet--fetched\">",
|
|
1047
|
+
renderHeader(data.user, assets.avatar),
|
|
1048
|
+
renderReply(data),
|
|
1049
|
+
`<div class="ox-tweet__body">${renderTweetText(data, { omitTrailingQuoteUrl: Boolean(quote) })}</div>`,
|
|
1050
|
+
renderMedia(assets, permalink),
|
|
1051
|
+
quote,
|
|
1052
|
+
renderFooter(permalink, data.created_at, options.lang),
|
|
1053
|
+
"</figure>"
|
|
1054
|
+
].join("");
|
|
1055
|
+
}
|
|
1056
|
+
function renderQuotedTweet(data, assets) {
|
|
1057
|
+
const permalink = quotedPermalink(data) ?? "";
|
|
1058
|
+
return [
|
|
1059
|
+
"<blockquote class=\"ox-tweet__quote\">",
|
|
1060
|
+
renderHeader(data.user, assets?.avatar, permalink || void 0, "ox-tweet__quote-header"),
|
|
1061
|
+
`<div class="ox-tweet__quote-body">${renderTweetText(data)}</div>`,
|
|
1062
|
+
renderMedia(assets ?? { media: [] }, permalink),
|
|
1063
|
+
"</blockquote>"
|
|
1064
|
+
].join("");
|
|
1065
|
+
}
|
|
1066
|
+
function renderHeader(user, avatarSrc, href, headerClass = "ox-tweet__header") {
|
|
1067
|
+
const screen = sanitizeScreenName(user.screen_name) ?? user.screen_name;
|
|
1068
|
+
const profile = href ?? `https://x.com/${encodeURIComponent(screen)}`;
|
|
1069
|
+
const avatar = avatarSrc ? `<img class="ox-tweet__avatar" src="${escapeAttribute$3(avatarSrc)}" alt="" width="48" height="48" loading="lazy" decoding="async">` : "";
|
|
1070
|
+
return [
|
|
1071
|
+
`<header class="${headerClass}">`,
|
|
1072
|
+
`<a class="ox-tweet__profile" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">`,
|
|
1073
|
+
avatar,
|
|
1074
|
+
`<span class="ox-tweet__author-name">${escapeHtml$6(user.name)}</span>`,
|
|
1075
|
+
`<span class="ox-tweet__author-handle">@${escapeHtml$6(user.screen_name)}</span>`,
|
|
1076
|
+
"</a></header>"
|
|
1077
|
+
].join("");
|
|
1078
|
+
}
|
|
1079
|
+
function renderReply(data) {
|
|
1080
|
+
const href = replyPermalink(data);
|
|
1081
|
+
const handle = data.in_reply_to_screen_name;
|
|
1082
|
+
if (!href || !handle) return "";
|
|
1083
|
+
return `<p class="ox-tweet__reply"><a class="ox-tweet__reply-link" href="${escapeAttribute$3(href)}" target="_blank" rel="noopener noreferrer">Replying to @${escapeHtml$6(handle)}</a></p>`;
|
|
695
1084
|
}
|
|
696
1085
|
function renderFooter(permalink, createdAt, lang) {
|
|
697
|
-
if (!createdAt) return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$
|
|
1086
|
+
if (!createdAt) return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer">View on X</a></footer>`;
|
|
698
1087
|
const date = new Date(createdAt);
|
|
699
1088
|
if (Number.isNaN(date.valueOf())) return renderFooter(permalink, void 0, lang);
|
|
700
1089
|
const iso = date.toISOString();
|
|
@@ -710,16 +1099,7 @@ function renderFooter(permalink, createdAt, lang) {
|
|
|
710
1099
|
timeZone: "UTC"
|
|
711
1100
|
}).format(date);
|
|
712
1101
|
}
|
|
713
|
-
return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$
|
|
714
|
-
}
|
|
715
|
-
function escapeText(value) {
|
|
716
|
-
return escapeHtml$6(value).replaceAll("\n", "<br>");
|
|
717
|
-
}
|
|
718
|
-
function escapeAttribute$2(value) {
|
|
719
|
-
return escapeHtml$6(value).replaceAll("`", "`");
|
|
720
|
-
}
|
|
721
|
-
function escapeHtml$6(value) {
|
|
722
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1102
|
+
return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer"><time datetime="${iso}">${escapeHtml$6(label)}</time></a></footer>`;
|
|
723
1103
|
}
|
|
724
1104
|
//#endregion
|
|
725
1105
|
//#region src/plugins/twitter/transform.ts
|
|
@@ -732,7 +1112,10 @@ function resolveTwitterEmbedOptions$1(options) {
|
|
|
732
1112
|
cache: options.cache ?? true,
|
|
733
1113
|
cacheDir: node_path.default.resolve(options.cacheDir ?? ".cache/ox-content/twitter"),
|
|
734
1114
|
mediaOutputDir: node_path.default.resolve(options.mediaOutputDir ?? "public/ox-content/twitter"),
|
|
735
|
-
mediaPublicPath: options.mediaPublicPath ?? "/ox-content/twitter"
|
|
1115
|
+
mediaPublicPath: options.mediaPublicPath ?? "/ox-content/twitter",
|
|
1116
|
+
downloadVideo: options.downloadVideo ?? false,
|
|
1117
|
+
maxVideoBytes: options.maxVideoBytes ?? 8388608,
|
|
1118
|
+
appearance: options.appearance === "full" ? "full" : "compact"
|
|
736
1119
|
};
|
|
737
1120
|
}
|
|
738
1121
|
async function transformFetchedTweets(html, options) {
|
|
@@ -743,20 +1126,23 @@ async function transformFetchedTweets(html, options) {
|
|
|
743
1126
|
for (const match of html.matchAll(TWEET_ELEMENT)) {
|
|
744
1127
|
const index = match.index ?? 0;
|
|
745
1128
|
output += html.slice(cursor, index);
|
|
746
|
-
const
|
|
747
|
-
if (!reference) {
|
|
1129
|
+
const attrs = tweetElementAttributes(match[2]);
|
|
1130
|
+
if (!attrs.reference) {
|
|
748
1131
|
output += match[0];
|
|
749
1132
|
cursor = index + match[0].length;
|
|
750
1133
|
continue;
|
|
751
1134
|
}
|
|
752
|
-
const data = await fetchTweetData(reference.id, resolved);
|
|
1135
|
+
const data = await fetchTweetData(attrs.reference.id, resolved);
|
|
753
1136
|
if (!data) {
|
|
754
1137
|
output += match[0];
|
|
755
1138
|
cursor = index + match[0].length;
|
|
756
1139
|
continue;
|
|
757
1140
|
}
|
|
758
|
-
const assets = await materializeTweetAssets(reference.id, data, resolved);
|
|
759
|
-
output += renderFetchedTweet(reference.url, data, assets,
|
|
1141
|
+
const assets = await materializeTweetAssets(attrs.reference.id, data, resolved);
|
|
1142
|
+
output += renderFetchedTweet(attrs.reference.url, data, assets, {
|
|
1143
|
+
...resolved,
|
|
1144
|
+
appearance: attrs.appearance ?? resolved.appearance
|
|
1145
|
+
});
|
|
760
1146
|
cursor = index + match[0].length;
|
|
761
1147
|
}
|
|
762
1148
|
return output + html.slice(cursor);
|
|
@@ -899,7 +1285,7 @@ function inferLanguage(path) {
|
|
|
899
1285
|
}
|
|
900
1286
|
//#endregion
|
|
901
1287
|
//#region src/plugins/github/types.ts
|
|
902
|
-
const defaultOptions
|
|
1288
|
+
const defaultOptions = {
|
|
903
1289
|
token: "",
|
|
904
1290
|
cache: true,
|
|
905
1291
|
cacheTTL: 36e5,
|
|
@@ -1009,7 +1395,7 @@ async function fetchGitHubSource(source, options) {
|
|
|
1009
1395
|
*/
|
|
1010
1396
|
async function prefetchGitHubRepos(repos, options) {
|
|
1011
1397
|
const mergedOptions = {
|
|
1012
|
-
...defaultOptions
|
|
1398
|
+
...defaultOptions,
|
|
1013
1399
|
...options
|
|
1014
1400
|
};
|
|
1015
1401
|
const results = /* @__PURE__ */ new Map();
|
|
@@ -1024,7 +1410,7 @@ async function prefetchGitHubRepos(repos, options) {
|
|
|
1024
1410
|
*/
|
|
1025
1411
|
async function prefetchGitHubSources(sources, options) {
|
|
1026
1412
|
const mergedOptions = {
|
|
1027
|
-
...defaultOptions
|
|
1413
|
+
...defaultOptions,
|
|
1028
1414
|
...options
|
|
1029
1415
|
};
|
|
1030
1416
|
const results = /* @__PURE__ */ new Map();
|
|
@@ -1441,7 +1827,7 @@ function rehypeGitHub(repoDataMap, sourceDataMap, options) {
|
|
|
1441
1827
|
*/
|
|
1442
1828
|
async function transformGitHub(html, repoDataMap, options) {
|
|
1443
1829
|
const mergedOptions = {
|
|
1444
|
-
...defaultOptions
|
|
1830
|
+
...defaultOptions,
|
|
1445
1831
|
...options
|
|
1446
1832
|
};
|
|
1447
1833
|
let dataMap = repoDataMap;
|
|
@@ -1454,29 +1840,111 @@ async function transformGitHub(html, repoDataMap, options) {
|
|
|
1454
1840
|
//#region src/plugins/github.ts
|
|
1455
1841
|
var github_exports = /* @__PURE__ */ require_vitepress.__exportAll({ transformGitHub: () => transformGitHub });
|
|
1456
1842
|
//#endregion
|
|
1457
|
-
//#region src/plugins/ogp.ts
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1843
|
+
//#region src/plugins/ogp/cache.ts
|
|
1844
|
+
const memoryCache = /* @__PURE__ */ new Map();
|
|
1845
|
+
function ogpCacheFilePath(directory, key) {
|
|
1846
|
+
return node_path.default.join(directory, `${key}.json`);
|
|
1847
|
+
}
|
|
1848
|
+
function isFreshOgpEntry(cachedAt, ttl, now) {
|
|
1849
|
+
return now - cachedAt < ttl;
|
|
1850
|
+
}
|
|
1851
|
+
function parseOgpCacheEntry(value) {
|
|
1852
|
+
if (!value || typeof value !== "object") return null;
|
|
1853
|
+
const entry = value;
|
|
1854
|
+
if (entry.v !== 1) return null;
|
|
1855
|
+
if (typeof entry.url !== "string" || entry.url.length === 0) return null;
|
|
1856
|
+
if (typeof entry.cachedAt !== "number" || !Number.isFinite(entry.cachedAt)) return null;
|
|
1857
|
+
if (entry.data !== null && !isOgpData(entry.data)) return null;
|
|
1858
|
+
return {
|
|
1859
|
+
v: 1,
|
|
1860
|
+
url: entry.url,
|
|
1861
|
+
cachedAt: entry.cachedAt,
|
|
1862
|
+
data: entry.data
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
function readMemoryOgp(key, options, now) {
|
|
1866
|
+
const cached = memoryCache.get(key);
|
|
1867
|
+
if (!cached || !isFreshOgpEntry(cached.timestamp, options.cacheTTL, now)) return;
|
|
1868
|
+
return cached.data;
|
|
1869
|
+
}
|
|
1870
|
+
function writeMemoryOgp(key, data, timestamp, options) {
|
|
1871
|
+
if (data === null && !options.persistCache) return;
|
|
1872
|
+
memoryCache.set(key, {
|
|
1873
|
+
data,
|
|
1874
|
+
timestamp
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
async function readDiskOgp(key, options, now) {
|
|
1878
|
+
const file = ogpCacheFilePath(options.cacheDir, key);
|
|
1879
|
+
try {
|
|
1880
|
+
const entry = parseOgpCacheEntry(JSON.parse(await (0, node_fs_promises.readFile)(file, "utf8")));
|
|
1881
|
+
if (!entry) {
|
|
1882
|
+
await discardCorruptEntry(file);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (!isFreshOgpEntry(entry.cachedAt, options.cacheTTL, now)) return void 0;
|
|
1886
|
+
return entry.data;
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
if (isEnoent(error)) return void 0;
|
|
1889
|
+
await discardCorruptEntry(file);
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
async function writeDiskOgp(key, url, data, options, cachedAt) {
|
|
1894
|
+
const directory = options.cacheDir;
|
|
1895
|
+
const target = ogpCacheFilePath(directory, key);
|
|
1896
|
+
const temp = node_path.default.join(directory, `.${key}.${process.pid}.${(0, node_crypto.randomBytes)(8).toString("hex")}.tmp`);
|
|
1897
|
+
const entry = {
|
|
1898
|
+
v: 1,
|
|
1899
|
+
url,
|
|
1900
|
+
cachedAt,
|
|
1901
|
+
data
|
|
1902
|
+
};
|
|
1903
|
+
try {
|
|
1904
|
+
await (0, node_fs_promises.mkdir)(directory, { recursive: true });
|
|
1905
|
+
await (0, node_fs_promises.writeFile)(temp, `${JSON.stringify(entry)}\n`);
|
|
1906
|
+
try {
|
|
1907
|
+
await (0, node_fs_promises.rename)(temp, target);
|
|
1908
|
+
} catch {
|
|
1909
|
+
await (0, node_fs_promises.writeFile)(target, `${JSON.stringify(entry)}\n`);
|
|
1910
|
+
await (0, node_fs_promises.rm)(temp, { force: true });
|
|
1911
|
+
}
|
|
1912
|
+
} catch {
|
|
1913
|
+
await (0, node_fs_promises.rm)(temp, { force: true }).catch(() => void 0);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function isOgpData(value) {
|
|
1917
|
+
if (!value || typeof value !== "object") return false;
|
|
1918
|
+
const data = value;
|
|
1919
|
+
if (typeof data.url !== "string" || typeof data.title !== "string") return false;
|
|
1920
|
+
for (const field of [
|
|
1921
|
+
"description",
|
|
1922
|
+
"image",
|
|
1923
|
+
"siteName",
|
|
1924
|
+
"favicon"
|
|
1925
|
+
]) if (data[field] !== void 0 && typeof data[field] !== "string") return false;
|
|
1926
|
+
return true;
|
|
1927
|
+
}
|
|
1928
|
+
async function discardCorruptEntry(file) {
|
|
1929
|
+
console.warn(`Ignoring corrupt Open Graph cache entry ${file}`);
|
|
1930
|
+
await (0, node_fs_promises.rm)(file, { force: true }).catch(() => void 0);
|
|
1931
|
+
}
|
|
1932
|
+
function isEnoent(error) {
|
|
1933
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
1934
|
+
}
|
|
1935
|
+
function resolveOgpOptions(options = {}) {
|
|
1936
|
+
return {
|
|
1937
|
+
timeout: options.timeout ?? 1e4,
|
|
1938
|
+
cache: options.cache ?? true,
|
|
1939
|
+
cacheTTL: options.cacheTTL ?? 36e5,
|
|
1940
|
+
persistCache: options.persistCache ?? false,
|
|
1941
|
+
cacheDir: node_path.default.resolve(options.cacheDir ?? ".cache/ox-content/ogp"),
|
|
1942
|
+
refresh: options.refresh ?? false,
|
|
1943
|
+
userAgent: options.userAgent ?? "ox-content-ogp-bot/1.0 (compatible; +https://github.com/ubugeeei-prod/ox-content)"
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region src/plugins/ogp/url.ts
|
|
1480
1948
|
function isPrivateIPv4(hostname) {
|
|
1481
1949
|
const parts = hostname.split(".").map(Number);
|
|
1482
1950
|
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
@@ -1496,17 +1964,6 @@ function isSafeOgpUrl(value) {
|
|
|
1496
1964
|
return false;
|
|
1497
1965
|
}
|
|
1498
1966
|
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Get element attribute value.
|
|
1501
|
-
*/
|
|
1502
|
-
function getAttribute$1(el, name) {
|
|
1503
|
-
const value = el.properties?.[name];
|
|
1504
|
-
if (typeof value === "string") return value;
|
|
1505
|
-
if (Array.isArray(value)) return value.join(" ");
|
|
1506
|
-
}
|
|
1507
|
-
/**
|
|
1508
|
-
* Extract domain from URL.
|
|
1509
|
-
*/
|
|
1510
1967
|
function extractDomain(url) {
|
|
1511
1968
|
try {
|
|
1512
1969
|
return new URL(url).hostname;
|
|
@@ -1514,9 +1971,6 @@ function extractDomain(url) {
|
|
|
1514
1971
|
return url;
|
|
1515
1972
|
}
|
|
1516
1973
|
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Get favicon URL for a domain.
|
|
1519
|
-
*/
|
|
1520
1974
|
function getFaviconUrl(url) {
|
|
1521
1975
|
try {
|
|
1522
1976
|
return `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`;
|
|
@@ -1525,40 +1979,60 @@ function getFaviconUrl(url) {
|
|
|
1525
1979
|
}
|
|
1526
1980
|
}
|
|
1527
1981
|
/**
|
|
1528
|
-
*
|
|
1982
|
+
* Normalize a URL for cache keys: lowercase host, drop default ports and
|
|
1983
|
+
* fragments, and strip a trailing slash that is not the root path.
|
|
1529
1984
|
*/
|
|
1530
|
-
function
|
|
1531
|
-
|
|
1532
|
-
url
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
if (imageMatch) {
|
|
1541
|
-
let imageUrl = imageMatch[1];
|
|
1542
|
-
if (imageUrl.startsWith("/")) try {
|
|
1543
|
-
const urlObj = new URL(url);
|
|
1544
|
-
imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;
|
|
1545
|
-
} catch {}
|
|
1546
|
-
result.image = imageUrl;
|
|
1985
|
+
function normalizeOgpUrl(url) {
|
|
1986
|
+
try {
|
|
1987
|
+
const parsed = new URL(url);
|
|
1988
|
+
parsed.hash = "";
|
|
1989
|
+
parsed.hostname = parsed.hostname.toLowerCase();
|
|
1990
|
+
if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") parsed.port = "";
|
|
1991
|
+
if (parsed.pathname.length > 1 && parsed.pathname.endsWith("/")) parsed.pathname = parsed.pathname.slice(0, -1);
|
|
1992
|
+
return parsed.href;
|
|
1993
|
+
} catch {
|
|
1994
|
+
return url;
|
|
1547
1995
|
}
|
|
1548
|
-
const siteNameMatch = html.match(/<meta[^>]*property=["']og:site_name["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:site_name["']/i);
|
|
1549
|
-
if (siteNameMatch) result.siteName = siteNameMatch[1];
|
|
1550
|
-
result.favicon = getFaviconUrl(url);
|
|
1551
|
-
return result;
|
|
1552
1996
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1997
|
+
function ogpCacheKey(url) {
|
|
1998
|
+
return (0, node_crypto.createHash)("sha256").update(normalizeOgpUrl(url)).digest("hex");
|
|
1999
|
+
}
|
|
2000
|
+
//#endregion
|
|
2001
|
+
//#region src/plugins/ogp/fetch.ts
|
|
2002
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
2003
|
+
async function fetchOgpData(url, options = {}) {
|
|
1557
2004
|
if (!isSafeOgpUrl(url)) return null;
|
|
2005
|
+
const resolved = resolveOgpOptions(options);
|
|
2006
|
+
const key = ogpCacheKey(url);
|
|
2007
|
+
const pending = inflight.get(key);
|
|
2008
|
+
if (pending) return pending;
|
|
2009
|
+
const request = loadOgpData(url, key, resolved).finally(() => {
|
|
2010
|
+
if (inflight.get(key) === request) inflight.delete(key);
|
|
2011
|
+
});
|
|
2012
|
+
inflight.set(key, request);
|
|
2013
|
+
return request;
|
|
2014
|
+
}
|
|
2015
|
+
async function loadOgpData(url, key, options) {
|
|
2016
|
+
const now = Date.now();
|
|
2017
|
+
if (options.cache && !options.refresh) {
|
|
2018
|
+
const memory = readMemoryOgp(key, options, now);
|
|
2019
|
+
if (memory !== void 0) return memory;
|
|
2020
|
+
if (options.persistCache) {
|
|
2021
|
+
const disk = await readDiskOgp(key, options, now);
|
|
2022
|
+
if (disk !== void 0) {
|
|
2023
|
+
writeMemoryOgp(key, disk, now, options);
|
|
2024
|
+
return disk;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
const data = await requestOgpData(url, options);
|
|
1558
2029
|
if (options.cache) {
|
|
1559
|
-
|
|
1560
|
-
if (
|
|
2030
|
+
writeMemoryOgp(key, data, now, options);
|
|
2031
|
+
if (options.persistCache) await writeDiskOgp(key, normalizeOgpUrl(url), data, options, now);
|
|
1561
2032
|
}
|
|
2033
|
+
return data;
|
|
2034
|
+
}
|
|
2035
|
+
async function requestOgpData(url, options) {
|
|
1562
2036
|
try {
|
|
1563
2037
|
const controller = new AbortController();
|
|
1564
2038
|
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
@@ -1574,37 +2048,54 @@ async function fetchOgpData(url, options) {
|
|
|
1574
2048
|
console.warn(`Failed to fetch OGP for ${url}: ${response.status}`);
|
|
1575
2049
|
return null;
|
|
1576
2050
|
}
|
|
1577
|
-
|
|
1578
|
-
if (options.cache) ogpCache.set(url, {
|
|
1579
|
-
data,
|
|
1580
|
-
timestamp: Date.now()
|
|
1581
|
-
});
|
|
1582
|
-
return data;
|
|
2051
|
+
return parseOgpFromHtml(await response.text(), url);
|
|
1583
2052
|
} catch (error) {
|
|
1584
2053
|
if (error instanceof Error && error.name === "AbortError") console.warn(`Timeout fetching OGP for ${url}`);
|
|
1585
2054
|
else console.warn(`Error fetching OGP for ${url}:`, error);
|
|
1586
2055
|
return null;
|
|
1587
2056
|
}
|
|
1588
2057
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2058
|
+
function parseOgpFromHtml(html, url) {
|
|
2059
|
+
const result = {
|
|
2060
|
+
url,
|
|
2061
|
+
title: ""
|
|
2062
|
+
};
|
|
2063
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
2064
|
+
result.title = (html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:title["']/i))?.[1] || titleMatch?.[1] || extractDomain(url);
|
|
2065
|
+
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);
|
|
2066
|
+
if (descMatch) result.description = descMatch[1];
|
|
2067
|
+
const imageMatch = html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:image["']/i);
|
|
2068
|
+
if (imageMatch) {
|
|
2069
|
+
let imageUrl = imageMatch[1];
|
|
2070
|
+
if (imageUrl.startsWith("/")) try {
|
|
2071
|
+
const urlObj = new URL(url);
|
|
2072
|
+
imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;
|
|
2073
|
+
} catch {}
|
|
2074
|
+
result.image = imageUrl;
|
|
2075
|
+
}
|
|
2076
|
+
const siteNameMatch = html.match(/<meta[^>]*property=["']og:site_name["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:site_name["']/i);
|
|
2077
|
+
if (siteNameMatch) result.siteName = siteNameMatch[1];
|
|
2078
|
+
result.favicon = getFaviconUrl(url);
|
|
2079
|
+
return result;
|
|
2080
|
+
}
|
|
2081
|
+
//#endregion
|
|
2082
|
+
//#region src/plugins/ogp/render.ts
|
|
2083
|
+
function createOgpCard(data) {
|
|
2084
|
+
const children = [];
|
|
2085
|
+
const contentChildren = [];
|
|
2086
|
+
contentChildren.push({
|
|
2087
|
+
type: "element",
|
|
2088
|
+
tagName: "div",
|
|
2089
|
+
properties: { className: ["ox-ogp-title"] },
|
|
2090
|
+
children: [{
|
|
2091
|
+
type: "text",
|
|
2092
|
+
value: data.title
|
|
2093
|
+
}]
|
|
2094
|
+
});
|
|
2095
|
+
if (data.description) contentChildren.push({
|
|
2096
|
+
type: "element",
|
|
2097
|
+
tagName: "div",
|
|
2098
|
+
properties: { className: ["ox-ogp-description"] },
|
|
1608
2099
|
children: [{
|
|
1609
2100
|
type: "text",
|
|
1610
2101
|
value: data.description
|
|
@@ -1666,9 +2157,6 @@ function createOgpCard(data) {
|
|
|
1666
2157
|
children
|
|
1667
2158
|
};
|
|
1668
2159
|
}
|
|
1669
|
-
/**
|
|
1670
|
-
* Create fallback element when OGP data is unavailable.
|
|
1671
|
-
*/
|
|
1672
2160
|
function createFallbackCard(url) {
|
|
1673
2161
|
return {
|
|
1674
2162
|
type: "element",
|
|
@@ -1700,9 +2188,15 @@ function createFallbackCard(url) {
|
|
|
1700
2188
|
}]
|
|
1701
2189
|
};
|
|
1702
2190
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
2191
|
+
//#endregion
|
|
2192
|
+
//#region src/plugins/ogp/transform.ts
|
|
2193
|
+
const rehypeParse$1 = require_interop.interopDefault(rehype_parse.default);
|
|
2194
|
+
const rehypeStringify$1 = require_interop.interopDefault(rehype_stringify.default);
|
|
2195
|
+
function getAttribute$1(el, name) {
|
|
2196
|
+
const value = el.properties?.[name];
|
|
2197
|
+
if (typeof value === "string") return value;
|
|
2198
|
+
if (Array.isArray(value)) return value.join(" ");
|
|
2199
|
+
}
|
|
1706
2200
|
async function collectOgpUrls(html) {
|
|
1707
2201
|
const urls = [];
|
|
1708
2202
|
const urlPattern = /<ogcard[^>]*\s+url=["']([^"']+)["']/gi;
|
|
@@ -1710,24 +2204,13 @@ async function collectOgpUrls(html) {
|
|
|
1710
2204
|
while ((match = urlPattern.exec(html)) !== null) if (isSafeOgpUrl(match[1])) urls.push(match[1]);
|
|
1711
2205
|
return urls;
|
|
1712
2206
|
}
|
|
1713
|
-
/**
|
|
1714
|
-
* Pre-fetch all OGP data.
|
|
1715
|
-
*/
|
|
1716
2207
|
async function prefetchOgpData(urls, options) {
|
|
1717
|
-
const mergedOptions = {
|
|
1718
|
-
...defaultOptions,
|
|
1719
|
-
...options
|
|
1720
|
-
};
|
|
1721
2208
|
const results = /* @__PURE__ */ new Map();
|
|
1722
2209
|
await Promise.all(urls.map(async (url) => {
|
|
1723
|
-
|
|
1724
|
-
results.set(url, data);
|
|
2210
|
+
results.set(url, await fetchOgpData(url, options));
|
|
1725
2211
|
}));
|
|
1726
2212
|
return results;
|
|
1727
2213
|
}
|
|
1728
|
-
/**
|
|
1729
|
-
* Rehype plugin to transform OgCard components.
|
|
1730
|
-
*/
|
|
1731
2214
|
function rehypeOgp(ogpDataMap) {
|
|
1732
2215
|
return (tree) => {
|
|
1733
2216
|
const visit = (node) => {
|
|
@@ -1738,8 +2221,7 @@ function rehypeOgp(ogpDataMap) {
|
|
|
1738
2221
|
const url = getAttribute$1(child, "url");
|
|
1739
2222
|
if (url) {
|
|
1740
2223
|
const ogpData = ogpDataMap.get(url);
|
|
1741
|
-
|
|
1742
|
-
node.children[i] = cardElement;
|
|
2224
|
+
node.children[i] = ogpData ? createOgpCard(ogpData) : createFallbackCard(url);
|
|
1743
2225
|
}
|
|
1744
2226
|
} else visit(child);
|
|
1745
2227
|
}
|
|
@@ -1748,9 +2230,6 @@ function rehypeOgp(ogpDataMap) {
|
|
|
1748
2230
|
visit(tree);
|
|
1749
2231
|
};
|
|
1750
2232
|
}
|
|
1751
|
-
/**
|
|
1752
|
-
* Transform OgCard components in HTML.
|
|
1753
|
-
*/
|
|
1754
2233
|
async function transformOgp(html, ogpDataMap, options) {
|
|
1755
2234
|
let dataMap = ogpDataMap;
|
|
1756
2235
|
if (!dataMap) dataMap = await prefetchOgpData(await collectOgpUrls(html), options);
|
|
@@ -1758,6 +2237,9 @@ async function transformOgp(html, ogpDataMap, options) {
|
|
|
1758
2237
|
return String(result);
|
|
1759
2238
|
}
|
|
1760
2239
|
//#endregion
|
|
2240
|
+
//#region src/plugins/ogp.ts
|
|
2241
|
+
var ogp_exports = /* @__PURE__ */ require_vitepress.__exportAll({ transformOgp: () => transformOgp });
|
|
2242
|
+
//#endregion
|
|
1761
2243
|
//#region src/plugins/index.ts
|
|
1762
2244
|
const SELF_CLOSING_EMBED_TAG = /<(GitHub|OgCard|Tweet|XPost|Bluesky|Spotify|StackBlitz|WebContainer|YouTube)((?:[^>"']|"[^"]*"|'[^']*')*?)\s*\/>/gi;
|
|
1763
2245
|
/**
|
|
@@ -2432,6 +2914,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
2432
2914
|
gfm: options.gfm,
|
|
2433
2915
|
mdx: resolveMdxForFilePath(filePath, options.mdx),
|
|
2434
2916
|
footnotes: options.footnotes,
|
|
2917
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
2435
2918
|
taskLists: options.taskLists,
|
|
2436
2919
|
tables: options.tables,
|
|
2437
2920
|
strikethrough: options.strikethrough,
|
|
@@ -2439,6 +2922,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
2439
2922
|
autolinkUrls: options.autolinks,
|
|
2440
2923
|
frontmatter: options.frontmatter,
|
|
2441
2924
|
tocMaxDepth: options.tocMaxDepth,
|
|
2925
|
+
headingPermalinks: options.headingPermalinks?.enabled ?? false,
|
|
2442
2926
|
convertMdLinks: ssgOptions?.convertMdLinks,
|
|
2443
2927
|
baseUrl: ssgOptions?.baseUrl,
|
|
2444
2928
|
sourcePath: ssgOptions?.sourcePath ?? filePath,
|
|
@@ -2456,6 +2940,13 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
2456
2940
|
} : void 0,
|
|
2457
2941
|
attributes: options.attrs?.enabled ? { enabled: true } : void 0,
|
|
2458
2942
|
badges: options.badges?.enabled ? { enabled: true } : void 0,
|
|
2943
|
+
magicLinks: options.magicLinks?.enabled ? {
|
|
2944
|
+
enabled: true,
|
|
2945
|
+
aliases: options.magicLinks.aliases,
|
|
2946
|
+
favicon: options.magicLinks.favicon,
|
|
2947
|
+
faviconTemplate: options.magicLinks.faviconTemplate,
|
|
2948
|
+
imageOverrides: options.magicLinks.imageOverrides
|
|
2949
|
+
} : void 0,
|
|
2459
2950
|
containers: options.containers?.enabled ? {
|
|
2460
2951
|
enabled: true,
|
|
2461
2952
|
types: options.containers.types
|
|
@@ -3939,6 +4430,108 @@ initIslands((el, props) => {
|
|
|
3939
4430
|
`;
|
|
3940
4431
|
}
|
|
3941
4432
|
//#endregion
|
|
4433
|
+
//#region src/versions-html.ts
|
|
4434
|
+
function versionSwitcherMarkup(links, badge) {
|
|
4435
|
+
if (links.length === 0) return "";
|
|
4436
|
+
const current = links.find((link) => link.current) ?? links[0];
|
|
4437
|
+
const items = links.map((link) => {
|
|
4438
|
+
const label = `${escapeHtml$4(link.label)}${badgeMarkup(link, badge)}`;
|
|
4439
|
+
if (link.current || !isSafeHref(link.href)) return `<li><span aria-current="page">${label}</span></li>`;
|
|
4440
|
+
return `<li><a href="${escapeHtml$4(link.href)}">${label}</a></li>`;
|
|
4441
|
+
}).join("");
|
|
4442
|
+
return `<nav class="ox-header-select ox-version-switcher" aria-label="Version"><button type="button" aria-expanded="false" aria-haspopup="true">${escapeHtml$4(current.label)}${badgeMarkup(current, badge)}</button><ul class="ox-header-select-menu">${items}</ul></nav><script>(function(){var n=document.currentScript&&document.currentScript.previousElementSibling;if(!n||!n.classList.contains("ox-version-switcher"))return;var b=n.querySelector("button");if(!b)return;function closeOthers(){document.querySelectorAll(".header-nav-dropdown > button[aria-expanded='true'], .ox-locale-switcher > button[aria-expanded='true']").forEach(function(btn){btn.setAttribute("aria-expanded","false");});}b.addEventListener("click",function(e){e.stopPropagation();var o=b.getAttribute("aria-expanded")==="true";closeOthers();b.setAttribute("aria-expanded",o?"false":"true");});document.addEventListener("click",function(e){if(!n.contains(e.target))b.setAttribute("aria-expanded","false");});document.addEventListener("keydown",function(e){if(e.key==="Escape"){b.setAttribute("aria-expanded","false");b.focus();}});})()<\/script>`;
|
|
4443
|
+
}
|
|
4444
|
+
function versionBannerMarkup(kind) {
|
|
4445
|
+
if (kind === "unreleased") return `<aside class="ox-version-banner ox-version-banner--unreleased" role="status">This documentation describes an unreleased version.</aside>`;
|
|
4446
|
+
if (kind === "unmaintained") return `<aside class="ox-version-banner ox-version-banner--unmaintained" role="status">This documentation is unmaintained.</aside>`;
|
|
4447
|
+
return "";
|
|
4448
|
+
}
|
|
4449
|
+
function injectVersionChrome(html, switcher, banner, searchFrom, searchTo) {
|
|
4450
|
+
let next = html;
|
|
4451
|
+
if (banner) next = next.replace(/<body([^>]*)>/, `<body$1>${banner}`);
|
|
4452
|
+
if (switcher) {
|
|
4453
|
+
if (next.includes("<div class=\"header-actions\">")) next = next.replace("<div class=\"header-actions\">", `<div class="header-actions">${switcher}`);
|
|
4454
|
+
else if (next.includes("</header>")) next = next.replace("</header>", `${switcher}</header>`);
|
|
4455
|
+
}
|
|
4456
|
+
if (searchTo && isSafeHref(searchTo)) next = next.replace(/<html([^>]*)>/i, (match, attrs) => {
|
|
4457
|
+
if (/\sdata-ox-search-index=/.test(attrs)) return match;
|
|
4458
|
+
return `<html${attrs} data-ox-search-index="${escapeHtml$4(searchTo)}">`;
|
|
4459
|
+
});
|
|
4460
|
+
if (searchFrom && searchTo && searchFrom !== searchTo && isSafeHref(searchTo)) {
|
|
4461
|
+
next = next.split(searchFrom).join(searchTo);
|
|
4462
|
+
const script = `<script>(function(){var f=${JSON.stringify(searchFrom)},t=${JSON.stringify(searchTo)};var o=window.fetch;window.fetch=function(i,n){if(typeof i==="string"&&i.indexOf(f)!==-1)i=i.split(f).join(t);return o.call(this,i,n);};})()<\/script>`;
|
|
4463
|
+
next = next.includes("</body>") ? next.replace("</body>", `${script}</body>`) : `${next}${script}`;
|
|
4464
|
+
}
|
|
4465
|
+
return next;
|
|
4466
|
+
}
|
|
4467
|
+
function searchIndexUrl(base, prefix) {
|
|
4468
|
+
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
4469
|
+
return prefix ? `${root}${prefix}/search-index.json` : `${root}search-index.json`;
|
|
4470
|
+
}
|
|
4471
|
+
function isSafeHref(href) {
|
|
4472
|
+
const trimmed = href.trim();
|
|
4473
|
+
if (!trimmed || trimmed.startsWith("//")) return false;
|
|
4474
|
+
const lower = trimmed.replace(/\s+/g, "").toLowerCase();
|
|
4475
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) return false;
|
|
4476
|
+
return trimmed.startsWith("/") || trimmed.startsWith("./") || !trimmed.includes(":");
|
|
4477
|
+
}
|
|
4478
|
+
function escapeHtml$4(value) {
|
|
4479
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
4480
|
+
}
|
|
4481
|
+
function badgeMarkup(link, badge) {
|
|
4482
|
+
if (!badge || !link.banner) return "";
|
|
4483
|
+
return `<span class="ox-version-badge">${link.banner === "unreleased" ? "unreleased" : "unmaintained"}</span>`;
|
|
4484
|
+
}
|
|
4485
|
+
//#endregion
|
|
4486
|
+
//#region src/search-filters.ts
|
|
4487
|
+
/**
|
|
4488
|
+
* Language and version filters for the default search dialog.
|
|
4489
|
+
*/
|
|
4490
|
+
const RESULTS_MARKUP = "<div class=\"search-results\"></div>";
|
|
4491
|
+
function injectSearchLocaleFilters(html, input) {
|
|
4492
|
+
const locales = input.locales.filter((locale) => locale.code.trim() && locale.name.trim());
|
|
4493
|
+
if (locales.length < 2) return html;
|
|
4494
|
+
const next = ensureSearchFilters(html);
|
|
4495
|
+
const select = selectMarkup(next, "locale");
|
|
4496
|
+
if (!select) return next;
|
|
4497
|
+
const defaultLocale = input.defaultLocale.trim() || locales[0].code;
|
|
4498
|
+
const selected = locales.some((locale) => locale.code === input.current) ? input.current : defaultLocale;
|
|
4499
|
+
const options = [`<option value="">All languages</option>`, ...locales.map((locale) => {
|
|
4500
|
+
return `<option value="${escapeHtml$4(locale.code)}"${locale.code === selected ? " selected" : ""}>${escapeHtml$4(locale.name)}</option>`;
|
|
4501
|
+
})].join("");
|
|
4502
|
+
return revealFilter(next.replace(select.markup, `<select class="search-filter-select" data-search-filter="locale" data-default-locale="${escapeHtml$4(defaultLocale)}" aria-label="Language">${options}</select>`), "locale");
|
|
4503
|
+
}
|
|
4504
|
+
function injectSearchVersionFilters(html, versions) {
|
|
4505
|
+
const safe = versions.filter((version) => version.id.trim() && version.label.trim() && isSafeHref(version.indexUrl));
|
|
4506
|
+
if (safe.length < 2) return html;
|
|
4507
|
+
const next = ensureSearchFilters(html);
|
|
4508
|
+
const select = selectMarkup(next, "version");
|
|
4509
|
+
if (!select) return next;
|
|
4510
|
+
const current = safe.find((version) => version.current) ?? safe[0];
|
|
4511
|
+
const options = safe.map((version) => {
|
|
4512
|
+
const selected = version.id === current.id ? " selected" : "";
|
|
4513
|
+
return `<option value="${escapeHtml$4(version.id)}" data-prefix="${escapeHtml$4(version.prefix)}" data-index="${escapeHtml$4(version.indexUrl)}"${selected}>${escapeHtml$4(version.label)}</option>`;
|
|
4514
|
+
}).join("");
|
|
4515
|
+
return revealFilter(next.replace(select.markup, `<select class="search-filter-select" data-search-filter="version" aria-label="Version">${options}</select>`), "version");
|
|
4516
|
+
}
|
|
4517
|
+
function ensureSearchFilters(html) {
|
|
4518
|
+
if (html.includes("class=\"search-filters\"") || !html.includes(RESULTS_MARKUP)) return html;
|
|
4519
|
+
return html.replace(RESULTS_MARKUP, `${searchFiltersMarkup()}${RESULTS_MARKUP}`);
|
|
4520
|
+
}
|
|
4521
|
+
function searchFiltersMarkup() {
|
|
4522
|
+
return `${searchFiltersStyle()}<div class="search-filters"><label class="search-filter" data-search-filter-label="locale" hidden><span class="search-filter-label">Language</span><select class="search-filter-select" data-search-filter="locale" data-default-locale="" aria-label="Language"></select></label><label class="search-filter" data-search-filter-label="version" hidden><span class="search-filter-label">Version</span><select class="search-filter-select" data-search-filter="version" aria-label="Version"></select></label></div>`;
|
|
4523
|
+
}
|
|
4524
|
+
function searchFiltersStyle() {
|
|
4525
|
+
return `<style class="ox-search-filters-style">.search-filters{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;padding:.65rem 1rem;border-bottom:1px solid var(--octc-color-border);background:var(--octc-color-bg-alt)}.search-filter{display:flex;align-items:center;gap:.4rem;min-width:0}.search-filter[hidden]{display:none}.search-filter-label{font-size:.75rem;color:var(--octc-color-text-muted);white-space:nowrap}.search-filter-select{min-width:8rem;max-width:12rem;padding:.25rem .4rem;border:1px solid var(--octc-color-border);border-radius:4px;background:var(--octc-color-bg);color:var(--octc-color-text);font:inherit;font-size:.8125rem}.search-filter-select:focus{outline:2px solid var(--octc-color-primary);outline-offset:1px}</style>`;
|
|
4526
|
+
}
|
|
4527
|
+
function selectMarkup(html, kind) {
|
|
4528
|
+
const match = html.match(new RegExp(`<select class="search-filter-select" data-search-filter="${kind}"[^>]*>[\\s\\S]*?<\\/select>`));
|
|
4529
|
+
return match?.[0] ? { markup: match[0] } : void 0;
|
|
4530
|
+
}
|
|
4531
|
+
function revealFilter(html, kind) {
|
|
4532
|
+
return html.replace(`data-search-filter-label="${kind}" hidden`, `data-search-filter-label="${kind}"`);
|
|
4533
|
+
}
|
|
4534
|
+
//#endregion
|
|
3942
4535
|
//#region src/locale-switcher.ts
|
|
3943
4536
|
/**
|
|
3944
4537
|
* Resolves `ssg.localeSwitcher`. Omitted / `false` stay off. `true` or an
|
|
@@ -4371,6 +4964,7 @@ function renderPage(page, options) {
|
|
|
4371
4964
|
contributors: page.contributors,
|
|
4372
4965
|
path: page.path,
|
|
4373
4966
|
url: page.url,
|
|
4967
|
+
markdownSource: page.markdownSource,
|
|
4374
4968
|
frontmatter: page.frontmatter,
|
|
4375
4969
|
layout: page.layout
|
|
4376
4970
|
},
|
|
@@ -4387,6 +4981,7 @@ function renderPage(page, options) {
|
|
|
4387
4981
|
contributors: p.contributors,
|
|
4388
4982
|
path: p.path,
|
|
4389
4983
|
url: p.url,
|
|
4984
|
+
markdownSource: p.markdownSource,
|
|
4390
4985
|
frontmatter: p.frontmatter,
|
|
4391
4986
|
layout: p.layout
|
|
4392
4987
|
}))
|
|
@@ -4445,8 +5040,8 @@ function DefaultTheme({ children }) {
|
|
|
4445
5040
|
<head>
|
|
4446
5041
|
<meta charset="UTF-8">
|
|
4447
5042
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4448
|
-
<title>${escapeHtml$
|
|
4449
|
-
${page.description ? `<meta name="description" content="${escapeHtml$
|
|
5043
|
+
<title>${escapeHtml$3(page.title)} - ${escapeHtml$3(site.name)}</title>
|
|
5044
|
+
${page.description ? `<meta name="description" content="${escapeHtml$3(page.description)}">` : ""}
|
|
4450
5045
|
<style>
|
|
4451
5046
|
:root {
|
|
4452
5047
|
--octc-color-primary: #4f6fae;
|
|
@@ -4470,7 +5065,7 @@ function DefaultTheme({ children }) {
|
|
|
4470
5065
|
</head>
|
|
4471
5066
|
<body>
|
|
4472
5067
|
<header>
|
|
4473
|
-
<h1>${escapeHtml$
|
|
5068
|
+
<h1>${escapeHtml$3(site.name)}</h1>
|
|
4474
5069
|
</header>
|
|
4475
5070
|
<main>
|
|
4476
5071
|
${children.__html}
|
|
@@ -4478,7 +5073,7 @@ function DefaultTheme({ children }) {
|
|
|
4478
5073
|
</body>
|
|
4479
5074
|
</html>` };
|
|
4480
5075
|
}
|
|
4481
|
-
function escapeHtml$
|
|
5076
|
+
function escapeHtml$3(str) {
|
|
4482
5077
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4483
5078
|
}
|
|
4484
5079
|
/**
|
|
@@ -4578,6 +5173,13 @@ async function writeSiteMapFiles(input) {
|
|
|
4578
5173
|
}
|
|
4579
5174
|
return { files };
|
|
4580
5175
|
}
|
|
5176
|
+
/** UTC `YYYY-MM-DD` for W3C lastmod. Invalid or negative timestamps are dropped. */
|
|
5177
|
+
function formatLastmod(timestampMs) {
|
|
5178
|
+
if (timestampMs == null || !Number.isFinite(timestampMs) || timestampMs < 0) return;
|
|
5179
|
+
const date = new Date(timestampMs);
|
|
5180
|
+
if (Number.isNaN(date.getTime())) return;
|
|
5181
|
+
return date.toISOString().slice(0, 10);
|
|
5182
|
+
}
|
|
4581
5183
|
function hasSiteUrl$2(siteUrl) {
|
|
4582
5184
|
return Boolean(siteUrl && siteUrl.trim());
|
|
4583
5185
|
}
|
|
@@ -4590,7 +5192,14 @@ function generateSitemapXml(pages) {
|
|
|
4590
5192
|
for (const page of pages) {
|
|
4591
5193
|
xml += " <url>\n <loc>";
|
|
4592
5194
|
xml += escapeXml$1(page.loc);
|
|
4593
|
-
xml += "</loc>\n
|
|
5195
|
+
xml += "</loc>\n";
|
|
5196
|
+
const lastmod = formatLastmod(page.lastUpdated);
|
|
5197
|
+
if (lastmod) {
|
|
5198
|
+
xml += " <lastmod>";
|
|
5199
|
+
xml += lastmod;
|
|
5200
|
+
xml += "</lastmod>\n";
|
|
5201
|
+
}
|
|
5202
|
+
xml += " </url>\n";
|
|
4594
5203
|
}
|
|
4595
5204
|
xml += "</urlset>\n";
|
|
4596
5205
|
return xml;
|
|
@@ -4651,102 +5260,6 @@ function escapeLlmsUrl(value) {
|
|
|
4651
5260
|
return escaped;
|
|
4652
5261
|
}
|
|
4653
5262
|
//#endregion
|
|
4654
|
-
//#region src/publish-state.ts
|
|
4655
|
-
/**
|
|
4656
|
-
* Opt-in draft / unlisted / scheduled page classification.
|
|
4657
|
-
*/
|
|
4658
|
-
/**
|
|
4659
|
-
* Resolves `publishState` with defaults.
|
|
4660
|
-
*
|
|
4661
|
-
* `false` / omitted stays off. `true` enables production filtering. An object
|
|
4662
|
-
* enables the feature and overrides only the fields the site set.
|
|
4663
|
-
*/
|
|
4664
|
-
function resolvePublishStateOptions(value) {
|
|
4665
|
-
if (!value) return {
|
|
4666
|
-
enabled: false,
|
|
4667
|
-
includeDrafts: false
|
|
4668
|
-
};
|
|
4669
|
-
if (value === true) return {
|
|
4670
|
-
enabled: true,
|
|
4671
|
-
includeDrafts: false
|
|
4672
|
-
};
|
|
4673
|
-
return {
|
|
4674
|
-
enabled: value.enabled ?? true,
|
|
4675
|
-
now: value.now,
|
|
4676
|
-
includeDrafts: value.includeDrafts ?? false
|
|
4677
|
-
};
|
|
4678
|
-
}
|
|
4679
|
-
/** Classifies one frontmatter object. Never throws. */
|
|
4680
|
-
function classifyPublishState(frontmatter, options) {
|
|
4681
|
-
try {
|
|
4682
|
-
return require_vitepress.importNapiModuleSync().classifyPublishState(JSON.stringify(frontmatter ?? {}), toNapiPublishState(options));
|
|
4683
|
-
} catch {
|
|
4684
|
-
return {
|
|
4685
|
-
output: true,
|
|
4686
|
-
listed: true
|
|
4687
|
-
};
|
|
4688
|
-
}
|
|
4689
|
-
}
|
|
4690
|
-
/** Splits pages into those that write HTML and those that appear in listings. */
|
|
4691
|
-
function partitionPublishedPages(pages, options) {
|
|
4692
|
-
if (!options?.enabled) return {
|
|
4693
|
-
output: [...pages],
|
|
4694
|
-
listed: [...pages]
|
|
4695
|
-
};
|
|
4696
|
-
const output = [];
|
|
4697
|
-
const listed = [];
|
|
4698
|
-
for (const page of pages) {
|
|
4699
|
-
const decision = classifyPublishState(page.frontmatter, options);
|
|
4700
|
-
if (decision.output) output.push(page);
|
|
4701
|
-
if (decision.listed) listed.push(page);
|
|
4702
|
-
}
|
|
4703
|
-
return {
|
|
4704
|
-
output,
|
|
4705
|
-
listed
|
|
4706
|
-
};
|
|
4707
|
-
}
|
|
4708
|
-
/** Drops nav items that resolve to hidden (unpublished or unlisted) pages. */
|
|
4709
|
-
function filterNavGroups(groups, hidden) {
|
|
4710
|
-
return groups.map((group) => ({
|
|
4711
|
-
...group,
|
|
4712
|
-
items: filterNavItems(group.items, hidden)
|
|
4713
|
-
})).filter((group) => group.items.length > 0);
|
|
4714
|
-
}
|
|
4715
|
-
function filterNavItems(items, hidden) {
|
|
4716
|
-
const kept = [];
|
|
4717
|
-
for (const item of items) {
|
|
4718
|
-
if (isHiddenNavTarget(item, hidden)) continue;
|
|
4719
|
-
const children = item.children?.length ? filterNavItems(item.children, hidden) : item.children;
|
|
4720
|
-
kept.push(children === item.children ? item : {
|
|
4721
|
-
...item,
|
|
4722
|
-
children
|
|
4723
|
-
});
|
|
4724
|
-
}
|
|
4725
|
-
return kept;
|
|
4726
|
-
}
|
|
4727
|
-
function isHiddenNavTarget(item, hidden) {
|
|
4728
|
-
return hidden.has(item.path) || hidden.has(item.href);
|
|
4729
|
-
}
|
|
4730
|
-
/** Keys used to match a page against generated nav items. */
|
|
4731
|
-
function hiddenNavKeys(pages, listed) {
|
|
4732
|
-
const listedPaths = new Set(listed.map((page) => page.inputPath));
|
|
4733
|
-
const hidden = /* @__PURE__ */ new Set();
|
|
4734
|
-
for (const page of pages) {
|
|
4735
|
-
if (listedPaths.has(page.inputPath)) continue;
|
|
4736
|
-
hidden.add(page.routePaths.urlPath);
|
|
4737
|
-
hidden.add(page.routePaths.href);
|
|
4738
|
-
}
|
|
4739
|
-
return hidden;
|
|
4740
|
-
}
|
|
4741
|
-
function toNapiPublishState(options) {
|
|
4742
|
-
if (!options) return;
|
|
4743
|
-
return {
|
|
4744
|
-
enabled: options.enabled,
|
|
4745
|
-
now: options.now,
|
|
4746
|
-
includeDrafts: options.includeDrafts
|
|
4747
|
-
};
|
|
4748
|
-
}
|
|
4749
|
-
//#endregion
|
|
4750
5263
|
//#region src/permalinks.ts
|
|
4751
5264
|
const RESERVED_CASCADE_KEYS = /* @__PURE__ */ new Set(["permalink", "slug"]);
|
|
4752
5265
|
/** Resolves `permalinks`. `false` / omitted stays off. `true` / `{}` enables. */
|
|
@@ -4796,6 +5309,18 @@ function resolvePageRoutes(input) {
|
|
|
4796
5309
|
errors
|
|
4797
5310
|
};
|
|
4798
5311
|
}
|
|
5312
|
+
/** Escapes a value for use in an HTML attribute. */
|
|
5313
|
+
function escapeAttribute$2(value) {
|
|
5314
|
+
return value.replace(/[&<>"']/gu, (ch) => {
|
|
5315
|
+
switch (ch) {
|
|
5316
|
+
case "&": return "&";
|
|
5317
|
+
case "<": return "<";
|
|
5318
|
+
case ">": return ">";
|
|
5319
|
+
case "\"": return """;
|
|
5320
|
+
default: return "'";
|
|
5321
|
+
}
|
|
5322
|
+
});
|
|
5323
|
+
}
|
|
4799
5324
|
function normalizeUrlPath$1(value) {
|
|
4800
5325
|
const segments = pathSegments(value);
|
|
4801
5326
|
return segments.length === 0 ? "/" : segments.join("/");
|
|
@@ -4897,32 +5422,320 @@ function ancestorDirs(source) {
|
|
|
4897
5422
|
return dirs;
|
|
4898
5423
|
}
|
|
4899
5424
|
//#endregion
|
|
4900
|
-
//#region src/
|
|
5425
|
+
//#region src/publish-state.ts
|
|
4901
5426
|
/**
|
|
4902
|
-
*
|
|
5427
|
+
* Opt-in draft / unlisted / scheduled page classification.
|
|
4903
5428
|
*/
|
|
4904
|
-
/**
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
}
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
5429
|
+
/**
|
|
5430
|
+
* Resolves `publishState` with defaults.
|
|
5431
|
+
*
|
|
5432
|
+
* `false` / omitted stays off. `true` enables production filtering. An object
|
|
5433
|
+
* enables the feature and overrides only the fields the site set.
|
|
5434
|
+
*/
|
|
5435
|
+
function resolvePublishStateOptions(value) {
|
|
5436
|
+
if (!value) return {
|
|
5437
|
+
enabled: false,
|
|
5438
|
+
includeDrafts: false
|
|
5439
|
+
};
|
|
5440
|
+
if (value === true) return {
|
|
5441
|
+
enabled: true,
|
|
5442
|
+
includeDrafts: false
|
|
5443
|
+
};
|
|
5444
|
+
return {
|
|
5445
|
+
enabled: value.enabled ?? true,
|
|
5446
|
+
now: value.now,
|
|
5447
|
+
includeDrafts: value.includeDrafts ?? false
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
/** Classifies one frontmatter object. Never throws. */
|
|
5451
|
+
function classifyPublishState(frontmatter, options) {
|
|
5452
|
+
try {
|
|
5453
|
+
return require_vitepress.importNapiModuleSync().classifyPublishState(JSON.stringify(frontmatter ?? {}), toNapiPublishState(options));
|
|
5454
|
+
} catch {
|
|
5455
|
+
return {
|
|
5456
|
+
output: true,
|
|
5457
|
+
listed: true
|
|
5458
|
+
};
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
/** Splits pages into those that write HTML and those that appear in listings. */
|
|
5462
|
+
function partitionPublishedPages(pages, options) {
|
|
5463
|
+
if (!options?.enabled) return {
|
|
5464
|
+
output: [...pages],
|
|
5465
|
+
listed: [...pages]
|
|
5466
|
+
};
|
|
5467
|
+
const output = [];
|
|
5468
|
+
const listed = [];
|
|
5469
|
+
for (const page of pages) {
|
|
5470
|
+
const decision = classifyPublishState(page.frontmatter, options);
|
|
5471
|
+
if (decision.output) output.push(page);
|
|
5472
|
+
if (decision.listed) listed.push(page);
|
|
5473
|
+
}
|
|
5474
|
+
return {
|
|
5475
|
+
output,
|
|
5476
|
+
listed
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5479
|
+
/** Drops nav items that resolve to hidden (unpublished or unlisted) pages. */
|
|
5480
|
+
function filterNavGroups(groups, hidden) {
|
|
5481
|
+
return groups.map((group) => ({
|
|
5482
|
+
...group,
|
|
5483
|
+
items: filterNavItems(group.items, hidden)
|
|
5484
|
+
})).filter((group) => group.items.length > 0);
|
|
5485
|
+
}
|
|
5486
|
+
function filterNavItems(items, hidden) {
|
|
5487
|
+
const kept = [];
|
|
5488
|
+
for (const item of items) {
|
|
5489
|
+
if (isHiddenNavTarget(item, hidden)) continue;
|
|
5490
|
+
const children = item.children?.length ? filterNavItems(item.children, hidden) : item.children;
|
|
5491
|
+
kept.push(children === item.children ? item : {
|
|
5492
|
+
...item,
|
|
5493
|
+
children
|
|
5494
|
+
});
|
|
5495
|
+
}
|
|
5496
|
+
return kept;
|
|
5497
|
+
}
|
|
5498
|
+
function isHiddenNavTarget(item, hidden) {
|
|
5499
|
+
return hidden.has(item.path) || hidden.has(item.href);
|
|
5500
|
+
}
|
|
5501
|
+
/** Keys used to match a page against generated nav items. */
|
|
5502
|
+
function hiddenNavKeys(pages, listed) {
|
|
5503
|
+
const listedPaths = new Set(listed.map((page) => page.inputPath));
|
|
5504
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
5505
|
+
for (const page of pages) {
|
|
5506
|
+
if (listedPaths.has(page.inputPath)) continue;
|
|
5507
|
+
hidden.add(page.routePaths.urlPath);
|
|
5508
|
+
hidden.add(page.routePaths.href);
|
|
5509
|
+
}
|
|
5510
|
+
return hidden;
|
|
5511
|
+
}
|
|
5512
|
+
function toNapiPublishState(options) {
|
|
5513
|
+
if (!options) return;
|
|
5514
|
+
return {
|
|
5515
|
+
enabled: options.enabled,
|
|
5516
|
+
now: options.now,
|
|
5517
|
+
includeDrafts: options.includeDrafts
|
|
5518
|
+
};
|
|
5519
|
+
}
|
|
5520
|
+
//#endregion
|
|
5521
|
+
//#region src/markdown-source.ts
|
|
5522
|
+
/**
|
|
5523
|
+
* Opt-in Markdown source companions written beside generated HTML.
|
|
5524
|
+
*
|
|
5525
|
+
* Copies already-read source bytes. Does not re-parse Markdown to emit them.
|
|
5526
|
+
*/
|
|
5527
|
+
/**
|
|
5528
|
+
* Resolves `ssg.markdownSource` with defaults.
|
|
5529
|
+
*
|
|
5530
|
+
* `false` / omitted stays off. `true` enables companions and the alternate
|
|
5531
|
+
* link. An object enables the feature and overrides only the fields set.
|
|
5532
|
+
*/
|
|
5533
|
+
function resolveMarkdownSourceOptions(value) {
|
|
5534
|
+
if (!value) return {
|
|
5535
|
+
enabled: false,
|
|
5536
|
+
alternate: true
|
|
5537
|
+
};
|
|
5538
|
+
if (value === true) return {
|
|
5539
|
+
enabled: true,
|
|
5540
|
+
alternate: true
|
|
5541
|
+
};
|
|
5542
|
+
return {
|
|
5543
|
+
enabled: true,
|
|
5544
|
+
alternate: value.alternate !== false
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5547
|
+
/** Companion href for one page after permalink / publish-state checks. */
|
|
5548
|
+
function markdownSourceHrefForPage(input) {
|
|
5549
|
+
if (!shouldPublishMarkdownSource(input.frontmatter, input.publishState)) return;
|
|
5550
|
+
return markdownSourceHref(resolvePageRoutes({
|
|
5551
|
+
pages: [{
|
|
5552
|
+
source: input.source,
|
|
5553
|
+
fileUrl: input.fileUrl,
|
|
5554
|
+
frontmatter: input.frontmatter
|
|
5555
|
+
}],
|
|
5556
|
+
permalinks: input.permalinks,
|
|
5557
|
+
cascade: input.cascade
|
|
5558
|
+
}).pages[0]?.urlPath ?? input.fileUrl, input.base);
|
|
5559
|
+
}
|
|
5560
|
+
/** Public companion href, including `base`. Always ends in `.md`. */
|
|
5561
|
+
function markdownSourceHref(urlPath, base) {
|
|
5562
|
+
const relative = companionRelativePath(urlPath);
|
|
5563
|
+
if (!relative) return;
|
|
5564
|
+
return `${normalizeBase$1(base)}${relative}`;
|
|
5565
|
+
}
|
|
5566
|
+
/** Filesystem path for a companion, or `undefined` when it would escape `outDir`. */
|
|
5567
|
+
function markdownSourceOutputPath(outDir, urlPath) {
|
|
5568
|
+
const relative = companionRelativePath(urlPath);
|
|
5569
|
+
if (!relative) return;
|
|
5570
|
+
return containedPath$3(outDir, ...relative.split("/"));
|
|
5571
|
+
}
|
|
5572
|
+
/**
|
|
5573
|
+
* Whether this page may publish a companion.
|
|
5574
|
+
*
|
|
5575
|
+
* Draft and unlisted source is never emitted. When `publishState` is on,
|
|
5576
|
+
* scheduled / expired pages follow that filter and `includeDrafts` is ignored
|
|
5577
|
+
* so preview HTML cannot leak source.
|
|
5578
|
+
*/
|
|
5579
|
+
function shouldPublishMarkdownSource(frontmatter, publishState) {
|
|
5580
|
+
if (frontmatter.draft === true || frontmatter.unlisted === true) return false;
|
|
5581
|
+
if (!publishState?.enabled) return true;
|
|
5582
|
+
return classifyPublishState(frontmatter, {
|
|
5583
|
+
...publishState,
|
|
5584
|
+
includeDrafts: false
|
|
5585
|
+
}).output;
|
|
5586
|
+
}
|
|
5587
|
+
/** Inserts `<link rel="alternate" type="text/markdown">` before `</head>`. */
|
|
5588
|
+
function injectMarkdownSourceAlternate(html, href) {
|
|
5589
|
+
if (!href || !/<\/head>/i.test(html)) return html;
|
|
5590
|
+
const tag = `<link rel="alternate" type="text/markdown" href="${escapeAttribute$2(href)}">`;
|
|
5591
|
+
const index = html.toLowerCase().lastIndexOf("</head>");
|
|
5592
|
+
return `${html.slice(0, index)} ${tag}\n${html.slice(index)}`;
|
|
5593
|
+
}
|
|
5594
|
+
/** Writes enabled companions from already-read source bytes. */
|
|
5595
|
+
async function writeMarkdownSourceFiles(input) {
|
|
5596
|
+
if (!input.options?.enabled) return {
|
|
5597
|
+
files: [],
|
|
5598
|
+
errors: []
|
|
5599
|
+
};
|
|
5600
|
+
const files = [];
|
|
5601
|
+
const errors = [];
|
|
5602
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5603
|
+
for (const page of input.pages) {
|
|
5604
|
+
if (page.source == null || !shouldPublishMarkdownSource(page.frontmatter, input.publishState)) continue;
|
|
5605
|
+
const outputPath = markdownSourceOutputPath(input.outDir, page.urlPath);
|
|
5606
|
+
if (!outputPath) {
|
|
5607
|
+
errors.push(`[ox-content] markdownSource skipped path-escape for ${page.inputPath}`);
|
|
5608
|
+
continue;
|
|
5609
|
+
}
|
|
5610
|
+
const previous = seen.get(outputPath);
|
|
5611
|
+
if (previous) {
|
|
5612
|
+
errors.push(`[ox-content] markdownSource collision: ${page.inputPath} and ${previous} both map to ${outputPath}`);
|
|
5613
|
+
continue;
|
|
5614
|
+
}
|
|
5615
|
+
seen.set(outputPath, page.inputPath);
|
|
5616
|
+
await node_fs_promises.mkdir(node_path.dirname(outputPath), { recursive: true });
|
|
5617
|
+
await node_fs_promises.writeFile(outputPath, page.source);
|
|
5618
|
+
files.push(outputPath);
|
|
5619
|
+
}
|
|
5620
|
+
return {
|
|
5621
|
+
files,
|
|
5622
|
+
errors
|
|
5623
|
+
};
|
|
5624
|
+
}
|
|
5625
|
+
/** True when the request pathname is a `.md` companion URL. */
|
|
5626
|
+
function isMarkdownSourceRequest(pathname) {
|
|
5627
|
+
const clean = stripSearch(pathname);
|
|
5628
|
+
return clean.toLowerCase().endsWith(".md") && !clean.includes("\\");
|
|
5629
|
+
}
|
|
5630
|
+
/** Builds a companion index from source files without transforming Markdown. */
|
|
5631
|
+
async function buildMarkdownSourceIndex(input) {
|
|
5632
|
+
const loaded = await Promise.all(input.files.map(async (file) => {
|
|
5633
|
+
const source = await node_fs_promises.readFile(file, "utf8");
|
|
5634
|
+
return {
|
|
5635
|
+
source: file,
|
|
5636
|
+
fileUrl: require_vitepress.importNapiModuleSync().getSsgUrlPath(file, input.srcDir),
|
|
5637
|
+
frontmatter: parseSourceFrontmatter(source),
|
|
5638
|
+
body: source
|
|
5639
|
+
};
|
|
5640
|
+
}));
|
|
5641
|
+
const routed = resolvePageRoutes({
|
|
5642
|
+
pages: loaded.map(({ source, fileUrl, frontmatter }) => ({
|
|
5643
|
+
source,
|
|
5644
|
+
fileUrl,
|
|
5645
|
+
frontmatter
|
|
5646
|
+
})),
|
|
5647
|
+
permalinks: input.permalinks,
|
|
5648
|
+
cascade: input.cascade
|
|
5649
|
+
});
|
|
5650
|
+
const bodies = new Map(loaded.map((page) => [page.source, page.body]));
|
|
5651
|
+
const index = /* @__PURE__ */ new Map();
|
|
5652
|
+
for (const page of routed.pages) {
|
|
5653
|
+
const href = markdownSourceHref(page.urlPath, "/");
|
|
5654
|
+
const body = bodies.get(page.source);
|
|
5655
|
+
if (!href || body == null) continue;
|
|
5656
|
+
index.set(normalizePathname(href), {
|
|
5657
|
+
source: body,
|
|
5658
|
+
allowed: shouldPublishMarkdownSource(page.frontmatter, input.publishState)
|
|
5659
|
+
});
|
|
5660
|
+
}
|
|
5661
|
+
return index;
|
|
5662
|
+
}
|
|
5663
|
+
/** Looks up a companion after the site `base` has been stripped. */
|
|
5664
|
+
function resolveMarkdownSourceRequest(pathname, index) {
|
|
5665
|
+
if (!isMarkdownSourceRequest(pathname)) return;
|
|
5666
|
+
return index.get(normalizePathname(stripSearch(pathname)));
|
|
5667
|
+
}
|
|
5668
|
+
/** Frontmatter keys only — not a Markdown parse. */
|
|
5669
|
+
function parseSourceFrontmatter(source) {
|
|
5670
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5671
|
+
if (!match?.[1]) return {};
|
|
5672
|
+
const result = {};
|
|
5673
|
+
for (const line of match[1].split("\n")) {
|
|
5674
|
+
const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
5675
|
+
if (!kv) continue;
|
|
5676
|
+
result[kv[1]] = parseFrontmatterScalar(kv[2].trim());
|
|
5677
|
+
}
|
|
5678
|
+
return result;
|
|
5679
|
+
}
|
|
5680
|
+
function parseFrontmatterScalar(value) {
|
|
5681
|
+
if (value === "true") return true;
|
|
5682
|
+
if (value === "false") return false;
|
|
5683
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
5684
|
+
return value;
|
|
5685
|
+
}
|
|
5686
|
+
function companionRelativePath(urlPath) {
|
|
5687
|
+
const trimmed = urlPath === "/" || !urlPath ? "index" : urlPath.replace(/^\/+|\/+$/gu, "");
|
|
5688
|
+
if (!trimmed) return;
|
|
5689
|
+
if (trimmed.split("/").some((segment) => !segment || segment === "." || segment === "..")) return;
|
|
5690
|
+
return `${trimmed}.md`;
|
|
5691
|
+
}
|
|
5692
|
+
function containedPath$3(outDir, ...segments) {
|
|
5693
|
+
const root = node_path.resolve(outDir);
|
|
5694
|
+
const resolved = node_path.resolve(root, ...segments);
|
|
5695
|
+
const prefix = root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`;
|
|
5696
|
+
if (resolved === root || !resolved.startsWith(prefix)) return;
|
|
5697
|
+
return resolved;
|
|
5698
|
+
}
|
|
5699
|
+
function normalizeBase$1(base) {
|
|
5700
|
+
if (!base || base === "/") return "/";
|
|
5701
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
5702
|
+
}
|
|
5703
|
+
function stripSearch(pathname) {
|
|
5704
|
+
return pathname.split("?")[0]?.split("#")[0] ?? pathname;
|
|
5705
|
+
}
|
|
5706
|
+
function normalizePathname(pathname) {
|
|
5707
|
+
const clean = stripSearch(pathname);
|
|
5708
|
+
if (!clean || clean === "/") return "/";
|
|
5709
|
+
const withSlash = clean.startsWith("/") ? clean : `/${clean}`;
|
|
5710
|
+
return withSlash.length > 1 && withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
|
|
5711
|
+
}
|
|
5712
|
+
//#endregion
|
|
5713
|
+
//#region src/apply-permalinks.ts
|
|
5714
|
+
/**
|
|
5715
|
+
* Applies resolved permalinks / cascade to SSG pages and collection entries.
|
|
5716
|
+
*/
|
|
5717
|
+
/** Rewrites SSG `routePaths` from resolved permalinks / slugs. */
|
|
5718
|
+
function applySsgPageRoutes(input) {
|
|
5719
|
+
const resolved = resolvePageRoutes({
|
|
5720
|
+
pages: input.pages.map((page) => ({
|
|
5721
|
+
source: page.inputPath,
|
|
5722
|
+
fileUrl: page.routePaths.urlPath,
|
|
5723
|
+
frontmatter: page.frontmatter
|
|
5724
|
+
})),
|
|
5725
|
+
permalinks: input.permalinks,
|
|
5726
|
+
cascade: input.cascade
|
|
5727
|
+
});
|
|
5728
|
+
const bySource = new Map(resolved.pages.map((page) => [page.source, page]));
|
|
5729
|
+
const pages = [];
|
|
5730
|
+
for (const page of input.pages) {
|
|
5731
|
+
const hit = bySource.get(page.inputPath);
|
|
5732
|
+
if (!hit) continue;
|
|
5733
|
+
pages.push({
|
|
5734
|
+
...page,
|
|
5735
|
+
frontmatter: hit.frontmatter,
|
|
5736
|
+
routePaths: routePathsFromUrl(hit.urlPath, input.srcDir, input.outDir, input.base, input.extension, input.siteUrl)
|
|
5737
|
+
});
|
|
5738
|
+
}
|
|
4926
5739
|
return {
|
|
4927
5740
|
pages,
|
|
4928
5741
|
errors: resolved.errors
|
|
@@ -5126,7 +5939,7 @@ async function writeRedirectFiles(input) {
|
|
|
5126
5939
|
}
|
|
5127
5940
|
/** Static HTML redirect body. `dest` is escaped. */
|
|
5128
5941
|
function generateRedirectHtml(dest) {
|
|
5129
|
-
const escaped = escapeHtml$
|
|
5942
|
+
const escaped = escapeHtml$2(dest);
|
|
5130
5943
|
return `\
|
|
5131
5944
|
<!DOCTYPE html>
|
|
5132
5945
|
<html lang="en">
|
|
@@ -5211,7 +6024,7 @@ function upsert(files, index, occupied, from, to, base) {
|
|
|
5211
6024
|
html
|
|
5212
6025
|
});
|
|
5213
6026
|
}
|
|
5214
|
-
function escapeHtml$
|
|
6027
|
+
function escapeHtml$2(value) {
|
|
5215
6028
|
return value.replace(/[&<>"']/g, (ch) => {
|
|
5216
6029
|
switch (ch) {
|
|
5217
6030
|
case "&": return "&";
|
|
@@ -5569,6 +6382,7 @@ function createNativeTransformOptions(options) {
|
|
|
5569
6382
|
return {
|
|
5570
6383
|
gfm: options.gfm,
|
|
5571
6384
|
footnotes: options.footnotes,
|
|
6385
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
5572
6386
|
taskLists: options.taskLists,
|
|
5573
6387
|
tables: options.tables,
|
|
5574
6388
|
strikethrough: options.strikethrough,
|
|
@@ -5576,6 +6390,7 @@ function createNativeTransformOptions(options) {
|
|
|
5576
6390
|
autolinkUrls: options.autolinks,
|
|
5577
6391
|
frontmatter: options.frontmatter,
|
|
5578
6392
|
tocMaxDepth: options.tocMaxDepth,
|
|
6393
|
+
headingPermalinks: options.headingPermalinks?.enabled ?? false,
|
|
5579
6394
|
codeAnnotations: options.codeAnnotations?.enabled ?? false,
|
|
5580
6395
|
codeAnnotationMetaKey: options.codeAnnotations?.metaKey ?? "annotate",
|
|
5581
6396
|
codeAnnotationSyntax: options.codeAnnotations?.notation ?? "attribute",
|
|
@@ -5590,6 +6405,13 @@ function createNativeTransformOptions(options) {
|
|
|
5590
6405
|
} : void 0,
|
|
5591
6406
|
attributes: options.attrs?.enabled ? { enabled: true } : void 0,
|
|
5592
6407
|
badges: options.badges?.enabled ? { enabled: true } : void 0,
|
|
6408
|
+
magicLinks: options.magicLinks?.enabled ? {
|
|
6409
|
+
enabled: true,
|
|
6410
|
+
aliases: options.magicLinks.aliases,
|
|
6411
|
+
favicon: options.magicLinks.favicon,
|
|
6412
|
+
faviconTemplate: options.magicLinks.faviconTemplate,
|
|
6413
|
+
imageOverrides: options.magicLinks.imageOverrides
|
|
6414
|
+
} : void 0,
|
|
5593
6415
|
containers: options.containers?.enabled ? {
|
|
5594
6416
|
enabled: true,
|
|
5595
6417
|
types: options.containers.types
|
|
@@ -6014,6 +6836,7 @@ function isExcludedFromFeed(item, publishState) {
|
|
|
6014
6836
|
const frontmatter = item.frontmatter ?? {};
|
|
6015
6837
|
if (item.draft === true || frontmatter.draft === true) return true;
|
|
6016
6838
|
if (item.unlisted === true || frontmatter.unlisted === true) return true;
|
|
6839
|
+
if (frontmatter.external === true) return true;
|
|
6017
6840
|
if (!publishState?.enabled) return false;
|
|
6018
6841
|
return !classifyPublishState({
|
|
6019
6842
|
...frontmatter,
|
|
@@ -6254,14 +7077,14 @@ function relatedMarkup(pages) {
|
|
|
6254
7077
|
}
|
|
6255
7078
|
function listPageContent(terms, base, urlName) {
|
|
6256
7079
|
const items = terms.map((term) => listItem$1(siteHref$3(base, urlName, term.slug), term.label)).join("");
|
|
6257
|
-
return `<h1>${escapeHtml$
|
|
7080
|
+
return `<h1>${escapeHtml$1(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
|
|
6258
7081
|
}
|
|
6259
7082
|
function termPageContent(term) {
|
|
6260
7083
|
const items = [...term.pages].sort((left, right) => {
|
|
6261
7084
|
const titleCmp = left.title.localeCompare(right.title);
|
|
6262
7085
|
return titleCmp !== 0 ? titleCmp : left.routePaths.href.localeCompare(right.routePaths.href);
|
|
6263
7086
|
}).map((page) => listItem$1(page.routePaths.href, page.title)).join("");
|
|
6264
|
-
return `<h1>${escapeHtml$
|
|
7087
|
+
return `<h1>${escapeHtml$1(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
|
|
6265
7088
|
}
|
|
6266
7089
|
function displayTaxonomyName(name) {
|
|
6267
7090
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -6279,9 +7102,9 @@ function containedPath$2(outDir, ...segments) {
|
|
|
6279
7102
|
return resolved;
|
|
6280
7103
|
}
|
|
6281
7104
|
function listItem$1(href, label) {
|
|
6282
|
-
return `<li><a href="${escapeHtml$
|
|
7105
|
+
return `<li><a href="${escapeHtml$1(href)}">${escapeHtml$1(label)}</a></li>`;
|
|
6283
7106
|
}
|
|
6284
|
-
function escapeHtml$
|
|
7107
|
+
function escapeHtml$1(value) {
|
|
6285
7108
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6286
7109
|
}
|
|
6287
7110
|
//#endregion
|
|
@@ -6555,18 +7378,21 @@ function resolveBlogOptions(value) {
|
|
|
6555
7378
|
if (!value) return {
|
|
6556
7379
|
enabled: false,
|
|
6557
7380
|
authors: {},
|
|
6558
|
-
pageSize: DEFAULT_PAGE_SIZE
|
|
7381
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7382
|
+
feeds: []
|
|
6559
7383
|
};
|
|
6560
7384
|
if (value === true) return {
|
|
6561
7385
|
enabled: true,
|
|
6562
7386
|
authors: {},
|
|
6563
|
-
pageSize: DEFAULT_PAGE_SIZE
|
|
7387
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7388
|
+
feeds: []
|
|
6564
7389
|
};
|
|
6565
7390
|
return {
|
|
6566
7391
|
enabled: true,
|
|
6567
7392
|
collection: value.collection,
|
|
6568
7393
|
authors: normalizeAuthors(value.authors),
|
|
6569
|
-
pageSize: normalizePageSize(value.pageSize)
|
|
7394
|
+
pageSize: normalizePageSize(value.pageSize),
|
|
7395
|
+
feeds: normalizeFeeds(value.feeds)
|
|
6570
7396
|
};
|
|
6571
7397
|
}
|
|
6572
7398
|
/**
|
|
@@ -6597,54 +7423,396 @@ function normalizePageSize(value) {
|
|
|
6597
7423
|
if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
|
|
6598
7424
|
return DEFAULT_PAGE_SIZE;
|
|
6599
7425
|
}
|
|
7426
|
+
function normalizeFeeds(feeds) {
|
|
7427
|
+
if (!Array.isArray(feeds)) return [];
|
|
7428
|
+
const resolved = [];
|
|
7429
|
+
for (const entry of feeds) {
|
|
7430
|
+
if (typeof entry === "string") {
|
|
7431
|
+
const url = entry.trim();
|
|
7432
|
+
if (url) resolved.push({
|
|
7433
|
+
url,
|
|
7434
|
+
onError: "warn"
|
|
7435
|
+
});
|
|
7436
|
+
continue;
|
|
7437
|
+
}
|
|
7438
|
+
if (!entry || typeof entry !== "object" || typeof entry.url !== "string") continue;
|
|
7439
|
+
const url = entry.url.trim();
|
|
7440
|
+
if (!url) continue;
|
|
7441
|
+
const language = trimOptional(entry.language);
|
|
7442
|
+
const author = trimOptional(entry.author);
|
|
7443
|
+
resolved.push({
|
|
7444
|
+
url,
|
|
7445
|
+
...language ? { language } : {},
|
|
7446
|
+
...author ? { author } : {},
|
|
7447
|
+
onError: entry.onError === "error" ? "error" : "warn"
|
|
7448
|
+
});
|
|
7449
|
+
}
|
|
7450
|
+
return resolved;
|
|
7451
|
+
}
|
|
7452
|
+
function trimOptional(value) {
|
|
7453
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
7454
|
+
}
|
|
6600
7455
|
//#endregion
|
|
6601
|
-
//#region src/blog-
|
|
7456
|
+
//#region src/blog-feed-date.ts
|
|
7457
|
+
/**
|
|
7458
|
+
* Publication dates from RSS / Atom items.
|
|
7459
|
+
*/
|
|
7460
|
+
const MONTHS = {
|
|
7461
|
+
jan: 1,
|
|
7462
|
+
feb: 2,
|
|
7463
|
+
mar: 3,
|
|
7464
|
+
apr: 4,
|
|
7465
|
+
may: 5,
|
|
7466
|
+
jun: 6,
|
|
7467
|
+
jul: 7,
|
|
7468
|
+
aug: 8,
|
|
7469
|
+
sep: 9,
|
|
7470
|
+
oct: 10,
|
|
7471
|
+
nov: 11,
|
|
7472
|
+
dec: 12
|
|
7473
|
+
};
|
|
7474
|
+
const RFC822 = /^(?:[A-Za-z]{3},\s+)?(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{2}):(\d{2})(?::(\d{2}))?\s+(?:GMT|UTC|UT|([+-]\d{4}))$/;
|
|
7475
|
+
function parseFeedDate(value) {
|
|
7476
|
+
const trimmed = value?.trim();
|
|
7477
|
+
if (!trimmed) return;
|
|
7478
|
+
return parseDate(trimmed) ?? parseRfc822(trimmed);
|
|
7479
|
+
}
|
|
7480
|
+
function feedDateLabel(date) {
|
|
7481
|
+
return `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
|
|
7482
|
+
}
|
|
7483
|
+
function feedDateIso(date) {
|
|
7484
|
+
return `${feedDateLabel(date)}T${String(date.hour).padStart(2, "0")}:${String(date.minute).padStart(2, "0")}:${String(date.second).padStart(2, "0")}Z`;
|
|
7485
|
+
}
|
|
7486
|
+
function parseRfc822(value) {
|
|
7487
|
+
const match = value.match(RFC822);
|
|
7488
|
+
if (!match) return;
|
|
7489
|
+
const month = MONTHS[match[2]?.toLowerCase() ?? ""];
|
|
7490
|
+
if (!month) return;
|
|
7491
|
+
const day = match[1]?.padStart(2, "0");
|
|
7492
|
+
const year = match[3];
|
|
7493
|
+
const hour = match[4];
|
|
7494
|
+
const minute = match[5];
|
|
7495
|
+
const second = (match[6] ?? "00").padStart(2, "0");
|
|
7496
|
+
const zone = match[7];
|
|
7497
|
+
const tz = zone ? `${zone.slice(0, 3)}:${zone.slice(3)}` : "Z";
|
|
7498
|
+
return parseDate(`${year}-${String(month).padStart(2, "0")}-${day}T${hour}:${minute}:${second}${tz}`);
|
|
7499
|
+
}
|
|
7500
|
+
//#endregion
|
|
7501
|
+
//#region src/blog-feed-url.ts
|
|
6602
7502
|
/**
|
|
6603
|
-
*
|
|
7503
|
+
* Safe-URL checks for configured external blog feeds.
|
|
6604
7504
|
*/
|
|
6605
|
-
const
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
7505
|
+
const CONTROL_CHARS = /[\n\r\t\0]/;
|
|
7506
|
+
function isSafeFeedUrl(value) {
|
|
7507
|
+
const trimmed = value.trim();
|
|
7508
|
+
if (!trimmed || CONTROL_CHARS.test(trimmed)) return false;
|
|
7509
|
+
try {
|
|
7510
|
+
const url = new URL(trimmed);
|
|
7511
|
+
if (url.protocol !== "https:") return false;
|
|
7512
|
+
if (url.username || url.password) return false;
|
|
7513
|
+
return !isBlockedFeedHost(url.hostname);
|
|
7514
|
+
} catch {
|
|
7515
|
+
return false;
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
function canonicalizeFeedItemUrl(value) {
|
|
7519
|
+
if (!isSafeFeedUrl(value)) return;
|
|
7520
|
+
const url = new URL(value.trim());
|
|
7521
|
+
url.hash = "";
|
|
7522
|
+
url.username = "";
|
|
7523
|
+
url.password = "";
|
|
7524
|
+
if (url.port === "443") url.port = "";
|
|
7525
|
+
url.hostname = url.hostname.toLowerCase();
|
|
7526
|
+
let href = url.href;
|
|
7527
|
+
if (url.pathname !== "/" && href.endsWith("/")) href = href.slice(0, -1);
|
|
7528
|
+
return href;
|
|
7529
|
+
}
|
|
7530
|
+
function isBlockedFeedHost(hostname) {
|
|
7531
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
7532
|
+
if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true;
|
|
7533
|
+
if (host.includes(":")) return isBlockedIPv6(host);
|
|
7534
|
+
return isIPv4(host) ? isBlockedIPv4(host) : false;
|
|
7535
|
+
}
|
|
7536
|
+
function isBlockedFeedAddress(address) {
|
|
7537
|
+
const value = address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
7538
|
+
if (value.includes(":")) return isBlockedIPv6(value);
|
|
7539
|
+
return isIPv4(value) ? isBlockedIPv4(value) : true;
|
|
7540
|
+
}
|
|
7541
|
+
function isIPv4(value) {
|
|
7542
|
+
const parts = value.split(".");
|
|
7543
|
+
if (parts.length !== 4) return false;
|
|
7544
|
+
return parts.every((part) => {
|
|
7545
|
+
const n = Number(part);
|
|
7546
|
+
return Number.isInteger(n) && n >= 0 && n <= 255 && String(n) === part;
|
|
7547
|
+
});
|
|
7548
|
+
}
|
|
7549
|
+
function isBlockedIPv4(ip) {
|
|
7550
|
+
const [a, b] = ip.split(".").map(Number);
|
|
7551
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
7552
|
+
}
|
|
7553
|
+
function isBlockedIPv6(ip) {
|
|
7554
|
+
if (ip === "::" || ip === "::1") return true;
|
|
7555
|
+
if (ip.startsWith("::ffff:")) {
|
|
7556
|
+
const mapped = ip.slice(7);
|
|
7557
|
+
return isIPv4(mapped) ? isBlockedIPv4(mapped) : true;
|
|
7558
|
+
}
|
|
7559
|
+
const first = Number.parseInt(ip.split(":")[0] ?? "", 16);
|
|
7560
|
+
if (!Number.isFinite(first)) return true;
|
|
7561
|
+
if (first >= 65152 && first <= 65215) return true;
|
|
7562
|
+
return (first & 65024) === 64512;
|
|
7563
|
+
}
|
|
7564
|
+
let installedNetwork = {};
|
|
7565
|
+
async function fetchBlogFeedBody(url, network = {}) {
|
|
7566
|
+
const timeoutMs = network.limits?.timeoutMs ?? installedNetwork.limits?.timeoutMs ?? 1e4;
|
|
7567
|
+
const maxBytes = network.limits?.maxBytes ?? installedNetwork.limits?.maxBytes ?? 1048576;
|
|
7568
|
+
const maxRedirects = network.limits?.maxRedirects ?? installedNetwork.limits?.maxRedirects ?? 5;
|
|
7569
|
+
const fetchFn = network.fetch ?? installedNetwork.fetch ?? defaultFetch;
|
|
7570
|
+
const lookup = network.lookup ?? installedNetwork.lookup ?? defaultLookup;
|
|
7571
|
+
const controller = new AbortController();
|
|
7572
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
7573
|
+
try {
|
|
7574
|
+
return await followFeed(url, fetchFn, lookup, controller.signal, maxBytes, maxRedirects);
|
|
7575
|
+
} catch (error) {
|
|
7576
|
+
if (isAbortError(error)) throw new Error("timeout");
|
|
7577
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
7578
|
+
} finally {
|
|
7579
|
+
clearTimeout(timer);
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
async function followFeed(startUrl, fetchFn, lookup, signal, maxBytes, maxRedirects) {
|
|
7583
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7584
|
+
let current = startUrl;
|
|
7585
|
+
for (let hops = 0; hops <= maxRedirects; hops += 1) {
|
|
7586
|
+
await assertSafeFeedTarget(current, lookup);
|
|
7587
|
+
if (seen.has(current)) throw new Error("too many redirects");
|
|
7588
|
+
seen.add(current);
|
|
7589
|
+
const response = await fetchFn(current, {
|
|
7590
|
+
method: "GET",
|
|
7591
|
+
redirect: "manual",
|
|
7592
|
+
signal,
|
|
7593
|
+
headers: {
|
|
7594
|
+
Accept: "application/rss+xml, application/atom+xml, application/xml, text/xml;q=0.9",
|
|
7595
|
+
"User-Agent": "ox-content-blog-feeds/1.0"
|
|
6623
7596
|
}
|
|
7597
|
+
});
|
|
7598
|
+
if (isRedirect(response.status)) {
|
|
7599
|
+
current = resolveRedirect(current, response.headers.get("location"));
|
|
6624
7600
|
continue;
|
|
6625
7601
|
}
|
|
6626
|
-
if (
|
|
6627
|
-
|
|
7602
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
7603
|
+
assertFeedContentType(response.headers.get("content-type"));
|
|
7604
|
+
return readBoundedBody(response, maxBytes);
|
|
6628
7605
|
}
|
|
6629
|
-
|
|
6630
|
-
return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
|
|
7606
|
+
throw new Error("too many redirects");
|
|
6631
7607
|
}
|
|
6632
|
-
function
|
|
6633
|
-
if (!
|
|
6634
|
-
const
|
|
6635
|
-
|
|
7608
|
+
async function assertSafeFeedTarget(url, lookup) {
|
|
7609
|
+
if (!isSafeFeedUrl(url)) throw new Error("unsafe URL");
|
|
7610
|
+
const hostname = new URL(url).hostname;
|
|
7611
|
+
const addresses = await lookup(hostname);
|
|
7612
|
+
if (addresses.length === 0 || addresses.some((address) => isBlockedFeedAddress(address))) throw new Error("private network");
|
|
6636
7613
|
}
|
|
6637
|
-
function
|
|
6638
|
-
return
|
|
7614
|
+
async function defaultLookup(hostname) {
|
|
7615
|
+
return (await (0, node_dns_promises.lookup)(hostname, {
|
|
7616
|
+
all: true,
|
|
7617
|
+
verbatim: true
|
|
7618
|
+
})).map((record) => record.address);
|
|
6639
7619
|
}
|
|
6640
|
-
function
|
|
6641
|
-
return
|
|
7620
|
+
function defaultFetch(input, init) {
|
|
7621
|
+
return fetch(input, init);
|
|
6642
7622
|
}
|
|
6643
|
-
function
|
|
6644
|
-
return
|
|
7623
|
+
function isRedirect(status) {
|
|
7624
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
6645
7625
|
}
|
|
6646
|
-
function
|
|
6647
|
-
|
|
7626
|
+
function resolveRedirect(current, location) {
|
|
7627
|
+
if (!location?.trim()) throw new Error("too many redirects");
|
|
7628
|
+
try {
|
|
7629
|
+
return new URL(location, current).href;
|
|
7630
|
+
} catch {
|
|
7631
|
+
throw new Error("unsafe URL");
|
|
7632
|
+
}
|
|
7633
|
+
}
|
|
7634
|
+
function assertFeedContentType(value) {
|
|
7635
|
+
if (!value) return;
|
|
7636
|
+
const type = value.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
7637
|
+
if (type === "text/html" || type === "application/xhtml+xml" || type === "application/json") throw new Error("not a feed");
|
|
7638
|
+
}
|
|
7639
|
+
async function readBoundedBody(response, maxBytes) {
|
|
7640
|
+
const length = Number(response.headers.get("content-length"));
|
|
7641
|
+
if (Number.isFinite(length) && length > maxBytes) throw new Error("oversized");
|
|
7642
|
+
const reader = response.body?.getReader();
|
|
7643
|
+
if (!reader) {
|
|
7644
|
+
const text = await response.text();
|
|
7645
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) throw new Error("oversized");
|
|
7646
|
+
return text;
|
|
7647
|
+
}
|
|
7648
|
+
const chunks = [];
|
|
7649
|
+
let total = 0;
|
|
7650
|
+
while (true) {
|
|
7651
|
+
const { done, value } = await reader.read();
|
|
7652
|
+
if (done) break;
|
|
7653
|
+
if (!value) continue;
|
|
7654
|
+
total += value.byteLength;
|
|
7655
|
+
if (total > maxBytes) {
|
|
7656
|
+
await reader.cancel();
|
|
7657
|
+
throw new Error("oversized");
|
|
7658
|
+
}
|
|
7659
|
+
chunks.push(value);
|
|
7660
|
+
}
|
|
7661
|
+
return new TextDecoder("utf-8").decode(concatBytes(chunks, total));
|
|
7662
|
+
}
|
|
7663
|
+
function concatBytes(chunks, total) {
|
|
7664
|
+
const out = new Uint8Array(total);
|
|
7665
|
+
let offset = 0;
|
|
7666
|
+
for (const chunk of chunks) {
|
|
7667
|
+
out.set(chunk, offset);
|
|
7668
|
+
offset += chunk.byteLength;
|
|
7669
|
+
}
|
|
7670
|
+
return out;
|
|
7671
|
+
}
|
|
7672
|
+
function isAbortError(error) {
|
|
7673
|
+
return error instanceof Error && (error.name === "AbortError" || error.message === "timeout");
|
|
7674
|
+
}
|
|
7675
|
+
//#endregion
|
|
7676
|
+
//#region src/blog-feed-parse.ts
|
|
7677
|
+
/**
|
|
7678
|
+
* RSS 2.0 / Atom 1.0 item extraction. HTML documents are rejected.
|
|
7679
|
+
*/
|
|
7680
|
+
const ITEM_BLOCK = /<(?:[\w.-]+:)?item\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?item>/gi;
|
|
7681
|
+
const ENTRY_BLOCK = /<(?:[\w.-]+:)?entry\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?entry>/gi;
|
|
7682
|
+
function parseBlogFeed(body, feedLanguage) {
|
|
7683
|
+
const xml = stripBom(body);
|
|
7684
|
+
if (looksLikeHtml(xml)) throw new Error("not a feed");
|
|
7685
|
+
if (!looksLikeXmlFeed(xml)) throw new Error("malformed XML");
|
|
7686
|
+
const channelLanguage = textChild(xml, ["language", "dc:language"]) ?? xmlLang(xml) ?? feedLanguage;
|
|
7687
|
+
const items = collectBlocks(xml, ITEM_BLOCK).map((block) => normalizeRssItem(block, channelLanguage));
|
|
7688
|
+
if (items.length > 0 || /<(?:[\w.-]+:)?rss\b/i.test(xml)) return items.filter((item) => item != null);
|
|
7689
|
+
return collectBlocks(xml, ENTRY_BLOCK).map((block) => normalizeAtomEntry(block, channelLanguage)).filter((item) => item != null);
|
|
7690
|
+
}
|
|
7691
|
+
function normalizeRssItem(block, fallbackLanguage) {
|
|
7692
|
+
const title = textChild(block, ["title"]);
|
|
7693
|
+
const guid = guidChild(block);
|
|
7694
|
+
const link = canonicalizeFeedItemUrl(textChild(block, ["link"]) ?? "") ?? (guid?.permalink ? canonicalizeFeedItemUrl(guid.value) : void 0);
|
|
7695
|
+
if (!title || !link) return;
|
|
7696
|
+
return {
|
|
7697
|
+
title,
|
|
7698
|
+
link,
|
|
7699
|
+
id: guid?.value || link,
|
|
7700
|
+
date: parseFeedDate(textChild(block, [
|
|
7701
|
+
"pubDate",
|
|
7702
|
+
"dc:date",
|
|
7703
|
+
"published",
|
|
7704
|
+
"updated"
|
|
7705
|
+
])),
|
|
7706
|
+
language: textChild(block, ["language", "dc:language"]) ?? xmlLang(block) ?? fallbackLanguage,
|
|
7707
|
+
summary: textChild(block, [
|
|
7708
|
+
"description",
|
|
7709
|
+
"summary",
|
|
7710
|
+
"content:encoded",
|
|
7711
|
+
"content"
|
|
7712
|
+
])
|
|
7713
|
+
};
|
|
7714
|
+
}
|
|
7715
|
+
function normalizeAtomEntry(block, fallbackLanguage) {
|
|
7716
|
+
const title = textChild(block, ["title"]);
|
|
7717
|
+
const link = canonicalizeFeedItemUrl(atomLink(block) ?? "");
|
|
7718
|
+
if (!title || !link) return;
|
|
7719
|
+
return {
|
|
7720
|
+
title,
|
|
7721
|
+
link,
|
|
7722
|
+
id: textChild(block, ["id"]) || link,
|
|
7723
|
+
date: parseFeedDate(textChild(block, [
|
|
7724
|
+
"published",
|
|
7725
|
+
"updated",
|
|
7726
|
+
"dc:date"
|
|
7727
|
+
])),
|
|
7728
|
+
language: xmlLang(block) ?? textChild(block, ["language", "dc:language"]) ?? fallbackLanguage,
|
|
7729
|
+
summary: textChild(block, [
|
|
7730
|
+
"summary",
|
|
7731
|
+
"content",
|
|
7732
|
+
"description"
|
|
7733
|
+
])
|
|
7734
|
+
};
|
|
7735
|
+
}
|
|
7736
|
+
function collectBlocks(xml, pattern) {
|
|
7737
|
+
return [...xml.matchAll(pattern)].flatMap((match) => match[1] ? [match[1]] : []);
|
|
7738
|
+
}
|
|
7739
|
+
function textChild(block, names) {
|
|
7740
|
+
for (const name of names) {
|
|
7741
|
+
const pattern = new RegExp(`<(?:[\\w.-]+:)?${escapeRegExp$1(localName(name))}(?:\\s[^>]*)?>([\\s\\S]*?)</(?:[\\w.-]+:)?${escapeRegExp$1(localName(name))}>`, "i");
|
|
7742
|
+
const match = block.match(pattern);
|
|
7743
|
+
if (match?.[1] != null) {
|
|
7744
|
+
const text = decodeXmlText(match[1]);
|
|
7745
|
+
if (text) return text;
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
function guidChild(block) {
|
|
7750
|
+
const match = block.match(/<(?:[\w.-]+:)?guid\b([^>]*)>([\s\S]*?)<\/(?:[\w.-]+:)?guid>/i);
|
|
7751
|
+
if (!match?.[2]) return;
|
|
7752
|
+
const value = decodeXmlText(match[2]);
|
|
7753
|
+
if (!value) return;
|
|
7754
|
+
const attrs = match[1] ?? "";
|
|
7755
|
+
return {
|
|
7756
|
+
value,
|
|
7757
|
+
permalink: !/isPermaLink\s*=\s*(['"]?)false\1/i.test(attrs)
|
|
7758
|
+
};
|
|
7759
|
+
}
|
|
7760
|
+
function atomLink(block) {
|
|
7761
|
+
const links = [];
|
|
7762
|
+
for (const match of block.matchAll(/<(?:[\w.-]+:)?link\b([^>]*)\/?>/gi)) {
|
|
7763
|
+
const href = attrValue(match[1] ?? "", "href");
|
|
7764
|
+
if (!href) continue;
|
|
7765
|
+
links.push({
|
|
7766
|
+
href,
|
|
7767
|
+
rel: (attrValue(match[1] ?? "", "rel") ?? "alternate").toLowerCase()
|
|
7768
|
+
});
|
|
7769
|
+
}
|
|
7770
|
+
return links.find((link) => link.rel === "alternate")?.href ?? links[0]?.href;
|
|
7771
|
+
}
|
|
7772
|
+
function xmlLang(block) {
|
|
7773
|
+
return block.match(/\bxml:lang\s*=\s*(['"])([^'"]+)\1/i)?.[2]?.trim() || void 0;
|
|
7774
|
+
}
|
|
7775
|
+
function attrValue(attrs, name) {
|
|
7776
|
+
return attrs.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, "i"))?.[2]?.trim() || void 0;
|
|
7777
|
+
}
|
|
7778
|
+
function decodeXmlText(value) {
|
|
7779
|
+
return decodeEntities(value.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, "$1").replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
7780
|
+
}
|
|
7781
|
+
function decodeEntities(value) {
|
|
7782
|
+
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, name) => {
|
|
7783
|
+
const lower = name.toLowerCase();
|
|
7784
|
+
if (lower === "amp") return "&";
|
|
7785
|
+
if (lower === "lt") return "<";
|
|
7786
|
+
if (lower === "gt") return ">";
|
|
7787
|
+
if (lower === "quot") return "\"";
|
|
7788
|
+
if (lower === "apos") return "'";
|
|
7789
|
+
if (lower.startsWith("#x")) {
|
|
7790
|
+
const code = Number.parseInt(lower.slice(2), 16);
|
|
7791
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
7792
|
+
}
|
|
7793
|
+
if (lower.startsWith("#")) {
|
|
7794
|
+
const code = Number.parseInt(lower.slice(1), 10);
|
|
7795
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
7796
|
+
}
|
|
7797
|
+
return entity;
|
|
7798
|
+
});
|
|
7799
|
+
}
|
|
7800
|
+
function looksLikeHtml(body) {
|
|
7801
|
+
const start = body.trim().slice(0, 256).toLowerCase();
|
|
7802
|
+
return start.startsWith("<!doctype html") || start.startsWith("<html");
|
|
7803
|
+
}
|
|
7804
|
+
function looksLikeXmlFeed(body) {
|
|
7805
|
+
return /<(?:[\w.-]+:)?(?:rss|feed|rdf:RDF|item|entry)\b/i.test(body);
|
|
7806
|
+
}
|
|
7807
|
+
function stripBom(value) {
|
|
7808
|
+
return value.charCodeAt(0) === 65279 ? value.slice(1) : value;
|
|
7809
|
+
}
|
|
7810
|
+
function localName(name) {
|
|
7811
|
+
const index = name.indexOf(":");
|
|
7812
|
+
return index === -1 ? name : name.slice(index + 1);
|
|
7813
|
+
}
|
|
7814
|
+
function escapeRegExp$1(value) {
|
|
7815
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6648
7816
|
}
|
|
6649
7817
|
//#endregion
|
|
6650
7818
|
//#region src/blog-html.ts
|
|
@@ -6660,13 +7828,13 @@ function isSafeBlogUrl(value) {
|
|
|
6660
7828
|
return trimmed.toLowerCase().startsWith("https:");
|
|
6661
7829
|
}
|
|
6662
7830
|
function postMetaMarkup(meta) {
|
|
6663
|
-
const parts = [`<p class="ox-blog-meta__reading-time">${escapeHtml
|
|
7831
|
+
const parts = [`<p class="ox-blog-meta__reading-time">${escapeHtml(String(meta.minutes))} min read</p>`];
|
|
6664
7832
|
if (meta.authors.length > 0) {
|
|
6665
7833
|
const items = meta.authors.map((author) => authorMarkup(author)).join("");
|
|
6666
7834
|
parts.push(`<ul class="ox-blog-meta__authors">${items}</ul>`);
|
|
6667
7835
|
}
|
|
6668
7836
|
if (meta.tags.length > 0) {
|
|
6669
|
-
const items = meta.tags.map((tag) => `<li><a href="${escapeHtml
|
|
7837
|
+
const items = meta.tags.map((tag) => `<li><a href="${escapeHtml(tag.href)}">${escapeHtml(tag.label)}</a></li>`).join("");
|
|
6670
7838
|
parts.push(`<ul class="ox-blog-meta__tags">${items}</ul>`);
|
|
6671
7839
|
}
|
|
6672
7840
|
return `<aside class="ox-blog-meta">${parts.join("")}</aside>\n`;
|
|
@@ -6674,25 +7842,25 @@ function postMetaMarkup(meta) {
|
|
|
6674
7842
|
function indexPageContent(items, pager) {
|
|
6675
7843
|
const list = items.map((item) => listItem(item)).join("");
|
|
6676
7844
|
const links = [];
|
|
6677
|
-
if (pager.newerHref) links.push(`<a href="${escapeHtml
|
|
6678
|
-
if (pager.olderHref) links.push(`<a href="${escapeHtml
|
|
7845
|
+
if (pager.newerHref) links.push(`<a href="${escapeHtml(pager.newerHref)}" rel="prev">Newer</a>`);
|
|
7846
|
+
if (pager.olderHref) links.push(`<a href="${escapeHtml(pager.olderHref)}" rel="next">Older</a>`);
|
|
6679
7847
|
return `<h1>Blog</h1><ul class="ox-blog">${list}</ul>${links.length > 0 ? `<nav class="ox-blog-pager">${links.join("")}</nav>` : ""}`;
|
|
6680
7848
|
}
|
|
6681
7849
|
function tagPageContent(label, items) {
|
|
6682
7850
|
const list = items.map((item) => listItem(item)).join("");
|
|
6683
|
-
return `<h1>${escapeHtml
|
|
7851
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog-tag">${list}</ul>`;
|
|
6684
7852
|
}
|
|
6685
7853
|
function archiveIndexContent(years) {
|
|
6686
|
-
return `<h1>Archive</h1><ul class="ox-blog-archive">${years.map((entry) => `<li><a href="${escapeHtml
|
|
7854
|
+
return `<h1>Archive</h1><ul class="ox-blog-archive">${years.map((entry) => `<li><a href="${escapeHtml(entry.href)}">${escapeHtml(entry.year)}</a></li>`).join("")}</ul>`;
|
|
6687
7855
|
}
|
|
6688
7856
|
function archiveYearContent(year, months, items) {
|
|
6689
|
-
const monthList = months.map((entry) => `<li><a href="${escapeHtml
|
|
7857
|
+
const monthList = months.map((entry) => `<li><a href="${escapeHtml(entry.href)}">${escapeHtml(entry.month)}</a></li>`).join("");
|
|
6690
7858
|
const posts = items.map((item) => listItem(item)).join("");
|
|
6691
|
-
return `<h1>${escapeHtml
|
|
7859
|
+
return `<h1>${escapeHtml(year)}</h1><ul class="ox-blog-archive-months">${monthList}</ul><ul class="ox-blog">${posts}</ul>`;
|
|
6692
7860
|
}
|
|
6693
7861
|
function archiveMonthContent(label, items) {
|
|
6694
7862
|
const list = items.map((item) => listItem(item)).join("");
|
|
6695
|
-
return `<h1>${escapeHtml
|
|
7863
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog">${list}</ul>`;
|
|
6696
7864
|
}
|
|
6697
7865
|
function siteHref$2(base, ...segments) {
|
|
6698
7866
|
const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
@@ -6706,17 +7874,18 @@ function containedPath$1(outDir, ...segments) {
|
|
|
6706
7874
|
if (resolved === root || !resolved.startsWith(prefix)) return;
|
|
6707
7875
|
return resolved;
|
|
6708
7876
|
}
|
|
6709
|
-
function escapeHtml
|
|
7877
|
+
function escapeHtml(value) {
|
|
6710
7878
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6711
7879
|
}
|
|
6712
7880
|
function authorMarkup(author) {
|
|
6713
|
-
const name = escapeHtml
|
|
7881
|
+
const name = escapeHtml(author.name);
|
|
6714
7882
|
const url = author.url?.trim();
|
|
6715
|
-
return `<li>${url && isSafeBlogUrl(url) ? `<a class="ox-blog-meta__name" href="${escapeHtml
|
|
7883
|
+
return `<li>${url && isSafeBlogUrl(url) ? `<a class="ox-blog-meta__name" href="${escapeHtml(url)}">${name}</a>` : `<span class="ox-blog-meta__name">${name}</span>`}${author.bio && author.bio.length > 0 ? `<p class="ox-blog-meta__bio">${escapeHtml(author.bio)}</p>` : ""}</li>`;
|
|
6716
7884
|
}
|
|
6717
7885
|
function listItem(item) {
|
|
6718
|
-
const time = item.dateLabel ? ` <time datetime="${escapeHtml
|
|
6719
|
-
return `<li><a href="${escapeHtml
|
|
7886
|
+
const time = item.dateLabel ? ` <time datetime="${escapeHtml(item.dateLabel)}">${escapeHtml(item.dateLabel)}</time>` : "";
|
|
7887
|
+
if (item.external) return `<li class="ox-blog-external" data-ox-blog-external="true"><a href="${escapeHtml(item.href)}" rel="external noopener noreferrer">${escapeHtml(item.title)}</a>${time}</li>`;
|
|
7888
|
+
return `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.title)}</a>${time}</li>`;
|
|
6720
7889
|
}
|
|
6721
7890
|
//#endregion
|
|
6722
7891
|
//#region src/blog-posts.ts
|
|
@@ -6822,7 +7991,8 @@ function toListItem(page) {
|
|
|
6822
7991
|
return {
|
|
6823
7992
|
title: page.title,
|
|
6824
7993
|
href: page.routePaths.href,
|
|
6825
|
-
dateLabel: parsed ? `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}` : void 0
|
|
7994
|
+
dateLabel: parsed ? `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}` : void 0,
|
|
7995
|
+
...page.external || page.frontmatter.external === true ? { external: true } : {}
|
|
6826
7996
|
};
|
|
6827
7997
|
}
|
|
6828
7998
|
function resolvePostAuthors(frontmatter, map) {
|
|
@@ -6882,6 +8052,158 @@ function dateField(value) {
|
|
|
6882
8052
|
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
6883
8053
|
}
|
|
6884
8054
|
//#endregion
|
|
8055
|
+
//#region src/blog-feeds.ts
|
|
8056
|
+
var BlogFeedError = class extends Error {
|
|
8057
|
+
issues;
|
|
8058
|
+
constructor(issues) {
|
|
8059
|
+
super(issues.join("\n"));
|
|
8060
|
+
this.name = "BlogFeedError";
|
|
8061
|
+
this.issues = issues;
|
|
8062
|
+
}
|
|
8063
|
+
};
|
|
8064
|
+
async function loadExternalBlogPosts(sources, network = {}) {
|
|
8065
|
+
const pages = [];
|
|
8066
|
+
const warnings = [];
|
|
8067
|
+
const fatals = [];
|
|
8068
|
+
if (sources.length === 0) return {
|
|
8069
|
+
pages,
|
|
8070
|
+
warnings,
|
|
8071
|
+
fatals
|
|
8072
|
+
};
|
|
8073
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
8074
|
+
const results = await Promise.all(sources.map(async (source) => {
|
|
8075
|
+
try {
|
|
8076
|
+
return {
|
|
8077
|
+
source,
|
|
8078
|
+
pages: parseBlogFeed(await cachedBody(source.url, bodies, network), source.language).map((item) => toExternalPage(item, source)).filter((page) => page != null)
|
|
8079
|
+
};
|
|
8080
|
+
} catch (error) {
|
|
8081
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8082
|
+
return {
|
|
8083
|
+
source,
|
|
8084
|
+
message: `[ox-content] blog feed ${source.url}: ${detail}`
|
|
8085
|
+
};
|
|
8086
|
+
}
|
|
8087
|
+
}));
|
|
8088
|
+
for (const result of results) {
|
|
8089
|
+
if ("pages" in result) {
|
|
8090
|
+
pages.push(...result.pages);
|
|
8091
|
+
continue;
|
|
8092
|
+
}
|
|
8093
|
+
if (result.source.onError === "error") fatals.push(result.message);
|
|
8094
|
+
else warnings.push(result.message);
|
|
8095
|
+
}
|
|
8096
|
+
return {
|
|
8097
|
+
pages,
|
|
8098
|
+
warnings,
|
|
8099
|
+
fatals
|
|
8100
|
+
};
|
|
8101
|
+
}
|
|
8102
|
+
function mergeBlogPosts(local, external) {
|
|
8103
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
8104
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
8105
|
+
const merged = [];
|
|
8106
|
+
for (const page of [...local, ...external]) {
|
|
8107
|
+
const keys = identityKeys(page);
|
|
8108
|
+
if (seenUrls.has(keys.url) || seenIds.has(keys.id)) continue;
|
|
8109
|
+
seenUrls.add(keys.url);
|
|
8110
|
+
seenIds.add(keys.id);
|
|
8111
|
+
merged.push(page);
|
|
8112
|
+
}
|
|
8113
|
+
return sortPosts(merged);
|
|
8114
|
+
}
|
|
8115
|
+
function cachedBody(url, cache, network) {
|
|
8116
|
+
const existing = cache.get(url);
|
|
8117
|
+
if (existing) return existing;
|
|
8118
|
+
const pending = fetchBlogFeedBody(url, network);
|
|
8119
|
+
cache.set(url, pending);
|
|
8120
|
+
return pending;
|
|
8121
|
+
}
|
|
8122
|
+
function toExternalPage(item, source) {
|
|
8123
|
+
const link = canonicalizeFeedItemUrl(item.link);
|
|
8124
|
+
if (!link) return;
|
|
8125
|
+
const id = item.id.trim() || link;
|
|
8126
|
+
const language = item.language ?? source.language;
|
|
8127
|
+
const author = source.author;
|
|
8128
|
+
return {
|
|
8129
|
+
title: item.title,
|
|
8130
|
+
inputPath: `external:${id}`,
|
|
8131
|
+
transformedHtml: "",
|
|
8132
|
+
external: true,
|
|
8133
|
+
routePaths: { href: link },
|
|
8134
|
+
frontmatter: {
|
|
8135
|
+
external: true,
|
|
8136
|
+
id,
|
|
8137
|
+
date: item.date ? feedDateIso(item.date) : void 0,
|
|
8138
|
+
language,
|
|
8139
|
+
author,
|
|
8140
|
+
summary: item.summary
|
|
8141
|
+
}
|
|
8142
|
+
};
|
|
8143
|
+
}
|
|
8144
|
+
function identityKeys(page) {
|
|
8145
|
+
const explicitId = stringField(page.frontmatter.id);
|
|
8146
|
+
const canonical = stringField(page.frontmatter.canonical);
|
|
8147
|
+
const href = page.routePaths.href;
|
|
8148
|
+
const url = canonicalizeFeedItemUrl(canonical ?? "") ?? canonicalizeFeedItemUrl(href) ?? href;
|
|
8149
|
+
return {
|
|
8150
|
+
url,
|
|
8151
|
+
id: explicitId || url
|
|
8152
|
+
};
|
|
8153
|
+
}
|
|
8154
|
+
function stringField(value) {
|
|
8155
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8156
|
+
}
|
|
8157
|
+
//#endregion
|
|
8158
|
+
//#region src/blog-reading.ts
|
|
8159
|
+
/**
|
|
8160
|
+
* Deterministic blog reading-time estimates.
|
|
8161
|
+
*/
|
|
8162
|
+
const LATIN_WORDS_PER_MINUTE = 200;
|
|
8163
|
+
const CJK_CHARS_PER_MINUTE = 500;
|
|
8164
|
+
function readingTimeMinutes(markdown) {
|
|
8165
|
+
const body = stripInlineCode(stripFences(stripFrontmatter(markdown)));
|
|
8166
|
+
let latin = 0;
|
|
8167
|
+
let cjk = 0;
|
|
8168
|
+
let latinRun = false;
|
|
8169
|
+
for (const char of body) {
|
|
8170
|
+
const code = char.codePointAt(0) ?? 0;
|
|
8171
|
+
if (isCjkCodePoint(code)) {
|
|
8172
|
+
cjk += 1;
|
|
8173
|
+
latinRun = false;
|
|
8174
|
+
continue;
|
|
8175
|
+
}
|
|
8176
|
+
if (isLatinWordChar(code)) {
|
|
8177
|
+
if (!latinRun) {
|
|
8178
|
+
latin += 1;
|
|
8179
|
+
latinRun = true;
|
|
8180
|
+
}
|
|
8181
|
+
continue;
|
|
8182
|
+
}
|
|
8183
|
+
if (char === "'" || char === "’") continue;
|
|
8184
|
+
latinRun = false;
|
|
8185
|
+
}
|
|
8186
|
+
if (latin === 0 && cjk === 0) return 0;
|
|
8187
|
+
return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
|
|
8188
|
+
}
|
|
8189
|
+
function stripFrontmatter(markdown) {
|
|
8190
|
+
if (!markdown.startsWith("---")) return markdown;
|
|
8191
|
+
const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
8192
|
+
return match ? markdown.slice(match[0].length) : markdown;
|
|
8193
|
+
}
|
|
8194
|
+
function stripFences(text) {
|
|
8195
|
+
return text.replace(/```[\s\S]*?(?:```|$)/g, " ");
|
|
8196
|
+
}
|
|
8197
|
+
function stripInlineCode(text) {
|
|
8198
|
+
return text.replace(/`[^`\n]*`/g, " ");
|
|
8199
|
+
}
|
|
8200
|
+
function isCjkCodePoint(code) {
|
|
8201
|
+
return code >= 12352 && code <= 12543 || code >= 12784 && code <= 12799 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 44032 && code <= 55215 || code >= 4352 && code <= 4607;
|
|
8202
|
+
}
|
|
8203
|
+
function isLatinWordChar(code) {
|
|
8204
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
|
|
8205
|
+
}
|
|
8206
|
+
//#endregion
|
|
6885
8207
|
//#region src/blog-pages.ts
|
|
6886
8208
|
/**
|
|
6887
8209
|
* Generated blog index, tag, and archive pages.
|
|
@@ -6893,7 +8215,7 @@ async function injectBlogPostMeta(input) {
|
|
|
6893
8215
|
if (posts === void 0) return;
|
|
6894
8216
|
const listedPaths = new Set(posts.map((page) => page.inputPath));
|
|
6895
8217
|
for (const page of input.pages) {
|
|
6896
|
-
if (!listedPaths.has(page.inputPath)) continue;
|
|
8218
|
+
if (!listedPaths.has(page.inputPath) || page.external === true) continue;
|
|
6897
8219
|
const markdown = await readMarkdown(page.inputPath);
|
|
6898
8220
|
page.transformedHtml = postMetaMarkup({
|
|
6899
8221
|
authors: resolvePostAuthors(page.frontmatter, input.options.authors),
|
|
@@ -6926,8 +8248,9 @@ async function appendBlogPages(input) {
|
|
|
6926
8248
|
input.errors.push(AMBIGUOUS_COLLECTION);
|
|
6927
8249
|
return;
|
|
6928
8250
|
}
|
|
6929
|
-
const
|
|
6930
|
-
if (
|
|
8251
|
+
const local = selectBlogPosts(input.listedPages, input.options, input.srcDir, input.collections);
|
|
8252
|
+
if (local === void 0) return;
|
|
8253
|
+
const posts = await collectIndexPosts(local, input.options, input.errors, input.feedNetwork);
|
|
6931
8254
|
for (const spec of blogPageSpecs(posts, input.options, input.outDir, input.base)) try {
|
|
6932
8255
|
input.generatedPages.push({
|
|
6933
8256
|
inputPath: spec.outputPath,
|
|
@@ -7022,6 +8345,14 @@ function blogPageSpecs(posts, options, outDir, base) {
|
|
|
7022
8345
|
}
|
|
7023
8346
|
return pages;
|
|
7024
8347
|
}
|
|
8348
|
+
async function collectIndexPosts(local, options, errors, network) {
|
|
8349
|
+
if (options.feeds.length === 0) return local;
|
|
8350
|
+
const loaded = await loadExternalBlogPosts(options.feeds, network);
|
|
8351
|
+
errors.push(...loaded.warnings);
|
|
8352
|
+
for (const warning of loaded.warnings) console.warn(warning);
|
|
8353
|
+
if (loaded.fatals.length > 0) throw new BlogFeedError(loaded.fatals);
|
|
8354
|
+
return mergeBlogPosts(local, loaded.pages);
|
|
8355
|
+
}
|
|
7025
8356
|
async function readMarkdown(inputPath) {
|
|
7026
8357
|
try {
|
|
7027
8358
|
return await node_fs_promises.readFile(inputPath, "utf8");
|
|
@@ -7534,59 +8865,6 @@ function generateSearchModule(options, indexPath) {
|
|
|
7534
8865
|
return require_vitepress.importNapiModuleSync().generateSearchModuleFromOptions(toLocalSearchRuntimeOptions(options), indexPath);
|
|
7535
8866
|
}
|
|
7536
8867
|
//#endregion
|
|
7537
|
-
//#region src/versions-html.ts
|
|
7538
|
-
function versionSwitcherMarkup(links, badge) {
|
|
7539
|
-
if (links.length === 0) return "";
|
|
7540
|
-
const current = links.find((link) => link.current) ?? links[0];
|
|
7541
|
-
const items = links.map((link) => {
|
|
7542
|
-
const label = `${escapeHtml(link.label)}${badgeMarkup(link, badge)}`;
|
|
7543
|
-
if (link.current || !isSafeHref(link.href)) return `<li><span aria-current="page">${label}</span></li>`;
|
|
7544
|
-
return `<li><a href="${escapeHtml(link.href)}">${label}</a></li>`;
|
|
7545
|
-
}).join("");
|
|
7546
|
-
return `<nav class="ox-header-select ox-version-switcher" aria-label="Version"><button type="button" aria-expanded="false" aria-haspopup="true">${escapeHtml(current.label)}${badgeMarkup(current, badge)}</button><ul class="ox-header-select-menu">${items}</ul></nav><script>(function(){var n=document.currentScript&&document.currentScript.previousElementSibling;if(!n||!n.classList.contains("ox-version-switcher"))return;var b=n.querySelector("button");if(!b)return;function closeOthers(){document.querySelectorAll(".header-nav-dropdown > button[aria-expanded='true'], .ox-locale-switcher > button[aria-expanded='true']").forEach(function(btn){btn.setAttribute("aria-expanded","false");});}b.addEventListener("click",function(e){e.stopPropagation();var o=b.getAttribute("aria-expanded")==="true";closeOthers();b.setAttribute("aria-expanded",o?"false":"true");});document.addEventListener("click",function(e){if(!n.contains(e.target))b.setAttribute("aria-expanded","false");});document.addEventListener("keydown",function(e){if(e.key==="Escape"){b.setAttribute("aria-expanded","false");b.focus();}});})()<\/script>`;
|
|
7547
|
-
}
|
|
7548
|
-
function versionBannerMarkup(kind) {
|
|
7549
|
-
if (kind === "unreleased") return `<aside class="ox-version-banner ox-version-banner--unreleased" role="status">This documentation describes an unreleased version.</aside>`;
|
|
7550
|
-
if (kind === "unmaintained") return `<aside class="ox-version-banner ox-version-banner--unmaintained" role="status">This documentation is unmaintained.</aside>`;
|
|
7551
|
-
return "";
|
|
7552
|
-
}
|
|
7553
|
-
function injectVersionChrome(html, switcher, banner, searchFrom, searchTo) {
|
|
7554
|
-
let next = html;
|
|
7555
|
-
if (banner) next = next.replace(/<body([^>]*)>/, `<body$1>${banner}`);
|
|
7556
|
-
if (switcher) {
|
|
7557
|
-
if (next.includes("<div class=\"header-actions\">")) next = next.replace("<div class=\"header-actions\">", `<div class="header-actions">${switcher}`);
|
|
7558
|
-
else if (next.includes("</header>")) next = next.replace("</header>", `${switcher}</header>`);
|
|
7559
|
-
}
|
|
7560
|
-
if (searchTo && isSafeHref(searchTo)) next = next.replace(/<html([^>]*)>/i, (match, attrs) => {
|
|
7561
|
-
if (/\sdata-ox-search-index=/.test(attrs)) return match;
|
|
7562
|
-
return `<html${attrs} data-ox-search-index="${escapeHtml(searchTo)}">`;
|
|
7563
|
-
});
|
|
7564
|
-
if (searchFrom && searchTo && searchFrom !== searchTo && isSafeHref(searchTo)) {
|
|
7565
|
-
next = next.split(searchFrom).join(searchTo);
|
|
7566
|
-
const script = `<script>(function(){var f=${JSON.stringify(searchFrom)},t=${JSON.stringify(searchTo)};var o=window.fetch;window.fetch=function(i,n){if(typeof i==="string"&&i.indexOf(f)!==-1)i=i.split(f).join(t);return o.call(this,i,n);};})()<\/script>`;
|
|
7567
|
-
next = next.includes("</body>") ? next.replace("</body>", `${script}</body>`) : `${next}${script}`;
|
|
7568
|
-
}
|
|
7569
|
-
return next;
|
|
7570
|
-
}
|
|
7571
|
-
function searchIndexUrl(base, prefix) {
|
|
7572
|
-
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
7573
|
-
return prefix ? `${root}${prefix}/search-index.json` : `${root}search-index.json`;
|
|
7574
|
-
}
|
|
7575
|
-
function isSafeHref(href) {
|
|
7576
|
-
const trimmed = href.trim();
|
|
7577
|
-
if (!trimmed || trimmed.startsWith("//")) return false;
|
|
7578
|
-
const lower = trimmed.replace(/\s+/g, "").toLowerCase();
|
|
7579
|
-
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) return false;
|
|
7580
|
-
return trimmed.startsWith("/") || trimmed.startsWith("./") || !trimmed.includes(":");
|
|
7581
|
-
}
|
|
7582
|
-
function escapeHtml(value) {
|
|
7583
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
7584
|
-
}
|
|
7585
|
-
function badgeMarkup(link, badge) {
|
|
7586
|
-
if (!badge || !link.banner) return "";
|
|
7587
|
-
return `<span class="ox-version-badge">${link.banner === "unreleased" ? "unreleased" : "unmaintained"}</span>`;
|
|
7588
|
-
}
|
|
7589
|
-
//#endregion
|
|
7590
8868
|
//#region src/versions.ts
|
|
7591
8869
|
/**
|
|
7592
8870
|
* Opt-in documentation versioning: prefixes, snapshots, and header chrome.
|
|
@@ -7717,7 +8995,13 @@ async function writeSnapshotSearchIndex(input) {
|
|
|
7717
8995
|
function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
|
|
7718
8996
|
if (!options.enabled) return html;
|
|
7719
8997
|
const active = options.entries.find((entry) => entry.id === activeId);
|
|
7720
|
-
return injectVersionChrome(html, options.switcher ? versionSwitcherMarkup(versionLinks(options, activeId, siblingPath, base, existingHrefs), options.badge) : "", versionBannerMarkup(active?.banner), searchIndexUrl(base, currentVersionPrefix(options)), searchIndexUrl(base, active?.prefix ?? ""))
|
|
8998
|
+
return injectSearchVersionFilters(injectVersionChrome(html, options.switcher ? versionSwitcherMarkup(versionLinks(options, activeId, siblingPath, base, existingHrefs), options.badge) : "", versionBannerMarkup(active?.banner), searchIndexUrl(base, currentVersionPrefix(options)), searchIndexUrl(base, active?.prefix ?? "")), options.entries.map((entry) => ({
|
|
8999
|
+
id: entry.id,
|
|
9000
|
+
label: entry.label,
|
|
9001
|
+
prefix: entry.prefix,
|
|
9002
|
+
indexUrl: searchIndexUrl(base, entry.prefix),
|
|
9003
|
+
current: entry.id === activeId
|
|
9004
|
+
})));
|
|
7721
9005
|
}
|
|
7722
9006
|
function sanitizePrefix(prefix) {
|
|
7723
9007
|
const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
|
|
@@ -7754,21 +9038,182 @@ function normalizeEntries(entries) {
|
|
|
7754
9038
|
banner: normalizeBanner(entry.banner)
|
|
7755
9039
|
});
|
|
7756
9040
|
}
|
|
7757
|
-
return resolved;
|
|
9041
|
+
return resolved;
|
|
9042
|
+
}
|
|
9043
|
+
function normalizeBanner(value) {
|
|
9044
|
+
return value === "unreleased" || value === "unmaintained" ? value : false;
|
|
9045
|
+
}
|
|
9046
|
+
function siteHref$1(base, prefix, rest) {
|
|
9047
|
+
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
9048
|
+
const parts = [prefix, rest].filter((part) => part && part !== "/");
|
|
9049
|
+
return parts.length === 0 ? root : `${root}${parts.join("/")}/`;
|
|
9050
|
+
}
|
|
9051
|
+
function relativeUrl(outputPath, outDir) {
|
|
9052
|
+
const rel = node_path.posix.normalize(node_path.relative(node_path.resolve(outDir), node_path.resolve(outputPath)).replaceAll(node_path.sep, "/"));
|
|
9053
|
+
if (rel.startsWith("..")) return "";
|
|
9054
|
+
const dir = rel.endsWith("/index.html") ? rel.slice(0, -11) : rel.replace(/\.html$/, "");
|
|
9055
|
+
return dir === "." ? "" : dir;
|
|
9056
|
+
}
|
|
9057
|
+
//#endregion
|
|
9058
|
+
//#region src/resources-dedupe.ts
|
|
9059
|
+
/**
|
|
9060
|
+
* Site-wide content-addressed emit for identical page-resource bytes.
|
|
9061
|
+
*
|
|
9062
|
+
* Hashing streams the file. The first digest+extension pair writes once;
|
|
9063
|
+
* later pages reuse that path. Image decode is never used here.
|
|
9064
|
+
*/
|
|
9065
|
+
const DEDUPE_ASSET_DIR = node_path.join("assets", "content");
|
|
9066
|
+
const TRANSFORM_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
9067
|
+
"width",
|
|
9068
|
+
"w",
|
|
9069
|
+
"height",
|
|
9070
|
+
"h",
|
|
9071
|
+
"crop",
|
|
9072
|
+
"format"
|
|
9073
|
+
]);
|
|
9074
|
+
function createResourceDedupeStore() {
|
|
9075
|
+
return {
|
|
9076
|
+
canonical: /* @__PURE__ */ new Map(),
|
|
9077
|
+
hashes: /* @__PURE__ */ new Map()
|
|
9078
|
+
};
|
|
9079
|
+
}
|
|
9080
|
+
function normalizeDedupeExt(ext) {
|
|
9081
|
+
const value = ext.replace(/^\./, "").trim().toLowerCase();
|
|
9082
|
+
if (value === "jpeg") return "jpg";
|
|
9083
|
+
return value || "bin";
|
|
9084
|
+
}
|
|
9085
|
+
function canonicalPublicPath(base, digest, ext) {
|
|
9086
|
+
return `${!base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`}${DEDUPE_ASSET_DIR.split(node_path.sep).join("/")}/${digest}.${ext}`;
|
|
9087
|
+
}
|
|
9088
|
+
function canonicalAbsolutePath(outDir, digest, ext) {
|
|
9089
|
+
return node_path.join(outDir, DEDUPE_ASSET_DIR, `${digest}.${ext}`);
|
|
9090
|
+
}
|
|
9091
|
+
/**
|
|
9092
|
+
* SHA-256 of emitted bytes plus a NUL and the serving extension so the
|
|
9093
|
+
* same payload cannot be served under an incompatible media type.
|
|
9094
|
+
*/
|
|
9095
|
+
async function hashResourceFile(filePath, ext, store, reuseKey) {
|
|
9096
|
+
const cached = store.hashes.get(reuseKey);
|
|
9097
|
+
if (cached) return cached;
|
|
9098
|
+
const hash = (0, node_crypto.createHash)("sha256");
|
|
9099
|
+
for await (const chunk of (0, node_fs.createReadStream)(filePath)) hash.update(chunk);
|
|
9100
|
+
hash.update("\0");
|
|
9101
|
+
hash.update(ext);
|
|
9102
|
+
const digest = hash.digest("hex");
|
|
9103
|
+
store.hashes.set(reuseKey, digest);
|
|
9104
|
+
return digest;
|
|
9105
|
+
}
|
|
9106
|
+
async function emitCanonicalResource(store, input) {
|
|
9107
|
+
const key = `${input.digest}\0${input.ext}`;
|
|
9108
|
+
const existing = store.canonical.get(key);
|
|
9109
|
+
const publicPath = canonicalPublicPath(input.base, input.digest, input.ext);
|
|
9110
|
+
if (existing) return {
|
|
9111
|
+
asset: {
|
|
9112
|
+
digest: input.digest,
|
|
9113
|
+
ext: input.ext,
|
|
9114
|
+
absolutePath: existing,
|
|
9115
|
+
publicPath
|
|
9116
|
+
},
|
|
9117
|
+
wrote: false
|
|
9118
|
+
};
|
|
9119
|
+
const absolutePath = canonicalAbsolutePath(input.outDir, input.digest, input.ext);
|
|
9120
|
+
await node_fs_promises.mkdir(node_path.dirname(absolutePath), { recursive: true });
|
|
9121
|
+
await node_fs_promises.copyFile(input.sourcePath, absolutePath);
|
|
9122
|
+
store.canonical.set(key, absolutePath);
|
|
9123
|
+
return {
|
|
9124
|
+
asset: {
|
|
9125
|
+
digest: input.digest,
|
|
9126
|
+
ext: input.ext,
|
|
9127
|
+
absolutePath,
|
|
9128
|
+
publicPath
|
|
9129
|
+
},
|
|
9130
|
+
wrote: true
|
|
9131
|
+
};
|
|
9132
|
+
}
|
|
9133
|
+
/**
|
|
9134
|
+
* Prefer a hard link at the original output path. `link` failure removes
|
|
9135
|
+
* any stale alias and copies so a shared inode is never overwritten.
|
|
9136
|
+
*/
|
|
9137
|
+
async function linkOrCopyAlias(canonical, alias, linker = node_fs_promises.link) {
|
|
9138
|
+
await node_fs_promises.mkdir(node_path.dirname(alias), { recursive: true });
|
|
9139
|
+
try {
|
|
9140
|
+
await linker(canonical, alias);
|
|
9141
|
+
return "link";
|
|
9142
|
+
} catch {
|
|
9143
|
+
await node_fs_promises.rm(alias, { force: true });
|
|
9144
|
+
}
|
|
9145
|
+
try {
|
|
9146
|
+
await linker(canonical, alias);
|
|
9147
|
+
return "link";
|
|
9148
|
+
} catch {
|
|
9149
|
+
await node_fs_promises.copyFile(canonical, alias);
|
|
9150
|
+
return "copy";
|
|
9151
|
+
}
|
|
7758
9152
|
}
|
|
7759
|
-
|
|
7760
|
-
|
|
9153
|
+
/** Keep leftover search/hash; drop consumed transform params. */
|
|
9154
|
+
function rewriteToCanonicalUrl(originalSrc, canonicalPath) {
|
|
9155
|
+
const hashIndex = originalSrc.indexOf("#");
|
|
9156
|
+
const hash = hashIndex === -1 ? "" : originalSrc.slice(hashIndex);
|
|
9157
|
+
const withoutHash = hashIndex === -1 ? originalSrc : originalSrc.slice(0, hashIndex);
|
|
9158
|
+
const queryIndex = withoutHash.indexOf("?");
|
|
9159
|
+
return `${canonicalPath}${leftoverQuery(queryIndex === -1 ? "" : withoutHash.slice(queryIndex + 1))}${hash}`;
|
|
7761
9160
|
}
|
|
7762
|
-
function
|
|
7763
|
-
|
|
7764
|
-
const
|
|
7765
|
-
|
|
9161
|
+
function leftoverQuery(query) {
|
|
9162
|
+
if (!query) return "";
|
|
9163
|
+
const params = new URLSearchParams(query);
|
|
9164
|
+
for (const key of TRANSFORM_QUERY_KEYS) params.delete(key);
|
|
9165
|
+
const next = params.toString();
|
|
9166
|
+
return next ? `?${next}` : "";
|
|
7766
9167
|
}
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
9168
|
+
//#endregion
|
|
9169
|
+
//#region src/resources-html.ts
|
|
9170
|
+
/**
|
|
9171
|
+
* Collect local `src`, `poster`, and relevant `href` values from HTML tags.
|
|
9172
|
+
*/
|
|
9173
|
+
const RESOURCE_TAG = /<(?:img|video|audio|source|track|a)\b[^>]*>/gi;
|
|
9174
|
+
const RESOURCE_ATTR = /\b(src|poster|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
9175
|
+
function collectResourceTags(html) {
|
|
9176
|
+
return (html.match(RESOURCE_TAG) ?? []).map((tag) => ({
|
|
9177
|
+
tag,
|
|
9178
|
+
refs: collectResourceRefs(tag)
|
|
9179
|
+
})).filter((entry) => entry.refs.length > 0);
|
|
9180
|
+
}
|
|
9181
|
+
function collectResourceRefs(tag) {
|
|
9182
|
+
const name = /^<([a-z]+)/i.exec(tag)?.[1]?.toLowerCase();
|
|
9183
|
+
if (!name) return [];
|
|
9184
|
+
const refs = [];
|
|
9185
|
+
RESOURCE_ATTR.lastIndex = 0;
|
|
9186
|
+
let match = RESOURCE_ATTR.exec(tag);
|
|
9187
|
+
while (match) {
|
|
9188
|
+
const attr = match[1].toLowerCase();
|
|
9189
|
+
if (isRelevantAttr(name, attr)) {
|
|
9190
|
+
const raw = match[2] ?? match[3] ?? "";
|
|
9191
|
+
refs.push({
|
|
9192
|
+
attr,
|
|
9193
|
+
raw,
|
|
9194
|
+
value: unescapeHtml(raw)
|
|
9195
|
+
});
|
|
9196
|
+
}
|
|
9197
|
+
match = RESOURCE_ATTR.exec(tag);
|
|
9198
|
+
}
|
|
9199
|
+
return refs;
|
|
9200
|
+
}
|
|
9201
|
+
function isRelevantAttr(tagName, attr) {
|
|
9202
|
+
if (tagName === "a") return attr === "href";
|
|
9203
|
+
if (attr === "href") return false;
|
|
9204
|
+
if (attr === "poster") return tagName === "video";
|
|
9205
|
+
return attr === "src";
|
|
9206
|
+
}
|
|
9207
|
+
function unescapeHtml(value) {
|
|
9208
|
+
return value.replaceAll("&", "&").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9209
|
+
}
|
|
9210
|
+
function escapeAttribute(value) {
|
|
9211
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9212
|
+
}
|
|
9213
|
+
function replaceAttributeRaw(tag, raw, nextRaw) {
|
|
9214
|
+
const index = tag.indexOf(raw);
|
|
9215
|
+
if (index === -1) return tag;
|
|
9216
|
+
return tag.slice(0, index) + nextRaw + tag.slice(index + raw.length);
|
|
7772
9217
|
}
|
|
7773
9218
|
//#endregion
|
|
7774
9219
|
//#region src/resources-jpeg.ts
|
|
@@ -8811,129 +10256,23 @@ function coverCrop(image, width, height) {
|
|
|
8811
10256
|
return cropImage(scaled, Math.max(0, Math.floor((scaled.width - width) / 2)), Math.max(0, Math.floor((scaled.height - height) / 2)), width, height);
|
|
8812
10257
|
}
|
|
8813
10258
|
//#endregion
|
|
8814
|
-
//#region src/resources-
|
|
10259
|
+
//#region src/resources-write.ts
|
|
8815
10260
|
/**
|
|
8816
|
-
*
|
|
10261
|
+
* Transform cache writes for page resources.
|
|
8817
10262
|
*/
|
|
8818
|
-
|
|
8819
|
-
const SRC_ATTR = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
|
|
8820
|
-
async function processPageResources(input) {
|
|
8821
|
-
if (!input.options.enabled) return {
|
|
8822
|
-
html: input.html,
|
|
8823
|
-
files: [],
|
|
8824
|
-
errors: [],
|
|
8825
|
-
fatal: []
|
|
8826
|
-
};
|
|
8827
|
-
const bundleRoot = node_path.dirname(input.inputPath);
|
|
8828
|
-
const outputDir = node_path.dirname(input.outputPath);
|
|
8829
|
-
const files = [];
|
|
8830
|
-
const errors = [];
|
|
8831
|
-
const fatal = [];
|
|
8832
|
-
let html = input.html;
|
|
8833
|
-
const tags = input.html.match(IMG_TAG) ?? [];
|
|
8834
|
-
for (const tag of tags) {
|
|
8835
|
-
const srcMatch = tag.match(SRC_ATTR);
|
|
8836
|
-
const rawSrc = srcMatch?.[1] ?? srcMatch?.[2];
|
|
8837
|
-
if (!rawSrc) continue;
|
|
8838
|
-
const src = unescapeHtml(rawSrc);
|
|
8839
|
-
const parsed = parseResourceSrc(src);
|
|
8840
|
-
if (!parsed) continue;
|
|
8841
|
-
const resolved = resolveBundlePath(parsed.pathname, bundleRoot, input.srcDir);
|
|
8842
|
-
if (!resolved.ok) {
|
|
8843
|
-
const message = `[ox-content] page resource ${JSON.stringify(src)} on ${input.inputPath} is outside the page bundle`;
|
|
8844
|
-
errors.push(message);
|
|
8845
|
-
fatal.push(message);
|
|
8846
|
-
continue;
|
|
8847
|
-
}
|
|
8848
|
-
let stat;
|
|
8849
|
-
try {
|
|
8850
|
-
stat = await node_fs_promises.stat(resolved.absolute);
|
|
8851
|
-
} catch {
|
|
8852
|
-
const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
|
|
8853
|
-
errors.push(message);
|
|
8854
|
-
if (input.options.missing === "error") fatal.push(message);
|
|
8855
|
-
continue;
|
|
8856
|
-
}
|
|
8857
|
-
const transformError = validateTransform(parsed.transform, input.options);
|
|
8858
|
-
if (transformError) {
|
|
8859
|
-
const message = `[ox-content] ${transformError} for ${JSON.stringify(src)} on ${input.inputPath}`;
|
|
8860
|
-
errors.push(message);
|
|
8861
|
-
fatal.push(message);
|
|
8862
|
-
continue;
|
|
8863
|
-
}
|
|
8864
|
-
const hasTransform = hasPixelOrFormatTransform(parsed.transform);
|
|
8865
|
-
const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : node_path.basename(resolved.absolute);
|
|
8866
|
-
const outputFile = node_path.join(outputDir, outputName);
|
|
8867
|
-
try {
|
|
8868
|
-
if (hasTransform) await writeTransformedResource({
|
|
8869
|
-
sourcePath: resolved.absolute,
|
|
8870
|
-
outputFile,
|
|
8871
|
-
cacheDir: input.cacheDir,
|
|
8872
|
-
mtimeMs: stat.mtimeMs,
|
|
8873
|
-
transform: parsed.transform
|
|
8874
|
-
});
|
|
8875
|
-
else {
|
|
8876
|
-
await node_fs_promises.mkdir(outputDir, { recursive: true });
|
|
8877
|
-
await node_fs_promises.copyFile(resolved.absolute, outputFile);
|
|
8878
|
-
}
|
|
8879
|
-
files.push(outputFile);
|
|
8880
|
-
const rewritten = tag.replace(rawSrc, escapeAttribute(outputName));
|
|
8881
|
-
html = html.replace(tag, rewritten);
|
|
8882
|
-
} catch (error) {
|
|
8883
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
8884
|
-
const message = `[ox-content] failed to process page resource ${JSON.stringify(src)} on ${input.inputPath}: ${detail}`;
|
|
8885
|
-
errors.push(message);
|
|
8886
|
-
fatal.push(message);
|
|
8887
|
-
}
|
|
8888
|
-
}
|
|
8889
|
-
return {
|
|
8890
|
-
html,
|
|
8891
|
-
files,
|
|
8892
|
-
errors,
|
|
8893
|
-
fatal
|
|
8894
|
-
};
|
|
8895
|
-
}
|
|
8896
|
-
function resolveBundlePath(pathname, bundleRoot, contentRoot) {
|
|
8897
|
-
if (node_path.isAbsolute(pathname) || pathname.includes("\0")) return { ok: false };
|
|
8898
|
-
const absolute = node_path.resolve(bundleRoot, pathname);
|
|
8899
|
-
if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
|
|
8900
|
-
return {
|
|
8901
|
-
ok: true,
|
|
8902
|
-
absolute
|
|
8903
|
-
};
|
|
8904
|
-
}
|
|
8905
|
-
function validateTransform(transform, options) {
|
|
8906
|
-
if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
|
|
8907
|
-
if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
|
|
8908
|
-
}
|
|
8909
|
-
function hasPixelOrFormatTransform(transform) {
|
|
8910
|
-
return Boolean(transform.width || transform.height || transform.crop || transform.format);
|
|
8911
|
-
}
|
|
8912
|
-
function transformedFileName(pathname, transform, cacheKey) {
|
|
8913
|
-
const stem = node_path.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
|
|
8914
|
-
const ext = outputExtension(pathname, transform.format);
|
|
8915
|
-
return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
|
|
8916
|
-
}
|
|
8917
|
-
function outputExtension(pathname, format) {
|
|
8918
|
-
if (format === "jpeg") return "jpg";
|
|
8919
|
-
if (format) return format;
|
|
8920
|
-
const ext = node_path.extname(pathname).slice(1).toLowerCase();
|
|
8921
|
-
return ext === "jpeg" ? "jpg" : ext || "png";
|
|
8922
|
-
}
|
|
8923
|
-
async function writeTransformedResource(input) {
|
|
10263
|
+
async function ensureTransformedCache(input) {
|
|
8924
10264
|
const key = resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform);
|
|
8925
10265
|
const ext = node_path.extname(input.outputFile);
|
|
8926
10266
|
const cacheFile = node_path.join(input.cacheDir, `${key}${ext}`);
|
|
8927
10267
|
try {
|
|
8928
|
-
await node_fs_promises.
|
|
8929
|
-
return;
|
|
10268
|
+
await node_fs_promises.access(cacheFile);
|
|
10269
|
+
return cacheFile;
|
|
8930
10270
|
} catch {}
|
|
8931
10271
|
const output = transformResourceBuffer(await node_fs_promises.readFile(input.sourcePath), input.sourcePath, input.transform);
|
|
8932
10272
|
if (output.length > 8388608) throw new Error("transform produced an oversized file");
|
|
8933
|
-
await node_fs_promises.mkdir(node_path.dirname(input.outputFile), { recursive: true });
|
|
8934
10273
|
await node_fs_promises.mkdir(input.cacheDir, { recursive: true });
|
|
8935
10274
|
await node_fs_promises.writeFile(cacheFile, output);
|
|
8936
|
-
|
|
10275
|
+
return cacheFile;
|
|
8937
10276
|
}
|
|
8938
10277
|
function transformResourceBuffer(source, sourcePath, transform) {
|
|
8939
10278
|
const needsPixels = Boolean(transform.width || transform.height || transform.crop);
|
|
@@ -8976,11 +10315,221 @@ function formatFromPath(filePath) {
|
|
|
8976
10315
|
const ext = node_path.extname(filePath).slice(1).toLowerCase();
|
|
8977
10316
|
return ext === "jpg" ? "jpeg" : ext;
|
|
8978
10317
|
}
|
|
8979
|
-
|
|
8980
|
-
|
|
10318
|
+
//#endregion
|
|
10319
|
+
//#region src/resources-process.ts
|
|
10320
|
+
/**
|
|
10321
|
+
* Page-resource HTML rewriting and transform writes.
|
|
10322
|
+
*/
|
|
10323
|
+
const PAGE_EXTS = /* @__PURE__ */ new Set([
|
|
10324
|
+
".md",
|
|
10325
|
+
".markdown",
|
|
10326
|
+
".mdx",
|
|
10327
|
+
".html",
|
|
10328
|
+
".htm"
|
|
10329
|
+
]);
|
|
10330
|
+
async function processPageResources(input) {
|
|
10331
|
+
if (!input.options.enabled) return {
|
|
10332
|
+
html: input.html,
|
|
10333
|
+
files: [],
|
|
10334
|
+
errors: [],
|
|
10335
|
+
fatal: []
|
|
10336
|
+
};
|
|
10337
|
+
if (input.options.dedupe && !input.outDir) {
|
|
10338
|
+
const message = "[ox-content] resources.dedupe requires outDir";
|
|
10339
|
+
return {
|
|
10340
|
+
html: input.html,
|
|
10341
|
+
files: [],
|
|
10342
|
+
errors: [message],
|
|
10343
|
+
fatal: [message]
|
|
10344
|
+
};
|
|
10345
|
+
}
|
|
10346
|
+
const bundleRoot = node_path.dirname(input.inputPath);
|
|
10347
|
+
const outputDir = node_path.dirname(input.outputPath);
|
|
10348
|
+
const files = [];
|
|
10349
|
+
const errors = [];
|
|
10350
|
+
const fatal = [];
|
|
10351
|
+
let html = input.html;
|
|
10352
|
+
const store = input.options.dedupe ? input.dedupeStore ?? createResourceDedupeStore() : void 0;
|
|
10353
|
+
for (const { tag, refs } of collectResourceTags(input.html)) {
|
|
10354
|
+
let nextTag = tag;
|
|
10355
|
+
for (const ref of refs) {
|
|
10356
|
+
const result = await processResourceRef(input, {
|
|
10357
|
+
bundleRoot,
|
|
10358
|
+
outputDir,
|
|
10359
|
+
ref: ref.attr,
|
|
10360
|
+
src: ref.value,
|
|
10361
|
+
store
|
|
10362
|
+
});
|
|
10363
|
+
errors.push(...result.errors);
|
|
10364
|
+
fatal.push(...result.fatal);
|
|
10365
|
+
files.push(...result.files);
|
|
10366
|
+
if (result.rewrite) nextTag = replaceAttributeRaw(nextTag, ref.raw, escapeAttribute(result.rewrite));
|
|
10367
|
+
}
|
|
10368
|
+
if (nextTag !== tag) html = html.replace(tag, nextTag);
|
|
10369
|
+
}
|
|
10370
|
+
return {
|
|
10371
|
+
html,
|
|
10372
|
+
files,
|
|
10373
|
+
errors,
|
|
10374
|
+
fatal
|
|
10375
|
+
};
|
|
8981
10376
|
}
|
|
8982
|
-
function
|
|
8983
|
-
|
|
10377
|
+
async function processResourceRef(input, ctx) {
|
|
10378
|
+
const parsed = parseResourceSrc(ctx.src);
|
|
10379
|
+
if (!parsed) return {
|
|
10380
|
+
files: [],
|
|
10381
|
+
errors: [],
|
|
10382
|
+
fatal: []
|
|
10383
|
+
};
|
|
10384
|
+
const resolved = resolveBundlePath(parsed.pathname, ctx.bundleRoot, input.srcDir);
|
|
10385
|
+
if (!resolved.ok) {
|
|
10386
|
+
if (ctx.ref === "href") return {
|
|
10387
|
+
files: [],
|
|
10388
|
+
errors: [],
|
|
10389
|
+
fatal: []
|
|
10390
|
+
};
|
|
10391
|
+
const message = `[ox-content] page resource ${JSON.stringify(ctx.src)} on ${input.inputPath} is outside the page bundle`;
|
|
10392
|
+
return {
|
|
10393
|
+
files: [],
|
|
10394
|
+
errors: [message],
|
|
10395
|
+
fatal: [message]
|
|
10396
|
+
};
|
|
10397
|
+
}
|
|
10398
|
+
let stat;
|
|
10399
|
+
try {
|
|
10400
|
+
stat = await node_fs_promises.stat(resolved.absolute);
|
|
10401
|
+
} catch {
|
|
10402
|
+
if (ctx.ref === "href") return {
|
|
10403
|
+
files: [],
|
|
10404
|
+
errors: [],
|
|
10405
|
+
fatal: []
|
|
10406
|
+
};
|
|
10407
|
+
const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
|
|
10408
|
+
return {
|
|
10409
|
+
files: [],
|
|
10410
|
+
errors: [message],
|
|
10411
|
+
fatal: input.options.missing === "error" ? [message] : []
|
|
10412
|
+
};
|
|
10413
|
+
}
|
|
10414
|
+
const hrefToPage = ctx.ref === "href" && PAGE_EXTS.has(node_path.extname(resolved.absolute).toLowerCase());
|
|
10415
|
+
if (!stat.isFile() || hrefToPage) return {
|
|
10416
|
+
files: [],
|
|
10417
|
+
errors: [],
|
|
10418
|
+
fatal: []
|
|
10419
|
+
};
|
|
10420
|
+
const transformError = validateTransform(parsed.transform, input.options);
|
|
10421
|
+
if (transformError) {
|
|
10422
|
+
const message = `[ox-content] ${transformError} for ${JSON.stringify(ctx.src)} on ${input.inputPath}`;
|
|
10423
|
+
return {
|
|
10424
|
+
files: [],
|
|
10425
|
+
errors: [message],
|
|
10426
|
+
fatal: [message]
|
|
10427
|
+
};
|
|
10428
|
+
}
|
|
10429
|
+
const hasTransform = hasPixelOrFormatTransform(parsed.transform);
|
|
10430
|
+
const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : decodedBasename(parsed.pathname) || node_path.basename(resolved.absolute);
|
|
10431
|
+
const outputFile = node_path.join(ctx.outputDir, outputName);
|
|
10432
|
+
try {
|
|
10433
|
+
const materialized = hasTransform ? await ensureTransformedCache({
|
|
10434
|
+
sourcePath: resolved.absolute,
|
|
10435
|
+
outputFile,
|
|
10436
|
+
cacheDir: input.cacheDir,
|
|
10437
|
+
mtimeMs: stat.mtimeMs,
|
|
10438
|
+
transform: parsed.transform
|
|
10439
|
+
}) : resolved.absolute;
|
|
10440
|
+
if (ctx.store && input.outDir) return await emitDedupedResource({
|
|
10441
|
+
store: ctx.store,
|
|
10442
|
+
materialized,
|
|
10443
|
+
outputFile,
|
|
10444
|
+
src: ctx.src,
|
|
10445
|
+
sourcePath: resolved.absolute,
|
|
10446
|
+
mtimeMs: stat.mtimeMs,
|
|
10447
|
+
transform: parsed.transform,
|
|
10448
|
+
hasTransform,
|
|
10449
|
+
outDir: input.outDir,
|
|
10450
|
+
base: input.base ?? "/"
|
|
10451
|
+
});
|
|
10452
|
+
if (hasTransform) {
|
|
10453
|
+
await node_fs_promises.mkdir(ctx.outputDir, { recursive: true });
|
|
10454
|
+
await node_fs_promises.copyFile(materialized, outputFile);
|
|
10455
|
+
} else {
|
|
10456
|
+
await node_fs_promises.mkdir(ctx.outputDir, { recursive: true });
|
|
10457
|
+
await node_fs_promises.copyFile(resolved.absolute, outputFile);
|
|
10458
|
+
}
|
|
10459
|
+
return {
|
|
10460
|
+
files: [outputFile],
|
|
10461
|
+
errors: [],
|
|
10462
|
+
fatal: [],
|
|
10463
|
+
rewrite: outputName
|
|
10464
|
+
};
|
|
10465
|
+
} catch (error) {
|
|
10466
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10467
|
+
const message = `[ox-content] failed to process page resource ${JSON.stringify(ctx.src)} on ${input.inputPath}: ${detail}`;
|
|
10468
|
+
return {
|
|
10469
|
+
files: [],
|
|
10470
|
+
errors: [message],
|
|
10471
|
+
fatal: [message]
|
|
10472
|
+
};
|
|
10473
|
+
}
|
|
10474
|
+
}
|
|
10475
|
+
async function emitDedupedResource(input) {
|
|
10476
|
+
const ext = normalizeDedupeExt(node_path.extname(input.outputFile));
|
|
10477
|
+
const reuseKey = input.hasTransform ? resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform) : `${input.sourcePath}\0${input.mtimeMs}\0copy`;
|
|
10478
|
+
const digest = await hashResourceFile(input.materialized, ext, input.store, reuseKey);
|
|
10479
|
+
const { asset, wrote } = await emitCanonicalResource(input.store, {
|
|
10480
|
+
digest,
|
|
10481
|
+
ext,
|
|
10482
|
+
sourcePath: input.materialized,
|
|
10483
|
+
outDir: input.outDir,
|
|
10484
|
+
base: input.base
|
|
10485
|
+
});
|
|
10486
|
+
await linkOrCopyAlias(asset.absolutePath, input.outputFile);
|
|
10487
|
+
return {
|
|
10488
|
+
files: wrote ? [asset.absolutePath, input.outputFile] : [input.outputFile],
|
|
10489
|
+
errors: [],
|
|
10490
|
+
fatal: [],
|
|
10491
|
+
rewrite: rewriteToCanonicalUrl(input.src, asset.publicPath)
|
|
10492
|
+
};
|
|
10493
|
+
}
|
|
10494
|
+
function resolveBundlePath(pathname, bundleRoot, contentRoot) {
|
|
10495
|
+
let decoded;
|
|
10496
|
+
try {
|
|
10497
|
+
decoded = decodeURIComponent(pathname);
|
|
10498
|
+
} catch {
|
|
10499
|
+
decoded = pathname;
|
|
10500
|
+
}
|
|
10501
|
+
if (node_path.isAbsolute(decoded) || decoded.includes("\0")) return { ok: false };
|
|
10502
|
+
const absolute = node_path.resolve(bundleRoot, decoded);
|
|
10503
|
+
if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
|
|
10504
|
+
return {
|
|
10505
|
+
ok: true,
|
|
10506
|
+
absolute
|
|
10507
|
+
};
|
|
10508
|
+
}
|
|
10509
|
+
function validateTransform(transform, options) {
|
|
10510
|
+
if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
|
|
10511
|
+
if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
|
|
10512
|
+
}
|
|
10513
|
+
function hasPixelOrFormatTransform(transform) {
|
|
10514
|
+
return Boolean(transform.width || transform.height || transform.crop || transform.format);
|
|
10515
|
+
}
|
|
10516
|
+
function transformedFileName(pathname, transform, cacheKey) {
|
|
10517
|
+
const stem = node_path.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
|
|
10518
|
+
const ext = outputExtension(pathname, transform.format);
|
|
10519
|
+
return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
|
|
10520
|
+
}
|
|
10521
|
+
function outputExtension(pathname, format) {
|
|
10522
|
+
if (format === "jpeg") return "jpg";
|
|
10523
|
+
if (format) return format;
|
|
10524
|
+
const ext = node_path.extname(pathname).slice(1).toLowerCase();
|
|
10525
|
+
return ext === "jpeg" ? "jpg" : ext || "png";
|
|
10526
|
+
}
|
|
10527
|
+
function decodedBasename(pathname) {
|
|
10528
|
+
try {
|
|
10529
|
+
return node_path.basename(decodeURIComponent(pathname));
|
|
10530
|
+
} catch {
|
|
10531
|
+
return node_path.basename(pathname);
|
|
10532
|
+
}
|
|
8984
10533
|
}
|
|
8985
10534
|
//#endregion
|
|
8986
10535
|
//#region src/resources.ts
|
|
@@ -9015,19 +10564,22 @@ function resolveResourcesOptions(value) {
|
|
|
9015
10564
|
enabled: false,
|
|
9016
10565
|
formats: [...DEFAULT_FORMATS],
|
|
9017
10566
|
widths: [],
|
|
9018
|
-
missing: "error"
|
|
10567
|
+
missing: "error",
|
|
10568
|
+
dedupe: false
|
|
9019
10569
|
};
|
|
9020
10570
|
if (value === true) return {
|
|
9021
10571
|
enabled: true,
|
|
9022
10572
|
formats: [...DEFAULT_FORMATS],
|
|
9023
10573
|
widths: [],
|
|
9024
|
-
missing: "error"
|
|
10574
|
+
missing: "error",
|
|
10575
|
+
dedupe: false
|
|
9025
10576
|
};
|
|
9026
10577
|
return {
|
|
9027
10578
|
enabled: true,
|
|
9028
10579
|
formats: normalizeFormats(value.formats),
|
|
9029
10580
|
widths: normalizeWidths(value.widths),
|
|
9030
|
-
missing: value.missing === "warn" ? "warn" : "error"
|
|
10581
|
+
missing: value.missing === "warn" ? "warn" : "error",
|
|
10582
|
+
dedupe: value.dedupe === true
|
|
9031
10583
|
};
|
|
9032
10584
|
}
|
|
9033
10585
|
/**
|
|
@@ -9278,6 +10830,7 @@ function resolveSsgOptions(ssg) {
|
|
|
9278
10830
|
localeSwitcher: false,
|
|
9279
10831
|
a11y: false,
|
|
9280
10832
|
pageChrome: false,
|
|
10833
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9281
10834
|
notFound: resolveNotFoundOptions(void 0),
|
|
9282
10835
|
team: resolveTeamOptions(void 0),
|
|
9283
10836
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9299,6 +10852,7 @@ function resolveSsgOptions(ssg) {
|
|
|
9299
10852
|
localeSwitcher: false,
|
|
9300
10853
|
a11y: false,
|
|
9301
10854
|
pageChrome: false,
|
|
10855
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9302
10856
|
notFound: resolveNotFoundOptions(void 0),
|
|
9303
10857
|
team: resolveTeamOptions(void 0),
|
|
9304
10858
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9328,6 +10882,7 @@ function resolveSsgOptions(ssg) {
|
|
|
9328
10882
|
localeSwitcher: resolveLocaleSwitcherOption(ssg.localeSwitcher),
|
|
9329
10883
|
a11y: resolveA11yOption(ssg.a11y),
|
|
9330
10884
|
pageChrome: require_vitepress.resolvePageChromeOption(ssg.pageChrome),
|
|
10885
|
+
markdownSource: resolveMarkdownSourceOptions(ssg.markdownSource),
|
|
9331
10886
|
notFound: resolveNotFoundOptions(ssg.notFound),
|
|
9332
10887
|
team: resolveTeamOptions(ssg.team),
|
|
9333
10888
|
blog: resolveBlogOptions(ssg.blog),
|
|
@@ -9500,7 +11055,7 @@ function localeCodesFor(locales) {
|
|
|
9500
11055
|
async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales, pagination = false, readerChrome = false, breadcrumbs = false, localeSwitcher = false, localePaths, a11y = false, team = {
|
|
9501
11056
|
enabled: false,
|
|
9502
11057
|
members: []
|
|
9503
|
-
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl, headValidation = false) {
|
|
11058
|
+
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl, headValidation = false, defaultLocale) {
|
|
9504
11059
|
const mod = await require_vitepress.importNapiModule();
|
|
9505
11060
|
const tocForRust = pageData.toc.map(toRustTocEntry);
|
|
9506
11061
|
const navGroupsForRust = convertNavGroupsForRust(navGroups);
|
|
@@ -9584,7 +11139,11 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9584
11139
|
});
|
|
9585
11140
|
const html = typeof result === "string" ? result : result.html;
|
|
9586
11141
|
reportHeadDiagnostics(typeof result === "string" ? [] : result.diagnostics ?? [], headValidation);
|
|
9587
|
-
return html
|
|
11142
|
+
return injectSearchLocaleFilters(html, {
|
|
11143
|
+
locales: availableLocales ?? [],
|
|
11144
|
+
current: locale,
|
|
11145
|
+
defaultLocale: defaultLocale ?? availableLocales?.[0]?.code ?? "en"
|
|
11146
|
+
});
|
|
9588
11147
|
}
|
|
9589
11148
|
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
9590
11149
|
const optimized = (await require_vitepress.importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
@@ -9669,6 +11228,7 @@ async function buildSsg(options, root) {
|
|
|
9669
11228
|
applyPermalinkRoutes(context, collected);
|
|
9670
11229
|
errors.push(...collected.errors);
|
|
9671
11230
|
const { outputPages, listedPages } = applyPublishState(context, collected);
|
|
11231
|
+
context.markdownSourcePages.push(...outputPages);
|
|
9672
11232
|
remapPermalinkNav(context, listedPages);
|
|
9673
11233
|
await applyPageResources(context, outputPages, generatedFiles, errors);
|
|
9674
11234
|
await generateOgImageAssets(context, collected, generatedFiles, errors);
|
|
@@ -9746,7 +11306,8 @@ async function createBuildSsgContext(options, root, srcDir, outDir, markdownFile
|
|
|
9746
11306
|
navItems: resolveNavigationGroups(ssgOptions.navigation, base, ssgOptions.extension) ?? (ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension)),
|
|
9747
11307
|
siteName: await resolveSiteName$1(root, ssgOptions),
|
|
9748
11308
|
shouldGenerateOgImages: shouldGenerateOgImages(options),
|
|
9749
|
-
|
|
11309
|
+
markdownSourcePages: [],
|
|
11310
|
+
napi: ssgOptions.lastUpdated || ssgOptions.contributors || options.siteMaps?.enabled ? await require_vitepress.importNapiModule() : void 0
|
|
9750
11311
|
};
|
|
9751
11312
|
}
|
|
9752
11313
|
/**
|
|
@@ -9776,6 +11337,7 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9776
11337
|
if (!options?.enabled) return;
|
|
9777
11338
|
const cacheDir = path.join(context.root, ".cache", "ox-content-resources");
|
|
9778
11339
|
const fatal = [];
|
|
11340
|
+
const dedupeStore = options.dedupe ? createResourceDedupeStore() : void 0;
|
|
9779
11341
|
for (const page of pages) {
|
|
9780
11342
|
const processed = await processPageResources({
|
|
9781
11343
|
html: page.transformedHtml,
|
|
@@ -9783,7 +11345,10 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9783
11345
|
outputPath: page.routePaths.outputPath,
|
|
9784
11346
|
srcDir: context.srcDir,
|
|
9785
11347
|
options,
|
|
9786
|
-
cacheDir
|
|
11348
|
+
cacheDir,
|
|
11349
|
+
outDir: context.outDir,
|
|
11350
|
+
base: context.base,
|
|
11351
|
+
dedupeStore
|
|
9787
11352
|
});
|
|
9788
11353
|
page.transformedHtml = processed.html;
|
|
9789
11354
|
generatedFiles.push(...processed.files);
|
|
@@ -9857,7 +11422,8 @@ function applyPublishState(context, collected) {
|
|
|
9857
11422
|
};
|
|
9858
11423
|
}
|
|
9859
11424
|
async function transformSsgPage(context, inputPath) {
|
|
9860
|
-
const
|
|
11425
|
+
const content = await fs_promises.readFile(inputPath, "utf-8");
|
|
11426
|
+
const result = await transformMarkdown(content, inputPath, context.options, {
|
|
9861
11427
|
convertMdLinks: true,
|
|
9862
11428
|
baseUrl: context.base,
|
|
9863
11429
|
sourcePath: inputPath
|
|
@@ -9867,11 +11433,12 @@ async function transformSsgPage(context, inputPath) {
|
|
|
9867
11433
|
const title = extractTitle$1(transformedHtml, frontmatter);
|
|
9868
11434
|
return {
|
|
9869
11435
|
inputPath,
|
|
11436
|
+
source: content,
|
|
9870
11437
|
routePaths: getRoutePaths(inputPath, context.srcDir, context.outDir, context.base, context.ssgOptions.extension, context.ssgOptions.siteUrl),
|
|
9871
11438
|
transformedHtml,
|
|
9872
11439
|
title,
|
|
9873
11440
|
description: frontmatter.description,
|
|
9874
|
-
lastUpdated: context.ssgOptions.lastUpdated ? context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0 : void 0,
|
|
11441
|
+
lastUpdated: context.ssgOptions.lastUpdated || context.options.siteMaps?.enabled ? context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0 : void 0,
|
|
9875
11442
|
contributors: contributorsForPage(context, inputPath),
|
|
9876
11443
|
frontmatter,
|
|
9877
11444
|
toc: result.toc
|
|
@@ -9962,17 +11529,18 @@ async function generateHtmlPages(context, pageResults, collected, errors) {
|
|
|
9962
11529
|
async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
9963
11530
|
const { ogImageUrlMap } = collected;
|
|
9964
11531
|
const pageOgImage = context.shouldGenerateOgImages && ogImageUrlMap.has(pageResult.inputPath) ? ogImageUrlMap.get(pageResult.inputPath) : context.ssgOptions.ogImage;
|
|
11532
|
+
const markdownSource = pageMarkdownSourceHref(context, pageResult);
|
|
9965
11533
|
if (context.ssgOptions.render) {
|
|
9966
11534
|
const nav = context.versionNavigation ? rewriteVersionedNavGroups(context.navItems, context.versionNavigation) : context.navItems;
|
|
9967
|
-
return renderPage(toThemePageData(pageResult), {
|
|
11535
|
+
return applyMarkdownSourceAlternate(context, renderPage(toThemePageData(pageResult, markdownSource), {
|
|
9968
11536
|
theme: context.ssgOptions.render,
|
|
9969
11537
|
siteName: context.siteName,
|
|
9970
11538
|
base: context.base,
|
|
9971
11539
|
nav,
|
|
9972
|
-
pages: allPageResults.map(toThemePageData)
|
|
9973
|
-
});
|
|
11540
|
+
pages: allPageResults.map((page) => toThemePageData(page, pageMarkdownSourceHref(context, page)))
|
|
11541
|
+
}), markdownSource);
|
|
9974
11542
|
}
|
|
9975
|
-
if (context.ssgOptions.bare) return generateBarePage({
|
|
11543
|
+
if (context.ssgOptions.bare) return applyMarkdownSourceAlternate(context, generateBarePage({
|
|
9976
11544
|
title: pageResult.title,
|
|
9977
11545
|
content: pageResult.transformedHtml,
|
|
9978
11546
|
lang: context.ssgOptions.lang ?? getPageLocale(pageResult.routePaths.urlPath, context.options.i18n),
|
|
@@ -9983,7 +11551,7 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
9983
11551
|
head: context.ssgOptions.head,
|
|
9984
11552
|
bodyStart: context.ssgOptions.bodyStart,
|
|
9985
11553
|
bodyEnd: context.ssgOptions.bodyEnd
|
|
9986
|
-
});
|
|
11554
|
+
}), markdownSource);
|
|
9987
11555
|
const pageData = createSsgPageData(pageResult);
|
|
9988
11556
|
const versionNavigation = context.versionNavigation;
|
|
9989
11557
|
if (versionNavigation) {
|
|
@@ -10024,10 +11592,18 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
10024
11592
|
base: context.base,
|
|
10025
11593
|
roots: versionNavigation ? versionedLocaleRoots(versionNavigation, i18n.locales, i18n.defaultLocale, i18n.hideDefaultLocale) : void 0
|
|
10026
11594
|
}) : void 0;
|
|
10027
|
-
return generateHtmlPage(pageData, navItems, context.siteName, context.base, pageOgImage, theme, locale, i18n ? i18n.locales : void 0, context.ssgOptions.pagination, context.ssgOptions.readerChrome, context.ssgOptions.breadcrumbs, context.ssgOptions.localeSwitcher, localePaths, context.ssgOptions.a11y, context.ssgOptions.team ?? {
|
|
11595
|
+
return applyMarkdownSourceAlternate(context, await generateHtmlPage(pageData, navItems, context.siteName, context.base, pageOgImage, theme, locale, i18n ? i18n.locales : void 0, context.ssgOptions.pagination, context.ssgOptions.readerChrome, context.ssgOptions.breadcrumbs, context.ssgOptions.localeSwitcher, localePaths, context.ssgOptions.a11y, context.ssgOptions.team ?? {
|
|
10028
11596
|
enabled: false,
|
|
10029
11597
|
members: []
|
|
10030
|
-
}, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl, context.ssgOptions.headValidation);
|
|
11598
|
+
}, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl, context.ssgOptions.headValidation, i18n?.defaultLocale), markdownSource);
|
|
11599
|
+
}
|
|
11600
|
+
function pageMarkdownSourceHref(context, page) {
|
|
11601
|
+
if (!context.ssgOptions.markdownSource?.enabled || !shouldPublishMarkdownSource(page.frontmatter, context.options.publishState)) return;
|
|
11602
|
+
return markdownSourceHref(page.routePaths.urlPath, context.base);
|
|
11603
|
+
}
|
|
11604
|
+
function applyMarkdownSourceAlternate(context, html, href) {
|
|
11605
|
+
if (!href || !context.ssgOptions.markdownSource?.alternate) return html;
|
|
11606
|
+
return injectMarkdownSourceAlternate(html, href);
|
|
10031
11607
|
}
|
|
10032
11608
|
function rewritePagerOverride(pager, context) {
|
|
10033
11609
|
return pager?.href ? {
|
|
@@ -10036,7 +11612,7 @@ function rewritePagerOverride(pager, context) {
|
|
|
10036
11612
|
} : pager;
|
|
10037
11613
|
}
|
|
10038
11614
|
/** Maps an internal page result onto the theme renderer's page shape. */
|
|
10039
|
-
function toThemePageData(pageResult) {
|
|
11615
|
+
function toThemePageData(pageResult, markdownSource) {
|
|
10040
11616
|
return {
|
|
10041
11617
|
title: pageResult.title,
|
|
10042
11618
|
description: pageResult.description,
|
|
@@ -10046,6 +11622,7 @@ function toThemePageData(pageResult) {
|
|
|
10046
11622
|
contributors: pageResult.contributors,
|
|
10047
11623
|
path: pageResult.inputPath,
|
|
10048
11624
|
url: pageResult.routePaths.href,
|
|
11625
|
+
markdownSource,
|
|
10049
11626
|
frontmatter: pageResult.frontmatter,
|
|
10050
11627
|
layout: typeof pageResult.frontmatter.layout === "string" ? pageResult.frontmatter.layout : void 0
|
|
10051
11628
|
};
|
|
@@ -10158,6 +11735,7 @@ async function applyDocumentationVersions(generatedPages, context, errors) {
|
|
|
10158
11735
|
...page.routePaths,
|
|
10159
11736
|
...prefixRoutePaths(page.routePaths, entry.prefix, context.outDir, context.base)
|
|
10160
11737
|
};
|
|
11738
|
+
context.markdownSourcePages.push(...outputPages);
|
|
10161
11739
|
snapContext.versionNavigation = createVersionNavigationContext({
|
|
10162
11740
|
prefix: entry.prefix,
|
|
10163
11741
|
base: context.base,
|
|
@@ -10270,6 +11848,20 @@ async function writeGeneratedPages(generatedPages, context, generatedFiles, list
|
|
|
10270
11848
|
errors.push(feeds.warning);
|
|
10271
11849
|
console.warn(feeds.warning);
|
|
10272
11850
|
}
|
|
11851
|
+
const markdownSource = await writeMarkdownSourceFiles({
|
|
11852
|
+
outDir: context.outDir,
|
|
11853
|
+
base: context.base,
|
|
11854
|
+
options: context.ssgOptions.markdownSource,
|
|
11855
|
+
publishState: context.options.publishState,
|
|
11856
|
+
pages: context.markdownSourcePages.map((page) => ({
|
|
11857
|
+
inputPath: page.inputPath,
|
|
11858
|
+
source: page.source,
|
|
11859
|
+
urlPath: page.routePaths.urlPath,
|
|
11860
|
+
frontmatter: page.frontmatter
|
|
11861
|
+
}))
|
|
11862
|
+
});
|
|
11863
|
+
generatedFiles.push(...markdownSource.files);
|
|
11864
|
+
errors.push(...markdownSource.errors);
|
|
10273
11865
|
}
|
|
10274
11866
|
/** Turns an SSG `urlPath` (`guide` or `/`) into a same-origin dest (`/guide`). */
|
|
10275
11867
|
function sitePathFromUrlPath(urlPath) {
|
|
@@ -10283,6 +11875,7 @@ function sitemapPages(context, listedPages, outputPages) {
|
|
|
10283
11875
|
loc: canonicalPageUrl(context, page.routePaths.urlPath) ?? "",
|
|
10284
11876
|
title: page.title,
|
|
10285
11877
|
description: page.description,
|
|
11878
|
+
lastUpdated: page.lastUpdated,
|
|
10286
11879
|
draft: page.frontmatter.draft === true,
|
|
10287
11880
|
unlisted: Boolean(context.options.publishState?.enabled) && !listedPaths.has(page.inputPath)
|
|
10288
11881
|
}));
|
|
@@ -10380,7 +11973,8 @@ function createDevServerCache() {
|
|
|
10380
11973
|
navGroups: null,
|
|
10381
11974
|
localePages: null,
|
|
10382
11975
|
pages: /* @__PURE__ */ new Map(),
|
|
10383
|
-
siteName: null
|
|
11976
|
+
siteName: null,
|
|
11977
|
+
markdownSourceIndex: null
|
|
10384
11978
|
};
|
|
10385
11979
|
}
|
|
10386
11980
|
/**
|
|
@@ -10389,6 +11983,7 @@ function createDevServerCache() {
|
|
|
10389
11983
|
function invalidateNavCache(cache) {
|
|
10390
11984
|
cache.navGroups = null;
|
|
10391
11985
|
cache.localePages = null;
|
|
11986
|
+
cache.markdownSourceIndex = null;
|
|
10392
11987
|
cache.pages.clear();
|
|
10393
11988
|
}
|
|
10394
11989
|
/**
|
|
@@ -10396,6 +11991,7 @@ function invalidateNavCache(cache) {
|
|
|
10396
11991
|
*/
|
|
10397
11992
|
function invalidatePageCache(cache, filePath) {
|
|
10398
11993
|
cache.pages.delete(filePath);
|
|
11994
|
+
cache.markdownSourceIndex = null;
|
|
10399
11995
|
}
|
|
10400
11996
|
/**
|
|
10401
11997
|
* Resolve site name from options or package.json.
|
|
@@ -10485,13 +12081,35 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
|
|
|
10485
12081
|
pages: localePages,
|
|
10486
12082
|
base
|
|
10487
12083
|
}) : void 0;
|
|
12084
|
+
const markdownSource = options.ssg.markdownSource?.enabled ? markdownSourceHrefForPage({
|
|
12085
|
+
source: filePath,
|
|
12086
|
+
fileUrl: pageData.path,
|
|
12087
|
+
frontmatter,
|
|
12088
|
+
base,
|
|
12089
|
+
permalinks: options.permalinks,
|
|
12090
|
+
cascade: options.cascade,
|
|
12091
|
+
publishState: options.publishState
|
|
12092
|
+
}) : void 0;
|
|
10488
12093
|
let html = await generateHtmlPage(pageData, localizedNav, siteName, base, options.ssg.ogImage, theme, locale, i18n ? i18n.locales : void 0, options.ssg.pagination, options.ssg.readerChrome, options.ssg.breadcrumbs, options.ssg.localeSwitcher, localePaths, options.ssg.a11y, options.ssg.team ?? {
|
|
10489
12094
|
enabled: false,
|
|
10490
12095
|
members: []
|
|
10491
|
-
}, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl, options.ssg.headValidation);
|
|
12096
|
+
}, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl, options.ssg.headValidation, i18n?.defaultLocale);
|
|
12097
|
+
if (markdownSource && options.ssg.markdownSource?.alternate) html = injectMarkdownSourceAlternate(html, markdownSource);
|
|
10492
12098
|
html = injectViteHmrClient(html);
|
|
10493
12099
|
return html;
|
|
10494
12100
|
}
|
|
12101
|
+
async function serveMarkdownSource(routeUrl, options, srcDir, cache) {
|
|
12102
|
+
if (!cache.markdownSourceIndex) cache.markdownSourceIndex = await buildMarkdownSourceIndex({
|
|
12103
|
+
files: await collectMarkdownFiles(srcDir, options.extensions),
|
|
12104
|
+
srcDir,
|
|
12105
|
+
permalinks: options.permalinks,
|
|
12106
|
+
cascade: options.cascade,
|
|
12107
|
+
publishState: options.publishState
|
|
12108
|
+
});
|
|
12109
|
+
const entry = resolveMarkdownSourceRequest(routeUrl, cache.markdownSourceIndex);
|
|
12110
|
+
if (!entry) return "missing";
|
|
12111
|
+
return entry.allowed ? entry.source : "hidden";
|
|
12112
|
+
}
|
|
10495
12113
|
/**
|
|
10496
12114
|
* Create the dev server middleware for SSG page serving.
|
|
10497
12115
|
*/
|
|
@@ -10504,6 +12122,19 @@ function createDevServerMiddleware(options, root, cache) {
|
|
|
10504
12122
|
let routeUrl = url;
|
|
10505
12123
|
if (base !== "/" && routeUrl.startsWith(base)) routeUrl = "/" + routeUrl.slice(base.length);
|
|
10506
12124
|
if (shouldSkip(routeUrl)) return next();
|
|
12125
|
+
if (options.ssg.markdownSource?.enabled && isMarkdownSourceRequest(routeUrl)) {
|
|
12126
|
+
const served = await serveMarkdownSource(routeUrl, options, srcDir, cache);
|
|
12127
|
+
if (served === "missing") return next();
|
|
12128
|
+
if (served === "hidden") {
|
|
12129
|
+
res.statusCode = 404;
|
|
12130
|
+
res.end();
|
|
12131
|
+
return;
|
|
12132
|
+
}
|
|
12133
|
+
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
12134
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
12135
|
+
res.end(served);
|
|
12136
|
+
return;
|
|
12137
|
+
}
|
|
10507
12138
|
const filePath = await resolveMarkdownFile(routeUrl, srcDir, options.extensions);
|
|
10508
12139
|
if (!filePath) return next();
|
|
10509
12140
|
try {
|
|
@@ -11020,6 +12651,43 @@ function resolveCardOptions(options) {
|
|
|
11020
12651
|
return { enabled: options.enabled ?? true };
|
|
11021
12652
|
}
|
|
11022
12653
|
//#endregion
|
|
12654
|
+
//#region src/heading-permalinks-options.ts
|
|
12655
|
+
function resolveHeadingPermalinksOptions(options) {
|
|
12656
|
+
if (!options) return { enabled: false };
|
|
12657
|
+
if (options === true) return { enabled: true };
|
|
12658
|
+
return { enabled: options.enabled ?? true };
|
|
12659
|
+
}
|
|
12660
|
+
//#endregion
|
|
12661
|
+
//#region src/magic-link-options.ts
|
|
12662
|
+
function resolveMagicLinkOptions(options) {
|
|
12663
|
+
if (!options) return {
|
|
12664
|
+
enabled: false,
|
|
12665
|
+
aliases: {},
|
|
12666
|
+
favicon: false,
|
|
12667
|
+
imageOverrides: []
|
|
12668
|
+
};
|
|
12669
|
+
if (options === true) return {
|
|
12670
|
+
enabled: true,
|
|
12671
|
+
aliases: {},
|
|
12672
|
+
favicon: false,
|
|
12673
|
+
imageOverrides: []
|
|
12674
|
+
};
|
|
12675
|
+
const favicon = options.favicon === true || typeof options.favicon === "object" && options.favicon != null;
|
|
12676
|
+
const faviconTemplate = typeof options.favicon === "object" ? options.favicon.template : void 0;
|
|
12677
|
+
return {
|
|
12678
|
+
enabled: options.enabled ?? true,
|
|
12679
|
+
aliases: normalizeAliases(options.aliases),
|
|
12680
|
+
favicon,
|
|
12681
|
+
faviconTemplate,
|
|
12682
|
+
imageOverrides: options.imageOverrides ?? []
|
|
12683
|
+
};
|
|
12684
|
+
}
|
|
12685
|
+
function normalizeAliases(aliases) {
|
|
12686
|
+
const normalized = {};
|
|
12687
|
+
for (const [key, value] of Object.entries(aliases ?? {})) normalized[key] = typeof value === "string" ? { href: value } : value;
|
|
12688
|
+
return normalized;
|
|
12689
|
+
}
|
|
12690
|
+
//#endregion
|
|
11023
12691
|
//#region src/include-options.ts
|
|
11024
12692
|
function resolveIncludeOptions(options) {
|
|
11025
12693
|
if (!options) return { enabled: false };
|
|
@@ -11594,6 +13262,12 @@ function createFrameworkMarkdownOptions(options) {
|
|
|
11594
13262
|
},
|
|
11595
13263
|
attrs: { enabled: false },
|
|
11596
13264
|
badges: { enabled: false },
|
|
13265
|
+
magicLinks: {
|
|
13266
|
+
enabled: false,
|
|
13267
|
+
aliases: {},
|
|
13268
|
+
favicon: false,
|
|
13269
|
+
imageOverrides: []
|
|
13270
|
+
},
|
|
11597
13271
|
containers: {
|
|
11598
13272
|
enabled: false,
|
|
11599
13273
|
types: {}
|
|
@@ -12593,7 +14267,7 @@ function createSsgPlugin(resolvedOptions, getRoot, ssgDevCache) {
|
|
|
12593
14267
|
for (const error of result.errors) console.warn(`[ox-content] ${error}`);
|
|
12594
14268
|
} catch (err) {
|
|
12595
14269
|
console.error("[ox-content] SSG build failed:", err);
|
|
12596
|
-
if (err instanceof PageResourceError) throw err;
|
|
14270
|
+
if (err instanceof PageResourceError || err instanceof BlogFeedError) throw err;
|
|
12597
14271
|
}
|
|
12598
14272
|
}
|
|
12599
14273
|
};
|
|
@@ -12708,6 +14382,7 @@ function resolveOptions(options) {
|
|
|
12708
14382
|
gfm: options.gfm ?? true,
|
|
12709
14383
|
mdx: options.mdx,
|
|
12710
14384
|
footnotes: options.footnotes ?? true,
|
|
14385
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
12711
14386
|
tables: options.tables ?? true,
|
|
12712
14387
|
taskLists: options.taskLists ?? true,
|
|
12713
14388
|
strikethrough: options.strikethrough ?? true,
|
|
@@ -12718,6 +14393,7 @@ function resolveOptions(options) {
|
|
|
12718
14393
|
emojiShortcodes: resolveEmojiShortcodeOptions(options.emojiShortcodes),
|
|
12719
14394
|
attrs: resolveAttrsOptions(options.attrs),
|
|
12720
14395
|
badges: resolveBadgeOptions(options.badges),
|
|
14396
|
+
magicLinks: resolveMagicLinkOptions(options.magicLinks),
|
|
12721
14397
|
containers: resolveContainerOptions(options.containers),
|
|
12722
14398
|
images: resolveImageOptions(options.images),
|
|
12723
14399
|
codeImports: resolveCodeImportOptions(options.codeImports),
|
|
@@ -12737,6 +14413,7 @@ function resolveOptions(options) {
|
|
|
12737
14413
|
frontmatter: options.frontmatter ?? true,
|
|
12738
14414
|
toc: options.toc ?? true,
|
|
12739
14415
|
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
14416
|
+
headingPermalinks: resolveHeadingPermalinksOptions(options.headingPermalinks),
|
|
12740
14417
|
ogImage: options.ogImage ?? false,
|
|
12741
14418
|
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
12742
14419
|
transformers: options.transformers ?? [],
|
|
@@ -12978,9 +14655,9 @@ function resolveCodeAnnotationsOptions(options) {
|
|
|
12978
14655
|
/**
|
|
12979
14656
|
* Generates virtual module content.
|
|
12980
14657
|
*/
|
|
12981
|
-
function generateVirtualModule(path$
|
|
12982
|
-
if (path$
|
|
12983
|
-
if (path$
|
|
14658
|
+
function generateVirtualModule(path$8, options) {
|
|
14659
|
+
if (path$8 === "config") return `export default ${JSON.stringify(options)};`;
|
|
14660
|
+
if (path$8 === "runtime") {
|
|
12984
14661
|
const base = normalizeRuntimeBase(options.base);
|
|
12985
14662
|
return `
|
|
12986
14663
|
export const base = ${JSON.stringify(base)};
|
|
@@ -13027,6 +14704,7 @@ function normalizeRuntimeBase(base) {
|
|
|
13027
14704
|
return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
|
|
13028
14705
|
}
|
|
13029
14706
|
//#endregion
|
|
14707
|
+
exports.BlogFeedError = BlogFeedError;
|
|
13030
14708
|
exports.DEFAULT_HTML_TEMPLATE = DEFAULT_HTML_TEMPLATE;
|
|
13031
14709
|
exports.DEFAULT_MARKDOWN_EXTENSIONS = DEFAULT_MARKDOWN_EXTENSIONS;
|
|
13032
14710
|
exports.DefaultTheme = DefaultTheme;
|
|
@@ -13135,10 +14813,12 @@ exports.resolveFeedsOptions = resolveFeedsOptions;
|
|
|
13135
14813
|
exports.resolveFileTreeOptions = resolveFileTreeOptions;
|
|
13136
14814
|
exports.resolveHeadValidation = resolveHeadValidation;
|
|
13137
14815
|
exports.resolveHeaderNavItems = require_vitepress.resolveHeaderNavItems;
|
|
14816
|
+
exports.resolveHeadingPermalinksOptions = resolveHeadingPermalinksOptions;
|
|
13138
14817
|
exports.resolveI18nOptions = resolveI18nOptions;
|
|
13139
14818
|
exports.resolveImageOptions = resolveImageOptions;
|
|
13140
14819
|
exports.resolveIncludeOptions = resolveIncludeOptions;
|
|
13141
14820
|
exports.resolveLocaleLabel = require_vitepress.resolveLocaleLabel;
|
|
14821
|
+
exports.resolveMarkdownSourceOptions = resolveMarkdownSourceOptions;
|
|
13142
14822
|
exports.resolveMathOptions = resolveMathOptions;
|
|
13143
14823
|
exports.resolveMdxForFilePath = resolveMdxForFilePath;
|
|
13144
14824
|
exports.resolveNotFoundOptions = resolveNotFoundOptions;
|