@ox-content/vite-plugin 3.0.0-alpha.4 → 3.0.0-alpha.6
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 +2299 -587
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +365 -25
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +365 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2320 -613
- 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
|
|
@@ -4138,6 +4731,22 @@ function stripLocalePrefix(sitePath, locales) {
|
|
|
4138
4731
|
return normalized;
|
|
4139
4732
|
}
|
|
4140
4733
|
//#endregion
|
|
4734
|
+
//#region src/page-head.ts
|
|
4735
|
+
/** Resolve descriptors to escaped `<head>` markup. Build-time only. */
|
|
4736
|
+
function renderHead(input) {
|
|
4737
|
+
return require_vitepress.importNapiModuleSync().renderHead(JSON.stringify(input));
|
|
4738
|
+
}
|
|
4739
|
+
function resolveHeadValidation(value) {
|
|
4740
|
+
if (value === "warn" || value === "strict") return value;
|
|
4741
|
+
return false;
|
|
4742
|
+
}
|
|
4743
|
+
function reportHeadDiagnostics(diagnostics, validation) {
|
|
4744
|
+
if (!validation || diagnostics.length === 0) return;
|
|
4745
|
+
const fatal = diagnostics.filter((item) => item.strict);
|
|
4746
|
+
if (validation === "strict" && fatal.length > 0) throw new Error(`[ox-content] ${fatal[0].message}`);
|
|
4747
|
+
if (validation === "warn") for (const item of diagnostics) console.warn(`[ox-content] ${item.message}`);
|
|
4748
|
+
}
|
|
4749
|
+
//#endregion
|
|
4141
4750
|
//#region src/page-context.ts
|
|
4142
4751
|
var page_context_exports = /* @__PURE__ */ require_vitepress.__exportAll({
|
|
4143
4752
|
clearRenderContext: () => clearRenderContext,
|
|
@@ -4355,6 +4964,7 @@ function renderPage(page, options) {
|
|
|
4355
4964
|
contributors: page.contributors,
|
|
4356
4965
|
path: page.path,
|
|
4357
4966
|
url: page.url,
|
|
4967
|
+
markdownSource: page.markdownSource,
|
|
4358
4968
|
frontmatter: page.frontmatter,
|
|
4359
4969
|
layout: page.layout
|
|
4360
4970
|
},
|
|
@@ -4371,6 +4981,7 @@ function renderPage(page, options) {
|
|
|
4371
4981
|
contributors: p.contributors,
|
|
4372
4982
|
path: p.path,
|
|
4373
4983
|
url: p.url,
|
|
4984
|
+
markdownSource: p.markdownSource,
|
|
4374
4985
|
frontmatter: p.frontmatter,
|
|
4375
4986
|
layout: p.layout
|
|
4376
4987
|
}))
|
|
@@ -4429,8 +5040,8 @@ function DefaultTheme({ children }) {
|
|
|
4429
5040
|
<head>
|
|
4430
5041
|
<meta charset="UTF-8">
|
|
4431
5042
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4432
|
-
<title>${escapeHtml$
|
|
4433
|
-
${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)}">` : ""}
|
|
4434
5045
|
<style>
|
|
4435
5046
|
:root {
|
|
4436
5047
|
--octc-color-primary: #4f6fae;
|
|
@@ -4454,7 +5065,7 @@ function DefaultTheme({ children }) {
|
|
|
4454
5065
|
</head>
|
|
4455
5066
|
<body>
|
|
4456
5067
|
<header>
|
|
4457
|
-
<h1>${escapeHtml$
|
|
5068
|
+
<h1>${escapeHtml$3(site.name)}</h1>
|
|
4458
5069
|
</header>
|
|
4459
5070
|
<main>
|
|
4460
5071
|
${children.__html}
|
|
@@ -4462,7 +5073,7 @@ function DefaultTheme({ children }) {
|
|
|
4462
5073
|
</body>
|
|
4463
5074
|
</html>` };
|
|
4464
5075
|
}
|
|
4465
|
-
function escapeHtml$
|
|
5076
|
+
function escapeHtml$3(str) {
|
|
4466
5077
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4467
5078
|
}
|
|
4468
5079
|
/**
|
|
@@ -4562,6 +5173,13 @@ async function writeSiteMapFiles(input) {
|
|
|
4562
5173
|
}
|
|
4563
5174
|
return { files };
|
|
4564
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
|
+
}
|
|
4565
5183
|
function hasSiteUrl$2(siteUrl) {
|
|
4566
5184
|
return Boolean(siteUrl && siteUrl.trim());
|
|
4567
5185
|
}
|
|
@@ -4574,7 +5192,14 @@ function generateSitemapXml(pages) {
|
|
|
4574
5192
|
for (const page of pages) {
|
|
4575
5193
|
xml += " <url>\n <loc>";
|
|
4576
5194
|
xml += escapeXml$1(page.loc);
|
|
4577
|
-
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";
|
|
4578
5203
|
}
|
|
4579
5204
|
xml += "</urlset>\n";
|
|
4580
5205
|
return xml;
|
|
@@ -4635,102 +5260,6 @@ function escapeLlmsUrl(value) {
|
|
|
4635
5260
|
return escaped;
|
|
4636
5261
|
}
|
|
4637
5262
|
//#endregion
|
|
4638
|
-
//#region src/publish-state.ts
|
|
4639
|
-
/**
|
|
4640
|
-
* Opt-in draft / unlisted / scheduled page classification.
|
|
4641
|
-
*/
|
|
4642
|
-
/**
|
|
4643
|
-
* Resolves `publishState` with defaults.
|
|
4644
|
-
*
|
|
4645
|
-
* `false` / omitted stays off. `true` enables production filtering. An object
|
|
4646
|
-
* enables the feature and overrides only the fields the site set.
|
|
4647
|
-
*/
|
|
4648
|
-
function resolvePublishStateOptions(value) {
|
|
4649
|
-
if (!value) return {
|
|
4650
|
-
enabled: false,
|
|
4651
|
-
includeDrafts: false
|
|
4652
|
-
};
|
|
4653
|
-
if (value === true) return {
|
|
4654
|
-
enabled: true,
|
|
4655
|
-
includeDrafts: false
|
|
4656
|
-
};
|
|
4657
|
-
return {
|
|
4658
|
-
enabled: value.enabled ?? true,
|
|
4659
|
-
now: value.now,
|
|
4660
|
-
includeDrafts: value.includeDrafts ?? false
|
|
4661
|
-
};
|
|
4662
|
-
}
|
|
4663
|
-
/** Classifies one frontmatter object. Never throws. */
|
|
4664
|
-
function classifyPublishState(frontmatter, options) {
|
|
4665
|
-
try {
|
|
4666
|
-
return require_vitepress.importNapiModuleSync().classifyPublishState(JSON.stringify(frontmatter ?? {}), toNapiPublishState(options));
|
|
4667
|
-
} catch {
|
|
4668
|
-
return {
|
|
4669
|
-
output: true,
|
|
4670
|
-
listed: true
|
|
4671
|
-
};
|
|
4672
|
-
}
|
|
4673
|
-
}
|
|
4674
|
-
/** Splits pages into those that write HTML and those that appear in listings. */
|
|
4675
|
-
function partitionPublishedPages(pages, options) {
|
|
4676
|
-
if (!options?.enabled) return {
|
|
4677
|
-
output: [...pages],
|
|
4678
|
-
listed: [...pages]
|
|
4679
|
-
};
|
|
4680
|
-
const output = [];
|
|
4681
|
-
const listed = [];
|
|
4682
|
-
for (const page of pages) {
|
|
4683
|
-
const decision = classifyPublishState(page.frontmatter, options);
|
|
4684
|
-
if (decision.output) output.push(page);
|
|
4685
|
-
if (decision.listed) listed.push(page);
|
|
4686
|
-
}
|
|
4687
|
-
return {
|
|
4688
|
-
output,
|
|
4689
|
-
listed
|
|
4690
|
-
};
|
|
4691
|
-
}
|
|
4692
|
-
/** Drops nav items that resolve to hidden (unpublished or unlisted) pages. */
|
|
4693
|
-
function filterNavGroups(groups, hidden) {
|
|
4694
|
-
return groups.map((group) => ({
|
|
4695
|
-
...group,
|
|
4696
|
-
items: filterNavItems(group.items, hidden)
|
|
4697
|
-
})).filter((group) => group.items.length > 0);
|
|
4698
|
-
}
|
|
4699
|
-
function filterNavItems(items, hidden) {
|
|
4700
|
-
const kept = [];
|
|
4701
|
-
for (const item of items) {
|
|
4702
|
-
if (isHiddenNavTarget(item, hidden)) continue;
|
|
4703
|
-
const children = item.children?.length ? filterNavItems(item.children, hidden) : item.children;
|
|
4704
|
-
kept.push(children === item.children ? item : {
|
|
4705
|
-
...item,
|
|
4706
|
-
children
|
|
4707
|
-
});
|
|
4708
|
-
}
|
|
4709
|
-
return kept;
|
|
4710
|
-
}
|
|
4711
|
-
function isHiddenNavTarget(item, hidden) {
|
|
4712
|
-
return hidden.has(item.path) || hidden.has(item.href);
|
|
4713
|
-
}
|
|
4714
|
-
/** Keys used to match a page against generated nav items. */
|
|
4715
|
-
function hiddenNavKeys(pages, listed) {
|
|
4716
|
-
const listedPaths = new Set(listed.map((page) => page.inputPath));
|
|
4717
|
-
const hidden = /* @__PURE__ */ new Set();
|
|
4718
|
-
for (const page of pages) {
|
|
4719
|
-
if (listedPaths.has(page.inputPath)) continue;
|
|
4720
|
-
hidden.add(page.routePaths.urlPath);
|
|
4721
|
-
hidden.add(page.routePaths.href);
|
|
4722
|
-
}
|
|
4723
|
-
return hidden;
|
|
4724
|
-
}
|
|
4725
|
-
function toNapiPublishState(options) {
|
|
4726
|
-
if (!options) return;
|
|
4727
|
-
return {
|
|
4728
|
-
enabled: options.enabled,
|
|
4729
|
-
now: options.now,
|
|
4730
|
-
includeDrafts: options.includeDrafts
|
|
4731
|
-
};
|
|
4732
|
-
}
|
|
4733
|
-
//#endregion
|
|
4734
5263
|
//#region src/permalinks.ts
|
|
4735
5264
|
const RESERVED_CASCADE_KEYS = /* @__PURE__ */ new Set(["permalink", "slug"]);
|
|
4736
5265
|
/** Resolves `permalinks`. `false` / omitted stays off. `true` / `{}` enables. */
|
|
@@ -4780,6 +5309,18 @@ function resolvePageRoutes(input) {
|
|
|
4780
5309
|
errors
|
|
4781
5310
|
};
|
|
4782
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
|
+
}
|
|
4783
5324
|
function normalizeUrlPath$1(value) {
|
|
4784
5325
|
const segments = pathSegments(value);
|
|
4785
5326
|
return segments.length === 0 ? "/" : segments.join("/");
|
|
@@ -4881,27 +5422,315 @@ function ancestorDirs(source) {
|
|
|
4881
5422
|
return dirs;
|
|
4882
5423
|
}
|
|
4883
5424
|
//#endregion
|
|
4884
|
-
//#region src/
|
|
5425
|
+
//#region src/publish-state.ts
|
|
4885
5426
|
/**
|
|
4886
|
-
*
|
|
5427
|
+
* Opt-in draft / unlisted / scheduled page classification.
|
|
4887
5428
|
*/
|
|
4888
|
-
/**
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
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({
|
|
4905
5734
|
...page,
|
|
4906
5735
|
frontmatter: hit.frontmatter,
|
|
4907
5736
|
routePaths: routePathsFromUrl(hit.urlPath, input.srcDir, input.outDir, input.base, input.extension, input.siteUrl)
|
|
@@ -5110,7 +5939,7 @@ async function writeRedirectFiles(input) {
|
|
|
5110
5939
|
}
|
|
5111
5940
|
/** Static HTML redirect body. `dest` is escaped. */
|
|
5112
5941
|
function generateRedirectHtml(dest) {
|
|
5113
|
-
const escaped = escapeHtml$
|
|
5942
|
+
const escaped = escapeHtml$2(dest);
|
|
5114
5943
|
return `\
|
|
5115
5944
|
<!DOCTYPE html>
|
|
5116
5945
|
<html lang="en">
|
|
@@ -5195,7 +6024,7 @@ function upsert(files, index, occupied, from, to, base) {
|
|
|
5195
6024
|
html
|
|
5196
6025
|
});
|
|
5197
6026
|
}
|
|
5198
|
-
function escapeHtml$
|
|
6027
|
+
function escapeHtml$2(value) {
|
|
5199
6028
|
return value.replace(/[&<>"']/g, (ch) => {
|
|
5200
6029
|
switch (ch) {
|
|
5201
6030
|
case "&": return "&";
|
|
@@ -5553,6 +6382,7 @@ function createNativeTransformOptions(options) {
|
|
|
5553
6382
|
return {
|
|
5554
6383
|
gfm: options.gfm,
|
|
5555
6384
|
footnotes: options.footnotes,
|
|
6385
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
5556
6386
|
taskLists: options.taskLists,
|
|
5557
6387
|
tables: options.tables,
|
|
5558
6388
|
strikethrough: options.strikethrough,
|
|
@@ -5560,6 +6390,7 @@ function createNativeTransformOptions(options) {
|
|
|
5560
6390
|
autolinkUrls: options.autolinks,
|
|
5561
6391
|
frontmatter: options.frontmatter,
|
|
5562
6392
|
tocMaxDepth: options.tocMaxDepth,
|
|
6393
|
+
headingPermalinks: options.headingPermalinks?.enabled ?? false,
|
|
5563
6394
|
codeAnnotations: options.codeAnnotations?.enabled ?? false,
|
|
5564
6395
|
codeAnnotationMetaKey: options.codeAnnotations?.metaKey ?? "annotate",
|
|
5565
6396
|
codeAnnotationSyntax: options.codeAnnotations?.notation ?? "attribute",
|
|
@@ -5574,6 +6405,13 @@ function createNativeTransformOptions(options) {
|
|
|
5574
6405
|
} : void 0,
|
|
5575
6406
|
attributes: options.attrs?.enabled ? { enabled: true } : void 0,
|
|
5576
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,
|
|
5577
6415
|
containers: options.containers?.enabled ? {
|
|
5578
6416
|
enabled: true,
|
|
5579
6417
|
types: options.containers.types
|
|
@@ -5998,6 +6836,7 @@ function isExcludedFromFeed(item, publishState) {
|
|
|
5998
6836
|
const frontmatter = item.frontmatter ?? {};
|
|
5999
6837
|
if (item.draft === true || frontmatter.draft === true) return true;
|
|
6000
6838
|
if (item.unlisted === true || frontmatter.unlisted === true) return true;
|
|
6839
|
+
if (frontmatter.external === true) return true;
|
|
6001
6840
|
if (!publishState?.enabled) return false;
|
|
6002
6841
|
return !classifyPublishState({
|
|
6003
6842
|
...frontmatter,
|
|
@@ -6238,14 +7077,14 @@ function relatedMarkup(pages) {
|
|
|
6238
7077
|
}
|
|
6239
7078
|
function listPageContent(terms, base, urlName) {
|
|
6240
7079
|
const items = terms.map((term) => listItem$1(siteHref$3(base, urlName, term.slug), term.label)).join("");
|
|
6241
|
-
return `<h1>${escapeHtml$
|
|
7080
|
+
return `<h1>${escapeHtml$1(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
|
|
6242
7081
|
}
|
|
6243
7082
|
function termPageContent(term) {
|
|
6244
7083
|
const items = [...term.pages].sort((left, right) => {
|
|
6245
7084
|
const titleCmp = left.title.localeCompare(right.title);
|
|
6246
7085
|
return titleCmp !== 0 ? titleCmp : left.routePaths.href.localeCompare(right.routePaths.href);
|
|
6247
7086
|
}).map((page) => listItem$1(page.routePaths.href, page.title)).join("");
|
|
6248
|
-
return `<h1>${escapeHtml$
|
|
7087
|
+
return `<h1>${escapeHtml$1(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
|
|
6249
7088
|
}
|
|
6250
7089
|
function displayTaxonomyName(name) {
|
|
6251
7090
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -6263,9 +7102,9 @@ function containedPath$2(outDir, ...segments) {
|
|
|
6263
7102
|
return resolved;
|
|
6264
7103
|
}
|
|
6265
7104
|
function listItem$1(href, label) {
|
|
6266
|
-
return `<li><a href="${escapeHtml$
|
|
7105
|
+
return `<li><a href="${escapeHtml$1(href)}">${escapeHtml$1(label)}</a></li>`;
|
|
6267
7106
|
}
|
|
6268
|
-
function escapeHtml$
|
|
7107
|
+
function escapeHtml$1(value) {
|
|
6269
7108
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6270
7109
|
}
|
|
6271
7110
|
//#endregion
|
|
@@ -6539,18 +7378,21 @@ function resolveBlogOptions(value) {
|
|
|
6539
7378
|
if (!value) return {
|
|
6540
7379
|
enabled: false,
|
|
6541
7380
|
authors: {},
|
|
6542
|
-
pageSize: DEFAULT_PAGE_SIZE
|
|
7381
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7382
|
+
feeds: []
|
|
6543
7383
|
};
|
|
6544
7384
|
if (value === true) return {
|
|
6545
7385
|
enabled: true,
|
|
6546
7386
|
authors: {},
|
|
6547
|
-
pageSize: DEFAULT_PAGE_SIZE
|
|
7387
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7388
|
+
feeds: []
|
|
6548
7389
|
};
|
|
6549
7390
|
return {
|
|
6550
7391
|
enabled: true,
|
|
6551
7392
|
collection: value.collection,
|
|
6552
7393
|
authors: normalizeAuthors(value.authors),
|
|
6553
|
-
pageSize: normalizePageSize(value.pageSize)
|
|
7394
|
+
pageSize: normalizePageSize(value.pageSize),
|
|
7395
|
+
feeds: normalizeFeeds(value.feeds)
|
|
6554
7396
|
};
|
|
6555
7397
|
}
|
|
6556
7398
|
/**
|
|
@@ -6581,54 +7423,396 @@ function normalizePageSize(value) {
|
|
|
6581
7423
|
if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
|
|
6582
7424
|
return DEFAULT_PAGE_SIZE;
|
|
6583
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
|
+
}
|
|
6584
7455
|
//#endregion
|
|
6585
|
-
//#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
|
|
6586
7502
|
/**
|
|
6587
|
-
*
|
|
7503
|
+
* Safe-URL checks for configured external blog feeds.
|
|
6588
7504
|
*/
|
|
6589
|
-
const
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
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"
|
|
6607
7596
|
}
|
|
7597
|
+
});
|
|
7598
|
+
if (isRedirect(response.status)) {
|
|
7599
|
+
current = resolveRedirect(current, response.headers.get("location"));
|
|
6608
7600
|
continue;
|
|
6609
7601
|
}
|
|
6610
|
-
if (
|
|
6611
|
-
|
|
7602
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
7603
|
+
assertFeedContentType(response.headers.get("content-type"));
|
|
7604
|
+
return readBoundedBody(response, maxBytes);
|
|
6612
7605
|
}
|
|
6613
|
-
|
|
6614
|
-
return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
|
|
7606
|
+
throw new Error("too many redirects");
|
|
6615
7607
|
}
|
|
6616
|
-
function
|
|
6617
|
-
if (!
|
|
6618
|
-
const
|
|
6619
|
-
|
|
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");
|
|
6620
7613
|
}
|
|
6621
|
-
function
|
|
6622
|
-
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);
|
|
6623
7619
|
}
|
|
6624
|
-
function
|
|
6625
|
-
return
|
|
7620
|
+
function defaultFetch(input, init) {
|
|
7621
|
+
return fetch(input, init);
|
|
6626
7622
|
}
|
|
6627
|
-
function
|
|
6628
|
-
return
|
|
7623
|
+
function isRedirect(status) {
|
|
7624
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
6629
7625
|
}
|
|
6630
|
-
function
|
|
6631
|
-
|
|
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, "\\$&");
|
|
6632
7816
|
}
|
|
6633
7817
|
//#endregion
|
|
6634
7818
|
//#region src/blog-html.ts
|
|
@@ -6644,13 +7828,13 @@ function isSafeBlogUrl(value) {
|
|
|
6644
7828
|
return trimmed.toLowerCase().startsWith("https:");
|
|
6645
7829
|
}
|
|
6646
7830
|
function postMetaMarkup(meta) {
|
|
6647
|
-
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>`];
|
|
6648
7832
|
if (meta.authors.length > 0) {
|
|
6649
7833
|
const items = meta.authors.map((author) => authorMarkup(author)).join("");
|
|
6650
7834
|
parts.push(`<ul class="ox-blog-meta__authors">${items}</ul>`);
|
|
6651
7835
|
}
|
|
6652
7836
|
if (meta.tags.length > 0) {
|
|
6653
|
-
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("");
|
|
6654
7838
|
parts.push(`<ul class="ox-blog-meta__tags">${items}</ul>`);
|
|
6655
7839
|
}
|
|
6656
7840
|
return `<aside class="ox-blog-meta">${parts.join("")}</aside>\n`;
|
|
@@ -6658,25 +7842,25 @@ function postMetaMarkup(meta) {
|
|
|
6658
7842
|
function indexPageContent(items, pager) {
|
|
6659
7843
|
const list = items.map((item) => listItem(item)).join("");
|
|
6660
7844
|
const links = [];
|
|
6661
|
-
if (pager.newerHref) links.push(`<a href="${escapeHtml
|
|
6662
|
-
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>`);
|
|
6663
7847
|
return `<h1>Blog</h1><ul class="ox-blog">${list}</ul>${links.length > 0 ? `<nav class="ox-blog-pager">${links.join("")}</nav>` : ""}`;
|
|
6664
7848
|
}
|
|
6665
7849
|
function tagPageContent(label, items) {
|
|
6666
7850
|
const list = items.map((item) => listItem(item)).join("");
|
|
6667
|
-
return `<h1>${escapeHtml
|
|
7851
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog-tag">${list}</ul>`;
|
|
6668
7852
|
}
|
|
6669
7853
|
function archiveIndexContent(years) {
|
|
6670
|
-
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>`;
|
|
6671
7855
|
}
|
|
6672
7856
|
function archiveYearContent(year, months, items) {
|
|
6673
|
-
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("");
|
|
6674
7858
|
const posts = items.map((item) => listItem(item)).join("");
|
|
6675
|
-
return `<h1>${escapeHtml
|
|
7859
|
+
return `<h1>${escapeHtml(year)}</h1><ul class="ox-blog-archive-months">${monthList}</ul><ul class="ox-blog">${posts}</ul>`;
|
|
6676
7860
|
}
|
|
6677
7861
|
function archiveMonthContent(label, items) {
|
|
6678
7862
|
const list = items.map((item) => listItem(item)).join("");
|
|
6679
|
-
return `<h1>${escapeHtml
|
|
7863
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog">${list}</ul>`;
|
|
6680
7864
|
}
|
|
6681
7865
|
function siteHref$2(base, ...segments) {
|
|
6682
7866
|
const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
@@ -6690,17 +7874,18 @@ function containedPath$1(outDir, ...segments) {
|
|
|
6690
7874
|
if (resolved === root || !resolved.startsWith(prefix)) return;
|
|
6691
7875
|
return resolved;
|
|
6692
7876
|
}
|
|
6693
|
-
function escapeHtml
|
|
7877
|
+
function escapeHtml(value) {
|
|
6694
7878
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6695
7879
|
}
|
|
6696
7880
|
function authorMarkup(author) {
|
|
6697
|
-
const name = escapeHtml
|
|
7881
|
+
const name = escapeHtml(author.name);
|
|
6698
7882
|
const url = author.url?.trim();
|
|
6699
|
-
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>`;
|
|
6700
7884
|
}
|
|
6701
7885
|
function listItem(item) {
|
|
6702
|
-
const time = item.dateLabel ? ` <time datetime="${escapeHtml
|
|
6703
|
-
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>`;
|
|
6704
7889
|
}
|
|
6705
7890
|
//#endregion
|
|
6706
7891
|
//#region src/blog-posts.ts
|
|
@@ -6806,7 +7991,8 @@ function toListItem(page) {
|
|
|
6806
7991
|
return {
|
|
6807
7992
|
title: page.title,
|
|
6808
7993
|
href: page.routePaths.href,
|
|
6809
|
-
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 } : {}
|
|
6810
7996
|
};
|
|
6811
7997
|
}
|
|
6812
7998
|
function resolvePostAuthors(frontmatter, map) {
|
|
@@ -6866,6 +8052,158 @@ function dateField(value) {
|
|
|
6866
8052
|
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
6867
8053
|
}
|
|
6868
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
|
|
6869
8207
|
//#region src/blog-pages.ts
|
|
6870
8208
|
/**
|
|
6871
8209
|
* Generated blog index, tag, and archive pages.
|
|
@@ -6877,7 +8215,7 @@ async function injectBlogPostMeta(input) {
|
|
|
6877
8215
|
if (posts === void 0) return;
|
|
6878
8216
|
const listedPaths = new Set(posts.map((page) => page.inputPath));
|
|
6879
8217
|
for (const page of input.pages) {
|
|
6880
|
-
if (!listedPaths.has(page.inputPath)) continue;
|
|
8218
|
+
if (!listedPaths.has(page.inputPath) || page.external === true) continue;
|
|
6881
8219
|
const markdown = await readMarkdown(page.inputPath);
|
|
6882
8220
|
page.transformedHtml = postMetaMarkup({
|
|
6883
8221
|
authors: resolvePostAuthors(page.frontmatter, input.options.authors),
|
|
@@ -6910,8 +8248,9 @@ async function appendBlogPages(input) {
|
|
|
6910
8248
|
input.errors.push(AMBIGUOUS_COLLECTION);
|
|
6911
8249
|
return;
|
|
6912
8250
|
}
|
|
6913
|
-
const
|
|
6914
|
-
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);
|
|
6915
8254
|
for (const spec of blogPageSpecs(posts, input.options, input.outDir, input.base)) try {
|
|
6916
8255
|
input.generatedPages.push({
|
|
6917
8256
|
inputPath: spec.outputPath,
|
|
@@ -7006,6 +8345,14 @@ function blogPageSpecs(posts, options, outDir, base) {
|
|
|
7006
8345
|
}
|
|
7007
8346
|
return pages;
|
|
7008
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
|
+
}
|
|
7009
8356
|
async function readMarkdown(inputPath) {
|
|
7010
8357
|
try {
|
|
7011
8358
|
return await node_fs_promises.readFile(inputPath, "utf8");
|
|
@@ -7518,59 +8865,6 @@ function generateSearchModule(options, indexPath) {
|
|
|
7518
8865
|
return require_vitepress.importNapiModuleSync().generateSearchModuleFromOptions(toLocalSearchRuntimeOptions(options), indexPath);
|
|
7519
8866
|
}
|
|
7520
8867
|
//#endregion
|
|
7521
|
-
//#region src/versions-html.ts
|
|
7522
|
-
function versionSwitcherMarkup(links, badge) {
|
|
7523
|
-
if (links.length === 0) return "";
|
|
7524
|
-
const current = links.find((link) => link.current) ?? links[0];
|
|
7525
|
-
const items = links.map((link) => {
|
|
7526
|
-
const label = `${escapeHtml(link.label)}${badgeMarkup(link, badge)}`;
|
|
7527
|
-
if (link.current || !isSafeHref(link.href)) return `<li><span aria-current="page">${label}</span></li>`;
|
|
7528
|
-
return `<li><a href="${escapeHtml(link.href)}">${label}</a></li>`;
|
|
7529
|
-
}).join("");
|
|
7530
|
-
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>`;
|
|
7531
|
-
}
|
|
7532
|
-
function versionBannerMarkup(kind) {
|
|
7533
|
-
if (kind === "unreleased") return `<aside class="ox-version-banner ox-version-banner--unreleased" role="status">This documentation describes an unreleased version.</aside>`;
|
|
7534
|
-
if (kind === "unmaintained") return `<aside class="ox-version-banner ox-version-banner--unmaintained" role="status">This documentation is unmaintained.</aside>`;
|
|
7535
|
-
return "";
|
|
7536
|
-
}
|
|
7537
|
-
function injectVersionChrome(html, switcher, banner, searchFrom, searchTo) {
|
|
7538
|
-
let next = html;
|
|
7539
|
-
if (banner) next = next.replace(/<body([^>]*)>/, `<body$1>${banner}`);
|
|
7540
|
-
if (switcher) {
|
|
7541
|
-
if (next.includes("<div class=\"header-actions\">")) next = next.replace("<div class=\"header-actions\">", `<div class="header-actions">${switcher}`);
|
|
7542
|
-
else if (next.includes("</header>")) next = next.replace("</header>", `${switcher}</header>`);
|
|
7543
|
-
}
|
|
7544
|
-
if (searchTo && isSafeHref(searchTo)) next = next.replace(/<html([^>]*)>/i, (match, attrs) => {
|
|
7545
|
-
if (/\sdata-ox-search-index=/.test(attrs)) return match;
|
|
7546
|
-
return `<html${attrs} data-ox-search-index="${escapeHtml(searchTo)}">`;
|
|
7547
|
-
});
|
|
7548
|
-
if (searchFrom && searchTo && searchFrom !== searchTo && isSafeHref(searchTo)) {
|
|
7549
|
-
next = next.split(searchFrom).join(searchTo);
|
|
7550
|
-
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>`;
|
|
7551
|
-
next = next.includes("</body>") ? next.replace("</body>", `${script}</body>`) : `${next}${script}`;
|
|
7552
|
-
}
|
|
7553
|
-
return next;
|
|
7554
|
-
}
|
|
7555
|
-
function searchIndexUrl(base, prefix) {
|
|
7556
|
-
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
7557
|
-
return prefix ? `${root}${prefix}/search-index.json` : `${root}search-index.json`;
|
|
7558
|
-
}
|
|
7559
|
-
function isSafeHref(href) {
|
|
7560
|
-
const trimmed = href.trim();
|
|
7561
|
-
if (!trimmed || trimmed.startsWith("//")) return false;
|
|
7562
|
-
const lower = trimmed.replace(/\s+/g, "").toLowerCase();
|
|
7563
|
-
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) return false;
|
|
7564
|
-
return trimmed.startsWith("/") || trimmed.startsWith("./") || !trimmed.includes(":");
|
|
7565
|
-
}
|
|
7566
|
-
function escapeHtml(value) {
|
|
7567
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
7568
|
-
}
|
|
7569
|
-
function badgeMarkup(link, badge) {
|
|
7570
|
-
if (!badge || !link.banner) return "";
|
|
7571
|
-
return `<span class="ox-version-badge">${link.banner === "unreleased" ? "unreleased" : "unmaintained"}</span>`;
|
|
7572
|
-
}
|
|
7573
|
-
//#endregion
|
|
7574
8868
|
//#region src/versions.ts
|
|
7575
8869
|
/**
|
|
7576
8870
|
* Opt-in documentation versioning: prefixes, snapshots, and header chrome.
|
|
@@ -7701,7 +8995,13 @@ async function writeSnapshotSearchIndex(input) {
|
|
|
7701
8995
|
function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
|
|
7702
8996
|
if (!options.enabled) return html;
|
|
7703
8997
|
const active = options.entries.find((entry) => entry.id === activeId);
|
|
7704
|
-
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
|
+
})));
|
|
7705
9005
|
}
|
|
7706
9006
|
function sanitizePrefix(prefix) {
|
|
7707
9007
|
const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
|
|
@@ -7738,21 +9038,182 @@ function normalizeEntries(entries) {
|
|
|
7738
9038
|
banner: normalizeBanner(entry.banner)
|
|
7739
9039
|
});
|
|
7740
9040
|
}
|
|
7741
|
-
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
|
+
}
|
|
7742
9152
|
}
|
|
7743
|
-
|
|
7744
|
-
|
|
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}`;
|
|
7745
9160
|
}
|
|
7746
|
-
function
|
|
7747
|
-
|
|
7748
|
-
const
|
|
7749
|
-
|
|
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}` : "";
|
|
7750
9167
|
}
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
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);
|
|
7756
9217
|
}
|
|
7757
9218
|
//#endregion
|
|
7758
9219
|
//#region src/resources-jpeg.ts
|
|
@@ -8795,129 +10256,23 @@ function coverCrop(image, width, height) {
|
|
|
8795
10256
|
return cropImage(scaled, Math.max(0, Math.floor((scaled.width - width) / 2)), Math.max(0, Math.floor((scaled.height - height) / 2)), width, height);
|
|
8796
10257
|
}
|
|
8797
10258
|
//#endregion
|
|
8798
|
-
//#region src/resources-
|
|
10259
|
+
//#region src/resources-write.ts
|
|
8799
10260
|
/**
|
|
8800
|
-
*
|
|
10261
|
+
* Transform cache writes for page resources.
|
|
8801
10262
|
*/
|
|
8802
|
-
|
|
8803
|
-
const SRC_ATTR = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
|
|
8804
|
-
async function processPageResources(input) {
|
|
8805
|
-
if (!input.options.enabled) return {
|
|
8806
|
-
html: input.html,
|
|
8807
|
-
files: [],
|
|
8808
|
-
errors: [],
|
|
8809
|
-
fatal: []
|
|
8810
|
-
};
|
|
8811
|
-
const bundleRoot = node_path.dirname(input.inputPath);
|
|
8812
|
-
const outputDir = node_path.dirname(input.outputPath);
|
|
8813
|
-
const files = [];
|
|
8814
|
-
const errors = [];
|
|
8815
|
-
const fatal = [];
|
|
8816
|
-
let html = input.html;
|
|
8817
|
-
const tags = input.html.match(IMG_TAG) ?? [];
|
|
8818
|
-
for (const tag of tags) {
|
|
8819
|
-
const srcMatch = tag.match(SRC_ATTR);
|
|
8820
|
-
const rawSrc = srcMatch?.[1] ?? srcMatch?.[2];
|
|
8821
|
-
if (!rawSrc) continue;
|
|
8822
|
-
const src = unescapeHtml(rawSrc);
|
|
8823
|
-
const parsed = parseResourceSrc(src);
|
|
8824
|
-
if (!parsed) continue;
|
|
8825
|
-
const resolved = resolveBundlePath(parsed.pathname, bundleRoot, input.srcDir);
|
|
8826
|
-
if (!resolved.ok) {
|
|
8827
|
-
const message = `[ox-content] page resource ${JSON.stringify(src)} on ${input.inputPath} is outside the page bundle`;
|
|
8828
|
-
errors.push(message);
|
|
8829
|
-
fatal.push(message);
|
|
8830
|
-
continue;
|
|
8831
|
-
}
|
|
8832
|
-
let stat;
|
|
8833
|
-
try {
|
|
8834
|
-
stat = await node_fs_promises.stat(resolved.absolute);
|
|
8835
|
-
} catch {
|
|
8836
|
-
const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
|
|
8837
|
-
errors.push(message);
|
|
8838
|
-
if (input.options.missing === "error") fatal.push(message);
|
|
8839
|
-
continue;
|
|
8840
|
-
}
|
|
8841
|
-
const transformError = validateTransform(parsed.transform, input.options);
|
|
8842
|
-
if (transformError) {
|
|
8843
|
-
const message = `[ox-content] ${transformError} for ${JSON.stringify(src)} on ${input.inputPath}`;
|
|
8844
|
-
errors.push(message);
|
|
8845
|
-
fatal.push(message);
|
|
8846
|
-
continue;
|
|
8847
|
-
}
|
|
8848
|
-
const hasTransform = hasPixelOrFormatTransform(parsed.transform);
|
|
8849
|
-
const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : node_path.basename(resolved.absolute);
|
|
8850
|
-
const outputFile = node_path.join(outputDir, outputName);
|
|
8851
|
-
try {
|
|
8852
|
-
if (hasTransform) await writeTransformedResource({
|
|
8853
|
-
sourcePath: resolved.absolute,
|
|
8854
|
-
outputFile,
|
|
8855
|
-
cacheDir: input.cacheDir,
|
|
8856
|
-
mtimeMs: stat.mtimeMs,
|
|
8857
|
-
transform: parsed.transform
|
|
8858
|
-
});
|
|
8859
|
-
else {
|
|
8860
|
-
await node_fs_promises.mkdir(outputDir, { recursive: true });
|
|
8861
|
-
await node_fs_promises.copyFile(resolved.absolute, outputFile);
|
|
8862
|
-
}
|
|
8863
|
-
files.push(outputFile);
|
|
8864
|
-
const rewritten = tag.replace(rawSrc, escapeAttribute(outputName));
|
|
8865
|
-
html = html.replace(tag, rewritten);
|
|
8866
|
-
} catch (error) {
|
|
8867
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
8868
|
-
const message = `[ox-content] failed to process page resource ${JSON.stringify(src)} on ${input.inputPath}: ${detail}`;
|
|
8869
|
-
errors.push(message);
|
|
8870
|
-
fatal.push(message);
|
|
8871
|
-
}
|
|
8872
|
-
}
|
|
8873
|
-
return {
|
|
8874
|
-
html,
|
|
8875
|
-
files,
|
|
8876
|
-
errors,
|
|
8877
|
-
fatal
|
|
8878
|
-
};
|
|
8879
|
-
}
|
|
8880
|
-
function resolveBundlePath(pathname, bundleRoot, contentRoot) {
|
|
8881
|
-
if (node_path.isAbsolute(pathname) || pathname.includes("\0")) return { ok: false };
|
|
8882
|
-
const absolute = node_path.resolve(bundleRoot, pathname);
|
|
8883
|
-
if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
|
|
8884
|
-
return {
|
|
8885
|
-
ok: true,
|
|
8886
|
-
absolute
|
|
8887
|
-
};
|
|
8888
|
-
}
|
|
8889
|
-
function validateTransform(transform, options) {
|
|
8890
|
-
if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
|
|
8891
|
-
if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
|
|
8892
|
-
}
|
|
8893
|
-
function hasPixelOrFormatTransform(transform) {
|
|
8894
|
-
return Boolean(transform.width || transform.height || transform.crop || transform.format);
|
|
8895
|
-
}
|
|
8896
|
-
function transformedFileName(pathname, transform, cacheKey) {
|
|
8897
|
-
const stem = node_path.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
|
|
8898
|
-
const ext = outputExtension(pathname, transform.format);
|
|
8899
|
-
return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
|
|
8900
|
-
}
|
|
8901
|
-
function outputExtension(pathname, format) {
|
|
8902
|
-
if (format === "jpeg") return "jpg";
|
|
8903
|
-
if (format) return format;
|
|
8904
|
-
const ext = node_path.extname(pathname).slice(1).toLowerCase();
|
|
8905
|
-
return ext === "jpeg" ? "jpg" : ext || "png";
|
|
8906
|
-
}
|
|
8907
|
-
async function writeTransformedResource(input) {
|
|
10263
|
+
async function ensureTransformedCache(input) {
|
|
8908
10264
|
const key = resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform);
|
|
8909
10265
|
const ext = node_path.extname(input.outputFile);
|
|
8910
10266
|
const cacheFile = node_path.join(input.cacheDir, `${key}${ext}`);
|
|
8911
10267
|
try {
|
|
8912
|
-
await node_fs_promises.
|
|
8913
|
-
return;
|
|
10268
|
+
await node_fs_promises.access(cacheFile);
|
|
10269
|
+
return cacheFile;
|
|
8914
10270
|
} catch {}
|
|
8915
10271
|
const output = transformResourceBuffer(await node_fs_promises.readFile(input.sourcePath), input.sourcePath, input.transform);
|
|
8916
10272
|
if (output.length > 8388608) throw new Error("transform produced an oversized file");
|
|
8917
|
-
await node_fs_promises.mkdir(node_path.dirname(input.outputFile), { recursive: true });
|
|
8918
10273
|
await node_fs_promises.mkdir(input.cacheDir, { recursive: true });
|
|
8919
10274
|
await node_fs_promises.writeFile(cacheFile, output);
|
|
8920
|
-
|
|
10275
|
+
return cacheFile;
|
|
8921
10276
|
}
|
|
8922
10277
|
function transformResourceBuffer(source, sourcePath, transform) {
|
|
8923
10278
|
const needsPixels = Boolean(transform.width || transform.height || transform.crop);
|
|
@@ -8960,11 +10315,221 @@ function formatFromPath(filePath) {
|
|
|
8960
10315
|
const ext = node_path.extname(filePath).slice(1).toLowerCase();
|
|
8961
10316
|
return ext === "jpg" ? "jpeg" : ext;
|
|
8962
10317
|
}
|
|
8963
|
-
|
|
8964
|
-
|
|
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
|
+
};
|
|
8965
10376
|
}
|
|
8966
|
-
function
|
|
8967
|
-
|
|
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
|
+
}
|
|
8968
10533
|
}
|
|
8969
10534
|
//#endregion
|
|
8970
10535
|
//#region src/resources.ts
|
|
@@ -8999,19 +10564,22 @@ function resolveResourcesOptions(value) {
|
|
|
8999
10564
|
enabled: false,
|
|
9000
10565
|
formats: [...DEFAULT_FORMATS],
|
|
9001
10566
|
widths: [],
|
|
9002
|
-
missing: "error"
|
|
10567
|
+
missing: "error",
|
|
10568
|
+
dedupe: false
|
|
9003
10569
|
};
|
|
9004
10570
|
if (value === true) return {
|
|
9005
10571
|
enabled: true,
|
|
9006
10572
|
formats: [...DEFAULT_FORMATS],
|
|
9007
10573
|
widths: [],
|
|
9008
|
-
missing: "error"
|
|
10574
|
+
missing: "error",
|
|
10575
|
+
dedupe: false
|
|
9009
10576
|
};
|
|
9010
10577
|
return {
|
|
9011
10578
|
enabled: true,
|
|
9012
10579
|
formats: normalizeFormats(value.formats),
|
|
9013
10580
|
widths: normalizeWidths(value.widths),
|
|
9014
|
-
missing: value.missing === "warn" ? "warn" : "error"
|
|
10581
|
+
missing: value.missing === "warn" ? "warn" : "error",
|
|
10582
|
+
dedupe: value.dedupe === true
|
|
9015
10583
|
};
|
|
9016
10584
|
}
|
|
9017
10585
|
/**
|
|
@@ -9257,10 +10825,12 @@ function resolveSsgOptions(ssg) {
|
|
|
9257
10825
|
pagination: false,
|
|
9258
10826
|
breadcrumbs: false,
|
|
9259
10827
|
jsonLd: false,
|
|
10828
|
+
headValidation: false,
|
|
9260
10829
|
readerChrome: false,
|
|
9261
10830
|
localeSwitcher: false,
|
|
9262
10831
|
a11y: false,
|
|
9263
10832
|
pageChrome: false,
|
|
10833
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9264
10834
|
notFound: resolveNotFoundOptions(void 0),
|
|
9265
10835
|
team: resolveTeamOptions(void 0),
|
|
9266
10836
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9277,10 +10847,12 @@ function resolveSsgOptions(ssg) {
|
|
|
9277
10847
|
pagination: false,
|
|
9278
10848
|
breadcrumbs: false,
|
|
9279
10849
|
jsonLd: false,
|
|
10850
|
+
headValidation: false,
|
|
9280
10851
|
readerChrome: false,
|
|
9281
10852
|
localeSwitcher: false,
|
|
9282
10853
|
a11y: false,
|
|
9283
10854
|
pageChrome: false,
|
|
10855
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9284
10856
|
notFound: resolveNotFoundOptions(void 0),
|
|
9285
10857
|
team: resolveTeamOptions(void 0),
|
|
9286
10858
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9305,10 +10877,12 @@ function resolveSsgOptions(ssg) {
|
|
|
9305
10877
|
pagination: resolvePaginationOption(ssg.pagination),
|
|
9306
10878
|
breadcrumbs: resolvePaginationOption(ssg.breadcrumbs),
|
|
9307
10879
|
jsonLd: resolveJsonLdOption(ssg.jsonLd),
|
|
10880
|
+
headValidation: resolveHeadValidation(ssg.headValidation),
|
|
9308
10881
|
readerChrome: resolveReaderChromeOption(ssg.readerChrome),
|
|
9309
10882
|
localeSwitcher: resolveLocaleSwitcherOption(ssg.localeSwitcher),
|
|
9310
10883
|
a11y: resolveA11yOption(ssg.a11y),
|
|
9311
10884
|
pageChrome: require_vitepress.resolvePageChromeOption(ssg.pageChrome),
|
|
10885
|
+
markdownSource: resolveMarkdownSourceOptions(ssg.markdownSource),
|
|
9312
10886
|
notFound: resolveNotFoundOptions(ssg.notFound),
|
|
9313
10887
|
team: resolveTeamOptions(ssg.team),
|
|
9314
10888
|
blog: resolveBlogOptions(ssg.blog),
|
|
@@ -9336,7 +10910,9 @@ function resolveJsonLdOption(value) {
|
|
|
9336
10910
|
const publisher = resolveJsonLdPublisher(value.publisher);
|
|
9337
10911
|
return {
|
|
9338
10912
|
breadcrumbs: value.breadcrumbs !== false,
|
|
9339
|
-
...publisher ? { publisher } : {}
|
|
10913
|
+
...publisher ? { publisher } : {},
|
|
10914
|
+
...value.type ? { type: value.type } : {},
|
|
10915
|
+
...value.graph ? { graph: value.graph } : {}
|
|
9340
10916
|
};
|
|
9341
10917
|
}
|
|
9342
10918
|
return false;
|
|
@@ -9479,7 +11055,7 @@ function localeCodesFor(locales) {
|
|
|
9479
11055
|
async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales, pagination = false, readerChrome = false, breadcrumbs = false, localeSwitcher = false, localePaths, a11y = false, team = {
|
|
9480
11056
|
enabled: false,
|
|
9481
11057
|
members: []
|
|
9482
|
-
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl) {
|
|
11058
|
+
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl, headValidation = false, defaultLocale) {
|
|
9483
11059
|
const mod = await require_vitepress.importNapiModule();
|
|
9484
11060
|
const tocForRust = pageData.toc.map(toRustTocEntry);
|
|
9485
11061
|
const navGroupsForRust = convertNavGroupsForRust(navGroups);
|
|
@@ -9515,7 +11091,7 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9515
11091
|
linkText: f.linkText
|
|
9516
11092
|
}))
|
|
9517
11093
|
} : void 0;
|
|
9518
|
-
|
|
11094
|
+
const result = mod.generateSsgHtml({
|
|
9519
11095
|
title: pageData.title,
|
|
9520
11096
|
description: pageData.description,
|
|
9521
11097
|
content: pageData.content,
|
|
@@ -9528,12 +11104,16 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9528
11104
|
next: pageData.next,
|
|
9529
11105
|
breadcrumbs: pageData.breadcrumbs,
|
|
9530
11106
|
layout: typeof pageData.frontmatter.layout === "string" ? pageData.frontmatter.layout : void 0,
|
|
9531
|
-
chrome: pageData.chrome
|
|
11107
|
+
chrome: pageData.chrome,
|
|
11108
|
+
robots: typeof pageData.frontmatter.robots === "string" ? pageData.frontmatter.robots : void 0,
|
|
11109
|
+
canonical: typeof pageData.frontmatter.canonical === "string" ? pageData.frontmatter.canonical : void 0
|
|
9532
11110
|
}, navGroupsForRust, {
|
|
9533
11111
|
siteName,
|
|
9534
11112
|
base,
|
|
9535
11113
|
breadcrumbRootHref,
|
|
9536
11114
|
ogImage,
|
|
11115
|
+
siteUrl,
|
|
11116
|
+
headValidation: headValidation || void 0,
|
|
9537
11117
|
theme: themeForRust,
|
|
9538
11118
|
locale,
|
|
9539
11119
|
availableLocales: availableLocales ? toRustLocales(availableLocales) : void 0,
|
|
@@ -9552,9 +11132,18 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9552
11132
|
jsonLd: jsonLd ? {
|
|
9553
11133
|
breadcrumbs: jsonLd.breadcrumbs,
|
|
9554
11134
|
publisher: jsonLd.publisher,
|
|
9555
|
-
siteUrl
|
|
11135
|
+
siteUrl,
|
|
11136
|
+
pageType: jsonLd.type,
|
|
11137
|
+
graph: jsonLd.graph?.map((node) => JSON.stringify(node))
|
|
9556
11138
|
} : void 0
|
|
9557
11139
|
});
|
|
11140
|
+
const html = typeof result === "string" ? result : result.html;
|
|
11141
|
+
reportHeadDiagnostics(typeof result === "string" ? [] : result.diagnostics ?? [], headValidation);
|
|
11142
|
+
return injectSearchLocaleFilters(html, {
|
|
11143
|
+
locales: availableLocales ?? [],
|
|
11144
|
+
current: locale,
|
|
11145
|
+
defaultLocale: defaultLocale ?? availableLocales?.[0]?.code ?? "en"
|
|
11146
|
+
});
|
|
9558
11147
|
}
|
|
9559
11148
|
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
9560
11149
|
const optimized = (await require_vitepress.importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
@@ -9639,6 +11228,7 @@ async function buildSsg(options, root) {
|
|
|
9639
11228
|
applyPermalinkRoutes(context, collected);
|
|
9640
11229
|
errors.push(...collected.errors);
|
|
9641
11230
|
const { outputPages, listedPages } = applyPublishState(context, collected);
|
|
11231
|
+
context.markdownSourcePages.push(...outputPages);
|
|
9642
11232
|
remapPermalinkNav(context, listedPages);
|
|
9643
11233
|
await applyPageResources(context, outputPages, generatedFiles, errors);
|
|
9644
11234
|
await generateOgImageAssets(context, collected, generatedFiles, errors);
|
|
@@ -9716,7 +11306,8 @@ async function createBuildSsgContext(options, root, srcDir, outDir, markdownFile
|
|
|
9716
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)),
|
|
9717
11307
|
siteName: await resolveSiteName$1(root, ssgOptions),
|
|
9718
11308
|
shouldGenerateOgImages: shouldGenerateOgImages(options),
|
|
9719
|
-
|
|
11309
|
+
markdownSourcePages: [],
|
|
11310
|
+
napi: ssgOptions.lastUpdated || ssgOptions.contributors || options.siteMaps?.enabled ? await require_vitepress.importNapiModule() : void 0
|
|
9720
11311
|
};
|
|
9721
11312
|
}
|
|
9722
11313
|
/**
|
|
@@ -9746,6 +11337,7 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9746
11337
|
if (!options?.enabled) return;
|
|
9747
11338
|
const cacheDir = path.join(context.root, ".cache", "ox-content-resources");
|
|
9748
11339
|
const fatal = [];
|
|
11340
|
+
const dedupeStore = options.dedupe ? createResourceDedupeStore() : void 0;
|
|
9749
11341
|
for (const page of pages) {
|
|
9750
11342
|
const processed = await processPageResources({
|
|
9751
11343
|
html: page.transformedHtml,
|
|
@@ -9753,7 +11345,10 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9753
11345
|
outputPath: page.routePaths.outputPath,
|
|
9754
11346
|
srcDir: context.srcDir,
|
|
9755
11347
|
options,
|
|
9756
|
-
cacheDir
|
|
11348
|
+
cacheDir,
|
|
11349
|
+
outDir: context.outDir,
|
|
11350
|
+
base: context.base,
|
|
11351
|
+
dedupeStore
|
|
9757
11352
|
});
|
|
9758
11353
|
page.transformedHtml = processed.html;
|
|
9759
11354
|
generatedFiles.push(...processed.files);
|
|
@@ -9827,7 +11422,8 @@ function applyPublishState(context, collected) {
|
|
|
9827
11422
|
};
|
|
9828
11423
|
}
|
|
9829
11424
|
async function transformSsgPage(context, inputPath) {
|
|
9830
|
-
const
|
|
11425
|
+
const content = await fs_promises.readFile(inputPath, "utf-8");
|
|
11426
|
+
const result = await transformMarkdown(content, inputPath, context.options, {
|
|
9831
11427
|
convertMdLinks: true,
|
|
9832
11428
|
baseUrl: context.base,
|
|
9833
11429
|
sourcePath: inputPath
|
|
@@ -9837,11 +11433,12 @@ async function transformSsgPage(context, inputPath) {
|
|
|
9837
11433
|
const title = extractTitle$1(transformedHtml, frontmatter);
|
|
9838
11434
|
return {
|
|
9839
11435
|
inputPath,
|
|
11436
|
+
source: content,
|
|
9840
11437
|
routePaths: getRoutePaths(inputPath, context.srcDir, context.outDir, context.base, context.ssgOptions.extension, context.ssgOptions.siteUrl),
|
|
9841
11438
|
transformedHtml,
|
|
9842
11439
|
title,
|
|
9843
11440
|
description: frontmatter.description,
|
|
9844
|
-
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,
|
|
9845
11442
|
contributors: contributorsForPage(context, inputPath),
|
|
9846
11443
|
frontmatter,
|
|
9847
11444
|
toc: result.toc
|
|
@@ -9932,17 +11529,18 @@ async function generateHtmlPages(context, pageResults, collected, errors) {
|
|
|
9932
11529
|
async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
9933
11530
|
const { ogImageUrlMap } = collected;
|
|
9934
11531
|
const pageOgImage = context.shouldGenerateOgImages && ogImageUrlMap.has(pageResult.inputPath) ? ogImageUrlMap.get(pageResult.inputPath) : context.ssgOptions.ogImage;
|
|
11532
|
+
const markdownSource = pageMarkdownSourceHref(context, pageResult);
|
|
9935
11533
|
if (context.ssgOptions.render) {
|
|
9936
11534
|
const nav = context.versionNavigation ? rewriteVersionedNavGroups(context.navItems, context.versionNavigation) : context.navItems;
|
|
9937
|
-
return renderPage(toThemePageData(pageResult), {
|
|
11535
|
+
return applyMarkdownSourceAlternate(context, renderPage(toThemePageData(pageResult, markdownSource), {
|
|
9938
11536
|
theme: context.ssgOptions.render,
|
|
9939
11537
|
siteName: context.siteName,
|
|
9940
11538
|
base: context.base,
|
|
9941
11539
|
nav,
|
|
9942
|
-
pages: allPageResults.map(toThemePageData)
|
|
9943
|
-
});
|
|
11540
|
+
pages: allPageResults.map((page) => toThemePageData(page, pageMarkdownSourceHref(context, page)))
|
|
11541
|
+
}), markdownSource);
|
|
9944
11542
|
}
|
|
9945
|
-
if (context.ssgOptions.bare) return generateBarePage({
|
|
11543
|
+
if (context.ssgOptions.bare) return applyMarkdownSourceAlternate(context, generateBarePage({
|
|
9946
11544
|
title: pageResult.title,
|
|
9947
11545
|
content: pageResult.transformedHtml,
|
|
9948
11546
|
lang: context.ssgOptions.lang ?? getPageLocale(pageResult.routePaths.urlPath, context.options.i18n),
|
|
@@ -9953,7 +11551,7 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
9953
11551
|
head: context.ssgOptions.head,
|
|
9954
11552
|
bodyStart: context.ssgOptions.bodyStart,
|
|
9955
11553
|
bodyEnd: context.ssgOptions.bodyEnd
|
|
9956
|
-
});
|
|
11554
|
+
}), markdownSource);
|
|
9957
11555
|
const pageData = createSsgPageData(pageResult);
|
|
9958
11556
|
const versionNavigation = context.versionNavigation;
|
|
9959
11557
|
if (versionNavigation) {
|
|
@@ -9994,10 +11592,18 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
9994
11592
|
base: context.base,
|
|
9995
11593
|
roots: versionNavigation ? versionedLocaleRoots(versionNavigation, i18n.locales, i18n.defaultLocale, i18n.hideDefaultLocale) : void 0
|
|
9996
11594
|
}) : void 0;
|
|
9997
|
-
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 ?? {
|
|
9998
11596
|
enabled: false,
|
|
9999
11597
|
members: []
|
|
10000
|
-
}, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl);
|
|
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);
|
|
10001
11607
|
}
|
|
10002
11608
|
function rewritePagerOverride(pager, context) {
|
|
10003
11609
|
return pager?.href ? {
|
|
@@ -10006,7 +11612,7 @@ function rewritePagerOverride(pager, context) {
|
|
|
10006
11612
|
} : pager;
|
|
10007
11613
|
}
|
|
10008
11614
|
/** Maps an internal page result onto the theme renderer's page shape. */
|
|
10009
|
-
function toThemePageData(pageResult) {
|
|
11615
|
+
function toThemePageData(pageResult, markdownSource) {
|
|
10010
11616
|
return {
|
|
10011
11617
|
title: pageResult.title,
|
|
10012
11618
|
description: pageResult.description,
|
|
@@ -10016,6 +11622,7 @@ function toThemePageData(pageResult) {
|
|
|
10016
11622
|
contributors: pageResult.contributors,
|
|
10017
11623
|
path: pageResult.inputPath,
|
|
10018
11624
|
url: pageResult.routePaths.href,
|
|
11625
|
+
markdownSource,
|
|
10019
11626
|
frontmatter: pageResult.frontmatter,
|
|
10020
11627
|
layout: typeof pageResult.frontmatter.layout === "string" ? pageResult.frontmatter.layout : void 0
|
|
10021
11628
|
};
|
|
@@ -10128,6 +11735,7 @@ async function applyDocumentationVersions(generatedPages, context, errors) {
|
|
|
10128
11735
|
...page.routePaths,
|
|
10129
11736
|
...prefixRoutePaths(page.routePaths, entry.prefix, context.outDir, context.base)
|
|
10130
11737
|
};
|
|
11738
|
+
context.markdownSourcePages.push(...outputPages);
|
|
10131
11739
|
snapContext.versionNavigation = createVersionNavigationContext({
|
|
10132
11740
|
prefix: entry.prefix,
|
|
10133
11741
|
base: context.base,
|
|
@@ -10240,6 +11848,20 @@ async function writeGeneratedPages(generatedPages, context, generatedFiles, list
|
|
|
10240
11848
|
errors.push(feeds.warning);
|
|
10241
11849
|
console.warn(feeds.warning);
|
|
10242
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);
|
|
10243
11865
|
}
|
|
10244
11866
|
/** Turns an SSG `urlPath` (`guide` or `/`) into a same-origin dest (`/guide`). */
|
|
10245
11867
|
function sitePathFromUrlPath(urlPath) {
|
|
@@ -10253,6 +11875,7 @@ function sitemapPages(context, listedPages, outputPages) {
|
|
|
10253
11875
|
loc: canonicalPageUrl(context, page.routePaths.urlPath) ?? "",
|
|
10254
11876
|
title: page.title,
|
|
10255
11877
|
description: page.description,
|
|
11878
|
+
lastUpdated: page.lastUpdated,
|
|
10256
11879
|
draft: page.frontmatter.draft === true,
|
|
10257
11880
|
unlisted: Boolean(context.options.publishState?.enabled) && !listedPaths.has(page.inputPath)
|
|
10258
11881
|
}));
|
|
@@ -10350,7 +11973,8 @@ function createDevServerCache() {
|
|
|
10350
11973
|
navGroups: null,
|
|
10351
11974
|
localePages: null,
|
|
10352
11975
|
pages: /* @__PURE__ */ new Map(),
|
|
10353
|
-
siteName: null
|
|
11976
|
+
siteName: null,
|
|
11977
|
+
markdownSourceIndex: null
|
|
10354
11978
|
};
|
|
10355
11979
|
}
|
|
10356
11980
|
/**
|
|
@@ -10359,6 +11983,7 @@ function createDevServerCache() {
|
|
|
10359
11983
|
function invalidateNavCache(cache) {
|
|
10360
11984
|
cache.navGroups = null;
|
|
10361
11985
|
cache.localePages = null;
|
|
11986
|
+
cache.markdownSourceIndex = null;
|
|
10362
11987
|
cache.pages.clear();
|
|
10363
11988
|
}
|
|
10364
11989
|
/**
|
|
@@ -10366,6 +11991,7 @@ function invalidateNavCache(cache) {
|
|
|
10366
11991
|
*/
|
|
10367
11992
|
function invalidatePageCache(cache, filePath) {
|
|
10368
11993
|
cache.pages.delete(filePath);
|
|
11994
|
+
cache.markdownSourceIndex = null;
|
|
10369
11995
|
}
|
|
10370
11996
|
/**
|
|
10371
11997
|
* Resolve site name from options or package.json.
|
|
@@ -10455,13 +12081,35 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
|
|
|
10455
12081
|
pages: localePages,
|
|
10456
12082
|
base
|
|
10457
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;
|
|
10458
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 ?? {
|
|
10459
12094
|
enabled: false,
|
|
10460
12095
|
members: []
|
|
10461
|
-
}, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl);
|
|
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);
|
|
10462
12098
|
html = injectViteHmrClient(html);
|
|
10463
12099
|
return html;
|
|
10464
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
|
+
}
|
|
10465
12113
|
/**
|
|
10466
12114
|
* Create the dev server middleware for SSG page serving.
|
|
10467
12115
|
*/
|
|
@@ -10474,6 +12122,19 @@ function createDevServerMiddleware(options, root, cache) {
|
|
|
10474
12122
|
let routeUrl = url;
|
|
10475
12123
|
if (base !== "/" && routeUrl.startsWith(base)) routeUrl = "/" + routeUrl.slice(base.length);
|
|
10476
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
|
+
}
|
|
10477
12138
|
const filePath = await resolveMarkdownFile(routeUrl, srcDir, options.extensions);
|
|
10478
12139
|
if (!filePath) return next();
|
|
10479
12140
|
try {
|
|
@@ -10990,6 +12651,43 @@ function resolveCardOptions(options) {
|
|
|
10990
12651
|
return { enabled: options.enabled ?? true };
|
|
10991
12652
|
}
|
|
10992
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
|
|
10993
12691
|
//#region src/include-options.ts
|
|
10994
12692
|
function resolveIncludeOptions(options) {
|
|
10995
12693
|
if (!options) return { enabled: false };
|
|
@@ -11564,6 +13262,12 @@ function createFrameworkMarkdownOptions(options) {
|
|
|
11564
13262
|
},
|
|
11565
13263
|
attrs: { enabled: false },
|
|
11566
13264
|
badges: { enabled: false },
|
|
13265
|
+
magicLinks: {
|
|
13266
|
+
enabled: false,
|
|
13267
|
+
aliases: {},
|
|
13268
|
+
favicon: false,
|
|
13269
|
+
imageOverrides: []
|
|
13270
|
+
},
|
|
11567
13271
|
containers: {
|
|
11568
13272
|
enabled: false,
|
|
11569
13273
|
types: {}
|
|
@@ -12563,7 +14267,7 @@ function createSsgPlugin(resolvedOptions, getRoot, ssgDevCache) {
|
|
|
12563
14267
|
for (const error of result.errors) console.warn(`[ox-content] ${error}`);
|
|
12564
14268
|
} catch (err) {
|
|
12565
14269
|
console.error("[ox-content] SSG build failed:", err);
|
|
12566
|
-
if (err instanceof PageResourceError) throw err;
|
|
14270
|
+
if (err instanceof PageResourceError || err instanceof BlogFeedError) throw err;
|
|
12567
14271
|
}
|
|
12568
14272
|
}
|
|
12569
14273
|
};
|
|
@@ -12678,6 +14382,7 @@ function resolveOptions(options) {
|
|
|
12678
14382
|
gfm: options.gfm ?? true,
|
|
12679
14383
|
mdx: options.mdx,
|
|
12680
14384
|
footnotes: options.footnotes ?? true,
|
|
14385
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
12681
14386
|
tables: options.tables ?? true,
|
|
12682
14387
|
taskLists: options.taskLists ?? true,
|
|
12683
14388
|
strikethrough: options.strikethrough ?? true,
|
|
@@ -12688,6 +14393,7 @@ function resolveOptions(options) {
|
|
|
12688
14393
|
emojiShortcodes: resolveEmojiShortcodeOptions(options.emojiShortcodes),
|
|
12689
14394
|
attrs: resolveAttrsOptions(options.attrs),
|
|
12690
14395
|
badges: resolveBadgeOptions(options.badges),
|
|
14396
|
+
magicLinks: resolveMagicLinkOptions(options.magicLinks),
|
|
12691
14397
|
containers: resolveContainerOptions(options.containers),
|
|
12692
14398
|
images: resolveImageOptions(options.images),
|
|
12693
14399
|
codeImports: resolveCodeImportOptions(options.codeImports),
|
|
@@ -12707,6 +14413,7 @@ function resolveOptions(options) {
|
|
|
12707
14413
|
frontmatter: options.frontmatter ?? true,
|
|
12708
14414
|
toc: options.toc ?? true,
|
|
12709
14415
|
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
14416
|
+
headingPermalinks: resolveHeadingPermalinksOptions(options.headingPermalinks),
|
|
12710
14417
|
ogImage: options.ogImage ?? false,
|
|
12711
14418
|
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
12712
14419
|
transformers: options.transformers ?? [],
|
|
@@ -12948,9 +14655,9 @@ function resolveCodeAnnotationsOptions(options) {
|
|
|
12948
14655
|
/**
|
|
12949
14656
|
* Generates virtual module content.
|
|
12950
14657
|
*/
|
|
12951
|
-
function generateVirtualModule(path$
|
|
12952
|
-
if (path$
|
|
12953
|
-
if (path$
|
|
14658
|
+
function generateVirtualModule(path$8, options) {
|
|
14659
|
+
if (path$8 === "config") return `export default ${JSON.stringify(options)};`;
|
|
14660
|
+
if (path$8 === "runtime") {
|
|
12954
14661
|
const base = normalizeRuntimeBase(options.base);
|
|
12955
14662
|
return `
|
|
12956
14663
|
export const base = ${JSON.stringify(base)};
|
|
@@ -12997,6 +14704,7 @@ function normalizeRuntimeBase(base) {
|
|
|
12997
14704
|
return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
|
|
12998
14705
|
}
|
|
12999
14706
|
//#endregion
|
|
14707
|
+
exports.BlogFeedError = BlogFeedError;
|
|
13000
14708
|
exports.DEFAULT_HTML_TEMPLATE = DEFAULT_HTML_TEMPLATE;
|
|
13001
14709
|
exports.DEFAULT_MARKDOWN_EXTENSIONS = DEFAULT_MARKDOWN_EXTENSIONS;
|
|
13002
14710
|
exports.DefaultTheme = DefaultTheme;
|
|
@@ -13080,6 +14788,7 @@ exports.prefetchOgpData = prefetchOgpData;
|
|
|
13080
14788
|
exports.raw = require_jsx_html.raw;
|
|
13081
14789
|
exports.readingTimeMinutes = readingTimeMinutes;
|
|
13082
14790
|
exports.renderAllPages = renderAllPages;
|
|
14791
|
+
exports.renderHead = renderHead;
|
|
13083
14792
|
exports.renderHtmlToFrameworkCode = renderHtmlToFrameworkCode;
|
|
13084
14793
|
exports.renderHtmlToReactComponent = renderHtmlToReactComponent;
|
|
13085
14794
|
exports.renderHtmlToReactCreateElement = renderHtmlToReactCreateElement;
|
|
@@ -13102,11 +14811,14 @@ exports.resolveDocsOptions = resolveDocsOptions;
|
|
|
13102
14811
|
exports.resolveDocumentComponentImports = resolveDocumentComponentImports;
|
|
13103
14812
|
exports.resolveFeedsOptions = resolveFeedsOptions;
|
|
13104
14813
|
exports.resolveFileTreeOptions = resolveFileTreeOptions;
|
|
14814
|
+
exports.resolveHeadValidation = resolveHeadValidation;
|
|
13105
14815
|
exports.resolveHeaderNavItems = require_vitepress.resolveHeaderNavItems;
|
|
14816
|
+
exports.resolveHeadingPermalinksOptions = resolveHeadingPermalinksOptions;
|
|
13106
14817
|
exports.resolveI18nOptions = resolveI18nOptions;
|
|
13107
14818
|
exports.resolveImageOptions = resolveImageOptions;
|
|
13108
14819
|
exports.resolveIncludeOptions = resolveIncludeOptions;
|
|
13109
14820
|
exports.resolveLocaleLabel = require_vitepress.resolveLocaleLabel;
|
|
14821
|
+
exports.resolveMarkdownSourceOptions = resolveMarkdownSourceOptions;
|
|
13110
14822
|
exports.resolveMathOptions = resolveMathOptions;
|
|
13111
14823
|
exports.resolveMdxForFilePath = resolveMdxForFilePath;
|
|
13112
14824
|
exports.resolveNotFoundOptions = resolveNotFoundOptions;
|