فهرست منبع

Merge branch 'feat/135772-pdf-page-bulk-export' into imprv/156177-159580-change-pdf-converter-type-to-module

Futa Arai 1 سال پیش
والد
کامیت
4bd0bff620
31فایلهای تغییر یافته به همراه697 افزوده شده و 236 حذف شده
  1. 3 1
      apps/app/package.json
  2. 8 4
      apps/app/public/static/locales/en_US/admin.json
  3. 6 4
      apps/app/public/static/locales/en_US/translation.json
  4. 8 4
      apps/app/public/static/locales/fr_FR/admin.json
  5. 6 4
      apps/app/public/static/locales/fr_FR/translation.json
  6. 8 4
      apps/app/public/static/locales/ja_JP/admin.json
  7. 6 4
      apps/app/public/static/locales/ja_JP/translation.json
  8. 8 4
      apps/app/public/static/locales/zh_CN/admin.json
  9. 6 4
      apps/app/public/static/locales/zh_CN/translation.json
  10. 0 48
      apps/app/src/client/components/Admin/App/AppSetting.jsx
  11. 8 0
      apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx
  12. 136 0
      apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx
  13. 42 28
      apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx
  14. 1 1
      apps/app/src/client/components/TreeItem/NewPageInput/use-new-page-input.tsx
  15. 0 17
      apps/app/src/client/services/AdminAppContainer.js
  16. 2 2
      apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss
  17. 8 7
      apps/app/src/features/openai/server/services/openai.ts
  18. 89 0
      apps/app/src/features/openai/server/utils/convert-markdown-to-html.ts
  19. 0 65
      apps/app/src/features/openai/server/utils/sanitize-markdown.ts
  20. 10 7
      apps/app/src/features/page-bulk-export/client/components/PageBulkExportSelectModal.tsx
  21. 0 4
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export.ts
  22. 3 0
      apps/app/src/interfaces/activity.ts
  23. 5 0
      apps/app/src/interfaces/res/admin/app-settings.ts
  24. 35 4
      apps/app/src/server/routes/apiv3/app-settings.js
  25. 1 1
      apps/app/src/server/service/config-loader.ts
  26. 2 1
      packages/core/src/utils/page-path-utils/index.ts
  27. 62 0
      packages/pdf-converter-client/dist/index.d.ts
  28. 31 0
      packages/pdf-converter-client/dist/index.js
  29. 1 0
      packages/pdf-converter-client/dist/index.js.map
  30. 2 2
      packages/remark-lsx/src/server/index.ts
  31. 200 16
      pnpm-lock.yaml

+ 3 - 1
apps/app/package.json

@@ -158,7 +158,7 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
-    "next": "^14.2.13",
+    "next": "^14.2.15",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
     "next-superjson": "^1.0.7",
@@ -198,9 +198,11 @@
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "rehype-katex": "^7.0.1",
+    "rehype-meta": "^4.0.1",
     "rehype-raw": "^7.0.0",
     "rehype-sanitize": "^6.0.0",
     "rehype-slug": "^6.0.0",
+    "rehype-stringify": "^10.0.1",
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^4.0.0",
     "remark-directive": "^3.0.0",

+ 8 - 4
apps/app/public/static/locales/en_US/admin.json

@@ -10,6 +10,7 @@
   "only_me": "Only me",
   "only_inside_the_group": "Only inside the group",
   "optional": "Optional",
