@xingwangzhe/stalux 1.35.11 → 1.35.13
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/README.md +24 -11
- package/package.json +5 -2
- package/src/components/stalux/common/Typewriter.astro +32 -1
- package/src/components/stalux/posts/postContent.astro +7 -1
- package/src/index.ts +51 -104
- package/src/internal/bounded-tasks.ts +28 -0
- package/src/internal/compact-styles.ts +55 -13
- package/src/internal/font-slices.ts +150 -47
- package/src/internal/html-images.ts +2 -0
- package/src/internal/html-output.ts +98 -0
- package/src/internal/image-dimensions.ts +99 -74
- package/src/internal/page-output-cache.ts +26 -9
- package/src/internal/public-assets.ts +59 -0
- package/src/internal/runtime-cache-key.ts +14 -3
- package/src/internal/style-names.generated.ts +27 -26
- package/src/layouts/PostLayout.astro +1 -0
- package/src/layouts/Stalux.astro +3 -1
- package/src/pages/api/post.abbrlink.json.ts +0 -3
- package/src/pages/api/posts.json.ts +16 -19
- package/src/pages/openapi.json.ts +3 -44
- package/src/pages/posts/[post].md.ts +4 -40
- package/src/plugins/feature-flags.ts +8 -1
- package/src/scripts/navigation-font.ts +28 -0
- package/src/scripts/navigation.ts +4 -2
- package/src/scripts/page-runtime.ts +20 -6
- package/src/scripts/pagefind-api.ts +63 -0
- package/src/scripts/photoswipe.ts +13 -3
- package/src/scripts/random-posts.ts +12 -10
- package/src/scripts/search-dialog.ts +2 -1
- package/src/scripts/view-transitions.ts +12 -1
- package/src/scripts/webmcp.ts +397 -141
- package/src/scripts/word-list.ts +13 -3
- package/src/styles/base.css +7 -0
- package/src/styles/generated.css +1 -1
- package/src/utils/collections-stats.ts +9 -22
- package/src/utils/content-index.ts +13 -4
- package/src/utils/dayjs.ts +26 -11
- package/src/utils/feed.ts +30 -29
- package/src/utils/post-lookup.ts +51 -0
- package/src/utils/post-markdown.ts +31 -0
- package/src/utils/site-path.ts +4 -0
- package/src/utils/toc-scrollspy.ts +20 -6
- package/src/utils/word-count-utils.ts +45 -41
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { sitePath } from "../utils/site-path";
|
|
2
|
+
import { createClientLogger } from "./logger";
|
|
3
|
+
|
|
4
|
+
export interface PagefindData {
|
|
5
|
+
url: string;
|
|
6
|
+
meta?: { title?: string };
|
|
7
|
+
plain_excerpt?: string;
|
|
8
|
+
content?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PagefindApi {
|
|
12
|
+
search: (
|
|
13
|
+
query: string | null,
|
|
14
|
+
options?: { filters?: Record<string, string> },
|
|
15
|
+
) => Promise<{
|
|
16
|
+
results: Array<{ data: () => Promise<PagefindData> }>;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Share successful imports and in-flight work; a failed attempt remains retryable. */
|
|
21
|
+
export function createPagefindLoader(
|
|
22
|
+
importModule: (url: string) => Promise<unknown> = (url) => import(/* @vite-ignore */ url),
|
|
23
|
+
origin: () => string = () => location.origin,
|
|
24
|
+
base = import.meta.env.BASE_URL,
|
|
25
|
+
): () => Promise<PagefindApi | null> {
|
|
26
|
+
let pending: Promise<PagefindApi | null> | undefined;
|
|
27
|
+
let failedAttempts = 0;
|
|
28
|
+
const logger = createClientLogger("webmcp/pagefind");
|
|
29
|
+
return () => {
|
|
30
|
+
if (!pending) {
|
|
31
|
+
pending = (async () => {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(sitePath("pagefind/pagefind.js", base), origin());
|
|
34
|
+
// Browsers can retain a failed ESM fetch; retry with a fresh module URL.
|
|
35
|
+
if (failedAttempts)
|
|
36
|
+
url.searchParams.set("stalux_retry", String(failedAttempts));
|
|
37
|
+
const loaded = await importModule(url.href);
|
|
38
|
+
const namespace = loaded as {
|
|
39
|
+
default?: PagefindApi;
|
|
40
|
+
search?: PagefindApi["search"];
|
|
41
|
+
};
|
|
42
|
+
const api = namespace.default ?? namespace;
|
|
43
|
+
if (typeof api.search !== "function")
|
|
44
|
+
throw new Error("Invalid Pagefind module");
|
|
45
|
+
return api as PagefindApi;
|
|
46
|
+
} catch {
|
|
47
|
+
failedAttempts++;
|
|
48
|
+
logger.warn(
|
|
49
|
+
"Pagefind index unavailable; build the static search index and retry",
|
|
50
|
+
);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
const attempt = pending;
|
|
55
|
+
void attempt.then((result) => {
|
|
56
|
+
if (!result && pending === attempt) pending = undefined;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return pending;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const loadPagefind = createPagefindLoader();
|
|
@@ -7,9 +7,10 @@ const logger = createClientLogger("photoswipe");
|
|
|
7
7
|
|
|
8
8
|
const GALLERY_SELECTOR = "a.pswp-gallery-item";
|
|
9
9
|
|
|
10
|
-
async function enrichPhotoSwipeData(container: Element): Promise<void> {
|
|
10
|
+
async function enrichPhotoSwipeData(container: Element, signal: AbortSignal): Promise<void> {
|
|
11
11
|
const links = container.querySelectorAll<HTMLAnchorElement>(GALLERY_SELECTOR);
|
|
12
12
|
for (const link of links) {
|
|
13
|
+
if (signal.aborted) return;
|
|
13
14
|
const image = link.querySelector<HTMLImageElement>("img");
|
|
14
15
|
if (!image) continue;
|
|
15
16
|
|
|
@@ -21,12 +22,18 @@ async function enrichPhotoSwipeData(container: Element): Promise<void> {
|
|
|
21
22
|
|
|
22
23
|
const temporaryImage = new Image();
|
|
23
24
|
temporaryImage.src = image.currentSrc || image.src;
|
|
25
|
+
const cancel = () => temporaryImage.removeAttribute("src");
|
|
26
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
24
27
|
try {
|
|
25
28
|
await temporaryImage.decode();
|
|
26
29
|
} catch {
|
|
30
|
+
if (signal.aborted) return;
|
|
27
31
|
logger.debug("image dimensions unavailable; skipping enrichment");
|
|
28
32
|
continue;
|
|
33
|
+
} finally {
|
|
34
|
+
signal.removeEventListener("abort", cancel);
|
|
29
35
|
}
|
|
36
|
+
if (signal.aborted) return;
|
|
30
37
|
link.dataset.pswpWidth = String(temporaryImage.naturalWidth);
|
|
31
38
|
link.dataset.pswpHeight = String(temporaryImage.naturalHeight);
|
|
32
39
|
}
|
|
@@ -41,6 +48,7 @@ registerPageLifecycle("photoswipe", () => {
|
|
|
41
48
|
const controller = new AbortController();
|
|
42
49
|
let lightbox: import("photoswipe/lightbox").default | undefined;
|
|
43
50
|
let initialized = false;
|
|
51
|
+
let disposed = false;
|
|
44
52
|
|
|
45
53
|
const initialize = async () => {
|
|
46
54
|
logger.debug("loading lightbox modules");
|
|
@@ -48,8 +56,8 @@ registerPageLifecycle("photoswipe", () => {
|
|
|
48
56
|
import("photoswipe/lightbox"),
|
|
49
57
|
import("photoswipe"),
|
|
50
58
|
]);
|
|
51
|
-
|
|
52
|
-
void enrichPhotoSwipeData(container);
|
|
59
|
+
if (disposed) return;
|
|
60
|
+
void enrichPhotoSwipeData(container, controller.signal);
|
|
53
61
|
lightbox = new PhotoSwipeLightbox({
|
|
54
62
|
gallery: container,
|
|
55
63
|
children: GALLERY_SELECTOR,
|
|
@@ -77,6 +85,7 @@ registerPageLifecycle("photoswipe", () => {
|
|
|
77
85
|
|
|
78
86
|
void ensureInitialized()
|
|
79
87
|
.then(() => {
|
|
88
|
+
if (disposed) return;
|
|
80
89
|
initialized = true;
|
|
81
90
|
logger.debug(`opening image index=${clickedIndex}`);
|
|
82
91
|
lightbox?.loadAndOpen(clickedIndex, undefined, initialPoint);
|
|
@@ -92,6 +101,7 @@ registerPageLifecycle("photoswipe", () => {
|
|
|
92
101
|
});
|
|
93
102
|
|
|
94
103
|
return () => {
|
|
104
|
+
disposed = true;
|
|
95
105
|
controller.abort();
|
|
96
106
|
lightbox?.destroy();
|
|
97
107
|
lightbox = undefined;
|
|
@@ -5,21 +5,23 @@ const logger = createClientLogger("random-posts");
|
|
|
5
5
|
|
|
6
6
|
const sleep = (ms: number, signal: AbortSignal) =>
|
|
7
7
|
new Promise<void>((resolve) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const finish = () => {
|
|
9
|
+
signal.removeEventListener("abort", aborted);
|
|
10
|
+
resolve();
|
|
11
|
+
};
|
|
12
|
+
const timer = window.setTimeout(finish, ms);
|
|
13
|
+
const aborted = () => {
|
|
14
|
+
window.clearTimeout(timer);
|
|
15
|
+
finish();
|
|
16
|
+
};
|
|
17
|
+
if (signal.aborted) aborted();
|
|
18
|
+
else signal.addEventListener("abort", aborted, { once: true });
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
function shuffle<T extends object>(items: T[]): T[] {
|
|
20
22
|
const result = [...items];
|
|
23
|
+
const bytes = new Uint32Array(1);
|
|
21
24
|
for (let i = result.length - 1; i > 0; i -= 1) {
|
|
22
|
-
const bytes = new Uint32Array(1);
|
|
23
25
|
const j = (crypto.getRandomValues(bytes)[0] ?? 0) % (i + 1);
|
|
24
26
|
const current = result[i];
|
|
25
27
|
const target = result[j];
|
|
@@ -34,9 +34,10 @@ export async function ensureSearchStyles(link: HTMLLinkElement | null): Promise<
|
|
|
34
34
|
export async function upgradeAndOpenSearchDialog<ElementType extends SearchDialogLike>(
|
|
35
35
|
registry: CustomElementRegistryLike<ElementType>,
|
|
36
36
|
dialog: ElementType | null,
|
|
37
|
+
signal?: AbortSignal,
|
|
37
38
|
): Promise<void> {
|
|
38
39
|
await registry.whenDefined("pagefind-modal");
|
|
39
|
-
if (!dialog) return;
|
|
40
|
+
if (!dialog || signal?.aborted) return;
|
|
40
41
|
registry.upgrade(dialog);
|
|
41
42
|
dialog.open?.();
|
|
42
43
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getRouteKind } from "@utils/public-routes";
|
|
2
2
|
import { createClientLogger } from "./logger";
|
|
3
|
+
import { prepareNavigationFont } from "./navigation-font";
|
|
3
4
|
|
|
4
5
|
const logger = createClientLogger("view-transitions");
|
|
5
6
|
|
|
@@ -19,6 +20,9 @@ interface NavigationEvent {
|
|
|
19
20
|
from: URL;
|
|
20
21
|
to: URL;
|
|
21
22
|
direction: string;
|
|
23
|
+
loader: () => Promise<void>;
|
|
24
|
+
newDocument: Document;
|
|
25
|
+
signal: AbortSignal;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function setNavigationState(event: NavigationEvent): void {
|
|
@@ -35,7 +39,14 @@ if (!transitionWindow.__STALUX_VT_LISTENER__) {
|
|
|
35
39
|
transitionWindow.__STALUX_VT_LISTENER__ = true;
|
|
36
40
|
|
|
37
41
|
document.addEventListener("astro:before-preparation", (event) => {
|
|
38
|
-
|
|
42
|
+
const navigation = event as unknown as NavigationEvent;
|
|
43
|
+
setNavigationState(navigation);
|
|
44
|
+
const loader = navigation.loader;
|
|
45
|
+
navigation.loader = async () => {
|
|
46
|
+
await loader();
|
|
47
|
+
if (!event.defaultPrevented)
|
|
48
|
+
await prepareNavigationFont(navigation.newDocument, navigation.signal);
|
|
49
|
+
};
|
|
39
50
|
});
|
|
40
51
|
|
|
41
52
|
document.addEventListener("astro:after-swap", () => {
|