Przeglądaj źródła

Merge pull request #10054 from weseek/support/156162-167198-remark-attachment-refs-biome

support: Configure biome for remark-attachment-refs package
mergify[bot] 9 miesięcy temu
rodzic
commit
f13fc78983

+ 1 - 2
biome.json

@@ -24,8 +24,7 @@
       "./packages/custom-icons/**",
       "./packages/editor/**",
       "./packages/pdf-converter-client/**",
-      "./packages/pluginkit/**",
-      "./packages/remark-attachment-refs/**"
+      "./packages/pluginkit/**"
     ]
   },
   "formatter": {

+ 1 - 1
packages/remark-attachment-refs/.eslintignore

@@ -1 +1 @@
-/dist/**
+*

+ 0 - 5
packages/remark-attachment-refs/.eslintrc.cjs

@@ -1,5 +0,0 @@
-module.exports = {
-  extends: [
-    'weseek/react',
-  ],
-};

+ 1 - 1
packages/remark-attachment-refs/package.json

@@ -37,7 +37,7 @@
     "watch": "run-p watch:*",
     "watch:client": "pnpm run dev:client -w --emptyOutDir=false",
     "watch:server": "pnpm run dev:server -w --emptyOutDir=false",
-    "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
+    "lint:js": "biome check",
     "lint:styles": "stylelint \"src/**/*.scss\" \"src/**/*.css\"",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint": "run-p lint:*",

+ 33 - 19
packages/remark-attachment-refs/src/client/components/AttachmentList.tsx

@@ -1,4 +1,4 @@
-import { useCallback, type JSX } from 'react';
+import { type JSX, useCallback } from 'react';
 
 import type { IAttachmentHasId } from '@growi/core';
 import { Attachment, LoadingSpinner } from '@growi/ui/dist/components';
@@ -6,16 +6,15 @@ import { Attachment, LoadingSpinner } from '@growi/ui/dist/components';
 import { ExtractedAttachments } from './ExtractedAttachments';
 import type { RefsContext } from './util/refs-context';
 
-
 import styles from './AttachmentList.module.scss';
 
 const AttachmentLink = Attachment;
 
 type Props = {
-  refsContext: RefsContext
-  isLoading: boolean
-  error?: Error
-  attachments: IAttachmentHasId[]
+  refsContext: RefsContext;
+  isLoading: boolean;
+  error?: Error;
+  attachments: IAttachmentHasId[];
 };
 
 export const AttachmentList = ({
@@ -28,12 +27,15 @@ export const AttachmentList = ({
     return (
       <div className="text-muted">
         <small>
-          <span className="material-symbols-outlined fs-5 me-1" aria-hidden="true">info</span>
-          {
-            refsContext.options?.prefix != null
-              ? `${refsContext.options.prefix} and descendant pages have no attachments`
-              : `${refsContext.pagePath} has no attachments`
-          }
+          <span
+            className="material-symbols-outlined fs-5 me-1"
+            aria-hidden="true"
+          >
+            info
+          </span>
+          {refsContext.options?.prefix != null
+            ? `${refsContext.options.prefix} and descendant pages have no attachments`
+            : `${refsContext.pagePath} has no attachments`}
         </small>
       </div>
     );
@@ -44,7 +46,9 @@ export const AttachmentList = ({
       return (
         <div className="text-muted">
           <LoadingSpinner className="me-1" />
-          <span className="attachment-refs-blink">{refsContext.toString()}</span>
+          <span className="attachment-refs-blink">
+            {refsContext.toString()}
+          </span>
         </div>
       );
     }
@@ -62,13 +66,23 @@ export const AttachmentList = ({
       return renderNoAttachmentsMessage();
     }
 
-    return (refsContext.isExtractImage)
-      ? <ExtractedAttachments attachments={attachments} refsContext={refsContext} />
-      : attachments.map((attachment) => {
-        return <AttachmentLink key={attachment._id} attachment={attachment} inUse={false} />;
-      });
+    return refsContext.isExtractImage ? (
+      <ExtractedAttachments
+        attachments={attachments}
+        refsContext={refsContext}
+      />
+    ) : (
+      attachments.map((attachment) => {
+        return (
+          <AttachmentLink
+            key={attachment._id}
+            attachment={attachment}
+            inUse={false}
+          />
+        );
+      })
+    );
   }, [isLoading, error, attachments, refsContext, renderNoAttachmentsMessage]);
 
   return <div className={styles['attachment-refs']}>{renderContents()}</div>;
-
 };

+ 187 - 176
packages/remark-attachment-refs/src/client/components/ExtractedAttachments.tsx

@@ -7,8 +7,8 @@ import type { Property } from 'csstype';
 import type { RefsContext } from './util/refs-context';
 
 type Props = {
-  attachments: IAttachmentHasId[],
-  refsContext: RefsContext,
+  attachments: IAttachmentHasId[];
+  refsContext: RefsContext;
 };
 
 /**
@@ -16,184 +16,195 @@ type Props = {
  *  2. when 'fileFormat' is not image, render Attachment as an Attachment component
  */
 // TODO https://redmine.weseek.co.jp/issues/121095: implement image carousel modal without using react-images
-export const ExtractedAttachments = React.memo(({
-  attachments,
-  refsContext,
-}: Props): JSX.Element => {
-
-  // const [showCarousel, setShowCarousel] = useState(false);
-  // const [currentIndex, setCurrentIndex] = useState<number | null>(null);
-
-  // const imageClickedHandler = useCallback((index: number) => {
-  //   setShowCarousel(true);
-  //   setCurrentIndex(index);
-  // }, []);
-
-  const getAttachmentsFilteredByFormat = useCallback(() => {
-    return attachments
-      .filter(attachment => attachment.fileFormat.startsWith('image/'));
-  }, [attachments]);
-
-  const getClassesAndStylesForNonGrid = useCallback(() => {
-    const { options } = refsContext;
-
-    const width = options?.width;
-    const height = options?.height;
-    const maxWidth = options?.maxWidth;
-    const maxHeight = options?.maxHeight;
-    const display = options?.display || 'block';
-
-    const containerStyles = {
-      width, height, maxWidth, maxHeight, display,
-    };
-
-    const imageClasses = [];
-    const imageStyles = {
-      width, height, maxWidth, maxHeight,
-    };
-
-    return {
-      containerStyles,
-      imageClasses,
-      imageStyles,
-    };
-  }, [refsContext]);
-
-  const getClassesAndStylesForGrid = useCallback(() => {
-    const { options } = refsContext;
+export const ExtractedAttachments = React.memo(
+  ({ attachments, refsContext }: Props): JSX.Element => {
+    // const [showCarousel, setShowCarousel] = useState(false);
+    // const [currentIndex, setCurrentIndex] = useState<number | null>(null);
+
+    // const imageClickedHandler = useCallback((index: number) => {
+    //   setShowCarousel(true);
+    //   setCurrentIndex(index);
+    // }, []);
+
+    const getAttachmentsFilteredByFormat = useCallback(() => {
+      return attachments.filter((attachment) =>
+        attachment.fileFormat.startsWith('image/'),
+      );
+    }, [attachments]);
+
+    const getClassesAndStylesForNonGrid = useCallback(() => {
+      const { options } = refsContext;
+
+      const width = options?.width;
+      const height = options?.height;
+      const maxWidth = options?.maxWidth;
+      const maxHeight = options?.maxHeight;
+      const display = options?.display || 'block';
+
+      const containerStyles = {
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+        display,
+      };
+
+      const imageClasses = [];
+      const imageStyles = {
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+      };
+
+      return {
+        containerStyles,
+        imageClasses,
+        imageStyles,
+      };
+    }, [refsContext]);
+
+    const getClassesAndStylesForGrid = useCallback(() => {
+      const { options } = refsContext;
+
+      const maxWidth = options?.maxWidth;
+      const maxHeight = options?.maxHeight;
+
+      const containerStyles = {
+        width: refsContext.getOptGridWidth(),
+        height: refsContext.getOptGridHeight(),
+        maxWidth,
+        maxHeight,
+      };
+
+      const imageClasses = ['w-100', 'h-100'];
+      const imageStyles = {
+        objectFit: 'cover' as Property.ObjectFit,
+        maxWidth,
+        maxHeight,
+      };
+
+      return {
+        containerStyles,
+        imageClasses,
+        imageStyles,
+      };
+    }, [refsContext]);
+
+    /**
+     * wrapper method for getClassesAndStylesForGrid/getClassesAndStylesForNonGrid
+     */
+    const getClassesAndStyles = useCallback(() => {
+      const { options } = refsContext;
+
+      return options?.grid != null
+        ? getClassesAndStylesForGrid()
+        : getClassesAndStylesForNonGrid();
+    }, [
+      getClassesAndStylesForGrid,
+      getClassesAndStylesForNonGrid,
+      refsContext,
+    ]);
+
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    const renderExtractedImage = useCallback(
+      (attachment: IAttachmentHasId, index: number) => {
+        const { options } = refsContext;
+
+        // determine alt
+        let alt = refsContext.isSingle ? options?.alt : undefined; // use only when single mode
+        alt = alt || attachment.originalName; //                     use 'originalName' if options.alt is not specified
+
+        // get styles
+        const { containerStyles, imageClasses, imageStyles } =
+          getClassesAndStyles();
+
+        // carousel settings
+        // let onClick;
+        // if (options?.noCarousel == null) {
+        //   // pointer cursor
+        //   Object.assign(containerStyles, { cursor: 'pointer' });
+        //   // set click handler
+        //   onClick = () => {
+        //     imageClickedHandler(index);
+        //   };
+        // }
+
+        return (
+          <div key={attachment._id} style={containerStyles}>
+            <img
+              src={attachment.filePathProxied}
+              alt={alt}
+              className={imageClasses.join(' ')}
+              style={imageStyles}
+            />
+          </div>
+        );
+      },
+      [getClassesAndStyles, refsContext],
+    );
 
-    const maxWidth = options?.maxWidth;
-    const maxHeight = options?.maxHeight;
-
-    const containerStyles = {
-      width: refsContext.getOptGridWidth(),
-      height: refsContext.getOptGridHeight(),
-      maxWidth,
-      maxHeight,
-    };
-
-    const imageClasses = ['w-100', 'h-100'];
-    const imageStyles = {
-      objectFit: 'cover' as Property.ObjectFit,
-      maxWidth,
-      maxHeight,
-    };
-
-    return {
-      containerStyles,
-      imageClasses,
-      imageStyles,
-    };
-  }, [refsContext]);
-
-  /**
-   * wrapper method for getClassesAndStylesForGrid/getClassesAndStylesForNonGrid
-   */
-  const getClassesAndStyles = useCallback(() => {
-    const { options } = refsContext;
+    // const renderCarousel = useCallback(() => {
+    //   const { options } = refsContext;
+    //   const withCarousel = options?.noCarousel == null;
+
+    //   const images = getAttachmentsFilteredByFormat()
+    //     .map((attachment) => {
+    //       return { src: attachment.filePathProxied };
+    //     });
+
+    //   // overwrite react-images modal styles
+    //   const zIndex = 1030; // > grw-navbar
+    //   const modalStyles = {
+    //     blanket: (styleObj) => {
+    //       return Object.assign(styleObj, { zIndex });
+    //     },
+    //     positioner: (styleObj) => {
+    //       return Object.assign(styleObj, { zIndex });
+    //     },
+    //   };
 
-    return (options?.grid != null)
-      ? getClassesAndStylesForGrid()
-      : getClassesAndStylesForNonGrid();
-  }, [getClassesAndStylesForGrid, getClassesAndStylesForNonGrid, refsContext]);
+    //   return (
+    //     <ModalGateway>
+    //       { withCarousel && showCarousel && (
+    //         <Modal styles={modalStyles} onClose={() => { setShowCarousel(false) }}>
+    //           <Carousel views={images} currentIndex={currentIndex} />
+    //         </Modal>
+    //       ) }
+    //     </ModalGateway>
+    //   );
+    // }, [refsContext]);
 
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  const renderExtractedImage = useCallback((attachment: IAttachmentHasId, index: number) => {
     const { options } = refsContext;
-
-    // determine alt
-    let alt = refsContext.isSingle ? options?.alt : undefined; // use only when single mode
-    alt = alt || attachment.originalName; //                     use 'originalName' if options.alt is not specified
-
-    // get styles
-    const {
-      containerStyles, imageClasses, imageStyles,
-    } = getClassesAndStyles();
-
-    // carousel settings
-    // let onClick;
-    // if (options?.noCarousel == null) {
-    //   // pointer cursor
-    //   Object.assign(containerStyles, { cursor: 'pointer' });
-    //   // set click handler
-    //   onClick = () => {
-    //     imageClickedHandler(index);
-    //   };
-    // }
+    const grid = options?.grid;
+    const gridGap = options?.gridGap;
+
+    const styles = {};
+
+    // Grid mode
+    if (grid != null) {
+      const gridTemplateColumns = refsContext.isOptGridColumnEnabled()
+        ? `repeat(${refsContext.getOptGridColumnsNum()}, 1fr)`
+        : `repeat(auto-fill, ${refsContext.getOptGridWidth()})`;
+
+      Object.assign(styles, {
+        display: 'grid',
+        gridTemplateColumns,
+        gridAutoRows: '1fr',
+        gridGap,
+      });
+    }
+
+    const contents = getAttachmentsFilteredByFormat().map((attachment, index) =>
+      renderExtractedImage(attachment, index),
+    );
 
     return (
-      <div
-        key={attachment._id}
-        style={containerStyles}
-      >
-        <img src={attachment.filePathProxied} alt={alt} className={imageClasses.join(' ')} style={imageStyles} />
-      </div>
+      <React.Fragment>
+        <div style={styles}>{contents}</div>
+
+        {/* { renderCarousel() } */}
+      </React.Fragment>
     );
-  }, [getClassesAndStyles, refsContext]);
-
-  // const renderCarousel = useCallback(() => {
-  //   const { options } = refsContext;
-  //   const withCarousel = options?.noCarousel == null;
-
-  //   const images = getAttachmentsFilteredByFormat()
-  //     .map((attachment) => {
-  //       return { src: attachment.filePathProxied };
-  //     });
-
-  //   // overwrite react-images modal styles
-  //   const zIndex = 1030; // > grw-navbar
-  //   const modalStyles = {
-  //     blanket: (styleObj) => {
-  //       return Object.assign(styleObj, { zIndex });
-  //     },
-  //     positioner: (styleObj) => {
-  //       return Object.assign(styleObj, { zIndex });
-  //     },
-  //   };
-
-  //   return (
-  //     <ModalGateway>
-  //       { withCarousel && showCarousel && (
-  //         <Modal styles={modalStyles} onClose={() => { setShowCarousel(false) }}>
-  //           <Carousel views={images} currentIndex={currentIndex} />
-  //         </Modal>
-  //       ) }
-  //     </ModalGateway>
-  //   );
-  // }, [refsContext]);
-
-  const { options } = refsContext;
-  const grid = options?.grid;
-  const gridGap = options?.gridGap;
-
-  const styles = {};
-
-  // Grid mode
-  if (grid != null) {
-
-    const gridTemplateColumns = (refsContext.isOptGridColumnEnabled())
-      ? `repeat(${refsContext.getOptGridColumnsNum()}, 1fr)`
-      : `repeat(auto-fill, ${refsContext.getOptGridWidth()})`;
-
-    Object.assign(styles, {
-      display: 'grid',
-      gridTemplateColumns,
-      gridAutoRows: '1fr',
-      gridGap,
-    });
-
-  }
-
-  const contents = getAttachmentsFilteredByFormat()
-    .map((attachment, index) => renderExtractedImage(attachment, index));
-
-  return (
-    <React.Fragment>
-      <div style={styles}>
-        {contents}
-      </div>
-
-      {/* { renderCarousel() } */}
-    </React.Fragment>
-  );
-});
+  },
+);

+ 9 - 5
packages/remark-attachment-refs/src/client/components/Gallery.tsx

@@ -12,10 +12,14 @@ export const Gallery = React.memo((props: Props): JSX.Element => {
   return <RefsImgSubstance grid={grid} gridGap={gridGap} {...props} />;
 });
 
-export const GalleryImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  const grid = props.grid || gridDefault;
-  const gridGap = props.gridGap || gridGapDefault;
-  return <RefsImgSubstance grid={grid} gridGap={gridGap} {...props} isImmutable />;
-});
+export const GalleryImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    const grid = props.grid || gridDefault;
+    const gridGap = props.gridGap || gridGapDefault;
+    return (
+      <RefsImgSubstance grid={grid} gridGap={gridGap} {...props} isImmutable />
+    );
+  },
+);
 
 Gallery.displayName = 'Gallery';

