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>