Commit 69f197b

HPCesia <me@hpcesia.com>
2025-08-20 05:20:39
feat: switch lightbox to photoswipe
1 parent eeb99c7
src/components/utils/Markdown.astro
@@ -1,8 +1,11 @@
 ---
+import { buildConfig } from '@/config';
 import '@/styles/markdown.css';
 import type { HTMLAttributes } from 'astro/types';
-import 'cheerio';
+import { inferRemoteSize } from 'astro:assets';
 import { load as cheerioLoad } from 'cheerio';
+import crypto from 'crypto';
+import type PhotoSwipeLightbox from 'photoswipe/lightbox';
 import Replacer from './Replacer.astro';
 
 interface Props extends HTMLAttributes<'article'> {
@@ -23,11 +26,16 @@ interface Props extends HTMLAttributes<'article'> {
       id: string;
     }[];
   };
+  zoomReplacer?: (html: string) => string;
+  zoomOption?: ConstructorParameters<typeof PhotoSwipeLightbox>[0];
 }
 
 const {
   class: className,
   'bidirectional-references': bidirectionalReferences,
+  id: wrapperId,
+  zoomReplacer,
+  zoomOption,
   ...rest
 } = Astro.props;
 
@@ -44,17 +52,73 @@ const referenceReplacer = (_: string, reference: string, alias: string) => {
 };
 const referencePattern = /%%%%(.*?)(?:%%(.*?))?%%%%/g;
 
-const imageZoomReplacer = (html: string) => {
-  const $ = cheerioLoad(html);
-  $('img:where(:not(inline img))').attr('data-zoom', '');
-  return $.html();
-};
+const imageZoomReplacer =
+  zoomReplacer ||
+  (async (html: string) => {
+    const $ = cheerioLoad(html);
+    const imgs = $('img:where(:not(inline img, a img))');
+    if (imgs.length === 0) return html;
+
+    const srcWHMap = new Map<string, { width: number; height: number }>();
+    await Promise.all(
+      imgs.map(async (_, el) => {
+        if (el.type !== 'tag') return '';
+        const src = el.attribs['src'];
+        if (!src) return '';
+        let width = parseInt(el.attribs['width']) || undefined;
+        let height = parseInt(el.attribs['height']) || undefined;
+        if (
+          (!width || !height) &&
+          !src.startsWith('/') &&
+          buildConfig.inferRemoteImageSize.enable
+        ) {
+          try {
+            const metadata = await inferRemoteSize(src);
+            width = metadata.width;
+            height = metadata.height;
+          } catch (error) {
+            console.warn(
+              `[WARN] Could not infer size for image "${src}": ${(error as Error).message}.`
+            );
+          }
+        }
+        if (width && height) {
+          srcWHMap.set(src, { width, height });
+        } else {
+          console.warn(
+            `[WARN] Size for image "${src}" could not be determined, the zoom feature may not work correctly.
+                    To ensure the zoom feature works, please provide width and height attributes for the image.
+                    If the image is in 'public' folder, you can use the 'width' and 'height' attributes in the <img> tag.
+                    If the image is from remote, please enable 'inferRemoteImageSize' in the config or provide the size manually.
+            `
+          );
+          srcWHMap.set(src, buildConfig.inferRemoteImageSize.defaultSize);
+        }
+      })
+    );
+
+    $('img:where(:not(inline img, a img))').wrap((_, el) => {
+      if (el.type !== 'tag') return '';
+      const src = el.attribs['src'];
+      if (!src) return '';
+      const { width, height } = srcWHMap.get(src) || {};
+      const attrs = [
+        `data-pswp-src="${el.attribs['src']}"`,
+        width ? `data-pswp-width="${width}"` : undefined,
+        height ? `data-pswp-height="${height}"` : undefined,
+      ];
+      return `<a ${attrs.join(' ')}></a>`;
+    });
+    return $.html();
+  });
 const imageZoomPattern = null; // Use null to process entire HTML
 
 const Fragment = bidirectionalReferences ? Replacer : 'Fragment';
