@ox-content/vite-plugin 3.0.0-alpha.4 → 3.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2299 -587
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +365 -25
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +365 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2320 -613
- package/dist/index.mjs.map +1 -1
- package/dist/vitepress.cjs +3 -0
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.mjs +3 -0
- package/dist/vitepress.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.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
|
-
|
|
634
|
-
|
|
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("`", "`");
|
|
799
|
+
}
|
|
800
|
+
function escapeHtml$6(value) {
|
|
801
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
648
802
|
}
|
|
649
|
-
|
|
650
|
-
|
|
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
|
|
@@ -4130,6 +4723,22 @@ function stripLocalePrefix(sitePath, locales) {
|
|
|
4130
4723
|
return normalized;
|
|
4131
4724
|
}
|
|
4132
4725
|
//#endregion
|
|
4726
|
+
//#region src/page-head.ts
|
|
4727
|
+
/** Resolve descriptors to escaped `<head>` markup. Build-time only. */
|
|
4728
|
+
function renderHead(input) {
|
|
4729
|
+
return importNapiModuleSync().renderHead(JSON.stringify(input));
|
|
4730
|
+
}
|
|
4731
|
+
function resolveHeadValidation(value) {
|
|
4732
|
+
if (value === "warn" || value === "strict") return value;
|
|
4733
|
+
return false;
|
|
4734
|
+
}
|
|
4735
|
+
function reportHeadDiagnostics(diagnostics, validation) {
|
|
4736
|
+
if (!validation || diagnostics.length === 0) return;
|
|
4737
|
+
const fatal = diagnostics.filter((item) => item.strict);
|
|
4738
|
+
if (validation === "strict" && fatal.length > 0) throw new Error(`[ox-content] ${fatal[0].message}`);
|
|
4739
|
+
if (validation === "warn") for (const item of diagnostics) console.warn(`[ox-content] ${item.message}`);
|
|
4740
|
+
}
|
|
4741
|
+
//#endregion
|
|
4133
4742
|
//#region src/page-context.ts
|
|
4134
4743
|
var page_context_exports = /* @__PURE__ */ __exportAll({
|
|
4135
4744
|
clearRenderContext: () => clearRenderContext,
|
|
@@ -4347,6 +4956,7 @@ function renderPage(page, options) {
|
|
|
4347
4956
|
contributors: page.contributors,
|
|
4348
4957
|
path: page.path,
|
|
4349
4958
|
url: page.url,
|
|
4959
|
+
markdownSource: page.markdownSource,
|
|
4350
4960
|
frontmatter: page.frontmatter,
|
|
4351
4961
|
layout: page.layout
|
|
4352
4962
|
},
|
|
@@ -4363,6 +4973,7 @@ function renderPage(page, options) {
|
|
|
4363
4973
|
contributors: p.contributors,
|
|
4364
4974
|
path: p.path,
|
|
4365
4975
|
url: p.url,
|
|
4976
|
+
markdownSource: p.markdownSource,
|
|
4366
4977
|
frontmatter: p.frontmatter,
|
|
4367
4978
|
layout: p.layout
|
|
4368
4979
|
}))
|
|
@@ -4421,8 +5032,8 @@ function DefaultTheme({ children }) {
|
|
|
4421
5032
|
<head>
|
|
4422
5033
|
<meta charset="UTF-8">
|
|
4423
5034
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4424
|
-
<title>${escapeHtml$
|
|
4425
|
-
${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)}">` : ""}
|
|
4426
5037
|
<style>
|
|
4427
5038
|
:root {
|
|
4428
5039
|
--octc-color-primary: #4f6fae;
|
|
@@ -4446,7 +5057,7 @@ function DefaultTheme({ children }) {
|
|
|
4446
5057
|
</head>
|
|
4447
5058
|
<body>
|
|
4448
5059
|
<header>
|
|
4449
|
-
<h1>${escapeHtml$
|
|
5060
|
+
<h1>${escapeHtml$3(site.name)}</h1>
|
|
4450
5061
|
</header>
|
|
4451
5062
|
<main>
|
|
4452
5063
|
${children.__html}
|
|
@@ -4454,7 +5065,7 @@ function DefaultTheme({ children }) {
|
|
|
4454
5065
|
</body>
|
|
4455
5066
|
</html>` };
|
|
4456
5067
|
}
|
|
4457
|
-
function escapeHtml$
|
|
5068
|
+
function escapeHtml$3(str) {
|
|
4458
5069
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4459
5070
|
}
|
|
4460
5071
|
/**
|
|
@@ -4554,6 +5165,13 @@ async function writeSiteMapFiles(input) {
|
|
|
4554
5165
|
}
|
|
4555
5166
|
return { files };
|
|
4556
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
|
+
}
|
|
4557
5175
|
function hasSiteUrl$2(siteUrl) {
|
|
4558
5176
|
return Boolean(siteUrl && siteUrl.trim());
|
|
4559
5177
|
}
|
|
@@ -4566,7 +5184,14 @@ function generateSitemapXml(pages) {
|
|
|
4566
5184
|
for (const page of pages) {
|
|
4567
5185
|
xml += " <url>\n <loc>";
|
|
4568
5186
|
xml += escapeXml$1(page.loc);
|
|
4569
|
-
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";
|
|
4570
5195
|
}
|
|
4571
5196
|
xml += "</urlset>\n";
|
|
4572
5197
|
return xml;
|
|
@@ -4627,6 +5252,168 @@ function escapeLlmsUrl(value) {
|
|
|
4627
5252
|
return escaped;
|
|
4628
5253
|
}
|
|
4629
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
|
|
4630
5417
|
//#region src/publish-state.ts
|
|
4631
5418
|
/**
|
|
4632
5419
|
* Opt-in draft / unlisted / scheduled page classification.
|
|
@@ -4717,160 +5504,202 @@ function hiddenNavKeys(pages, listed) {
|
|
|
4717
5504
|
function toNapiPublishState(options) {
|
|
4718
5505
|
if (!options) return;
|
|
4719
5506
|
return {
|
|
4720
|
-
enabled: options.enabled,
|
|
4721
|
-
now: options.now,
|
|
4722
|
-
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
|
|
4723
5537
|
};
|
|
4724
5538
|
}
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
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);
|
|
4731
5551
|
}
|
|
4732
|
-
/**
|
|
4733
|
-
function
|
|
4734
|
-
|
|
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("/"));
|
|
4735
5563
|
}
|
|
4736
5564
|
/**
|
|
4737
|
-
*
|
|
5565
|
+
* Whether this page may publish a companion.
|
|
4738
5566
|
*
|
|
4739
|
-
*
|
|
4740
|
-
*
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
if (
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
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: [],
|
|
4750
5590
|
errors: []
|
|
4751
5591
|
};
|
|
4752
|
-
const
|
|
5592
|
+
const files = [];
|
|
4753
5593
|
const errors = [];
|
|
4754
|
-
const
|
|
4755
|
-
for (const page of
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
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}`);
|
|
4761
5600
|
continue;
|
|
4762
5601
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
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);
|
|
4769
5611
|
}
|
|
4770
5612
|
return {
|
|
4771
|
-
|
|
5613
|
+
files,
|
|
4772
5614
|
errors
|
|
4773
5615
|
};
|
|
4774
5616
|
}
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
function resolveFlag(value) {
|
|
4780
|
-
if (!value) return { enabled: false };
|
|
4781
|
-
if (value === true) return { enabled: true };
|
|
4782
|
-
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("\\");
|
|
4783
5621
|
}
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
}));
|
|
4789
|
-
const indexes = /* @__PURE__ */ new Map();
|
|
4790
|
-
for (const page of pages) {
|
|
4791
|
-
const source = normalizeSeparators(page.source);
|
|
4792
|
-
if (isIndexFile(source)) indexes.set(directoryOf(source), { ...page.frontmatter });
|
|
4793
|
-
}
|
|
4794
|
-
return pages.map((page) => {
|
|
4795
|
-
const source = normalizeSeparators(page.source);
|
|
4796
|
-
const frontmatter = { ...page.frontmatter };
|
|
4797
|
-
for (const dir of ancestorDirs(source)) {
|
|
4798
|
-
const defaults = indexes.get(dir);
|
|
4799
|
-
if (!defaults || isIndexFile(source) && directoryOf(source) === dir) continue;
|
|
4800
|
-
for (const [key, value] of Object.entries(defaults)) if (!RESERVED_CASCADE_KEYS.has(key) && !(key in frontmatter)) frontmatter[key] = value;
|
|
4801
|
-
}
|
|
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");
|
|
4802
5626
|
return {
|
|
4803
|
-
|
|
4804
|
-
|
|
5627
|
+
source: file,
|
|
5628
|
+
fileUrl: importNapiModuleSync().getSsgUrlPath(file, input.srcDir),
|
|
5629
|
+
frontmatter: parseSourceFrontmatter(source),
|
|
5630
|
+
body: source
|
|
4805
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
|
|
4806
5641
|
});
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
const
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
};
|
|
4817
|
-
}
|
|
4818
|
-
const slug = readString(page.frontmatter.slug);
|
|
4819
|
-
if (slug !== void 0) {
|
|
4820
|
-
const url = rewriteSlug(fileUrl, slug);
|
|
4821
|
-
return url ? { urlPath: url } : {
|
|
4822
|
-
urlPath: fileUrl,
|
|
4823
|
-
error: `[ox-content] rejected slug ${JSON.stringify(slug)} on ${page.source} (path escape); using the file-tree URL`
|
|
4824
|
-
};
|
|
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
|
+
});
|
|
4825
5652
|
}
|
|
4826
|
-
return
|
|
5653
|
+
return index;
|
|
4827
5654
|
}
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
if (
|
|
4831
|
-
|
|
4832
|
-
if (normalized === "/") return;
|
|
4833
|
-
if (fileUrl === "/") return normalized;
|
|
4834
|
-
const segments = fileUrl.split("/").filter(Boolean);
|
|
4835
|
-
segments.pop();
|
|
4836
|
-
segments.push(normalized);
|
|
4837
|
-
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)));
|
|
4838
5659
|
}
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
if (
|
|
4843
|
-
const
|
|
4844
|
-
|
|
4845
|
-
|
|
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;
|
|
4846
5671
|
}
|
|
4847
|
-
function
|
|
4848
|
-
|
|
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;
|
|
4849
5677
|
}
|
|
4850
|
-
function
|
|
4851
|
-
|
|
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`;
|
|
4852
5683
|
}
|
|
4853
|
-
function
|
|
4854
|
-
|
|
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;
|
|
4855
5690
|
}
|
|
4856
|
-
function
|
|
4857
|
-
|
|
4858
|
-
return
|
|
5691
|
+
function normalizeBase$1(base) {
|
|
5692
|
+
if (!base || base === "/") return "/";
|
|
5693
|
+
return base.endsWith("/") ? base : `${base}/`;
|
|
4859
5694
|
}
|
|
4860
|
-
function
|
|
4861
|
-
|
|
4862
|
-
return index === -1 ? "" : source.slice(0, index);
|
|
5695
|
+
function stripSearch(pathname) {
|
|
5696
|
+
return pathname.split("?")[0]?.split("#")[0] ?? pathname;
|
|
4863
5697
|
}
|
|
4864
|
-
function
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
for (const segment of dir.split("/")) {
|
|
4870
|
-
acc = acc ? `${acc}/${segment}` : segment;
|
|
4871
|
-
dirs.push(acc);
|
|
4872
|
-
}
|
|
4873
|
-
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;
|
|
4874
5703
|
}
|
|
4875
5704
|
//#endregion
|
|
4876
5705
|
//#region src/apply-permalinks.ts
|
|
@@ -5102,7 +5931,7 @@ async function writeRedirectFiles(input) {
|
|
|
5102
5931
|
}
|
|
5103
5932
|
/** Static HTML redirect body. `dest` is escaped. */
|
|
5104
5933
|
function generateRedirectHtml(dest) {
|
|
5105
|
-
const escaped = escapeHtml$
|
|
5934
|
+
const escaped = escapeHtml$2(dest);
|
|
5106
5935
|
return `\
|
|
5107
5936
|
<!DOCTYPE html>
|
|
5108
5937
|
<html lang="en">
|
|
@@ -5187,7 +6016,7 @@ function upsert(files, index, occupied, from, to, base) {
|
|
|
5187
6016
|
html
|
|
5188
6017
|
});
|
|
5189
6018
|
}
|
|
5190
|
-
function escapeHtml$
|
|
6019
|
+
function escapeHtml$2(value) {
|
|
5191
6020
|
return value.replace(/[&<>"']/g, (ch) => {
|
|
5192
6021
|
switch (ch) {
|
|
5193
6022
|
case "&": return "&";
|
|
@@ -5545,6 +6374,7 @@ function createNativeTransformOptions(options) {
|
|
|
5545
6374
|
return {
|
|
5546
6375
|
gfm: options.gfm,
|
|
5547
6376
|
footnotes: options.footnotes,
|
|
6377
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
5548
6378
|
taskLists: options.taskLists,
|
|
5549
6379
|
tables: options.tables,
|
|
5550
6380
|
strikethrough: options.strikethrough,
|
|
@@ -5552,6 +6382,7 @@ function createNativeTransformOptions(options) {
|
|
|
5552
6382
|
autolinkUrls: options.autolinks,
|
|
5553
6383
|
frontmatter: options.frontmatter,
|
|
5554
6384
|
tocMaxDepth: options.tocMaxDepth,
|
|
6385
|
+
headingPermalinks: options.headingPermalinks?.enabled ?? false,
|
|
5555
6386
|
codeAnnotations: options.codeAnnotations?.enabled ?? false,
|
|
5556
6387
|
codeAnnotationMetaKey: options.codeAnnotations?.metaKey ?? "annotate",
|
|
5557
6388
|
codeAnnotationSyntax: options.codeAnnotations?.notation ?? "attribute",
|
|
@@ -5566,6 +6397,13 @@ function createNativeTransformOptions(options) {
|
|
|
5566
6397
|
} : void 0,
|
|
5567
6398
|
attributes: options.attrs?.enabled ? { enabled: true } : void 0,
|
|
5568
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,
|
|
5569
6407
|
containers: options.containers?.enabled ? {
|
|
5570
6408
|
enabled: true,
|
|
5571
6409
|
types: options.containers.types
|
|
@@ -5990,6 +6828,7 @@ function isExcludedFromFeed(item, publishState) {
|
|
|
5990
6828
|
const frontmatter = item.frontmatter ?? {};
|
|
5991
6829
|
if (item.draft === true || frontmatter.draft === true) return true;
|
|
5992
6830
|
if (item.unlisted === true || frontmatter.unlisted === true) return true;
|
|
6831
|
+
if (frontmatter.external === true) return true;
|
|
5993
6832
|
if (!publishState?.enabled) return false;
|
|
5994
6833
|
return !classifyPublishState({
|
|
5995
6834
|
...frontmatter,
|
|
@@ -6230,14 +7069,14 @@ function relatedMarkup(pages) {
|
|
|
6230
7069
|
}
|
|
6231
7070
|
function listPageContent(terms, base, urlName) {
|
|
6232
7071
|
const items = terms.map((term) => listItem$1(siteHref$3(base, urlName, term.slug), term.label)).join("");
|
|
6233
|
-
return `<h1>${escapeHtml$
|
|
7072
|
+
return `<h1>${escapeHtml$1(displayTaxonomyName(urlName))}</h1><ul class="ox-taxonomy">${items}</ul>`;
|
|
6234
7073
|
}
|
|
6235
7074
|
function termPageContent(term) {
|
|
6236
7075
|
const items = [...term.pages].sort((left, right) => {
|
|
6237
7076
|
const titleCmp = left.title.localeCompare(right.title);
|
|
6238
7077
|
return titleCmp !== 0 ? titleCmp : left.routePaths.href.localeCompare(right.routePaths.href);
|
|
6239
7078
|
}).map((page) => listItem$1(page.routePaths.href, page.title)).join("");
|
|
6240
|
-
return `<h1>${escapeHtml$
|
|
7079
|
+
return `<h1>${escapeHtml$1(term.label)}</h1><ul class="ox-taxonomy-term">${items}</ul>`;
|
|
6241
7080
|
}
|
|
6242
7081
|
function displayTaxonomyName(name) {
|
|
6243
7082
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -6255,9 +7094,9 @@ function containedPath$2(outDir, ...segments) {
|
|
|
6255
7094
|
return resolved;
|
|
6256
7095
|
}
|
|
6257
7096
|
function listItem$1(href, label) {
|
|
6258
|
-
return `<li><a href="${escapeHtml$
|
|
7097
|
+
return `<li><a href="${escapeHtml$1(href)}">${escapeHtml$1(label)}</a></li>`;
|
|
6259
7098
|
}
|
|
6260
|
-
function escapeHtml$
|
|
7099
|
+
function escapeHtml$1(value) {
|
|
6261
7100
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6262
7101
|
}
|
|
6263
7102
|
//#endregion
|
|
@@ -6531,96 +7370,441 @@ function resolveBlogOptions(value) {
|
|
|
6531
7370
|
if (!value) return {
|
|
6532
7371
|
enabled: false,
|
|
6533
7372
|
authors: {},
|
|
6534
|
-
pageSize: DEFAULT_PAGE_SIZE
|
|
7373
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
7374
|
+
feeds: []
|
|
6535
7375
|
};
|
|
6536
7376
|
if (value === true) return {
|
|
6537
7377
|
enabled: true,
|
|
6538
7378
|
authors: {},
|
|
6539
|
-
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)
|
|
6540
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;
|
|
6541
7688
|
return {
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
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
|
+
}
|
|
6547
7740
|
}
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
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
|
+
};
|
|
6558
7751
|
}
|
|
6559
|
-
function
|
|
6560
|
-
|
|
6561
|
-
const
|
|
6562
|
-
|
|
6563
|
-
if (!
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
};
|
|
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
|
+
});
|
|
6569
7761
|
}
|
|
6570
|
-
return
|
|
7762
|
+
return links.find((link) => link.rel === "alternate")?.href ?? links[0]?.href;
|
|
6571
7763
|
}
|
|
6572
|
-
function
|
|
6573
|
-
|
|
6574
|
-
return DEFAULT_PAGE_SIZE;
|
|
7764
|
+
function xmlLang(block) {
|
|
7765
|
+
return block.match(/\bxml:lang\s*=\s*(['"])([^'"]+)\1/i)?.[2]?.trim() || void 0;
|
|
6575
7766
|
}
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
if (
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
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;
|
|
6594
7784
|
}
|
|
6595
|
-
if (
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
latinRun = true;
|
|
6599
|
-
}
|
|
6600
|
-
continue;
|
|
7785
|
+
if (lower.startsWith("#")) {
|
|
7786
|
+
const code = Number.parseInt(lower.slice(1), 10);
|
|
7787
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
6601
7788
|
}
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
}
|
|
6605
|
-
if (latin === 0 && cjk === 0) return 0;
|
|
6606
|
-
return Math.max(1, Math.ceil(latin / LATIN_WORDS_PER_MINUTE + cjk / CJK_CHARS_PER_MINUTE));
|
|
7789
|
+
return entity;
|
|
7790
|
+
});
|
|
6607
7791
|
}
|
|
6608
|
-
function
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
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");
|
|
6612
7795
|
}
|
|
6613
|
-
function
|
|
6614
|
-
return
|
|
7796
|
+
function looksLikeXmlFeed(body) {
|
|
7797
|
+
return /<(?:[\w.-]+:)?(?:rss|feed|rdf:RDF|item|entry)\b/i.test(body);
|
|
6615
7798
|
}
|
|
6616
|
-
function
|
|
6617
|
-
return
|
|
7799
|
+
function stripBom(value) {
|
|
7800
|
+
return value.charCodeAt(0) === 65279 ? value.slice(1) : value;
|
|
6618
7801
|
}
|
|
6619
|
-
function
|
|
6620
|
-
|
|
7802
|
+
function localName(name) {
|
|
7803
|
+
const index = name.indexOf(":");
|
|
7804
|
+
return index === -1 ? name : name.slice(index + 1);
|
|
6621
7805
|
}
|
|
6622
|
-
function
|
|
6623
|
-
return
|
|
7806
|
+
function escapeRegExp$1(value) {
|
|
7807
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6624
7808
|
}
|
|
6625
7809
|
//#endregion
|
|
6626
7810
|
//#region src/blog-html.ts
|
|
@@ -6636,13 +7820,13 @@ function isSafeBlogUrl(value) {
|
|
|
6636
7820
|
return trimmed.toLowerCase().startsWith("https:");
|
|
6637
7821
|
}
|
|
6638
7822
|
function postMetaMarkup(meta) {
|
|
6639
|
-
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>`];
|
|
6640
7824
|
if (meta.authors.length > 0) {
|
|
6641
7825
|
const items = meta.authors.map((author) => authorMarkup(author)).join("");
|
|
6642
7826
|
parts.push(`<ul class="ox-blog-meta__authors">${items}</ul>`);
|
|
6643
7827
|
}
|
|
6644
7828
|
if (meta.tags.length > 0) {
|
|
6645
|
-
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("");
|
|
6646
7830
|
parts.push(`<ul class="ox-blog-meta__tags">${items}</ul>`);
|
|
6647
7831
|
}
|
|
6648
7832
|
return `<aside class="ox-blog-meta">${parts.join("")}</aside>\n`;
|
|
@@ -6650,25 +7834,25 @@ function postMetaMarkup(meta) {
|
|
|
6650
7834
|
function indexPageContent(items, pager) {
|
|
6651
7835
|
const list = items.map((item) => listItem(item)).join("");
|
|
6652
7836
|
const links = [];
|
|
6653
|
-
if (pager.newerHref) links.push(`<a href="${escapeHtml
|
|
6654
|
-
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>`);
|
|
6655
7839
|
return `<h1>Blog</h1><ul class="ox-blog">${list}</ul>${links.length > 0 ? `<nav class="ox-blog-pager">${links.join("")}</nav>` : ""}`;
|
|
6656
7840
|
}
|
|
6657
7841
|
function tagPageContent(label, items) {
|
|
6658
7842
|
const list = items.map((item) => listItem(item)).join("");
|
|
6659
|
-
return `<h1>${escapeHtml
|
|
7843
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog-tag">${list}</ul>`;
|
|
6660
7844
|
}
|
|
6661
7845
|
function archiveIndexContent(years) {
|
|
6662
|
-
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>`;
|
|
6663
7847
|
}
|
|
6664
7848
|
function archiveYearContent(year, months, items) {
|
|
6665
|
-
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("");
|
|
6666
7850
|
const posts = items.map((item) => listItem(item)).join("");
|
|
6667
|
-
return `<h1>${escapeHtml
|
|
7851
|
+
return `<h1>${escapeHtml(year)}</h1><ul class="ox-blog-archive-months">${monthList}</ul><ul class="ox-blog">${posts}</ul>`;
|
|
6668
7852
|
}
|
|
6669
7853
|
function archiveMonthContent(label, items) {
|
|
6670
7854
|
const list = items.map((item) => listItem(item)).join("");
|
|
6671
|
-
return `<h1>${escapeHtml
|
|
7855
|
+
return `<h1>${escapeHtml(label)}</h1><ul class="ox-blog">${list}</ul>`;
|
|
6672
7856
|
}
|
|
6673
7857
|
function siteHref$2(base, ...segments) {
|
|
6674
7858
|
const prefix = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
@@ -6682,17 +7866,18 @@ function containedPath$1(outDir, ...segments) {
|
|
|
6682
7866
|
if (resolved === root || !resolved.startsWith(prefix)) return;
|
|
6683
7867
|
return resolved;
|
|
6684
7868
|
}
|
|
6685
|
-
function escapeHtml
|
|
7869
|
+
function escapeHtml(value) {
|
|
6686
7870
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
6687
7871
|
}
|
|
6688
7872
|
function authorMarkup(author) {
|
|
6689
|
-
const name = escapeHtml
|
|
7873
|
+
const name = escapeHtml(author.name);
|
|
6690
7874
|
const url = author.url?.trim();
|
|
6691
|
-
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>`;
|
|
6692
7876
|
}
|
|
6693
7877
|
function listItem(item) {
|
|
6694
|
-
const time = item.dateLabel ? ` <time datetime="${escapeHtml
|
|
6695
|
-
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>`;
|
|
6696
7881
|
}
|
|
6697
7882
|
//#endregion
|
|
6698
7883
|
//#region src/blog-posts.ts
|
|
@@ -6798,7 +7983,8 @@ function toListItem(page) {
|
|
|
6798
7983
|
return {
|
|
6799
7984
|
title: page.title,
|
|
6800
7985
|
href: page.routePaths.href,
|
|
6801
|
-
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 } : {}
|
|
6802
7988
|
};
|
|
6803
7989
|
}
|
|
6804
7990
|
function resolvePostAuthors(frontmatter, map) {
|
|
@@ -6858,6 +8044,158 @@ function dateField(value) {
|
|
|
6858
8044
|
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
6859
8045
|
}
|
|
6860
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
|
|
6861
8199
|
//#region src/blog-pages.ts
|
|
6862
8200
|
/**
|
|
6863
8201
|
* Generated blog index, tag, and archive pages.
|
|
@@ -6869,7 +8207,7 @@ async function injectBlogPostMeta(input) {
|
|
|
6869
8207
|
if (posts === void 0) return;
|
|
6870
8208
|
const listedPaths = new Set(posts.map((page) => page.inputPath));
|
|
6871
8209
|
for (const page of input.pages) {
|
|
6872
|
-
if (!listedPaths.has(page.inputPath)) continue;
|
|
8210
|
+
if (!listedPaths.has(page.inputPath) || page.external === true) continue;
|
|
6873
8211
|
const markdown = await readMarkdown(page.inputPath);
|
|
6874
8212
|
page.transformedHtml = postMetaMarkup({
|
|
6875
8213
|
authors: resolvePostAuthors(page.frontmatter, input.options.authors),
|
|
@@ -6902,8 +8240,9 @@ async function appendBlogPages(input) {
|
|
|
6902
8240
|
input.errors.push(AMBIGUOUS_COLLECTION);
|
|
6903
8241
|
return;
|
|
6904
8242
|
}
|
|
6905
|
-
const
|
|
6906
|
-
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);
|
|
6907
8246
|
for (const spec of blogPageSpecs(posts, input.options, input.outDir, input.base)) try {
|
|
6908
8247
|
input.generatedPages.push({
|
|
6909
8248
|
inputPath: spec.outputPath,
|
|
@@ -6998,6 +8337,14 @@ function blogPageSpecs(posts, options, outDir, base) {
|
|
|
6998
8337
|
}
|
|
6999
8338
|
return pages;
|
|
7000
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
|
+
}
|
|
7001
8348
|
async function readMarkdown(inputPath) {
|
|
7002
8349
|
try {
|
|
7003
8350
|
return await fs$2.readFile(inputPath, "utf8");
|
|
@@ -7510,59 +8857,6 @@ function generateSearchModule(options, indexPath) {
|
|
|
7510
8857
|
return importNapiModuleSync().generateSearchModuleFromOptions(toLocalSearchRuntimeOptions(options), indexPath);
|
|
7511
8858
|
}
|
|
7512
8859
|
//#endregion
|
|
7513
|
-
//#region src/versions-html.ts
|
|
7514
|
-
function versionSwitcherMarkup(links, badge) {
|
|
7515
|
-
if (links.length === 0) return "";
|
|
7516
|
-
const current = links.find((link) => link.current) ?? links[0];
|
|
7517
|
-
const items = links.map((link) => {
|
|
7518
|
-
const label = `${escapeHtml(link.label)}${badgeMarkup(link, badge)}`;
|
|
7519
|
-
if (link.current || !isSafeHref(link.href)) return `<li><span aria-current="page">${label}</span></li>`;
|
|
7520
|
-
return `<li><a href="${escapeHtml(link.href)}">${label}</a></li>`;
|
|
7521
|
-
}).join("");
|
|
7522
|
-
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>`;
|
|
7523
|
-
}
|
|
7524
|
-
function versionBannerMarkup(kind) {
|
|
7525
|
-
if (kind === "unreleased") return `<aside class="ox-version-banner ox-version-banner--unreleased" role="status">This documentation describes an unreleased version.</aside>`;
|
|
7526
|
-
if (kind === "unmaintained") return `<aside class="ox-version-banner ox-version-banner--unmaintained" role="status">This documentation is unmaintained.</aside>`;
|
|
7527
|
-
return "";
|
|
7528
|
-
}
|
|
7529
|
-
function injectVersionChrome(html, switcher, banner, searchFrom, searchTo) {
|
|
7530
|
-
let next = html;
|
|
7531
|
-
if (banner) next = next.replace(/<body([^>]*)>/, `<body$1>${banner}`);
|
|
7532
|
-
if (switcher) {
|
|
7533
|
-
if (next.includes("<div class=\"header-actions\">")) next = next.replace("<div class=\"header-actions\">", `<div class="header-actions">${switcher}`);
|
|
7534
|
-
else if (next.includes("</header>")) next = next.replace("</header>", `${switcher}</header>`);
|
|
7535
|
-
}
|
|
7536
|
-
if (searchTo && isSafeHref(searchTo)) next = next.replace(/<html([^>]*)>/i, (match, attrs) => {
|
|
7537
|
-
if (/\sdata-ox-search-index=/.test(attrs)) return match;
|
|
7538
|
-
return `<html${attrs} data-ox-search-index="${escapeHtml(searchTo)}">`;
|
|
7539
|
-
});
|
|
7540
|
-
if (searchFrom && searchTo && searchFrom !== searchTo && isSafeHref(searchTo)) {
|
|
7541
|
-
next = next.split(searchFrom).join(searchTo);
|
|
7542
|
-
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>`;
|
|
7543
|
-
next = next.includes("</body>") ? next.replace("</body>", `${script}</body>`) : `${next}${script}`;
|
|
7544
|
-
}
|
|
7545
|
-
return next;
|
|
7546
|
-
}
|
|
7547
|
-
function searchIndexUrl(base, prefix) {
|
|
7548
|
-
const root = !base || base === "/" ? "/" : base.endsWith("/") ? base : `${base}/`;
|
|
7549
|
-
return prefix ? `${root}${prefix}/search-index.json` : `${root}search-index.json`;
|
|
7550
|
-
}
|
|
7551
|
-
function isSafeHref(href) {
|
|
7552
|
-
const trimmed = href.trim();
|
|
7553
|
-
if (!trimmed || trimmed.startsWith("//")) return false;
|
|
7554
|
-
const lower = trimmed.replace(/\s+/g, "").toLowerCase();
|
|
7555
|
-
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:")) return false;
|
|
7556
|
-
return trimmed.startsWith("/") || trimmed.startsWith("./") || !trimmed.includes(":");
|
|
7557
|
-
}
|
|
7558
|
-
function escapeHtml(value) {
|
|
7559
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
7560
|
-
}
|
|
7561
|
-
function badgeMarkup(link, badge) {
|
|
7562
|
-
if (!badge || !link.banner) return "";
|
|
7563
|
-
return `<span class="ox-version-badge">${link.banner === "unreleased" ? "unreleased" : "unmaintained"}</span>`;
|
|
7564
|
-
}
|
|
7565
|
-
//#endregion
|
|
7566
8860
|
//#region src/versions.ts
|
|
7567
8861
|
/**
|
|
7568
8862
|
* Opt-in documentation versioning: prefixes, snapshots, and header chrome.
|
|
@@ -7693,7 +8987,13 @@ async function writeSnapshotSearchIndex(input) {
|
|
|
7693
8987
|
function applyVersionChrome(html, options, activeId, siblingPath, base, existingHrefs) {
|
|
7694
8988
|
if (!options.enabled) return html;
|
|
7695
8989
|
const active = options.entries.find((entry) => entry.id === activeId);
|
|
7696
|
-
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
|
+
})));
|
|
7697
8997
|
}
|
|
7698
8998
|
function sanitizePrefix(prefix) {
|
|
7699
8999
|
const trimmed = prefix.trim().replace(/^\/+|\/+$/g, "");
|
|
@@ -7730,21 +9030,182 @@ function normalizeEntries(entries) {
|
|
|
7730
9030
|
banner: normalizeBanner(entry.banner)
|
|
7731
9031
|
});
|
|
7732
9032
|
}
|
|
7733
|
-
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
|
+
}
|
|
7734
9144
|
}
|
|
7735
|
-
|
|
7736
|
-
|
|
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}`;
|
|
7737
9152
|
}
|
|
7738
|
-
function
|
|
7739
|
-
|
|
7740
|
-
const
|
|
7741
|
-
|
|
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}` : "";
|
|
7742
9159
|
}
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
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);
|
|
7748
9209
|
}
|
|
7749
9210
|
//#endregion
|
|
7750
9211
|
//#region src/resources-jpeg.ts
|
|
@@ -8787,129 +10248,23 @@ function coverCrop(image, width, height) {
|
|
|
8787
10248
|
return cropImage(scaled, Math.max(0, Math.floor((scaled.width - width) / 2)), Math.max(0, Math.floor((scaled.height - height) / 2)), width, height);
|
|
8788
10249
|
}
|
|
8789
10250
|
//#endregion
|
|
8790
|
-
//#region src/resources-
|
|
10251
|
+
//#region src/resources-write.ts
|
|
8791
10252
|
/**
|
|
8792
|
-
*
|
|
10253
|
+
* Transform cache writes for page resources.
|
|
8793
10254
|
*/
|
|
8794
|
-
|
|
8795
|
-
const SRC_ATTR = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
|
|
8796
|
-
async function processPageResources(input) {
|
|
8797
|
-
if (!input.options.enabled) return {
|
|
8798
|
-
html: input.html,
|
|
8799
|
-
files: [],
|
|
8800
|
-
errors: [],
|
|
8801
|
-
fatal: []
|
|
8802
|
-
};
|
|
8803
|
-
const bundleRoot = path$1.dirname(input.inputPath);
|
|
8804
|
-
const outputDir = path$1.dirname(input.outputPath);
|
|
8805
|
-
const files = [];
|
|
8806
|
-
const errors = [];
|
|
8807
|
-
const fatal = [];
|
|
8808
|
-
let html = input.html;
|
|
8809
|
-
const tags = input.html.match(IMG_TAG) ?? [];
|
|
8810
|
-
for (const tag of tags) {
|
|
8811
|
-
const srcMatch = tag.match(SRC_ATTR);
|
|
8812
|
-
const rawSrc = srcMatch?.[1] ?? srcMatch?.[2];
|
|
8813
|
-
if (!rawSrc) continue;
|
|
8814
|
-
const src = unescapeHtml(rawSrc);
|
|
8815
|
-
const parsed = parseResourceSrc(src);
|
|
8816
|
-
if (!parsed) continue;
|
|
8817
|
-
const resolved = resolveBundlePath(parsed.pathname, bundleRoot, input.srcDir);
|
|
8818
|
-
if (!resolved.ok) {
|
|
8819
|
-
const message = `[ox-content] page resource ${JSON.stringify(src)} on ${input.inputPath} is outside the page bundle`;
|
|
8820
|
-
errors.push(message);
|
|
8821
|
-
fatal.push(message);
|
|
8822
|
-
continue;
|
|
8823
|
-
}
|
|
8824
|
-
let stat;
|
|
8825
|
-
try {
|
|
8826
|
-
stat = await fs$2.stat(resolved.absolute);
|
|
8827
|
-
} catch {
|
|
8828
|
-
const message = `[ox-content] missing page resource ${JSON.stringify(parsed.pathname)} on ${input.inputPath}`;
|
|
8829
|
-
errors.push(message);
|
|
8830
|
-
if (input.options.missing === "error") fatal.push(message);
|
|
8831
|
-
continue;
|
|
8832
|
-
}
|
|
8833
|
-
const transformError = validateTransform(parsed.transform, input.options);
|
|
8834
|
-
if (transformError) {
|
|
8835
|
-
const message = `[ox-content] ${transformError} for ${JSON.stringify(src)} on ${input.inputPath}`;
|
|
8836
|
-
errors.push(message);
|
|
8837
|
-
fatal.push(message);
|
|
8838
|
-
continue;
|
|
8839
|
-
}
|
|
8840
|
-
const hasTransform = hasPixelOrFormatTransform(parsed.transform);
|
|
8841
|
-
const outputName = hasTransform ? transformedFileName(parsed.pathname, parsed.transform, resourceCacheKey(resolved.absolute, stat.mtimeMs, parsed.transform)) : path$1.basename(resolved.absolute);
|
|
8842
|
-
const outputFile = path$1.join(outputDir, outputName);
|
|
8843
|
-
try {
|
|
8844
|
-
if (hasTransform) await writeTransformedResource({
|
|
8845
|
-
sourcePath: resolved.absolute,
|
|
8846
|
-
outputFile,
|
|
8847
|
-
cacheDir: input.cacheDir,
|
|
8848
|
-
mtimeMs: stat.mtimeMs,
|
|
8849
|
-
transform: parsed.transform
|
|
8850
|
-
});
|
|
8851
|
-
else {
|
|
8852
|
-
await fs$2.mkdir(outputDir, { recursive: true });
|
|
8853
|
-
await fs$2.copyFile(resolved.absolute, outputFile);
|
|
8854
|
-
}
|
|
8855
|
-
files.push(outputFile);
|
|
8856
|
-
const rewritten = tag.replace(rawSrc, escapeAttribute(outputName));
|
|
8857
|
-
html = html.replace(tag, rewritten);
|
|
8858
|
-
} catch (error) {
|
|
8859
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
8860
|
-
const message = `[ox-content] failed to process page resource ${JSON.stringify(src)} on ${input.inputPath}: ${detail}`;
|
|
8861
|
-
errors.push(message);
|
|
8862
|
-
fatal.push(message);
|
|
8863
|
-
}
|
|
8864
|
-
}
|
|
8865
|
-
return {
|
|
8866
|
-
html,
|
|
8867
|
-
files,
|
|
8868
|
-
errors,
|
|
8869
|
-
fatal
|
|
8870
|
-
};
|
|
8871
|
-
}
|
|
8872
|
-
function resolveBundlePath(pathname, bundleRoot, contentRoot) {
|
|
8873
|
-
if (path$1.isAbsolute(pathname) || pathname.includes("\0")) return { ok: false };
|
|
8874
|
-
const absolute = path$1.resolve(bundleRoot, pathname);
|
|
8875
|
-
if (!isInsideRoot$1(bundleRoot, absolute) || !isInsideRoot$1(contentRoot, absolute)) return { ok: false };
|
|
8876
|
-
return {
|
|
8877
|
-
ok: true,
|
|
8878
|
-
absolute
|
|
8879
|
-
};
|
|
8880
|
-
}
|
|
8881
|
-
function validateTransform(transform, options) {
|
|
8882
|
-
if (transform.width && options.widths.length > 0 && !options.widths.includes(transform.width)) return `width ${transform.width} is not in resources.widths`;
|
|
8883
|
-
if (transform.format && !options.formats.includes(transform.format)) return `format ${transform.format} is not in resources.formats`;
|
|
8884
|
-
}
|
|
8885
|
-
function hasPixelOrFormatTransform(transform) {
|
|
8886
|
-
return Boolean(transform.width || transform.height || transform.crop || transform.format);
|
|
8887
|
-
}
|
|
8888
|
-
function transformedFileName(pathname, transform, cacheKey) {
|
|
8889
|
-
const stem = path$1.basename(pathname).replace(/\.[^.]+$/, "") || "resource";
|
|
8890
|
-
const ext = outputExtension(pathname, transform.format);
|
|
8891
|
-
return `${stem}.${cacheKey.slice(0, 12)}.${ext}`;
|
|
8892
|
-
}
|
|
8893
|
-
function outputExtension(pathname, format) {
|
|
8894
|
-
if (format === "jpeg") return "jpg";
|
|
8895
|
-
if (format) return format;
|
|
8896
|
-
const ext = path$1.extname(pathname).slice(1).toLowerCase();
|
|
8897
|
-
return ext === "jpeg" ? "jpg" : ext || "png";
|
|
8898
|
-
}
|
|
8899
|
-
async function writeTransformedResource(input) {
|
|
10255
|
+
async function ensureTransformedCache(input) {
|
|
8900
10256
|
const key = resourceCacheKey(input.sourcePath, input.mtimeMs, input.transform);
|
|
8901
10257
|
const ext = path$1.extname(input.outputFile);
|
|
8902
10258
|
const cacheFile = path$1.join(input.cacheDir, `${key}${ext}`);
|
|
8903
10259
|
try {
|
|
8904
|
-
await fs$2.
|
|
8905
|
-
return;
|
|
10260
|
+
await fs$2.access(cacheFile);
|
|
10261
|
+
return cacheFile;
|
|
8906
10262
|
} catch {}
|
|
8907
10263
|
const output = transformResourceBuffer(await fs$2.readFile(input.sourcePath), input.sourcePath, input.transform);
|
|
8908
10264
|
if (output.length > 8388608) throw new Error("transform produced an oversized file");
|
|
8909
|
-
await fs$2.mkdir(path$1.dirname(input.outputFile), { recursive: true });
|
|
8910
10265
|
await fs$2.mkdir(input.cacheDir, { recursive: true });
|
|
8911
10266
|
await fs$2.writeFile(cacheFile, output);
|
|
8912
|
-
|
|
10267
|
+
return cacheFile;
|
|
8913
10268
|
}
|
|
8914
10269
|
function transformResourceBuffer(source, sourcePath, transform) {
|
|
8915
10270
|
const needsPixels = Boolean(transform.width || transform.height || transform.crop);
|
|
@@ -8952,11 +10307,221 @@ function formatFromPath(filePath) {
|
|
|
8952
10307
|
const ext = path$1.extname(filePath).slice(1).toLowerCase();
|
|
8953
10308
|
return ext === "jpg" ? "jpeg" : ext;
|
|
8954
10309
|
}
|
|
8955
|
-
|
|
8956
|
-
|
|
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
|
+
};
|
|
8957
10368
|
}
|
|
8958
|
-
function
|
|
8959
|
-
|
|
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
|
+
}
|
|
8960
10525
|
}
|
|
8961
10526
|
//#endregion
|
|
8962
10527
|
//#region src/resources.ts
|
|
@@ -8991,19 +10556,22 @@ function resolveResourcesOptions(value) {
|
|
|
8991
10556
|
enabled: false,
|
|
8992
10557
|
formats: [...DEFAULT_FORMATS],
|
|
8993
10558
|
widths: [],
|
|
8994
|
-
missing: "error"
|
|
10559
|
+
missing: "error",
|
|
10560
|
+
dedupe: false
|
|
8995
10561
|
};
|
|
8996
10562
|
if (value === true) return {
|
|
8997
10563
|
enabled: true,
|
|
8998
10564
|
formats: [...DEFAULT_FORMATS],
|
|
8999
10565
|
widths: [],
|
|
9000
|
-
missing: "error"
|
|
10566
|
+
missing: "error",
|
|
10567
|
+
dedupe: false
|
|
9001
10568
|
};
|
|
9002
10569
|
return {
|
|
9003
10570
|
enabled: true,
|
|
9004
10571
|
formats: normalizeFormats(value.formats),
|
|
9005
10572
|
widths: normalizeWidths(value.widths),
|
|
9006
|
-
missing: value.missing === "warn" ? "warn" : "error"
|
|
10573
|
+
missing: value.missing === "warn" ? "warn" : "error",
|
|
10574
|
+
dedupe: value.dedupe === true
|
|
9007
10575
|
};
|
|
9008
10576
|
}
|
|
9009
10577
|
/**
|
|
@@ -9249,10 +10817,12 @@ function resolveSsgOptions(ssg) {
|
|
|
9249
10817
|
pagination: false,
|
|
9250
10818
|
breadcrumbs: false,
|
|
9251
10819
|
jsonLd: false,
|
|
10820
|
+
headValidation: false,
|
|
9252
10821
|
readerChrome: false,
|
|
9253
10822
|
localeSwitcher: false,
|
|
9254
10823
|
a11y: false,
|
|
9255
10824
|
pageChrome: false,
|
|
10825
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9256
10826
|
notFound: resolveNotFoundOptions(void 0),
|
|
9257
10827
|
team: resolveTeamOptions(void 0),
|
|
9258
10828
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9269,10 +10839,12 @@ function resolveSsgOptions(ssg) {
|
|
|
9269
10839
|
pagination: false,
|
|
9270
10840
|
breadcrumbs: false,
|
|
9271
10841
|
jsonLd: false,
|
|
10842
|
+
headValidation: false,
|
|
9272
10843
|
readerChrome: false,
|
|
9273
10844
|
localeSwitcher: false,
|
|
9274
10845
|
a11y: false,
|
|
9275
10846
|
pageChrome: false,
|
|
10847
|
+
markdownSource: resolveMarkdownSourceOptions(void 0),
|
|
9276
10848
|
notFound: resolveNotFoundOptions(void 0),
|
|
9277
10849
|
team: resolveTeamOptions(void 0),
|
|
9278
10850
|
blog: resolveBlogOptions(void 0),
|
|
@@ -9297,10 +10869,12 @@ function resolveSsgOptions(ssg) {
|
|
|
9297
10869
|
pagination: resolvePaginationOption(ssg.pagination),
|
|
9298
10870
|
breadcrumbs: resolvePaginationOption(ssg.breadcrumbs),
|
|
9299
10871
|
jsonLd: resolveJsonLdOption(ssg.jsonLd),
|
|
10872
|
+
headValidation: resolveHeadValidation(ssg.headValidation),
|
|
9300
10873
|
readerChrome: resolveReaderChromeOption(ssg.readerChrome),
|
|
9301
10874
|
localeSwitcher: resolveLocaleSwitcherOption(ssg.localeSwitcher),
|
|
9302
10875
|
a11y: resolveA11yOption(ssg.a11y),
|
|
9303
10876
|
pageChrome: resolvePageChromeOption(ssg.pageChrome),
|
|
10877
|
+
markdownSource: resolveMarkdownSourceOptions(ssg.markdownSource),
|
|
9304
10878
|
notFound: resolveNotFoundOptions(ssg.notFound),
|
|
9305
10879
|
team: resolveTeamOptions(ssg.team),
|
|
9306
10880
|
blog: resolveBlogOptions(ssg.blog),
|
|
@@ -9328,7 +10902,9 @@ function resolveJsonLdOption(value) {
|
|
|
9328
10902
|
const publisher = resolveJsonLdPublisher(value.publisher);
|
|
9329
10903
|
return {
|
|
9330
10904
|
breadcrumbs: value.breadcrumbs !== false,
|
|
9331
|
-
...publisher ? { publisher } : {}
|
|
10905
|
+
...publisher ? { publisher } : {},
|
|
10906
|
+
...value.type ? { type: value.type } : {},
|
|
10907
|
+
...value.graph ? { graph: value.graph } : {}
|
|
9332
10908
|
};
|
|
9333
10909
|
}
|
|
9334
10910
|
return false;
|
|
@@ -9471,7 +11047,7 @@ function localeCodesFor(locales) {
|
|
|
9471
11047
|
async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, theme, locale, availableLocales, pagination = false, readerChrome = false, breadcrumbs = false, localeSwitcher = false, localePaths, a11y = false, team = {
|
|
9472
11048
|
enabled: false,
|
|
9473
11049
|
members: []
|
|
9474
|
-
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl) {
|
|
11050
|
+
}, pageChrome = false, breadcrumbRootHref, jsonLd = false, siteUrl, headValidation = false, defaultLocale) {
|
|
9475
11051
|
const mod = await importNapiModule();
|
|
9476
11052
|
const tocForRust = pageData.toc.map(toRustTocEntry);
|
|
9477
11053
|
const navGroupsForRust = convertNavGroupsForRust(navGroups);
|
|
@@ -9507,7 +11083,7 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9507
11083
|
linkText: f.linkText
|
|
9508
11084
|
}))
|
|
9509
11085
|
} : void 0;
|
|
9510
|
-
|
|
11086
|
+
const result = mod.generateSsgHtml({
|
|
9511
11087
|
title: pageData.title,
|
|
9512
11088
|
description: pageData.description,
|
|
9513
11089
|
content: pageData.content,
|
|
@@ -9520,12 +11096,16 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9520
11096
|
next: pageData.next,
|
|
9521
11097
|
breadcrumbs: pageData.breadcrumbs,
|
|
9522
11098
|
layout: typeof pageData.frontmatter.layout === "string" ? pageData.frontmatter.layout : void 0,
|
|
9523
|
-
chrome: pageData.chrome
|
|
11099
|
+
chrome: pageData.chrome,
|
|
11100
|
+
robots: typeof pageData.frontmatter.robots === "string" ? pageData.frontmatter.robots : void 0,
|
|
11101
|
+
canonical: typeof pageData.frontmatter.canonical === "string" ? pageData.frontmatter.canonical : void 0
|
|
9524
11102
|
}, navGroupsForRust, {
|
|
9525
11103
|
siteName,
|
|
9526
11104
|
base,
|
|
9527
11105
|
breadcrumbRootHref,
|
|
9528
11106
|
ogImage,
|
|
11107
|
+
siteUrl,
|
|
11108
|
+
headValidation: headValidation || void 0,
|
|
9529
11109
|
theme: themeForRust,
|
|
9530
11110
|
locale,
|
|
9531
11111
|
availableLocales: availableLocales ? toRustLocales(availableLocales) : void 0,
|
|
@@ -9544,9 +11124,18 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
|
|
|
9544
11124
|
jsonLd: jsonLd ? {
|
|
9545
11125
|
breadcrumbs: jsonLd.breadcrumbs,
|
|
9546
11126
|
publisher: jsonLd.publisher,
|
|
9547
|
-
siteUrl
|
|
11127
|
+
siteUrl,
|
|
11128
|
+
pageType: jsonLd.type,
|
|
11129
|
+
graph: jsonLd.graph?.map((node) => JSON.stringify(node))
|
|
9548
11130
|
} : void 0
|
|
9549
11131
|
});
|
|
11132
|
+
const html = typeof result === "string" ? result : result.html;
|
|
11133
|
+
reportHeadDiagnostics(typeof result === "string" ? [] : result.diagnostics ?? [], headValidation);
|
|
11134
|
+
return injectSearchLocaleFilters(html, {
|
|
11135
|
+
locales: availableLocales ?? [],
|
|
11136
|
+
current: locale,
|
|
11137
|
+
defaultLocale: defaultLocale ?? availableLocales?.[0]?.code ?? "en"
|
|
11138
|
+
});
|
|
9550
11139
|
}
|
|
9551
11140
|
async function externalizeSharedPageAssets(pages, outDir, base) {
|
|
9552
11141
|
const optimized = (await importNapiModule()).externalizeSsgAssets(pages, outDir, base);
|
|
@@ -9631,6 +11220,7 @@ async function buildSsg(options, root) {
|
|
|
9631
11220
|
applyPermalinkRoutes(context, collected);
|
|
9632
11221
|
errors.push(...collected.errors);
|
|
9633
11222
|
const { outputPages, listedPages } = applyPublishState(context, collected);
|
|
11223
|
+
context.markdownSourcePages.push(...outputPages);
|
|
9634
11224
|
remapPermalinkNav(context, listedPages);
|
|
9635
11225
|
await applyPageResources(context, outputPages, generatedFiles, errors);
|
|
9636
11226
|
await generateOgImageAssets(context, collected, generatedFiles, errors);
|
|
@@ -9708,7 +11298,8 @@ async function createBuildSsgContext(options, root, srcDir, outDir, markdownFile
|
|
|
9708
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)),
|
|
9709
11299
|
siteName: await resolveSiteName$1(root, ssgOptions),
|
|
9710
11300
|
shouldGenerateOgImages: shouldGenerateOgImages(options),
|
|
9711
|
-
|
|
11301
|
+
markdownSourcePages: [],
|
|
11302
|
+
napi: ssgOptions.lastUpdated || ssgOptions.contributors || options.siteMaps?.enabled ? await importNapiModule() : void 0
|
|
9712
11303
|
};
|
|
9713
11304
|
}
|
|
9714
11305
|
/**
|
|
@@ -9738,6 +11329,7 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9738
11329
|
if (!options?.enabled) return;
|
|
9739
11330
|
const cacheDir = path$2.join(context.root, ".cache", "ox-content-resources");
|
|
9740
11331
|
const fatal = [];
|
|
11332
|
+
const dedupeStore = options.dedupe ? createResourceDedupeStore() : void 0;
|
|
9741
11333
|
for (const page of pages) {
|
|
9742
11334
|
const processed = await processPageResources({
|
|
9743
11335
|
html: page.transformedHtml,
|
|
@@ -9745,7 +11337,10 @@ async function applyPageResources(context, pages, generatedFiles, errors) {
|
|
|
9745
11337
|
outputPath: page.routePaths.outputPath,
|
|
9746
11338
|
srcDir: context.srcDir,
|
|
9747
11339
|
options,
|
|
9748
|
-
cacheDir
|
|
11340
|
+
cacheDir,
|
|
11341
|
+
outDir: context.outDir,
|
|
11342
|
+
base: context.base,
|
|
11343
|
+
dedupeStore
|
|
9749
11344
|
});
|
|
9750
11345
|
page.transformedHtml = processed.html;
|
|
9751
11346
|
generatedFiles.push(...processed.files);
|
|
@@ -9819,7 +11414,8 @@ function applyPublishState(context, collected) {
|
|
|
9819
11414
|
};
|
|
9820
11415
|
}
|
|
9821
11416
|
async function transformSsgPage(context, inputPath) {
|
|
9822
|
-
const
|
|
11417
|
+
const content = await fs$3.readFile(inputPath, "utf-8");
|
|
11418
|
+
const result = await transformMarkdown(content, inputPath, context.options, {
|
|
9823
11419
|
convertMdLinks: true,
|
|
9824
11420
|
baseUrl: context.base,
|
|
9825
11421
|
sourcePath: inputPath
|
|
@@ -9829,11 +11425,12 @@ async function transformSsgPage(context, inputPath) {
|
|
|
9829
11425
|
const title = extractTitle$1(transformedHtml, frontmatter);
|
|
9830
11426
|
return {
|
|
9831
11427
|
inputPath,
|
|
11428
|
+
source: content,
|
|
9832
11429
|
routePaths: getRoutePaths(inputPath, context.srcDir, context.outDir, context.base, context.ssgOptions.extension, context.ssgOptions.siteUrl),
|
|
9833
11430
|
transformedHtml,
|
|
9834
11431
|
title,
|
|
9835
11432
|
description: frontmatter.description,
|
|
9836
|
-
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,
|
|
9837
11434
|
contributors: contributorsForPage(context, inputPath),
|
|
9838
11435
|
frontmatter,
|
|
9839
11436
|
toc: result.toc
|
|
@@ -9924,17 +11521,18 @@ async function generateHtmlPages(context, pageResults, collected, errors) {
|
|
|
9924
11521
|
async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
9925
11522
|
const { ogImageUrlMap } = collected;
|
|
9926
11523
|
const pageOgImage = context.shouldGenerateOgImages && ogImageUrlMap.has(pageResult.inputPath) ? ogImageUrlMap.get(pageResult.inputPath) : context.ssgOptions.ogImage;
|
|
11524
|
+
const markdownSource = pageMarkdownSourceHref(context, pageResult);
|
|
9927
11525
|
if (context.ssgOptions.render) {
|
|
9928
11526
|
const nav = context.versionNavigation ? rewriteVersionedNavGroups(context.navItems, context.versionNavigation) : context.navItems;
|
|
9929
|
-
return renderPage(toThemePageData(pageResult), {
|
|
11527
|
+
return applyMarkdownSourceAlternate(context, renderPage(toThemePageData(pageResult, markdownSource), {
|
|
9930
11528
|
theme: context.ssgOptions.render,
|
|
9931
11529
|
siteName: context.siteName,
|
|
9932
11530
|
base: context.base,
|
|
9933
11531
|
nav,
|
|
9934
|
-
pages: allPageResults.map(toThemePageData)
|
|
9935
|
-
});
|
|
11532
|
+
pages: allPageResults.map((page) => toThemePageData(page, pageMarkdownSourceHref(context, page)))
|
|
11533
|
+
}), markdownSource);
|
|
9936
11534
|
}
|
|
9937
|
-
if (context.ssgOptions.bare) return generateBarePage({
|
|
11535
|
+
if (context.ssgOptions.bare) return applyMarkdownSourceAlternate(context, generateBarePage({
|
|
9938
11536
|
title: pageResult.title,
|
|
9939
11537
|
content: pageResult.transformedHtml,
|
|
9940
11538
|
lang: context.ssgOptions.lang ?? getPageLocale(pageResult.routePaths.urlPath, context.options.i18n),
|
|
@@ -9945,7 +11543,7 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
9945
11543
|
head: context.ssgOptions.head,
|
|
9946
11544
|
bodyStart: context.ssgOptions.bodyStart,
|
|
9947
11545
|
bodyEnd: context.ssgOptions.bodyEnd
|
|
9948
|
-
});
|
|
11546
|
+
}), markdownSource);
|
|
9949
11547
|
const pageData = createSsgPageData(pageResult);
|
|
9950
11548
|
const versionNavigation = context.versionNavigation;
|
|
9951
11549
|
if (versionNavigation) {
|
|
@@ -9986,10 +11584,18 @@ async function renderSsgPage(context, pageResult, collected, allPageResults) {
|
|
|
9986
11584
|
base: context.base,
|
|
9987
11585
|
roots: versionNavigation ? versionedLocaleRoots(versionNavigation, i18n.locales, i18n.defaultLocale, i18n.hideDefaultLocale) : void 0
|
|
9988
11586
|
}) : void 0;
|
|
9989
|
-
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 ?? {
|
|
9990
11588
|
enabled: false,
|
|
9991
11589
|
members: []
|
|
9992
|
-
}, context.ssgOptions.pageChrome, versionNavigation?.root.href, context.ssgOptions.jsonLd, context.ssgOptions.siteUrl);
|
|
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);
|
|
9993
11599
|
}
|
|
9994
11600
|
function rewritePagerOverride(pager, context) {
|
|
9995
11601
|
return pager?.href ? {
|
|
@@ -9998,7 +11604,7 @@ function rewritePagerOverride(pager, context) {
|
|
|
9998
11604
|
} : pager;
|
|
9999
11605
|
}
|
|
10000
11606
|
/** Maps an internal page result onto the theme renderer's page shape. */
|
|
10001
|
-
function toThemePageData(pageResult) {
|
|
11607
|
+
function toThemePageData(pageResult, markdownSource) {
|
|
10002
11608
|
return {
|
|
10003
11609
|
title: pageResult.title,
|
|
10004
11610
|
description: pageResult.description,
|
|
@@ -10008,6 +11614,7 @@ function toThemePageData(pageResult) {
|
|
|
10008
11614
|
contributors: pageResult.contributors,
|
|
10009
11615
|
path: pageResult.inputPath,
|
|
10010
11616
|
url: pageResult.routePaths.href,
|
|
11617
|
+
markdownSource,
|
|
10011
11618
|
frontmatter: pageResult.frontmatter,
|
|
10012
11619
|
layout: typeof pageResult.frontmatter.layout === "string" ? pageResult.frontmatter.layout : void 0
|
|
10013
11620
|
};
|
|
@@ -10120,6 +11727,7 @@ async function applyDocumentationVersions(generatedPages, context, errors) {
|
|
|
10120
11727
|
...page.routePaths,
|
|
10121
11728
|
...prefixRoutePaths(page.routePaths, entry.prefix, context.outDir, context.base)
|
|
10122
11729
|
};
|
|
11730
|
+
context.markdownSourcePages.push(...outputPages);
|
|
10123
11731
|
snapContext.versionNavigation = createVersionNavigationContext({
|
|
10124
11732
|
prefix: entry.prefix,
|
|
10125
11733
|
base: context.base,
|
|
@@ -10232,6 +11840,20 @@ async function writeGeneratedPages(generatedPages, context, generatedFiles, list
|
|
|
10232
11840
|
errors.push(feeds.warning);
|
|
10233
11841
|
console.warn(feeds.warning);
|
|
10234
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);
|
|
10235
11857
|
}
|
|
10236
11858
|
/** Turns an SSG `urlPath` (`guide` or `/`) into a same-origin dest (`/guide`). */
|
|
10237
11859
|
function sitePathFromUrlPath(urlPath) {
|
|
@@ -10245,6 +11867,7 @@ function sitemapPages(context, listedPages, outputPages) {
|
|
|
10245
11867
|
loc: canonicalPageUrl(context, page.routePaths.urlPath) ?? "",
|
|
10246
11868
|
title: page.title,
|
|
10247
11869
|
description: page.description,
|
|
11870
|
+
lastUpdated: page.lastUpdated,
|
|
10248
11871
|
draft: page.frontmatter.draft === true,
|
|
10249
11872
|
unlisted: Boolean(context.options.publishState?.enabled) && !listedPaths.has(page.inputPath)
|
|
10250
11873
|
}));
|
|
@@ -10342,7 +11965,8 @@ function createDevServerCache() {
|
|
|
10342
11965
|
navGroups: null,
|
|
10343
11966
|
localePages: null,
|
|
10344
11967
|
pages: /* @__PURE__ */ new Map(),
|
|
10345
|
-
siteName: null
|
|
11968
|
+
siteName: null,
|
|
11969
|
+
markdownSourceIndex: null
|
|
10346
11970
|
};
|
|
10347
11971
|
}
|
|
10348
11972
|
/**
|
|
@@ -10351,6 +11975,7 @@ function createDevServerCache() {
|
|
|
10351
11975
|
function invalidateNavCache(cache) {
|
|
10352
11976
|
cache.navGroups = null;
|
|
10353
11977
|
cache.localePages = null;
|
|
11978
|
+
cache.markdownSourceIndex = null;
|
|
10354
11979
|
cache.pages.clear();
|
|
10355
11980
|
}
|
|
10356
11981
|
/**
|
|
@@ -10358,6 +11983,7 @@ function invalidateNavCache(cache) {
|
|
|
10358
11983
|
*/
|
|
10359
11984
|
function invalidatePageCache(cache, filePath) {
|
|
10360
11985
|
cache.pages.delete(filePath);
|
|
11986
|
+
cache.markdownSourceIndex = null;
|
|
10361
11987
|
}
|
|
10362
11988
|
/**
|
|
10363
11989
|
* Resolve site name from options or package.json.
|
|
@@ -10447,13 +12073,35 @@ async function renderPage$1(filePath, options, navGroups, siteName, base, root,
|
|
|
10447
12073
|
pages: localePages,
|
|
10448
12074
|
base
|
|
10449
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;
|
|
10450
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 ?? {
|
|
10451
12086
|
enabled: false,
|
|
10452
12087
|
members: []
|
|
10453
|
-
}, options.ssg.pageChrome, void 0, options.ssg.jsonLd, options.ssg.siteUrl);
|
|
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);
|
|
10454
12090
|
html = injectViteHmrClient(html);
|
|
10455
12091
|
return html;
|
|
10456
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
|
+
}
|
|
10457
12105
|
/**
|
|
10458
12106
|
* Create the dev server middleware for SSG page serving.
|
|
10459
12107
|
*/
|
|
@@ -10466,6 +12114,19 @@ function createDevServerMiddleware(options, root, cache) {
|
|
|
10466
12114
|
let routeUrl = url;
|
|
10467
12115
|
if (base !== "/" && routeUrl.startsWith(base)) routeUrl = "/" + routeUrl.slice(base.length);
|
|
10468
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
|
+
}
|
|
10469
12130
|
const filePath = await resolveMarkdownFile(routeUrl, srcDir, options.extensions);
|
|
10470
12131
|
if (!filePath) return next();
|
|
10471
12132
|
try {
|
|
@@ -10982,6 +12643,43 @@ function resolveCardOptions(options) {
|
|
|
10982
12643
|
return { enabled: options.enabled ?? true };
|
|
10983
12644
|
}
|
|
10984
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
|
|
10985
12683
|
//#region src/include-options.ts
|
|
10986
12684
|
function resolveIncludeOptions(options) {
|
|
10987
12685
|
if (!options) return { enabled: false };
|
|
@@ -11556,6 +13254,12 @@ function createFrameworkMarkdownOptions(options) {
|
|
|
11556
13254
|
},
|
|
11557
13255
|
attrs: { enabled: false },
|
|
11558
13256
|
badges: { enabled: false },
|
|
13257
|
+
magicLinks: {
|
|
13258
|
+
enabled: false,
|
|
13259
|
+
aliases: {},
|
|
13260
|
+
favicon: false,
|
|
13261
|
+
imageOverrides: []
|
|
13262
|
+
},
|
|
11559
13263
|
containers: {
|
|
11560
13264
|
enabled: false,
|
|
11561
13265
|
types: {}
|
|
@@ -12555,7 +14259,7 @@ function createSsgPlugin(resolvedOptions, getRoot, ssgDevCache) {
|
|
|
12555
14259
|
for (const error of result.errors) console.warn(`[ox-content] ${error}`);
|
|
12556
14260
|
} catch (err) {
|
|
12557
14261
|
console.error("[ox-content] SSG build failed:", err);
|
|
12558
|
-
if (err instanceof PageResourceError) throw err;
|
|
14262
|
+
if (err instanceof PageResourceError || err instanceof BlogFeedError) throw err;
|
|
12559
14263
|
}
|
|
12560
14264
|
}
|
|
12561
14265
|
};
|
|
@@ -12670,6 +14374,7 @@ function resolveOptions(options) {
|
|
|
12670
14374
|
gfm: options.gfm ?? true,
|
|
12671
14375
|
mdx: options.mdx,
|
|
12672
14376
|
footnotes: options.footnotes ?? true,
|
|
14377
|
+
semanticFootnotes: options.semanticFootnotes ?? false,
|
|
12673
14378
|
tables: options.tables ?? true,
|
|
12674
14379
|
taskLists: options.taskLists ?? true,
|
|
12675
14380
|
strikethrough: options.strikethrough ?? true,
|
|
@@ -12680,6 +14385,7 @@ function resolveOptions(options) {
|
|
|
12680
14385
|
emojiShortcodes: resolveEmojiShortcodeOptions(options.emojiShortcodes),
|
|
12681
14386
|
attrs: resolveAttrsOptions(options.attrs),
|
|
12682
14387
|
badges: resolveBadgeOptions(options.badges),
|
|
14388
|
+
magicLinks: resolveMagicLinkOptions(options.magicLinks),
|
|
12683
14389
|
containers: resolveContainerOptions(options.containers),
|
|
12684
14390
|
images: resolveImageOptions(options.images),
|
|
12685
14391
|
codeImports: resolveCodeImportOptions(options.codeImports),
|
|
@@ -12699,6 +14405,7 @@ function resolveOptions(options) {
|
|
|
12699
14405
|
frontmatter: options.frontmatter ?? true,
|
|
12700
14406
|
toc: options.toc ?? true,
|
|
12701
14407
|
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
14408
|
+
headingPermalinks: resolveHeadingPermalinksOptions(options.headingPermalinks),
|
|
12702
14409
|
ogImage: options.ogImage ?? false,
|
|
12703
14410
|
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
12704
14411
|
transformers: options.transformers ?? [],
|
|
@@ -12989,6 +14696,6 @@ function normalizeRuntimeBase(base) {
|
|
|
12989
14696
|
return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
|
|
12990
14697
|
}
|
|
12991
14698
|
//#endregion
|
|
12992
|
-
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, renderHtmlToFrameworkCode, renderHtmlToReactComponent, renderHtmlToReactCreateElement, renderHtmlToSvelteComponent, renderHtmlToVueComponent, renderHtmlToVueH, renderIslandComponentImports, renderMarkdownStream, renderPage, renderToString, resolveBadgeOptions, resolveBlogCollectionName, resolveBlogOptions, resolveBuiltinEmbedOptions, resolveCardOptions, resolveCascadeOptions, resolveCollectionsOptions, resolveContentRootPath, resolveDocsOptions, resolveDocumentComponentImports, resolveFeedsOptions, resolveFileTreeOptions, 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 };
|
|
12993
14700
|
|
|
12994
14701
|
//# sourceMappingURL=index.mjs.map
|