+ 31 - 28
packages/remark-attachment-refs/src/client/components/Ref.tsx

@@ -5,41 +5,44 @@ import { useSWRxRef } from '../stores/refs';
 import { AttachmentList } from './AttachmentList';
 import { RefsContext } from './util/refs-context';
 
-
 type Props = {
-  fileNameOrId: string,
-  pagePath: string,
-  isImmutable?: boolean,
+  fileNameOrId: string;
+  pagePath: string;
+  isImmutable?: boolean;
 };
 
-const RefSubstance = React.memo(({
-  fileNameOrId,
-  pagePath,
-  isImmutable,
-}: Props): JSX.Element => {
-  const refsContext = useMemo(() => {
-    return new RefsContext('ref', pagePath, { fileNameOrId });
-  }, [fileNameOrId, pagePath]);
-
-  const { data, error, isLoading } = useSWRxRef(pagePath, fileNameOrId, isImmutable);
-  const attachments = data != null ? [data] : [];
-
-  return (
-    <AttachmentList
-      refsContext={refsContext}
-      isLoading={isLoading}
-      error={error}
-      attachments={attachments}
-    />
-  );
-});
+const RefSubstance = React.memo(
+  ({ fileNameOrId, pagePath, isImmutable }: Props): JSX.Element => {
+    const refsContext = useMemo(() => {
+      return new RefsContext('ref', pagePath, { fileNameOrId });
+    }, [fileNameOrId, pagePath]);
+
+    const { data, error, isLoading } = useSWRxRef(
+      pagePath,
+      fileNameOrId,
+      isImmutable,
+    );
+    const attachments = data != null ? [data] : [];
+
+    return (
+      <AttachmentList
+        refsContext={refsContext}
+        isLoading={isLoading}
+        error={error}
+        attachments={attachments}
+      />
+    );
+  },
+);
 
 export const Ref = React.memo((props: Props): JSX.Element => {
   return <RefSubstance {...props} />;
 });
 
