@xingwangzhe/stalux 1.51.1 → 1.52.2
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/.release/cli.mjs +16 -20
- package/package.json +48 -45
- package/src/components/stalux/footer/FooterBadges.astro +2 -2
- package/src/components/stalux/layout/head.astro +1 -1
- package/src/components/stalux/posts/postContent.astro +1 -1
- package/src/index.ts +57 -51
- package/src/internal/cjk-search-index.ts +4 -7
- package/src/internal/components-plugin.ts +1 -3
- package/src/internal/font-slices.ts +36 -29
- package/src/internal/html-images.ts +1 -1
- package/src/internal/html-styles.ts +1 -1
- package/src/internal/image-dimensions.ts +3 -8
- package/src/internal/injected-routes.ts +1 -3
- package/src/internal/page-output-cache.ts +11 -15
- package/src/internal/post-cover.ts +22 -26
- package/src/internal/project-mode.ts +3 -2
- package/src/internal/public-assets.ts +42 -23
- package/src/internal/runtime-cache-key.ts +42 -27
- package/src/internal/satteri-config.ts +2 -2
- package/src/internal/search-style.ts +6 -7
- package/src/internal/social-card-cover.ts +12 -10
- package/src/internal/social-card-renderer.ts +8 -11
- package/src/internal/webmcp-mutations.ts +11 -5
- package/src/layouts/Stalux.astro +4 -4
- package/src/pages/openapi.json.ts +1 -1
- package/src/plugins/feature-flags.ts +2 -2
- package/src/schemas/collections.ts +1 -1
- package/src/schemas/config.ts +4 -1
- package/src/scripts/list-reveal.ts +7 -5
- package/src/scripts/swup-router.ts +1 -1
- package/src/scripts/weather.ts +2 -2
- package/src/scripts/webmcp.ts +4 -4
- package/src/utils/accent-color.ts +1 -1
- package/src/utils/ai-discovery.ts +6 -6
- package/src/utils/badge-generator.ts +1 -2
- package/src/utils/cc.ts +2 -2
- package/src/utils/dayjs.ts +5 -5
- package/src/utils/diagnostics.ts +3 -3
- package/src/utils/public-routes.ts +3 -3
- package/src/utils/seo-document.ts +2 -2
- package/src/utils/site-path.ts +1 -1
- package/src/utils/social-image.ts +1 -3
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
1
|
import path from "node:path";
|
|
4
2
|
import { imageSize } from "image-size";
|
|
5
3
|
|
|
@@ -52,8 +50,9 @@ function assertDimensions(bytes: Buffer): void {
|
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
async function readLocalCover(local: string): Promise<Buffer> {
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
const file = Bun.file(local);
|
|
54
|
+
if (file.size > MAX_COVER_BYTES) throw new Error("cover exceeds 8 MiB");
|
|
55
|
+
const bytes = Buffer.from(await file.bytes());
|
|
57
56
|
assertDimensions(bytes);
|
|
58
57
|
return bytes;
|
|
59
58
|
}
|
|
@@ -71,16 +70,15 @@ async function readResponseBytes(response: Response): Promise<Buffer> {
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
async function fetchCover(url: URL, cacheFile: string): Promise<Buffer> {
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
73
|
+
const cached = Bun.file(cacheFile);
|
|
74
|
+
if (cached.size > 0 && Date.now() - cached.lastModified < COVER_CACHE_TTL_MS)
|
|
75
|
+
return Buffer.from(await cached.bytes());
|
|
77
76
|
const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
|
78
77
|
if (!(response.ok && response.headers.get("content-type")?.startsWith("image/")))
|
|
79
78
|
throw new Error(`cover returned HTTP ${response.status} or non-image content`);
|
|
80
79
|
const bytes = await readResponseBytes(response);
|
|
81
80
|
assertDimensions(bytes);
|
|
82
|
-
await
|
|
83
|
-
await writeFile(cacheFile, bytes);
|
|
81
|
+
await Bun.write(cacheFile, bytes);
|
|
84
82
|
return bytes;
|
|
85
83
|
}
|
|
86
84
|
|
|
@@ -91,6 +89,10 @@ function loadCoverUncached(cover: string, paths: SocialCardPaths): Promise<Buffe
|
|
|
91
89
|
if (!["https:", "http:"].includes(url.protocol)) throw new Error("invalid cover URL");
|
|
92
90
|
return fetchCover(
|
|
93
91
|
url,
|
|
94
|
-
path.join(
|
|
92
|
+
path.join(
|
|
93
|
+
paths.cacheDir,
|
|
94
|
+
"covers",
|
|
95
|
+
new Bun.CryptoHasher("sha256").update(url.href).digest("hex"),
|
|
96
|
+
),
|
|
95
97
|
);
|
|
96
98
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
1
|
import path from "node:path";
|
|
4
2
|
import { Renderer } from "@takumi-rs/core";
|
|
5
3
|
import { type SocialCardData, socialCardFormat } from "../utils/social-image";
|
|
@@ -23,7 +21,7 @@ function getRenderer(fontPath: string): Promise<Renderer> {
|
|
|
23
21
|
const renderer = new Renderer({ cacheMaxBytes: 32 * 1024 * 1024 });
|
|
24
22
|
await renderer.registerFont({
|
|
25
23
|
name: CARD_FONT_FAMILY,
|
|
26
|
-
data: await
|
|
24
|
+
data: await Bun.file(fontPath).bytes(),
|
|
27
25
|
});
|
|
28
26
|
return renderer;
|
|
29
27
|
})();
|
|
@@ -36,9 +34,9 @@ const fontDigests = new Map<string, Promise<string>>();
|
|
|
36
34
|
function getFontDigest(fontPath: string): Promise<string> {
|
|
37
35
|
let pending = fontDigests.get(fontPath);
|
|
38
36
|
if (!pending) {
|
|
39
|
-
pending =
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
pending = Bun.file(fontPath)
|
|
38
|
+
.bytes()
|
|
39
|
+
.then((bytes) => new Bun.CryptoHasher("sha256").update(bytes).digest("hex"));
|
|
42
40
|
fontDigests.set(fontPath, pending);
|
|
43
41
|
}
|
|
44
42
|
return pending;
|
|
@@ -62,13 +60,13 @@ export async function renderSocialCard(
|
|
|
62
60
|
// always reuses the cached render, whatever the filesystem timestamps say.
|
|
63
61
|
// It is checked before any raster work, so a warm build only reads bytes.
|
|
64
62
|
const fontDigest = await getFontDigest(paths.fontPath);
|
|
65
|
-
const key =
|
|
63
|
+
const key = new Bun.CryptoHasher("sha256")
|
|
66
64
|
.update(JSON.stringify([TEMPLATE, format, data, fontDigest]))
|
|
67
65
|
.update(coverBytes ?? "")
|
|
68
66
|
.digest("hex");
|
|
69
67
|
const cacheFile = path.join(paths.cacheDir, `${key}.${format}`);
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
68
|
+
const cached = Bun.file(cacheFile);
|
|
69
|
+
if (cached.size > 0) return Buffer.from(await cached.bytes());
|
|
72
70
|
|
|
73
71
|
const renderer = await getRenderer(paths.fontPath);
|
|
74
72
|
const render = (withCover: boolean) =>
|
|
@@ -92,7 +90,6 @@ export async function renderSocialCard(
|
|
|
92
90
|
cacheable = false;
|
|
93
91
|
}
|
|
94
92
|
if (!cacheable) return bytes;
|
|
95
|
-
await
|
|
96
|
-
await writeFile(cacheFile, bytes);
|
|
93
|
+
await Bun.write(cacheFile, bytes);
|
|
97
94
|
return bytes;
|
|
98
95
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
2
|
import type { Plugin } from "vite";
|
|
5
3
|
import { affectsDeclarativeTools } from "../scripts/webmcp-mutations";
|
|
6
4
|
|
|
@@ -21,9 +19,17 @@ export function optimizeWebMcpMutations(code: string, version: string): string |
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export function webMcpMutationsPlugin(): Plugin {
|
|
24
|
-
const entry =
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const entry = Bun.resolveSync(
|
|
23
|
+
"@mcp-b/webmcp-polyfill",
|
|
24
|
+
Bun.fileURLToPath(new URL(".", import.meta.url)),
|
|
25
|
+
);
|
|
26
|
+
const version = (
|
|
27
|
+
JSON.parse(
|
|
28
|
+
readFileSync(
|
|
29
|
+
Bun.fileURLToPath(new URL("../package.json", Bun.pathToFileURL(entry))),
|
|
30
|
+
"utf8",
|
|
31
|
+
),
|
|
32
|
+
) as { version: string }
|
|
27
33
|
).version;
|
|
28
34
|
return {
|
|
29
35
|
name: "stalux:webmcp-mutations",
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -46,10 +46,10 @@ const htmlLang = siteConfig.lang || "zh-CN";
|
|
|
46
46
|
// 保证增量构建依赖图 hash 稳定。
|
|
47
47
|
const llmPromoteRaw = promoteConfig?.llm_promote ?? "";
|
|
48
48
|
const llmPromote = llmPromoteRaw
|
|
49
|
-
?.replace(/\{author\}/
|
|
50
|
-
?.replace(/\{url\}/
|
|
51
|
-
?.replace(/\{title\}/
|
|
52
|
-
?.replace(/\{cc\}/
|
|
49
|
+
?.replace(/\{author\}/gu, authorConfig.name)
|
|
50
|
+
?.replace(/\{url\}/gu, siteConfig.url)
|
|
51
|
+
?.replace(/\{title\}/gu, siteConfig.title)
|
|
52
|
+
?.replace(/\{cc\}/gu, "CC-BY-NC-SA-4.0");
|
|
53
53
|
const routeKind = getRouteKind(Astro.url.pathname);
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -79,8 +79,8 @@ function getState(ctx: PluginContext): FeatureFlagsState {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function getFrontmatter(ctx: PluginContext): Record<string, unknown> {
|
|
82
|
-
const astroData = ctx.data.astro;
|
|
83
|
-
if (!astroData) throw new Error("Satteri did not provide Astro frontmatter data");
|
|
82
|
+
const astroData = ctx.data.astro as { frontmatter?: Record<string, unknown> } | undefined;
|
|
83
|
+
if (!astroData?.frontmatter) throw new Error("Satteri did not provide Astro frontmatter data");
|
|
84
84
|
return astroData.frontmatter;
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -61,7 +61,7 @@ export interface CollectionPaths {
|
|
|
61
61
|
* 因此在 schema 层直接拒绝,让构建尽快失败,而不是产出错误时间。
|
|
62
62
|
*/
|
|
63
63
|
export const PADDED_DATETIME_PATTERN =
|
|
64
|
-
/^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:?\d{2})?)
|
|
64
|
+
/^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:?\d{2})?)?$/u;
|
|
65
65
|
|
|
66
66
|
const datetimeField = (label: string) =>
|
|
67
67
|
z
|
package/src/schemas/config.ts
CHANGED
|
@@ -18,7 +18,10 @@ export const siteSchema = z.object({
|
|
|
18
18
|
description: z.string().min(1, "site.description is required"),
|
|
19
19
|
accentColor: z
|
|
20
20
|
.string()
|
|
21
|
-
.regex(
|
|
21
|
+
.regex(
|
|
22
|
+
/^#[\da-fA-F]{6}$/u,
|
|
23
|
+
"site.accentColor must be a six-digit HEX color such as #EAB308",
|
|
24
|
+
)
|
|
22
25
|
.optional()
|
|
23
26
|
.default("#EAB308"),
|
|
24
27
|
seoTitle: z.string().min(1).optional(),
|
|
@@ -274,9 +274,11 @@ declare global {
|
|
|
274
274
|
__staluxListRevealPrime?: boolean;
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
277
|
+
if (
|
|
278
|
+
typeof document !== "undefined" &&
|
|
279
|
+
typeof window !== "undefined" &&
|
|
280
|
+
!window.__staluxListRevealPrime
|
|
281
|
+
) {
|
|
282
|
+
window.__staluxListRevealPrime = true;
|
|
283
|
+
document.addEventListener("astro:after-swap", primeListReveal);
|
|
282
284
|
}
|
|
@@ -111,7 +111,7 @@ export function installSwupRouter(): Swup {
|
|
|
111
111
|
document.querySelector("base[target]")?.getAttribute("target");
|
|
112
112
|
return (
|
|
113
113
|
getRouteKind(url.pathname) === "other" ||
|
|
114
|
-
/\.(?:md|json|xml|txt|html?|pdf|png|webp|avif|svg|jpe?g)$/
|
|
114
|
+
/\.(?:md|json|xml|txt|html?|pdf|png|webp|avif|svg|jpe?g)$/iu.test(url.pathname) ||
|
|
115
115
|
Boolean(el?.closest("[data-no-swup],[data-astro-reload],[rel~=external]")) ||
|
|
116
116
|
Boolean(target && target !== "_self")
|
|
117
117
|
);
|
package/src/scripts/weather.ts
CHANGED
|
@@ -57,7 +57,7 @@ function parseLocation(value: unknown): ApproximateLocation {
|
|
|
57
57
|
"ip" in value &&
|
|
58
58
|
typeof value.ip === "string" &&
|
|
59
59
|
value.ip.length <= 45 &&
|
|
60
|
-
/^[0-9a-f:.]+$/
|
|
60
|
+
/^[0-9a-f:.]+$/iu.test(value.ip);
|
|
61
61
|
return { city: value.city.trim(), lat, lon, ...(usableIp ? { ip: value.ip as string } : {}) };
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -97,7 +97,7 @@ function parseWeatherData(value: unknown): WeatherData | undefined {
|
|
|
97
97
|
if (
|
|
98
98
|
typeof value.ip !== "string" ||
|
|
99
99
|
value.ip.length > 45 ||
|
|
100
|
-
!/^[0-9a-f:.]+$/
|
|
100
|
+
!/^[0-9a-f:.]+$/iu.test(value.ip)
|
|
101
101
|
)
|
|
102
102
|
return undefined;
|
|
103
103
|
ip = value.ip;
|
package/src/scripts/webmcp.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
// 浏览器无原生 document.modelContext 时由 polyfill 兜底安装;
|
|
19
19
|
// 有原生实现或已存在 WebMCP-aware 扩展时,polyfill 检测到后自动 no-op。
|
|
20
|
-
// ESM entry exports
|
|
21
|
-
import {
|
|
20
|
+
// ESM entry exports installWebMCP; importing it alone does not install the API.
|
|
21
|
+
import { installWebMCP } from "@mcp-b/webmcp-polyfill";
|
|
22
22
|
import { createPostLookup } from "../utils/post-lookup";
|
|
23
23
|
import { sitePath } from "../utils/site-path";
|
|
24
24
|
import { createClientLogger } from "./logger";
|
|
@@ -211,7 +211,7 @@ function postIdFromUrl(value: string): string | null {
|
|
|
211
211
|
const prefix = sitePath("posts/");
|
|
212
212
|
if (!url.pathname.startsWith(prefix)) return null;
|
|
213
213
|
const suffix = url.pathname.slice(prefix.length);
|
|
214
|
-
const path = suffix.endsWith("/") ? suffix.slice(0, -1) : suffix.replace(/\.md
|
|
214
|
+
const path = suffix.endsWith("/") ? suffix.slice(0, -1) : suffix.replace(/\.md$/u, "");
|
|
215
215
|
if (!path || path.includes("/")) return null;
|
|
216
216
|
return decodeURIComponent(path);
|
|
217
217
|
} catch {
|
|
@@ -841,7 +841,7 @@ async function registerTools(
|
|
|
841
841
|
registerPageLifecycle("webmcp", () => {
|
|
842
842
|
// Astro can hoist processed scripts from conditional template branches.
|
|
843
843
|
if (document.body?.dataset.staluxWebmcpEnabled === "false") return;
|
|
844
|
-
|
|
844
|
+
installWebMCP();
|
|
845
845
|
const controller = new AbortController();
|
|
846
846
|
void registerTools(controller.signal)
|
|
847
847
|
.then((r) => {
|
|
@@ -61,22 +61,22 @@ function getSiteConfig(config: ConfigMap): Record<string, unknown> {
|
|
|
61
61
|
export function getSite(config: ConfigMap, contextSite?: string): string {
|
|
62
62
|
const siteData = getSiteConfig(config);
|
|
63
63
|
const configUrl = siteData.url as string | undefined;
|
|
64
|
-
return (configUrl || contextSite || "").replace(
|
|
64
|
+
return (configUrl || contextSite || "").replace(/\/$/u, "");
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** 将任意空白压缩为单行 */
|
|
68
68
|
function inlineText(text: string | undefined): string {
|
|
69
|
-
return (text || "").replace(/\s+/
|
|
69
|
+
return (text || "").replace(/\s+/gu, " ").trim();
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** 转义 markdown 文本中的特殊字符(用于描述) */
|
|
73
73
|
function escapeMd(text: string | undefined): string {
|
|
74
|
-
return inlineText(text).replace(/([\\[*_`#])/
|
|
74
|
+
return inlineText(text).replace(/([\\[*_`#])/gu, "\\$1");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** 转义 markdown 链接标题中的特殊字符 */
|
|
78
78
|
function escapeMdLinkTitle(text: string | undefined): string {
|
|
79
|
-
return inlineText(text).replace(/([\\[\]])/
|
|
79
|
+
return inlineText(text).replace(/([\\[\]])/gu, "\\$1");
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** 构建 markdown 列表项 */
|
|
@@ -125,7 +125,7 @@ function getMediaLinks(
|
|
|
125
125
|
try {
|
|
126
126
|
const hostname = new URL(url.startsWith("http") ? url : `https://example.com${url}`)
|
|
127
127
|
.hostname;
|
|
128
|
-
const domain = hostname.replace(/^www
|
|
128
|
+
const domain = hostname.replace(/^www\./u, "").split(".")[0];
|
|
129
129
|
name = domain ? domain.charAt(0).toUpperCase() + domain.slice(1) : "Link";
|
|
130
130
|
} catch {
|
|
131
131
|
logDetail(
|
|
@@ -147,7 +147,7 @@ function getEmail(config: ConfigMap): string | undefined {
|
|
|
147
147
|
| undefined;
|
|
148
148
|
if (!items) return undefined;
|
|
149
149
|
const mail = items.find((l) => l.link?.startsWith("mailto:"))?.link;
|
|
150
|
-
if (mail) return mail.replace(/^mailto
|
|
150
|
+
if (mail) return mail.replace(/^mailto:/u, "");
|
|
151
151
|
return undefined;
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import type { AstroRuntimeLogger } from "astro";
|
|
3
2
|
/**
|
|
4
3
|
* 徽章生成器工具
|
|
@@ -59,7 +58,7 @@ export function generateBadge(options: BadgeOptions, logger?: AstroRuntimeLogger
|
|
|
59
58
|
|
|
60
59
|
/** Content-addressed assets are shared by all pages and remain lazy inside details. */
|
|
61
60
|
export function badgeAssetId(svg: string): string {
|
|
62
|
-
return
|
|
61
|
+
return new Bun.CryptoHasher("sha256").update(svg).digest("hex").slice(0, 24);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
export function normalizeBadgeStyle(style: string | undefined): BadgeOptions["style"] {
|
package/src/utils/cc.ts
CHANGED
|
@@ -11,7 +11,7 @@ export function buildCCLink(licenseCode: string): string {
|
|
|
11
11
|
return "https://creativecommons.org/publicdomain/zero/1.0/";
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
|
|
14
|
+
const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/u);
|
|
15
15
|
if (!match) {
|
|
16
16
|
return "https://creativecommons.org/licenses/by-nc-sa/4.0/";
|
|
17
17
|
}
|
|
@@ -36,7 +36,7 @@ export function buildCCName(licenseCode: string, t: Translator["t"]): string {
|
|
|
36
36
|
return t("cc.zero");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
|
|
39
|
+
const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/u);
|
|
40
40
|
if (!match) {
|
|
41
41
|
// 无法解析时回退为默认 CC-BY-NC-SA-4.0
|
|
42
42
|
const elements = ["BY", "NC", "SA"].map((code) => t(`cc.${code}`)).join("-");
|
package/src/utils/dayjs.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const toTimestamp = (v: unknown, timeZone?: string): number =>
|
|
|
33
33
|
* RSS pubDate、页面 datetime 全体平移 8 小时(2026-10-24 事故)。
|
|
34
34
|
*/
|
|
35
35
|
const NAIVE_DATE_PATTERN =
|
|
36
|
-
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)
|
|
36
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)?$/u;
|
|
37
37
|
|
|
38
38
|
/** 用共享的 formatter 计算目标时区在给定时刻的 UTC 偏移(毫秒,东八区为 +28800000) */
|
|
39
39
|
function zoneOffsetMs(date: Date, timeZone: string): number {
|
|
@@ -128,7 +128,7 @@ export function format(date: Date | string | number, formatStr: string): string
|
|
|
128
128
|
mm: pad2(d.getMinutes()),
|
|
129
129
|
ss: pad2(d.getSeconds()),
|
|
130
130
|
};
|
|
131
|
-
return formatStr.replace(/yyyy|MM|dd|HH|mm|ss/
|
|
131
|
+
return formatStr.replace(/yyyy|MM|dd|HH|mm|ss/gu, (t) => tokens[t] ?? t);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
@@ -159,11 +159,11 @@ export function formatInTimeZone(
|
|
|
159
159
|
if (offset === "GMT" || offset === "UTC") {
|
|
160
160
|
offset = "+00:00";
|
|
161
161
|
} else {
|
|
162
|
-
offset = offset.replace(/^GMT
|
|
162
|
+
offset = offset.replace(/^GMT/u, "").replace(/^UTC/u, "");
|
|
163
163
|
if (!(offset.startsWith("+") || offset.startsWith("-"))) {
|
|
164
164
|
offset = `+${offset}`;
|
|
165
165
|
}
|
|
166
|
-
const m = offset.match(/^([+-])(\d{1,2})(?::?(\d{2}))?$/);
|
|
166
|
+
const m = offset.match(/^([+-])(\d{1,2})(?::?(\d{2}))?$/u);
|
|
167
167
|
const sign = m?.[1];
|
|
168
168
|
const hours = m?.[2];
|
|
169
169
|
if (sign && hours) {
|
|
@@ -186,5 +186,5 @@ export function formatInTimeZone(
|
|
|
186
186
|
ss: second,
|
|
187
187
|
XXX: offset,
|
|
188
188
|
};
|
|
189
|
-
return formatStr.replace(/yyyy|MM|dd|HH|mm|ss|XXX/
|
|
189
|
+
return formatStr.replace(/yyyy|MM|dd|HH|mm|ss|XXX/gu, (t) => tokens[t] ?? t);
|
|
190
190
|
}
|
package/src/utils/diagnostics.ts
CHANGED
|
@@ -19,9 +19,9 @@ export function describeError(error: unknown): string {
|
|
|
19
19
|
}
|
|
20
20
|
const text = parts.join("\nCaused by: ");
|
|
21
21
|
return text
|
|
22
|
-
.replace(/(https?:\/\/[^\s?#]+)[?#][^\s)]+/
|
|
23
|
-
.replace(/(Bearer\s+)[^\s]+/
|
|
24
|
-
.replace(/((?:token|password|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/
|
|
22
|
+
.replace(/(https?:\/\/[^\s?#]+)[?#][^\s)]+/gu, "$1?[redacted]")
|
|
23
|
+
.replace(/(Bearer\s+)[^\s]+/giu, "$1[redacted]")
|
|
24
|
+
.replace(/((?:token|password|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Runtime loggers have no debug method. Detail is opt-in, then sent through official info. */
|
|
@@ -13,7 +13,7 @@ export type RouteKind =
|
|
|
13
13
|
| "other";
|
|
14
14
|
|
|
15
15
|
export function normalizePublicPath(pathname: string): string {
|
|
16
|
-
const path = pathname.split(/[?#]
|
|
16
|
+
const path = pathname.split(/[?#]/u, 1)[0]?.replace(/\/+$/u, "") ?? "";
|
|
17
17
|
return path || "/";
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -41,12 +41,12 @@ export function getMarkdownPath(pathname: string): string | undefined {
|
|
|
41
41
|
if (path === "/") return "/index.md";
|
|
42
42
|
if (path === "/tags" || path === "/categories") return `${path}/index.md`;
|
|
43
43
|
if (["/about", "/archives", "/links", "/words"].includes(path)) return `${path}.md`;
|
|
44
|
-
if (/^\/(?:posts|tags|categories)\/[^/]
|
|
44
|
+
if (/^\/(?:posts|tags|categories)\/[^/]+$/u.test(path)) return `${path}.md`;
|
|
45
45
|
return undefined;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export function toPublicUrl(site: string, pathname: string, markdown = false): string {
|
|
49
|
-
const base = site.replace(
|
|
49
|
+
const base = site.replace(/\/+$/u, "");
|
|
50
50
|
const normalized = normalizePublicPath(pathname);
|
|
51
51
|
const path = markdown ? getMarkdownPath(normalized) : normalized;
|
|
52
52
|
if (!path) return `${base}${normalized === "/" ? "/" : `${normalized}/`}`;
|
|
@@ -46,7 +46,7 @@ export interface SeoDocument {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function normalizeSiteUrl(value: string): string {
|
|
49
|
-
return new URL(value).href.replace(
|
|
49
|
+
return new URL(value).href.replace(/\/+$/u, "");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function absoluteUrl(value: string, base: string, protocol: string): string {
|
|
@@ -155,7 +155,7 @@ export function buildSeoDocument(input: SeoDocumentInput): SeoDocument {
|
|
|
155
155
|
]),
|
|
156
156
|
];
|
|
157
157
|
|
|
158
|
-
const sameAs = input.mediaLinks.filter((link) => /^https?:\/\//
|
|
158
|
+
const sameAs = input.mediaLinks.filter((link) => /^https?:\/\//iu.test(link));
|
|
159
159
|
const authorImage = absoluteUrl(author.avatar, siteUrl, input.protocol);
|
|
160
160
|
const authorEntity = {
|
|
161
161
|
"@type": "Person",
|
package/src/utils/site-path.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Resolve a known static resource under Astro's configured deployment base. */
|
|
2
2
|
export function sitePath(pathname: string, base = import.meta.env.BASE_URL): string {
|
|
3
|
-
return `${(base || "/").replace(
|
|
3
|
+
return `${(base || "/").replace(/\/+$/u, "")}/${pathname.replace(/^\/+/u, "")}`;
|
|
4
4
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
1
|
export interface SocialCardData {
|
|
4
2
|
title: string;
|
|
5
3
|
siteTitle: string;
|
|
@@ -43,7 +41,7 @@ export function socialCardFormat(data: Pick<SocialCardData, "cover">): SocialCar
|
|
|
43
41
|
|
|
44
42
|
export function socialCardPath(data: SocialCardData, base = "/"): string {
|
|
45
43
|
// Bump the template revision when layout changes. Metadata changes get a fresh URL.
|
|
46
|
-
const hash =
|
|
44
|
+
const hash = new Bun.CryptoHasher("sha256")
|
|
47
45
|
.update(JSON.stringify(["stalux-mono-v2", data]))
|
|
48
46
|
.digest("hex")
|
|
49
47
|
.slice(0, 20);
|