master
1---
2import { buildConfig } from '@/config';
3import '@/styles/markdown.css';
4import { getCachedRemoteImageSize } from '@/utils/image-size-cache';
5import type { HTMLAttributes } from 'astro/types';
6import { load as cheerioLoad } from 'cheerio';
7import crypto from 'crypto';
8import type PhotoSwipeLightbox from 'photoswipe/lightbox';
9import Replacer from './Replacer.astro';
10
11interface Props extends HTMLAttributes<'article'> {
12 'bidirectional-references'?: {
13 references: {
14 reference: string;
15 context: string;
16 id: string;
17 }[];
18 allRefByCurrent: {
19 refTo: {
20 title: string;
21 collection: 'posts' | 'spec';
22 id: string;
23 };
24 context: string;
25 offset: [number, number];
26 id: string;
27 }[];
28 };
29 zoomReplacer?: (html: string) => string;
30 zoomOption?: ConstructorParameters<typeof PhotoSwipeLightbox>[0];
31}
32
33const {
34 class: className,
35 'bidirectional-references': bidirectionalReferences,
36 id: wrapperId,
37 zoomReplacer,
38 zoomOption,
39 ...rest
40} = Astro.props;
41
42const references = bidirectionalReferences?.references;
43const allRefByCurrent = bidirectionalReferences?.allRefByCurrent;
44const referenceReplacer = (_: string, reference: string, alias: string) => {
45 const id = references?.find((item) => item.reference === reference.split('#')[0])?.id;
46 if (!id) return '';
47 const refTo = allRefByCurrent?.find((it) => it.id === id);
48 if (!refTo) return '';
49 const url =
50 refTo.refTo.collection === 'posts' ? `/posts/${refTo.refTo.id}/` : `/${refTo.refTo.id}/`;
51 return `<a href="${url}" id="wiki-${id}">${alias || reference}</a>`;
52};
53const referencePattern = /%%%%(.*?)(?:%%(.*?))?%%%%/g;
54
55const imageZoomReplacer = !buildConfig.enableImageZoom
56 ? (html: string) => html
57 : zoomReplacer ||
58 (async (html: string) => {
59 const $ = cheerioLoad(html);
60 const imgs = $('img:where(:not(inline img, a img))');
61 if (imgs.length === 0) return html;
62
63 const srcWHMap = new Map<string, { width: number; height: number }>();
64 await Promise.all(
65 imgs.map(async (_, el) => {
66 if (el.type !== 'tag') return '';
67 const src = el.attribs['src'];
68 if (!src) return '';
69 let width = parseInt(el.attribs['width']) || undefined;
70 let height = parseInt(el.attribs['height']) || undefined;
71 if (
72 (!width || !height) &&
73 !src.startsWith('/') &&
74 buildConfig.inferRemoteImageSize.enable
75 ) {
76 const dimensions = await getCachedRemoteImageSize(src);
77 if (dimensions) {
78 width = dimensions.width;
79 height = dimensions.height;
80 }
81 }
82 if (width && height) {
83 srcWHMap.set(src, { width, height });
84 } else {
85 console.warn(
86 [
87 `[WARN] Size for image "${src}" could not be determined, the zoom feature may not work correctly.`,
88 `Default size will be used: ${JSON.stringify(buildConfig.inferRemoteImageSize.defaultSize)}`,
89 `To ensure the zoom feature works, please provide width and height attributes for the image.`,
90 `If the image is in 'public' folder, you can use the 'width' and 'height' attributes in the <img> tag.`,
91 `If the image is from remote, please enable 'inferRemoteImageSize' in the config or provide the size manually.`,
92 `Or you can disable the zoom feature by setting 'enableImageZoom' to false in the config.`,
93 ].join('\n ')
94 );
95 srcWHMap.set(src, buildConfig.inferRemoteImageSize.defaultSize);
96 }
97 })
98 );
99
100 $('img:where(:not(inline img, a img))').wrap((_, el) => {
101 if (el.type !== 'tag') return '';
102 const src = el.attribs['src'];
103 if (!src) return '';
104 const { width, height } = srcWHMap.get(src) || {};
105 const attrs = [
106 `data-pswp-src="${el.attribs['src']}"`,
107 width ? `data-pswp-width="${width}"` : undefined,
108 height ? `data-pswp-height="${height}"` : undefined,
109 ];
110 return `<a ${attrs.join(' ')}></a>`;
111 });
112 return $.html();
113 });
114const imageZoomPattern = null; // Use null to process entire HTML
115
116const Fragment = bidirectionalReferences ? Replacer : 'Fragment';
117
118const id = wrapperId || `markdown-${crypto.randomUUID()}`;
119---
120
121<article class:list={['pswp-gallery', className]} id={id} {...rest}>
122 <Fragment
123 options={[
124 { pattern: referencePattern, replacer: referenceReplacer },
125 { pattern: imageZoomPattern, replacer: imageZoomReplacer },
126 ]}
127 >
128 <slot />
129 </Fragment>
130</article>
131
132<script is:inline define:vars={{ id, option: zoomOption }}>
133 function initPhotoSwipe() {
134 // console.log(
135 // `Consumer (ID: ${id}): 'pswp:enable' event received or lightbox was ready. Initializing.`
136 // );
137
138 const lightbox = new window.lightbox(
139 option || {
140 gallery: `#${id}`,
141 children: 'a[data-pswp-src]',
142 pswpModule: window.pswpModuleImporter,
143 }
144 );
145 lightbox.init();
146
147 document.addEventListener(
148 'astro:before-swap',
149 () => {
150 if (lightbox) {
151 lightbox.destroy();
152 }
153 },
154 { once: true }
155 );
156 }
157
158 if (window.lightbox) {
159 initPhotoSwipe();
160 } else {
161 // console.log(`Consumer (ID: ${id}): lightbox not ready. Waiting for 'pswp:enable' event.`);
162 document.addEventListener('pswp:enable', initPhotoSwipe, { once: true });
163 }
164</script>