From da2023b42bbe62e8ef645e7721b88297d8b9ba9f Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Mon, 23 Mar 2026 17:09:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(artifacts):=20=E6=96=B0=E5=A2=9E=20Markdow?= =?UTF-8?q?n=20=E5=AF=BC=E5=87=BA=20DOCX/PDF=20=E5=8A=9F=E8=83=BD=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20document-converter.ts=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=8C=E6=94=AF=E6=8C=81=20Markdown=20?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E4=B8=BA=20DOCX=EF=BC=88docx=20=E5=BA=93?= =?UTF-8?q?=EF=BC=89=E5=92=8C=20PDF=EF=BC=88html2pdf.js=EF=BC=89=20-=20?= =?UTF-8?q?=E5=9C=A8=20artifact-file-detail=20=E6=B7=BB=E5=8A=A0=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E8=8F=9C=E5=8D=95=E9=80=89=E9=A1=B9=EF=BC=88downloadA?= =?UTF-8?q?sDocx=E3=80=81downloadAsPdf=EF=BC=89=20-=20.gitignore=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20docs=20=E7=9B=AE=E5=BD=95=E5=BF=BD?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.gitignore | 2 + frontend/package.json | 3 + frontend/pnpm-lock.yaml | 365 ++++++++++++-- .../src/components/ai-elements/artifact.tsx | 3 + .../artifacts/artifact-file-detail.tsx | 156 ++++-- frontend/src/core/i18n/locales/en-US.ts | 3 + frontend/src/core/i18n/locales/types.ts | 3 + frontend/src/core/i18n/locales/zh-CN.ts | 3 + .../core/utils/markdown-download/converter.ts | 464 ++++++++++++++++++ .../src/core/utils/markdown-download/index.ts | 47 ++ .../use-markdown-download.ts | 127 +++++ 11 files changed, 1072 insertions(+), 104 deletions(-) create mode 100644 frontend/src/core/utils/markdown-download/converter.ts create mode 100644 frontend/src/core/utils/markdown-download/index.ts create mode 100644 frontend/src/core/utils/markdown-download/use-markdown-download.ts diff --git a/frontend/.gitignore b/frontend/.gitignore index c24a8359..1a0009e8 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -21,6 +21,8 @@ next-env.d.ts # production /build +docs + # misc .DS_Store *.pem diff --git a/frontend/package.json b/frontend/package.json index f12500fd..d1f35acb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,12 +59,15 @@ "cmdk": "^1.1.1", "codemirror": "^6.0.2", "date-fns": "^4.1.0", + "docx": "^9.6.1", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", "gsap": "^3.13.0", "hast": "^1.0.0", + "html2pdf.js": "^0.14.0", "katex": "^0.16.28", "lucide-react": "^0.562.0", + "marked": "^17.0.5", "motion": "^12.26.2", "nanoid": "^5.1.6", "next": "^16.1.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 4de85325..b6011dbb 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + docx: + specifier: ^9.6.1 + version: 9.6.1 dotenv: specifier: ^17.2.3 version: 17.2.4 @@ -146,12 +149,18 @@ importers: hast: specifier: ^1.0.0 version: 1.0.0 + html2pdf.js: + specifier: ^0.14.0 + version: 0.14.0 katex: specifier: ^0.16.28 version: 0.16.28 lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.4) + marked: + specifier: ^17.0.5 + version: 17.0.5 motion: specifier: ^12.26.2 version: 12.34.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -716,105 +725,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -956,28 +949,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.1.6': resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} @@ -1571,28 +1560,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-arm64-musl@2.6.2': resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@resvg/resvg-js-linux-x64-gnu@2.6.2': resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@resvg/resvg-js-linux-x64-musl@2.6.2': resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@resvg/resvg-js-win32-arm64-msvc@2.6.2': resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} @@ -1654,79 +1639,66 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -1869,28 +1841,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -2076,6 +2044,15 @@ packages: '@types/node@20.19.33': resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2253,49 +2230,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -2458,6 +2427,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@0.0.8: resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} engines: {node: '>= 0.4'} @@ -2589,6 +2562,10 @@ packages: canvas-confetti@1.9.4: resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2690,6 +2667,12 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -2720,6 +2703,9 @@ packages: resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==} engines: {node: '>=16'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-to-react-native@3.2.0: resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} @@ -2965,6 +2951,10 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + docx@9.6.1: + resolution: {integrity: sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==} + engines: {node: '>=10'} + dompurify@3.3.1: resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} @@ -3221,6 +3211,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3236,6 +3229,9 @@ packages: fflate@0.7.4: resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -3382,6 +3378,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3442,6 +3441,13 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + + html2pdf.js@0.14.0: + resolution: {integrity: sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -3467,6 +3473,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3475,6 +3484,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -3489,6 +3501,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -3636,6 +3651,9 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3682,10 +3700,16 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + katex@0.16.28: resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==} hasBin: true @@ -3745,6 +3769,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lighthouse-logger@2.0.2: resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} @@ -3783,28 +3810,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -3870,6 +3893,11 @@ packages: engines: {node: '>= 20'} hasBin: true + marked@17.0.5: + resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==} + engines: {node: '>= 20'} + hasBin: true + marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} @@ -4030,6 +4058,9 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4259,6 +4290,12 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4300,6 +4337,9 @@ packages: perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4417,6 +4457,9 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4433,6 +4476,9 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -4493,6 +4539,9 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -4501,6 +4550,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -4558,6 +4610,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -4582,6 +4638,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -4600,6 +4659,10 @@ packages: resolution: {integrity: sha512-HanEzgXHlX3fzpGgxPoR3qI7FDpc/B+uE/KplzA6BkZGlWMaH98B/1Amq+OBF1pYPlGNzAXPYNHlrEVBvRBnHQ==} engines: {node: '>=16'} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4630,6 +4693,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -4693,6 +4759,10 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -4731,6 +4801,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -4786,6 +4859,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -4796,6 +4873,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -4894,6 +4974,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + unhead@2.1.4: resolution: {integrity: sha512-+5091sJqtNNmgfQ07zJOgUnMIMKzVKAWjeMlSrTdSGPB6JSozhpjUKuMfWEoLxlMAfhIvgOU8Me0XJvmMA/0fA==} @@ -5037,6 +5120,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -5160,6 +5249,13 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6990,6 +7086,15 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + + '@types/pako@2.0.4': {} + + '@types/raf@3.4.3': + optional: true + '@types/react-dom@19.2.3(@types/react@19.2.13)': dependencies: '@types/react': 19.2.13 @@ -7439,6 +7544,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + base64-js@0.0.8: {} base64-js@1.5.1: {} @@ -7531,6 +7638,18 @@ snapshots: canvas-confetti@1.9.4: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.6 + '@types/raf': 3.4.3 + core-js: 3.49.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + ccount@2.0.1: {} chalk@4.1.2: @@ -7637,6 +7756,11 @@ snapshots: cookie-es@1.2.2: {} + core-js@3.49.0: + optional: true + + core-util-is@1.0.3: {} + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -7665,6 +7789,10 @@ snapshots: css-gradient-parser@0.0.17: {} + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-to-react-native@3.2.0: dependencies: camelize: 1.0.1 @@ -7931,6 +8059,15 @@ snapshots: dependencies: esutils: 2.0.3 + docx@9.6.1: + dependencies: + '@types/node': 25.5.0 + hash.js: 1.1.7 + jszip: 3.10.1 + nanoid: 5.1.6 + xml: 1.0.1 + xml-js: 1.6.11 + dompurify@3.3.1: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -8361,6 +8498,12 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -8371,6 +8514,8 @@ snapshots: fflate@0.7.4: {} + fflate@0.8.2: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -8525,6 +8670,11 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -8659,6 +8809,17 @@ snapshots: html-void-elements@3.0.0: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + + html2pdf.js@0.14.0: + dependencies: + dompurify: 3.3.1 + html2canvas: 1.4.1 + jspdf: 4.2.1 + human-signals@5.0.0: {} human-signals@8.0.1: {} @@ -8673,6 +8834,8 @@ snapshots: image-size@2.0.2: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -8680,6 +8843,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -8692,6 +8857,8 @@ snapshots: internmap@2.0.3: {} + iobuffer@5.4.0: {} + iron-webcrypto@1.2.1: {} is-alphabetical@2.0.1: {} @@ -8833,6 +9000,8 @@ snapshots: dependencies: is-docker: 2.2.1 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8874,6 +9043,17 @@ snapshots: dependencies: minimist: 1.2.8 + jspdf@4.2.1: + dependencies: + '@babel/runtime': 7.28.6 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.49.0 + dompurify: 3.3.1 + html2canvas: 1.4.1 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -8881,6 +9061,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + katex@0.16.28: dependencies: commander: 8.3.0 @@ -8931,6 +9118,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lighthouse-logger@2.0.2: dependencies: debug: 4.4.3 @@ -9026,6 +9217,8 @@ snapshots: marked@16.4.2: {} + marked@17.0.5: {} + marky@1.3.0: {} math-intrinsics@1.1.0: {} @@ -9430,6 +9623,8 @@ snapshots: mimic-fn@4.0.0: {} + minimalistic-assert@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -9710,6 +9905,10 @@ snapshots: pako@0.2.9: {} + pako@1.0.11: {} + + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -9749,6 +9948,9 @@ snapshots: perfect-debounce@2.1.0: {} + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -9804,6 +10006,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + process-nextick-args@2.0.1: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -9818,6 +10022,11 @@ snapshots: radix3@1.1.2: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -9887,6 +10096,16 @@ snapshots: react@19.2.4: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@5.0.0: {} reflect.getprototypeof@1.0.10: @@ -9900,6 +10119,9 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.13.11: + optional: true + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -10000,6 +10222,9 @@ snapshots: reusify@1.1.0: {} + rgbcolor@1.0.1: + optional: true + robust-predicates@3.0.2: {} rollup@4.57.1: @@ -10056,6 +10281,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -10087,6 +10314,8 @@ snapshots: postcss-value-parser: 4.2.0 yoga-layout: 3.2.1 + sax@1.6.0: {} + scheduler@0.27.0: {} scule@1.3.0: {} @@ -10119,6 +10348,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -10222,6 +10453,9 @@ snapshots: stable-hash@0.0.5: {} + stackblur-canvas@2.7.0: + optional: true + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -10301,6 +10535,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -10341,12 +10579,19 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + tailwind-merge@3.4.0: {} tailwindcss@4.1.18: {} tapable@2.3.0: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + tiny-inflate@1.0.3: {} tinyexec@1.0.2: {} @@ -10462,6 +10707,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.18.2: {} + unhead@2.1.4: dependencies: hookable: 6.0.1 @@ -10602,6 +10849,12 @@ snapshots: dependencies: react: 19.2.4 + util-deprecate@1.0.2: {} + + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + uuid@10.0.0: {} uuid@11.1.0: {} @@ -10717,6 +10970,12 @@ snapshots: word-wrap@1.2.5: {} + xml-js@1.6.11: + dependencies: + sax: 1.6.0 + + xml@1.0.1: {} + yocto-queue@0.1.0: {} yoctocolors@2.1.2: {} diff --git a/frontend/src/components/ai-elements/artifact.tsx b/frontend/src/components/ai-elements/artifact.tsx index 387580d1..9d273e2d 100644 --- a/frontend/src/components/ai-elements/artifact.tsx +++ b/frontend/src/components/ai-elements/artifact.tsx @@ -90,6 +90,7 @@ export type ArtifactActionProps = ComponentProps & { tooltip?: string; label?: string; icon?: LucideIcon; + asChild?: boolean; }; export const ArtifactAction = ({ @@ -100,6 +101,7 @@ export const ArtifactAction = ({ className, size = "sm", variant = "ghost", + asChild = false, ...props }: ArtifactActionProps) => { const button = ( @@ -111,6 +113,7 @@ export const ArtifactAction = ({ size={size} type="button" variant={variant} + asChild={asChild} {...props} > {Icon ? : children} diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index d7851956..8017ed8c 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -1,21 +1,8 @@ -import { - Code2Icon, - CopyIcon, - DownloadIcon, - EyeIcon, - LoaderIcon, - PackageIcon, - SquareArrowOutUpRightIcon, - XIcon, - ZoomIn, - ZoomOut, - type LucideIcon, -} from "lucide-react"; +import { DownloadIcon, FileTextIcon, LoaderIcon, FileTypeIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, - useRef, useState, type HTMLAttributes, } from "react"; @@ -30,14 +17,13 @@ import { ArtifactHeader, ArtifactTitle, } from "@/components/ai-elements/artifact"; -import { DropdownSelector } from "@/components/ui/dropdown-selector"; -import { Select, SelectItem } from "@/components/ui/select"; import { - SelectContent, - SelectGroup, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { DropdownSelector } from "@/components/ui/dropdown-selector"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { CodeEditor } from "@/components/workspace/code-editor"; import { useArtifactContent } from "@/core/artifacts/hooks"; @@ -47,11 +33,10 @@ import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { installSkill } from "@/core/skills/api"; import { streamdownPlugins } from "@/core/streamdown"; import { checkCodeFile, getFileName } from "@/core/utils/files"; -import { env } from "@/env"; +import { useMarkdownDownload } from "@/core/utils/markdown-download"; import { cn, copyToClipboard } from "@/lib/utils"; import { CitationLink } from "../citations/citation-link"; -import { Tooltip } from "../tooltip"; import { useArtifacts } from "./context"; @@ -114,6 +99,34 @@ export function ArtifactFileDetail({ const [isInstalling, setIsInstalling] = useState(false); const [zoom, setZoom] = useState(80); + // 获取文件名(不含路径) + const fileName = useMemo(() => getFileName(filepath), [filepath]); + + // 是否可以转换为docx/pdf(仅markdown文件支持) + const canConvertToDocxPdf = language === "markdown"; + + // 使用 Markdown 下载 hook + const { isDownloading, downloadAsDocx, downloadAsPdf } = useMarkdownDownload({ + onError: (error, format) => { + console.error(`Failed to download as ${format}:`, error); + toast.error(`Failed to download as ${format.toUpperCase()}`); + }, + }); + + // 下载为 DOCX + const handleDownloadDocx = useCallback(() => { + if (content) { + void downloadAsDocx(content, fileName); + } + }, [content, fileName, downloadAsDocx]); + + // 下载为 PDF + const handleDownloadPdf = useCallback(() => { + if (content) { + void downloadAsPdf(content, fileName); + } + }, [content, fileName, downloadAsPdf]); + // 全屏切换处理 const handleFullscreenToggle = useCallback(() => { const newFullscreen = !fullscreen; @@ -153,6 +166,7 @@ export function ArtifactFileDetail({ setIsInstalling(false); } }, [threadId, filepath, isInstalling]); + return ( // 给滚动遮挡头部定位relative @@ -274,35 +288,75 @@ export function ArtifactFileDetail({ )} {!isWriteFile && ( - - - + + - - - - - + {isDownloading ? ( + + ) : ( + + + + + )} + + + + + + + {t.common.downloadOriginal} + + + {/* DOCX 和 PDF 导出选项仅对 Markdown 文件显示。 */} + {canConvertToDocxPdf && ( + <> + + + {isDownloading === "docx" ? t.common.loading : t.common.downloadAsDocx} + + + + {isDownloading === "pdf" ? t.common.loading : t.common.downloadAsPdf} + + + )} + + )} {/* 全屏按钮 */} { + const { codeFont = "Courier New", codeFontSize = 22 } = options; + + const tokens = marked.lexer(markdown); + const children = parseTokensToDocx(tokens, { codeFont, codeFontSize }); + + const doc = new DocxDocument({ + sections: [{ children }], + }); + + const blob = await Packer.toBlob(doc); + downloadBlob(blob, normalizeFilename(filename, ".docx")); +} + +// ============================================================================ +// PDF Converter +// ============================================================================ + +/** + * 将 Markdown 内容转换为 PDF 文件并下载 + * + * @param markdown - Markdown 文本内容 + * @param filename - 文件名(不含扩展名,或包含 .md 扩展名) + * @param options - 转换选项 + * + * @example + * ```ts + * await downloadMarkdownAsPdf("# Hello World", "document"); + * ``` + */ +export async function downloadMarkdownAsPdf(markdown: string, filename: string, options: PdfOptions = {}): Promise { + const html2pdf = await loadHtml2Pdf(); + + const { margin = [15, 15, 15, 15], format = "a4", orientation = "portrait", scale = 2 } = options; + + // 解析 Markdown 为 HTML + const htmlContent = await marked.parse(markdown); + + // 创建容器并应用样式 + const container = createStyledContainer(htmlContent); + + // 配置 html2pdf + const opt = { + margin, + filename: normalizeFilename(filename, ".pdf"), + image: { type: "jpeg" as const, quality: 0.98 }, + html2canvas: { + scale, + useCORS: true, + logging: false, + onclone: fixColorsForHtml2Canvas, + }, + jsPDF: { unit: "mm" as const, format, orientation }, + }; + + await html2pdf().set(opt).from(container).save(); +} + +// ============================================================================ +// Internal Utilities +// ============================================================================ + +/** + * 动态加载 html2pdf.js + */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +async function loadHtml2Pdf(): Promise { + const html2pdf = await import("html2pdf.js"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return html2pdf.default; +} + +/** + * 创建带样式的 HTML 容器 + */ +function createStyledContainer(htmlContent: string): HTMLDivElement { + const container = document.createElement("div"); + container.innerHTML = htmlContent; + + // 容器基础样式 + container.style.cssText = ` + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.6; + padding: 20px; + max-width: 800px; + color: #333333; + background-color: #ffffff; + `; + + // 应用元素样式 + applyElementStyles(container); + + return container; +} + +/** + * 应用元素样式 + */ +function applyElementStyles(container: HTMLElement): void { + // 标题 + container.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => { + const el = h as HTMLElement; + el.style.marginTop = "1.5em"; + el.style.marginBottom = "0.5em"; + el.style.fontWeight = "600"; + el.style.color = "#1a1a1a"; + }); + + // 段落 + container.querySelectorAll("p").forEach((p) => { + (p as HTMLElement).style.marginBottom = "1em"; + }); + + // 代码块 + container.querySelectorAll("pre, code").forEach((code) => { + const el = code as HTMLElement; + el.style.fontFamily = "'SF Mono', 'Fira Code', Consolas, monospace"; + el.style.backgroundColor = "#f5f5f5"; + el.style.color = "#333333"; + el.style.fontSize = "13px"; + if (code.tagName === "PRE") { + el.style.padding = "12px"; + el.style.borderRadius = "6px"; + el.style.overflow = "auto"; + } else { + el.style.padding = "2px 4px"; + el.style.borderRadius = "3px"; + } + }); + + // 列表 + container.querySelectorAll("ul, ol").forEach((list) => { + const el = list as HTMLElement; + el.style.marginBottom = "1em"; + el.style.paddingLeft = "2em"; + }); + + // 引用块 + container.querySelectorAll("blockquote").forEach((bq) => { + const el = bq as HTMLElement; + el.style.borderLeft = "4px solid #dddddd"; + el.style.marginLeft = "0"; + el.style.paddingLeft = "16px"; + el.style.color = "#666666"; + }); + + // 表格 + container.querySelectorAll("table").forEach((table) => { + const el = table as HTMLElement; + el.style.borderCollapse = "collapse"; + el.style.width = "100%"; + el.style.marginBottom = "1em"; + }); + + container.querySelectorAll("th, td").forEach((cell) => { + const el = cell as HTMLElement; + el.style.border = "1px solid #dddddd"; + el.style.padding = "8px"; + }); + + // 链接 + container.querySelectorAll("a").forEach((link) => { + const el = link as HTMLElement; + el.style.color = "#0066cc"; + el.style.textDecoration = "underline"; + }); + + // 分割线 + container.querySelectorAll("hr").forEach((hr) => { + const el = hr as HTMLElement; + el.style.border = "none"; + el.style.borderTop = "1px solid #dddddd"; + el.style.margin = "2em 0"; + }); +} + +/** + * 修复 html2canvas 不支持的颜色函数 + */ +function fixColorsForHtml2Canvas(clonedDoc: Document): void { + // 移除外部样式表(可能包含 lab、oklab 等不支持的颜色) + clonedDoc.querySelectorAll('link[rel="stylesheet"], style').forEach((sheet) => sheet.remove()); + + // 重置所有元素的颜色属性为安全值 + clonedDoc.querySelectorAll("*").forEach((el) => { + const props = [ + "color", + "background-color", + "border-color", + "border-top-color", + "border-bottom-color", + "border-left-color", + "border-right-color", + "outline-color", + "text-decoration-color", + "caret-color", + "column-rule-color", + "accent-color", + "fill", + "stroke", + ]; + + props.forEach((prop) => el.style.removeProperty(prop)); + + el.style.color = "#333333"; + el.style.backgroundColor = "transparent"; + }); + + // 设置 body 背景 + const body = clonedDoc.body; + body.style.color = "#333333"; + body.style.backgroundColor = "#ffffff"; +} + +/** + * 解析 Markdown Token 为 DOCX Paragraph + */ +function parseTokensToDocx(tokens: MarkdownToken[], options: Required): Paragraph[] { + const paragraphs: Paragraph[] = []; + + for (const token of tokens) { + switch (token.type) { + case "heading": { + const runs = parseInlineTokens(token.tokens ?? [], options); + paragraphs.push( + new Paragraph({ + children: runs, + heading: getHeadingLevel(token.depth), + spacing: { before: 240, after: 120 }, + }) + ); + break; + } + + case "paragraph": { + const runs = parseInlineTokens(token.tokens ?? [], options); + paragraphs.push( + new Paragraph({ + children: runs.length > 0 ? runs : [new TextRun("")], + spacing: { after: 200 }, + }) + ); + break; + } + + case "code": { + const lines = token.text.split("\n"); + lines.forEach((line: string) => { + paragraphs.push( + new Paragraph({ + children: [ + new TextRun({ + text: line.length > 0 ? line : " ", + font: options.codeFont, + size: options.codeFontSize, + }), + ], + shading: { fill: "F5F5F5" }, + }) + ); + }); + paragraphs.push(new Paragraph({ children: [] })); + break; + } + + case "list": { + token.items?.forEach((item: MarkdownToken) => { + const runs = parseInlineTokens(item.tokens?.[0]?.tokens ?? [], options); + paragraphs.push( + new Paragraph({ + children: runs.length > 0 ? runs : [new TextRun("")], + bullet: { level: 0 }, + spacing: { after: 80 }, + }) + ); + }); + break; + } + + case "blockquote": { + const runs = parseInlineTokens(token.tokens?.[0]?.tokens ?? [], options); + paragraphs.push( + new Paragraph({ + children: runs.length > 0 ? runs : [new TextRun("")], + indent: { left: 720 }, + border: { left: { style: "single", size: 12, color: "CCCCCC" } }, + spacing: { after: 200 }, + }) + ); + break; + } + + case "hr": { + paragraphs.push( + new Paragraph({ + children: [new TextRun({ text: "─".repeat(50), color: "CCCCCC" })], + spacing: { before: 200, after: 200 }, + }) + ); + break; + } + + case "space": { + paragraphs.push(new Paragraph({ children: [] })); + break; + } + } + } + + return paragraphs; +} + +/** + * 解析行内 Token 为 TextRun + */ +function parseInlineTokens(tokens: MarkdownToken[], options: Required): TextRun[] { + const runs: TextRun[] = []; + + for (const token of tokens) { + switch (token.type) { + case "text": + runs.push(new TextRun(token.raw ?? token.text ?? "")); + break; + + case "strong": + runs.push(new TextRun({ text: token.text, bold: true })); + break; + + case "em": + runs.push(new TextRun({ text: token.text, italics: true })); + break; + + case "codespan": + runs.push( + new TextRun({ + text: token.text, + font: options.codeFont, + shading: { fill: "F0F0F0" }, + }) + ); + break; + + case "link": + runs.push( + new TextRun({ + text: token.text, + color: "0066CC", + underline: {}, + }) + ); + break; + + case "br": + runs.push(new TextRun({ text: "", break: 1 })); + break; + + default: + runs.push(new TextRun(token.raw ?? "")); + } + } + + return runs; +} + +/** + * 获取标题级别 + */ +function getHeadingLevel(depth: number): typeof HeadingLevel[keyof typeof HeadingLevel] | undefined { + const levels = [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, HeadingLevel.HEADING_4, HeadingLevel.HEADING_5, HeadingLevel.HEADING_6]; + return levels[depth - 1]; +} + +/** + * 规范化文件名 + */ +function normalizeFilename(filename: string, extension: string): string { + return filename.replace(/\.md$/i, "") + extension; +} + +/** + * 触发 Blob 下载 + */ +function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} \ No newline at end of file diff --git a/frontend/src/core/utils/markdown-download/index.ts b/frontend/src/core/utils/markdown-download/index.ts new file mode 100644 index 00000000..7b3c158c --- /dev/null +++ b/frontend/src/core/utils/markdown-download/index.ts @@ -0,0 +1,47 @@ +/** + * Markdown 文档下载工具 + * + * @description + * 将 Markdown 内容转换为 DOCX 或 PDF 格式并下载。 + * 可在任何 React + TypeScript 项目中使用。 + * + * @example + * ```tsx + * // React Hook 使用方式 + * import { useMarkdownDownload } from "./markdown-download"; + * + * function MyComponent() { + * const { downloadAsDocx, downloadAsPdf, isDownloading } = useMarkdownDownload(); + * + * return ( + *
+ * + * + *
+ * ); + * } + * ``` + * + * @example + * ```ts + * // 非 React 环境直接使用转换函数 + * import { downloadMarkdownAsDocx, downloadMarkdownAsPdf } from "./markdown-download"; + * + * await downloadMarkdownAsDocx("# Hello World", "document"); + * await downloadMarkdownAsPdf("# Hello World", "document", { format: "a4" }); + * ``` + */ + +// React Hook +export { useMarkdownDownload } from "./use-markdown-download"; + +// 类型 +export type { UseMarkdownDownloadOptions, UseMarkdownDownloadReturn } from "./use-markdown-download"; +export type { PdfOptions, DocxOptions } from "./converter"; + +// 转换函数(供非 React 环境使用) +export { downloadMarkdownAsDocx, downloadMarkdownAsPdf } from "./converter"; \ No newline at end of file diff --git a/frontend/src/core/utils/markdown-download/use-markdown-download.ts b/frontend/src/core/utils/markdown-download/use-markdown-download.ts new file mode 100644 index 00000000..d95b2172 --- /dev/null +++ b/frontend/src/core/utils/markdown-download/use-markdown-download.ts @@ -0,0 +1,127 @@ +import { useCallback, useState } from "react"; + +import { downloadMarkdownAsDocx, downloadMarkdownAsPdf } from "./converter"; + +/** + * Markdown 下载 Hook 配置选项 + */ +export interface UseMarkdownDownloadOptions { + /** + * 下载开始时的回调 + */ + onDownloadStart?: (format: "docx" | "pdf") => void; + /** + * 下载完成时的回调 + */ + onDownloadEnd?: (format: "docx" | "pdf") => void; + /** + * 下载失败时的回调 + */ + onError?: (error: Error, format: "docx" | "pdf") => void; +} + +/** + * Markdown 下载 Hook 返回值 + */ +export interface UseMarkdownDownloadReturn { + /** + * 当前下载状态 + */ + isDownloading: "docx" | "pdf" | null; + /** + * 下载为 DOCX + */ + downloadAsDocx: (markdown: string, filename: string) => Promise; + /** + * 下载为 PDF + */ + downloadAsPdf: (markdown: string, filename: string) => Promise; + /** + * 是否可以下载(没有正在进行的下载) + */ + canDownload: boolean; +} + +/** + * Markdown 文档下载 Hook + * + * @description + * 将 Markdown 内容转换为 DOCX 或 PDF 格式并下载。 + * 可在任何 React + TypeScript 项目中使用。 + * + * @example + * ```tsx + * import { useMarkdownDownload } from "./hooks/use-markdown-download"; + * + * function MyComponent() { + * const { downloadAsDocx, downloadAsPdf, isDownloading, canDownload } = useMarkdownDownload({ + * onError: (error, format) => { + * console.error(`Failed to download as ${format}:`, error); + * }, + * }); + * + * const handleDownload = () => { + * downloadAsDocx("# Hello World", "document"); + * }; + * + * return ( + * + * ); + * } + * ``` + */ +export function useMarkdownDownload(options: UseMarkdownDownloadOptions = {}): UseMarkdownDownloadReturn { + const { onDownloadStart, onDownloadEnd, onError } = options; + + const [isDownloading, setIsDownloading] = useState<"docx" | "pdf" | null>(null); + + const downloadAsDocx = useCallback( + async (markdown: string, filename: string) => { + if (isDownloading) return; + + setIsDownloading("docx"); + onDownloadStart?.("docx"); + + try { + await downloadMarkdownAsDocx(markdown, filename); + } catch (error) { + onError?.(error instanceof Error ? error : new Error(String(error)), "docx"); + } finally { + setIsDownloading(null); + onDownloadEnd?.("docx"); + } + }, + [isDownloading, onDownloadStart, onDownloadEnd, onError] + ); + + const downloadAsPdf = useCallback( + async (markdown: string, filename: string) => { + if (isDownloading) return; + + setIsDownloading("pdf"); + onDownloadStart?.("pdf"); + + try { + await downloadMarkdownAsPdf(markdown, filename); + } catch (error) { + onError?.(error instanceof Error ? error : new Error(String(error)), "pdf"); + } finally { + setIsDownloading(null); + onDownloadEnd?.("pdf"); + } + }, + [isDownloading, onDownloadStart, onDownloadEnd, onError] + ); + + return { + isDownloading, + downloadAsDocx, + downloadAsPdf, + canDownload: isDownloading === null, + }; +} + +// 导出转换函数,供非 React 环境使用 +export { downloadMarkdownAsDocx, downloadMarkdownAsPdf } from "./converter"; \ No newline at end of file