-export const RefImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  return <RefSubstance {...props} isImmutable />;
-});
+export const RefImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    return <RefSubstance {...props} isImmutable />;
+  },
+);
 
 Ref.displayName = 'Ref';

+ 54 - 42
packages/remark-attachment-refs/src/client/components/RefImg.tsx

@@ -5,55 +5,67 @@ import { useSWRxRef } from '../stores/refs';
 import { AttachmentList } from './AttachmentList';
 import { RefsContext } from './util/refs-context';
 
-
 type Props = {
-  fileNameOrId: string
-  pagePath: string
-  width?: string
-  height?: string
-  maxWidth?: string
-  maxHeight?: string
-  alt?: string
-
-  isImmutable?: boolean
+  fileNameOrId: string;
+  pagePath: string;
+  width?: string;
+  height?: string;
+  maxWidth?: string;
+  maxHeight?: string;
+  alt?: string;
+
+  isImmutable?: boolean;
 };
 
-const RefImgSubstance = React.memo(({
-  fileNameOrId,
-  pagePath,
-  width,
-  height,
-  maxWidth,
-  maxHeight,
-  alt,
-  isImmutable,
-}: Props): JSX.Element => {
-  const refsContext = useMemo(() => {
-    const options = {
-      fileNameOrId, width, height, maxWidth, maxHeight, alt,
-    };
-    return new RefsContext('refimg', pagePath, options);
-  }, [fileNameOrId, pagePath, width, height, maxWidth, maxHeight, alt]);
-
-  const { data, error, isLoading } = useSWRxRef(pagePath, fileNameOrId, isImmutable);
-  const attachments = data != null ? [data] : [];
-
-  return (
-    <AttachmentList
-      refsContext={refsContext}
-      isLoading={isLoading}
-      error={error}
-      attachments={attachments}
-    />
-  );
-});
+const RefImgSubstance = React.memo(
+  ({
+    fileNameOrId,
+    pagePath,
+    width,
+    height,
+    maxWidth,
+    maxHeight,
+    alt,
+    isImmutable,
+  }: Props): JSX.Element => {
+    const refsContext = useMemo(() => {
+      const options = {
+        fileNameOrId,
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+        alt,
+      };
+      return new RefsContext('refimg', pagePath, options);
+    }, [fileNameOrId, pagePath, width, height, maxWidth, maxHeight, alt]);
+
+    const { data, error, isLoading } = useSWRxRef(
+      pagePath,
+      fileNameOrId,
+      isImmutable,
+    );
+    const attachments = data != null ? [data] : [];
+
+    return (
+      <AttachmentList
+        refsContext={refsContext}
+        isLoading={isLoading}
+        error={error}
+        attachments={attachments}
+      />
+    );
+  },
+);
 
 export const RefImg = React.memo((props: Props): JSX.Element => {
   return <RefImgSubstance {...props} />;
 });
 