+  "days": "days",
   "security_settings": {
     "security_settings": "Security Settings",
     "scope_of_page_disclosure": "Scope of page disclosure",
@@ -362,9 +363,11 @@
     "file_uploading": "File uploading",
     "enable_files_except_image": "Enabling this option will allow upload of any file type. Without this option, only image file upload is supported.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
-    "enable_page_bulk_export": "Enable bulk exporting a page and it's child pages",
-    "page_bulk_export_explanation": "When enabled, users will be able to bulk export from the page menu.",
-    "page_bulk_export_warning": "The download period for exported results is 24 hours, and they will be deleted after that. During this period, running multiple large exports consecutively can fill up the file system.",
+    "page_bulk_export_settings": "Page Bulk Export Settings",
+    "enable_page_bulk_export": "Enable bulk export",
+    "page_bulk_export_explanation": "Enables a feature that allows all users to export a page and all it's child pages at once from the menu. Exported data will be automatically deleted after the storage period has passed.",
+    "page_bulk_export_warning": "The bulk page export feature is available to all users. In order to maintain system resources, we ask for your cooperation in using the minimum amount necessary. If you are an administrator, please inform all users of this.",
+    "page_bulk_export_storage_period": "Storage period",
     "update": "Update",
     "mail_settings": "E-mail Settings",
     "mailer_is_not_set_up": "E-mail setting is not set up.",
@@ -1066,7 +1069,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "Add User to User Group",
     "ADMIN_SEARCH_CONNECTION": "Attempting to reconnect to Elasticsearch",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Normalize of Elasticsearch indexes",
-    "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes"
+    "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "Update Page Bulk Export Settings"
   },
   "g2g": {
     "transfer_success": "Completed GROWI to GROWI transfer successfully",

+ 6 - 4
apps/app/public/static/locales/en_US/translation.json

@@ -117,7 +117,7 @@
   "Create under": "Create page under below:",
   "V5 Page Migration": "Convert To V5 Compatibility",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
-  "See_more_detail_on_new_schema": "See more detail on <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a> <span className='growi-custom-icons'>external_link</span> ",
+  "See_more_detail_on_new_schema": "See more detail on <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a> <span class='growi-custom-icons'>external_link</span> ",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "Basic Settings": "Basic Settings",
@@ -157,7 +157,7 @@
   "duplicated_path": "Duplicated path",
   "Link sharing is disabled": "Link sharing is disabled",
   "successfully_saved_the_page": "Successfully saved the page",
-  "you_can_not_create_page_with_this_name": "You can not create page with this name",
+  "you_can_not_create_page_with_this_name_or_hierarchy": "You can not create page with this name or page hierarchy",
   "not_allowed_to_see_this_page": "You cannot see this page",
   "Confirm": "Confirm",
   "Successfully requested": "Successfully requested.",
@@ -615,7 +615,7 @@
     "alert_desc1": "On this page, you can select pages with the checkbox and batch convert to the new v5 compatible format from the \"Bulk operation\" button at the top of the screen.",
     "nopages_title": "Congratulations. Ready to use GROWI v5!",
     "nopages_desc1": "Now all the pages you can manage seem to be in v5 compatible format.",
-    "detail_info": "See the detail information from <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Upgrading GROWI to v5.0.x <span className='growi-custom-icons'>external_link</span></a>.",
+    "detail_info": "See the detail information from <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Upgrading GROWI to v5.0.x <span class='growi-custom-icons'>external_link</span></a>.",
     "modal": {
       "title": "Convert to new v5 compatible format",
       "converting_pages": "Converting pages",
@@ -660,7 +660,9 @@
     "export_page_markdown": "Export page as Markdown",
     "export_page_pdf": "Export page as PDF",
     "bulk_export": "Export page and all child pages",
-    "bulk_export_notice": "Once a download link is ready, a notification will be sent. If the number of pages is large, it may take a while for preparation.",
+    "bulk_export_download_explanation": "A notification will be sent when the export is complete. To download the exported file, click the notification.",
+    "bulk_export_exec_time_warning": "If the number of pages is large, it may take a while to export",
+    "large_bulk_export_warning": "To conserve system resources, please refrain from exporting a large number of pages consecutively",
     "markdown": "Markdown",
     "choose_export_format": "Select export format",
     "bulk_export_started": "Please wait a moment...",

+ 8 - 4
apps/app/public/static/locales/fr_FR/admin.json

@@ -10,6 +10,7 @@
   "only_me": "Seulement moi",
   "only_inside_the_group": "Utilisateurs du groupe",
   "optional": "Optionnel",
+  "days": "jours",
   "security_settings": {
     "security_settings": "Paramètres de sécurité",
     "scope_of_page_disclosure": "Confidentialité de la page",
@@ -362,9 +363,11 @@
     "file_uploading": "Téléversement de fichiers",
     "enable_files_except_image": "Autorise le téléversement de fichiers de n'importe quel type. Lorsque désactivé, seul les fichiers de type image sont autorisés.",
     "attach_enable": "Autorise le téléversement de fichiers de n'importe quel type",
-    "enable_page_bulk_export": "Autoriser l'exportation groupée de pages et de leurs pages subordonnées",
-    "page_bulk_export_explanation": "Si autorisé, l'exportation groupée sera possible à partir du menu de la page.",
-    "page_bulk_export_warning": "La période de téléchargement des résultats exportés est de 24 heures, après quoi ils seront supprimés. Pendant cette période, l’exécution consécutive de plusieurs exportations volumineuses peut remplir le système de fichiers.",
+    "page_bulk_export_settings": "Paramètres d'exportation de pages par lots",
+    "enable_page_bulk_export": "Activer l'exportation groupée",
+    "page_bulk_export_explanation": "Active une fonctionnalité qui permet à tous les utilisateurs d'exporter simultanément toutes les pages sélectionnées dans le menu des pages et leurs pages subordonnées. Les données exportées seront automatiquement supprimées une fois la période de conservation écoulée.",
+    "page_bulk_export_warning": "La fonctionnalité d’exportation de pages en masse est disponible pour tous les utilisateurs. Afin de maintenir les ressources du système, nous demandons votre coopération pour utiliser le montant minimum nécessaire. Si vous êtes administrateur, veuillez en informer tous les utilisateurs.",
+    "page_bulk_export_storage_period": "Date limite de téléchargement",
     "update": "Sauvegarder",
     "mail_settings": "Configuration e-mail",
     "mailer_is_not_set_up": "Paramètres e-mail non configurés.",
@@ -1065,7 +1068,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "Ajouter l'utilisateur au groupe",
     "ADMIN_SEARCH_CONNECTION": "Essai de reconnexion Elasticsearch",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Nomarliser l'index Elasticsearch",
-    "ADMIN_SEARCH_INDICES_REBUILD": "Reconstruire l'index Elasticsearch"
+    "ADMIN_SEARCH_INDICES_REBUILD": "Reconstruire l'index Elasticsearch",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "Mettre à jour les paramètres d'exportation groupée de la page"
   },
   "g2g": {
     "transfer_success": "Transfert de GROWI vers GROWI complété!",

+ 6 - 4
apps/app/public/static/locales/fr_FR/translation.json

@@ -117,7 +117,7 @@
   "Create under": "Créer la page sous:",
   "V5 Page Migration": "Convertir vers la V5",
   "GROWI.5.0_new_schema": "Nouveau schéma GROWI.5.0",
-  "See_more_detail_on_new_schema": "Plus de détails sur <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a> <i class='icon-share-alt'></i> ",
+  "See_more_detail_on_new_schema": "Plus de détails sur <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a><span class='growi-custom-icons'>external_link</span> ",
   "external_account_management": "Gestion des comptes externes",
   "UserGroup": "Groupe utilisateur",
   "Basic Settings": "Paramètres de base",
@@ -157,7 +157,7 @@
   "duplicated_path": "Chemin dupliqué",
   "Link sharing is disabled": "Le partage est désactivé",
   "successfully_saved_the_page": "Page sauvegardée",
-  "you_can_not_create_page_with_this_name": "Vous ne pouvez pas créer cette page",
+  "you_can_not_create_page_with_this_name_or_hierarchy": "Vous ne pouvez pas créer de page avec ce nom ou cette hiérarchie de pages",
   "not_allowed_to_see_this_page": "Vous ne pouvez pas voir cette page",
   "Confirm": "Confirmer",
   "Successfully requested": "Demande envoyée.",
@@ -608,7 +608,7 @@
     "alert_desc1": "Sélectionner les pages à convertir vers le format V5 avec le bouton \"Opération de masse\".",
     "nopages_title": "GROWI V5 est maintenant utilisable!",
     "nopages_desc1": "Toutes les pages ont été converties au format V5.",
-    "detail_info": "Pour plus de détails, voir <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Convertir vers GROWI v5.0.x <span className='growi-custom-icons'>external_link</span></a>.",
+    "detail_info": "Pour plus de détails, voir <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Convertir vers GROWI v5.0.x <span class='growi-custom-icons'>external_link</span></a>.",
     "modal": {
       "title": "Convertir au format V5",
       "converting_pages": "Conversion des pages",
@@ -653,7 +653,9 @@
     "export_page_markdown": "Exporter la page en Markdown",
     "export_page_pdf": "Exporter la page en PDF",
     "bulk_export": "Exporter la page et toutes les pages enfants",
-    "bulk_export_notice": "Une fois qu'un lien de téléchargement est prêt, une notification sera envoyée. Si le nombre de pages est important, la préparation peut prendre un certain temps.",
+    "bulk_export_download_explanation": "Une notification sera envoyée lorsque l’exportation sera terminée. Pour télécharger le fichier exporté, cliquez sur la notification.",
+    "bulk_export_exec_time_warning": "Si le nombre de pages est important, l'exportation peut prendre un certain temps.",
+    "large_bulk_export_warning": "Pour préserver les ressources du système, veuillez éviter d'exporter un grand nombre de pages consécutivement",
     "markdown": "Markdown",
     "choose_export_format": "Sélectionnez le format d'exportation",
     "bulk_export_started": "Patientez s'il-vous-plait...",

+ 8 - 4
apps/app/public/static/locales/ja_JP/admin.json

@@ -19,6 +19,7 @@
   "only_me": "自分のみ",
   "only_inside_the_group": "特定グループのみ",
   "optional": "オプション",
+  "days": "日",
   "security_settings": {
     "security_settings": "セキュリティ設定",
     "scope_of_page_disclosure": "ページの公開範囲",
@@ -371,9 +372,11 @@
     "file_uploading": "ファイルアップロード",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
-    "enable_page_bulk_export": "ページとその配下のページの一括エクスポートを許可",
-    "page_bulk_export_explanation": "許可している場合、個別ページのメニューから一括エクスポートが可能になります。",
-    "page_bulk_export_warning": "エクスポート結果のダウンロード期限は24時間で、それを過ぎるとファイルシステムから削除されます。この間にページ数の多いエクスポートを連続で実行すると、ファイルシステムを圧迫する可能性があります。",
+    "page_bulk_export_settings": "ページ一括エクスポート設定",
+    "enable_page_bulk_export": "一括エクスポートを有効にする",
+    "page_bulk_export_explanation": "すべてのユーザーが、ページメニューから選択したページとその配下ページをまとめてエクスポートできる機能を有効化します。エクスポートされたデータは保存期間経過後に自動的に削除されます。",
+    "page_bulk_export_warning": "ページ一括エクスポート機能は全ユーザーが利用可能です。システムリソースの維持のため、必要最小限の利用にご協力をお願いいたします。管理者の方は、この旨をユーザーの皆様にご周知ください。",
+    "page_bulk_export_storage_period": "保存期間",
     "update": "更新",
     "mail_settings": "メールの設定",
     "mailer_is_not_set_up": "メール設定がセットアップされていません。",
@@ -1076,7 +1079,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "ユーザーグループにユーザーを追加",
     "ADMIN_SEARCH_CONNECTION": "Elasticsearch の再接続の試行",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch のインデックスの正規化",
-    "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド"
+    "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "ページ一括エクスポート設定の更新"
   },
   "g2g": {
     "transfer_success": "G2G移行が完了しました",

+ 6 - 4
apps/app/public/static/locales/ja_JP/translation.json

@@ -116,7 +116,7 @@
   "Create under": "ページを以下に作成",
   "V5 Page Migration": "V5 互換形式 への変換",
   "GROWI.5.0_new_schema": "GROWI.5.0における新スキーマについて",
-  "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><span className='growi-custom-icons'>external_link</span>を参照ください。",
+  "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><span class='growi-custom-icons'>external_link</span>を参照ください。",
   "external_account_management": "外部アカウント管理",
   "UserGroup": "グループ",
   "Basic Settings": "基本設定",
@@ -158,7 +158,7 @@
   "duplicated_path": "重複したパス",
   "Link sharing is disabled": "リンクのシェアは無効化されています",
   "successfully_saved_the_page": "ページが正常に保存されました",
-  "you_can_not_create_page_with_this_name": "この名前でページを作成することはできません",
+  "you_can_not_create_page_with_this_name_or_hierarchy": "この名前、または階層でページを作成することはできません",
   "not_allowed_to_see_this_page": "このページは閲覧できません",
   "Confirm": "確認",
   "Successfully requested": "正常に処理を受け付けました",
@@ -647,7 +647,7 @@
     "alert_desc1": "このページでは、チェックボックスでページを選択し、画面上部の「一括操作」ボタンから新しい v5 互換形式に一括変換できます。",
     "nopages_title": "おめでとうございます。GROWI v5 を使う準備が完了しました!",
     "nopages_desc1": "今あなたが管理可能なページはすべて v5 互換形式になっているようです。",
-    "detail_info": "詳しくは <a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>GROWI v5.0.x へのアップグレード <span className='growi-custom-icons'>external_link</span></a> を参照ください。",
+    "detail_info": "詳しくは <a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>GROWI v5.0.x へのアップグレード <span class='growi-custom-icons'>external_link</span></a> を参照ください。",
     "modal": {
       "title": "新しい v5 互換形式への変換",
       "converting_pages": "以下のページを変換します",
@@ -692,7 +692,9 @@
     "export_page_markdown": "マークダウン形式でページをエクスポート",
     "export_page_pdf": "PDF形式でページをエクスポート",
     "bulk_export": "ページとその配下のページを全てエクスポート",
-    "bulk_export_notice": "ダウンロードの準備が完了すると、通知が届きます。ページ数が多いと、準備に時間がかかる場合があります。",
+    "bulk_export_download_explanation": "エクスポート完了後に通知が届きます。通知をクリックし、ファイルをダウンロードしてください。",
+    "bulk_export_exec_time_warning": "ページ数が多いと、エクスポートに時間がかかる場合があります",
+    "large_bulk_export_warning": "システムリソースの維持のため、ページ数の多いエクスポートを連続して実行することはご遠慮ください",
     "markdown": "マークダウン",
     "choose_export_format": "エクスポート形式を選択してください",
     "bulk_export_started": "ただいま準備中です...",

+ 8 - 4
apps/app/public/static/locales/zh_CN/admin.json

@@ -19,6 +19,7 @@
   "only_me": "只有我",
   "only_inside_the_group": "仅组内",
   "optional": "可选的",
+  "days": "天",
   "security_settings": {
     "security_settings": "安全设置",
     "scope_of_page_disclosure": "页面公开范围",
@@ -371,9 +372,11 @@
     "file_uploading": "文件上传",
     "enable_files_except_image": "启用此选项将允许上传任何文件类型。如果没有此选项,则仅支持图像文件上载。",
     "attach_enable": "如果启用此选项,则可以附加图像文件以外的文件。",
-    "enable_page_bulk_export": "允许批量导出页面及其子页面",
-    "page_bulk_export_explanation": "如果允许,可以从各个页面的菜单中批量导出。",
-    "page_bulk_export_warning": "导出结果的下载期限为24小时,下载期限过后将被删除。在此期间,连续运行多个大导出可能会填满文件系统。",
+    "page_bulk_export_settings": "页面批量导出设置",
+    "enable_page_bulk_export": "启用批量导出",
+    "page_bulk_export_explanation": "启用一项功能,允许所有用户一次性导出从页面菜单中选择的所有页面及其下级页面。保留期限过后,导出的数据将自动删除。",
+    "page_bulk_export_warning": "批量页面导出功能可供所有用户使用。为了维护系统资源,请您配合使用最低限度的资源。如果您是管理员,请将此事实告知所有用户。",
+    "page_bulk_export_storage_period": "储存期限",
     "update": "更新",
     "mail_settings": "邮件设置",
     "mailer_is_not_set_up": "邮件设置尚未完成。",
@@ -1075,7 +1078,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "添加用户到用户组",
     "ADMIN_SEARCH_CONNECTION": "重试Elasticsearch连接",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "试图重新连接Elasticsearch",
-    "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引"
+    "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "更新页面批量导出设置"
   },
   "g2g": {
     "transfer_success": "Completed GROWI to GROWI transfer successfully",

+ 6 - 4
apps/app/public/static/locales/zh_CN/translation.json

@@ -122,7 +122,7 @@
   "Create under": "Create page under below:",
   "V5 Page Migration": "转换为V5的兼容性",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
-  "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <span className='growi-custom-icons'>external_link</span> ",
+  "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <span class='growi-custom-icons'>external_link</span> ",
 	"Markdown Settings": "Markdown设置",
 	"external_account_management": "外部账户管理",
   "UserGroup": "用户组",
@@ -163,7 +163,7 @@
   "duplicated_path": "Duplicated path",
   "Link sharing is disabled": "你不允许分享该链接",
   "successfully_saved_the_page": "成功地保存了该页面",
-  "you_can_not_create_page_with_this_name": "您无法使用此名称创建页面",
+  "you_can_not_create_page_with_this_name_or_hierarchy": "您無法使用此名稱或頁面層級建立頁面",
   "not_allowed_to_see_this_page": "你不能看到这个页面",
   "Confirm": "确定",
   "Successfully requested": "进程成功接受",
@@ -617,7 +617,7 @@
     "alert_desc1": "在这一页,你可以用复选框选择页面,并通过屏幕上方的批量操作按钮批量转换为新的v5兼容格式。",
     "nopages_title": "恭喜你。准备使用GROWI v5!",
     "nopages_desc1": "现在你能管理的所有页面似乎都是v5兼容的格式。",
-    "detail_info": "请参见 <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>升级GROWI到v5.0.x <span className='growi-custom-icons'>external_link</span></a>.的详细内容。",
+    "detail_info": "请参见 <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>升级GROWI到v5.0.x <span class='growi-custom-icons'>external_link</span></a>.的详细内容。",
     "modal": {
       "title": "转换为新的v5兼容格式",
       "converting_pages": "转换页面",
@@ -662,7 +662,9 @@
     "export_page_markdown": "以Markdown格式导出页面",
     "export_page_pdf": "以PDF格式导出页面",
     "bulk_export": "导出页面及其下的所有页面",
-    "bulk_export_notice": "下载链接准备好后,将发送通知。如果页数较多,则可能需要一段时间准备。",
+    "bulk_export_download_explanation": "导出完成后将发送通知。要下载导出的文件,请单击通知。",
+    "bulk_export_exec_time_warning": "如果页数较多,导出可能需要一段时间",
+    "large_bulk_export_warning": "为了节省系统资源,请避免连续导出大量页面",
     "markdown": "Markdown",
     "choose_export_format": "选择导出格式",
     "bulk_export_started": "目前我们正在准备...",

+ 0 - 48
apps/app/src/client/components/Admin/App/AppSetting.jsx

@@ -170,54 +170,6 @@ const AppSetting = (props) => {
         </div>
       </div>
 
-      <div className="row mb-2">
-        <label className="text-start text-md-end col-md-3 col-form-label"></label>
-        <div className="col-md-6">
-          <div className="form-check form-check-info">
-            <input
-              type="checkbox"
-              id="cbIsPageBulkExportEnabled"
-              className="form-check-input"
-              name="isBulkExportPagesEnabled"
-              checked={adminAppContainer.state.isBulkExportPagesEnabled}
-              disabled={adminAppContainer.state.isFixedIsBulkExportPagesEnabled}
-              onChange={(e) => {
-                adminAppContainer.changeIsPageBulkExportEnabled(e.target.checked);
-              }}
-            />
-            <label
-              className="form-label form-check-label"
-              htmlFor="cbIsPageBulkExportEnabled"
-            >
-              {t('admin:app_setting.enable_page_bulk_export')}
-            </label>
-          </div>
-
-          <p className="form-text text-muted">
-            {t('admin:app_setting.page_bulk_export_explanation')}
-          </p>
-
-          <p className="alert alert-warning mt-2">
-            {t('admin:app_setting.page_bulk_export_warning')}
-          </p>
-
-          {adminAppContainer.state.isFixedIsBulkExportPagesEnabled && (
-            <p className="alert alert-warning mt-2 text-start">
-              <span className="material-symbols-outlined">help</span>
-              <b>FIXED</b><br />
-              {/* eslint-disable-next-line react/no-danger */}
-              <b dangerouslySetInnerHTML={{
-                __html: t('admin:app_setting.fixed_by_env_var', {
-                  envKey: 'BULK_EXPORT_PAGES_ENABLED',
-                  envVar: adminAppContainer.state.isBulkExportPagesEnabled,
-                }),
-              }}
-              />
-            </p>
-          )}
-        </div>
-      </div>
-
       <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
     </React.Fragment>
   );

+ 8 - 0
apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx

@@ -14,6 +14,7 @@ import AppSetting from './AppSetting';
 import FileUploadSetting from './FileUploadSetting';
 import MailSetting from './MailSetting';
 import { MaintenanceMode } from './MaintenanceMode';
+import PageBulkExportSettings from './PageBulkExportSettings';
 import QuestionnaireSettings from './QuestionnaireSettings';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
@@ -108,6 +109,13 @@ const AppSettingsPageContents = (props: Props) => {
         </div>
       </div>
 
+      <div className="row mt-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">{t('admin:app_setting.page_bulk_export_settings')}</h2>
+          <PageBulkExportSettings />
+        </div>
+      </div>
+
       <div className="row mt-5">
         <div className="col-lg-12">
           <h2 className="admin-setting-header">{t('admin:app_setting.questionnaire_settings')}</h2>

+ 136 - 0
apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx

@@ -0,0 +1,136 @@
+import {
+  useState, useCallback, useEffect,
+} from 'react';
+
+import { LoadingSpinner } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
+
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRxAppSettings } from '~/stores/admin/app-settings';
+
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const PageBulkExportSettings = (): JSX.Element => {
+  const { t } = useTranslation(['admin', 'commons']);
+
+  const { data, error, mutate } = useSWRxAppSettings();
+
+  const [isBulkExportPagesEnabled, setIsBulkExportPagesEnabled] = useState(data?.isBulkExportPagesEnabled);
+  const [bulkExportDownloadExpirationSeconds, setBulkExportDownloadExpirationSeconds] = useState(data?.bulkExportDownloadExpirationSeconds);
+
+  const changeBulkExportDownloadExpirationSeconds = (bulkExportDownloadExpirationDays: number) => {
+    const bulkExportDownloadExpirationSeconds = bulkExportDownloadExpirationDays * 24 * 60 * 60;
+    setBulkExportDownloadExpirationSeconds(bulkExportDownloadExpirationSeconds);
+  };
+
+  const onSubmitHandler = useCallback(async() => {
+    try {
+      await apiv3Put('/app-settings/page-bulk-export-settings', {
+        isBulkExportPagesEnabled,
+        bulkExportDownloadExpirationSeconds,
+      });
+      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.questionnaire_settings') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+    mutate();
+  }, [isBulkExportPagesEnabled, bulkExportDownloadExpirationSeconds, mutate, t]);
+
+  useEffect(() => {
+    if (data?.useOnlyEnvVarForFileUploadType) {
+      setIsBulkExportPagesEnabled(data?.envIsBulkExportPagesEnabled);
+    }
+    else {
+      setIsBulkExportPagesEnabled(data?.isBulkExportPagesEnabled);
+    }
+    setBulkExportDownloadExpirationSeconds(data?.bulkExportDownloadExpirationSeconds);
+  }, [data]);
+
+  const isLoading = data === undefined && error === undefined;
+
+  return (
+    <>
+      {isLoading && (
+        <div className="text-muted text-center mb-5">
+          <LoadingSpinner className="me-1 fs-3" />
+        </div>
+      )}
+
+      {!isLoading && (
+        <>
+          <p className="card custom-card bg-warning-subtle my-3">
+            {t('admin:app_setting.page_bulk_export_explanation')} <br />
+            <span className="text-danger mt-1">
+              {t('admin:app_setting.page_bulk_export_warning')}
+            </span>
+          </p>
+
+          <div className="my-4 row">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+            </label>
+
+            <div className="col-md-6">
+              <div className="form-check form-switch form-check-info">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  id="cbIsPageBulkExportEnabled"
+                  checked={isBulkExportPagesEnabled}
+                  disabled={data?.useOnlyEnvVarsForIsBulkExportPagesEnabled}
+                  onChange={e => setIsBulkExportPagesEnabled(e.target.checked)}
+                />
+                <label className="form-label form-check-label" htmlFor="cbIsPageBulkExportEnabled">
+                  {t('app_setting.enable_page_bulk_export')}
+                </label>
+              </div>
+              {data?.useOnlyEnvVarsForIsBulkExportPagesEnabled && (
+                <p className="form-text text-muted">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <b dangerouslySetInnerHTML={{
+                    __html: t('admin:app_setting.fixed_by_env_var', {
+                      envKey: 'BULK_EXPORT_PAGES_ENABLED',
+                      envVar: isBulkExportPagesEnabled,
+                    }),
+                  }}
+                  />
+                </p>
+              )}
+            </div>
+          </div>
+
+          <div className="mb-4">
+            <div className="row">
+              <label
+                className="text-start text-md-end col-md-3 col-form-label"
+              >
+                {t('app_setting.page_bulk_export_storage_period')}
+              </label>
+
+              <div className="col-md-2">
+                <select
+                  className="form-select"
+                  value={(bulkExportDownloadExpirationSeconds ?? 0) / (24 * 60 * 60)}
+                  onChange={(e) => { changeBulkExportDownloadExpirationSeconds(Number(e.target.value)) }}
+                >
+                  {Array.from({ length: 7 }, (_, i) => i + 1).map(number => (
+                    <option key={`be-download-expiration-option-${number}`} value={number}>
+                      {number} {t('admin:days')}
+                    </option>
+                  ))}
+                </select>
+              </div>
+            </div>
+          </div>
+
+          <AdminUpdateButtonRow onClick={onSubmitHandler} />
+        </>
+      )}
+    </>
+  );
+};
+
+export default PageBulkExportSettings;

+ 42 - 28
apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx

@@ -72,37 +72,51 @@ const QuestionnaireSettings = (): JSX.Element => {
 
       {!isLoading && (
         <>
-          <div className="my-4">
-            <div className="form-check form-switch form-check-info">
-              <input
-                type="checkbox"
-                className="form-check-input"
-                id="isQuestionnaireEnabled"
-                checked={isQuestionnaireEnabled}
-                onChange={onChangeIsQuestionnaireEnabledHandler}
-              />
-              <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
-                {t('app_setting.enable_questionnaire')}
-              </label>
+          <div className="my-4 row">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+            </label>
+
+            <div className="col-md-6">
+              <div className="form-check form-switch form-check-info">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  id="isQuestionnaireEnabled"
+                  checked={isQuestionnaireEnabled}
+                  onChange={onChangeIsQuestionnaireEnabledHandler}
+                />
+                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
+                  {t('app_setting.enable_questionnaire')}
+                </label>
+              </div>
             </div>
           </div>
 
-          <div className="my-4">
-            <div className="form-check form-check-info">
-              <input
-                type="checkbox"
-                className="form-check-input"
-                id="isAppSiteUrlHashed"
-                checked={isAppSiteUrlHashed}
-                onChange={onChangeisAppSiteUrlHashedHandler}
-                disabled={!isQuestionnaireEnabled}
-              />
-              <label className="form-label form-check-label" htmlFor="isAppSiteUrlHashed">
-                {t('app_setting.anonymize_app_site_url')}
-              </label>
-              <p className="form-text text-muted small">
-                {t('app_setting.url_anonymization_explanation')}
-              </p>
+          <div className="my-4 row">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+            </label>
+
+            <div className="col-md-6">
+              <div className="form-check form-check-info">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  id="isAppSiteUrlHashed"
+                  checked={isAppSiteUrlHashed}
+                  onChange={onChangeisAppSiteUrlHashedHandler}
+                  disabled={!isQuestionnaireEnabled}
+                />
+                <label className="form-label form-check-label" htmlFor="isAppSiteUrlHashed">
+                  {t('app_setting.anonymize_app_site_url')}
+                </label>
+                <p className="form-text text-muted small">
+                  {t('app_setting.url_anonymization_explanation')}
+                </p>
+              </div>
             </div>
           </div>
 

+ 1 - 1
apps/app/src/client/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -99,7 +99,7 @@ export const useNewPageInput = (): UseNewPageInput => {
       const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
 
       if (!isCreatable) {
-        toastWarning(t('you_can_not_create_page_with_this_name'));
+        toastWarning(t('you_can_not_create_page_with_this_name_or_hierarchy'));
         return;
       }
 

+ 0 - 17
apps/app/src/client/services/AdminAppContainer.js

@@ -23,8 +23,6 @@ export default class AdminAppContainer extends Container {
       globalLang: '',
       isEmailPublishedForNewUser: true,
       fileUpload: '',
-      isBulkExportPagesEnabled: false,
-      isFixedIsBulkExportPagesEnabled: false,
 
       isV5Compatible: null,
       siteUrl: '',
@@ -102,7 +100,6 @@ export default class AdminAppContainer extends Container {
       globalLang: appSettingsParams.globalLang,
       isEmailPublishedForNewUser: appSettingsParams.isEmailPublishedForNewUser,
       fileUpload: appSettingsParams.fileUpload,
-      isBulkExportPagesEnabled: appSettingsParams.isBulkExportPagesEnabled,
       isV5Compatible: appSettingsParams.isV5Compatible,
       siteUrl: appSettingsParams.siteUrl,
       siteUrlUseOnlyEnvVars: appSettingsParams.siteUrlUseOnlyEnvVars,
@@ -160,12 +157,6 @@ export default class AdminAppContainer extends Container {
       this.setState({ fileUploadType: appSettingsParams.envFileUploadType });
       this.setState({ isFixedFileUploadByEnvVar: true });
     }
-
-    if (appSettingsParams.useOnlyEnvVarsForIsBulkExportPagesEnabled) {
-      this.setState({ isBulkExportPagesEnabled: appSettingsParams.envIsBulkExportPagesEnabled });
-      this.setState({ isFixedIsBulkExportPagesEnabled: true });
-    }
-
   }
 
   /**
@@ -203,13 +194,6 @@ export default class AdminAppContainer extends Container {
     this.setState({ fileUpload });
   }
 
-  /**
-   * Change isBulkExportPagesEnabled
-   */
-  changeIsPageBulkExportEnabled(isBulkExportPagesEnabled) {
-    this.setState({ isBulkExportPagesEnabled });
-  }
-
   /**
    * Change site url
    */
@@ -412,7 +396,6 @@ export default class AdminAppContainer extends Container {
       globalLang: this.state.globalLang,
       isEmailPublishedForNewUser: this.state.isEmailPublishedForNewUser,
       fileUpload: this.state.fileUpload,
-      isBulkExportPagesEnabled: this.state.isBulkExportPagesEnabled,
     });
     const { appSettingParams } = response.data;
     return appSettingParams;

+ 2 - 2
apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss

@@ -4,7 +4,7 @@
 
 .grw-page-path-nav-layout :global {
   .grw-page-path-nav-copydropdown {
-    display: none;
+    visibility: hidden;
     @include bs.media-breakpoint-down(md) {
       display: block;
     }
@@ -15,7 +15,7 @@
   &:global {
     &:hover {
       .grw-page-path-nav-copydropdown {
-        display: block;
+        visibility: visible;
       }
     }
   }

+ 8 - 7
apps/app/src/features/openai/server/services/openai.ts

@@ -2,6 +2,7 @@ import assert from 'node:assert';
 import { Readable, Transform } from 'stream';
 import { pipeline } from 'stream/promises';
 
+import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { PageGrant, isPopulated } from '@growi/core';
 import type { HydratedDocument, Types } from 'mongoose';
 import mongoose from 'mongoose';
@@ -20,7 +21,7 @@ import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 
 import { OpenaiServiceTypes } from '../../interfaces/ai';
-import { sanitizeMarkdown } from '../utils/sanitize-markdown';
+import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
 
 import { getClient } from './client-delegator';
 // import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
@@ -157,9 +158,9 @@ class OpenaiService implements IOpenaiService {
   //   }
   // }
 
-  private async uploadFile(pageId: Types.ObjectId, body: string): Promise<OpenAI.Files.FileObject> {
-    const sanitizedMarkdown = await sanitizeMarkdown(body);
-    const file = await toFile(Readable.from(sanitizedMarkdown), `${pageId}.md`);
+  private async uploadFile(pageId: Types.ObjectId, pagePath: string, revisionBody: string): Promise<OpenAI.Files.FileObject> {
+    const convertedHtml = await convertMarkdownToHtml({ pagePath, revisionBody });
+    const file = await toFile(Readable.from(convertedHtml), `${pageId}.html`);
     const uploadedFile = await this.client.uploadFile(file);
     return uploadedFile;
   }
@@ -183,17 +184,17 @@ class OpenaiService implements IOpenaiService {
   async createVectorStoreFile(pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
     const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
     const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
-    const processUploadFile = async(page: PageDocument) => {
+    const processUploadFile = async(page: HydratedDocument<PageDocument>) => {
       if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
         if (isPopulated(page.revision) && page.revision.body.length > 0) {
-          const uploadedFile = await this.uploadFile(page._id, page.revision.body);
+          const uploadedFile = await this.uploadFile(page._id, page.path, page.revision.body);
           prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
           return;
         }
 
         const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
         if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
-          const uploadedFile = await this.uploadFile(page._id, pagePopulatedToShowRevision.revision.body);
+          const uploadedFile = await this.uploadFile(page._id, page.path, pagePopulatedToShowRevision.revision.body);
           prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
         }
       }

+ 89 - 0
apps/app/src/features/openai/server/utils/convert-markdown-to-html.ts

@@ -0,0 +1,89 @@
+import { dynamicImport } from '@cspell/dynamic-import';
+import type { Root, Code } from 'mdast';
+import type * as RehypeMeta from 'rehype-meta';
+import type * as RehypeStringify from 'rehype-stringify';
+import type * as RemarkParse from 'remark-parse';
+import type * as RemarkRehype from 'remark-rehype';
+import type * as Unified from 'unified';
+import type * as UnistUtilVisit from 'unist-util-visit';
+
+interface ModuleCache {
+  unified?: typeof Unified.unified;
+  visit?: typeof UnistUtilVisit.visit;
+  remarkParse?: typeof RemarkParse.default;
+  remarkRehype?: typeof RemarkRehype.default;
+  rehypeMeta?: typeof RehypeMeta.default;
+  rehypeStringify?: typeof RehypeStringify.default;
+}
+
+let moduleCache: ModuleCache = {};
+
+const initializeModules = async(): Promise<void> => {
+  if (moduleCache.unified != null
+    && moduleCache.visit != null
+    && moduleCache.remarkParse != null
+    && moduleCache.remarkRehype != null
+    && moduleCache.rehypeMeta != null
+    && moduleCache.rehypeStringify != null
+  ) {
+    return;
+  }
+
+  const [
+    { unified },
+    { visit },
+    { default: remarkParse },
+    { default: remarkRehype },
+    { default: rehypeMeta },
+    { default: rehypeStringify },
+  ] = await Promise.all([
+    dynamicImport<typeof Unified>('unified', __dirname),
+    dynamicImport<typeof UnistUtilVisit>('unist-util-visit', __dirname),
+    dynamicImport<typeof RemarkParse>('remark-parse', __dirname),
+    dynamicImport<typeof RemarkRehype>('remark-rehype', __dirname),
+    dynamicImport<typeof RehypeMeta>('rehype-meta', __dirname),
+    dynamicImport<typeof RehypeStringify>('rehype-stringify', __dirname),
+  ]);
+
+  moduleCache = {
+    unified,
+    visit,
+    remarkParse,
+    remarkRehype,
+    rehypeMeta,
+    rehypeStringify,
+  };
+};
+
+export const convertMarkdownToHtml = async({ pagePath, revisionBody }: { pagePath: string, revisionBody: string }): Promise<string> => {
+  await initializeModules();
+
+  const {
+    unified, visit, remarkParse, remarkRehype, rehypeMeta, rehypeStringify,
+  } = moduleCache;
+
+  if (unified == null || visit == null || remarkParse == null || remarkRehype == null || rehypeMeta == null || rehypeStringify == null) {
+    throw new Error('Failed to initialize required modules');
+  }
+
+  const sanitizeMarkdown = () => {
+    return (tree: Root) => {
+      visit(tree, 'code', (node: Code) => {
+        if (node.lang === 'drawio') {
+          node.value = '<!-- drawio content replaced -->';
+        }
+      });
+    };
+  };
+
+  const processor = unified()
+    .use(remarkParse)
+    .use(sanitizeMarkdown)
+    .use(remarkRehype)
+    .use(rehypeMeta, {
+      title: pagePath,
+    })
+    .use(rehypeStringify);
+
+  return processor.processSync(revisionBody).toString();
+};

+ 0 - 65
apps/app/src/features/openai/server/utils/sanitize-markdown.ts

@@ -1,65 +0,0 @@
-import { dynamicImport } from '@cspell/dynamic-import';
-import type { Root, Code } from 'mdast';
-import type * as RemarkParse from 'remark-parse';
-import type * as RemarkStringify from 'remark-stringify';
-import type * as Unified from 'unified';
-import type * as UnistUtilVisit from 'unist-util-visit';
-
-interface ModuleCache {
-  remarkParse?: typeof RemarkParse.default;
-  remarkStringify?: typeof RemarkStringify.default;
-  unified?: typeof Unified.unified;
-  visit?: typeof UnistUtilVisit.visit;
-}
-
-let moduleCache: ModuleCache = {};
-
-const initializeModules = async(): Promise<void> => {
-  if (moduleCache.remarkParse != null && moduleCache.remarkStringify != null && moduleCache.unified != null && moduleCache.visit != null) {
-    return;
-  }
-
-  const [{ default: remarkParse }, { default: remarkStringify }, { unified }, { visit }] = await Promise.all([
-    dynamicImport<typeof RemarkParse>('remark-parse', __dirname),
-    dynamicImport<typeof RemarkStringify>('remark-stringify', __dirname),
-    dynamicImport<typeof Unified>('unified', __dirname),
-    dynamicImport<typeof UnistUtilVisit>('unist-util-visit', __dirname),
-  ]);
-
-  moduleCache = {
-    remarkParse,
-    remarkStringify,
-    unified,
-    visit,
-  };
-};
-
-export const sanitizeMarkdown = async(markdown: string): Promise<string> => {
-  await initializeModules();
-
-  const {
-    remarkParse, remarkStringify, unified, visit,
-  } = moduleCache;
-
-
-  if (remarkParse == null || remarkStringify == null || unified == null || visit == null) {
-    throw new Error('Failed to initialize required modules');
-  }
-
-  const sanitize = () => {
-    return (tree: Root) => {
-      visit(tree, 'code', (node: Code) => {
-        if (node.lang === 'drawio') {
-          node.value = '<!-- drawio content replaced -->';
-        }
-      });
-    };
-  };
-
-  const processor = unified()
-    .use(remarkParse)
-    .use(sanitize)
-    .use(remarkStringify);
-
-  return processor.processSync(markdown).toString();
-};

+ 10 - 7
apps/app/src/features/page-bulk-export/client/components/PageBulkExportSelectModal.tsx

@@ -54,17 +54,20 @@ const PageBulkExportSelectModal = (): JSX.Element => {
   return (
     <>
       {status != null && (
-        <Modal isOpen={status.isOpened} toggle={close}>
+        <Modal isOpen={status.isOpened} toggle={close} size="lg">
           <ModalHeader tag="h4" toggle={close}>
             {t('page_export.bulk_export')}
           </ModalHeader>
           <ModalBody>
-            {t('page_export.choose_export_format')}
-            <div className="my-1">
-              <small className="text-muted">
-                {t('page_export.bulk_export_notice')}
-              </small>
-            </div>
+            <p className="card custom-card bg-warning-subtle pt-3 px-3">
+              {t('page_export.bulk_export_download_explanation')}
+              <span className="mt-3"><span className="material-symbols-outlined me-1">warning</span>{t('Warning')}</span>
+              <ul className="mt-2">
+                <li>{t('page_export.bulk_export_exec_time_warning')}</li>
+                <li>{t('page_export.large_bulk_export_warning')}</li>
+              </ul>
+            </p>
+            {t('page_export.choose_export_format')}:
             <div className="d-flex justify-content-center mt-3">
               <button className="btn btn-primary" type="button" onClick={() => startBulkExport(PageBulkExportFormat.md)}>
                 {t('page_export.markdown')}

+ 0 - 4
apps/app/src/features/page-bulk-export/server/service/page-bulk-export.ts

@@ -34,10 +34,6 @@ export interface IPageBulkExportService {
 
 class PageBulkExportService implements IPageBulkExportService {
 
-  // temporal path of local fs to output page files before upload
-  // TODO: If necessary, change to a proper path in https://redmine.weseek.co.jp/issues/149512
-  tmpOutputRootDir = '/tmp/page-bulk-export';
-
   /**
    * Create a new page bulk export job or reset the existing one
    */

+ 3 - 0
apps/app/src/interfaces/activity.ts

@@ -79,6 +79,7 @@ const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
 const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
 const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE = 'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
 const ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE = 'ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE';
+const ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE = 'ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE';
 const ACTION_ADMIN_MAINTENANCEMODE_ENABLED = 'ADMIN_MAINTENANCEMODE_ENABLED';
 const ACTION_ADMIN_MAINTENANCEMODE_DISABLED = 'ADMIN_MAINTENANCEMODE_DISABLED';
 const ACTION_ADMIN_SECURITY_SETTINGS_UPDATE = 'ADMIN_SECURITY_SETTINGS_UPDATE';
@@ -259,6 +260,7 @@ export const SupportedAction = {
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
+  ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
   ACTION_ADMIN_SECURITY_SETTINGS_UPDATE,
@@ -452,6 +454,7 @@ export const LargeActionGroup = {
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
+  ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
   ACTION_ADMIN_SECURITY_SETTINGS_UPDATE,

+ 5 - 0
apps/app/src/interfaces/res/admin/app-settings.ts

@@ -59,4 +59,9 @@ export type IResAppSettings = {
   isAppSiteUrlHashed: boolean,
 
   isMaintenanceMode: boolean,
+
+  isBulkExportPagesEnabled: boolean,
+  envIsBulkExportPagesEnabled: boolean,
+  bulkExportDownloadExpirationSeconds: number,
+  useOnlyEnvVarsForIsBulkExportPagesEnabled: boolean,
 }

+ 35 - 4
apps/app/src/server/routes/apiv3/app-settings.js

@@ -401,6 +401,10 @@ module.exports = (crowi) => {
       body('isQuestionnaireEnabled').isBoolean(),
       body('isAppSiteUrlHashed').isBoolean(),
     ],
+    pageBulkExportSettings: [
+      body('isBulkExportPagesEnabled').isBoolean(),
+      body('bulkExportDownloadExpirationSeconds').isInt(),
+    ],
     maintenanceMode: [
       body('flag').isBoolean(),
     ],
@@ -435,8 +439,6 @@ module.exports = (crowi) => {
       globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
       isEmailPublishedForNewUser: crowi.configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser'),
       fileUpload: crowi.configManager.getConfig('crowi', 'app:fileUpload'),
-      isBulkExportPagesEnabled: crowi.configManager.getConfig('crowi', 'app:isBulkExportPagesEnabled'),
-      envIsBulkExportPagesEnabled: crowi.configManager.getConfigFromEnvVars('crowi', 'app:isBulkExportPagesEnabled'),
       useOnlyEnvVarsForIsBulkExportPagesEnabled: crowi.configManager.getConfig('crowi', 'env:useOnlyEnvVars:app:isBulkExportPagesEnabled'),
       isV5Compatible: crowi.configManager.getConfig('crowi', 'app:isV5Compatible'),
       siteUrl: crowi.configManager.getConfig('crowi', 'app:siteUrl'),
@@ -493,6 +495,10 @@ module.exports = (crowi) => {
       isAppSiteUrlHashed: crowi.configManager.getConfig('crowi', 'questionnaire:isAppSiteUrlHashed'),
 
       isMaintenanceMode: crowi.configManager.getConfig('crowi', 'app:isMaintenanceMode'),
+
+      isBulkExportPagesEnabled: crowi.configManager.getConfig('crowi', 'app:isBulkExportPagesEnabled'),
+      envIsBulkExportPagesEnabled: crowi.configManager.getConfigFromEnvVars('crowi', 'app:isBulkExportPagesEnabled'),
+      bulkExportDownloadExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:bulkExportDownloadExpirationSeconds'),
     };
     return res.apiv3({ appSettingsParams });
 
@@ -534,7 +540,6 @@ module.exports = (crowi) => {
       'app:globalLang': req.body.globalLang,
       'customize:isEmailPublishedForNewUser': req.body.isEmailPublishedForNewUser,
       'app:fileUpload': req.body.fileUpload,
-      'app:isBulkExportPagesEnabled': req.body.isBulkExportPagesEnabled,
     };
 
     try {
@@ -545,7 +550,6 @@ module.exports = (crowi) => {
         globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
         isEmailPublishedForNewUser: crowi.configManager.getConfig('crowi', 'customize:isEmailPublishedForNewUser'),
         fileUpload: crowi.configManager.getConfig('crowi', 'app:fileUpload'),
-        isBulkExportPagesEnabled: crowi.configManager.getConfig('crowi', 'app:isBulkExportPagesEnabled'),
       };
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE };
@@ -1023,6 +1027,33 @@ module.exports = (crowi) => {
 
   });
 
+  router.put('/page-bulk-export-settings', loginRequiredStrictly, adminRequired, addActivity, validator.pageBulkExportSettings, apiV3FormValidator,
+    async(req, res) => {
+      const requestParams = {
+        'app:isBulkExportPagesEnabled': req.body.isBulkExportPagesEnabled,
+        'app:bulkExportDownloadExpirationSeconds': req.body.bulkExportDownloadExpirationSeconds,
+      };
+
+      try {
+        await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams, true);
+        const responseParams = {
+          isBulkExportPagesEnabled: crowi.configManager.getConfig('crowi', 'app:isBulkExportPagesEnabled'),
+          bulkExportDownloadExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:bulkExportDownloadExpirationSeconds'),
+        };
+
+        const parameters = { action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+
+        return res.apiv3({ responseParams });
+      }
+      catch (err) {
+        const msg = 'Error occurred in updating page bulk export settings';
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'update-page-bulk-export-settings-failed'));
+      }
+
+    });
+
   /**
    * @swagger
    *

+ 1 - 1
apps/app/src/server/service/config-loader.ts

@@ -761,7 +761,7 @@ const ENV_VAR_NAME_TO_CONFIG_INFO: Record<string, EnvConfig> = {
     ns: 'crowi',
     key: 'app:bulkExportDownloadExpirationSeconds',
     type: ValueType.NUMBER,
-    default: 86400, // 1 day
+    default: 259200, // 3 days
   },
   BULK_EXPORT_JOB_CRON_SCHEDULE: {
     ns: 'crowi',

+ 2 - 1
packages/core/src/utils/page-path-utils/index.ts

@@ -1,6 +1,6 @@
 import escapeStringRegexp from 'escape-string-regexp';
 
-import { IUser } from '~/interfaces';
+import type { IUser } from '~/interfaces';
 
 import { isValidObjectId } from '../objectid-utils';
 import { addTrailingSlash } from '../path-utils';
@@ -117,6 +117,7 @@ const restrictedPatternsToCreate: Array<RegExp> = [
   /^\/(_search|_private-legacy-pages)(\/.*|$)/,
   /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments|tags|share|attachment)(\/.*|$)/,
   /^\/user(?:\/[^/]+)?$/, // https://regex101.com/r/9Eh2S1/1
+  /^(\/.+){130,}$/, // avoid deep layer path. see: https://regex101.com/r/L0kzOD/1
 ];
 export const isCreatablePage = (path: string): boolean => {
   return !restrictedPatternsToCreate.some(pattern => path.match(pattern));

+ 62 - 0
packages/pdf-converter-client/dist/index.d.ts

@@ -0,0 +1,62 @@
+import type { AxiosRequestConfig, AxiosResponse } from 'axios';
+export type PdfCtrlSyncJobStatus202Status = typeof PdfCtrlSyncJobStatus202Status[keyof typeof PdfCtrlSyncJobStatus202Status];
+export declare const PdfCtrlSyncJobStatus202Status: {
+    readonly HTML_EXPORT_IN_PROGRESS: "HTML_EXPORT_IN_PROGRESS";
+    readonly HTML_EXPORT_DONE: "HTML_EXPORT_DONE";
+    readonly FAILED: "FAILED";
+    readonly PDF_EXPORT_DONE: "PDF_EXPORT_DONE";
+};
+export type PdfCtrlSyncJobStatus202 = {
+    status: PdfCtrlSyncJobStatus202Status;
+};
+export type PdfCtrlSyncJobStatusBodyStatus = typeof PdfCtrlSyncJobStatusBodyStatus[keyof typeof PdfCtrlSyncJobStatusBodyStatus];
+export declare const PdfCtrlSyncJobStatusBodyStatus: {
+    readonly HTML_EXPORT_IN_PROGRESS: "HTML_EXPORT_IN_PROGRESS";
+    readonly HTML_EXPORT_DONE: "HTML_EXPORT_DONE";
+    readonly FAILED: "FAILED";
+};
+export type PdfCtrlSyncJobStatusBody = {
+    expirationDate?: string;
+    jobId?: string;
+    status?: PdfCtrlSyncJobStatusBodyStatus;
+};
+export interface GenericError {
+    /**
+     * An error message
+     * @minLength 1
+     */
+    message: string;
+    /**
+     * The error name
+     * @minLength 1
+     */
+    name: string;
+    [key: string]: unknown;
+}
+export interface InternalServerError {
+    /** A list of related errors */
+    errors?: GenericError[];
+    /**
+     * An error message
+     * @minLength 1
+     */
+    message: string;
+    /**
+     * The error name
+     * @minLength 1
+     */
+    name: string;
+    /** The stack trace (only in development mode) */
+    stack?: string;
+    /** The status code of the exception */
+    status: number;
+}
+/**
+*
+  Sync job pdf convert status with GROWI.
+  Register or update job inside pdf-converter with given jobId, expirationDate, and status.
+  Return resulting status of job to GROWI.
+
+*/
+export declare const pdfCtrlSyncJobStatus: <TData = AxiosResponse<PdfCtrlSyncJobStatus202, any>>(pdfCtrlSyncJobStatusBody?: PdfCtrlSyncJobStatusBody, options?: AxiosRequestConfig) => Promise<TData>;
+export type PdfCtrlSyncJobStatusResult = AxiosResponse<PdfCtrlSyncJobStatus202>;

+ 31 - 0
packages/pdf-converter-client/dist/index.js

@@ -0,0 +1,31 @@
+/**
+ * Generated by orval v7.2.0 🍺
+ * Do not edit manually.
+ * Api documentation
+ * OpenAPI spec version: 1.0.0
+ */
+import axios from 'axios';
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const PdfCtrlSyncJobStatus202Status = {
+    HTML_EXPORT_IN_PROGRESS: 'HTML_EXPORT_IN_PROGRESS',
+    HTML_EXPORT_DONE: 'HTML_EXPORT_DONE',
+    FAILED: 'FAILED',
+    PDF_EXPORT_DONE: 'PDF_EXPORT_DONE',
+};
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const PdfCtrlSyncJobStatusBodyStatus = {
+    HTML_EXPORT_IN_PROGRESS: 'HTML_EXPORT_IN_PROGRESS',
+    HTML_EXPORT_DONE: 'HTML_EXPORT_DONE',
+    FAILED: 'FAILED',
+};
+/**
+*
+  Sync job pdf convert status with GROWI.
+  Register or update job inside pdf-converter with given jobId, expirationDate, and status.
+  Return resulting status of job to GROWI.
+
+*/
+export const pdfCtrlSyncJobStatus = (pdfCtrlSyncJobStatusBody, options) => {
+    return axios.post(`/pdf/sync-job`, pdfCtrlSyncJobStatusBody, options);
+};
+//# sourceMappingURL=index.js.map

+ 1 - 0
packages/pdf-converter-client/dist/index.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AAQzB,2DAA2D;AAC3D,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,uBAAuB,EAAE,yBAAyB;IAClD,gBAAgB,EAAE,kBAAkB;IACpC,MAAM,EAAE,QAAQ;IAChB,eAAe,EAAE,iBAAiB;CAC1B,CAAC;AASX,2DAA2D;AAC3D,MAAM,CAAC,MAAM,8BAA8B,GAAG;IAC5C,uBAAuB,EAAE,yBAAyB;IAClD,gBAAgB,EAAE,kBAAkB;IACpC,MAAM,EAAE,QAAQ;CACR,CAAC;AA6CT;;;;;;EAMC;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAChC,wBAAmD,EAAE,OAA4B,EAClE,EAAE;IACjB,OAAO,KAAK,CAAC,IAAI,CACf,eAAe,EACf,wBAAwB,EAAC,OAAO,CACjC,CAAC;AACJ,CAAC,CAAA"}

+ 2 - 2
packages/remark-lsx/src/server/index.ts

@@ -14,8 +14,8 @@ const filterXSS = new FilterXSS();
 
 const lsxValidator = [
   query('pagePath').notEmpty().isString(),
-  query('offset').optional().isInt(),
-  query('limit').optional().isInt(),
+  query('offset').optional().isInt().toInt(),
+  query('limit').optional().isInt().toInt(),
   query('options')
     .optional()
     .customSanitizer((options) => {

+ 200 - 16
pnpm-lock.yaml

@@ -74,7 +74,7 @@ importers:
         version: 8.41.0
       eslint-config-next:
         specifier: ^12.1.6
-        version: 12.1.6(eslint@8.41.0)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(typescript@5.0.4)
+        version: 12.1.6(eslint@8.41.0)(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(typescript@5.0.4)
       eslint-config-weseek:
         specifier: ^2.1.1
         version: 2.1.1(@babel/core@7.24.6)(@babel/eslint-parser@7.24.7(@babel/core@7.24.6)(eslint@8.41.0))(@typescript-eslint/eslint-plugin@5.59.7(@typescript-eslint/parser@5.59.7(eslint@8.41.0)(typescript@5.0.4))(eslint@8.41.0)(typescript@5.0.4))(@typescript-eslint/parser@5.59.7(eslint@8.41.0)(typescript@5.0.4))(eslint-import-resolver-typescript@3.2.5)(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.5.1(eslint@8.41.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.41.0))(eslint-plugin-react@7.30.1(eslint@8.41.0))(eslint-plugin-vue@7.20.0(eslint@8.41.0))(eslint@8.41.0)
@@ -484,20 +484,20 @@ importers:
         specifier: ^4.2.0
         version: 4.2.0
       next:
-        specifier: ^14.2.13
-        version: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+        specifier: ^14.2.15
+        version: 14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
       next-dynamic-loading-props:
         specifier: ^0.1.1
         version: 0.1.1(react@18.2.0)
       next-i18next:
         specifier: ^15.3.1
-        version: 15.3.1(i18next@23.16.5)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
+        version: 15.3.1(i18next@23.16.5)(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
       next-superjson:
         specifier: ^1.0.7
         version: 1.0.7(@swc/helpers@0.5.15)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)
       next-themes:
         specifier: ^0.2.1
-        version: 0.2.1(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+        version: 0.2.1(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
       nocache:
         specifier: ^4.0.0
         version: 4.0.0
@@ -603,6 +603,9 @@ importers:
       rehype-katex:
         specifier: ^7.0.1
         version: 7.0.1
+      rehype-meta:
+        specifier: ^4.0.1
+        version: 4.0.1
       rehype-raw:
         specifier: ^7.0.0
         version: 7.0.0
@@ -612,6 +615,9 @@ importers:
       rehype-slug:
         specifier: ^6.0.0
         version: 6.0.0
+      rehype-stringify:
+        specifier: ^10.0.1
+        version: 10.0.1
       rehype-toc:
         specifier: ^3.0.2
         version: 3.0.2
@@ -3281,6 +3287,9 @@ packages:
   '@next/env@14.2.13':
     resolution: {integrity: sha512-s3lh6K8cbW1h5Nga7NNeXrbe0+2jIIYK9YaA9T7IufDWnZpozdFUp6Hf0d5rNWUKu4fEuSX2rCKlGjCrtylfDw==}
 
+  '@next/env@14.2.15':
+    resolution: {integrity: sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==}
+
   '@next/eslint-plugin-next@12.1.6':
     resolution: {integrity: sha512-yNUtJ90NEiYFT6TJnNyofKMPYqirKDwpahcbxBgSIuABwYOdkGwzos1ZkYD51Qf0diYwpQZBeVqElTk7Q2WNqw==}
 
@@ -3290,54 +3299,108 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
+  '@next/swc-darwin-arm64@14.2.15':
+    resolution: {integrity: sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [darwin]
+
   '@next/swc-darwin-x64@14.2.13':
     resolution: {integrity: sha512-Dv1RBGs2TTjkwEnFMVL5XIfJEavnLqqwYSD6LXgTPdEy/u6FlSrLBSSfe1pcfqhFEXRAgVL3Wpjibe5wXJzWog==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [darwin]
 
+  '@next/swc-darwin-x64@14.2.15':
+    resolution: {integrity: sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [darwin]
+
   '@next/swc-linux-arm64-gnu@14.2.13':
     resolution: {integrity: sha512-yB1tYEFFqo4ZNWkwrJultbsw7NPAAxlPXURXioRl9SdW6aIefOLS+0TEsKrWBtbJ9moTDgU3HRILL6QBQnMevg==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [linux]
 
+  '@next/swc-linux-arm64-gnu@14.2.15':
+    resolution: {integrity: sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
   '@next/swc-linux-arm64-musl@14.2.13':
     resolution: {integrity: sha512-v5jZ/FV/eHGoWhMKYrsAweQ7CWb8xsWGM/8m1mwwZQ/sutJjoFaXchwK4pX8NqwImILEvQmZWyb8pPTcP7htWg==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [linux]
 
+  '@next/swc-linux-arm64-musl@14.2.15':
+    resolution: {integrity: sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
   '@next/swc-linux-x64-gnu@14.2.13':
     resolution: {integrity: sha512-aVc7m4YL7ViiRv7SOXK3RplXzOEe/qQzRA5R2vpXboHABs3w8vtFslGTz+5tKiQzWUmTmBNVW0UQdhkKRORmGA==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [linux]
 
+  '@next/swc-linux-x64-gnu@14.2.15':
+    resolution: {integrity: sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
   '@next/swc-linux-x64-musl@14.2.13':
     resolution: {integrity: sha512-4wWY7/OsSaJOOKvMsu1Teylku7vKyTuocvDLTZQq0TYv9OjiYYWt63PiE1nTuZnqQ4RPvME7Xai+9enoiN0Wrg==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [linux]
 
+  '@next/swc-linux-x64-musl@14.2.15':
+    resolution: {integrity: sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
   '@next/swc-win32-arm64-msvc@14.2.13':
     resolution: {integrity: sha512-uP1XkqCqV2NVH9+g2sC7qIw+w2tRbcMiXFEbMihkQ8B1+V6m28sshBwAB0SDmOe0u44ne1vFU66+gx/28RsBVQ==}
     engines: {node: '>= 10'}
     cpu: [arm64]
     os: [win32]
 
+  '@next/swc-win32-arm64-msvc@14.2.15':
+    resolution: {integrity: sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [win32]
+
   '@next/swc-win32-ia32-msvc@14.2.13':
     resolution: {integrity: sha512-V26ezyjPqQpDBV4lcWIh8B/QICQ4v+M5Bo9ykLN+sqeKKBxJVDpEc6biDVyluTXTC40f5IqCU0ttth7Es2ZuMw==}
     engines: {node: '>= 10'}
     cpu: [ia32]
     os: [win32]
 
+  '@next/swc-win32-ia32-msvc@14.2.15':
+    resolution: {integrity: sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==}
+    engines: {node: '>= 10'}
+    cpu: [ia32]
+    os: [win32]
+
   '@next/swc-win32-x64-msvc@14.2.13':
     resolution: {integrity: sha512-WwzOEAFBGhlDHE5Z73mNU8CO8mqMNLqaG+AO9ETmzdCQlJhVtWZnOl2+rqgVQS+YHunjOWptdFmNfbpwcUuEsw==}
     engines: {node: '>= 10'}
     cpu: [x64]
     os: [win32]
 
+  '@next/swc-win32-x64-msvc@14.2.15':
+    resolution: {integrity: sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [win32]
+
   '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
     resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
 
@@ -5323,10 +5386,12 @@ packages:
   abstract-leveldown@6.2.3:
     resolution: {integrity: sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==}
     engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
 
   abstract-leveldown@6.3.0:
     resolution: {integrity: sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==}
     engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
 
   abstract-logging@2.0.1:
     resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
@@ -7203,6 +7268,7 @@ packages:
   deferred-leveldown@5.3.0:
     resolution: {integrity: sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==}
     engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
 
   define-data-property@1.1.4:
     resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
@@ -8513,6 +8579,9 @@ packages:
   hast-util-from-parse5@8.0.1:
     resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==}
 
+  hast-util-from-selector@3.0.1:
+    resolution: {integrity: sha512-CA2dwcsAS6a7DNZq8HT5fNP4FzUq2PUpQpKnAtOCmfTk429jR0RtasLSMlFA1FNKd8lgfeCIAFl3/vD95be8Lg==}
+
   hast-util-has-property@3.0.0:
     resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==}
 
@@ -8561,6 +8630,9 @@ packages:
   hastscript@8.0.0:
     resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==}
 
+  hastscript@9.0.0:
+    resolution: {integrity: sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==}
+
   he@1.2.0:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
@@ -9586,6 +9658,7 @@ packages:
 
   level-js@5.0.2:
     resolution: {integrity: sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==}
+    deprecated: Superseded by browser-level (https://github.com/Level/community#faq)
 
   level-packager@5.1.1:
     resolution: {integrity: sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==}
@@ -9602,10 +9675,12 @@ packages:
   leveldown@5.6.0:
     resolution: {integrity: sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==}
     engines: {node: '>=8.6.0'}
+    deprecated: Superseded by classic-level (https://github.com/Level/community#faq)
 
   levelup@4.4.0:
     resolution: {integrity: sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==}
     engines: {node: '>=6'}
+    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
 
   leven@3.1.0:
     resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@@ -10561,6 +10636,24 @@ packages:
       sass:
         optional: true
 
+  next@14.2.15:
+    resolution: {integrity: sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==}
+    engines: {node: '>=18.17.0'}
+    hasBin: true
+    peerDependencies:
+      '@opentelemetry/api': ^1.1.0
+      '@playwright/test': ^1.41.2
+      react: ^18.2.0
+      react-dom: ^18.2.0
+      sass: ^1.3.0
+    peerDependenciesMeta:
+      '@opentelemetry/api':
+        optional: true
+      '@playwright/test':
+        optional: true
+      sass:
+        optional: true
+
   nice-try@1.0.4:
     resolution: {integrity: sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==}
 
@@ -11821,6 +11914,9 @@ packages:
   rehype-katex@7.0.1:
     resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
 
+  rehype-meta@4.0.1:
+    resolution: {integrity: sha512-nLwA17+GbtBYi3C1KSrFR8JlqXv76mz185U//xDEAYgzE3g/bSD6WKSXva1W95ttzouUCJwA09X3AQZIi3R+Nw==}
+
   rehype-raw@7.0.0:
     resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
 
@@ -11834,6 +11930,9 @@ packages:
   rehype-slug@6.0.0:
     resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
 
+  rehype-stringify@10.0.1:
+    resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
+
   rehype-toc@3.0.2:
     resolution: {integrity: sha512-DMt376+4i1KJGgHJL7Ezd65qKkJ7Eqp6JSB47BJ90ReBrohI9ufrornArM6f4oJjP2E2DVZZHufWucv/9t7GUQ==}
     engines: {node: '>=10'}
@@ -16752,6 +16851,8 @@ snapshots:
 
   '@next/env@14.2.13': {}
 
+  '@next/env@14.2.15': {}
+
   '@next/eslint-plugin-next@12.1.6':
     dependencies:
       glob: 7.1.7
@@ -16759,30 +16860,57 @@ snapshots:
   '@next/swc-darwin-arm64@14.2.13':
     optional: true
 
+  '@next/swc-darwin-arm64@14.2.15':
+    optional: true
+
   '@next/swc-darwin-x64@14.2.13':
     optional: true
 
+  '@next/swc-darwin-x64@14.2.15':
+    optional: true
+
   '@next/swc-linux-arm64-gnu@14.2.13':
     optional: true
 
+  '@next/swc-linux-arm64-gnu@14.2.15':
+    optional: true
+
   '@next/swc-linux-arm64-musl@14.2.13':
     optional: true
 
+  '@next/swc-linux-arm64-musl@14.2.15':
+    optional: true
+
   '@next/swc-linux-x64-gnu@14.2.13':
     optional: true
 
+  '@next/swc-linux-x64-gnu@14.2.15':
+    optional: true
+
   '@next/swc-linux-x64-musl@14.2.13':
     optional: true
 
+  '@next/swc-linux-x64-musl@14.2.15':
+    optional: true
+
   '@next/swc-win32-arm64-msvc@14.2.13':
     optional: true
 
+  '@next/swc-win32-arm64-msvc@14.2.15':
+    optional: true
+
   '@next/swc-win32-ia32-msvc@14.2.13':
     optional: true
 
+  '@next/swc-win32-ia32-msvc@14.2.15':
+    optional: true
+
   '@next/swc-win32-x64-msvc@14.2.13':
     optional: true
 
+  '@next/swc-win32-x64-msvc@14.2.15':
+    optional: true
+
   '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
     dependencies:
       eslint-scope: 5.1.1
@@ -21932,7 +22060,7 @@ snapshots:
       object.assign: 4.1.5
       object.entries: 1.1.5
 
-  eslint-config-next@12.1.6(eslint@8.41.0)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(typescript@5.0.4):
+  eslint-config-next@12.1.6(eslint@8.41.0)(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(typescript@5.0.4):
     dependencies:
       '@next/eslint-plugin-next': 12.1.6
       '@rushstack/eslint-patch': 1.1.3
@@ -21944,7 +22072,7 @@ snapshots:
       eslint-plugin-jsx-a11y: 6.5.1(eslint@8.41.0)
       eslint-plugin-react: 7.30.1(eslint@8.41.0)
       eslint-plugin-react-hooks: 4.6.0(eslint@8.41.0)
-      next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+      next: 14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
     optionalDependencies:
       typescript: 5.0.4
     transitivePeerDependencies:
@@ -23081,6 +23209,13 @@ snapshots:
       vfile-location: 5.0.3
       web-namespaces: 2.0.1
 
+  hast-util-from-selector@3.0.1:
+    dependencies:
+      '@types/hast': 3.0.4
+      css-selector-parser: 3.0.5
+      devlop: 1.1.0
+      hastscript: 9.0.0
+
   hast-util-has-property@3.0.0:
     dependencies:
       '@types/hast': 3.0.4
@@ -23182,7 +23317,7 @@ snapshots:
       property-information: 6.1.1
       space-separated-tokens: 2.0.1
       web-namespaces: 2.0.1
-      zwitch: 2.0.2
+      zwitch: 2.0.4
 
   hast-util-to-string@3.0.1:
     dependencies:
@@ -23215,6 +23350,14 @@ snapshots:
       property-information: 6.1.1
       space-separated-tokens: 2.0.1
 
+  hastscript@9.0.0:
+    dependencies:
+      '@types/hast': 3.0.4
+      comma-separated-tokens: 2.0.2
+      hast-util-parse-selector: 4.0.0
+      property-information: 6.1.1
+      space-separated-tokens: 2.0.1
+
   he@1.2.0: {}
 
   header-case@2.0.4:
@@ -25645,7 +25788,7 @@ snapshots:
     dependencies:
       react: 18.2.0
 
-  next-i18next@15.3.1(i18next@23.16.5)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0):
+  next-i18next@15.3.1(i18next@23.16.5)(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0):
     dependencies:
       '@babel/runtime': 7.25.4
       '@types/hoist-non-react-statics': 3.3.5
@@ -25653,29 +25796,29 @@ snapshots:
       hoist-non-react-statics: 3.3.2
       i18next: 23.16.5
       i18next-fs-backend: 2.3.2
-      next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+      next: 14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
       react: 18.2.0
       react-i18next: 15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
 
-  next-superjson-plugin@0.6.3(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3):
+  next-superjson-plugin@0.6.3(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3):
     dependencies:
       hoist-non-react-statics: 3.3.2
-      next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+      next: 14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
       superjson: 1.13.3
 
   next-superjson@1.0.7(@swc/helpers@0.5.15)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3):
     dependencies:
       '@swc/core': 1.4.17(@swc/helpers@0.5.15)
       '@swc/types': 0.1.12
-      next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
-      next-superjson-plugin: 0.6.3(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)
+      next: 14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+      next-superjson-plugin: 0.6.3(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)
     transitivePeerDependencies:
       - '@swc/helpers'
       - superjson
 
-  next-themes@0.2.1(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+  next-themes@0.2.1(next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
     dependencies:
-      next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+      next: 14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
 
@@ -25706,6 +25849,33 @@ snapshots:
       - '@babel/core'
       - babel-plugin-macros
 
+  next@14.2.15(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6):
+    dependencies:
+      '@next/env': 14.2.15
+      '@swc/helpers': 0.5.5
+      busboy: 1.6.0
+      caniuse-lite: 1.0.30001680
+      graceful-fs: 4.2.11
+      postcss: 8.4.31
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      styled-jsx: 5.1.1(@babel/core@7.24.6)(react@18.2.0)
+    optionalDependencies:
+      '@next/swc-darwin-arm64': 14.2.15
+      '@next/swc-darwin-x64': 14.2.15
+      '@next/swc-linux-arm64-gnu': 14.2.15
+      '@next/swc-linux-arm64-musl': 14.2.15
+      '@next/swc-linux-x64-gnu': 14.2.15
+      '@next/swc-linux-x64-musl': 14.2.15
+      '@next/swc-win32-arm64-msvc': 14.2.15
+      '@next/swc-win32-ia32-msvc': 14.2.15
+      '@next/swc-win32-x64-msvc': 14.2.15
+      '@playwright/test': 1.46.0
+      sass: 1.77.6
+    transitivePeerDependencies:
+      - '@babel/core'
+      - babel-plugin-macros
+
   nice-try@1.0.4: {}
 
   nimma@0.2.2:
@@ -27193,6 +27363,14 @@ snapshots:
       unist-util-visit-parents: 6.0.1
       vfile: 6.0.3
 
+  rehype-meta@4.0.1:
+    dependencies:
+      '@types/hast': 3.0.4
+      hast-util-from-selector: 3.0.1
+      hast-util-select: 6.0.2
+      hastscript: 9.0.0
+      vfile: 6.0.3
+
   rehype-raw@7.0.0:
     dependencies:
       '@types/hast': 3.0.4
@@ -27218,6 +27396,12 @@ snapshots:
       hast-util-to-string: 3.0.1
       unist-util-visit: 5.0.0
 
+  rehype-stringify@10.0.1:
+    dependencies:
+      '@types/hast': 3.0.4
+      hast-util-to-html: 9.0.4
+      unified: 11.0.5
+
   rehype-toc@3.0.2:
     dependencies:
       '@jsdevtools/rehype-toc': 3.0.2