Commit 69f197b
Changed files (7)
src
components
utils
layouts
styles
types
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: {}