-export const RefImgImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  return <RefImgSubstance {...props} isImmutable />;
-});
+export const RefImgImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    return <RefImgSubstance {...props} isImmutable />;
+  },
+);
 
 RefImg.displayName = 'RefImg';

+ 46 - 36
packages/remark-attachment-refs/src/client/components/Refs.tsx

@@ -5,50 +5,60 @@ import { useSWRxRefs } from '../stores/refs';
 import { AttachmentList } from './AttachmentList';
 import { RefsContext } from './util/refs-context';
 
-
 type Props = {
-  pagePath: string,
-  prefix?: string,
-  depth?: string,
-  regexp?: string,
+  pagePath: string;
+  prefix?: string;
+  depth?: string;
+  regexp?: string;
 
-  isImmutable?: boolean,
+  isImmutable?: boolean;
 };
 
-const RefsSubstance = React.memo(({
-  pagePath,
-  prefix,
-  depth,
-  regexp,
-
-  isImmutable,
-}: Props): JSX.Element => {
-  const refsContext = useMemo(() => {
-    const options = {
-      prefix, depth, regexp,
-    };
-    return new RefsContext('refs', pagePath, options);
-  }, [pagePath, prefix, depth, regexp]);
-
-  const { data, error, isLoading } = useSWRxRefs(pagePath, prefix, { depth, regexp }, isImmutable);
-  const attachments = data != null ? data : [];
-
-  return (
-    <AttachmentList
-      refsContext={refsContext}
-      isLoading={isLoading}
-      error={error}
-      attachments={attachments}
-    />
-  );
-});
+const RefsSubstance = React.memo(
+  ({
+    pagePath,
+    prefix,
+    depth,
+    regexp,
+
+    isImmutable,
+  }: Props): JSX.Element => {
+    const refsContext = useMemo(() => {
+      const options = {
+        prefix,
+        depth,
+        regexp,
+      };
+      return new RefsContext('refs', pagePath, options);
+    }, [pagePath, prefix, depth, regexp]);
+
+    const { data, error, isLoading } = useSWRxRefs(
+      pagePath,
+      prefix,
+      { depth, regexp },
+      isImmutable,
+    );
+    const attachments = data != null ? data : [];
+
+    return (
+      <AttachmentList
+        refsContext={refsContext}
+        isLoading={isLoading}
+        error={error}
+        attachments={attachments}
+      />
+    );
+  },
+);
 
 export const Refs = React.memo((props: Props): JSX.Element => {
   return <RefsSubstance {...props} />;
 });
 
-export const RefsImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  return <RefsSubstance {...props} isImmutable />;
-});
+export const RefsImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    return <RefsSubstance {...props} isImmutable />;
+  },
+);
 
 Refs.displayName = 'Refs';

+ 88 - 55
packages/remark-attachment-refs/src/client/components/RefsImg.tsx

@@ -5,33 +5,57 @@ import { useSWRxRefs } from '../stores/refs';
 import { AttachmentList } from './AttachmentList';
 import { RefsContext } from './util/refs-context';
 
-
 export type Props = {
-  pagePath: string
-  prefix?: string
-  depth?: string
-  regexp?: string
-  width?: string
-  height?: string
-  maxWidth?: string
-  maxHeight?: string
-  display?: string
-  grid?: string
-  gridGap?: string
-  noCarousel?: string
+  pagePath: string;
+  prefix?: string;
+  depth?: string;
+  regexp?: string;
+  width?: string;
+  height?: string;
+  maxWidth?: string;
+  maxHeight?: string;
+  display?: string;
+  grid?: string;
+  gridGap?: string;
+  noCarousel?: string;
 
-  isImmutable?: boolean,
+  isImmutable?: boolean;
 };
 