+
+const id = wrapperId || `markdown-${crypto.randomUUID()}`;
 ---
 
-<article class={className} {...rest}>
+<article class:list={['pswp-gallery', className]} id={id} {...rest}>
   <Fragment
     options={[
       { pattern: referencePattern, replacer: referenceReplacer },
@@ -64,3 +128,37 @@ const Fragment = bidirectionalReferences ? Replacer : 'Fragment';
     <slot />
   </Fragment>
 </article>
+
+<script is:inline define:vars={{ id, option: zoomOption }}>
+  function initPhotoSwipe() {
+    // console.log(
+    //   `Consumer (ID: ${id}): 'pswp:enable' event received or lightbox was ready. Initializing.`
+    // );
+
+    const lightbox = new window.lightbox(
+      option || {
+        gallery: `#${id}`,
+        children: 'a[data-pswp-src]',
+        pswpModule: window.pswpModuleImporter,
+      }
+    );
+    lightbox.init();
+
+    document.addEventListener(
+      'astro:before-swap',
+      () => {
+        if (lightbox) {
+          lightbox.destroy();
+        }
+      },
+      { once: true }
+    );
+  }
+
+  if (window.lightbox) {
+    initPhotoSwipe();
+  } else {
+    // console.log(`Consumer (ID: ${id}): lightbox not ready. Waiting for 'pswp:enable' event.`);
+    document.addEventListener('pswp:enable', initPhotoSwipe, { once: true });
+  }
+</script>
src/layouts/GlobalLayout.astro
@@ -7,6 +7,7 @@ import Navbar from '@components/Navbar.astro';
 import PageFooter from '@components/PageFooter.astro';
 import Search from '@components/Search.astro';
 import SideToolBar from '@components/SideToolBar.astro';
+import 'photoswipe/dist/photoswipe.css';
 import { pwaAssetsHead } from 'virtual:pwa-assets/head';
 import { pwaInfo } from 'virtual:pwa-info';
 
@@ -131,7 +132,6 @@ const siteLang = lang.replace('_', '-');
 
 <script>
   import { convertTimeToRelative } from '@scripts/utils';
-  import mediumZoom from 'medium-zoom/dist/pure';
 
   // 深色模式切换
   function applyDarkMode() {
@@ -147,19 +147,25 @@ const siteLang = lang.replace('_', '-');
   function setup() {
     applyDarkMode();
     convertTimeToRelative();
-    mediumZoom('[data-zoom]', {
-      background: 'rgba(0, 0, 0, 0.4)',
-    });
   }
 
   document.addEventListener('astro:after-swap', setup);
   setup();
 </script>
 
+<script>
+  import PhotoSwipeLightbox from 'photoswipe/lightbox';
+
+  window.lightbox = PhotoSwipeLightbox;
+  window.pswpModuleImporter = () => import('photoswipe');
+
+  const event = new CustomEvent('pswp:enable');
+  document.dispatchEvent(event);
+  // console.log("Producer: PhotoSwipe resources are ready and 'pswp:enable' event dispatched.");
+</script>
+
 <style is:global>
-  @import 'medium-zoom/dist/style.css';
-  .medium-zoom-overlay,
-  .medium-zoom-image--opened {
-    z-index: 999;
+  a[data-pwsp-src] {
+    cursor: zoom-in;
   }
 </style>
src/styles/markdown.css
@@ -58,7 +58,7 @@ article {
     margin: 0.75rem 0;
   }
 
-  a:not(.card) {
+  a:not(.card, [data-pswp-src]) {
     @apply text-primary underline decoration-dashed;
   }
 
@@ -67,22 +67,29 @@ article {
   }
 
   /* 媒体元素 */
-  img {
-    &:where(:not(inline img)) {
-      max-width: 75%;
-      max-height: 40rem;
-
-      @apply relative mx-auto mt-4 mb-6 rounded-md;
-    }
+  inline img {
+    display: inline;
+    max-height: 2em;
+    vertical-align: middle;
+    margin: 0 0.25em;
+    object-fit: contain;
+    width: auto;
+    border-radius: 0.25rem;
+  }
 
-    &:where(inline img) {
-      display: inline;
-      max-height: 2em;
-      vertical-align: middle;
-      margin: 0 0.25em;
-      object-fit: contain;
-      width: auto;
-      border-radius: 0.25rem;
+  a[data-pswp-src] {
+    display: block;
+    max-width: 75%;
+    max-height: 40rem;
+    width: fit-content;
+    height: fit-content;
+    position: relative;
+    margin-inline: auto;
+    margin-top: calc(var(--spacing, 0.25rem) * 4);
+    margin-bottom: calc(var(--spacing, 0.25rem) * 6);
+    cursor: zoom-in;
+    img {
+      border-radius: var(--radius-md, 0.375rem);
     }
   }
 
src/types/config.d.ts
@@ -217,13 +217,9 @@ export type BuildConfig = {
    */
   showDraftsOnDev: boolean;
   /**
-   * Fetch the size data of remote images during build.\
-   * This feature **CAN NOT** fetch images in `.md` files.
-   * To fetch images in `.md` files, see https://docs.astro.build/en/reference/configuration-reference/#imageservice
+   * Fetch the size data of remote images during build.
    *
-   * 在构建时获取远程图像的大小数据。\
-   * 需要注意,本功能**不能**获取 `.md` 中的图像。
-   * 获取 `.md` 中的图像,请参考 https://docs.astro.build/zh-cn/reference/configuration-reference/#imageservice
+   * 在构建时获取远程图像的大小数据。
    */
   inferRemoteImageSize: {
     /**
src/global.d.ts
@@ -1,8 +1,11 @@
+import type PhotoSwipeLightbox from 'photoswipe/lightbox';
 import type { Swup } from 'swup';
 
 declare global {
   interface Window {
     swup: Swup;
+    lightbox: typeof PhotoSwipeLightbox;
+    pswpModuleImporter: () => Promise<typeof import('photoswipe')>;
   }
 
   const twikoo: {
package.json
@@ -47,8 +47,8 @@
     "hastscript": "^9.0.1",
     "markdown-it": "^14.1.0",
     "mdast-util-to-string": "^4.0.0",
-    "medium-zoom": "^1.1.0",
     "nanostores": "^0.11.4",
+    "photoswipe": "^5.4.4",
     "postcss-load-config": "^6.0.1",
     "reading-time": "^1.5.0",
     "rehype-autolink-headings": "^7.1.0",
pnpm-lock.yaml
@@ -110,12 +110,12 @@ importers:
       mdast-util-to-string:
         specifier: ^4.0.0
         version: 4.0.0
-      medium-zoom:
-        specifier: ^1.1.0
-        version: 1.1.0
       nanostores:
         specifier: ^0.11.4
         version: 0.11.4
+      photoswipe:
+        specifier: ^5.4.4
+        version: 5.4.4
       postcss-load-config:
         specifier: ^6.0.1
         version: 6.0.1(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0)
@@ -4538,9 +4538,6 @@ packages:
   mdurl@2.0.0:
     resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
 
-  medium-zoom@1.1.0:
-    resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==}
-
   meow@13.2.0:
     resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
     engines: {node: '>=18'}
@@ -5017,6 +5014,10 @@ packages:
   perfect-debounce@1.0.0:
     resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
 
+  photoswipe@5.4.4:
+    resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==}
+    engines: {node: '>= 0.12.0'}
+
   picocolors@1.1.1:
     resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
 
@@ -11908,8 +11909,6 @@ snapshots:
 
   mdurl@2.0.0: {}
 
-  medium-zoom@1.1.0: {}
-
   meow@13.2.0: {}
 
   merge-stream@2.0.0: {}
@@ -12580,6 +12579,8 @@ snapshots:
 
   perfect-debounce@1.0.0: {}
 
+  photoswipe@5.4.4: {}
+
   picocolors@1.1.1: {}
 
   picomatch@2.3.1: {}