@ox-content/vite-plugin 3.0.0-alpha.5 → 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 +2269 -589
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +269 -25
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +269 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2287 -610
- package/dist/index.mjs.map +1 -1
- package/dist/vitepress.cjs +3 -0
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.mjs +3 -0
- package/dist/vitepress.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -11,14 +11,15 @@ import fs, { createReadStream, existsSync, readFileSync } from "node:fs";
|
|
|
11
11
|
import * as path$1 from "node:path";
|
|
12
12
|
import path, { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
13
13
|
import * as fs$2 from "node:fs/promises";
|
|
14
|
-
import { access, copyFile, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import { access, copyFile, cp, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
15
15
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
16
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
16
17
|
import { tmpdir } from "node:os";
|
|
17
18
|
import { promisify } from "node:util";
|
|
18
19
|
import { execFile, spawn } from "node:child_process";
|
|
19
20
|
import * as fs$3 from "fs/promises";
|
|
20
21
|
import * as crypto from "crypto";
|
|
21
|
-
import {
|
|
22
|
+
import { lookup } from "node:dns/promises";
|
|
22
23
|
import { deflateSync, inflateSync } from "node:zlib";
|
|
23
24
|
import * as fs$1 from "fs";
|
|
24
25
|
import { glob } from "glob";
|
|
@@ -449,7 +450,8 @@ async function transformPm(html, options) {
|
|
|
449
450
|
* YouTube Plugin - Privacy-enhanced iframe embedding
|
|
450
451
|
*
|
|
451
452
|
* Transforms <YouTube> components into responsive iframe embeds using
|
|
452
|
-
* youtube-nocookie.com for enhanced privacy.
|
|
453
|
+
* youtube-nocookie.com for enhanced privacy. A digits-only `start` attribute
|
|
454
|
+
* becomes `?start=` on the iframe URL.
|
|
453
455
|
*
|
|
454
456
|
* The HTML rewrite is performed in Rust (`transformYoutubeEmbeds` in
|
|
455
457
|
* @ox-content/napi), replacing the previous rehype parse/stringify
|
|
@@ -480,7 +482,7 @@ async function transformYouTube(html, options) {
|
|
|
480
482
|
}
|
|
481
483
|
//#endregion
|
|
482
484
|
//#region src/plugins/twitter/url.ts
|
|
483
|
-
const STATUS_PATH = /^\/(?:[^/]+|i\/web)\/status\/(\d+)(?:\/.*)?$/;
|
|
485
|
+
const STATUS_PATH$1 = /^\/(?:[^/]+|i\/web)\/status\/(\d+)(?:\/.*)?$/;
|
|
484
486
|
function createSyndicationToken(id) {
|
|
485
487
|
return (Number(id) / 0x38d7ea4c68000 * Math.PI).toString(36).replaceAll(/(0+|\.)/g, "");
|
|
486
488
|
}
|
|
@@ -494,7 +496,7 @@ function parseTweetReference(value) {
|
|
|
494
496
|
const url = new URL(trimmed);
|
|
495
497
|
const hostname = url.hostname.toLowerCase().replace(/^(?:www\.|mobile\.)/, "");
|
|
496
498
|
if (url.protocol !== "https:" || hostname !== "x.com" && hostname !== "twitter.com") return null;
|
|
497
|
-
const match = url.pathname.match(STATUS_PATH);
|
|
499
|
+
const match = url.pathname.match(STATUS_PATH$1);
|
|
498
500
|
if (!match) return null;
|
|
499
501
|
const screenName = url.pathname.startsWith("/i/web/status/") ? "i/web" : url.pathname.split("/")[1];
|
|
500
502
|
return {
|
|
@@ -505,10 +507,164 @@ function parseTweetReference(value) {
|
|
|
505
507
|
return null;
|
|
506
508
|
}
|
|
507
509
|
}
|
|
508
|
-
function
|
|
510
|
+
function tweetElementAttributes(attributes) {
|
|
509
511
|
const values = /* @__PURE__ */ new Map();
|
|
510
|
-
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] ?? "");
|
|
511
|
-
|
|
512
|
+
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] ?? "");
|
|
513
|
+
const appearance = values.get("appearance");
|
|
514
|
+
return {
|
|
515
|
+
reference: parseTweetReference(values.get("url") ?? values.get("href") ?? values.get("id") ?? ""),
|
|
516
|
+
appearance: appearance === "full" || appearance === "compact" ? appearance : void 0
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/plugins/twitter/validate.ts
|
|
521
|
+
const SCREEN_NAME = /^[A-Za-z0-9_]{1,15}$/;
|
|
522
|
+
const STATUS_ID = /^\d+$/;
|
|
523
|
+
const STATUS_PATH = /(?:x\.com|twitter\.com)\/(?:[^/]+|i\/web)\/status\/(\d+)/i;
|
|
524
|
+
const TCO = /^https?:\/\/t\.co\/[A-Za-z0-9]+$/i;
|
|
525
|
+
function isTweetData(data) {
|
|
526
|
+
return isTweetBodyData(data);
|
|
527
|
+
}
|
|
528
|
+
function parseTweetData(data) {
|
|
529
|
+
return isTweetData(data) ? normalizeTweetData(data) : null;
|
|
530
|
+
}
|
|
531
|
+
function isTweetBodyData(data) {
|
|
532
|
+
if (!data || typeof data !== "object") return false;
|
|
533
|
+
const value = data;
|
|
534
|
+
return typeof value.text === "string" && isTweetUser(value.user);
|
|
535
|
+
}
|
|
536
|
+
function isTweetUser(value) {
|
|
537
|
+
if (!value || typeof value !== "object") return false;
|
|
538
|
+
const user = value;
|
|
539
|
+
return typeof user.name === "string" && typeof user.screen_name === "string";
|
|
540
|
+
}
|
|
541
|
+
function normalizeTweetData(data) {
|
|
542
|
+
const handle = sanitizeScreenName(data.in_reply_to_screen_name);
|
|
543
|
+
return {
|
|
544
|
+
...data,
|
|
545
|
+
quoted_tweet: isTweetBodyData(data.quoted_tweet) ? stripNestedQuote(data.quoted_tweet) : void 0,
|
|
546
|
+
in_reply_to_screen_name: handle,
|
|
547
|
+
in_reply_to_status_id_str: handle ? sanitizeStatusId(data.in_reply_to_status_id_str) : void 0
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function stripNestedQuote(data) {
|
|
551
|
+
const { quoted_tweet: _nested, ...quoted } = data;
|
|
552
|
+
return quoted;
|
|
553
|
+
}
|
|
554
|
+
function sanitizeScreenName(value) {
|
|
555
|
+
return value && SCREEN_NAME.test(value) ? value : void 0;
|
|
556
|
+
}
|
|
557
|
+
function sanitizeStatusId(value) {
|
|
558
|
+
return value && STATUS_ID.test(value) ? value : void 0;
|
|
559
|
+
}
|
|
560
|
+
function quotedPermalink(quoted) {
|
|
561
|
+
const id = sanitizeStatusId(quoted.id_str);
|
|
562
|
+
if (!id) return void 0;
|
|
563
|
+
return `https://x.com/${sanitizeScreenName(quoted.user.screen_name) ?? "i/web"}/status/${id}`;
|
|
564
|
+
}
|
|
565
|
+
function replyPermalink(data) {
|
|
566
|
+
const handle = sanitizeScreenName(data.in_reply_to_screen_name);
|
|
567
|
+
if (!handle) return void 0;
|
|
568
|
+
const id = sanitizeStatusId(data.in_reply_to_status_id_str);
|
|
569
|
+
return id ? `https://x.com/${handle}/status/${id}` : `https://x.com/${handle}`;
|
|
570
|
+
}
|
|
571
|
+
function visibleTextRange(data, omitTrailingQuoteUrl = false) {
|
|
572
|
+
const start = Math.max(0, data.display_text_range?.[0] ?? 0);
|
|
573
|
+
let end = Math.min(data.text.length, data.display_text_range?.[1] ?? data.text.length);
|
|
574
|
+
if (!omitTrailingQuoteUrl || start >= end) return [start, end];
|
|
575
|
+
const quoted = "quoted_tweet" in data ? data.quoted_tweet : void 0;
|
|
576
|
+
for (const entity of data.entities?.urls ?? []) {
|
|
577
|
+
const indices = entity.indices;
|
|
578
|
+
if (!indices || !isQuoteUrlEntity(entity, quoted)) continue;
|
|
579
|
+
const [entityStart, entityEnd] = indices;
|
|
580
|
+
if (entityStart >= start && isTrailingEntity(entityEnd, end, data.text)) end = Math.min(end, entityStart);
|
|
581
|
+
}
|
|
582
|
+
while (end > start && isUtf16Space(data.text, end - 1)) end -= 1;
|
|
583
|
+
return [start, end];
|
|
584
|
+
}
|
|
585
|
+
function isQuoteUrlEntity(entity, quoted) {
|
|
586
|
+
for (const href of [
|
|
587
|
+
entity.expanded_url,
|
|
588
|
+
entity.url,
|
|
589
|
+
entity.display_url
|
|
590
|
+
]) {
|
|
591
|
+
if (!href) continue;
|
|
592
|
+
const match = href.match(STATUS_PATH);
|
|
593
|
+
if (match) return !quoted?.id_str || match[1] === quoted.id_str;
|
|
594
|
+
if (TCO.test(href)) return true;
|
|
595
|
+
}
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
function isTrailingEntity(entityEnd, rangeEnd, text) {
|
|
599
|
+
if (entityEnd >= rangeEnd || entityEnd === text.length) return true;
|
|
600
|
+
return entityEnd > 0 && /^[\t\n\r ]*$/.test(text.slice(entityEnd, rangeEnd));
|
|
601
|
+
}
|
|
602
|
+
function isUtf16Space(text, index) {
|
|
603
|
+
const char = text[index];
|
|
604
|
+
return char === " " || char === "\n" || char === " " || char === "\r";
|
|
605
|
+
}
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/plugins/twitter/video.ts
|
|
608
|
+
const VIDEO_HOSTS = /* @__PURE__ */ new Set(["pbs.twimg.com", "video.twimg.com"]);
|
|
609
|
+
function selectBestMp4Url(variants) {
|
|
610
|
+
const candidates = (variants ?? []).filter((variant) => isVideoMp4Type(variant.content_type) && isAllowedVideoUrl(variant.url));
|
|
611
|
+
if (candidates.length === 0) return void 0;
|
|
612
|
+
return candidates.reduce((best, variant) => {
|
|
613
|
+
const bestBitrate = best.bitrate ?? Number.NEGATIVE_INFINITY;
|
|
614
|
+
const nextBitrate = variant.bitrate ?? Number.NEGATIVE_INFINITY;
|
|
615
|
+
if (nextBitrate > bestBitrate) return variant;
|
|
616
|
+
if (nextBitrate === bestBitrate && variant.url < best.url) return variant;
|
|
617
|
+
return best;
|
|
618
|
+
}).url;
|
|
619
|
+
}
|
|
620
|
+
function isVideoMp4Type(value) {
|
|
621
|
+
return (value ?? "").split(";", 1)[0].trim().toLowerCase() === "video/mp4";
|
|
622
|
+
}
|
|
623
|
+
function isAllowedVideoUrl(value) {
|
|
624
|
+
if (!value) return false;
|
|
625
|
+
try {
|
|
626
|
+
const url = new URL(value);
|
|
627
|
+
return url.protocol === "https:" && VIDEO_HOSTS.has(url.hostname.toLowerCase());
|
|
628
|
+
} catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function downloadVideoAsset(source, basename, options) {
|
|
633
|
+
if (!isAllowedVideoUrl(source)) return void 0;
|
|
634
|
+
const filename = `${sanitizeFilename(basename)}.mp4`;
|
|
635
|
+
const output = path.join(options.mediaOutputDir, filename);
|
|
636
|
+
const publicPath = joinPublicPath$1(options.mediaPublicPath, filename);
|
|
637
|
+
try {
|
|
638
|
+
await access(output);
|
|
639
|
+
return publicPath;
|
|
640
|
+
} catch {}
|
|
641
|
+
const controller = new AbortController();
|
|
642
|
+
const timeout = setTimeout(() => controller.abort(), options.timeout);
|
|
643
|
+
try {
|
|
644
|
+
const response = await fetch(source, {
|
|
645
|
+
headers: { Accept: "video/mp4" },
|
|
646
|
+
signal: controller.signal
|
|
647
|
+
});
|
|
648
|
+
if (!response.ok) return void 0;
|
|
649
|
+
if (!isVideoMp4Type(response.headers?.get("content-type"))) return void 0;
|
|
650
|
+
const declared = Number(response.headers?.get("content-length"));
|
|
651
|
+
if (Number.isFinite(declared) && declared > options.maxVideoBytes) return void 0;
|
|
652
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
653
|
+
if (bytes.byteLength > options.maxVideoBytes) return void 0;
|
|
654
|
+
await mkdir(options.mediaOutputDir, { recursive: true });
|
|
655
|
+
await writeFile(output, bytes);
|
|
656
|
+
return publicPath;
|
|
657
|
+
} catch {
|
|
658
|
+
return;
|
|
659
|
+
} finally {
|
|
660
|
+
clearTimeout(timeout);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function sanitizeFilename(value) {
|
|
664
|
+
return value.replaceAll(/[^a-zA-Z0-9_-]/g, "-") || "video";
|
|
665
|
+
}
|
|
666
|
+
function joinPublicPath$1(prefix, filename) {
|
|
667
|
+
return `${prefix.replace(/\/$/, "")}/${filename}`;
|
|
512
668
|
}
|
|
513
669
|
//#endregion
|
|
514
670
|
//#region src/plugins/twitter/fetch.ts
|
|
@@ -536,8 +692,8 @@ async function fetchTweetData(id, options) {
|
|
|
536
692
|
signal: controller.signal
|
|
537
693
|
});
|
|
538
694
|
if (!response.ok) return null;
|
|
539
|
-
const data = await response.json();
|
|
540
|
-
if (!
|
|
695
|
+
const data = parseTweetData(await response.json());
|
|
696
|
+
if (!data) return null;
|
|
541
697
|
if (options.cache) {
|
|
542
698
|
tweetCache.set(key, data);
|
|
543
699
|
await writeCachedTweet(key, data, options.cacheDir);
|
|
@@ -550,15 +706,29 @@ async function fetchTweetData(id, options) {
|
|
|
550
706
|
}
|
|
551
707
|
}
|
|
552
708
|
async function materializeTweetAssets(id, data, options) {
|
|
709
|
+
const assets = await materializeBodyAssets(id, data, options);
|
|
710
|
+
if (data.quoted_tweet) assets.quoted = await materializeBodyAssets(`${id}-quoted`, data.quoted_tweet, options);
|
|
711
|
+
return assets;
|
|
712
|
+
}
|
|
713
|
+
async function materializeBodyAssets(id, data, options) {
|
|
553
714
|
const assets = { media: [] };
|
|
554
715
|
const avatarUrl = data.user.profile_image_url_https?.replace(/_normal(?=\.[^.]+$)/, "_bigger");
|
|
555
716
|
if (avatarUrl) assets.avatar = await downloadAsset(avatarUrl, `${id}-avatar`, options);
|
|
556
717
|
const media = data.mediaDetails ?? data.entities?.media ?? [];
|
|
557
718
|
for (const [index, item] of media.entries()) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
719
|
+
const kind = item.type === "video" || item.type === "animated_gif" ? item.type : "photo";
|
|
720
|
+
const basename = `${id}-media-${index + 1}`;
|
|
721
|
+
if (kind === "photo") {
|
|
722
|
+
if (item.type && item.type !== "photo") continue;
|
|
723
|
+
if (!item.media_url_https) continue;
|
|
724
|
+
const src = await downloadAsset(item.media_url_https, basename, options);
|
|
725
|
+
if (src) assets.media.push(assetRecord("photo", src, item));
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const poster = item.media_url_https ? await downloadAsset(item.media_url_https, `${basename}-poster`, options) : void 0;
|
|
729
|
+
const videoUrl = options.downloadVideo ? selectBestMp4Url(item.video_info?.variants) : void 0;
|
|
730
|
+
const src = videoUrl ? await downloadVideoAsset(videoUrl, basename, options) : void 0;
|
|
731
|
+
assets.media.push(assetRecord(kind, src, item, poster));
|
|
562
732
|
}
|
|
563
733
|
return assets;
|
|
564
734
|
}
|
|
@@ -588,8 +758,7 @@ async function downloadAsset(source, basename, options) {
|
|
|
588
758
|
}
|
|
589
759
|
async function readCachedTweet(key, directory) {
|
|
590
760
|
try {
|
|
591
|
-
|
|
592
|
-
return isTweetData(data) ? data : null;
|
|
761
|
+
return parseTweetData(JSON.parse(await readFile(path.join(directory, `${key}.json`), "utf8")));
|
|
593
762
|
} catch {
|
|
594
763
|
return null;
|
|
595
764
|
}
|
|
@@ -600,11 +769,6 @@ async function writeCachedTweet(key, data, directory) {
|
|
|
600
769
|
await writeFile(path.join(directory, `${key}.json`), `${JSON.stringify(data)}\n`);
|
|
601
770
|
} catch {}
|
|
602
771
|
}
|
|
603
|
-
function isTweetData(data) {
|
|
604
|
-
if (!data || typeof data !== "object") return false;
|
|
605
|
-
const value = data;
|
|
606
|
-
return typeof value.text === "string" && Boolean(value.user) && typeof value.user?.name === "string" && typeof value.user.screen_name === "string";
|
|
607
|
-
}
|
|
608
772
|
function extensionFromUrl(url) {
|
|
609
773
|
const match = url.pathname.match(/\.(jpe?g|png|webp|gif)$/i);
|
|
610
774
|
return match ? `.${match[1].toLowerCase().replace("jpeg", "jpg")}` : ".jpg";
|
|
@@ -615,39 +779,64 @@ function joinPublicPath(prefix, filename) {
|
|
|
615
779
|
function sanitizeSegment(value) {
|
|
616
780
|
return value.replaceAll(/[^a-zA-Z0-9_-]/g, "-");
|
|
617
781
|
}
|
|
618
|
-
function assetRecord(src, media) {
|
|
782
|
+
function assetRecord(kind, src, media, poster) {
|
|
619
783
|
return {
|
|
784
|
+
kind,
|
|
620
785
|
src,
|
|
786
|
+
poster,
|
|
621
787
|
alt: media.ext_alt_text,
|
|
622
788
|
width: media.original_info?.width,
|
|
623
789
|
height: media.original_info?.height
|
|
624
790
|
};
|
|
625
791
|
}
|
|
626
792
|
//#endregion
|
|
627
|
-
//#region src/plugins/twitter/
|
|
628
|
-
function
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const media = renderMedia(assets);
|
|
634
|
-
const footer = renderFooter(permalink, data.created_at, options.lang);
|
|
635
|
-
return [
|
|
636
|
-
"<figure class=\"ox-tweet ox-tweet--fetched\">",
|
|
637
|
-
"<header class=\"ox-tweet__header\">",
|
|
638
|
-
`<a class="ox-tweet__profile" href="${escapeAttribute$2(profile)}" target="_blank" rel="noopener noreferrer">`,
|
|
639
|
-
avatar,
|
|
640
|
-
`<span class="ox-tweet__author-name">${author}</span>`,
|
|
641
|
-
`<span class="ox-tweet__author-handle">@${handle}</span>`,
|
|
642
|
-
"</a></header>",
|
|
643
|
-
`<div class="ox-tweet__body">${renderTweetText(data)}</div>`,
|
|
644
|
-
media,
|
|
645
|
-
footer,
|
|
646
|
-
"</figure>"
|
|
647
|
-
].join("");
|
|
793
|
+
//#region src/plugins/twitter/html.ts
|
|
794
|
+
function escapeText(value) {
|
|
795
|
+
return escapeHtml$6(value).replaceAll("\n", "<br>");
|
|
796
|
+
}
|
|
797
|
+
function escapeAttribute$3(value) {
|
|
798
|
+
return escapeHtml$6(value).replaceAll("`", "`");
|
|
648
799
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
800
|
+
function escapeHtml$6(value) {
|
|
801
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
802
|
+
}
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/plugins/twitter/markup.ts
|
|
805
|
+
function renderMedia(assets, permalink) {
|
|
806
|
+
if (assets.media.length === 0) return "";
|
|
807
|
+
const items = assets.media.map((item) => renderMediaItem(item, permalink)).join("");
|
|
808
|
+
return `<div class="ox-tweet__media" data-count="${assets.media.length}">${items}</div>`;
|
|
809
|
+
}
|
|
810
|
+
function renderMediaItem(item, permalink) {
|
|
811
|
+
if (item.kind === "video" || item.kind === "animated_gif") return renderVideoItem(item, permalink);
|
|
812
|
+
const size = sizeAttributes(item);
|
|
813
|
+
return `<img class="ox-tweet__media-item" src="${escapeAttribute$3(item.src ?? "")}" alt="${escapeAttribute$3(item.alt ?? "")}"${size} loading="lazy" decoding="async">`;
|
|
814
|
+
}
|
|
815
|
+
function renderVideoItem(item, permalink) {
|
|
816
|
+
const watch = watchOnX(permalink);
|
|
817
|
+
const size = sizeAttributes(item);
|
|
818
|
+
const src = selfHostedMediaSrc(item.src);
|
|
819
|
+
if (src) {
|
|
820
|
+
const poster = item.poster ? ` poster="${escapeAttribute$3(item.poster)}"` : "";
|
|
821
|
+
const gif = item.kind === "animated_gif" ? " muted loop" : "";
|
|
822
|
+
return `<video class="ox-tweet__media-item" src="${escapeAttribute$3(src)}"${poster}${size} controls playsinline preload="none"${gif}>${watch}</video>`;
|
|
823
|
+
}
|
|
824
|
+
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>`;
|
|
825
|
+
}
|
|
826
|
+
function selfHostedMediaSrc(src) {
|
|
827
|
+
return src && !src.includes("video.twimg.com") ? src : void 0;
|
|
828
|
+
}
|
|
829
|
+
function sizeAttributes(item) {
|
|
830
|
+
return [item.width ? ` width="${item.width}"` : "", item.height ? ` height="${item.height}"` : ""].join("");
|
|
831
|
+
}
|
|
832
|
+
function watchOnX(permalink) {
|
|
833
|
+
if (!permalink) return "";
|
|
834
|
+
return `<a class="ox-tweet__watch" href="${escapeAttribute$3(permalink)}" target="_blank" rel="noopener noreferrer">Watch on X</a>`;
|
|
835
|
+
}
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/plugins/twitter/text.ts
|
|
838
|
+
function renderTweetText(data, options) {
|
|
839
|
+
const [start, end] = visibleTextRange(data, options?.omitTrailingQuoteUrl === true);
|
|
651
840
|
const entities = collectEntities(data).filter((entity) => validRange(entity.indices, start, end)).sort((left, right) => left.indices[0] - right.indices[0]);
|
|
652
841
|
let cursor = start;
|
|
653
842
|
let output = "";
|
|
@@ -655,10 +844,9 @@ function renderTweetText(data) {
|
|
|
655
844
|
const [entityStart, entityEnd] = entity.indices;
|
|
656
845
|
if (entityStart < cursor) continue;
|
|
657
846
|
output += escapeText(data.text.slice(cursor, entityStart));
|
|
658
|
-
if (entity.
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
output += `<a href="${escapeAttribute$2(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(label)}</a>`;
|
|
847
|
+
if (entity.href) {
|
|
848
|
+
const label = entity.label ?? data.text.slice(entityStart, entityEnd);
|
|
849
|
+
output += `<a href="${escapeAttribute$3(entity.href)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(label)}</a>`;
|
|
662
850
|
}
|
|
663
851
|
cursor = entityEnd;
|
|
664
852
|
}
|
|
@@ -666,27 +854,228 @@ function renderTweetText(data) {
|
|
|
666
854
|
return output.trim();
|
|
667
855
|
}
|
|
668
856
|
function collectEntities(data) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
kind: "url"
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
})
|
|
857
|
+
const collected = [];
|
|
858
|
+
for (const entity of data.entities?.urls ?? []) collected.push({
|
|
859
|
+
kind: "url",
|
|
860
|
+
indices: entity.indices,
|
|
861
|
+
href: entity.expanded_url ?? entity.url,
|
|
862
|
+
label: entity.display_url ?? entity.expanded_url ?? entity.url
|
|
863
|
+
});
|
|
864
|
+
for (const entity of data.entities?.media ?? []) collected.push({
|
|
865
|
+
kind: "media",
|
|
866
|
+
indices: entity.indices
|
|
867
|
+
});
|
|
868
|
+
for (const entity of data.entities?.hashtags ?? []) {
|
|
869
|
+
if (!entity.text) continue;
|
|
870
|
+
collected.push({
|
|
871
|
+
kind: "hashtag",
|
|
872
|
+
indices: entity.indices,
|
|
873
|
+
href: `https://x.com/hashtag/${encodeURIComponent(entity.text)}`
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
for (const entity of data.entities?.user_mentions ?? []) {
|
|
877
|
+
const screen = sanitizeScreenName(entity.screen_name);
|
|
878
|
+
if (!screen) continue;
|
|
879
|
+
collected.push({
|
|
880
|
+
kind: "mention",
|
|
881
|
+
indices: entity.indices,
|
|
882
|
+
href: `https://x.com/${encodeURIComponent(screen)}`
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
for (const entity of data.entities?.symbols ?? []) {
|
|
886
|
+
if (!entity.text) continue;
|
|
887
|
+
collected.push({
|
|
888
|
+
kind: "symbol",
|
|
889
|
+
indices: entity.indices,
|
|
890
|
+
href: `https://x.com/search?q=%24${encodeURIComponent(entity.text)}`
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return collected;
|
|
676
894
|
}
|
|
677
895
|
function validRange(indices, start, end) {
|
|
678
896
|
return Boolean(indices && indices[0] >= start && indices[1] <= end && indices[0] < indices[1]);
|
|
679
897
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
898
|
+
//#endregion
|
|
899
|
+
//#region src/plugins/twitter/full.ts
|
|
900
|
+
const HELP_HREF = "https://help.x.com/en/x-for-websites-ads-info-and-privacy";
|
|
901
|
+
function renderFullTweet(permalink, data, assets) {
|
|
902
|
+
const quote = data.quoted_tweet ? renderFullQuote(data.quoted_tweet, assets.quoted) : "";
|
|
903
|
+
return [
|
|
904
|
+
"<figure class=\"ox-tweet ox-tweet--fetched ox-tweet--full\">",
|
|
905
|
+
renderFullHeader(data.user, assets.avatar, permalink),
|
|
906
|
+
renderReply$1(data),
|
|
907
|
+
`<div class="ox-tweet__body">${renderTweetText(data, { omitTrailingQuoteUrl: Boolean(quote) })}</div>`,
|
|
908
|
+
renderMedia(assets, permalink),
|
|
909
|
+
quote,
|
|
910
|
+
renderInfo(permalink, data.created_at),
|
|
911
|
+
renderActions(permalink, data),
|
|
912
|
+
renderReplies(permalink, data.conversation_count),
|
|
913
|
+
"</figure>"
|
|
914
|
+
].join("");
|
|
915
|
+
}
|
|
916
|
+
function renderFullQuote(data, assets) {
|
|
917
|
+
const permalink = quotedPermalink(data) ?? "";
|
|
918
|
+
return [
|
|
919
|
+
"<blockquote class=\"ox-tweet__quote\">",
|
|
920
|
+
renderQuoteHeader(data.user, assets?.avatar, permalink),
|
|
921
|
+
`<div class="ox-tweet__quote-body">${renderTweetText(data)}</div>`,
|
|
922
|
+
renderMedia(assets ?? { media: [] }, permalink),
|
|
923
|
+
"</blockquote>"
|
|
924
|
+
].join("");
|
|
925
|
+
}
|
|
926
|
+
function renderFullHeader(user, avatarSrc, permalink) {
|
|
927
|
+
const profile = profileHref(user);
|
|
928
|
+
const follow = followHref(user);
|
|
929
|
+
return [
|
|
930
|
+
"<header class=\"ox-tweet__header\">",
|
|
931
|
+
`<a class="ox-tweet__avatar-link" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">`,
|
|
932
|
+
avatar(avatarSrc, 48),
|
|
933
|
+
"</a>",
|
|
934
|
+
"<div class=\"ox-tweet__author\">",
|
|
935
|
+
`<a class="ox-tweet__author-name" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">${escapeHtml$6(user.name)}${verifiedBadge(user)}</a>`,
|
|
936
|
+
"<div class=\"ox-tweet__author-meta\">",
|
|
937
|
+
`<a class="ox-tweet__author-handle" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">@${escapeHtml$6(user.screen_name)}</a>`,
|
|
938
|
+
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>` : "",
|
|
939
|
+
"</div></div>",
|
|
940
|
+
`<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>`,
|
|
941
|
+
"</header>"
|
|
942
|
+
].join("");
|
|
943
|
+
}
|
|
944
|
+
function renderQuoteHeader(user, avatarSrc, permalink) {
|
|
945
|
+
return [
|
|
946
|
+
"<header class=\"ox-tweet__quote-header\">",
|
|
947
|
+
`<a class="ox-tweet__profile" href="${escapeAttribute$3(permalink || profileHref(user))}" target="_blank" rel="noopener noreferrer">`,
|
|
948
|
+
avatar(avatarSrc, 20),
|
|
949
|
+
`<span class="ox-tweet__author-name">${escapeHtml$6(user.name)}${verifiedBadge(user)}</span>`,
|
|
950
|
+
`<span class="ox-tweet__author-handle">@${escapeHtml$6(user.screen_name)}</span>`,
|
|
951
|
+
"</a></header>"
|
|
952
|
+
].join("");
|
|
953
|
+
}
|
|
954
|
+
function renderReply$1(data) {
|
|
955
|
+
const href = replyPermalink(data);
|
|
956
|
+
const handle = data.in_reply_to_screen_name;
|
|
957
|
+
if (!href || !handle) return "";
|
|
958
|
+
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>`;
|
|
959
|
+
}
|
|
960
|
+
function renderInfo(permalink, createdAt) {
|
|
961
|
+
const formatted = formatFullDate(createdAt);
|
|
962
|
+
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>`;
|
|
963
|
+
}
|
|
964
|
+
function renderActions(permalink, data) {
|
|
965
|
+
const id = statusId(data, permalink);
|
|
966
|
+
if (!id) return "";
|
|
967
|
+
return [
|
|
968
|
+
"<div class=\"ox-tweet__actions\">",
|
|
969
|
+
`<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>`,
|
|
970
|
+
`<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>`,
|
|
971
|
+
"</div>"
|
|
972
|
+
].join("");
|
|
973
|
+
}
|
|
974
|
+
function renderReplies(permalink, conversationCount) {
|
|
975
|
+
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>`;
|
|
976
|
+
}
|
|
977
|
+
function avatar(src, size) {
|
|
978
|
+
return src ? `<img class="ox-tweet__avatar" src="${escapeAttribute$3(src)}" alt="" width="${size}" height="${size}" loading="lazy" decoding="async">` : "";
|
|
979
|
+
}
|
|
980
|
+
function verifiedBadge(user) {
|
|
981
|
+
const kind = verifiedKind(user);
|
|
982
|
+
return kind ? `<span class="ox-tweet__badge ox-tweet__badge--${kind}" title="Verified"></span>` : "";
|
|
983
|
+
}
|
|
984
|
+
function verifiedKind(user) {
|
|
985
|
+
if (user.verified_type === "Government") return "gray";
|
|
986
|
+
if (user.verified_type === "Business") return "gold";
|
|
987
|
+
if (user.is_blue_verified) return "blue";
|
|
988
|
+
if (user.verified) return "gray";
|
|
989
|
+
}
|
|
990
|
+
function profileHref(user) {
|
|
991
|
+
const screen = sanitizeScreenName(user.screen_name) ?? user.screen_name;
|
|
992
|
+
return `https://x.com/${encodeURIComponent(screen)}`;
|
|
993
|
+
}
|
|
994
|
+
function followHref(user) {
|
|
995
|
+
const screen = sanitizeScreenName(user.screen_name);
|
|
996
|
+
return screen ? `https://x.com/intent/follow?screen_name=${encodeURIComponent(screen)}` : void 0;
|
|
997
|
+
}
|
|
998
|
+
function statusId(data, permalink) {
|
|
999
|
+
return sanitizeStatusId(data.id_str) ?? permalink.match(/\/status\/(\d+)/)?.[1];
|
|
1000
|
+
}
|
|
1001
|
+
function formatCount(value) {
|
|
1002
|
+
const n = typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0;
|
|
1003
|
+
if (n > 999999) return `${(n / 1e6).toFixed(1)}M`;
|
|
1004
|
+
if (n > 999) return `${(n / 1e3).toFixed(1)}K`;
|
|
1005
|
+
return String(n);
|
|
1006
|
+
}
|
|
1007
|
+
function repliesLabel(value) {
|
|
1008
|
+
const n = typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1009
|
+
if (n === 0) return "Read more on X";
|
|
1010
|
+
if (n === 1) return "Read 1 reply";
|
|
1011
|
+
return `Read ${formatCount(n)} replies`;
|
|
1012
|
+
}
|
|
1013
|
+
function formatFullDate(createdAt) {
|
|
1014
|
+
if (!createdAt) return void 0;
|
|
1015
|
+
const date = new Date(createdAt);
|
|
1016
|
+
if (Number.isNaN(date.valueOf())) return void 0;
|
|
1017
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
1018
|
+
hour: "numeric",
|
|
1019
|
+
minute: "2-digit",
|
|
1020
|
+
hour12: true,
|
|
1021
|
+
month: "short",
|
|
1022
|
+
day: "numeric",
|
|
1023
|
+
year: "numeric",
|
|
1024
|
+
timeZone: "UTC"
|
|
1025
|
+
}).formatToParts(date);
|
|
1026
|
+
const get = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
1027
|
+
return {
|
|
1028
|
+
iso: date.toISOString(),
|
|
1029
|
+
label: `${get("hour")}:${get("minute")} ${get("dayPeriod")} · ${get("month")} ${get("day")}, ${get("year")}`
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region src/plugins/twitter/render.ts
|
|
1034
|
+
function renderFetchedTweet(permalink, data, assets, options) {
|
|
1035
|
+
if (options.appearance === "full") return renderFullTweet(permalink, data, assets);
|
|
1036
|
+
const quote = data.quoted_tweet ? renderQuotedTweet(data.quoted_tweet, assets.quoted) : "";
|
|
1037
|
+
return [
|
|
1038
|
+
"<figure class=\"ox-tweet ox-tweet--fetched\">",
|
|
1039
|
+
renderHeader(data.user, assets.avatar),
|
|
1040
|
+
renderReply(data),
|
|
1041
|
+
`<div class="ox-tweet__body">${renderTweetText(data, { omitTrailingQuoteUrl: Boolean(quote) })}</div>`,
|
|
1042
|
+
renderMedia(assets, permalink),
|
|
1043
|
+
quote,
|
|
1044
|
+
renderFooter(permalink, data.created_at, options.lang),
|
|
1045
|
+
"</figure>"
|
|
1046
|
+
].join("");
|
|
1047
|
+
}
|
|
1048
|
+
function renderQuotedTweet(data, assets) {
|
|
1049
|
+
const permalink = quotedPermalink(data) ?? "";
|
|
1050
|
+
return [
|
|
1051
|
+
"<blockquote class=\"ox-tweet__quote\">",
|
|
1052
|
+
renderHeader(data.user, assets?.avatar, permalink || void 0, "ox-tweet__quote-header"),
|
|
1053
|
+
`<div class="ox-tweet__quote-body">${renderTweetText(data)}</div>`,
|
|
1054
|
+
renderMedia(assets ?? { media: [] }, permalink),
|
|
1055
|
+
"</blockquote>"
|
|
1056
|
+
].join("");
|
|
1057
|
+
}
|
|
1058
|
+
function renderHeader(user, avatarSrc, href, headerClass = "ox-tweet__header") {
|
|
1059
|
+
const screen = sanitizeScreenName(user.screen_name) ?? user.screen_name;
|
|
1060
|
+
const profile = href ?? `https://x.com/${encodeURIComponent(screen)}`;
|
|
1061
|
+
const avatar = avatarSrc ? `<img class="ox-tweet__avatar" src="${escapeAttribute$3(avatarSrc)}" alt="" width="48" height="48" loading="lazy" decoding="async">` : "";
|
|
1062
|
+
return [
|
|
1063
|
+
`<header class="${headerClass}">`,
|
|
1064
|
+
`<a class="ox-tweet__profile" href="${escapeAttribute$3(profile)}" target="_blank" rel="noopener noreferrer">`,
|
|
1065
|
+
avatar,
|
|
1066
|
+
`<span class="ox-tweet__author-name">${escapeHtml$6(user.name)}</span>`,
|
|
1067
|
+
`<span class="ox-tweet__author-handle">@${escapeHtml$6(user.screen_name)}</span>`,
|
|
1068
|
+
"</a></header>"
|
|
1069
|
+
].join("");
|
|
1070
|
+
}
|
|
1071
|
+
function renderReply(data) {
|
|
1072
|
+
const href = replyPermalink(data);
|
|
1073
|
+
const handle = data.in_reply_to_screen_name;
|
|
1074
|
+
if (!href || !handle) return "";
|
|
1075
|
+
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>`;
|
|
687
1076
|
}
|
|
688
1077
|
function renderFooter(permalink, createdAt, lang) {
|
|
689
|
-
if (!createdAt) return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$
|
|
1078
|
+
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>`;
|
|
690
1079
|
const date = new Date(createdAt);
|
|
691
1080
|
if (Number.isNaN(date.valueOf())) return renderFooter(permalink, void 0, lang);
|
|
692
1081
|
const iso = date.toISOString();
|
|
@@ -702,16 +1091,7 @@ function renderFooter(permalink, createdAt, lang) {
|
|
|
702
1091
|
timeZone: "UTC"
|
|
703
1092
|
}).format(date);
|
|
704
1093
|
}
|
|
705
|
-
return `<footer class="ox-tweet__footer"><a class="ox-tweet__permalink" href="${escapeAttribute$
|
|
706
|
-
}
|
|
707
|
-
function escapeText(value) {
|
|
708
|
-
return escapeHtml$6(value).replaceAll("\n", "<br>");
|
|
709
|
-
}
|
|
710
|
-
function escapeAttribute$2(value) {
|
|
711
|
-
return escapeHtml$6(value).replaceAll("`", "`");
|
|
712
|
-
}
|
|
713
|
-
function escapeHtml$6(value) {
|
|
714
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1094
|
+
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>`;
|
|
715
1095
|
}
|
|
716
1096
|
//#endregion
|
|
717
1097
|
//#region src/plugins/twitter/transform.ts
|
|
@@ -724,7 +1104,10 @@ function resolveTwitterEmbedOptions$1(options) {
|
|
|
724
1104
|
cache: options.cache ?? true,
|
|
725
1105
|
cacheDir: path.resolve(options.cacheDir ?? ".cache/ox-content/twitter"),
|
|
726
1106
|
mediaOutputDir: path.resolve(options.mediaOutputDir ?? "public/ox-content/twitter"),
|
|
727
|
-
mediaPublicPath: options.mediaPublicPath ?? "/ox-content/twitter"
|
|
1107
|
+
mediaPublicPath: options.mediaPublicPath ?? "/ox-content/twitter",
|
|
1108
|
+
downloadVideo: options.downloadVideo ?? false,
|
|
1109
|
+
maxVideoBytes: options.maxVideoBytes ?? 8388608,
|
|
1110
|
+
appearance: options.appearance === "full" ? "full" : "compact"
|
|
728
1111
|
};
|
|
729
1112
|
}
|
|
730
1113
|
async function transformFetchedTweets(html, options) {
|
|
@@ -735,20 +1118,23 @@ async function transformFetchedTweets(html, options) {
|
|
|
735
1118
|
for (const match of html.matchAll(TWEET_ELEMENT)) {
|
|
736
1119
|
const index = match.index ?? 0;
|
|
737
1120
|
output += html.slice(cursor, index);
|
|
738
|
-
const
|
|
739
|
-
if (!reference) {
|
|
1121
|
+
const attrs = tweetElementAttributes(match[2]);
|
|
1122
|
+
if (!attrs.reference) {
|
|
740
1123
|
output += match[0];
|
|
741
1124
|
cursor = index + match[0].length;
|
|
742
1125
|
continue;
|
|
743
1126
|
}
|
|
744
|
-
const data = await fetchTweetData(reference.id, resolved);
|
|
1127
|
+
const data = await fetchTweetData(attrs.reference.id, resolved);
|
|
745
1128
|
if (!data) {
|
|
746
1129
|
output += match[0];
|
|
747
1130
|
cursor = index + match[0].length;
|
|
748
1131
|
continue;
|
|
749
1132
|
}
|
|
750
|
-
const assets = await materializeTweetAssets(reference.id, data, resolved);
|
|
751
|
-
output += renderFetchedTweet(reference.url, data, assets,
|
|
1133
|
+
const assets = await materializeTweetAssets(attrs.reference.id, data, resolved);
|
|
1134
|
+
output += renderFetchedTweet(attrs.reference.url, data, assets, {
|
|
1135
|
+
...resolved,
|
|
1136
|
+
appearance: attrs.appearance ?? resolved.appearance
|
|
1137
|
+
});
|
|
752
1138
|
cursor = index + match[0].length;
|
|
753
1139
|
}
|
|
754
1140
|
return output + html.slice(cursor);
|
|
@@ -891,7 +1277,7 @@ function inferLanguage(path) {
|
|
|
891
1277
|
}
|
|
892
1278
|
//#endregion
|
|
893
1279
|
//#region src/plugins/github/types.ts
|
|
894
|
-
const defaultOptions
|
|
1280
|
+
const defaultOptions = {
|
|
895
1281
|
token: "",
|
|
896
1282
|
cache: true,
|
|
897
1283
|
cacheTTL: 36e5,
|
|
@@ -1001,7 +1387,7 @@ async function fetchGitHubSource(source, options) {
|
|
|
1001
1387
|
*/
|
|
1002
1388
|
async function prefetchGitHubRepos(repos, options) {
|
|
1003
1389
|
const mergedOptions = {
|
|
1004
|
-
...defaultOptions
|
|
1390
|
+
...defaultOptions,
|
|
1005
1391
|
...options
|
|
1006
1392
|
};
|
|
1007
1393
|
const results = /* @__PURE__ */ new Map();
|
|
@@ -1016,7 +1402,7 @@ async function prefetchGitHubRepos(repos, options) {
|
|
|
1016
1402
|
*/
|
|
1017
1403
|
async function prefetchGitHubSources(sources, options) {
|
|
1018
1404
|
const mergedOptions = {
|
|
1019
|
-
...defaultOptions
|
|
1405
|
+
...defaultOptions,
|
|
1020
1406
|
...options
|
|
1021
1407
|
};
|
|
1022
1408
|
const results = /* @__PURE__ */ new Map();
|
|
@@ -1433,7 +1819,7 @@ function rehypeGitHub(repoDataMap, sourceDataMap, options) {
|
|
|
1433
1819
|
*/
|
|
1434
1820
|
async function transformGitHub(html, repoDataMap, options) {
|
|
1435
1821
|
const mergedOptions = {
|
|
1436
|
-
...defaultOptions
|
|
1822
|
+
...defaultOptions,
|
|
1437
1823
|
...options
|
|
1438
1824
|
};
|
|
1439
1825
|
let dataMap = repoDataMap;
|
|
@@ -1446,29 +1832,111 @@ async function transformGitHub(html, repoDataMap, options) {
|
|
|
1446
1832
|
//#region src/plugins/github.ts
|
|
1447
1833
|
var github_exports = /* @__PURE__ */ __exportAll({ transformGitHub: () => transformGitHub });
|
|
1448
1834
|
//#endregion
|
|
1449
|
-
//#region src/plugins/ogp.ts
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1835
|
+
//#region src/plugins/ogp/cache.ts
|
|
1836
|
+
const memoryCache = /* @__PURE__ */ new Map();
|
|
1837
|
+
function ogpCacheFilePath(directory, key) {
|
|
1838
|
+
return path.join(directory, `${key}.json`);
|
|
1839
|
+
}
|
|
1840
|
+
function isFreshOgpEntry(cachedAt, ttl, now) {
|
|
1841
|
+
return now - cachedAt < ttl;
|
|
1842
|
+
}
|
|
1843
|
+
function parseOgpCacheEntry(value) {
|
|
1844
|
+
if (!value || typeof value !== "object") return null;
|
|
1845
|
+
const entry = value;
|
|
1846
|
+
if (entry.v !== 1) return null;
|
|
1847
|
+
if (typeof entry.url !== "string" || entry.url.length === 0) return null;
|
|
1848
|
+
if (typeof entry.cachedAt !== "number" || !Number.isFinite(entry.cachedAt)) return null;
|
|
1849
|
+
if (entry.data !== null && !isOgpData(entry.data)) return null;
|
|
1850
|
+
return {
|
|
1851
|
+
v: 1,
|
|
1852
|
+
url: entry.url,
|
|
1853
|
+
cachedAt: entry.cachedAt,
|
|
1854
|
+
data: entry.data
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
function readMemoryOgp(key, options, now) {
|
|
1858
|
+
const cached = memoryCache.get(key);
|
|
1859
|
+
if (!cached || !isFreshOgpEntry(cached.timestamp, options.cacheTTL, now)) return;
|
|
1860
|
+
return cached.data;
|
|
1861
|
+
}
|
|
1862
|
+
function writeMemoryOgp(key, data, timestamp, options) {
|
|
1863
|
+
if (data === null && !options.persistCache) return;
|
|
1864
|
+
memoryCache.set(key, {
|
|
1865
|
+
data,
|
|
1866
|
+
timestamp
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
async function readDiskOgp(key, options, now) {
|
|
1870
|
+
const file = ogpCacheFilePath(options.cacheDir, key);
|
|
1871
|
+
try {
|
|
1872
|
+
const entry = parseOgpCacheEntry(JSON.parse(await readFile(file, "utf8")));
|
|
1873
|
+
if (!entry) {
|
|
1874
|
+
await discardCorruptEntry(file);
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
if (!isFreshOgpEntry(entry.cachedAt, options.cacheTTL, now)) return void 0;
|
|
1878
|
+
return entry.data;
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
if (isEnoent(error)) return void 0;
|
|
1881
|
+
await discardCorruptEntry(file);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
async function writeDiskOgp(key, url, data, options, cachedAt) {
|
|
1886
|
+
const directory = options.cacheDir;
|
|
1887
|
+
const target = ogpCacheFilePath(directory, key);
|
|
1888
|
+
const temp = path.join(directory, `.${key}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
1889
|
+
const entry = {
|
|
1890
|
+
v: 1,
|
|
1891
|
+
url,
|
|
1892
|
+
cachedAt,
|
|
1893
|
+
data
|
|
1894
|
+
};
|
|
1895
|
+
try {
|
|
1896
|
+
await mkdir(directory, { recursive: true });
|
|
1897
|
+
await writeFile(temp, `${JSON.stringify(entry)}\n`);
|
|
1898
|
+
try {
|
|
1899
|
+
await rename(temp, target);
|
|
1900
|
+
} catch {
|
|
1901
|
+
await writeFile(target, `${JSON.stringify(entry)}\n`);
|
|
1902
|
+
await rm(temp, { force: true });
|
|
1903
|
+
}
|
|
1904
|
+
} catch {
|
|
1905
|
+
await rm(temp, { force: true }).catch(() => void 0);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
function isOgpData(value) {
|
|
1909
|
+
if (!value || typeof value !== "object") return false;
|
|
1910
|
+
const data = value;
|
|
1911
|
+
if (typeof data.url !== "string" || typeof data.title !== "string") return false;
|
|
1912
|
+
for (const field of [
|
|
1913
|
+
"description",
|
|
1914
|
+
"image",
|
|
1915
|
+
"siteName",
|
|
1916
|
+
"favicon"
|
|
1917
|
+
]) if (data[field] !== void 0 && typeof data[field] !== "string") return false;
|
|
1918
|
+
return true;
|
|
1919
|
+
}
|
|
1920
|
+
async function discardCorruptEntry(file) {
|
|
1921
|
+
console.warn(`Ignoring corrupt Open Graph cache entry ${file}`);
|
|
1922
|
+
await rm(file, { force: true }).catch(() => void 0);
|
|
1923
|
+
}
|
|
1924
|
+
function isEnoent(error) {
|
|
1925
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
1926
|
+
}
|
|
1927
|
+
function resolveOgpOptions(options = {}) {
|
|
1928
|
+
return {
|
|
1929
|
+
timeout: options.timeout ?? 1e4,
|
|
1930
|
+
cache: options.cache ?? true,
|
|
1931
|
+
cacheTTL: options.cacheTTL ?? 36e5,
|
|
1932
|
+
persistCache: options.persistCache ?? false,
|
|
1933
|
+
cacheDir: path.resolve(options.cacheDir ?? ".cache/ox-content/ogp"),
|
|
1934
|
+
refresh: options.refresh ?? false,
|
|
1935
|
+
userAgent: options.userAgent ?? "ox-content-ogp-bot/1.0 (compatible; +https://github.com/ubugeeei-prod/ox-content)"
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region src/plugins/ogp/url.ts
|
|
1472
1940
|
function isPrivateIPv4(hostname) {
|
|
1473
1941
|
const parts = hostname.split(".").map(Number);
|
|
1474
1942
|
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
@@ -1488,17 +1956,6 @@ function isSafeOgpUrl(value) {
|
|
|
1488
1956
|
return false;
|
|
1489
1957
|
}
|
|
1490
1958
|
}
|
|
1491
|
-
/**
|
|
1492
|
-
* Get element attribute value.
|
|
1493
|
-
*/
|
|
1494
|
-
function getAttribute$1(el, name) {
|
|
1495
|
-
const value = el.properties?.[name];
|
|
1496
|
-
if (typeof value === "string") return value;
|
|
1497
|
-
if (Array.isArray(value)) return value.join(" ");
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Extract domain from URL.
|
|
1501
|
-
*/
|
|
1502
1959
|
function extractDomain(url) {
|
|
1503
1960
|
try {
|
|
1504
1961
|
return new URL(url).hostname;
|
|
@@ -1506,9 +1963,6 @@ function extractDomain(url) {
|
|
|
1506
1963
|
return url;
|
|
1507
1964
|
}
|
|
1508
1965
|
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Get favicon URL for a domain.
|
|
1511
|
-
*/
|
|
1512
1966
|
function getFaviconUrl(url) {
|
|
1513
1967
|
try {
|
|
1514
1968
|
return `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`;
|
|
@@ -1517,40 +1971,60 @@ function getFaviconUrl(url) {
|
|
|
1517
1971
|
}
|
|
1518
1972
|
}
|
|
1519
1973
|
/**
|
|
1520
|
-
*
|
|
1974
|
+
* Normalize a URL for cache keys: lowercase host, drop default ports and
|
|
1975
|
+
* fragments, and strip a trailing slash that is not the root path.
|
|
1521
1976
|
*/
|
|
1522
|
-
function
|
|
1523
|
-
|
|
1524
|
-
url
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
if (imageMatch) {
|
|
1533
|
-
let imageUrl = imageMatch[1];
|
|
1534
|
-
if (imageUrl.startsWith("/")) try {
|
|
1535
|
-
const urlObj = new URL(url);
|
|
1536
|
-
imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;
|
|
1537
|
-
} catch {}
|
|
1538
|
-
result.image = imageUrl;
|
|
1977
|
+
function normalizeOgpUrl(url) {
|
|
1978
|
+
try {
|
|
1979
|
+
const parsed = new URL(url);
|
|
1980
|
+
parsed.hash = "";
|
|
1981
|
+
parsed.hostname = parsed.hostname.toLowerCase();
|
|
1982
|
+
if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") parsed.port = "";
|
|
1983
|
+
if (parsed.pathname.length > 1 && parsed.pathname.endsWith("/")) parsed.pathname = parsed.pathname.slice(0, -1);
|
|
1984
|
+
return parsed.href;
|
|
1985
|
+
} catch {
|
|
1986
|
+
return url;
|
|
1539
1987
|
}
|
|
1540
|
-
const siteNameMatch = html.match(/<meta[^>]*property=["']og:site_name["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:site_name["']/i);
|
|
1541
|
-
if (siteNameMatch) result.siteName = siteNameMatch[1];
|
|
1542
|
-
result.favicon = getFaviconUrl(url);
|
|
1543
|
-
return result;
|
|
1544
1988
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1989
|
+
function ogpCacheKey(url) {
|
|
1990
|
+
return createHash("sha256").update(normalizeOgpUrl(url)).digest("hex");
|
|
1991
|
+
}
|
|
1992
|
+
//#endregion
|
|
1993
|
+
//#region src/plugins/ogp/fetch.ts
|
|
1994
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1995
|
+
async function fetchOgpData(url, options = {}) {
|
|
1549
1996
|
if (!isSafeOgpUrl(url)) return null;
|
|
1997
|
+
const resolved = resolveOgpOptions(options);
|
|
1998
|
+
const key = ogpCacheKey(url);
|
|
1999
|
+
const pending = inflight.get(key);
|
|
2000
|
+
if (pending) return pending;
|
|
2001
|
+
const request = loadOgpData(url, key, resolved).finally(() => {
|
|
2002
|
+
if (inflight.get(key) === request) inflight.delete(key);
|
|
2003
|
+
});
|
|
2004
|
+
inflight.set(key, request);
|
|
2005
|
+
return request;
|
|
2006
|
+
}
|
|
2007
|
+
async function loadOgpData(url, key, options) {
|
|
2008
|
+
const now = Date.now();
|
|
2009
|
+
if (options.cache && !options.refresh) {
|
|
2010
|
+
const memory = readMemoryOgp(key, options, now);
|
|
2011
|
+
if (memory !== void 0) return memory;
|
|
2012
|
+
if (options.persistCache) {
|
|
2013
|
+
const disk = await readDiskOgp(key, options, now);
|
|
2014
|
+
if (disk !== void 0) {
|
|
2015
|
+
writeMemoryOgp(key, disk, now, options);
|
|
2016
|
+
return disk;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const data = await requestOgpData(url, options);
|
|
1550
2021
|
if (options.cache) {
|
|
1551
|
-
|
|
1552
|
-
if (
|
|
2022
|
+
writeMemoryOgp(key, data, now, options);
|
|
2023
|
+
if (options.persistCache) await writeDiskOgp(key, normalizeOgpUrl(url), data, options, now);
|
|
1553
2024
|
}
|
|
2025
|
+
return data;
|
|
2026
|
+
}
|
|
2027
|
+
async function requestOgpData(url, options) {
|
|
1554
2028
|
try {
|
|
1555
2029
|
const controller = new AbortController();
|
|
1556
2030
|
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
@@ -1566,21 +2040,38 @@ async function fetchOgpData(url, options) {
|
|
|
1566
2040
|
console.warn(`Failed to fetch OGP for ${url}: ${response.status}`);
|
|
1567
2041
|
return null;
|
|
1568
2042
|
}
|
|
1569
|
-
|
|
1570
|
-
if (options.cache) ogpCache.set(url, {
|
|
1571
|
-
data,
|
|
1572
|
-
timestamp: Date.now()
|
|
1573
|
-
});
|
|
1574
|
-
return data;
|
|
2043
|
+
return parseOgpFromHtml(await response.text(), url);
|
|
1575
2044
|
} catch (error) {
|
|
1576
2045
|
if (error instanceof Error && error.name === "AbortError") console.warn(`Timeout fetching OGP for ${url}`);
|
|
1577
2046
|
else console.warn(`Error fetching OGP for ${url}:`, error);
|
|
1578
2047
|
return null;
|
|
1579
2048
|
}
|
|
1580
2049
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2050
|
+
function parseOgpFromHtml(html, url) {
|
|
2051
|
+
const result = {
|
|
2052
|
+
url,
|
|
2053
|
+
title: ""
|
|
2054
|
+
};
|
|
2055
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
2056
|
+
result.title = (html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:title["']/i))?.[1] || titleMatch?.[1] || extractDomain(url);
|
|
2057
|
+
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);
|
|
2058
|
+
if (descMatch) result.description = descMatch[1];
|
|
2059
|
+
const imageMatch = html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:image["']/i);
|
|
2060
|
+
if (imageMatch) {
|
|
2061
|
+
let imageUrl = imageMatch[1];
|
|
2062
|
+
if (imageUrl.startsWith("/")) try {
|
|
2063
|
+
const urlObj = new URL(url);
|
|
2064
|
+
imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;
|
|
2065
|
+
} catch {}
|
|
2066
|
+
result.image = imageUrl;
|
|
2067
|
+
}
|
|
2068
|
+
const siteNameMatch = html.match(/<meta[^>]*property=["']og:site_name["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:site_name["']/i);
|
|
2069
|
+
if (siteNameMatch) result.siteName = siteNameMatch[1];
|
|
2070
|
+
result.favicon = getFaviconUrl(url);
|
|
2071
|
+
return result;
|
|
2072
|
+
}
|
|
2073
|
+
//#endregion
|
|
2074
|
+
//#region src/plugins/ogp/render.ts
|
|
1584
2075
|
function createOgpCard(data) {
|
|
1585
2076
|
const children = [];
|
|
1586
2077
|
const contentChildren = [];
|
|
@@ -1658,9 +2149,6 @@ function createOgpCard(data) {
|
|
|
1658
2149
|
children
|
|
1659
2150
|
};
|
|
1660
2151
|
}
|
|
1661
|
-
/**
|
|
1662
|
-
* Create fallback element when OGP data is unavailable.
|
|
1663
|
-
*/
|
|
1664
2152
|
function createFallbackCard(url) {
|
|
1665
2153
|
return {
|
|
1666
2154
|
type: "element",
|
|
@@ -1692,9 +2180,15 @@ function createFallbackCard(url) {
|
|
|
1692
2180
|
}]
|
|
1693
2181
|
};
|
|
1694
2182
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2183
|
+
//#endregion
|
|
2184
|
+
//#region src/plugins/ogp/transform.ts
|
|
2185
|
+
const rehypeParse$1 = interopDefault(rehypeParsePlugin);
|
|
2186
|
+
const rehypeStringify$1 = interopDefault(rehypeStringifyPlugin);
|
|
2187
|
+
function getAttribute$1(el, name) {
|
|
2188
|
+
const value = el.properties?.[name];
|
|
2189
|
+
if (typeof value === "string") return value;
|
|
2190
|
+
if (Array.isArray(value)) return value.join(" ");
|
|
2191
|
+
}
|
|
1698
2192
|
async function collectOgpUrls(html) {
|
|
1699
2193
|
const urls = [];
|
|
1700
2194
|
const urlPattern = /<ogcard[^>]*\s+url=["']([^"']+)["']/gi;
|
|
@@ -1702,24 +2196,13 @@ async function collectOgpUrls(html) {
|
|
|
1702
2196
|
while ((match = urlPattern.exec(html)) !== null) if (isSafeOgpUrl(match[1])) urls.push(match[1]);
|
|
1703
2197
|
return urls;
|
|
1704
2198
|
}
|
|
1705
|
-
/**
|
|
1706
|
-
* Pre-fetch all OGP data.
|
|
1707
|
-
*/
|
|
1708
2199
|
async function prefetchOgpData(urls, options) {
|
|
1709
|
-
const mergedOptions = {
|
|
1710
|
-
...defaultOptions,
|
|
1711
|
-
...options
|
|
1712
|
-
};
|
|
1713
2200
|
const results = /* @__PURE__ */ new Map();
|
|
1714
2201
|
await Promise.all(urls.map(async (url) => {
|
|
1715
|
-
|
|
1716
|
-
results.set(url, data);
|
|
2202
|
+
results.set(url, await fetchOgpData(url, options));
|
|
1717
2203
|
}));
|
|
1718
2204
|
return results;
|
|
1719
2205
|
}
|
|
1720
|
-
/**
|
|
1721
|
-
* Rehype plugin to transform OgCard components.
|
|
1722
|
-
*/
|
|
1723
2206
|
function rehypeOgp(ogpDataMap) {
|
|
1724
2207
|
return (tree) => {
|
|
1725
2208
|
const visit = (node) => {
|
|
@@ -1730,8 +2213,7 @@ function rehypeOgp(ogpDataMap) {
|
|
|
1730
2213
|
const url = getAttribute$1(child, "url");
|
|
1731
2214
|
if (url) {
|
|
1732
2215
|
const ogpData = ogpDataMap.get(url);
|
|
1733
|
-
|
|
1734
|
-
node.children[i] = cardElement;
|
|
2216
|
+
node.children[i] = ogpData ? createOgpCard(ogpData) : createFallbackCard(url);
|
|
1735
2217
|
}
|
|
1736
2218
|
} else visit(child);
|
|
1737
2219
|
}
|
|
@@ -1740,9 +2222,6 @@ function rehypeOgp(ogpDataMap) {
|
|
|
1740
2222
|
visit(tree);
|
|
1741
2223
|
};
|
|
1742
2224
|
}
|
|
1743
|
-
/**
|
|
1744
|
-
* Transform OgCard components in HTML.
|
|
1745
|
-
*/
|
|
1746
2225
|
async function transformOgp(html, ogpDataMap, options) {
|
|
1747
2226
|
let dataMap = ogpDataMap;
|
|
1748
2227
|
if (!dataMap) dataMap = await prefetchOgpData(await collectOgpUrls(html), options);
|
|
@@ -1750,6 +2229,9 @@ async function transformOgp(html, ogpDataMap, options) {
|
|
|
1750
2229
|
return String(result);
|
|
1751
2230
|
}
|
|
1752
2231
|
//#endregion
|
|
2232
|
+
//#region src/plugins/ogp.ts
|
|
2233
|
+
var ogp_exports = /* @__PURE__ */ __exportAll({ transformOgp: () => transformOgp });
|
|
2234
|
+
//#endregion
|
|
1753
2235
|
//#region src/plugins/index.ts
|
|
1754
2236
|
const SELF_CLOSING_EMBED_TAG = /<(GitHub|OgCard|Tweet|XPost|Bluesky|Spotify|StackBlitz|WebContainer|YouTube)((?:[^>"']|"[^"]*"|'[^']*')*?)\s*\/>/gi;
|
|
1755
2237
|
/**
|
|
@@ -2424,6 +2906,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
2424
2906
|
gfm: options.gfm,
|
|
2425
2907
|
mdx: resolveMdxForFilePath(filePath, options.mdx),
|
|
2426
2908
|
footnotes: options.footnotes,
|
|
2909
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
2427
2910
|
taskLists: options.taskLists,
|
|
2428
2911
|
tables: options.tables,
|
|
2429
2912
|
strikethrough: options.strikethrough,
|
|
@@ -2431,6 +2914,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
2431
2914
|
autolinkUrls: options.autolinks,
|
|
2432
2915
|
frontmatter: options.frontmatter,
|
|
2433
2916
|
tocMaxDepth: options.tocMaxDepth,
|
|
2917
|
+
headingPermalinks: options.headingPermalinks?.enabled ?? false,
|
|
2434
2918
|
convertMdLinks: ssgOptions?.convertMdLinks,
|
|
2435
2919
|
baseUrl: ssgOptions?.baseUrl,
|
|
2436
2920
|
sourcePath: ssgOptions?.sourcePath ?? filePath,
|
|
@@ -2448,6 +2932,13 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
2448
2932
|
} : void 0,
|
|
2449
2933
|
attributes: options.attrs?.enabled ? { enabled: true } : void 0,
|
|
2450
2934
|
badges: options.badges?.enabled ? { enabled: true } : void 0,
|
|
2935
|
+
magicLinks: options.magicLinks?.enabled ? {
|
|
2936
|
+
enabled: true,
|
|
2937
|
+
aliases: options.magicLinks.aliases,
|
|
2938
|
+
favicon: options.magicLinks.favicon,
|
|
2939
|
+
faviconTemplate: options.magicLinks.faviconTemplate,
|
|
2940
|
+
imageOverrides: options.magicLinks.imageOverrides
|
|
2941
|
+
} : void 0,
|
|
2451
2942
|
containers: options.containers?.enabled ? {
|
|
2452
2943
|
enabled: true,
|
|
2453
2944
|
types: options.containers.types
|
|
@@ -3931,6 +4422,108 @@ initIslands((el, props) => {
|
|
|
3931
4422
|
`;
|
|
3932
4423
|
}
|
|
3933
4424
|
//#endregion
|
|
4425
|
+
//#region src/versions-html.ts
|
|
4426
|
+
function versionSwitcherMarkup(links, badge) {
|
|
4427
|
+
if (links.length === 0) return "";
|
|
4428
|
+
const current = links.find((link) => link.current) ?? links[0];
|
|
4429
|
+
const items = links.map((link) => {
|
|
4430
|
+
const label = `${escapeHtml$4(link.label)}${badgeMarkup(link, badge)}`;
|
|
4431
|
+
if (link.current || !isSafeHref(link.href)) return `<li><span aria-current="page">${label}</span></li>`;
|
|
4432
|
+
return `<li><a href="${escapeHtml$4(link.href)}">${label}</a></li>`;
|
|
4433
|
+
}).join("");
|
|
4434
|
+
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>`;
|
|
4435
|
+
}
|
|
4436
|
+
function versionBannerMarkup(kind) {
|
|
4437
|
+
if (kind === "unreleased") return `<aside class="ox-version-banner ox-version-banner--unreleased" role="status">This documentation describes an unreleased version.</aside>`;
|
|
4438
|
+
if (kind === "unmaintained") return `<aside class="ox-version-banner ox-version-banner--unmaintained" role="status">This documentation is unmaintained.</aside>`;
|
|
4439
|
+
return "";
|
|
4440
|
+
}
|
|
4441
|
+
function injectVersionChrome(html, switcher, banner, searchFrom, searchTo) {
|
|
4442
|
+
let next = html;
|
|
4443
|
+
if (banner) next = next.replace(/<body([^>]*)>/, `<body$1>${banner}`);
|
|
4444
|
+
if (switcher) {
|
|
4445
|
+
if (next.includes("<div class=\"header-actions\">")) next = next.replace("<div class=\"header-actions\">", `<div class="header-actions">${switcher}`);
|
|
4446
|
+
else if (next.includes("</header>")) next = next.replace("</header>", `${switcher}</header>`);
|
|
4447
|
+
}
|
|
4448
|
+
if (searchTo && isSafeHref(searchTo)) next = next.replace(/<html([^>]*)>/i, (match, attrs) => {
|
|
4449
|
+
if (/\sdata-ox-search-index=/.test(attrs)) return match;
|
|
4450
|
+
return `<html${attrs} data-ox-search-index="${escapeHtml$4(searchTo)}">`;
|
|
4451
|
+
});
|
|
4452
|
+
if (searchFrom && searchTo && searchFrom !== searchTo && isSafeHref(searchTo)) {
|
|
4453
|
+
next = next.split(searchFrom).join(searchTo);
|
|
4454
|
+
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>`;
|
|
4455
|
+
next = next.includes("</body>") ? next.replace("</body>", `${script}</body>`) : `${next}${script}`;
|
|
4456
|
+
}
|
|
4457
|
+
return next;
|
|
4458
|
+
}
|
|
4459
|
+
function searchIndexUrl(base, prefix) {
|
|
4460
|
+
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
4461
|
+
return prefix ? `${root}${prefix}/search-index.json` : `${root}search-index.json`;
|
|
4462
|
+
}
|
|
4463
|
+
function isSafeHref(href) {
|
|
4464
|
+
const trimmed = href.trim();
|
|
4465
|
+
if (!trimmed || trimmed.startsWith("//")) return false;
|
|
4466
|
+
const lower = trimmed.replace(/\s+/g, "").toLowerCase();
|
|
4467
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) return false;
|
|
4468
|
+
return trimmed.startsWith("/") || trimmed.startsWith("./") || !trimmed.includes(":");
|
|
4469
|
+
}
|
|
4470
|
+
function escapeHtml$4(value) {
|
|
4471
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
4472
|
+
}
|
|
4473
|
+
function badgeMarkup(link, badge) {
|
|
4474
|
+
if (!badge || !link.banner) return "";
|
|
4475
|
+
return `<span class="ox-version-badge">${link.banner === "unreleased" ? "unreleased" : "unmaintained"}</span>`;
|
|
4476
|
+
}
|
|
4477
|
+
//#endregion
|
|
4478
|
+
//#region src/search-filters.ts
|
|
4479
|
+
/**
|
|
4480
|
+
* Language and version filters for the default search dialog.
|
|
4481
|
+
*/
|
|
4482
|
+
const RESULTS_MARKUP = "<div class=\"search-results\"></div>";
|
|
4483
|
+
function injectSearchLocaleFilters(html, input) {
|
|
4484
|
+
const locales = input.locales.filter((locale) => locale.code.trim() && locale.name.trim());
|
|
4485
|
+
if (locales.length < 2) return html;
|
|
4486
|
+
const next = ensureSearchFilters(html);
|
|
4487
|
+
const select = selectMarkup(next, "locale");
|
|
4488
|
+
if (!select) return next;
|
|
4489
|
+
const defaultLocale = input.defaultLocale.trim() || locales[0].code;
|
|
4490
|
+
const selected = locales.some((locale) => locale.code === input.current) ? input.current : defaultLocale;
|
|
4491
|
+
const options = [`<option value="">All languages</option>`, ...locales.map((locale) => {
|
|
4492
|
+
return `<option value="${escapeHtml$4(locale.code)}"${locale.code === selected ? " selected" : ""}>${escapeHtml$4(locale.name)}</option>`;
|
|
4493
|
+
})].join("");
|
|
4494
|
+
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");
|
|
4495
|
+
}
|
|
4496
|
+
function injectSearchVersionFilters(html, versions) {
|
|
4497
|
+
const safe = versions.filter((version) => version.id.trim() && version.label.trim() && isSafeHref(version.indexUrl));
|
|
4498
|
+
if (safe.length < 2) return html;
|
|
4499
|
+
const next = ensureSearchFilters(html);
|
|
4500
|
+
const select = selectMarkup(next, "version");
|
|
4501
|
+
if (!select) return next;
|
|
4502
|
+
const current = safe.find((version) => version.current) ?? safe[0];
|
|
4503
|
+
const options = safe.map((version) => {
|
|
4504
|
+
const selected = version.id === current.id ? " selected" : "";
|
|
4505
|
+
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>`;
|
|
4506
|
+
}).join("");
|
|
4507
|
+
return revealFilter(next.replace(select.markup, `<select class="search-filter-select" data-search-filter="version" aria-label="Version">${options}</select>`), "version");
|
|
4508
|
+
}
|
|
4509
|
+
function ensureSearchFilters(html) {
|
|
4510
|
+
if (html.includes("class=\"search-filters\"") || !html.includes(RESULTS_MARKUP)) return html;
|
|
4511
|
+
return html.replace(RESULTS_MARKUP, `${searchFiltersMarkup()}${RESULTS_MARKUP}`);
|
|
4512
|
+
}
|
|
4513
|
+
function searchFiltersMarkup() {
|
|
4514
|
+
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>`;
|
|
4515
|
+
}
|
|
4516
|
+
function searchFiltersStyle() {
|
|
4517
|
+
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>`;
|
|
4518
|
+
}
|
|
4519
|
+
function selectMarkup(html, kind) {
|
|
4520
|
+
const match = html.match(new RegExp(`<select class="search-filter-select" data-search-filter="${kind}"[^>]*>[\\s\\S]*?<\\/select>`));
|
|
4521
|
+
return match?.[0] ? { markup: match[0] } : void 0;
|
|
4522
|
+
}
|
|
4523
|
+
function revealFilter(html, kind) {
|
|
4524
|
+
return html.replace(`data-search-filter-label="${kind}" hidden`, `data-search-filter-label="${kind}"`);
|
|
4525
|
+
}
|
|
4526
|
+
//#endregion
|
|
3934
4527
|
//#region src/locale-switcher.ts
|
|
3935
4528
|
/**
|
|
3936
4529
|
* Resolves `ssg.localeSwitcher`. Omitted / `false` stay off. `true` or an
|
|
@@ -4363,6 +4956,7 @@ function renderPage(page, options) {
|
|
|
4363
4956
|
contributors: page.contributors,
|
|
4364
4957
|
path: page.path,
|
|
4365
4958
|
url: page.url,
|
|
4959
|
+
markdownSource: page.markdownSource,
|
|
4366
4960
|
frontmatter: page.frontmatter,
|
|
4367
4961
|
layout: page.layout
|
|
4368
4962
|
},
|
|
@@ -4379,6 +4973,7 @@ function renderPage(page, options) {
|
|
|
4379
4973
|
contributors: p.contributors,
|
|
4380
4974
|
path: p.path,
|
|
4381
4975
|
url: p.url,
|
|
4976
|
+
markdownSource: p.markdownSource,
|
|
4382
4977
|
frontmatter: p.frontmatter,
|
|
4383
4978
|
layout: p.layout
|
|
4384
4979
|
}))
|
|
@@ -4437,8 +5032,8 @@ function DefaultTheme({ children }) {
|
|
|
4437
5032
|
<head>
|
|
4438
5033
|
<meta charset="UTF-8">
|
|
4439
5034
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4440
|
-
<title>${escapeHtml$
|
|
4441
|
-
${page.description ? `<meta name="description" content="${escapeHtml$
|
|
5035
|
+
<title>${escapeHtml$3(page.title)} - ${escapeHtml$3(site.name)}</title>
|
|
5036
|
+
${page.description ? `<meta name="description" content="${escapeHtml$3(page.description)}">` : ""}
|
|
4442
5037
|
<style>
|
|
4443
5038
|
:root {
|
|
4444
5039
|
--octc-color-primary: #4f6fae;
|
|
@@ -4462,7 +5057,7 @@ function DefaultTheme({ children }) {
|
|
|
4462
5057
|
</head>
|
|
4463
5058
|
<body>
|
|
4464
5059
|
<header>
|
|
4465
|
-
<h1>${escapeHtml$
|
|
5060
|
+
<h1>${escapeHtml$3(site.name)}</h1>
|
|
4466
5061
|
</header>
|
|
4467
5062
|
<main>
|
|
4468
5063
|
${children.__html}
|
|
@@ -4470,7 +5065,7 @@ function DefaultTheme({ children }) {
|
|
|
4470
5065
|
</body>
|
|
4471
5066
|
</html>` };
|
|
4472
5067
|
}
|
|
4473
|
-
function escapeHtml$
|
|
5068
|
+
function escapeHtml$3(str) {
|
|
4474
5069
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4475
5070
|
}
|
|
4476
5071
|
/**
|
|
@@ -4570,6 +5165,13 @@ async function writeSiteMapFiles(input) {
|
|
|
4570
5165
|
}
|
|
4571
5166
|
return { files };
|
|
4572
5167
|
}
|
|
5168
|
+
/** UTC `YYYY-MM-DD` for W3C lastmod. Invalid or negative timestamps are dropped. */
|
|
5169
|
+
function formatLastmod(timestampMs) {
|
|
5170
|
+
if (timestampMs == null || !Number.isFinite(timestampMs) || timestampMs < 0) return;
|
|
5171
|
+
const date = new Date(timestampMs);
|
|
5172
|
+
if (Number.isNaN(date.getTime())) return;
|
|
5173
|
+
return date.toISOString().slice(0, 10);
|
|
5174
|
+
}
|
|
4573
5175
|
function hasSiteUrl$2(siteUrl) {
|
|
4574
5176
|
return Boolean(siteUrl && siteUrl.trim());
|
|
4575
5177
|
}
|
|
@@ -4582,7 +5184,14 @@ function generateSitemapXml(pages) {
|
|
|
4582
5184
|
for (const page of pages) {
|
|
4583
5185
|
xml += " <url>\n <loc>";
|
|
4584
5186
|
xml += escapeXml$1(page.loc);
|
|
4585
|
-
xml += "</loc>\n
|
|
5187
|
+
xml += "</loc>\n";
|
|
5188
|
+
const lastmod = formatLastmod(page.lastUpdated);
|
|
5189
|
+
if (lastmod) {
|
|
5190
|
+
xml += " <lastmod>";
|
|
5191
|
+
xml += lastmod;
|
|
5192
|
+
xml += "</lastmod>\n";
|
|
5193
|
+
}
|
|
5194
|
+
xml += " </url>\n";
|
|
4586
5195
|
}
|
|
4587
5196
|
xml += "</urlset>\n";
|
|
4588
5197
|
return xml;
|
|
@@ -4643,6 +5252,168 @@ function escapeLlmsUrl(value) {
|
|
|
4643
5252
|
return escaped;
|
|
4644
5253
|
}
|
|
4645
5254
|
//#endregion
|
|
5255
|
+
//#region src/permalinks.ts
|
|
5256
|
+
const RESERVED_CASCADE_KEYS = /* @__PURE__ */ new Set(["permalink", "slug"]);
|
|
5257
|
+
/** Resolves `permalinks`. `false` / omitted stays off. `true` / `{}` enables. */
|
|
5258
|
+
function resolvePermalinksOptions(value) {
|
|
5259
|
+
return resolveFlag(value);
|
|
5260
|
+
}
|
|
5261
|
+
/** Resolves `cascade`. `false` / omitted stays off. `true` / `{}` enables. */
|
|
5262
|
+
function resolveCascadeOptions(value) {
|
|
5263
|
+
return resolveFlag(value);
|
|
5264
|
+
}
|
|
5265
|
+
/**
|
|
5266
|
+
* Applies cascade (when on) then permalink / slug rewriting (when on).
|
|
5267
|
+
*
|
|
5268
|
+
* Collisions skip the later page and keep the first. Rejected permalinks stay
|
|
5269
|
+
* on the file-tree URL. Hostile non-string values are ignored.
|
|
5270
|
+
*/
|
|
5271
|
+
function resolvePageRoutes(input) {
|
|
5272
|
+
const cascaded = applyCascade(input.pages, input.cascade);
|
|
5273
|
+
if (!input.permalinks?.enabled) return {
|
|
5274
|
+
pages: cascaded.map((page) => ({
|
|
5275
|
+
source: page.source,
|
|
5276
|
+
urlPath: normalizeUrlPath$1(page.fileUrl),
|
|
5277
|
+
frontmatter: page.frontmatter
|
|
5278
|
+
})),
|
|
5279
|
+
errors: []
|
|
5280
|
+
};
|
|
5281
|
+
const pages = [];
|
|
5282
|
+
const errors = [];
|
|
5283
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
5284
|
+
for (const page of cascaded) {
|
|
5285
|
+
const { urlPath, error } = resolveOne(page);
|
|
5286
|
+
if (error) errors.push(error);
|
|
5287
|
+
const owner = claimed.get(urlPath);
|
|
5288
|
+
if (owner) {
|
|
5289
|
+
errors.push(`[ox-content] URL collision at "${urlPath}": ${owner} kept, ${page.source} skipped`);
|
|
5290
|
+
continue;
|
|
5291
|
+
}
|
|
5292
|
+
claimed.set(urlPath, page.source);
|
|
5293
|
+
pages.push({
|
|
5294
|
+
source: page.source,
|
|
5295
|
+
urlPath,
|
|
5296
|
+
frontmatter: page.frontmatter
|
|
5297
|
+
});
|
|
5298
|
+
}
|
|
5299
|
+
return {
|
|
5300
|
+
pages,
|
|
5301
|
+
errors
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
/** Escapes a value for use in an HTML attribute. */
|
|
5305
|
+
function escapeAttribute$2(value) {
|
|
5306
|
+
return value.replace(/[&<>"']/gu, (ch) => {
|
|
5307
|
+
switch (ch) {
|
|
5308
|
+
case "&": return "&";
|
|
5309
|
+
case "<": return "<";
|
|
5310
|
+
case ">": return ">";
|
|
5311
|
+
case "\"": return """;
|
|
5312
|
+
default: return "'";
|
|
5313
|
+
}
|
|
5314
|
+
});
|
|
5315
|
+
}
|
|
5316
|
+
function normalizeUrlPath$1(value) {
|
|
5317
|
+
const segments = pathSegments(value);
|
|
5318
|
+
return segments.length === 0 ? "/" : segments.join("/");
|
|
5319
|
+
}
|
|
5320
|
+
function resolveFlag(value) {
|
|
5321
|
+
if (!value) return { enabled: false };
|
|
5322
|
+
if (value === true) return { enabled: true };
|
|
5323
|
+
return { enabled: value.enabled !== false };
|
|
5324
|
+
}
|
|
5325
|
+
function applyCascade(pages, options) {
|
|
5326
|
+
if (!options?.enabled) return pages.map((page) => ({
|
|
5327
|
+
...page,
|
|
5328
|
+
frontmatter: { ...page.frontmatter }
|
|
5329
|
+
}));
|
|
5330
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
5331
|
+
for (const page of pages) {
|
|
5332
|
+
const source = normalizeSeparators(page.source);
|
|
5333
|
+
if (isIndexFile(source)) indexes.set(directoryOf(source), { ...page.frontmatter });
|
|
5334
|
+
}
|
|
5335
|
+
return pages.map((page) => {
|
|
5336
|
+
const source = normalizeSeparators(page.source);
|
|
5337
|
+
const frontmatter = { ...page.frontmatter };
|
|
5338
|
+
for (const dir of ancestorDirs(source)) {
|
|
5339
|
+
const defaults = indexes.get(dir);
|
|
5340
|
+
if (!defaults || isIndexFile(source) && directoryOf(source) === dir) continue;
|
|
5341
|
+
for (const [key, value] of Object.entries(defaults)) if (!RESERVED_CASCADE_KEYS.has(key) && !(key in frontmatter)) frontmatter[key] = value;
|
|
5342
|
+
}
|
|
5343
|
+
return {
|
|
5344
|
+
...page,
|
|
5345
|
+
frontmatter
|
|
5346
|
+
};
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
5349
|
+
function resolveOne(page) {
|
|
5350
|
+
const fileUrl = normalizeUrlPath$1(page.fileUrl);
|
|
5351
|
+
const permalink = readString(page.frontmatter.permalink);
|
|
5352
|
+
if (permalink !== void 0) {
|
|
5353
|
+
const url = isSafePermalink(permalink) ? normalizeUrlPath$1(permalink) : void 0;
|
|
5354
|
+
return url ? { urlPath: url } : {
|
|
5355
|
+
urlPath: fileUrl,
|
|
5356
|
+
error: `[ox-content] rejected permalink ${JSON.stringify(permalink)} on ${page.source} (path escape); using the file-tree URL`
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
const slug = readString(page.frontmatter.slug);
|
|
5360
|
+
if (slug !== void 0) {
|
|
5361
|
+
const url = rewriteSlug(fileUrl, slug);
|
|
5362
|
+
return url ? { urlPath: url } : {
|
|
5363
|
+
urlPath: fileUrl,
|
|
5364
|
+
error: `[ox-content] rejected slug ${JSON.stringify(slug)} on ${page.source} (path escape); using the file-tree URL`
|
|
5365
|
+
};
|
|
5366
|
+
}
|
|
5367
|
+
return { urlPath: fileUrl };
|
|
5368
|
+
}
|
|
5369
|
+
function rewriteSlug(fileUrl, slug) {
|
|
5370
|
+
const trimmed = slug.trim();
|
|
5371
|
+
if (trimmed.includes("/") || !isSafePermalink(trimmed)) return;
|
|
5372
|
+
const normalized = normalizeUrlPath$1(trimmed);
|
|
5373
|
+
if (normalized === "/") return;
|
|
5374
|
+
if (fileUrl === "/") return normalized;
|
|
5375
|
+
const segments = fileUrl.split("/").filter(Boolean);
|
|
5376
|
+
segments.pop();
|
|
5377
|
+
segments.push(normalized);
|
|
5378
|
+
return segments.join("/");
|
|
5379
|
+
}
|
|
5380
|
+
function isSafePermalink(value) {
|
|
5381
|
+
const trimmed = value.trim();
|
|
5382
|
+
if (!trimmed || /[\n\r\0]/u.test(trimmed) || trimmed.includes("\\") || trimmed.startsWith("//")) return false;
|
|
5383
|
+
if (/^[A-Za-z]:/u.test(trimmed)) return false;
|
|
5384
|
+
const lower = trimmed.toLowerCase();
|
|
5385
|
+
if (lower.includes("javascript:") || lower.includes("data:") || lower.includes("vbscript:") || lower.includes("file:") || lower.includes("://")) return false;
|
|
5386
|
+
return pathSegments(trimmed).every((segment) => segment !== ".." && segment !== ".");
|
|
5387
|
+
}
|
|
5388
|
+
function pathSegments(value) {
|
|
5389
|
+
return value.trim().replace(/^\/+|\/+$/gu, "").split("/").filter(Boolean);
|
|
5390
|
+
}
|
|
5391
|
+
function readString(value) {
|
|
5392
|
+
return typeof value === "string" ? value : void 0;
|
|
5393
|
+
}
|
|
5394
|
+
function normalizeSeparators(value) {
|
|
5395
|
+
return value.replaceAll("\\", "/");
|
|
5396
|
+
}
|
|
5397
|
+
function isIndexFile(source) {
|
|
5398
|
+
const name = source.split("/").pop() ?? source;
|
|
5399
|
+
return (name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name).toLowerCase() === "_index";
|
|
5400
|
+
}
|
|
5401
|
+
function directoryOf(source) {
|
|
5402
|
+
const index = source.lastIndexOf("/");
|
|
5403
|
+
return index === -1 ? "" : source.slice(0, index);
|
|
5404
|
+
}
|
|
5405
|
+
function ancestorDirs(source) {
|
|
5406
|
+
const dir = directoryOf(source);
|
|
5407
|
+
const dirs = [""];
|
|
5408
|
+
if (!dir) return dirs;
|
|
5409
|
+
let acc = "";
|
|
5410
|
+
for (const segment of dir.split("/")) {
|
|
5411
|
+
acc = acc ? `${acc}/${segment}` : segment;
|
|
5412
|
+
dirs.push(acc);
|
|
5413
|
+
}
|
|
5414
|
+
return dirs;
|
|
5415
|
+
}
|
|
5416
|
+
//#endregion
|
|
4646
5417
|
//#region src/publish-state.ts
|
|
4647
5418
|
/**
|
|
4648
5419
|
* Opt-in draft / unlisted / scheduled page classification.
|
|
@@ -4733,160 +5504,202 @@ function hiddenNavKeys(pages, listed) {
|
|
|
4733
5504
|
function toNapiPublishState(options) {
|
|
4734
5505
|
if (!options) return;
|
|
4735
5506
|
return {
|
|
4736
|
-
enabled: options.enabled,
|
|
4737
|
-
now: options.now,
|
|
4738
|
-
includeDrafts: options.includeDrafts
|
|
5507
|
+
enabled: options.enabled,
|
|
5508
|
+
now: options.now,
|
|
5509
|
+
includeDrafts: options.includeDrafts
|
|
5510
|
+
};
|
|
5511
|
+
}
|
|
5512
|
+
//#endregion
|
|
5513
|
+
//#region src/markdown-source.ts
|
|
5514
|
+
/**
|
|
5515
|
+
* Opt-in Markdown source companions written beside generated HTML.
|
|
5516
|
+
*
|
|
5517
|
+
* Copies already-read source bytes. Does not re-parse Markdown to emit them.
|
|
5518
|
+
*/
|
|
5519
|
+
/**
|
|
5520
|
+
* Resolves `ssg.markdownSource` with defaults.
|
|
5521
|
+
*
|
|
5522
|
+
* `false` / omitted stays off. `true` enables companions and the alternate
|
|
5523
|
+
* link. An object enables the feature and overrides only the fields set.
|
|
5524
|
+
*/
|
|
5525
|
+
function resolveMarkdownSourceOptions(value) {
|
|
5526
|
+
if (!value) return {
|
|
5527
|
+
enabled: false,
|
|
5528
|
+
alternate: true
|
|
5529
|
+
};
|
|
5530
|
+
if (value === true) return {
|
|
5531
|
+
enabled: true,
|
|
5532
|
+
alternate: true
|
|
5533
|
+
};
|
|
5534
|
+
return {
|
|
5535
|
+
enabled: true,
|
|
5536
|
+
alternate: value.alternate !== false
|
|
4739
5537
|
};
|
|
4740
5538
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
5539
|
+
/** Companion href for one page after permalink / publish-state checks. */
|
|
5540
|
+
function markdownSourceHrefForPage(input) {
|
|
5541
|
+
if (!shouldPublishMarkdownSource(input.frontmatter, input.publishState)) return;
|
|
5542
|
+
return markdownSourceHref(resolvePageRoutes({
|
|
5543
|
+
pages: [{
|
|
5544
|
+
source: input.source,
|
|
5545
|
+
fileUrl: input.fileUrl,
|
|
5546
|
+
frontmatter: input.frontmatter
|
|
5547
|
+
}],
|
|
5548
|
+
permalinks: input.permalinks,
|
|
5549
|
+
cascade: input.cascade
|
|
5550
|
+
}).pages[0]?.urlPath ?? input.fileUrl, input.base);
|
|
4747
5551
|
}
|
|
4748
|
-
/**
|
|
4749
|
-
function
|
|
4750
|
-
|
|
5552
|
+
/** Public companion href, including `base`. Always ends in `.md`. */
|
|
5553
|
+
function markdownSourceHref(urlPath, base) {
|
|
5554
|
+
const relative = companionRelativePath(urlPath);
|
|
5555
|
+
if (!relative) return;
|
|
5556
|
+
return `${normalizeBase$1(base)}${relative}`;
|
|
5557
|
+
}
|
|
5558
|
+
/** Filesystem path for a companion, or `undefined` when it would escape `outDir`. */
|
|
5559
|
+
function markdownSourceOutputPath(outDir, urlPath) {
|
|
5560
|
+
const relative = companionRelativePath(urlPath);
|
|
5561
|
+
if (!relative) return;
|
|
5562
|
+
return containedPath$3(outDir, ...relative.split("/"));
|
|
4751
5563
|
}
|
|
4752
5564
|
/**
|
|
4753
|
-
*
|
|
5565
|
+
* Whether this page may publish a companion.
|
|
4754
5566
|
*
|
|
4755
|
-
*
|
|
4756
|
-
*
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
if (
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
5567
|
+
* Draft and unlisted source is never emitted. When `publishState` is on,
|
|
5568
|
+
* scheduled / expired pages follow that filter and `includeDrafts` is ignored
|
|
5569
|
+
* so preview HTML cannot leak source.
|
|
5570
|
+
*/
|
|
5571
|
+
function shouldPublishMarkdownSource(frontmatter, publishState) {
|
|
5572
|
+
if (frontmatter.draft === true || frontmatter.unlisted === true) return false;
|
|
5573
|
+
if (!publishState?.enabled) return true;
|
|
5574
|
+
return classifyPublishState(frontmatter, {
|
|
5575
|
+
...publishState,
|
|
5576
|
+
includeDrafts: false
|
|
5577
|
+
}).output;
|
|
5578
|
+
}
|
|
5579
|
+
/** Inserts `<link rel="alternate" type="text/markdown">` before `</head>`. */
|
|
5580
|
+
function injectMarkdownSourceAlternate(html, href) {
|
|
5581
|
+
if (!href || !/<\/head>/i.test(html)) return html;
|
|
5582
|
+
const tag = `<link rel="alternate" type="text/markdown" href="${escapeAttribute$2(href)}">`;
|
|
5583
|
+
const index = html.toLowerCase().lastIndexOf("</head>");
|
|
5584
|
+
return `${html.slice(0, index)} ${tag}\n${html.slice(index)}`;
|
|
5585
|
+
}
|
|
5586
|
+
/** Writes enabled companions from already-read source bytes. */
|
|
5587
|
+
async function writeMarkdownSourceFiles(input) {
|
|
5588
|
+
if (!input.options?.enabled) return {
|
|
5589
|
+
files: [],
|
|
4766
5590
|
errors: []
|
|
4767
5591
|
};
|
|
4768
|
-
const
|
|
5592
|
+
const files = [];
|
|
4769
5593
|
const errors = [];
|
|
4770
|
-
const
|
|
4771
|
-
for (const page of
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
errors.push(`[ox-content] URL collision at "${urlPath}": ${owner} kept, ${page.source} skipped`);
|
|
5594
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5595
|
+
for (const page of input.pages) {
|
|
5596
|
+
if (page.source == null || !shouldPublishMarkdownSource(page.frontmatter, input.publishState)) continue;
|
|
5597
|
+
const outputPath = markdownSourceOutputPath(input.outDir, page.urlPath);
|
|
5598
|
+
if (!outputPath) {
|
|
5599
|
+
errors.push(`[ox-content] markdownSource skipped path-escape for ${page.inputPath}`);
|
|
4777
5600
|
continue;
|
|
4778
5601
|
}
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
5602
|
+
const previous = seen.get(outputPath);
|
|
5603
|
+
if (previous) {
|
|
5604
|
+
errors.push(`[ox-content] markdownSource collision: ${page.inputPath} and ${previous} both map to ${outputPath}`);
|
|
5605
|
+
continue;
|
|
5606
|
+
}
|
|
5607
|
+
seen.set(outputPath, page.inputPath);
|
|
5608
|
+
await fs$2.mkdir(path$1.dirname(outputPath), { recursive: true });
|
|
5609
|
+
await fs$2.writeFile(outputPath, page.source);
|
|
5610
|
+
files.push(outputPath);
|
|
4785
5611
|
}
|
|
4786
5612
|
return {
|
|
4787
|
-
|
|
5613
|
+
files,
|
|
4788
5614
|
errors
|
|
4789
5615
|
};
|
|
4790
5616
|
}
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
function resolveFlag(value) {
|
|
4796
|
-
if (!value) return { enabled: false };
|
|
4797
|
-
if (value === true) return { enabled: true };
|
|
4798
|
-
return { enabled: value.enabled !== false };
|
|
5617
|
+
/** True when the request pathname is a `.md` companion URL. */
|
|
5618
|
+
function isMarkdownSourceRequest(pathname) {
|
|
5619
|
+
const clean = stripSearch(pathname);
|
|
5620
|
+
return clean.toLowerCase().endsWith(".md") && !clean.includes("\\");
|
|
4799
5621
|
}
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
}));
|
|
4805
|
-
const indexes = /* @__PURE__ */ new Map();
|
|
4806
|
-
for (const page of pages) {
|
|
4807
|
-
const source = normalizeSeparators(page.source);
|
|
4808
|
-
if (isIndexFile(source)) indexes.set(directoryOf(source), { ...page.frontmatter });
|
|
4809
|
-
}
|
|
4810
|
-
return pages.map((page) => {
|
|
4811
|
-
const source = normalizeSeparators(page.source);
|
|
4812
|
-
const frontmatter = { ...page.frontmatter };
|
|
4813
|
-
for (const dir of ancestorDirs(source)) {
|
|
4814
|
-
const defaults = indexes.get(dir);
|
|
4815
|
-
if (!defaults || isIndexFile(source) && directoryOf(source) === dir) continue;
|
|
4816
|
-
for (const [key, value] of Object.entries(defaults)) if (!RESERVED_CASCADE_KEYS.has(key) && !(key in frontmatter)) frontmatter[key] = value;
|
|
4817
|
-
}
|
|
5622
|
+
/** Builds a companion index from source files without transforming Markdown. */
|
|
5623
|
+
async function buildMarkdownSourceIndex(input) {
|
|
5624
|
+
const loaded = await Promise.all(input.files.map(async (file) => {
|
|
5625
|
+
const source = await fs$2.readFile(file, "utf8");
|
|
4818
5626
|
return {
|
|
4819
|
-
|
|
4820
|
-
|
|
5627
|
+
source: file,
|
|
5628
|
+
fileUrl: importNapiModuleSync().getSsgUrlPath(file, input.srcDir),
|
|
5629
|
+
frontmatter: parseSourceFrontmatter(source),
|
|
5630
|
+
body: source
|
|
4821
5631
|
};
|
|
5632
|
+
}));
|
|
5633
|
+
const routed = resolvePageRoutes({
|
|
5634
|
+
pages: loaded.map(({ source, fileUrl, frontmatter }) => ({
|
|
5635
|
+
source,
|
|
5636
|
+
fileUrl,
|
|
5637
|
+
frontmatter
|
|
5638
|
+
})),
|
|
5639
|
+
permalinks: input.permalinks,
|
|
5640
|
+
cascade: input.cascade
|
|
4822
5641
|
});
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
const
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
};
|
|
4833
|
-
}
|
|
4834
|
-
const slug = readString(page.frontmatter.slug);
|
|
4835
|
-
if (slug !== void 0) {
|
|
4836
|
-
const url = rewriteSlug(fileUrl, slug);
|
|
4837
|
-
return url ? { urlPath: url } : {
|
|
4838
|
-
urlPath: fileUrl,
|
|
4839
|
-
error: `[ox-content] rejected slug ${JSON.stringify(slug)} on ${page.source} (path escape); using the file-tree URL`
|
|
4840
|
-
};
|
|
5642
|
+
const bodies = new Map(loaded.map((page) => [page.source, page.body]));
|
|
5643
|
+
const index = /* @__PURE__ */ new Map();
|
|
5644
|
+
for (const page of routed.pages) {
|
|
5645
|
+
const href = markdownSourceHref(page.urlPath, "/");
|
|
5646
|
+
const body = bodies.get(page.source);
|
|
5647
|
+
if (!href || body == null) continue;
|
|
5648
|
+
index.set(normalizePathname(href), {
|
|
5649
|
+
source: body,
|
|
5650
|
+
allowed: shouldPublishMarkdownSource(page.frontmatter, input.publishState)
|
|
5651
|
+
});
|
|
4841
5652
|
}
|
|
4842
|
-
return
|
|
5653
|
+
return index;
|
|
4843
5654
|
}
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
if (
|
|
4847
|
-
|
|
4848
|
-
if (normalized === "/") return;
|
|
4849
|
-
if (fileUrl === "/") return normalized;
|
|
4850
|
-
const segments = fileUrl.split("/").filter(Boolean);
|
|
4851
|
-
segments.pop();
|
|
4852
|
-
segments.push(normalized);
|
|
4853
|
-
return segments.join("/");
|
|
5655
|
+
/** Looks up a companion after the site `base` has been stripped. */
|
|
5656
|
+
function resolveMarkdownSourceRequest(pathname, index) {
|
|
5657
|
+
if (!isMarkdownSourceRequest(pathname)) return;
|
|
5658
|
+
return index.get(normalizePathname(stripSearch(pathname)));
|
|
4854
5659
|
}
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
if (
|
|
4859
|
-
const
|
|
4860
|
-
|
|
4861
|
-
|
|
5660
|
+
/** Frontmatter keys only — not a Markdown parse. */
|
|
5661
|
+
function parseSourceFrontmatter(source) {
|
|
5662
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5663
|
+
if (!match?.[1]) return {};
|
|
5664
|
+
const result = {};
|
|
5665
|
+
for (const line of match[1].split("\n")) {
|
|
5666
|
+
const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
5667
|
+
if (!kv) continue;
|
|
5668
|
+
result[kv[1]] = parseFrontmatterScalar(kv[2].trim());
|
|
5669
|
+
}
|
|
5670
|
+
return result;
|
|
4862
5671
|
}
|
|
4863
|
-
function
|
|
4864
|
-
|
|
5672
|
+
function parseFrontmatterScalar(value) {
|
|
5673
|
+
if (value === "true") return true;
|
|
5674
|
+
if (value === "false") return false;
|
|
5675
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
5676
|
+
return value;
|
|
4865
5677
|
}
|
|
4866
|
-
function
|
|
4867
|
-
|
|
5678
|
+
function companionRelativePath(urlPath) {
|
|
5679
|
+
const trimmed = urlPath === "/" || !urlPath ? "index" : urlPath.replace(/^\/+|\/+$/gu, "");
|
|
5680
|
+
if (!trimmed) return;
|
|
5681
|
+
if (trimmed.split("/").some((segment) => !segment || segment === "." || segment === "..")) return;
|
|
5682
|
+
return `${trimmed}.md`;
|
|
4868
5683
|
}
|
|
4869
|
-
function
|
|
4870
|
-
|
|
5684
|
+
function containedPath$3(outDir, ...segments) {
|
|
5685
|
+
const root = path$1.resolve(outDir);
|
|
5686
|
+
const resolved = path$1.resolve(root, ...segments);
|
|
5687
|
+
const prefix = root.endsWith(path$1.sep) ? root : `${root}${path$1.sep}`;
|
|
5688
|
+
if (resolved === root || !resolved.startsWith(prefix)) return;
|
|
5689
|
+
return resolved;
|
|
4871
5690
|
}
|
|
4872
|
-
function
|
|
4873
|
-
|
|
4874
|
-
return
|
|
5691
|
+
function normalizeBase$1(base) {
|
|
5692
|
+
if (!base || base === "/") return "/";
|
|
5693
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
4875
5694
|
}
|
|
4876
|
-
function
|
|
4877
|
-
|
|
4878
|
-
return index === -1 ? "" : source.slice(0, index);
|
|
5695
|
+
function stripSearch(pathname) {
|
|
5696
|
+
return pathname.split("?")[0]?.split("#")[0] ?? pathname;
|
|
4879
5697
|
}
|
|
4880
|
-
function
|
|
4881
|
-
const
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
for (const segment of dir.split("/")) {
|
|
4886
|
-
acc = acc ? `${acc}/${segment}` : segment;
|
|
4887
|
-
dirs.push(acc);
|
|
4888
|
-
}
|
|
4889
|
-
return dirs;
|
|
5698
|
+
function normalizePathname(pathname) {
|
|
5699
|
+
const clean = stripSearch(pathname);
|
|
5700
|
+
if (!clean || clean === "/") return "/";
|
|
5701
|
+
const withSlash = clean.startsWith("/") ? clean : `/${clean}`;
|
|
5702
|
+
return withSlash.length > 1 && withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
|
|
4890
5703
|
}
|
|
4891
5704
|
//#endregion
|
|
4892
5705
|
//#region src/apply-permalinks.ts
|
|
@@ -5118,7 +5931,7 @@ async function writeRedirectFiles(input) {
|
|
|
5118
5931
|
}
|
|
5119
5932
|
/** Static HTML redirect body. `dest` is escaped. */
|
|
5120
5933
|
function generateRedirectHtml(dest) {
|
|
5121
|
-
const escaped = escapeHtml$
|
|
5934
|
+
const escaped = escapeHtml$2(dest);
|
|
5122
5935
|
return `\
|
|
5123
5936
|
<!DOCTYPE html>
|
|
5124
5937
|
<html lang="en">
|
|
@@ -5203,7 +6016,7 @@ function upsert(files, index, occupied, from, to, base) {
|
|
|
5203
6016
|
html
|
|
5204
6017
|
});
|
|
5205
6018
|
}
|
|
5206
|
-
function escapeHtml$
|
|
6019
|
+
function escapeHtml$2(value) {
|
|
5207
6020
|
return value.replace(/[&<>"']/g, (ch) => {
|
|
5208
6021
|
switch (ch) {
|
|
5209
6022
|
case "&": return "&";
|
|
@@ -5561,6 +6374,7 @@ function createNativeTransformOptions(options) {
|
|
|
5561
6374
|
return {
|
|
5562
6375
|
gfm: options.gfm,
|
|
5563
6376
|
footnotes: options.footnotes,
|
|
6377
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
5564
6378
|
taskLists: options.taskLists,
|
|
5565
6379
|
tables: options.tables,
|
|
5566
6380
|
strikethrough: options.strikethrough,
|
|
@@ -5568,6 +6382,7 @@ function createNativeTransformOptions(options) {
|
|
|
5568
6382
|
autolinkUrls: options.autolinks,
|
|
5569
6383
|
frontmatter: options.frontmatter,
|
|
5570
6384
|
tocMaxDepth: options.tocMaxDepth,
|
|
6385
|
+
headingPermalinks: options.headingPermalinks?.enabled ?? false,
|
|
5571
6386
|
codeAnnotations: options.codeAnnotations?.enabled ?? false,
|
|
5572
6387
|
codeAnnotationMetaKey: options.codeAnnotations?.metaKey ?? "annotate",
|
|
5573
6388
|
codeAnnotationSyntax: options.codeAnnotations?.notation ?? "attribute",
|
|
@@ -5582,6 +6397,13 @@ function createNativeTransformOptions(options) {
|
|
|
5582
6397
|
} : void 0,
|
|
5583
6398
|
attributes: options.attrs?.enabled ? { enabled: true } : void 0,
|
|
5584
6399
|
badges: options.badges?.enabled ? { enabled: true } : void 0,
|
|
6400
|
+
magicLinks: options.magicLinks?.enabled ? {
|
|
6401
|
+
enabled: true,
|
|
6402
|
+
aliases: options.magicLinks.aliases,
|
|
6403
|
+
favicon: options.magicLinks.favicon,
|
|
6404
|
+
faviconTemplate: options.magicLinks.faviconTemplate,
|
|
6405
|
+
imageOverrides: options.magicLinks.imageOverrides
|
|
6406
|
+
} : void 0,
|
|
5585
6407
|
containers: options.containers?.enabled ? {
|
|
5586
6408
|
enabled: true,
|
|
5587
6409
|
types: options.containers.types
|
|
@@ -6006,6 +6828,7 @@ function isExcludedFromFeed(item, publishState) {
|
|
|
6006
6828
|
const frontmatter = item.frontmatter ?? {};
|
|
6007
6829
|
if (item.draft === true || frontmatter.draft === true) return true;
|
|
6008
6830
|
if (item.unlisted === true || frontmatter.unlisted === true) return true;
|
|
6831
|
+
if (frontmatter.external === true) return true;
|
|
6009
6832
|
if (!publishState?.enabled) return false;
|
|
6010
6833
|
return !classifyPublishState({
|
|
6011
6834
|
...frontmatter,
|
|
@@ -6246,14 +7069,14 @@ function relatedMarkup(pages) {
|
|
|
6246
7069
|
}
|
|
6247
7070
|
function listPageContent(terms, base, urlName) {
|
|
6248
7071
|
const items = terms.map((term) => listItem$1(siteHref$3(base, urlName, term.slug), term.label)).join("");
|
|
6249
|
-
return `<h1>${escapeHtml$
|
|
7072
|
+
return `<h1>${escapeHtml$1(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
|
|
6250
7073
|
}
|
|
6251
7074
|
function termPageContent(term) {
|
|
6252
7075
|
const items = [...term.pages].sort((left, right) => {
|
|
6253
7076
|
const titleCmp = left.title.localeCompare(right.title);
|
|
6254
7077
|
return titleCmp !== 0 ? titleCmp : left.routePaths.href.localeCompare(right.routePaths.href);
|
|
6255
7078
|
}).map((page) => listItem$1(page.routePaths.href, page.title)).join("");
|
|
6256
|
-
return `<h1>${escapeHtml$
|
|
7079
|
+
return `<h1>${escapeHtml$1(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
|
|
6257
7080
|
}
|
|
6258
7081
|
function displayTaxonomyName(name) {
|
|
6259
7082
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -6271,9 +7094,9 @@ function containedPath$2(outDir, ...segments) {
|
|
|
6271
7094
|
return resolved;
|
|
6272
7095
|
}
|
|
6273
7096
|
function listItem$1(href, label) {
|
|
6274
|
-
return `<li><a href="${escapeHtml$
|
|
7097
|
+
return `<li><a href="${escapeHtml$1(href)}">${escapeHtml$1(label)}</a></li>`;
|
|
6275
7098
|
}
|
|
6276
|
-
function escapeHtml$
|
|
7099
|
+
function escapeHtml$1(value) {
|
|
6277
7100
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6278
7101
|
}
|
|
6279
7102
|
//#endregion
|
|
@@ -6547,96 +7370,441 @@ function resolveBlogOptions(value) {
|
|
|
6547
7370
|
if (!value) return {
|
|
6548
7371
|
enabled: false,
|
|
6549
7372
|
authors: {},
|
|
6550
|
-
pageSize: DEFAULT_PAGE_SIZE
|
|
7373
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7374
|
+
feeds: []
|
|
6551
7375
|
};
|
|
6552
7376
|
if (value === true) return {
|
|
6553
7377
|
enabled: true,
|
|
6554
7378
|
authors: {},
|
|
6555
|
-
pageSize: DEFAULT_PAGE_SIZE
|
|
7379
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7380
|
+
feeds: []
|
|
7381
|
+
};
|
|
7382
|
+
return {
|
|
7383
|
+
enabled: true,
|
|
7384
|
+
collection: value.collection,
|
|
7385
|
+
authors: normalizeAuthors(value.authors),
|
|
7386
|
+
pageSize: normalizePageSize(value.pageSize),
|
|
7387
|
+
feeds: normalizeFeeds(value.feeds)
|
|
6556
7388
|
};
|
|
7389
|
+
}
|
|
7390
|
+
/**
|
|
7391
|
+
* Picks a collection named `blog`, else the only configured collection.
|
|
7392
|
+
*
|
|
7393
|
+
* An explicit name always wins. Several collections and no `blog` name
|
|
7394
|
+
* require `blog.collection`.
|
|
7395
|
+
*/
|
|
7396
|
+
function resolveBlogCollectionName(requested, collectionNames) {
|
|
7397
|
+
if (requested) return requested;
|
|
7398
|
+
if (collectionNames.includes("blog")) return "blog";
|
|
7399
|
+
if (collectionNames.length === 1) return collectionNames[0];
|
|
7400
|
+
}
|
|
7401
|
+
function normalizeAuthors(authors) {
|
|
7402
|
+
if (!authors || typeof authors !== "object") return {};
|
|
7403
|
+
const resolved = {};
|
|
7404
|
+
for (const [key, value] of Object.entries(authors)) {
|
|
7405
|
+
if (!value || typeof value.name !== "string") continue;
|
|
7406
|
+
resolved[key] = {
|
|
7407
|
+
name: value.name,
|
|
7408
|
+
bio: typeof value.bio === "string" ? value.bio : void 0,
|
|
7409
|
+
url: typeof value.url === "string" ? value.url : void 0
|
|
7410
|
+
};
|
|
7411
|
+
}
|
|
7412
|
+
return resolved;
|
|
7413
|
+
}
|
|
7414
|
+
function normalizePageSize(value) {
|
|
7415
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
|
|
7416
|
+
return DEFAULT_PAGE_SIZE;
|
|
7417
|
+
}
|
|
7418
|
+
function normalizeFeeds(feeds) {
|
|
7419
|
+
if (!Array.isArray(feeds)) return [];
|
|
7420
|
+
const resolved = [];
|
|
7421
|
+
for (const entry of feeds) {
|
|
7422
|
+
if (typeof entry === "string") {
|
|
7423
|
+
const url = entry.trim();
|
|
7424
|
+
if (url) resolved.push({
|
|
7425
|
+
url,
|
|
7426
|
+
onError: "warn"
|
|
7427
|
+
});
|
|
7428
|
+
continue;
|
|
7429
|
+
}
|
|
7430
|
+
if (!entry || typeof entry !== "object" || typeof entry.url !== "string") continue;
|
|
7431
|
+
const url = entry.url.trim();
|
|
7432
|
+
if (!url) continue;
|
|
7433
|
+
const language = trimOptional(entry.language);
|
|
7434
|
+
const author = trimOptional(entry.author);
|
|
7435
|
+
resolved.push({
|
|
7436
|
+
url,
|
|
7437
|
+
...language ? { language } : {},
|
|
7438
|
+
...author ? { author } : {},
|
|
7439
|
+
onError: entry.onError === "error" ? "error" : "warn"
|
|
7440
|
+
});
|
|
7441
|
+
}
|
|
7442
|
+
return resolved;
|
|
7443
|
+
}
|
|
7444
|
+
function trimOptional(value) {
|
|
7445
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
7446
|
+
}
|
|
7447
|
+
//#endregion
|
|
7448
|
+
//#region src/blog-feed-date.ts
|
|
7449
|
+
/**
|
|
7450
|
+
* Publication dates from RSS / Atom items.
|
|
7451
|
+
*/
|
|
7452
|
+
const MONTHS = {
|
|
7453
|
+
jan: 1,
|
|
7454
|
+
feb: 2,
|
|
7455
|
+
mar: 3,
|
|
7456
|
+
apr: 4,
|
|
7457
|
+
may: 5,
|
|
7458
|
+
jun: 6,
|
|
7459
|
+
jul: 7,
|
|
7460
|
+
aug: 8,
|
|
7461
|
+
sep: 9,
|
|
7462
|
+
oct: 10,
|
|
7463
|
+
nov: 11,
|
|
7464
|
+
dec: 12
|
|
7465
|
+
};
|
|
7466
|
+
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}))$/;
|
|
7467
|
+
function parseFeedDate(value) {
|
|
7468
|
+
const trimmed = value?.trim();
|
|
7469
|
+
if (!trimmed) return;
|
|
7470
|
+
return parseDate(trimmed) ?? parseRfc822(trimmed);
|
|
7471
|
+
}
|
|
7472
|
+
function feedDateLabel(date) {
|
|
7473
|
+
return `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
|
|
7474
|
+
}
|
|
7475
|
+
function feedDateIso(date) {
|
|
7476
|
+
return `${feedDateLabel(date)}T${String(date.hour).padStart(2, "0")}:${String(date.minute).padStart(2, "0")}:${String(date.second).padStart(2, "0")}Z`;
|
|
7477
|
+
}
|
|
7478
|
+
function parseRfc822(value) {
|
|
7479
|
+
const match = value.match(RFC822);
|
|
7480
|
+
if (!match) return;
|
|
7481
|
+
const month = MONTHS[match[2]?.toLowerCase() ?? ""];
|
|
7482
|
+
if (!month) return;
|
|
7483
|
+
const day = match[1]?.padStart(2, "0");
|
|
7484
|
+
const year = match[3];
|
|
7485
|
+
const hour = match[4];
|
|
7486
|
+
const minute = match[5];
|
|
7487
|
+
const second = (match[6] ?? "00").padStart(2, "0");
|
|
7488
|
+
const zone = match[7];
|
|
7489
|
+
const tz = zone ? `${zone.slice(0, 3)}:${zone.slice(3)}` : "Z";
|
|
7490
|
+
return parseDate(`${year}-${String(month).padStart(2, "0")}-${day}T${hour}:${minute}:${second}${tz}`);
|
|
7491
|
+
}
|
|
7492
|
+
//#endregion
|
|
7493
|
+
//#region src/blog-feed-url.ts
|
|
7494
|
+
/**
|
|
7495
|
+
* Safe-URL checks for configured external blog feeds.
|
|
7496
|
+
*/
|
|
7497
|
+
const CONTROL_CHARS = /[\n\r\t\0]/;
|
|
7498
|
+
function isSafeFeedUrl(value) {
|
|
7499
|
+
const trimmed = value.trim();
|
|
7500
|
+
if (!trimmed || CONTROL_CHARS.test(trimmed)) return false;
|
|
7501
|
+
try {
|
|
7502
|
+
const url = new URL(trimmed);
|
|
7503
|
+
if (url.protocol !== "https:") return false;
|
|
7504
|
+
if (url.username || url.password) return false;
|
|
7505
|
+
return !isBlockedFeedHost(url.hostname);
|
|
7506
|
+
} catch {
|
|
7507
|
+
return false;
|
|
7508
|
+
}
|
|
7509
|
+
}
|
|
7510
|
+
function canonicalizeFeedItemUrl(value) {
|
|
7511
|
+
if (!isSafeFeedUrl(value)) return;
|
|
7512
|
+
const url = new URL(value.trim());
|
|
7513
|
+
url.hash = "";
|
|
7514
|
+
url.username = "";
|
|
7515
|
+
url.password = "";
|
|
7516
|
+
if (url.port === "443") url.port = "";
|
|
7517
|
+
url.hostname = url.hostname.toLowerCase();
|
|
7518
|
+
let href = url.href;
|
|
7519
|
+
if (url.pathname !== "/" && href.endsWith("/")) href = href.slice(0, -1);
|
|
7520
|
+
return href;
|
|
7521
|
+
}
|
|
7522
|
+
function isBlockedFeedHost(hostname) {
|
|
7523
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
7524
|
+
if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true;
|
|
7525
|
+
if (host.includes(":")) return isBlockedIPv6(host);
|
|
7526
|
+
return isIPv4(host) ? isBlockedIPv4(host) : false;
|
|
7527
|
+
}
|
|
7528
|
+
function isBlockedFeedAddress(address) {
|
|
7529
|
+
const value = address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
7530
|
+
if (value.includes(":")) return isBlockedIPv6(value);
|
|
7531
|
+
return isIPv4(value) ? isBlockedIPv4(value) : true;
|
|
7532
|
+
}
|
|
7533
|
+
function isIPv4(value) {
|
|
7534
|
+
const parts = value.split(".");
|
|
7535
|
+
if (parts.length !== 4) return false;
|
|
7536
|
+
return parts.every((part) => {
|
|
7537
|
+
const n = Number(part);
|
|
7538
|
+
return Number.isInteger(n) && n >= 0 && n <= 255 && String(n) === part;
|
|
7539
|
+
});
|
|
7540
|
+
}
|
|
7541
|
+
function isBlockedIPv4(ip) {
|
|
7542
|
+
const [a, b] = ip.split(".").map(Number);
|
|
7543
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
7544
|
+
}
|
|
7545
|
+
function isBlockedIPv6(ip) {
|
|
7546
|
+
if (ip === "::" || ip === "::1") return true;
|
|
7547
|
+
if (ip.startsWith("::ffff:")) {
|
|
7548
|
+
const mapped = ip.slice(7);
|
|
7549
|
+
return isIPv4(mapped) ? isBlockedIPv4(mapped) : true;
|
|
7550
|
+
}
|
|
7551
|
+
const first = Number.parseInt(ip.split(":")[0] ?? "", 16);
|
|
7552
|
+
if (!Number.isFinite(first)) return true;
|
|
7553
|
+
if (first >= 65152 && first <= 65215) return true;
|
|
7554
|
+
return (first & 65024) === 64512;
|
|
7555
|
+
}
|
|
7556
|
+
let installedNetwork = {};
|
|
7557
|
+
async function fetchBlogFeedBody(url, network = {}) {
|
|
7558
|
+
const timeoutMs = network.limits?.timeoutMs ?? installedNetwork.limits?.timeoutMs ?? 1e4;
|
|
7559
|
+
const maxBytes = network.limits?.maxBytes ?? installedNetwork.limits?.maxBytes ?? 1048576;
|
|
7560
|
+
const maxRedirects = network.limits?.maxRedirects ?? installedNetwork.limits?.maxRedirects ?? 5;
|
|
7561
|
+
const fetchFn = network.fetch ?? installedNetwork.fetch ?? defaultFetch;
|
|
7562
|
+
const lookup = network.lookup ?? installedNetwork.lookup ?? defaultLookup;
|
|
7563
|
+
const controller = new AbortController();
|
|
7564
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
7565
|
+
try {
|
|
7566
|
+
return await followFeed(url, fetchFn, lookup, controller.signal, maxBytes, maxRedirects);
|
|
7567
|
+
} catch (error) {
|
|
7568
|
+
if (isAbortError(error)) throw new Error("timeout");
|
|
7569
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
7570
|
+
} finally {
|
|
7571
|
+
clearTimeout(timer);
|
|
7572
|
+
}
|
|
7573
|
+
}
|
|
7574
|
+
async function followFeed(startUrl, fetchFn, lookup, signal, maxBytes, maxRedirects) {
|
|
7575
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7576
|
+
let current = startUrl;
|
|
7577
|
+
for (let hops = 0; hops <= maxRedirects; hops += 1) {
|
|
7578
|
+
await assertSafeFeedTarget(current, lookup);
|
|
7579
|
+
if (seen.has(current)) throw new Error("too many redirects");
|
|
7580
|
+
seen.add(current);
|
|
7581
|
+
const response = await fetchFn(current, {
|
|
7582
|
+
method: "GET",
|
|
7583
|
+
redirect: "manual",
|
|
7584
|
+
signal,
|
|
7585
|
+
headers: {
|
|
7586
|
+
Accept: "application/rss+xml, application/atom+xml, application/xml, text/xml;q=0.9",
|
|
7587
|
+
"User-Agent": "ox-content-blog-feeds/1.0"
|
|
7588
|
+
}
|
|
7589
|
+
});
|
|
7590
|
+
if (isRedirect(response.status)) {
|
|
7591
|
+
current = resolveRedirect(current, response.headers.get("location"));
|
|
7592
|
+
continue;
|
|
7593
|
+
}
|
|
7594
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
7595
|
+
assertFeedContentType(response.headers.get("content-type"));
|
|
7596
|
+
return readBoundedBody(response, maxBytes);
|
|
7597
|
+
}
|
|
7598
|
+
throw new Error("too many redirects");
|
|
7599
|
+
}
|
|
7600
|
+
async function assertSafeFeedTarget(url, lookup) {
|
|
7601
|
+
if (!isSafeFeedUrl(url)) throw new Error("unsafe URL");
|
|
7602
|
+
const hostname = new URL(url).hostname;
|
|
7603
|
+
const addresses = await lookup(hostname);
|
|
7604
|
+
if (addresses.length === 0 || addresses.some((address) => isBlockedFeedAddress(address))) throw new Error("private network");
|
|
7605
|
+
}
|
|
7606
|
+
async function defaultLookup(hostname) {
|
|
7607
|
+
return (await lookup(hostname, {
|
|
7608
|
+
all: true,
|
|
7609
|
+
verbatim: true
|
|
7610
|
+
})).map((record) => record.address);
|
|
7611
|
+
}
|
|
7612
|
+
function defaultFetch(input, init) {
|
|
7613
|
+
return fetch(input, init);
|
|
7614
|
+
}
|
|
7615
|
+
function isRedirect(status) {
|
|
7616
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
7617
|
+
}
|
|
7618
|
+
function resolveRedirect(current, location) {
|
|
7619
|
+
if (!location?.trim()) throw new Error("too many redirects");
|
|
7620
|
+
try {
|
|
7621
|
+
return new URL(location, current).href;
|
|
7622
|
+
} catch {
|
|
7623
|
+
throw new Error("unsafe URL");
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7626
|
+
function assertFeedContentType(value) {
|
|
7627
|
+
if (!value) return;
|
|
7628
|
+
const type = value.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
7629
|
+
if (type === "text/html" || type === "application/xhtml+xml" || type === "application/json") throw new Error("not a feed");
|
|
7630
|
+
}
|
|
7631
|
+
async function readBoundedBody(response, maxBytes) {
|
|
7632
|
+
const length = Number(response.headers.get("content-length"));
|
|
7633
|
+
if (Number.isFinite(length) && length > maxBytes) throw new Error("oversized");
|
|
7634
|
+
const reader = response.body?.getReader();
|
|
7635
|
+
if (!reader) {
|
|
7636
|
+
const text = await response.text();
|
|
7637
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) throw new Error("oversized");
|
|
7638
|
+
return text;
|
|
7639
|
+
}
|
|
7640
|
+
const chunks = [];
|
|
7641
|
+
let total = 0;
|
|
7642
|
+
while (true) {
|
|
7643
|
+
const { done, value } = await reader.read();
|
|
7644
|
+
if (done) break;
|
|
7645
|
+
if (!value) continue;
|
|
7646
|
+
total += value.byteLength;
|
|
7647
|
+
if (total > maxBytes) {
|
|
7648
|
+
await reader.cancel();
|
|
7649
|
+
throw new Error("oversized");
|
|
7650
|
+
}
|
|
7651
|
+
chunks.push(value);
|
|
7652
|
+
}
|
|
7653
|
+
return new TextDecoder("utf-8").decode(concatBytes(chunks, total));
|
|
7654
|
+
}
|
|
7655
|
+
function concatBytes(chunks, total) {
|
|
7656
|
+
const out = new Uint8Array(total);
|
|
7657
|
+
let offset = 0;
|
|
7658
|
+
for (const chunk of chunks) {
|
|
7659
|
+
out.set(chunk, offset);
|
|
7660
|
+
offset += chunk.byteLength;
|
|
7661
|
+
}
|
|
7662
|
+
return out;
|
|
7663
|
+
}
|
|
7664
|
+
function isAbortError(error) {
|
|
7665
|
+
return error instanceof Error && (error.name === "AbortError" || error.message === "timeout");
|
|
7666
|
+
}
|
|
7667
|
+
//#endregion
|
|
7668
|
+
//#region src/blog-feed-parse.ts
|
|
7669
|
+
/**
|
|
7670
|
+
* RSS 2.0 / Atom 1.0 item extraction. HTML documents are rejected.
|
|
7671
|
+
*/
|
|
7672
|
+
const ITEM_BLOCK = /<(?:[\w.-]+:)?item\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?item>/gi;
|
|
7673
|
+
const ENTRY_BLOCK = /<(?:[\w.-]+:)?entry\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?entry>/gi;
|
|
7674
|
+
function parseBlogFeed(body, feedLanguage) {
|
|
7675
|
+
const xml = stripBom(body);
|
|
7676
|
+
if (looksLikeHtml(xml)) throw new Error("not a feed");
|
|
7677
|
+
if (!looksLikeXmlFeed(xml)) throw new Error("malformed XML");
|
|
7678
|
+
const channelLanguage = textChild(xml, ["language", "dc:language"]) ?? xmlLang(xml) ?? feedLanguage;
|
|
7679
|
+
const items = collectBlocks(xml, ITEM_BLOCK).map((block) => normalizeRssItem(block, channelLanguage));
|
|
7680
|
+
if (items.length > 0 || /<(?:[\w.-]+:)?rss\b/i.test(xml)) return items.filter((item) => item != null);
|
|
7681
|
+
return collectBlocks(xml, ENTRY_BLOCK).map((block) => normalizeAtomEntry(block, channelLanguage)).filter((item) => item != null);
|
|
7682
|
+
}
|
|
7683
|
+
function normalizeRssItem(block, fallbackLanguage) {
|
|
7684
|
+
const title = textChild(block, ["title"]);
|
|
7685
|
+
const guid = guidChild(block);
|
|
7686
|
+
const link = canonicalizeFeedItemUrl(textChild(block, ["link"]) ?? "") ?? (guid?.permalink ? canonicalizeFeedItemUrl(guid.value) : void 0);
|
|
7687
|
+
if (!title || !link) return;
|
|
6557
7688
|
return {
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
7689
|
+
title,
|
|
7690
|
+
link,
|
|
7691
|
+
id: guid?.value || link,
|
|
7692
|
+
date: parseFeedDate(textChild(block, [
|
|
7693
|
+
"pubDate",
|
|
7694
|
+
"dc:date",
|
|
7695
|
+
"published",
|
|
7696
|
+
"updated"
|
|
7697
|
+
])),
|
|
7698
|
+
language: textChild(block, ["language", "dc:language"]) ?? xmlLang(block) ?? fallbackLanguage,
|
|
7699
|
+
summary: textChild(block, [
|
|
7700
|
+
"description",
|
|
7701
|
+
"summary",
|
|
7702
|
+
"content:encoded",
|
|
7703
|
+
"content"
|
|
7704
|
+
])
|
|
7705
|
+
};
|
|
7706
|
+
}
|
|
7707
|
+
function normalizeAtomEntry(block, fallbackLanguage) {
|
|
7708
|
+
const title = textChild(block, ["title"]);
|
|
7709
|
+
const link = canonicalizeFeedItemUrl(atomLink(block) ?? "");
|
|
7710
|
+
if (!title || !link) return;
|
|
7711
|
+
return {
|
|
7712
|
+
title,
|
|
7713
|
+
link,
|
|
7714
|
+
id: textChild(block, ["id"]) || link,
|
|
7715
|
+
date: parseFeedDate(textChild(block, [
|
|
7716
|
+
"published",
|
|
7717
|
+
"updated",
|
|
7718
|
+
"dc:date"
|
|
7719
|
+
])),
|
|
7720
|
+
language: xmlLang(block) ?? textChild(block, ["language", "dc:language"]) ?? fallbackLanguage,
|
|
7721
|
+
summary: textChild(block, [
|
|
7722
|
+
"summary",
|
|
7723
|
+
"content",
|
|
7724
|
+
"description"
|
|
7725
|
+
])
|
|
7726
|
+
};
|
|
7727
|
+
}
|
|
7728
|
+
function collectBlocks(xml, pattern) {
|
|
7729
|
+
return [...xml.matchAll(pattern)].flatMap((match) => match[1] ? [match[1]] : []);
|
|
7730
|
+
}
|
|
7731
|
+
function textChild(block, names) {
|
|
7732
|
+
for (const name of names) {
|
|
7733
|
+
const pattern = new RegExp(`<(?:[\\w.-]+:)?${escapeRegExp$1(localName(name))}(?:\\s[^>]*)?>([\\s\\S]*?)</(?:[\\w.-]+:)?${escapeRegExp$1(localName(name))}>`, "i");
|
|
7734
|
+
const match = block.match(pattern);
|
|
7735
|
+
if (match?.[1] != null) {
|
|
7736
|
+
const text = decodeXmlText(match[1]);
|
|
7737
|
+
if (text) return text;
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
6563
7740
|
}
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
7741
|
+
function guidChild(block) {
|
|
7742
|
+
const match = block.match(/<(?:[\w.-]+:)?guid\b([^>]*)>([\s\S]*?)<\/(?:[\w.-]+:)?guid>/i);
|
|
7743
|
+
if (!match?.[2]) return;
|
|
7744
|
+
const value = decodeXmlText(match[2]);
|
|
7745
|
+
if (!value) return;
|
|
7746
|
+
const attrs = match[1] ?? "";
|
|
7747
|
+
return {
|
|
7748
|
+
value,
|
|
7749
|
+
permalink: !/isPermaLink\s*=\s*(['"]?)false\1/i.test(attrs)
|
|
7750
|
+
};
|
|
6574
7751
|
}
|
|
6575
|
-
function
|
|
6576
|
-
|
|
6577
|
-
const
|
|
6578
|
-
|
|
6579
|
-
if (!
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
};
|
|
7752
|
+
function atomLink(block) {
|
|
7753
|
+
const links = [];
|
|
7754
|
+
for (const match of block.matchAll(/<(?:[\w.-]+:)?link\b([^>]*)\/?>/gi)) {
|
|
7755
|
+
const href = attrValue(match[1] ?? "", "href");
|
|
7756
|
+
if (!href) continue;
|
|
7757
|
+
links.push({
|
|
7758
|
+
href,
|
|
7759
|
+
rel: (attrValue(match[1] ?? "", "rel") ?? "alternate").toLowerCase()
|
|
7760
|
+
});
|
|
6585
7761
|
}
|
|
6586
|
-
return
|
|
7762
|
+
return links.find((link) => link.rel === "alternate")?.href ?? links[0]?.href;
|
|
6587
7763
|
}
|
|
6588
|
-
function
|
|
6589
|
-
|
|
6590
|
-
return DEFAULT_PAGE_SIZE;
|
|
7764
|
+
function xmlLang(block) {
|
|
7765
|
+
return block.match(/\bxml:lang\s*=\s*(['"])([^'"]+)\1/i)?.[2]?.trim() || void 0;
|
|
6591
7766
|
}
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
if (
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
continue;
|
|
7767
|
+
function attrValue(attrs, name) {
|
|
7768
|
+
return attrs.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, "i"))?.[2]?.trim() || void 0;
|
|
7769
|
+
}
|
|
7770
|
+
function decodeXmlText(value) {
|
|
7771
|
+
return decodeEntities(value.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, "$1").replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
7772
|
+
}
|
|
7773
|
+
function decodeEntities(value) {
|
|
7774
|
+
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, name) => {
|
|
7775
|
+
const lower = name.toLowerCase();
|
|
7776
|
+
if (lower === "amp") return "&";
|
|
7777
|
+
if (lower === "lt") return "<";
|
|
7778
|
+
if (lower === "gt") return ">";
|
|
7779
|
+
if (lower === "quot") return "\"";
|
|
7780
|
+
if (lower === "apos") return "'";
|
|
7781
|
+
if (lower.startsWith("#x")) {
|
|
7782
|
+
const code = Number.parseInt(lower.slice(2), 16);
|
|
7783
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
6610
7784
|
}
|
|
6611
|
-
if (
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
latinRun = true;
|
|
6615
|
-
}
|
|
6616
|
-
continue;
|
|
7785
|
+
if (lower.startsWith("#")) {
|
|
7786
|
+
const code = Number.parseInt(lower.slice(1), 10);
|
|
7787
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
6617
7788
|
}
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
}
|
|
6621
|
-
if (latin === 0 && cjk === 0) return 0;
|
|
6622
|
-
return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
|
|
7789
|
+
return entity;
|
|
7790
|
+
});
|
|
6623
7791
|
}
|
|
6624
|
-
function
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
return match ? markdown.slice(match[0].length) : markdown;
|
|
7792
|
+
function looksLikeHtml(body) {
|
|
7793
|
+
const start = body.trim().slice(0, 256).toLowerCase();
|
|
7794
|
+
return start.startsWith("<!doctype html") || start.startsWith("<html");
|
|
6628
7795
|
}
|
|
6629
|
-
function
|
|
6630
|
-
return
|
|
7796
|
+
function looksLikeXmlFeed(body) {
|
|
7797
|
+
return /<(?:[\w.-]+:)?(?:rss|feed|rdf:RDF|item|entry)\b/i.test(body);
|
|
6631
7798
|
}
|
|
6632
|
-
function
|
|
6633
|
-
return
|
|
7799
|
+
function stripBom(value) {
|
|
7800
|
+
return value.charCodeAt(0) === 65279 ? value.slice(1) : value;
|
|
6634
7801
|
}
|
|
6635
|
-
function
|
|
6636
|
-
|
|
7802
|
+
function localName(name) {
|
|
7803
|
+
const index = name.indexOf(":");
|
|
7804
|
+
return index === -1 ? name : name.slice(index + 1);
|
|
6637
7805
|
}
|
|
6638
|
-
function
|
|
6639
|
-
return
|
|
7806
|
+
function escapeRegExp$1(value) {
|
|
7807
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6640
7808
|
}
|
|
6641
7809
|
//#endregion
|
|
6642
7810
|
//#region src/blog-html.ts
|
|
@@ -6652,13 +7820,13 @@ function isSafeBlogUrl(value) {
|
|
|
6652
7820
|
return trimmed.toLowerCase().startsWith("https:");
|
|
6653
7821
|
}
|
|
6654
7822
|
function postMetaMarkup(meta) {
|
|
6655
|
-
const parts = [`<p class="ox-blog-meta__reading-time">${escapeHtml
|
|
7823
|
+
const parts = [`<p class="ox-blog-meta__reading-time">${escapeHtml(String(meta.minutes))} min read</p>`];
|
|
6656
7824
|
if (meta.authors.length > 0) {
|
|
6657
7825
|
const items = meta.authors.map((author) => authorMarkup(author)).join("");
|
|
6658
7826
|
parts.push(`<ul class="ox-blog-meta__authors">${items}</ul>`);
|
|
6659
7827
|
}
|
|
6660
7828
|
if (meta.tags.length > 0) {
|
|
6661
|
-
const items = meta.tags.map((tag) => `<li><a href="${escapeHtml
|
|
7829
|
+
const items = meta.tags.map((tag) => `<li><a href="${escapeHtml(tag.href)}">${escapeHtml(tag.label)}</a></li>`).join("");
|
|
6662
7830
|
parts.push(`<ul class="ox-blog-meta__tags">${items}</ul>`);
|
|
6663
7831
|
}
|
|
6664
7832
|
return `<aside class="ox-blog-meta">${parts.join("")}</aside>\n`;
|
|
@@ -6666,25 +7834,25 @@ function postMetaMarkup(meta) {
|
|
|
6666
7834
|
function indexPageContent(items, pager) {
|
|
6667
7835
|
const list = items.map((item) => listItem(item)).join("");
|
|
6668
7836
|
const links = [];
|
|
6669
|
-
if (pager.newerHref) links.push(`<a href="${escapeHtml
|
|
6670
|
-
if (pager.olderHref) links.push(`<a href="${escapeHtml
|
|
7837
|
+
if (pager.newerHref) links.push(`<a href="${escapeHtml(pager.newerHref)}" rel="prev">Newer</a>`);
|
|
7838
|
+
if (pager.olderHref) links.push(`<a href="${escapeHtml(pager.olderHref)}" rel="next">Older</a>`);
|
|
6671
7839
|
return `<h1>Blog</h1><ul class="ox-blog">${list}</ul>${links.length > 0 ? `<nav class="ox-blog-pager">${links.join("")}</nav>` : ""}`;
|
|
6672
7840
|
}
|
|
6673
7841
|
function tagPageContent(label, items) {
|
|
6674
7842
|
const list = items.map((item) => listItem(item)).join("");
|
|
6675
|
-
return `<h1>${escapeHtml
|
|
7843
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog-tag">${list}</ul>`;
|
|
6676
7844
|
}
|
|
6677
7845
|
function archiveIndexContent(years) {
|
|
6678
|
-
return `<h1>Archive</h1><ul class="ox-blog-archive">${years.map((entry) => `<li><a href="${escapeHtml
|
|
7846
|
+
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>`;
|
|
6679
7847
|
}
|
|
6680
7848
|
function archiveYearContent(year, months, items) {
|
|
6681
|
-
const monthList = months.map((entry) => `<li><a href="${escapeHtml
|
|
7849
|
+
const monthList = months.map((entry) => `<li><a href="${escapeHtml(entry.href)}">${escapeHtml(entry.month)}</a></li>`).join("");
|
|
6682
7850
|
const posts = items.map((item) => listItem(item)).join("");
|
|
6683
|
-
return `<h1>${escapeHtml
|
|
7851
|
+
return `<h1>${escapeHtml(year)}</h1><ul class="ox-blog-archive-months">${monthList}</ul><ul class="ox-blog">${posts}</ul>`;
|
|
6684
7852
|
}
|
|
6685
7853
|
function archiveMonthContent(label, items) {
|
|
6686
7854
|
const list = items.map((item) => listItem(item)).join("");
|
|
6687
|
-
return `<h1>${escapeHtml
|
|
7855
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog">${list}</ul>`;
|
|
6688
7856
|
}
|
|
6689
7857
|
function siteHref$2(base, ...segments) {
|
|
6690
7858
|
const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
@@ -6698,17 +7866,18 @@ function containedPath$1(outDir, ...segments) {
|
|
|
6698
7866
|
if (resolved === root || !resolved.startsWith(prefix)) return;
|
|
6699
7867
|
return resolved;
|
|
6700
7868
|
}
|
|
6701
|
-
function escapeHtml
|
|
7869
|
+
function escapeHtml(value) {
|
|
6702
7870
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6703
7871
|
}
|
|
6704
7872
|
function authorMarkup(author) {
|
|
6705
|
-
const name = escapeHtml
|
|
7873
|
+
const name = escapeHtml(author.name);
|
|
6706
7874
|
const url = author.url?.trim();
|
|
6707
|
-
return `<li>${url && isSafeBlogUrl(url) ? `<a class="ox-blog-meta__name" href="${escapeHtml
|
|
7875
|
+
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>`;
|
|
6708
7876
|
}
|
|
6709
7877
|
function listItem(item) {
|
|
6710
|
-
const time = item.dateLabel ? ` <time datetime="${escapeHtml
|
|
6711
|
-
return `<li><a href="${escapeHtml
|
|
7878
|
+
const time = item.dateLabel ? ` <time datetime="${escapeHtml(item.dateLabel)}">${escapeHtml(item.dateLabel)}</time>` : "";
|
|
7879
|
+
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>`;
|
|
7880
|
+
return `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.title)}</a>${time}</li>`;
|
|
6712
7881
|
}
|
|
6713
7882
|
//#endregion
|
|
6714
7883
|
//#region src/blog-posts.ts
|
|
@@ -6814,7 +7983,8 @@ function toListItem(page) {
|
|
|
6814
7983
|
return {
|
|
6815
7984
|
title: page.title,
|
|
6816
7985
|
href: page.routePaths.href,
|
|
6817
|
-
dateLabel: parsed ? `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}` : void 0
|
|
7986
|
+
dateLabel: parsed ? `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}-${String(parsed.day).padStart(2, "0")}` : void 0,
|
|
7987
|
+
...page.external || page.frontmatter.external === true ? { external: true } : {}
|
|
6818
7988
|
};
|
|
6819
7989
|
}
|
|
6820
7990
|
function resolvePostAuthors(frontmatter, map) {
|
|
@@ -6874,6 +8044,158 @@ function dateField(value) {
|
|
|
6874
8044
|
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
6875
8045
|
}
|
|
6876
8046
|
//#endregion
|
|
8047
|
+
//#region src/blog-feeds.ts
|
|
8048
|
+
var BlogFeedError = class extends Error {
|
|
8049
|
+
issues;
|
|
8050
|
+
constructor(issues) {
|
|
8051
|
+
super(issues.join("\n"));
|
|
8052
|
+
this.name = "BlogFeedError";
|
|
8053
|
+
this.issues = issues;
|
|
8054
|
+
}
|
|
8055
|
+
};
|
|
8056
|
+
async function loadExternalBlogPosts(sources, network = {}) {
|
|
8057
|
+
const pages = [];
|
|
8058
|
+
const warnings = [];
|
|
8059
|
+
const fatals = [];
|
|
8060
|
+
if (sources.length === 0) return {
|
|
8061
|
+
pages,
|
|
8062
|
+
warnings,
|
|
8063
|
+
fatals
|
|
8064
|
+
};
|
|
8065
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
8066
|
+
const results = await Promise.all(sources.map(async (source) => {
|
|
8067
|
+
try {
|
|
8068
|
+
return {
|
|
8069
|
+
source,
|
|
8070
|
+
pages: parseBlogFeed(await cachedBody(source.url, bodies, network), source.language).map((item) => toExternalPage(item, source)).filter((page) => page != null)
|
|
8071
|
+
};
|
|
8072
|
+
} catch (error) {
|
|
8073
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
8074
|
+
return {
|
|
8075
|
+
source,
|
|
8076
|
+
message: `[ox-content] blog feed ${source.url}: ${detail}`
|
|
8077
|
+
};
|
|
8078
|
+
}
|
|
8079
|
+
}));
|
|
8080
|
+
for (const result of results) {
|
|
8081
|
+
if ("pages" in result) {
|
|
8082
|
+
pages.push(...result.pages);
|
|
8083
|
+
continue;
|
|
8084
|
+
}
|
|
8085
|
+
if (result.source.onError === "error") fatals.push(result.message);
|
|
8086
|
+
else warnings.push(result.message);
|
|
8087
|
+
}
|
|
8088
|
+
return {
|
|
8089
|
+
pages,
|
|
8090
|
+
warnings,
|
|
8091
|
+
fatals
|
|
8092
|
+
};
|
|
8093
|
+
}
|
|
8094
|
+
function mergeBlogPosts(local, external) {
|
|
8095
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
8096
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
8097
|
+
const merged = [];
|
|
8098
|
+
for (const page of [...local, ...external]) {
|
|
8099
|
+
const keys = identityKeys(page);
|
|
8100
|
+
if (seenUrls.has(keys.url) || seenIds.has(keys.id)) continue;
|
|
8101
|
+
seenUrls.add(keys.url);
|
|
8102
|
+
seenIds.add(keys.id);
|
|
8103
|
+
merged.push(page);
|
|
8104
|
+
}
|
|
8105
|
+
return sortPosts(merged);
|
|
8106
|
+
}
|
|
8107
|
+
function cachedBody(url, cache, network) {
|
|
8108
|
+
const existing = cache.get(url);
|
|
8109
|
+
if (existing) return existing;
|
|
8110
|
+
const pending = fetchBlogFeedBody(url, network);
|
|
8111
|
+
cache.set(url, pending);
|
|
8112
|
+
return pending;
|
|
8113
|
+
}
|
|
8114
|
+
function toExternalPage(item, source) {
|
|
8115
|
+
const link = canonicalizeFeedItemUrl(item.link);
|
|
8116
|
+
if (!link) return;
|
|
8117
|
+
const id = item.id.trim() || link;
|
|
8118
|
+
const language = item.language ?? source.language;
|
|
8119
|
+
const author = source.author;
|
|
8120
|
+
return {
|
|
8121
|
+
title: item.title,
|
|
8122
|
+
inputPath: `external:${id}`,
|
|
8123
|
+
transformedHtml: "",
|
|
8124
|
+
external: true,
|
|
8125
|
+
routePaths: { href: link },
|
|
8126
|
+
frontmatter: {
|
|
8127
|
+
external: true,
|
|
8128
|
+
id,
|
|
8129
|
+
date: item.date ? feedDateIso(item.date) : void 0,
|
|
8130
|
+
language,
|
|
8131
|
+
author,
|
|
8132
|
+
summary: item.summary
|
|
8133
|
+
}
|
|
8134
|
+
};
|
|
8135
|
+
}
|
|
8136
|
+
function identityKeys(page) {
|
|
8137
|
+
const explicitId = stringField(page.frontmatter.id);
|
|
8138
|
+
const canonical = stringField(page.frontmatter.canonical);
|
|
8139
|
+
const href = page.routePaths.href;
|
|
8140
|
+
const url = canonicalizeFeedItemUrl(canonical ?? "") ?? canonicalizeFeedItemUrl(href) ?? href;
|
|
8141
|
+
return {
|
|
8142
|
+
url,
|
|
8143
|
+
id: explicitId || url
|
|
8144
|
+
};
|
|
8145
|
+
}
|
|
8146
|
+
function stringField(value) {
|
|
8147
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
8148
|
+
}
|
|
8149
|
+
//#endregion
|
|
8150
|
+
//#region src/blog-reading.ts
|
|
8151
|
+
/**
|
|
8152
|
+
* Deterministic blog reading-time estimates.
|
|
8153
|
+
*/
|
|
8154
|
+
const LATIN_WORDS_PER_MINUTE = 200;
|
|
8155
|
+
const CJK_CHARS_PER_MINUTE = 500;
|
|
8156
|
+
function readingTimeMinutes(markdown) {
|
|
8157
|
+
const body = stripInlineCode(stripFences(stripFrontmatter(markdown)));
|
|
8158
|
+
let latin = 0;
|
|
8159
|
+
let cjk = 0;
|
|
8160
|
+
let latinRun = false;
|
|
8161
|
+
for (const char of body) {
|
|
8162
|
+
const code = char.codePointAt(0) ?? 0;
|
|
8163
|
+
if (isCjkCodePoint(code)) {
|
|
8164
|
+
cjk += 1;
|
|
8165
|
+
latinRun = false;
|
|
8166
|
+
continue;
|
|
8167
|
+
}
|
|
8168
|
+
if (isLatinWordChar(code)) {
|
|
8169
|
+
if (!latinRun) {
|
|
8170
|
+
latin += 1;
|
|
8171
|
+
latinRun = true;
|
|
8172
|
+
}
|
|
8173
|
+
continue;
|
|
8174
|
+
}
|
|
8175
|
+
if (char === "'" || char === "’") continue;
|
|
8176
|
+
latinRun = false;
|
|
8177
|
+
}
|
|
8178
|
+
if (latin === 0 && cjk === 0) return 0;
|
|
8179
|
+
return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
|
|
8180
|
+
}
|
|
8181
|
+
function stripFrontmatter(markdown) {
|
|
8182
|
+
if (!markdown.startsWith("---")) return markdown;
|
|
8183
|
+
const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
8184
|
+
return match ? markdown.slice(match[0].length) : markdown;
|
|
8185
|
+
}
|
|
8186
|
+
function stripFences(text) {
|
|
8187
|
+
return text.replace(/```[\s\S]*?(?:```|$)/g, " ");
|
|
8188
|
+
}
|
|
8189
|
+
function stripInlineCode(text) {
|
|
8190
|
+
return text.replace(/`[^`\n]*`/g, " ");
|
|
8191
|
+
}
|
|
8192
|
+
function isCjkCodePoint(code) {
|
|
8193
|
+
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;
|
|
8194
|
+
}
|
|
8195
|
+
function isLatinWordChar(code) {
|
|
8196
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122;
|
|
8197
|
+
}
|
|
8198
|
+
//#endregion
|
|
6877
8199
|
//#region src/blog-pages.ts
|
|
6878
8200
|
/**
|
|
6879
8201
|
* Generated blog index, tag, and archive pages.
|
|
@@ -6885,7 +8207,7 @@ async function injectBlogPostMeta(input) {
|
|
|
6885
8207
|
if (posts === void 0) return;
|
|
6886
8208
|
const listedPaths = new Set(posts.map((page) => page.inputPath));
|
|
6887
8209
|
for (const page of input.pages) {
|
|
6888
|
-
if (!listedPaths.has(page.inputPath)) continue;
|
|
8210
|
+
if (!listedPaths.has(page.inputPath) || page.external === true) continue;
|
|
6889
8211
|
const markdown = await readMarkdown(page.inputPath);
|
|
6890
8212
|
page.transformedHtml = postMetaMarkup({
|
|
6891
8213
|
authors: resolvePostAuthors(page.frontmatter, input.options.authors),
|
|
@@ -6918,8 +8240,9 @@ async function appendBlogPages(input) {
|
|
|
6918
8240
|
input.errors.push(AMBIGUOUS_COLLECTION);
|
|
6919
8241
|
return;
|
|
6920
8242
|
}
|
|
6921
|
-
const
|
|
6922
|
-
if (
|
|
8243
|
+
const local = selectBlogPosts(input.listedPages, input.options, input.srcDir, input.collections);
|
|
8244
|
+
if (local === void 0) return;
|
|
8245
|
+
const posts = await collectIndexPosts(local, input.options, input.errors, input.feedNetwork);
|
|
6923
8246
|
for (const spec of blogPageSpecs(posts, input.options, input.outDir, input.base)) try {
|
|
6924
8247
|
input.generatedPages.push({
|
|
6925
8248
|
inputPath: spec.outputPath,
|
|
@@ -7014,6 +8337,14 @@ function blogPageSpecs(posts, options, outDir, base) {
|
|
|
7014
8337
|
}
|
|
7015
8338
|
return pages;
|
|
7016
8339
|
}
|
|
8340
|
+
async function collectIndexPosts(local, options, errors, network) {
|
|
8341
|
+
if (options.feeds.length === 0) return local;
|
|
8342
|
+
const loaded = await loadExternalBlogPosts(options.feeds, network);
|
|
8343
|
+
errors.push(...loaded.warnings);
|
|
8344
|
+
for (const warning of loaded.warnings) console.warn(warning);
|
|
8345
|
+
if (loaded.fatals.length > 0) throw new BlogFeedError(loaded.fatals);
|
|
8346
|
+
return mergeBlogPosts(local, loaded.pages);
|
|
8347
|
+
}
|
|
7017
8348
|
async function readMarkdown(inputPath) {
|
|
7018
8349
|
try {
|
|
7019
8350
|
return await fs$2.readFile(inputPath, "utf8");
|
|
@@ -7526,59 +8857,6 @@ function generateSearchModule(options, indexPath) {
|
|
|
7526
8857
|
return importNapiModuleSync().generateSearchModuleFromOptions(toLocalSearchRuntimeOptions(options), indexPath);
|
|
7527
8858
|
}
|
|
7528
8859
|
//#endregion
|
|
7529
|
-
//#region src/versions-html.ts
|
|
7530
|
-
function versionSwitcherMarkup(links, badge) {
|
|
7531
|
-
if (links.length === 0) return "";
|
|
7532
|
-
const current = links.find((link) => link.current) ?? links[0];
|
|
7533
|
-
const items = links.map((link) => {
|
|
7534
|
-
const label = `${escapeHtml(link.label)}${badgeMarkup(link, badge)}`;
|
|
7535
|
-
if (link.current || !isSafeHref(link.href)) return `<li><span aria-current="page">${label}</span></li>`;
|
|
7536
|
-
return `<li><a href="${escapeHtml(link.href)}">${label}</a></li>`;
|
|
7537
|
-
}).join("");
|
|
7538
|
-
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>`;
|
|
7539
|
-
}
|
|
7540
|
-
function versionBannerMarkup(kind) {
|
|
7541
|
-
if (kind === "unreleased") return `<aside class="ox-version-banner ox-version-banner--unreleased" role="status">This documentation describes an unreleased version.</aside>`;
|
|
7542
|
-
if (kind === "unmaintained") return `<aside class="ox-version-banner ox-version-banner--unmaintained" role="status">This documentation is unmaintained.</aside>`;
|
|
7543
|
-
return "";
|
|
7544
|
-
}
|
|
7545
|
-
function injectVersionChrome(html, switcher, banner, searchFrom, searchTo) {
|
|
7546
|
-
let next = html;
|
|
7547
|
-
if (banner) next = next.replace(/<body([^>]*)>/, `<body$1>${banner}`);
|
|
7548
|
-
if (switcher) {
|
|
7549
|
-
if (next.includes("<div class=\"header-actions\">")) next = next.replace("<div class=\"header-actions\">", `<div class="header-actions">${switcher}`);
|
|
7550
|
-
else if (next.includes("</header>")) next = next.replace("</header>", `${switcher}</header>`);
|
|
7551
|
-
}
|
|
7552
|
-
if (searchTo && isSafeHref(searchTo)) next = next.replace(/<html([^>]*)>/i, (match, attrs) => {
|
|
7553
|
-
if (/\sdata-ox-search-index=/.test(attrs)) return match;
|
|
7554
|
-
return `<html${attrs} data-ox-search-index="${escapeHtml(searchTo)}">`;
|
|
7555
|
-
});
|
|
7556
|
-
if (searchFrom && searchTo && searchFrom !== searchTo && isSafeHref(searchTo)) {
|
|
7557
|
-
next = next.split(searchFrom).join(searchTo);
|
|
7558
|
-
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>`;
|
|
7559
|
-
next = next.includes("</body>") ? next.replace("</body>", `${script}</body>`) : `${next}${script}`;
|
|
7560
|
-
}
|
|
7561
|
-
return next;
|
|
7562
|
-
}
|
|
7563
|
-
function searchIndexUrl(base, prefix) {
|
|
7564
|
-
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
7565
|
-
return prefix ? `${root}${prefix}/search-index.json` : `${root}search-index.json`;
|
|
7566
|
-
}
|
|
7567
|
-
function isSafeHref(href) {
|
|
7568
|
-
const trimmed = href.trim();
|
|
7569
|
-
if (!trimmed || trimmed.startsWith("//")) return false;
|
|
7570
|
-
const lower = trimmed.replace(/\s+/g, "").toLowerCase();
|
|
7571
|
-
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) return false;
|
|
7572
|
-
return trimmed.startsWith("/") || trimmed.startsWith("./") || !trimmed.includes(":");
|
|
7573
|
-
}
|
|
7574
|
-
function escapeHtml(value) {
|
|
7575
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
7576
|
-
}
|
|
7577
|
-
function badgeMarkup(link, badge) {
|
|
7578
|
-
if (!badge || !link.banner) return "";
|
|
7579
|
-
return `<span class="ox-version-badge">${link.banner === "unreleased" ? "unreleased" : "unmaintained"}</span>`;
|
|
7580
|
-
}
|
|
7581
|
-
//#endregion
|
|
7582
8860
|
//#region src/versions.ts
|
|
7583
8861
|
/**
|
|
7584
8862
|
* Opt-in documentation versioning: prefixes, snapshots, and header chrome.
|
|
@@ -7709,7 +8987,13 @@ async function writeSnapshotSearchIndex(input) {
|
|
|
7709
8987
|
function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
|
|
7710
8988
|
if (!options.enabled) return html;
|
|
7711
8989
|
const active = options.entries.find((entry) => entry.id === activeId);
|
|
7712
|
-
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 ?? ""))
|
|
8990
|
+
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) => ({
|
|
8991
|
+
id: entry.id,
|
|
8992
|
+
label: entry.label,
|
|
8993
|
+
prefix: entry.prefix,
|
|
8994
|
+
indexUrl: searchIndexUrl(base, entry.prefix),
|
|
8995
|
+
current: entry.id === activeId
|
|
8996
|
+
})));
|
|
7713
8997
|
}
|
|
7714
8998
|
function sanitizePrefix(prefix) {
|
|
7715
8999
|
const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
|
|
@@ -7746,21 +9030,182 @@ function normalizeEntries(entries) {
|
|
|
7746
9030
|
banner: normalizeBanner(entry.banner)
|
|
7747
9031
|
});
|
|
7748
9032
|
}
|
|
7749
|
-
return resolved;
|
|
9033
|
+
return resolved;
|
|
9034
|
+
}
|
|
9035
|
+
function normalizeBanner(value) {
|
|
9036
|
+
return value === "unreleased" || value === "unmaintained" ? value : false;
|
|
9037
|
+
}
|
|
9038
|
+
function siteHref$1(base, prefix, rest) {
|
|
9039
|
+
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
9040
|
+
const parts = [prefix, rest].filter((part) => part && part !== "/");
|
|
9041
|
+
return parts.length === 0 ? root : `${root}${parts.join("/")}/`;
|
|
9042
|
+
}
|
|
9043
|
+
function relativeUrl(outputPath, outDir) {
|
|
9044
|
+
const rel = path$1.posix.normalize(path$1.relative(path$1.resolve(outDir), path$1.resolve(outputPath)).replaceAll(path$1.sep, "/"));
|
|
9045
|
+
if (rel.startsWith("..")) return "";
|
|
9046
|
+
const dir = rel.endsWith("/index.html") ? rel.slice(0, -11) : rel.replace(/\.html$/, "");
|
|
9047
|
+
return dir === "." ? "" : dir;
|
|
9048
|
+
}
|
|
9049
|
+
//#endregion
|
|
9050
|
+
//#region src/resources-dedupe.ts
|
|
9051
|
+
/**
|
|
9052
|
+
* Site-wide content-addressed emit for identical page-resource bytes.
|
|
9053
|
+
*
|
|
9054
|
+
* Hashing streams the file. The first digest+extension pair writes once;
|
|
9055
|
+
* later pages reuse that path. Image decode is never used here.
|
|
9056
|
+
*/
|
|
9057
|
+
const DEDUPE_ASSET_DIR = path$1.join("assets", "content");
|
|
9058
|
+
const TRANSFORM_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
9059
|
+
"width",
|
|
9060
|
+
"w",
|
|
9061
|
+
"height",
|
|
9062
|
+
"h",
|
|
9063
|
+
"crop",
|
|
9064
|
+
"format"
|
|
9065
|
+
]);
|
|
9066
|
+
function createResourceDedupeStore() {
|
|
9067
|
+
return {
|
|
9068
|
+
canonical: /* @__PURE__ */ new Map(),
|
|
9069
|
+
hashes: /* @__PURE__ */ new Map()
|
|
9070
|
+
};
|
|
9071
|
+
}
|
|
9072
|
+
function normalizeDedupeExt(ext) {
|
|
9073
|
+
const value = ext.replace(/^\./, "").trim().toLowerCase();
|
|
9074
|
+
if (value === "jpeg") return "jpg";
|
|
9075
|
+
return value || "bin";
|
|
9076
|
+
}
|
|
9077
|
+
function canonicalPublicPath(base, digest, ext) {
|
|
9078
|
+
return `${!base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`}${DEDUPE_ASSET_DIR.split(path$1.sep).join("/")}/${digest}.${ext}`;
|
|
9079
|
+
}
|
|
9080
|
+
function canonicalAbsolutePath(outDir, digest, ext) {
|
|
9081
|
+
return path$1.join(outDir, DEDUPE_ASSET_DIR, `${digest}.${ext}`);
|
|
9082
|
+
}
|
|
9083
|
+
/**
|
|
9084
|
+
* SHA-256 of emitted bytes plus a NUL and the serving extension so the
|
|
9085
|
+
* same payload cannot be served under an incompatible media type.
|
|
9086
|
+
*/
|
|
9087
|
+
async function hashResourceFile(filePath, ext, store, reuseKey) {
|
|
9088
|
+
const cached = store.hashes.get(reuseKey);
|
|
9089
|
+
if (cached) return cached;
|
|
9090
|
+
const hash = createHash("sha256");
|
|
9091
|
+
for await (const chunk of createReadStream(filePath)) hash.update(chunk);
|
|
9092
|
+
hash.update("\0");
|
|
9093
|
+
hash.update(ext);
|
|
9094
|
+
const digest = hash.digest("hex");
|
|
9095
|
+
store.hashes.set(reuseKey, digest);
|
|
9096
|
+
return digest;
|
|
9097
|
+
}
|
|
9098
|
+
async function emitCanonicalResource(store, input) {
|
|
9099
|
+
const key = `${input.digest}\0${input.ext}`;
|
|
9100
|
+
const existing = store.canonical.get(key);
|
|
9101
|
+
const publicPath = canonicalPublicPath(input.base, input.digest, input.ext);
|
|
9102
|
+
if (existing) return {
|
|
9103
|
+
asset: {
|
|
9104
|
+
digest: input.digest,
|
|
9105
|
+
ext: input.ext,
|
|
9106
|
+
absolutePath: existing,
|
|
9107
|
+
publicPath
|
|
9108
|
+
},
|
|
9109
|
+
wrote: false
|
|
9110
|
+
};
|
|
9111
|
+
const absolutePath = canonicalAbsolutePath(input.outDir, input.digest, input.ext);
|
|
9112
|
+
await fs$2.mkdir(path$1.dirname(absolutePath), { recursive: true });
|
|
9113
|
+
await fs$2.copyFile(input.sourcePath, absolutePath);
|
|
9114
|
+
store.canonical.set(key, absolutePath);
|
|
9115
|
+
return {
|
|
9116
|
+
asset: {
|
|
9117
|
+
digest: input.digest,
|
|
9118
|
+
ext: input.ext,
|
|
9119
|
+
absolutePath,
|
|
9120
|
+
publicPath
|
|
9121
|
+
},
|
|
9122
|
+
wrote: true
|
|
9123
|
+
};
|
|
9124
|
+
}
|
|
9125
|
+
/**
|
|
9126
|
+
* Prefer a hard link at the original output path. `link` failure removes
|
|
9127
|
+
* any stale alias and copies so a shared inode is never overwritten.
|
|
9128
|
+
*/
|
|
9129
|
+
async function linkOrCopyAlias(canonical, alias, linker = fs$2.link) {
|
|
9130
|
+
await fs$2.mkdir(path$1.dirname(alias), { recursive: true });
|
|
9131
|
+
try {
|
|
9132
|
+
await linker(canonical, alias);
|
|
9133
|
+
return "link";
|
|
9134
|
+
} catch {
|
|
9135
|
+
await fs$2.rm(alias, { force: true });
|
|
9136
|
+
}
|
|
9137
|
+
try {
|
|
9138
|
+
await linker(canonical, alias);
|
|
9139
|
+
return "link";
|
|
9140
|
+
} catch {
|
|
9141
|
+
await fs$2.copyFile(canonical, alias);
|
|
9142
|
+
return "copy";
|
|
9143
|
+
}
|
|
7750
9144
|
}
|
|
7751
|
-
|
|
7752
|
-
|
|
9145
|
+
/** Keep leftover search/hash; drop consumed transform params. */
|
|
9146
|
+
function rewriteToCanonicalUrl(originalSrc, canonicalPath) {
|
|
9147
|
+
const hashIndex = originalSrc.indexOf("#");
|
|
9148
|
+
const hash = hashIndex === -1 ? "" : originalSrc.slice(hashIndex);
|
|
9149
|
+
const withoutHash = hashIndex === -1 ? originalSrc : originalSrc.slice(0, hashIndex);
|
|
9150
|
+
const queryIndex = withoutHash.indexOf("?");
|
|
9151
|
+
return `${canonicalPath}${leftoverQuery(queryIndex === -1 ? "" : withoutHash.slice(queryIndex + 1))}${hash}`;
|
|
7753
9152
|
}
|
|
7754
|
-
function
|
|
7755
|
-
|
|
7756
|
-
const
|
|
7757
|
-
|
|
9153
|
+
function leftoverQuery(query) {
|
|
9154
|
+
if (!query) return "";
|
|
9155
|
+
const params = new URLSearchParams(query);
|
|
9156
|
+
for (const key of TRANSFORM_QUERY_KEYS) params.delete(key);
|
|
9157
|
+
const next = params.toString();
|
|
9158
|
+
return next ? `?${next}` : "";
|
|
7758
9159
|
}
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
9160
|
+
//#endregion
|
|
9161
|
+
//#region src/resources-html.ts
|
|
9162
|
+
/**
|
|
9163
|
+
* Collect local `src`, `poster`, and relevant `href` values from HTML tags.
|
|
9164
|
+
*/
|
|
9165
|
+
const RESOURCE_TAG = /<(?:img|video|audio|source|track|a)\b[^>]*>/gi;
|
|
9166
|
+
const RESOURCE_ATTR = /\b(src|poster|href)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
9167
|
+
function collectResourceTags(html) {
|
|
9168
|
+
return (html.match(RESOURCE_TAG) ?? []).map((tag) => ({
|
|
9169
|
+
tag,
|
|
9170
|
+
refs: collectResourceRefs(tag)
|
|
9171
|
+
})).filter((entry) => entry.refs.length > 0);
|
|
9172
|
+
}
|
|
9173
|
+
function collectResourceRefs(tag) {
|
|
9174
|
+
const name = /^<([a-z]+)/i.exec(tag)?.[1]?.toLowerCase();
|
|
9175
|
+
if (!name) return [];
|
|
9176
|
+
const refs = [];
|
|
9177
|
+
RESOURCE_ATTR.lastIndex = 0;
|
|
9178
|
+
let match = RESOURCE_ATTR.exec(tag);
|
|
9179
|
+
while (match) {
|
|
9180
|
+
const attr = match[1].toLowerCase();
|
|
9181
|
+
if (isRelevantAttr(name, attr)) {
|
|
9182
|
+
const raw = match[2] ?? match[3] ?? "";
|
|
9183
|
+
refs.push({
|
|
9184
|
+
attr,
|
|
9185
|
+
raw,
|
|
9186
|
+
value: unescapeHtml(raw)
|
|
9187
|
+
});
|
|
9188
|
+
}
|
|
9189
|
+
match = RESOURCE_ATTR.exec(tag);
|
|
9190
|
+
}
|
|
9191
|
+
return refs;
|
|
9192
|
+
}
|
|
9193
|
+
function isRelevantAttr(tagName, attr) {
|
|
9194
|
+
if (tagName === "a") return attr === "href";
|
|
9195
|
+
if (attr === "href") return false;
|
|
9196
|
+
if (attr === "poster") return tagName === "video";
|
|
9197
|
+
return attr === "src";
|
|
9198
|
+
}
|
|
9199
|
+
function unescapeHtml(value) {
|
|
9200
|
+
return value.replaceAll("&", "&").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9201
|
+
}
|
|
9202
|
+
function escapeAttribute(value) {
|
|
9203
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9204
|
+
}
|
|
9205
|
+
function replaceAttributeRaw(tag, raw, nextRaw) {
|
|
9206
|
+
const index = tag.indexOf(raw);
|
|
9207
|
+
if (index === -1) return tag;
|
|
9208
|
+
return tag.slice(0, index) + nextRaw + tag.slice(index + raw.length);
|
|
7764
9209
|
}
|
|
7765
9210
|
//#endregion
|
|
7766
9211
|
//#region src/resources-jpeg.ts
|
|
@@ -8803,129 +10248,23 @@ function coverCrop(image, width, height) {
|
|
|
8803
10248
|
return cropImage(scaled, Math.max(0, Math.floor((scaled.width - width) / 2)), Math.max(0, Math.floor((scaled.height - height) / 2)), width, height);
|
|
8804
10249
|
}
|
|
8805
10250
|
//#endregion
|
|
8806
|
-
//#region src/resources-
|
|
10251
|
+
//#region src/resources-write.ts
|
|
8807
10252
|
/**
|
|
8808
|
-
*
|
|
10253
|
+
* Transform cache writes for page resources.
|
|
8809
10254
|
*/
|
|
8810
|
-
|
|
8811
|
-
const SRC_ATTR = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
|
|
8812
|
-
async function processPageResources(input) {
|
|
8813
|
-
if (!input.options.enabled) return {
|
|
8814
|
-
html: input.html,
|
|
8815
|
-
files: [],
|
|
8816
|
-
errors: [],
|
|
8817
|
-
fatal: []
|
|
8818
|
-
};
|
|
8819
|
-
const bundleRoot = path$1.dirname(input.inputPath);
|
|
8820
|
-
const outputDir = path$1.dirname(input.outputPath);
|
|
8821
|
-
const files = [];
|
|
8822
|
-
const errors = [];
|
|
8823
|
-
const fatal = [];
|
|
8824
|
-
let html = input.html;
|
|
8825
|
-
const tags = input.html.match(IMG_TAG) ?? [];
|
|
8826
|
-
for (const tag of tags) {
|
|
8827
|
-
const srcMatch = tag.match(SRC_ATTR);
|
|
8828
|
-
const rawSrc = srcMatch?.[1] ?? srcMatch?.[2];
|
|
8829
|
-
if (!rawSrc) continue;
|
|
8830
|
-
const src = unescapeHtml(rawSrc);
|
|
8831
|
-
const parsed = parseResourceSrc(src);
|
|
8832
|
-
if (!parsed) continue;
|
|
8833
|
-
const resolved = resolveBundlePath(parsed.pathname, bundleRoot, input.srcDir);
|
|
8834
|
-
if (!resolved.ok) {
|
|
8835
|
-
const message = `[ox-content] page resource ${JSON.stringify(src)} on ${input.inputPath} is outside the page bundle`;
|
|
8836
|
-
errors.push(message);
|
|
8837
|
-
fatal.push(message);
|
|
8838
|
-
continue;
|
|
8839
|
-
}
|
|
8840
|
-
let stat;
|
|
8841
|
-
try {
|
|
8842
|
-
stat = await fs$2.stat(resolved.absolute);
|
|
8843
|
-
} catch {
|
|
8844
|
-
const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
|
|
8845
|
-
errors.push(message);
|
|
8846
|
-
if (input.options.missing === "error") fatal.push(message);
|
|
8847
|
-
continue;
|
|
8848
|
-
}
|
|
8849
|
-
const transformError = validateTransform(parsed.transform, input.options);
|
|
8850
|
-
if (transformError) {
|
|
8851
|
-
const message = `[ox-content] ${transformError} for ${JSON.stringify(src)} on ${input.inputPath}`;
|
|
8852
|
-
errors.push(message);
|
|
8853
|
-
fatal.push(message);
|
|
8854
|
-
continue;
|
|
8855
|
-
}
|
|
8856
|
-
const hasTransform = hasPixelOrFormatTransform(parsed.transform);
|
|
8857
|
-
const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : path$1.basename(resolved.absolute);
|
|
8858
|
-
const outputFile = path$1.join(outputDir, outputName);
|
|
8859
|
-
try {
|
|
8860
|
-
if (hasTransform) await writeTransformedResource({
|
|
8861
|
-
sourcePath: resolved.absolute,
|
|
8862
|
-
outputFile,
|
|
8863
|
-
cacheDir: input.cacheDir,
|
|
8864
|
-
mtimeMs: stat.mtimeMs,
|
|
8865
|
-
transform: parsed.transform
|
|
8866
|
-
});
|
|
8867
|
-
else {
|
|
8868
|
-
await fs$2.mkdir(outputDir, { recursive: true });
|
|
8869
|
-
await fs$2.copyFile(resolved.absolute, outputFile);
|
|
8870
|
-
}
|
|
8871
|
-
files.push(outputFile);
|
|
8872
|
-
const rewritten = tag.replace(rawSrc, escapeAttribute(outputName));
|
|
8873
|
-
html = html.replace(tag, rewritten);
|
|
8874
|
-
} catch (error) {
|
|
8875
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
8876
|
-
const message = `[ox-content] failed to process page resource ${JSON.stringify(src)} on ${input.inputPath}: ${detail}`;
|
|
8877
|
-
errors.push(message);
|
|
8878
|
-
fatal.push(message);
|
|
8879
|
-
}
|
|
8880
|
-
}
|
|
8881
|
-
return {
|
|
8882
|
-
html,
|
|
8883
|
-
files,
|
|
8884
|
-
errors,
|
|
8885
|
-
fatal
|
|
8886
|
-
};
|
|
8887
|
-
}
|
|
8888
|
-
function resolveBundlePath(pathname, bundleRoot, contentRoot) {
|
|
8889
|
-
if (path$1.isAbsolute(pathname) || pathname.includes("\0")) return { ok: false };
|
|
8890
|
-
const absolute = path$1.resolve(bundleRoot, pathname);
|
|
8891
|
-
if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
|
|
8892
|
-
return {
|
|
8893
|
-
ok: true,
|
|
8894
|
-
absolute
|
|
8895
|
-
};
|
|
8896
|
-
}
|
|
8897
|
-
function validateTransform(transform, options) {
|
|
8898
|
-
if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
|
|
8899
|
-
if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
|
|
8900
|
-
}
|
|
8901
|
-
function hasPixelOrFormatTransform(transform) {
|
|
8902
|
-
return Boolean(transform.width || transform.height || transform.crop || transform.format);
|
|
8903
|
-
}
|
|
8904
|
-
function transformedFileName(pathname, transform, cacheKey) {
|
|
8905
|
-
const stem = path$1.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
|
|
8906
|
-
const ext = outputExtension(pathname, transform.format);
|
|
8907
|
-
return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
|
|
8908
|
-
}
|
|
8909
|
-
function outputExtension(pathname, format) {
|
|
8910
|
-
if (format === "jpeg") return "jpg";
|
|
8911
|
-
if (format) return format;
|
|
8912
|
-
const ext = path$1.extname(pathname).slice(1).toLowerCase();
|
|
8913
|
-
return ext === "jpeg" ? "jpg" : ext || "png";
|
|
8914
|
-
}
|
|
8915
|
-
async function writeTransformedResource(input) {
|
|
10255
|
+
async function ensureTransformedCache(input) {
|
|
8916
10256
|
const key = resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform);
|
|
8917
10257
|
const ext = path$1.extname(input.outputFile);
|
|
8918
10258
|
const cacheFile = path$1.join(input.cacheDir, `${key}${ext}`);
|
|
8919
10259
|
try {
|
|
8920
|
-
await fs$2.
|
|
8921
|
-
return;
|
|
10260
|
+
await fs$2.access(cacheFile);
|
|
10261
|
+
return cacheFile;
|
|
8922
10262
|
} catch {}
|
|
8923
10263
|
const output = transformResourceBuffer(await fs$2.readFile(input.sourcePath), input.sourcePath, input.transform);
|
|
8924
10264
|
if (output.length > 8388608) throw new Error("transform produced an oversized file");
|
|
8925
|
-
await fs$2.mkdir(path$1.dirname(input.outputFile), { recursive: true });
|
|
8926
10265
|
await fs$2.mkdir(input.cacheDir, { recursive: true });
|
|
8927
10266
|
await fs$2.writeFile(cacheFile, output);
|
|
8928
|
-
|
|
10267
|
+
return cacheFile;
|
|
8929
10268
|
}
|
|
8930
10269
|
function transformResourceBuffer(source, sourcePath, transform) {
|
|
8931
10270
|
const needsPixels = Boolean(transform.width || transform.height || transform.crop);
|
|
@@ -8968,11 +10307,221 @@ function formatFromPath(filePath) {
|
|
|
8968
10307
|
const ext = path$1.extname(filePath).slice(1).toLowerCase();
|
|
8969
10308
|
return ext === "jpg" ? "jpeg" : ext;
|
|
8970
10309
|
}
|
|
8971
|
-
|
|
8972
|
-
|
|
10310
|
+
//#endregion
|
|
10311
|
+
//#region src/resources-process.ts
|
|
10312
|
+
/**
|
|
10313
|
+
* Page-resource HTML rewriting and transform writes.
|
|
10314
|
+
*/
|
|
10315
|
+
const PAGE_EXTS = /* @__PURE__ */ new Set([
|
|
10316
|
+
".md",
|
|
10317
|
+
".markdown",
|
|
10318
|
+
".mdx",
|
|
10319
|
+
".html",
|
|
10320
|
+
".htm"
|
|
10321
|
+
]);
|
|
10322
|
+
async function processPageResources(input) {
|
|
10323
|
+
if (!input.options.enabled) return {
|
|
10324
|
+
html: input.html,
|
|
10325
|
+
files: [],
|
|
10326
|
+
errors: [],
|
|
10327
|
+
fatal: []
|
|
10328
|
+
};
|
|
10329
|
+
if (input.options.dedupe && !input.outDir) {
|
|
10330
|
+
const message = "[ox-content] resources.dedupe requires outDir";
|
|
10331
|
+
return {
|
|
10332
|
+
html: input.html,
|
|
10333
|
+
files: [],
|
|
10334
|
+
errors: [message],
|
|
10335
|
+
fatal: [message]
|
|
10336
|
+
};
|
|
10337
|
+
}
|
|
10338
|
+
const bundleRoot = path$1.dirname(input.inputPath);
|
|
10339
|
+
const outputDir = path$1.dirname(input.outputPath);
|
|
10340
|
+
const files = [];
|
|
10341
|
+
const errors = [];
|
|
10342
|
+
const fatal = [];
|
|
10343
|
+
let html = input.html;
|
|
10344
|
+
const store = input.options.dedupe ? input.dedupeStore ?? createResourceDedupeStore() : void 0;
|
|
10345
|
+
for (const { tag, refs } of collectResourceTags(input.html)) {
|
|
10346
|
+
let nextTag = tag;
|
|
10347
|
+
for (const ref of refs) {
|
|
10348
|
+
const result = await processResourceRef(input, {
|
|
10349
|
+
bundleRoot,
|
|
10350
|
+
outputDir,
|
|
10351
|
+
ref: ref.attr,
|
|
10352
|
+
src: ref.value,
|
|
10353
|
+
store
|
|
10354
|
+
});
|
|
10355
|
+
errors.push(...result.errors);
|
|
10356
|
+
fatal.push(...result.fatal);
|
|
10357
|
+
files.push(...result.files);
|
|
10358
|
+
if (result.rewrite) nextTag = replaceAttributeRaw(nextTag, ref.raw, escapeAttribute(result.rewrite));
|
|
10359
|
+
}
|
|
10360
|
+
if (nextTag !== tag) html = html.replace(tag, nextTag);
|
|
10361
|
+
}
|
|
10362
|
+
return {
|
|
10363
|
+
html,
|
|
10364
|
+
files,
|
|
10365
|
+
errors,
|
|
10366
|
+
fatal
|
|
10367
|
+
};
|
|
8973
10368
|
}
|
|
8974
|
-
function
|
|
8975
|
-
|
|
10369
|
+
async function processResourceRef(input, ctx) {
|
|
10370
|
+
const parsed = parseResourceSrc(ctx.src);
|
|
10371
|
+
if (!parsed) return {
|
|
10372
|
+
files: [],
|
|
10373
|
+
errors: [],
|
|
10374
|
+
fatal: []
|
|
10375
|
+
};
|
|
10376
|
+
const resolved = resolveBundlePath(parsed.pathname, ctx.bundleRoot, input.srcDir);
|
|
10377
|
+
if (!resolved.ok) {
|
|
10378
|
+
if (ctx.ref === "href") return {
|
|
10379
|
+
files: [],
|
|
10380
|
+
errors: [],
|
|
10381
|
+
fatal: []
|
|
10382
|
+
};
|
|
10383
|
+
const message = `[ox-content] page resource ${JSON.stringify(ctx.src)} on ${input.inputPath} is outside the page bundle`;
|
|
10384
|
+
return {
|
|
10385
|
+
files: [],
|
|
10386
|
+
errors: [message],
|
|
10387
|
+
fatal: [message]
|
|
10388
|
+
};
|
|
10389
|
+
}
|
|
10390
|
+
let stat;
|
|
10391
|
+
try {
|
|
10392
|
+
stat = await fs$2.stat(resolved.absolute);
|
|
10393
|
+
} catch {
|
|
10394
|
+
if (ctx.ref === "href") return {
|
|
10395
|
+
files: [],
|
|
10396
|
+
errors: [],
|
|
10397
|
+
fatal: []
|
|
10398
|
+
};
|
|
10399
|
+
const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
|
|
10400
|
+
return {
|
|
10401
|
+
files: [],
|
|
10402
|
+
errors: [message],
|
|
10403
|
+
fatal: input.options.missing === "error" ? [message] : []
|
|
10404
|
+
};
|
|
10405
|
+
}
|
|
10406
|
+
const hrefToPage = ctx.ref === "href" && PAGE_EXTS.has(path$1.extname(resolved.absolute).toLowerCase());
|
|
10407
|
+
if (!stat.isFile() || hrefToPage) return {
|
|
10408
|
+
files: [],
|
|
10409
|
+
errors: [],
|
|
10410
|
+
fatal: []
|
|
10411
|
+
};
|
|
10412
|
+
const transformError = validateTransform(parsed.transform, input.options);
|
|
10413
|
+
if (transformError) {
|
|
10414
|
+
const message = `[ox-content] ${transformError} for ${JSON.stringify(ctx.src)} on ${input.inputPath}`;
|
|
10415
|
+
return {
|
|
10416
|
+
files: [],
|
|
10417
|
+
errors: [message],
|
|
10418
|
+
fatal: [message]
|
|
10419
|
+
};
|
|
10420
|
+
}
|
|
10421
|
+
const hasTransform = hasPixelOrFormatTransform(parsed.transform);
|
|
10422
|
+
const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : decodedBasename(parsed.pathname) || path$1.basename(resolved.absolute);
|
|
10423
|
+
const outputFile = path$1.join(ctx.outputDir, outputName);
|
|
10424
|
+
try {
|
|
10425
|
+
const materialized = hasTransform ? await ensureTransformedCache({
|
|
10426
|
+
sourcePath: resolved.absolute,
|
|
10427
|
+
outputFile,
|
|
10428
|
+
cacheDir: input.cacheDir,
|
|
10429
|
+
mtimeMs: stat.mtimeMs,
|
|
10430
|
+
transform: parsed.transform
|
|
10431
|
+
}) : resolved.absolute;
|
|
10432
|
+
if (ctx.store && input.outDir) return await emitDedupedResource({
|
|
10433
|
+
store: ctx.store,
|
|
10434
|
+
materialized,
|
|
10435
|
+
outputFile,
|
|
10436
|
+
src: ctx.src,
|
|
10437
|
+
sourcePath: resolved.absolute,
|
|
10438
|
+
mtimeMs: stat.mtimeMs,
|
|
10439
|
+
transform: parsed.transform,
|
|
10440
|
+
hasTransform,
|
|
10441
|
+
outDir: input.outDir,
|
|
10442
|
+
base: input.base ?? "/"
|
|
10443
|
+
});
|
|
10444
|
+
if (hasTransform) {
|
|
10445
|
+
await fs$2.mkdir(ctx.outputDir, { recursive: true });
|
|
10446
|
+
await fs$2.copyFile(materialized, outputFile);
|
|
10447
|
+
} else {
|
|
10448
|
+
await fs$2.mkdir(ctx.outputDir, { recursive: true });
|
|
10449
|
+
await fs$2.copyFile(resolved.absolute, outputFile);
|
|
10450
|
+
}
|
|
10451
|
+
return {
|
|
10452
|
+
files: [outputFile],
|
|
10453
|
+
errors: [],
|
|
10454
|
+
fatal: [],
|
|
10455
|
+
rewrite: outputName
|
|
10456
|
+
};
|
|
10457
|
+
} catch (error) {
|
|
10458
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10459
|
+
const message = `[ox-content] failed to process page resource ${JSON.stringify(ctx.src)} on ${input.inputPath}: ${detail}`;
|
|
10460
|
+
return {
|
|
10461
|
+
files: [],
|
|
10462
|
+
errors: [message],
|
|
10463
|
+
fatal: [message]
|
|
10464
|
+
};
|
|
10465
|
+
}
|
|
10466
|
+
}
|
|
10467
|
+
async function emitDedupedResource(input) {
|
|
10468
|
+
const ext = normalizeDedupeExt(path$1.extname(input.outputFile));
|
|
10469
|
+
const reuseKey = input.hasTransform ? resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform) : `${input.sourcePath}\0${input.mtimeMs}\0copy`;
|
|
10470
|
+
const digest = await hashResourceFile(input.materialized, ext, input.store, reuseKey);
|
|
10471
|
+
const { asset, wrote } = await emitCanonicalResource(input.store, {
|
|
10472
|
+
digest,
|
|
10473
|
+
ext,
|
|
10474
|
+
sourcePath: input.materialized,
|
|
10475
|
+
outDir: input.outDir,
|
|
10476
|
+
base: input.base
|
|
10477
|
+
});
|
|
10478
|
+
await linkOrCopyAlias(asset.absolutePath, input.outputFile);
|
|
10479
|
+
return {
|
|
10480
|
+
files: wrote ? [asset.absolutePath, input.outputFile] : [input.outputFile],
|
|
10481
|
+
errors: [],
|
|
10482
|
+
fatal: [],
|
|
10483
|
+
rewrite: rewriteToCanonicalUrl(input.src, asset.publicPath)
|
|
10484
|
+
};
|
|
10485
|
+
}
|
|
10486
|
+
function resolveBundlePath(pathname, bundleRoot, contentRoot) {
|
|
10487
|
+
let decoded;
|
|
10488
|
+
try {
|
|
10489
|
+
decoded = decodeURIComponent(pathname);
|
|
10490
|
+
} catch {
|
|
10491
|
+
decoded = pathname;
|
|
10492
|
+
}
|
|
10493
|
+
if (path$1.isAbsolute(decoded) || decoded.includes("\0")) return { ok: false };
|
|
10494
|
+
const absolute = path$1.resolve(bundleRoot, decoded);
|
|
10495
|
+
if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
|
|
10496
|
+
return {
|
|
10497
|
+
ok: true,
|
|
10498
|
+
absolute
|
|
10499
|
+
};
|
|
10500
|
+
}
|
|
10501
|
+
function validateTransform(transform, options) {
|
|
10502
|
+
if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
|
|
10503
|
+
if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
|
|
10504
|
+
}
|
|
10505
|
+
function hasPixelOrFormatTransform(transform) {
|
|
10506
|
+
return Boolean(transform.width || transform.height || transform.crop || transform.format);
|
|
10507
|
+
}
|
|
10508
|
+
function transformedFileName(pathname, transform, cacheKey) {
|
|
10509
|
+
const stem = path$1.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
|
|
10510
|
+
const ext = outputExtension(pathname, transform.format);
|
|
10511
|
+
return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
|
|
10512
|
+
}
|
|
10513
|
+
function outputExtension(pathname, format) {
|
|
10514
|
+
if (format === "jpeg") return "jpg";
|
|
10515
|
+
if (format) return format;
|
|
10516
|
+
const ext = path$1.extname(pathname).slice(1).toLowerCase();
|
|
10517
|
+
return ext === "jpeg" ? "jpg" : ext || "png";
|
|
10518
|
+
}
|
|
10519
|
+
function decodedBasename(pathname) {
|
|
10520
|
+
try {
|
|
10521
|
+
return path$1.basename(decodeURIComponent(pathname));
|
|
10522
|
+
} catch {
|
|
10523
|
+
return path$1.basename(pathname);
|
|
10524
|
+
}
|
|
8976
10525
|
}
|
|
8977
10526
|
//#endregion
|
|
8978
10527
|
//#region src/resources.ts
|
|
@@ -9007,19 +10556,22 @@ function resolveResourcesOptions(value) {
|
|
|
9007
10556
|
enabled: false,
|
|
9008
10557
|
formats: [...DEFAULT_FORMATS],
|
|
9009
10558
|
widths: [],
|
|
9010
|
-
missing: "error"
|
|
10559
|
+
missing: "error",
|
|
10560
|
+
dedupe: false
|
|
9011
10561
|
};
|
|
9012
10562
|
if (value === true) return {
|
|
9013
10563
|
enabled: true,
|
|
9014
10564
|
formats: [...DEFAULT_FORMATS],
|
|
9015
10565
|
widths: [],
|
|
9016
|
-
missing: "error"
|
|
10566
|
+
missing: "error",
|
|
10567
|
+
dedupe: false
|
|
9017
10568
|
};
|
|
9018
10569
|
return {
|
|
9019
10570
|
enabled: true,
|
|
9020
10571
|
formats: normalizeFormats(value.formats),
|
|
9021
10572
|
widths: normalizeWidths(value.widths),
|
|
9022
|
-
missing: value.missing === "warn" ? "warn" : "error"
|
|
10573
|
+
missing: value.missing === "warn" ? "warn" : "error",
|
|
10574
|
+
dedupe: value.dedupe === true
|
|
9023
10575
|
};
|
|
9024
10576
|
}
|
|
9025
10577
|
/**
|
|
@@ -9270,6 +10822,7 @@ function resolveSsgOptions(ssg) {
|
|
|
9270
10822
|
localeSwitcher: false,
|
|
9271
10823
|
a11y: false,
|
|
9272
10824
|
pageChrome: false,
|
|
10825
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9273
10826
|
notFound: resolveNotFoundOptions(void 0),
|
|
9274
10827
|
team: resolveTeamOptions(void 0),
|
|
9275
10828
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9291,6 +10844,7 @@ function resolveSsgOptions(ssg) {
|
|
|
9291
10844
|
localeSwitcher: false,
|
|
9292
10845
|
a11y: false,
|
|
9293
10846
|
pageChrome: false,
|
|
10847
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9294
10848
|
notFound: resolveNotFoundOptions(void 0),
|
|
9295
10849
|
team: resolveTeamOptions(void 0),
|
|
9296
10850
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9320,6 +10874,7 @@ function resolveSsgOptions(ssg) {
|
|
|
9320
10874
|
localeSwitcher: resolveLocaleSwitcherOption(ssg.localeSwitcher),
|
|
9321
10875
|
a11y: resolveA11yOption(ssg.a11y),
|
|
9322
10876
|
pageChrome: resolvePageChromeOption(ssg.pageChrome),
|
|
10877
|
+
markdownSource: resolveMarkdownSourceOptions(ssg.markdownSource),
|
|
9323
10878
|
notFound: resolveNotFoundOptions(ssg.notFound),
|
|
9324
10879
|
team: resolveTeamOptions(ssg.team),
|
|
9325
10880
|
blog: resolveBlogOptions(ssg.blog),
|
|
@@ -9492,7 +11047,7 @@ function localeCodesFor(locales) {
|
|
|
9492
11047
|
async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales, pagination = false, readerChrome = false, breadcrumbs = false, localeSwitcher = false, localePaths, a11y = false, team = {
|
|
9493
11048
|
enabled: false,
|
|
9494
11049
|
members: []
|
|
9495
|
-
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl, headValidation = false) {
|
|
11050
|
+
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl, headValidation = false, defaultLocale) {
|
|
9496
11051
|
const mod = await importNapiModule();
|
|
9497
11052
|
const tocForRust = pageData.toc.map(toRustTocEntry);
|
|
9498
11053
|
const navGroupsForRust = convertNavGroupsForRust(navGroups);
|
|
@@ -9576,7 +11131,11 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9576
11131
|
});
|
|
9577
11132
|
const html = typeof result === "string" ? result : result.html;
|
|
9578
11133
|
reportHeadDiagnostics(typeof result === "string" ? [] : result.diagnostics ?? [], headValidation);
|
|
9579
|
-
return html
|
|
11134
|
+
return injectSearchLocaleFilters(html, {
|
|
11135
|
+
locales: availableLocales ?? [],
|
|
11136
|
+
current: locale,
|
|
11137
|
+
defaultLocale: defaultLocale ?? availableLocales?.[0]?.code ?? "en"
|
|
11138
|
+
});
|
|
9580
11139
|
}
|
|
9581
11140
|
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
9582
11141
|
const optimized = (await importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
@@ -9661,6 +11220,7 @@ async function buildSsg(options, root) {
|
|
|
9661
11220
|
applyPermalinkRoutes(context, collected);
|
|
9662
11221
|
errors.push(...collected.errors);
|
|
9663
11222
|
const { outputPages, listedPages } = applyPublishState(context, collected);
|
|
11223
|
+
context.markdownSourcePages.push(...outputPages);
|
|
9664
11224
|
remapPermalinkNav(context, listedPages);
|
|
9665
11225
|
await applyPageResources(context, outputPages, generatedFiles, errors);
|
|
9666
11226
|
await generateOgImageAssets(context, collected, generatedFiles, errors);
|
|
@@ -9738,7 +11298,8 @@ async function createBuildSsgContext(options, root, srcDir, outDir, markdownFile
|
|
|
9738
11298
|
navItems: resolveNavigationGroups(ssgOptions.navigation, base, ssgOptions.extension) ?? (ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension)),
|
|
9739
11299
|
siteName: await resolveSiteName$1(root, ssgOptions),
|
|
9740
11300
|
shouldGenerateOgImages: shouldGenerateOgImages(options),
|
|
9741
|
-
|
|
11301
|
+
markdownSourcePages: [],
|
|
11302
|
+
napi: ssgOptions.lastUpdated || ssgOptions.contributors || options.siteMaps?.enabled ? await importNapiModule() : void 0
|
|
9742
11303
|
};
|
|
9743
11304
|
}
|
|
9744
11305
|
/**
|
|
@@ -9768,6 +11329,7 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9768
11329
|
if (!options?.enabled) return;
|
|
9769
11330
|
const cacheDir = path$2.join(context.root, ".cache", "ox-content-resources");
|
|
9770
11331
|
const fatal = [];
|
|
11332
|
+
const dedupeStore = options.dedupe ? createResourceDedupeStore() : void 0;
|
|
9771
11333
|
for (const page of pages) {
|
|
9772
11334
|
const processed = await processPageResources({
|
|
9773
11335
|
html: page.transformedHtml,
|
|
@@ -9775,7 +11337,10 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9775
11337
|
outputPath: page.routePaths.outputPath,
|
|
9776
11338
|
srcDir: context.srcDir,
|
|
9777
11339
|
options,
|
|
9778
|
-
cacheDir
|
|
11340
|
+
cacheDir,
|
|
11341
|
+
outDir: context.outDir,
|
|
11342
|
+
base: context.base,
|
|
11343
|
+
dedupeStore
|
|
9779
11344
|
});
|
|
9780
11345
|
page.transformedHtml = processed.html;
|
|
9781
11346
|
generatedFiles.push(...processed.files);
|
|
@@ -9849,7 +11414,8 @@ function applyPublishState(context, collected) {
|
|
|
9849
11414
|
};
|
|
9850
11415
|
}
|
|
9851
11416
|
async function transformSsgPage(context, inputPath) {
|
|
9852
|
-
const
|
|
11417
|
+
const content = await fs$3.readFile(inputPath, "utf-8");
|
|
11418
|
+
const result = await transformMarkdown(content, inputPath, context.options, {
|
|
9853
11419
|
convertMdLinks: true,
|
|
9854
11420
|
baseUrl: context.base,
|
|
9855
11421
|
sourcePath: inputPath
|
|
@@ -9859,11 +11425,12 @@ async function transformSsgPage(context, inputPath) {
|
|
|
9859
11425
|
const title = extractTitle$1(transformedHtml, frontmatter);
|
|
9860
11426
|
return {
|
|
9861
11427
|
inputPath,
|
|
11428
|
+
source: content,
|
|
9862
11429
|
routePaths: getRoutePaths(inputPath, context.srcDir, context.outDir, context.base, context.ssgOptions.extension, context.ssgOptions.siteUrl),
|
|
9863
11430
|
transformedHtml,
|
|
9864
11431
|
title,
|
|
9865
11432
|
description: frontmatter.description,
|
|
9866
|
-
lastUpdated: context.ssgOptions.lastUpdated ? context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0 : void 0,
|
|
11433
|
+
lastUpdated: context.ssgOptions.lastUpdated || context.options.siteMaps?.enabled ? context.napi?.getGitLastUpdated(inputPath, context.root) ?? void 0 : void 0,
|
|
9867
11434
|
contributors: contributorsForPage(context, inputPath),
|
|
9868
11435
|
frontmatter,
|
|
9869
11436
|
toc: result.toc
|
|
@@ -9954,17 +11521,18 @@ async function generateHtmlPages(context, pageResults, collected, errors) {
|
|
|
9954
11521
|
async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
9955
11522
|
const { ogImageUrlMap } = collected;
|
|
9956
11523
|
const pageOgImage = context.shouldGenerateOgImages && ogImageUrlMap.has(pageResult.inputPath) ? ogImageUrlMap.get(pageResult.inputPath) : context.ssgOptions.ogImage;
|
|
11524
|
+
const markdownSource = pageMarkdownSourceHref(context, pageResult);
|
|
9957
11525
|
if (context.ssgOptions.render) {
|
|
9958
11526
|
const nav = context.versionNavigation ? rewriteVersionedNavGroups(context.navItems, context.versionNavigation) : context.navItems;
|
|
9959
|
-
return renderPage(toThemePageData(pageResult), {
|
|
11527
|
+
return applyMarkdownSourceAlternate(context, renderPage(toThemePageData(pageResult, markdownSource), {
|
|
9960
11528
|
theme: context.ssgOptions.render,
|
|
9961
11529
|
siteName: context.siteName,
|
|
9962
11530
|
base: context.base,
|
|
9963
11531
|
nav,
|
|
9964
|
-
pages: allPageResults.map(toThemePageData)
|
|
9965
|
-
});
|
|
11532
|
+
pages: allPageResults.map((page) => toThemePageData(page, pageMarkdownSourceHref(context, page)))
|
|
11533
|
+
}), markdownSource);
|
|
9966
11534
|
}
|
|
9967
|
-
if (context.ssgOptions.bare) return generateBarePage({
|
|
11535
|
+
if (context.ssgOptions.bare) return applyMarkdownSourceAlternate(context, generateBarePage({
|
|
9968
11536
|
title: pageResult.title,
|
|
9969
11537
|
content: pageResult.transformedHtml,
|
|
9970
11538
|
lang: context.ssgOptions.lang ?? getPageLocale(pageResult.routePaths.urlPath, context.options.i18n),
|
|
@@ -9975,7 +11543,7 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
9975
11543
|
head: context.ssgOptions.head,
|
|
9976
11544
|
bodyStart: context.ssgOptions.bodyStart,
|
|
9977
11545
|
bodyEnd: context.ssgOptions.bodyEnd
|
|
9978
|
-
});
|
|
11546
|
+
}), markdownSource);
|
|
9979
11547
|
const pageData = createSsgPageData(pageResult);
|
|
9980
11548
|
const versionNavigation = context.versionNavigation;
|
|
9981
11549
|
if (versionNavigation) {
|
|
@@ -10016,10 +11584,18 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
10016
11584
|
base: context.base,
|
|
10017
11585
|
roots: versionNavigation ? versionedLocaleRoots(versionNavigation, i18n.locales, i18n.defaultLocale, i18n.hideDefaultLocale) : void 0
|
|
10018
11586
|
}) : void 0;
|
|
10019
|
-
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 ?? {
|
|
11587
|
+
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 ?? {
|
|
10020
11588
|
enabled: false,
|
|
10021
11589
|
members: []
|
|
10022
|
-
}, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl, context.ssgOptions.headValidation);
|
|
11590
|
+
}, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl, context.ssgOptions.headValidation, i18n?.defaultLocale), markdownSource);
|
|
11591
|
+
}
|
|
11592
|
+
function pageMarkdownSourceHref(context, page) {
|
|
11593
|
+
if (!context.ssgOptions.markdownSource?.enabled || !shouldPublishMarkdownSource(page.frontmatter, context.options.publishState)) return;
|
|
11594
|
+
return markdownSourceHref(page.routePaths.urlPath, context.base);
|
|
11595
|
+
}
|
|
11596
|
+
function applyMarkdownSourceAlternate(context, html, href) {
|
|
11597
|
+
if (!href || !context.ssgOptions.markdownSource?.alternate) return html;
|
|
11598
|
+
return injectMarkdownSourceAlternate(html, href);
|
|
10023
11599
|
}
|
|
10024
11600
|
function rewritePagerOverride(pager, context) {
|
|
10025
11601
|
return pager?.href ? {
|
|
@@ -10028,7 +11604,7 @@ function rewritePagerOverride(pager, context) {
|
|
|
10028
11604
|
} : pager;
|
|
10029
11605
|
}
|
|
10030
11606
|
/** Maps an internal page result onto the theme renderer's page shape. */
|
|
10031
|
-
function toThemePageData(pageResult) {
|
|
11607
|
+
function toThemePageData(pageResult, markdownSource) {
|
|
10032
11608
|
return {
|
|
10033
11609
|
title: pageResult.title,
|
|
10034
11610
|
description: pageResult.description,
|
|
@@ -10038,6 +11614,7 @@ function toThemePageData(pageResult) {
|
|
|
10038
11614
|
contributors: pageResult.contributors,
|
|
10039
11615
|
path: pageResult.inputPath,
|
|
10040
11616
|
url: pageResult.routePaths.href,
|
|
11617
|
+
markdownSource,
|
|
10041
11618
|
frontmatter: pageResult.frontmatter,
|
|
10042
11619
|
layout: typeof pageResult.frontmatter.layout === "string" ? pageResult.frontmatter.layout : void 0
|
|
10043
11620
|
};
|
|
@@ -10150,6 +11727,7 @@ async function applyDocumentationVersions(generatedPages, context, errors) {
|
|
|
10150
11727
|
...page.routePaths,
|
|
10151
11728
|
...prefixRoutePaths(page.routePaths, entry.prefix, context.outDir, context.base)
|
|
10152
11729
|
};
|
|
11730
|
+
context.markdownSourcePages.push(...outputPages);
|
|
10153
11731
|
snapContext.versionNavigation = createVersionNavigationContext({
|
|
10154
11732
|
prefix: entry.prefix,
|
|
10155
11733
|
base: context.base,
|
|
@@ -10262,6 +11840,20 @@ async function writeGeneratedPages(generatedPages, context, generatedFiles, list
|
|
|
10262
11840
|
errors.push(feeds.warning);
|
|
10263
11841
|
console.warn(feeds.warning);
|
|
10264
11842
|
}
|
|
11843
|
+
const markdownSource = await writeMarkdownSourceFiles({
|
|
11844
|
+
outDir: context.outDir,
|
|
11845
|
+
base: context.base,
|
|
11846
|
+
options: context.ssgOptions.markdownSource,
|
|
11847
|
+
publishState: context.options.publishState,
|
|
11848
|
+
pages: context.markdownSourcePages.map((page) => ({
|
|
11849
|
+
inputPath: page.inputPath,
|
|
11850
|
+
source: page.source,
|
|
11851
|
+
urlPath: page.routePaths.urlPath,
|
|
11852
|
+
frontmatter: page.frontmatter
|
|
11853
|
+
}))
|
|
11854
|
+
});
|
|
11855
|
+
generatedFiles.push(...markdownSource.files);
|
|
11856
|
+
errors.push(...markdownSource.errors);
|
|
10265
11857
|
}
|
|
10266
11858
|
/** Turns an SSG `urlPath` (`guide` or `/`) into a same-origin dest (`/guide`). */
|
|
10267
11859
|
function sitePathFromUrlPath(urlPath) {
|
|
@@ -10275,6 +11867,7 @@ function sitemapPages(context, listedPages, outputPages) {
|
|
|
10275
11867
|
loc: canonicalPageUrl(context, page.routePaths.urlPath) ?? "",
|
|
10276
11868
|
title: page.title,
|
|
10277
11869
|
description: page.description,
|
|
11870
|
+
lastUpdated: page.lastUpdated,
|
|
10278
11871
|
draft: page.frontmatter.draft === true,
|
|
10279
11872
|
unlisted: Boolean(context.options.publishState?.enabled) && !listedPaths.has(page.inputPath)
|
|
10280
11873
|
}));
|
|
@@ -10372,7 +11965,8 @@ function createDevServerCache() {
|
|
|
10372
11965
|
navGroups: null,
|
|
10373
11966
|
localePages: null,
|
|
10374
11967
|
pages: /* @__PURE__ */ new Map(),
|
|
10375
|
-
siteName: null
|
|
11968
|
+
siteName: null,
|
|
11969
|
+
markdownSourceIndex: null
|
|
10376
11970
|
};
|
|
10377
11971
|
}
|
|
10378
11972
|
/**
|
|
@@ -10381,6 +11975,7 @@ function createDevServerCache() {
|
|
|
10381
11975
|
function invalidateNavCache(cache) {
|
|
10382
11976
|
cache.navGroups = null;
|
|
10383
11977
|
cache.localePages = null;
|
|
11978
|
+
cache.markdownSourceIndex = null;
|
|
10384
11979
|
cache.pages.clear();
|
|
10385
11980
|
}
|
|
10386
11981
|
/**
|
|
@@ -10388,6 +11983,7 @@ function invalidateNavCache(cache) {
|
|
|
10388
11983
|
*/
|
|
10389
11984
|
function invalidatePageCache(cache, filePath) {
|
|
10390
11985
|
cache.pages.delete(filePath);
|
|
11986
|
+
cache.markdownSourceIndex = null;
|
|
10391
11987
|
}
|
|
10392
11988
|
/**
|
|
10393
11989
|
* Resolve site name from options or package.json.
|
|
@@ -10477,13 +12073,35 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
|
|
|
10477
12073
|
pages: localePages,
|
|
10478
12074
|
base
|
|
10479
12075
|
}) : void 0;
|
|
12076
|
+
const markdownSource = options.ssg.markdownSource?.enabled ? markdownSourceHrefForPage({
|
|
12077
|
+
source: filePath,
|
|
12078
|
+
fileUrl: pageData.path,
|
|
12079
|
+
frontmatter,
|
|
12080
|
+
base,
|
|
12081
|
+
permalinks: options.permalinks,
|
|
12082
|
+
cascade: options.cascade,
|
|
12083
|
+
publishState: options.publishState
|
|
12084
|
+
}) : void 0;
|
|
10480
12085
|
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 ?? {
|
|
10481
12086
|
enabled: false,
|
|
10482
12087
|
members: []
|
|
10483
|
-
}, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl, options.ssg.headValidation);
|
|
12088
|
+
}, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl, options.ssg.headValidation, i18n?.defaultLocale);
|
|
12089
|
+
if (markdownSource && options.ssg.markdownSource?.alternate) html = injectMarkdownSourceAlternate(html, markdownSource);
|
|
10484
12090
|
html = injectViteHmrClient(html);
|
|
10485
12091
|
return html;
|
|
10486
12092
|
}
|
|
12093
|
+
async function serveMarkdownSource(routeUrl, options, srcDir, cache) {
|
|
12094
|
+
if (!cache.markdownSourceIndex) cache.markdownSourceIndex = await buildMarkdownSourceIndex({
|
|
12095
|
+
files: await collectMarkdownFiles(srcDir, options.extensions),
|
|
12096
|
+
srcDir,
|
|
12097
|
+
permalinks: options.permalinks,
|
|
12098
|
+
cascade: options.cascade,
|
|
12099
|
+
publishState: options.publishState
|
|
12100
|
+
});
|
|
12101
|
+
const entry = resolveMarkdownSourceRequest(routeUrl, cache.markdownSourceIndex);
|
|
12102
|
+
if (!entry) return "missing";
|
|
12103
|
+
return entry.allowed ? entry.source : "hidden";
|
|
12104
|
+
}
|
|
10487
12105
|
/**
|
|
10488
12106
|
* Create the dev server middleware for SSG page serving.
|
|
10489
12107
|
*/
|
|
@@ -10496,6 +12114,19 @@ function createDevServerMiddleware(options, root, cache) {
|
|
|
10496
12114
|
let routeUrl = url;
|
|
10497
12115
|
if (base !== "/" && routeUrl.startsWith(base)) routeUrl = "/" + routeUrl.slice(base.length);
|
|
10498
12116
|
if (shouldSkip(routeUrl)) return next();
|
|
12117
|
+
if (options.ssg.markdownSource?.enabled && isMarkdownSourceRequest(routeUrl)) {
|
|
12118
|
+
const served = await serveMarkdownSource(routeUrl, options, srcDir, cache);
|
|
12119
|
+
if (served === "missing") return next();
|
|
12120
|
+
if (served === "hidden") {
|
|
12121
|
+
res.statusCode = 404;
|
|
12122
|
+
res.end();
|
|
12123
|
+
return;
|
|
12124
|
+
}
|
|
12125
|
+
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
12126
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
12127
|
+
res.end(served);
|
|
12128
|
+
return;
|
|
12129
|
+
}
|
|
10499
12130
|
const filePath = await resolveMarkdownFile(routeUrl, srcDir, options.extensions);
|
|
10500
12131
|
if (!filePath) return next();
|
|
10501
12132
|
try {
|
|
@@ -11012,6 +12643,43 @@ function resolveCardOptions(options) {
|
|
|
11012
12643
|
return { enabled: options.enabled ?? true };
|
|
11013
12644
|
}
|
|
11014
12645
|
//#endregion
|
|
12646
|
+
//#region src/heading-permalinks-options.ts
|
|
12647
|
+
function resolveHeadingPermalinksOptions(options) {
|
|
12648
|
+
if (!options) return { enabled: false };
|
|
12649
|
+
if (options === true) return { enabled: true };
|
|
12650
|
+
return { enabled: options.enabled ?? true };
|
|
12651
|
+
}
|
|
12652
|
+
//#endregion
|
|
12653
|
+
//#region src/magic-link-options.ts
|
|
12654
|
+
function resolveMagicLinkOptions(options) {
|
|
12655
|
+
if (!options) return {
|
|
12656
|
+
enabled: false,
|
|
12657
|
+
aliases: {},
|
|
12658
|
+
favicon: false,
|
|
12659
|
+
imageOverrides: []
|
|
12660
|
+
};
|
|
12661
|
+
if (options === true) return {
|
|
12662
|
+
enabled: true,
|
|
12663
|
+
aliases: {},
|
|
12664
|
+
favicon: false,
|
|
12665
|
+
imageOverrides: []
|
|
12666
|
+
};
|
|
12667
|
+
const favicon = options.favicon === true || typeof options.favicon === "object" && options.favicon != null;
|
|
12668
|
+
const faviconTemplate = typeof options.favicon === "object" ? options.favicon.template : void 0;
|
|
12669
|
+
return {
|
|
12670
|
+
enabled: options.enabled ?? true,
|
|
12671
|
+
aliases: normalizeAliases(options.aliases),
|
|
12672
|
+
favicon,
|
|
12673
|
+
faviconTemplate,
|
|
12674
|
+
imageOverrides: options.imageOverrides ?? []
|
|
12675
|
+
};
|
|
12676
|
+
}
|
|
12677
|
+
function normalizeAliases(aliases) {
|
|
12678
|
+
const normalized = {};
|
|
12679
|
+
for (const [key, value] of Object.entries(aliases ?? {})) normalized[key] = typeof value === "string" ? { href: value } : value;
|
|
12680
|
+
return normalized;
|
|
12681
|
+
}
|
|
12682
|
+
//#endregion
|
|
11015
12683
|
//#region src/include-options.ts
|
|
11016
12684
|
function resolveIncludeOptions(options) {
|
|
11017
12685
|
if (!options) return { enabled: false };
|
|
@@ -11586,6 +13254,12 @@ function createFrameworkMarkdownOptions(options) {
|
|
|
11586
13254
|
},
|
|
11587
13255
|
attrs: { enabled: false },
|
|
11588
13256
|
badges: { enabled: false },
|
|
13257
|
+
magicLinks: {
|
|
13258
|
+
enabled: false,
|
|
13259
|
+
aliases: {},
|
|
13260
|
+
favicon: false,
|
|
13261
|
+
imageOverrides: []
|
|
13262
|
+
},
|
|
11589
13263
|
containers: {
|
|
11590
13264
|
enabled: false,
|
|
11591
13265
|
types: {}
|
|
@@ -12585,7 +14259,7 @@ function createSsgPlugin(resolvedOptions, getRoot, ssgDevCache) {
|
|
|
12585
14259
|
for (const error of result.errors) console.warn(`[ox-content] ${error}`);
|
|
12586
14260
|
} catch (err) {
|
|
12587
14261
|
console.error("[ox-content] SSG build failed:", err);
|
|
12588
|
-
if (err instanceof PageResourceError) throw err;
|
|
14262
|
+
if (err instanceof PageResourceError || err instanceof BlogFeedError) throw err;
|
|
12589
14263
|
}
|
|
12590
14264
|
}
|
|
12591
14265
|
};
|
|
@@ -12700,6 +14374,7 @@ function resolveOptions(options) {
|
|
|
12700
14374
|
gfm: options.gfm ?? true,
|
|
12701
14375
|
mdx: options.mdx,
|
|
12702
14376
|
footnotes: options.footnotes ?? true,
|
|
14377
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
12703
14378
|
tables: options.tables ?? true,
|
|
12704
14379
|
taskLists: options.taskLists ?? true,
|
|
12705
14380
|
strikethrough: options.strikethrough ?? true,
|
|
@@ -12710,6 +14385,7 @@ function resolveOptions(options) {
|
|
|
12710
14385
|
emojiShortcodes: resolveEmojiShortcodeOptions(options.emojiShortcodes),
|
|
12711
14386
|
attrs: resolveAttrsOptions(options.attrs),
|
|
12712
14387
|
badges: resolveBadgeOptions(options.badges),
|
|
14388
|
+
magicLinks: resolveMagicLinkOptions(options.magicLinks),
|
|
12713
14389
|
containers: resolveContainerOptions(options.containers),
|
|
12714
14390
|
images: resolveImageOptions(options.images),
|
|
12715
14391
|
codeImports: resolveCodeImportOptions(options.codeImports),
|
|
@@ -12729,6 +14405,7 @@ function resolveOptions(options) {
|
|
|
12729
14405
|
frontmatter: options.frontmatter ?? true,
|
|
12730
14406
|
toc: options.toc ?? true,
|
|
12731
14407
|
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
14408
|
+
headingPermalinks: resolveHeadingPermalinksOptions(options.headingPermalinks),
|
|
12732
14409
|
ogImage: options.ogImage ?? false,
|
|
12733
14410
|
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
12734
14411
|
transformers: options.transformers ?? [],
|
|
@@ -13019,6 +14696,6 @@ function normalizeRuntimeBase(base) {
|
|
|
13019
14696
|
return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
|
|
13020
14697
|
}
|
|
13021
14698
|
//#endregion
|
|
13022
|
-
export { DEFAULT_HTML_TEMPLATE, DEFAULT_MARKDOWN_EXTENSIONS, DefaultTheme, DocsTestRunError, Fragment, IncrementalMarkdownParser, IncrementalMarkdownRenderer, PageResourceError, applyIslandSsrHtml, buildCollectionManifest, buildSearchIndex, buildSsg, classifyPublishState, clearRenderContext, collectDocsTests, collectGitHubRepos, collectGitHubSources, collectMdxIslandNamesFromHtml, collectMdxJsxNamesFromAst, collectOgpUrls, convertVitePressNav, convertVitePressSidebar, createFrameworkMarkdownOptions, createI18nPlugin, createIncrementalMarkdownParser, createIncrementalMarkdownRenderer, createMarkdownEnvironment, createTheme, defaultTheme, defineCollection, defineCollections, defineTheme, discoverDocumentMdxIslands, discoverRegisteredMdxComponents, each, escapeSvelteMarkup, extractCodeBlocks, extractDocs, extractDocsTests, extractIslandInfo, extractVideoId, fetchGitHubSource, fetchOgpData, fetchRepoData, fromVitePressConfig, generateCollectionsVirtualModule, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, generateVirtualModule, generateVitePressMigrationConfig, hasIslands, inferType, intersectHydratableComponentNames, intersectRegisteredComponentNames, isMarkdownFilePath, isMdxFilePath, isRegisteredComponent, jsx, jsxs, lintCodeBlocks, lintMarkdown, lintMarkdownAsync, lintMarkdownFile, lintMarkdownFiles, mergeThemes, mermaidClientScript, normalizeMarkdownExtensions, normalizeVitePressFrontmatter, oxContent, parseGitHubLineRange, parseGitHubPermalink, parsePageChromeFlags, partitionPublishedPages, prefetchGitHubRepos, prefetchGitHubSources, prefetchOgpData, raw, readingTimeMinutes, renderAllPages, renderHead, renderHtmlToFrameworkCode, renderHtmlToReactComponent, renderHtmlToReactCreateElement, renderHtmlToSvelteComponent, renderHtmlToVueComponent, renderHtmlToVueH, renderIslandComponentImports, renderMarkdownStream, renderPage, renderToString, resolveBadgeOptions, resolveBlogCollectionName, resolveBlogOptions, resolveBuiltinEmbedOptions, resolveCardOptions, resolveCascadeOptions, resolveCollectionsOptions, resolveContentRootPath, resolveDocsOptions, resolveDocumentComponentImports, resolveFeedsOptions, resolveFileTreeOptions, resolveHeadValidation, resolveHeaderNavItems, resolveI18nOptions, resolveImageOptions, resolveIncludeOptions, resolveLocaleLabel, resolveMathOptions, resolveMdxForFilePath, resolveNotFoundOptions, resolveOgImageOptions, resolvePageChromeOption, resolvePermalinksOptions, resolvePublishStateOptions, resolvePwaOptions, resolveRedirectsOptions, resolveResourcesOptions, resolveSearchOptions, resolveSectionIndexOptions, resolveSiteMapsOptions, resolveSsgOptions, resolveStepsOptions, resolveTaxonomiesOptions, resolveTeamOptions, resolveTheme, resolveTypedHoverOptions, resolveVersionsOptions, runDocsTests, setRenderContext, shouldLintMarkdownFile, stripMarkdownExtension, stripViteQuery, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, typecheckCodeBlocks, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeDocsTestFiles, writeSearchIndex };
|
|
14699
|
+
export { BlogFeedError, DEFAULT_HTML_TEMPLATE, DEFAULT_MARKDOWN_EXTENSIONS, DefaultTheme, DocsTestRunError, Fragment, IncrementalMarkdownParser, IncrementalMarkdownRenderer, PageResourceError, applyIslandSsrHtml, buildCollectionManifest, buildSearchIndex, buildSsg, classifyPublishState, clearRenderContext, collectDocsTests, collectGitHubRepos, collectGitHubSources, collectMdxIslandNamesFromHtml, collectMdxJsxNamesFromAst, collectOgpUrls, convertVitePressNav, convertVitePressSidebar, createFrameworkMarkdownOptions, createI18nPlugin, createIncrementalMarkdownParser, createIncrementalMarkdownRenderer, createMarkdownEnvironment, createTheme, defaultTheme, defineCollection, defineCollections, defineTheme, discoverDocumentMdxIslands, discoverRegisteredMdxComponents, each, escapeSvelteMarkup, extractCodeBlocks, extractDocs, extractDocsTests, extractIslandInfo, extractVideoId, fetchGitHubSource, fetchOgpData, fetchRepoData, fromVitePressConfig, generateCollectionsVirtualModule, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, generateVirtualModule, generateVitePressMigrationConfig, hasIslands, inferType, intersectHydratableComponentNames, intersectRegisteredComponentNames, isMarkdownFilePath, isMdxFilePath, isRegisteredComponent, jsx, jsxs, lintCodeBlocks, lintMarkdown, lintMarkdownAsync, lintMarkdownFile, lintMarkdownFiles, mergeThemes, mermaidClientScript, normalizeMarkdownExtensions, normalizeVitePressFrontmatter, oxContent, parseGitHubLineRange, parseGitHubPermalink, parsePageChromeFlags, partitionPublishedPages, prefetchGitHubRepos, prefetchGitHubSources, prefetchOgpData, raw, readingTimeMinutes, renderAllPages, renderHead, renderHtmlToFrameworkCode, renderHtmlToReactComponent, renderHtmlToReactCreateElement, renderHtmlToSvelteComponent, renderHtmlToVueComponent, renderHtmlToVueH, renderIslandComponentImports, renderMarkdownStream, renderPage, renderToString, resolveBadgeOptions, resolveBlogCollectionName, resolveBlogOptions, resolveBuiltinEmbedOptions, resolveCardOptions, resolveCascadeOptions, resolveCollectionsOptions, resolveContentRootPath, resolveDocsOptions, resolveDocumentComponentImports, resolveFeedsOptions, resolveFileTreeOptions, resolveHeadValidation, resolveHeaderNavItems, resolveHeadingPermalinksOptions, resolveI18nOptions, resolveImageOptions, resolveIncludeOptions, resolveLocaleLabel, resolveMarkdownSourceOptions, resolveMathOptions, resolveMdxForFilePath, resolveNotFoundOptions, resolveOgImageOptions, resolvePageChromeOption, resolvePermalinksOptions, resolvePublishStateOptions, resolvePwaOptions, resolveRedirectsOptions, resolveResourcesOptions, resolveSearchOptions, resolveSectionIndexOptions, resolveSiteMapsOptions, resolveSsgOptions, resolveStepsOptions, resolveTaxonomiesOptions, resolveTeamOptions, resolveTheme, resolveTypedHoverOptions, resolveVersionsOptions, runDocsTests, setRenderContext, shouldLintMarkdownFile, stripMarkdownExtension, stripViteQuery, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, typecheckCodeBlocks, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeDocsTestFiles, writeSearchIndex };
|
|
13023
14700
|
|
|
13024
14701
|
//# sourceMappingURL=index.mjs.map
|