Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/master' into support/156162-167477-pluginkit-package-biome

Yuki Takei 9 mesi fa
parent
commit
c842e89635
27 ha cambiato i file con 1126 aggiunte e 612 eliminazioni
  1. 1 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupForm.tsx
  2. 1 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupTable.tsx
  3. 2 2
      apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  4. 1 1
      apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx
  5. 1 1
      apps/app/src/client/components/Admin/Users/UserTable.tsx
  6. 1 1
      apps/app/src/client/components/Me/ExternalAccountRow.jsx
  7. 285 0
      apps/app/src/utils/axios-date-conversion.spec.ts
  8. 46 0
      apps/app/src/utils/axios.ts
  9. 1 4
      biome.json
  10. 1 1
      packages/remark-attachment-refs/.eslintignore
  11. 0 5
      packages/remark-attachment-refs/.eslintrc.cjs
  12. 1 1
      packages/remark-attachment-refs/package.json
  13. 33 19
      packages/remark-attachment-refs/src/client/components/AttachmentList.tsx
  14. 187 176
      packages/remark-attachment-refs/src/client/components/ExtractedAttachments.tsx
  15. 9 5
      packages/remark-attachment-refs/src/client/components/Gallery.tsx
  16. 31 28
      packages/remark-attachment-refs/src/client/components/Ref.tsx
  17. 54 42
      packages/remark-attachment-refs/src/client/components/RefImg.tsx
  18. 46 36
      packages/remark-attachment-refs/src/client/components/Refs.tsx
  19. 88 55
      packages/remark-attachment-refs/src/client/components/RefsImg.tsx
  20. 16 8
      packages/remark-attachment-refs/src/client/components/util/refs-context.ts
  21. 116 70
      packages/remark-attachment-refs/src/client/services/renderer/refs.ts
  22. 32 17
      packages/remark-attachment-refs/src/client/stores/refs.tsx
  23. 1 1
      packages/remark-attachment-refs/src/server/index.ts
  24. 167 127
      packages/remark-attachment-refs/src/server/routes/refs.ts
  25. 3 4
      packages/remark-attachment-refs/src/utils/logger/index.ts
  26. 1 3
      packages/remark-attachment-refs/tsconfig.json
  27. 1 3
      packages/remark-attachment-refs/vite.server.config.ts

+ 1 - 1
apps/app/src/client/components/Admin/UserGroup/UserGroupForm.tsx

@@ -76,7 +76,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
           userGroup?.createdAt != null && (
             <div className="row mb-3">
               <p className="col-md-2 col-form-label">{t('Created')}</p>
-              <p className="col-md-6 my-auto">{dateFnsFormat(new Date(userGroup.createdAt), 'yyyy-MM-dd')}</p>
+              <p className="col-md-6 my-auto">{dateFnsFormat(userGroup.createdAt, 'yyyy-MM-dd')}</p>
             </div>
           )
         }

+ 1 - 1
apps/app/src/client/components/Admin/UserGroup/UserGroupTable.tsx

@@ -218,7 +218,7 @@ export const UserGroupTable: FC<Props> = ({
                     })}
                   </ul>
                 </td>