-export const RefsImgSubstance = React.memo(({
-  pagePath, prefix, depth, regexp,
-  width, height, maxWidth, maxHeight,
-  display, grid, gridGap, noCarousel,
+export const RefsImgSubstance = React.memo(
+  ({
+    pagePath,
+    prefix,
+    depth,
+    regexp,
+    width,
+    height,
+    maxWidth,
+    maxHeight,
+    display,
+    grid,
+    gridGap,
+    noCarousel,
 
-  isImmutable,
-}: Props): JSX.Element => {
-  const refsContext = useMemo(() => {
-    const options = {
+    isImmutable,
+  }: Props): JSX.Element => {
+    const refsContext = useMemo(() => {
+      const options = {
+        pagePath,
+        prefix,
+        depth,
+        regexp,
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+        display,
+        grid,
+        gridGap,
+        noCarousel,
+      };
+      return new RefsContext('refsimg', pagePath, options);
+    }, [
       pagePath,
       prefix,
       depth,
@@ -44,46 +68,55 @@ export const RefsImgSubstance = React.memo(({
       grid,
       gridGap,
       noCarousel,
-    };
-    return new RefsContext('refsimg', pagePath, options);
-  }, [pagePath, prefix, depth, regexp,
-      width, height, maxWidth, maxHeight,
-      display, grid, gridGap, noCarousel]);
+    ]);
 
-  const { data, error: axiosError, isLoading } = useSWRxRefs(pagePath, prefix, {
-    depth,
-    regexp,
-    width,
-    height,
-    maxWidth,
-    maxHeight,
-    display,
-    grid,
-    gridGap,
-    noCarousel,
-  }, isImmutable);
-  const attachments = data != null ? data : [];
+    const {
+      data,
+      error: axiosError,
+      isLoading,
+    } = useSWRxRefs(
+      pagePath,
+      prefix,
+      {
+        depth,
+        regexp,
+        width,
+        height,
+        maxWidth,
+        maxHeight,
+        display,
+        grid,
+        gridGap,
+        noCarousel,
+      },
+      isImmutable,
+    );
+    const attachments = data != null ? data : [];
 
-  const error = axiosError != null
-    ? new Error(axiosError.response?.data ?? axiosError.message)
-    : undefined;
+    const error =
+      axiosError != null
+        ? new Error(axiosError.response?.data ?? axiosError.message)
+        : undefined;
 
-  return (
-    <AttachmentList
-      refsContext={refsContext}
-      isLoading={isLoading}
-      error={error}
-      attachments={attachments}
-    />
-  );
-});
+    return (
+      <AttachmentList
+        refsContext={refsContext}
+        isLoading={isLoading}
+        error={error}
+        attachments={attachments}
+      />
+    );
+  },
+);
 
 export const RefsImg = React.memo((props: Props): JSX.Element => {
   return <RefsImgSubstance {...props} />;
 });
 
-export const RefsImgImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
-  return <RefsImgSubstance {...props} isImmutable />;
-});
+export const RefsImgImmutable = React.memo(
+  (props: Omit<Props, 'isImmutable'>): JSX.Element => {
+    return <RefsImgSubstance {...props} isImmutable />;
+  },
+);
 
 RefsImg.displayName = 'RefsImg';

+ 16 - 8
packages/remark-attachment-refs/src/client/components/util/refs-context.ts

@@ -13,26 +13,33 @@ const GRID_AVAILABLE_OPTIONS_LIST = [
   'col-6',
 ];
 
-type tags = 'ref' | 'refs' | 'refimg' | 'refsimg'
+type tags = 'ref' | 'refs' | 'refimg' | 'refsimg';
 
 /**
  * Context Object class for $ref() and $refimg()
  */
 export class RefsContext {
-
   tag: tags;
 
   pagePath: string;
 
-  options?: Record<string, string|undefined>;
+  options?: Record<string, string | undefined>;
 
-  constructor(tag: tags, pagePath: string, options: Record<string, string|undefined>) {
+  constructor(
+    tag: tags,
+    pagePath: string,
+    options: Record<string, string | undefined>,
+  ) {
     this.tag = tag;
 
     this.pagePath = pagePath;
 
     // remove undefined keys
-    Object.keys(options).forEach(key => options[key] === undefined && delete options[key]);
+    for (const key of Object.keys(options)) {
+      if (options[key] === undefined) {
+        delete options[key];
+      }
+    }
 
     this.options = options;
   }
@@ -66,12 +73,14 @@ export class RefsContext {
   }
 
   getOptGrid(): string | undefined {
-    return GRID_AVAILABLE_OPTIONS_LIST.find(item => item === this.options?.grid);
+    return GRID_AVAILABLE_OPTIONS_LIST.find(
+      (item) => item === this.options?.grid,
+    );
   }
 
   isOptGridColumnEnabled(): boolean {
     const optGrid = this.getOptGrid();
-    return (optGrid != null) && optGrid.startsWith('col-');
+    return optGrid?.startsWith('col-') ?? false;
   }
 
   /**
@@ -162,5 +171,4 @@ export class RefsContext {
 
     return columnsNum;
   }
-
 }

+ 116 - 70
packages/remark-attachment-refs/src/client/services/renderer/refs.ts

@@ -1,5 +1,8 @@
 import { pathUtils } from '@growi/core/dist/utils';
-import type { TextGrowiPluginDirective, LeafGrowiPluginDirective } from '@growi/remark-growi-directive';
+import type {
+  LeafGrowiPluginDirective,
+  TextGrowiPluginDirective,
+} from '@growi/remark-growi-directive';
 import { remarkGrowiDirectivePluginType } from '@growi/remark-growi-directive';
 import type { Nodes as HastNode } from 'hast';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
@@ -9,85 +12,122 @@ import { visit } from 'unist-util-visit';
 
 import loggerFactory from '../../../utils/logger';
 
-const logger = loggerFactory('growi:remark-attachment-refs:services:renderer:refs');
+const logger = loggerFactory(
+  'growi:remark-attachment-refs:services:renderer:refs',
+);
 
 const REF_SINGLE_NAME_PATTERN = new RegExp(/refimg|ref/);
 const REF_MULTI_NAME_PATTERN = new RegExp(/refsimg|refs|gallery/);
 
 const REF_SUPPORTED_ATTRIBUTES = ['fileNameOrId', 'pagePath'];
-const REF_IMG_SUPPORTED_ATTRIBUTES = ['fileNameOrId', 'pagePath', 'width', 'height', 'maxWidth', 'maxHeight', 'alt'];
+const REF_IMG_SUPPORTED_ATTRIBUTES = [
+  'fileNameOrId',
+  'pagePath',
+  'width',
+  'height',
+  'maxWidth',
+  'maxHeight',
+  'alt',
+];
 const REFS_SUPPORTED_ATTRIBUTES = ['pagePath', 'prefix', 'depth', 'regexp'];
 const REFS_IMG_SUPPORTED_ATTRIBUTES = [
-  'pagePath', 'prefix', 'depth', 'regexp', 'width', 'height', 'maxWidth', 'maxHeight', 'display', 'grid', 'gridGap', 'noCarousel',
+  'pagePath',
+  'prefix',
+  'depth',
+  'regexp',
+  'width',
+  'height',
+  'maxWidth',
+  'maxHeight',
+  'display',
+  'grid',
+  'gridGap',
+  'noCarousel',
 ];
 
-type DirectiveAttributes = Record<string, string>
-type GrowiPluginDirective = TextGrowiPluginDirective | LeafGrowiPluginDirective
+type DirectiveAttributes = Record<string, string>;
+type GrowiPluginDirective = TextGrowiPluginDirective | LeafGrowiPluginDirective;
 
-export const remarkPlugin: Plugin = function() {
-  return (tree) => {
-    visit(tree, (node: GrowiPluginDirective) => {
-      if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
-        if (typeof node.name !== 'string') {
-          return;
-        }
-        const data = node.data ?? (node.data = {});
-        const attributes = node.attributes as DirectiveAttributes || {};
-        const attrEntries = Object.entries(attributes);
-
-        if (REF_SINGLE_NAME_PATTERN.test(node.name)) {
-          // determine fileNameOrId
-          // order:
-          //   1: ref(file=..., ...)
-          //   2: ref(id=..., ...)
-          //   3: refs(firstArgs, ...)
-          let fileNameOrId: string = attributes.file || attributes.id;
-          if (fileNameOrId == null && attrEntries.length > 0) {
-            const [firstAttrKey, firstAttrValue] = attrEntries[0];
-            fileNameOrId = (firstAttrValue === '' && !REF_SUPPORTED_ATTRIBUTES.concat(REF_IMG_SUPPORTED_ATTRIBUTES).includes(firstAttrValue))
-              ? firstAttrKey : '';
-          }
-          attributes.fileNameOrId = fileNameOrId;
+export const remarkPlugin: Plugin = () => (tree) => {
+  visit(tree, (node: GrowiPluginDirective) => {
+    if (
+      node.type === remarkGrowiDirectivePluginType.Text ||
+      node.type === remarkGrowiDirectivePluginType.Leaf
+    ) {
+      if (typeof node.name !== 'string') {
+        return;
+      }
+      if (node.data == null) {
+        node.data = {};
+      }
+      const data = node.data;
+      const attributes = (node.attributes as DirectiveAttributes) || {};
+      const attrEntries = Object.entries(attributes);
+
+      if (REF_SINGLE_NAME_PATTERN.test(node.name)) {
+        // determine fileNameOrId
+        // order:
+        //   1: ref(file=..., ...)
+        //   2: ref(id=..., ...)
+        //   3: refs(firstArgs, ...)
+        let fileNameOrId: string = attributes.file || attributes.id;
+        if (fileNameOrId == null && attrEntries.length > 0) {
+          const [firstAttrKey, firstAttrValue] = attrEntries[0];
+          fileNameOrId =
+            firstAttrValue === '' &&
+            !REF_SUPPORTED_ATTRIBUTES.concat(
+              REF_IMG_SUPPORTED_ATTRIBUTES,
+            ).includes(firstAttrValue)
+              ? firstAttrKey
+              : '';
         }
-        else if (REF_MULTI_NAME_PATTERN.test(node.name)) {
-          // set 'page' attribute if the first attribute is only value
-          // e.g.
-          //   case 1: refs(page=/path..., ...)    => page="/path"
-          //   case 2: refs(/path, ...)            => page="/path"
-          //   case 3: refs(/foo, page=/bar ...)   => page="/bar"
-          if (attributes.page == null && attrEntries.length > 0) {
-            const [firstAttrKey, firstAttrValue] = attrEntries[0];
-
-            if (firstAttrValue === '' && !REFS_SUPPORTED_ATTRIBUTES.concat(REFS_IMG_SUPPORTED_ATTRIBUTES).includes(firstAttrValue)) {
-              attributes.page = firstAttrKey;
-            }
+        attributes.fileNameOrId = fileNameOrId;
+      } else if (REF_MULTI_NAME_PATTERN.test(node.name)) {
+        // set 'page' attribute if the first attribute is only value
+        // e.g.
+        //   case 1: refs(page=/path..., ...)    => page="/path"
+        //   case 2: refs(/path, ...)            => page="/path"
+        //   case 3: refs(/foo, page=/bar ...)   => page="/bar"
+        if (attributes.page == null && attrEntries.length > 0) {
+          const [firstAttrKey, firstAttrValue] = attrEntries[0];
+
+          if (
+            firstAttrValue === '' &&
+            !REFS_SUPPORTED_ATTRIBUTES.concat(
+              REFS_IMG_SUPPORTED_ATTRIBUTES,
+            ).includes(firstAttrValue)
+          ) {
+            attributes.page = firstAttrKey;
           }
         }
-        else {
-          return;
-        }
+      } else {
+        return;
+      }
 
-        logger.debug('a node detected', attributes);
+      logger.debug('a node detected', attributes);
 
-        // kebab case to camel case
-        attributes.maxWidth = attributes['max-width'];
-        attributes.maxHeight = attributes['max-height'];
-        attributes.gridGap = attributes['grid-gap'];
-        attributes.noCarousel = attributes['no-carousel'];
+      // kebab case to camel case
+      attributes.maxWidth = attributes['max-width'];
+      attributes.maxHeight = attributes['max-height'];
+      attributes.gridGap = attributes['grid-gap'];
+      attributes.noCarousel = attributes['no-carousel'];
 
-        data.hName = node.name;
-        data.hProperties = attributes;
-      }
-    });
-  };
+      data.hName = node.name;
+      data.hProperties = attributes;
+    }
+  });
 };
 
 // return absolute path for the specified path
 const getAbsolutePathFor = (relativePath: string, basePath: string) => {
-  const baseUrl = new URL(pathUtils.addTrailingSlash(basePath), 'https://example.com');
+  const baseUrl = new URL(
+    pathUtils.addTrailingSlash(basePath),
+    'https://example.com',
+  );
   const absoluteUrl = new URL(relativePath, baseUrl);
   return decodeURIComponent(
-    pathUtils.normalizePath( // normalize like /foo/bar
+    pathUtils.normalizePath(
+      // normalize like /foo/bar
       absoluteUrl.pathname,
     ),
   );
@@ -100,19 +140,22 @@ const getAbsolutePathFor = (relativePath: string, basePath: string) => {
 //        `pagePath` to be /fuga
 //   when `fromPagePath`=/hoge and `specifiedPath`=undefined,
 //        `pagePath` to be /hoge
-const resolvePath = (pagePath:string, basePath: string) => {
-  const baseUrl = new URL(pathUtils.addTrailingSlash(basePath), 'https://example.com');
+const resolvePath = (pagePath: string, basePath: string) => {
+  const baseUrl = new URL(
+    pathUtils.addTrailingSlash(basePath),
+    'https://example.com',
+  );
   const absoluteUrl = new URL(pagePath, baseUrl);
   return decodeURIComponent(absoluteUrl.pathname);
 };
 
 type RefRehypePluginParams = {
-  pagePath?: string,
-}
+  pagePath?: string;
+};
 
 export const rehypePlugin: Plugin<[RefRehypePluginParams]> = (options = {}) => {
   if (options.pagePath == null) {
-    throw new Error('refs rehype plugin requires \'pagePath\' option');
+    throw new Error("refs rehype plugin requires 'pagePath' option");
   }
 
   return (tree) => {
@@ -121,11 +164,14 @@ export const rehypePlugin: Plugin<[RefRehypePluginParams]> = (options = {}) => {
     }
 
     const basePagePath = options.pagePath;
-    const elements = selectAll('ref, refimg, refs, refsimg, gallery', tree as HastNode);
+    const elements = selectAll(
+      'ref, refimg, refs, refsimg, gallery',
+      tree as HastNode,
+    );
 
-    elements.forEach((refElem) => {
+    for (const refElem of elements) {
       if (refElem.properties == null) {
-        return;
+        continue;
       }
 
       const prefix = refElem.properties.prefix;
@@ -140,17 +186,17 @@ export const rehypePlugin: Plugin<[RefRehypePluginParams]> = (options = {}) => {
       // set basePagePath when pagePath is undefined or invalid
       if (pagePath == null || typeof pagePath !== 'string') {
         refElem.properties.pagePath = basePagePath;
-        return;
+        continue;
       }
 
-      // return when page is already determined and aboslute path
+      // return when page is already determined and absolute path
       if (pathUtils.hasHeadingSlash(pagePath)) {
-        return;
+        continue;
       }
 
       // resolve relative path
       refElem.properties.pagePath = getAbsolutePathFor(pagePath, basePagePath);
-    });
+    }
   };
 };
 

+ 32 - 17
packages/remark-attachment-refs/src/client/stores/refs.tsx

@@ -6,17 +6,21 @@ import type { SWRResponse } from 'swr';
 import useSWR, { unstable_serialize } from 'swr';
 
 export const useSWRxRef = (
-    pagePath: string, fileNameOrId: string, isImmutable?: boolean,
+  pagePath: string,
+  fileNameOrId: string,
+  isImmutable?: boolean,
 ): SWRResponse<IAttachmentHasId | null, Error> => {
   return useSWR(
     ['/_api/attachment-refs/ref', pagePath, fileNameOrId, isImmutable],
     ([endpoint, pagePath, fileNameOrId]) => {
-      return axios.get(endpoint, {
-        params: {
-          pagePath,
-          fileNameOrId,
-        },
-      }).then(result => result.data.attachment)
+      return axios
+        .get(endpoint, {
+          params: {
+            pagePath,
+            fileNameOrId,
+          },
+        })
+        .then((result) => result.data.attachment)
         .catch(() => null);
     },
     {
@@ -29,20 +33,31 @@ export const useSWRxRef = (
 };
 
 export const useSWRxRefs = (
-    pagePath: string, prefix?: string, options?: Record<string, string | undefined>, isImmutable?: boolean,
+  pagePath: string,
+  prefix?: string,
+  options?: Record<string, string | undefined>,
+  isImmutable?: boolean,
 ): SWRResponse<IAttachmentHasId[], AxiosError<string>> => {
   const serializedOptions = unstable_serialize(options);
 
   return useSWR(
-    ['/_api/attachment-refs/refs', pagePath, prefix, serializedOptions, isImmutable],
-    async([endpoint, pagePath, prefix]) => {
-      return axios.get(endpoint, {
-        params: {
-          pagePath,
-          prefix,
-          options,
-        },
-      }).then(result => result.data.attachments);
+    [
+      '/_api/attachment-refs/refs',
+      pagePath,
+      prefix,
+      serializedOptions,
+      isImmutable,
+    ],
+    async ([endpoint, pagePath, prefix]) => {
+      return axios
+        .get(endpoint, {
+          params: {
+            pagePath,
+            prefix,
+            options,
+          },
+        })
+        .then((result) => result.data.attachments);
     },
     {
       keepPreviousData: true,

+ 1 - 1
packages/remark-attachment-refs/src/server/index.ts

@@ -1,6 +1,6 @@
 import { routesFactory } from './routes/refs';
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
+// biome-ignore lint/suspicious/noExplicitAny: ignore
 const middleware = (crowi: any, app: any): void => {
   const refs = routesFactory(crowi);
 

+ 167 - 127
packages/remark-attachment-refs/src/server/routes/refs.ts

@@ -1,9 +1,9 @@
-import type { IPage, IUser, IAttachment } from '@growi/core';
+import type { IAttachment, IPage, IUser } from '@growi/core';
 import { serializeAttachmentSecurely } from '@growi/core/dist/models/serializers';
 import { OptionParser } from '@growi/core/dist/remark-plugins';
 import type { Request } from 'express';
 import { Router } from 'express';
-import type { Model, HydratedDocument } from 'mongoose';
+import type { HydratedDocument, Model } from 'mongoose';
 import mongoose, { model, Types } from 'mongoose';
 import { FilterXSS } from 'xss';
 
@@ -11,12 +11,11 @@ import loggerFactory from '../../utils/logger';
 
 const logger = loggerFactory('growi:remark-attachment-refs:routes:refs');
 
-
 function generateRegexp(expression: string): RegExp {
   // https://regex101.com/r/uOrwqt/2
   const matches = expression.match(/^\/(.+)\/(.*)?$/);
 
-  return (matches != null)
+  return matches != null
     ? new RegExp(matches[1], matches[2])
     : new RegExp(expression);
 }
@@ -45,7 +44,9 @@ function addDepthCondition(query, pagePath, optionsDepth) {
   const end = range.end;
 
   if (start < 1 || end < 1) {
-    throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
+    throw new Error(
+      `specified depth is [${start}:${end}] : start and end are must be larger than 1`,
+    );
   }
 
   // count slash
@@ -58,168 +59,207 @@ function addDepthCondition(query, pagePath, optionsDepth) {
   });
 }
 
-
 type RequestWithUser = Request & { user: HydratedDocument<IUser> };
 
 const loginRequiredFallback = (req, res) => {
   return res.status(403).send('login required');
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+// biome-ignore lint/suspicious/noExplicitAny: ignore
 export const routesFactory = (crowi): any => {
-
-  const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
+  const loginRequired = crowi.require('../middlewares/login-required')(
+    crowi,
+    true,
+    loginRequiredFallback,
+  );
   const accessTokenParser = crowi.accessTokenParser;
 
   const router = Router();
 
   const ObjectId = Types.ObjectId;
 
-  const Page = mongoose.model <HydratedDocument<IPage>, Model<any> & any>('Page');
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
+  const Page = mongoose.model<HydratedDocument<IPage>, Model<any> & any>(
+    'Page',
+  );
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   const { PageQueryBuilder } = Page as any;
 
   /**
    * return an Attachment model
    */
-  router.get('/ref', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
-    const user = req.user;
-    const { pagePath, fileNameOrId } = req.query;
-    const filterXSS = new FilterXSS();
-
-    if (pagePath == null) {
-      res.status(400).send('the param \'pagePath\' must be set.');
-      return;
-    }
-
-    const page = await Page.findByPathAndViewer(pagePath, user, undefined, true);
-
-    // not found
-    if (page == null) {
-      res.status(404).send(filterXSS.process(`pagePath: '${pagePath}' is not found or forbidden.`));
-      return;
-    }
-
-    // convert ObjectId
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const orConditions: any[] = [{ originalName: fileNameOrId }];
-    if (fileNameOrId != null && ObjectId.isValid(fileNameOrId.toString())) {
-      orConditions.push({ _id: new ObjectId(fileNameOrId.toString()) });
-    }
-
-    const Attachment = model<IAttachment>('Attachment');
-    const attachment = await Attachment
-      .findOne({
+  router.get(
+    '/ref',
+    accessTokenParser,
+    loginRequired,
+    async (req: RequestWithUser, res) => {
+      const user = req.user;
+      const { pagePath, fileNameOrId } = req.query;
+      const filterXSS = new FilterXSS();
+
+      if (pagePath == null) {
+        res.status(400).send("the param 'pagePath' must be set.");
+        return;
+      }
+
+      const page = await Page.findByPathAndViewer(
+        pagePath,
+        user,
+        undefined,
+        true,
+      );
+
+      // not found
+      if (page == null) {
+        res
+          .status(404)
+          .send(
+            filterXSS.process(
+              `pagePath: '${pagePath}' is not found or forbidden.`,
+            ),
+          );
+        return;
+      }
+
+      // convert ObjectId
+      // biome-ignore lint/suspicious/noExplicitAny: ignore
+      const orConditions: any[] = [{ originalName: fileNameOrId }];
+      if (fileNameOrId != null && ObjectId.isValid(fileNameOrId.toString())) {
+        orConditions.push({ _id: new ObjectId(fileNameOrId.toString()) });
+      }
+
+      const Attachment = model<IAttachment>('Attachment');
+      const attachment = await Attachment.findOne({
         page: page._id,
         $or: orConditions,
-      })
-      .populate('creator');
-
-    // not found
-    if (attachment == null) {
-      res.status(404).send(filterXSS.process(`attachment '${fileNameOrId}' is not found.`));
-      return;
-    }
-
-    logger.debug(`attachment '${attachment.id}' is found from fileNameOrId '${fileNameOrId}'`);
-
-    // forbidden
-    const isAccessible = await Page.isAccessiblePageByViewer(attachment.page, user);
-    if (!isAccessible) {
-      logger.debug(`attachment '${attachment.id}' is forbidden for user '${user && user.username}'`);
-      res.status(403).send(`page '${attachment.page}' is forbidden.`);
-      return;
-    }
-
-    res.status(200).send({ attachment: serializeAttachmentSecurely(attachment) });
-  });
+      }).populate('creator');
+
+      // not found
+      if (attachment == null) {
+        res
+          .status(404)
+          .send(
+            filterXSS.process(`attachment '${fileNameOrId}' is not found.`),
+          );
+        return;
+      }
+
+      logger.debug(
+        `attachment '${attachment.id}' is found from fileNameOrId '${fileNameOrId}'`,
+      );
+
+      // forbidden
+      const isAccessible = await Page.isAccessiblePageByViewer(
+        attachment.page,
+        user,
+      );
+      if (!isAccessible) {
+        logger.debug(
+          `attachment '${attachment.id}' is forbidden for user '${user?.username}'`,
+        );
+        res.status(403).send(`page '${attachment.page}' is forbidden.`);
+        return;
+      }
+
+      res
+        .status(200)
+        .send({ attachment: serializeAttachmentSecurely(attachment) });
+    },
+  );
 
   /**
    * return a list of Attachment
    */
-  router.get('/refs', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
-    const user = req.user;
-    const { prefix, pagePath } = req.query;
-    const options: Record<string, string | undefined> = JSON.parse(req.query.options?.toString() ?? '');
-
-    // check either 'prefix' or 'pagePath ' is specified
-    if (prefix == null && pagePath == null) {
-      res.status(400).send('either the param \'prefix\' or \'pagePath\' must be set.');
-      return;
-    }
-
-    // check regex
-    let regex: RegExp | null = null;
-    const regexOptionValue = options.regexp ?? options.regex;
-    if (regexOptionValue != null) {
-      // check the length to avoid ReDoS
-      if (regexOptionValue.length > 400) {
-        res.status(400).send('the length of the \'regex\' option is too long.');
+  router.get(
+    '/refs',
+    accessTokenParser,
+    loginRequired,
+    async (req: RequestWithUser, res) => {
+      const user = req.user;
+      const { prefix, pagePath } = req.query;
+      const options: Record<string, string | undefined> = JSON.parse(
+        req.query.options?.toString() ?? '',
+      );
+
+      // check either 'prefix' or 'pagePath ' is specified
+      if (prefix == null && pagePath == null) {
+        res
+          .status(400)
+          .send("either the param 'prefix' or 'pagePath' must be set.");
         return;
       }
 
-      try {
-        regex = generateRegexp(regexOptionValue);
-      }
-      catch (err) {
-        res.status(400).send('the \'regex\' option is invalid as RegExp.');
-        return;
+      // check regex
+      let regex: RegExp | null = null;
+      const regexOptionValue = options.regexp ?? options.regex;
+      if (regexOptionValue != null) {
+        // check the length to avoid ReDoS
+        if (regexOptionValue.length > 400) {
+          res.status(400).send("the length of the 'regex' option is too long.");
+          return;
+        }
+
+        try {
+          regex = generateRegexp(regexOptionValue);
+        } catch (err) {
+          res.status(400).send("the 'regex' option is invalid as RegExp.");
+          return;
+        }
       }
-    }
 
-    let builder;
+      // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
+      let builder;
 
-    // builder to retrieve descendance
-    if (prefix != null) {
-      builder = new PageQueryBuilder(Page.find())
-        .addConditionToListWithDescendants(prefix)
-        .addConditionToExcludeTrashed();
-    }
-    // builder to get single page
-    else {
-      builder = new PageQueryBuilder(Page.find({ path: pagePath }));
-    }
+      // builder to retrieve descendance
+      if (prefix != null) {
+        builder = new PageQueryBuilder(Page.find())
+          .addConditionToListWithDescendants(prefix)
+          .addConditionToExcludeTrashed();
+      }
+      // builder to get single page
+      else {
+        builder = new PageQueryBuilder(Page.find({ path: { $eq: pagePath } }));
+      }
 
-    Page.addConditionToFilteringByViewerForList(builder, user, false);
+      Page.addConditionToFilteringByViewerForList(builder, user, false);
 
-    let pageQuery = builder.query;
+      let pageQuery = builder.query;
 
-    // depth
-    try {
-      if (prefix != null && options.depth != null) {
-        pageQuery = addDepthCondition(pageQuery, prefix, options.depth);
+      // depth
+      try {
+        if (prefix != null && options.depth != null) {
+          pageQuery = addDepthCondition(pageQuery, prefix, options.depth);
+        }
+      } catch (err) {
+        const filterXSS = new FilterXSS();
+        return res.status(400).send(filterXSS.process(err.toString()));
       }
-    }
-    catch (err) {
-      const filterXSS = new FilterXSS();
-      return res.status(400).send(filterXSS.process(err.toString()));
-    }
 
-    const results = await pageQuery.select('id').exec();
-    const pageIds = results.map(result => result.id);
+      const results = await pageQuery.select('id').exec();
+      const pageIds = results.map((result) => result.id);
 
-    logger.debug('retrieve attachments for pages:', pageIds);
+      logger.debug('retrieve attachments for pages:', pageIds);
 
-    // create query to find
-    const Attachment = model<IAttachment>('Attachment');
-    let query = Attachment
-      .find({
+      // create query to find
+      const Attachment = model<IAttachment>('Attachment');
+      let query = Attachment.find({
         page: { $in: pageIds },
       });
-    // add regex condition
-    if (regex != null) {
-      query = query.and([
-        { originalName: { $regex: regex } },
-      ]);
-    }
-
-    const attachments = await query
-      .populate('creator')
-      .exec();
-
-    res.status(200).send({ attachments: attachments.map(attachment => serializeAttachmentSecurely(attachment)) });
-  });
+      // add regex condition
+      if (regex != null) {
+        query = query.and([{ originalName: { $regex: regex } }]);
+      }
+
+      const attachments = await query.populate('creator').exec();
+
+      res.status(200).send({
+        attachments: attachments.map((attachment) =>
+          serializeAttachmentSecurely(attachment),
+        ),
+      });
+    },
+  );
 
   return router;
 };

+ 3 - 4
packages/remark-attachment-refs/src/utils/logger/index.ts

@@ -1,11 +1,10 @@
-import Logger from 'bunyan';
+import type Logger from 'bunyan';
 import { createLogger } from 'universal-bunyan';
 
-const loggerFactory = function(name: string): Logger {
-  return createLogger({
+const loggerFactory = (name: string): Logger =>
+  createLogger({
     name,
     config: { default: 'info' },
   });
-};
 
 export default loggerFactory;

+ 1 - 3
packages/remark-attachment-refs/tsconfig.json

@@ -15,7 +15,5 @@
     "noImplicitAny": false,
     "noImplicitOverride": true
   },
-  "include": [
-    "src"
-  ]
+  "include": ["src"]
 }

+ 1 - 3
packages/remark-attachment-refs/vite.server.config.ts

@@ -21,9 +21,7 @@ export default defineConfig({
     outDir: 'dist/server',
     sourcemap: true,
     lib: {
-      entry: [
-        'src/server/index.ts',
-      ],
+      entry: ['src/server/index.ts'],
       name: 'remark-attachment-refs-libs',
       formats: ['cjs'],
     },