-                <td>{dateFnsFormat(new Date(group.createdAt), 'yyyy-MM-dd')}</td>
+                <td>{dateFnsFormat(group.createdAt, 'yyyy-MM-dd')}</td>
                 {isAclEnabled
                   ? (
                     <td>

+ 2 - 2
apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -43,8 +43,8 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
                 <strong>{relatedUser.username}</strong>
               </td>
               <td>{relatedUser.name}</td>
-              <td>{relatedUser.createdAt ? dateFnsFormat(new Date(relatedUser.createdAt), 'yyyy-MM-dd') : ''}</td>
-              <td>{relatedUser.lastLoginAt ? dateFnsFormat(new Date(relatedUser.lastLoginAt), 'yyyy-MM-dd HH:mm:ss') : ''}</td>
+              <td>{relatedUser.createdAt ? dateFnsFormat(relatedUser.createdAt, 'yyyy-MM-dd') : ''}</td>
+              <td>{relatedUser.lastLoginAt ? dateFnsFormat(relatedUser.lastLoginAt, 'yyyy-MM-dd HH:mm:ss') : ''}</td>
               {!props.isExternalGroup && (
                 <td>
                   <div className="btn-group admin-user-menu">

+ 1 - 1
apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx

@@ -89,7 +89,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
                     : (<span className="badge bg-warning text-dark">{t('user_management.unset')}</span>)
                   }
                 </td>
-                <td>{dateFnsFormat(new Date(ea.createdAt), 'yyyy-MM-dd')}</td>
+                <td>{dateFnsFormat(ea.createdAt, 'yyyy-MM-dd')}</td>
                 <td>
                   <div className="btn-group admin-user-menu">
                     <button type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">

+ 1 - 1
apps/app/src/client/components/Admin/Users/UserTable.tsx

@@ -168,7 +168,7 @@ const UserTable = (props: UserTableProps) => {
                 </td>
                 <td>{user.name}</td>
                 <td>{user.email}</td>
-                <td>{dateFnsFormat(new Date(user.createdAt), 'yyyy-MM-dd')}</td>
+                <td>{dateFnsFormat(user.createdAt, 'yyyy-MM-dd')}</td>
                 <td>
                   {user.lastLoginAt && <span>{dateFnsFormat(new Date(user.lastLoginAt), 'yyyy-MM-dd HH:mm')}</span>}
                 </td>

+ 1 - 1
apps/app/src/client/components/Me/ExternalAccountRow.jsx

@@ -15,7 +15,7 @@ const ExternalAccountRow = (props) => {
       <td>
         <strong>{ account.accountId }</strong>
       </td>
-      <td>{dateFnsFormat(new Date(account.createdAt), 'yyyy-MM-dd')}</td>
+      <td>{dateFnsFormat(account.createdAt, 'yyyy-MM-dd')}</td>
       <td className="text-center">
         <button
           type="button"

+ 285 - 0
apps/app/src/utils/axios-date-conversion.spec.ts

@@ -0,0 +1,285 @@
+import { convertDateStringsToDates } from './axios';
+
+describe('convertDateStringsToDates', () => {
+
+  // Test case 1: Basic conversion in a flat object
+  test('should convert ISO date strings to Date objects in a flat object', () => {
+    const dateString = '2023-01-15T10:00:00.000Z';
+    const input = {
+      id: 1,
+      createdAt: dateString,
+      name: 'Test Item',
+    };
+    const expected = {
+      id: 1,
+      createdAt: new Date(dateString),
+      name: 'Test Item',
+    };
+    const result = convertDateStringsToDates(input);
+    expect(result.createdAt).toBeInstanceOf(Date);
+    expect(result.createdAt.toISOString()).toEqual(dateString);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 2: Nested objects
+  test('should recursively convert ISO date strings in nested objects', () => {
+    const dateString1 = '2023-02-20T12:30:00.000Z';
+    const dateString2 = '2023-03-01T08:00:00.000Z';
+    const input = {
+      data: {
+        item1: {
+          updatedAt: dateString1,
+          value: 10,
+        },
+        item2: {
+          nested: {
+            deletedAt: dateString2,
+            isActive: false,
+          },
+        },
+      },
+    };
+    const expected = {
+      data: {
+        item1: {
+          updatedAt: new Date(dateString1),
+          value: 10,
+        },
+        item2: {
+          nested: {
+            deletedAt: new Date(dateString2),
+            isActive: false,
+          },
+        },
+      },
+    };
+    const result = convertDateStringsToDates(input);
+    expect(result.data.item1.updatedAt).toBeInstanceOf(Date);
+    expect(result.data.item1.updatedAt.toISOString()).toEqual(dateString1);
+    expect(result.data.item2.nested.deletedAt).toBeInstanceOf(Date);
+    expect(result.data.item2.nested.deletedAt.toISOString()).toEqual(dateString2);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 3: Arrays of objects
+  test('should recursively convert ISO date strings in arrays of objects', () => {
+    const dateString1 = '2023-04-05T14:15:00.000Z';
+    const dateString2 = '2023-05-10T16:00:00.000Z';
+    const input = [
+      { id: 1, eventDate: dateString1 },
+      { id: 2, eventDate: dateString2, data: { nestedProp: 'value' } },
+    ];
+    const expected = [
+      { id: 1, eventDate: new Date(dateString1) },
+      { id: 2, eventDate: new Date(dateString2), data: { nestedProp: 'value' } },
+    ];
+    const result = convertDateStringsToDates(input);
+    expect(result[0].eventDate).toBeInstanceOf(Date);
+    expect(result[0].eventDate.toISOString()).toEqual(dateString1);
+    expect(result[1].eventDate).toBeInstanceOf(Date);
+    expect(result[1].eventDate.toISOString()).toEqual(dateString2);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 4: Array containing date strings directly (though less common for this function)
+  test('should handle arrays containing date strings directly', () => {
+    const dateString = '2023-06-20T18:00:00.000Z';
+    const input = ['text', dateString, 123];
+    const expected = ['text', new Date(dateString), 123];
+    const result = convertDateStringsToDates(input);
+    expect(result[1]).toBeInstanceOf(Date);
+    expect(result[1].toISOString()).toEqual(dateString);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 5: Data without date strings should remain unchanged
+  test('should not modify data without ISO date strings', () => {
+    const input = {
+      name: 'Product A',
+      price: 99.99,
+      tags: ['electronic', 'sale'],
+      description: 'Some text',
+    };
+    const originalInput = JSON.parse(JSON.stringify(input)); // Deep copy to ensure no mutation
+    const result = convertDateStringsToDates(input);
+    expect(result).toEqual(originalInput); // Should be deeply equal
+    expect(result).toBe(input); // Confirm it mutated the original object
+  });
+
+  // Test case 6: Null, undefined, and primitive values
+  test('should return primitive values as is', () => {
+    expect(convertDateStringsToDates(null)).toBeNull();
+    expect(convertDateStringsToDates(undefined)).toBeUndefined();
+    expect(convertDateStringsToDates(123)).toBe(123);
+    expect(convertDateStringsToDates('hello')).toBe('hello');
+    expect(convertDateStringsToDates(true)).toBe(true);
+  });
+
+  // Test case 7: Edge case - empty objects/arrays
+  test('should handle empty objects and arrays correctly', () => {
+    const emptyObject = {};
+    const emptyArray = [];
+    expect(convertDateStringsToDates(emptyObject)).toEqual({});
+    expect(convertDateStringsToDates(emptyArray)).toEqual([]);
+    expect(convertDateStringsToDates(emptyObject)).toBe(emptyObject);
+    expect(convertDateStringsToDates(emptyArray)).toEqual(emptyArray);
+  });
+
+  // Test case 8: Date string with different milliseconds (isoDateRegex without .000)
+  test('should handle date strings with varied milliseconds', () => {
+    const dateString = '2023-01-15T10:00:00Z'; // No milliseconds
+    const input = { createdAt: dateString };
+    const expected = { createdAt: new Date(dateString) };
+    const result = convertDateStringsToDates(input);
+    expect(result.createdAt).toBeInstanceOf(Date);
+    expect(result.createdAt.toISOString()).toEqual('2023-01-15T10:00:00.000Z');
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 9: Object with null properties
+  test('should handle objects with null properties', () => {
+    const dateString = '2023-07-01T00:00:00.000Z';
+    const input = {
+      prop1: dateString,
+      prop2: null,
+      prop3: {
+        nestedNull: null,
+        nestedDate: dateString,
+      },
+    };
+    const expected = {
+      prop1: new Date(dateString),
+      prop2: null,
+      prop3: {
+        nestedNull: null,
+        nestedDate: new Date(dateString),
+      },
+    };
+    const result = convertDateStringsToDates(input);
+    expect(result.prop1).toBeInstanceOf(Date);
+    expect(result.prop3.nestedDate).toBeInstanceOf(Date);
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 10: Date string with UTC offset (e.g., +09:00)
+  test('should convert ISO date strings with UTC offset to Date objects', () => {
+    const dateStringWithOffset = '2025-06-12T14:00:00+09:00';
+    const input = {
+      id: 2,
+      eventTime: dateStringWithOffset,
+      details: {
+        lastActivity: '2025-06-12T05:00:00-04:00',
+      },
+    };
+    const expected = {
+      id: 2,
+      eventTime: new Date(dateStringWithOffset),
+      details: {
+        lastActivity: new Date('2025-06-12T05:00:00-04:00'),
+      },
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.eventTime).toBeInstanceOf(Date);
+    expect(result.eventTime.toISOString()).toEqual(new Date(dateStringWithOffset).toISOString());
+    expect(result.details.lastActivity).toBeInstanceOf(Date);
+    expect(result.details.lastActivity.toISOString()).toEqual(new Date('2025-06-12T05:00:00-04:00').toISOString());
+
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 11: Date string with negative UTC offset
+  test('should convert ISO date strings with negative UTC offset (-05:00) to Date objects', () => {
+    const dateStringWithNegativeOffset = '2025-01-01T10:00:00-05:00';
+    const input = {
+      startTime: dateStringWithNegativeOffset,
+    };
+    const expected = {
+      startTime: new Date(dateStringWithNegativeOffset),
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.startTime).toBeInstanceOf(Date);
+    expect(result.startTime.toISOString()).toEqual(new Date(dateStringWithNegativeOffset).toISOString());
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 12: Date string with zero UTC offset (+00:00)
+  test('should convert ISO date strings with explicit zero UTC offset (+00:00) to Date objects', () => {
+    const dateStringWithZeroOffset = '2025-03-15T12:00:00+00:00';
+    const input = {
+      zeroOffsetDate: dateStringWithZeroOffset,
+    };
+    const expected = {
+      zeroOffsetDate: new Date(dateStringWithZeroOffset),
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.zeroOffsetDate).toBeInstanceOf(Date);
+    expect(result.zeroOffsetDate.toISOString()).toEqual(new Date(dateStringWithZeroOffset).toISOString());
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 13: Date string with milliseconds and UTC offset
+  test('should convert ISO date strings with milliseconds and UTC offset to Date objects', () => {
+    const dateStringWithMsAndOffset = '2025-10-20T23:59:59.999-07:00';
+    const input = {
+      detailedTime: dateStringWithMsAndOffset,
+    };
+    const expected = {
+      detailedTime: new Date(dateStringWithMsAndOffset),
+    };
+
+    const result = convertDateStringsToDates(input);
+
+    expect(result.detailedTime).toBeInstanceOf(Date);
+    expect(result.detailedTime.toISOString()).toEqual(new Date(dateStringWithMsAndOffset).toISOString());
+    expect(result).toEqual(expected);
+  });
+
+  // Test case 14: Should NOT convert strings that look like dates but are NOT ISO 8601 or missing timezone
+  test('should NOT convert non-ISO 8601 date-like strings or strings missing timezone', () => {
+    const nonIsoDate1 = '2025/06/12 14:00:00Z'; // Wrong separator
+    const nonIsoDate2 = '2025-06-12T14:00:00'; // Missing timezone
+    const nonIsoDate3 = 'June 12, 2025 14:00:00 GMT'; // Different format
+    const nonIsoDate4 = '2025-06-12T14:00:00+0900'; // Missing colon in offset
+    const nonIsoDate5 = '2025-06-12'; // Date only
+
+    const input = {
+      date1: nonIsoDate1,
+      date2: nonIsoDate2,
+      date3: nonIsoDate3,
+      date4: nonIsoDate4,
+      date5: nonIsoDate5,
+      someOtherString: 'hello world',
+    };
+
+    // Deep copy to ensure comparison is accurate since the function modifies in place
+    const expected = JSON.parse(JSON.stringify(input));
+
+    const result = convertDateStringsToDates(input);
+
+    // Assert that they remain strings (or whatever their original type was)
+    expect(typeof result.date1).toBe('string');
+    expect(typeof result.date2).toBe('string');
+    expect(typeof result.date3).toBe('string');
+    expect(typeof result.date4).toBe('string');
+    expect(typeof result.date5).toBe('string');
+    expect(typeof result.someOtherString).toBe('string');
+
+    // Ensure the entire object is unchanged for these properties
+    expect(result.date1).toEqual(nonIsoDate1);
+    expect(result.date2).toEqual(nonIsoDate2);
+    expect(result.date3).toEqual(nonIsoDate3);
+    expect(result.date4).toEqual(nonIsoDate4);
+    expect(result.date5).toEqual(nonIsoDate5);
+    expect(result.someOtherString).toEqual('hello world');
+
+    // Finally, assert that the overall result is identical to the input for these non-matching strings
+    expect(result).toEqual(expected);
+  });
+
+});

+ 46 - 0
apps/app/src/utils/axios.ts

@@ -6,11 +6,57 @@ import qs from 'qs';
 // eslint-disable-next-line no-restricted-imports
 export * from 'axios';
 
+const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(Z|[+-]\d{2}:\d{2})$/;
+
+export function convertDateStringsToDates(data: any): any {
+  if (typeof data !== 'object' || data === null) {
+    if (typeof data === 'string' && isoDateRegex.test(data)) {
+      return new Date(data);
+    }
+    return data;
+  }
+
+  if (Array.isArray(data)) {
+    return data.map(item => convertDateStringsToDates(item));
+  }
+
+  for (const key of Object.keys(data)) {
+    const value = data[key];
+    if (typeof value === 'string' && isoDateRegex.test(value)) {
+      data[key] = new Date(value);
+    }
+
+    else if (typeof value === 'object' && value !== null) {
+      data[key] = convertDateStringsToDates(value);
+    }
+  }
+  return data;
+}
+
+// Determine the base array of transformers
+let baseTransformers = axios.defaults.transformResponse;
+
+if (baseTransformers == null) {
+  baseTransformers = [];
+}
+
+else if (!Array.isArray(baseTransformers)) {
+  // If it's a single transformer function, wrap it in an array
+  baseTransformers = [baseTransformers];
+}
+
+
 const customAxios = axios.create({
   headers: {
     'X-Requested-With': 'XMLHttpRequest',
     'Content-Type': 'application/json',
   },
+
+  transformResponse: baseTransformers.concat(
+    (data) => {
+      return convertDateStringsToDates(data);
+    },
+  ),
 });
 
 // serialize Date config: https://github.com/axios/axios/issues/1548#issuecomment-548306666

+ 1 - 4
biome.json

@@ -21,10 +21,7 @@
       "./apps/**",
       "./packages/core/**",
       "./packages/editor/**",
-      "./packages/pdf-converter-client/**",
-      "./packages/pluginkit/**",
-      "./packages/presentation/**",
-      "./packages/remark-attachment-refs/**"
+      "./packages/pdf-converter-client/**"
     ]
   },
   "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'],
     },