From 68d9946b70dc596962b1f57ffc2d5f39a914ac14 Mon Sep 17 00:00:00 2001 From: Timi Date: Fri, 21 Aug 2026 01:11:23 +0800 Subject: [PATCH] add tiptap --- .eslintrc.js | 2 +- package.json | 22 +- pnpm-lock.yaml | 960 ++++++++++++++++++ src/api/ArticleAPI.ts | 6 +- src/api/NotifyAPI.ts | 8 + src/components/article-editor/index.ts | 13 + src/components/article-editor/index.vue | 289 ++++++ src/components/article-editor/shared.ts | 138 +++ src/components/article-view/index.ts | 6 + src/components/article-view/index.vue | 118 +++ src/components/code-editor/index.ts | 26 + src/components/code-editor/index.vue | 450 ++++++++ src/components/code-editor/input.ts | 208 ++++ src/components/code-editor/style.less | 202 ++++ src/components/editor-search/index.vue | 271 +++++ src/components/editor-search/search.ts | 134 +++ src/components/index.ts | 34 +- src/components/prism-code-block.less | 181 ++++ src/components/tiptap-content.less | 46 + .../tiptap-editor/PrismCodeBlock.ts | 174 ++++ .../tiptap-editor/TiptapCodeBlockButton.vue | 56 + .../tiptap-editor/TiptapIframeButton.vue | 113 +++ .../tiptap-editor/TiptapLinkButton.vue | 95 ++ .../tiptap-editor/TiptapMediaButton.vue | 184 ++++ .../tiptap-editor/TiptapPaddingButton.vue | 99 ++ .../tiptap-editor/TiptapTableButton.vue | 126 +++ .../tiptap-editor/TiptapToolbar.vue | 431 ++++++++ src/components/tiptap-editor/extensions.ts | 259 +++++ src/components/tiptap-editor/index.ts | 15 + src/components/tiptap-editor/index.vue | 617 +++++++++++ src/components/tiptap-editor/shared.ts | 81 ++ .../tiptap-editor/useTiptapEditor.ts | 135 +++ src/components/tiptap-view/index.ts | 6 + src/components/tiptap-view/index.vue | 93 ++ src/index.ts | 5 + src/types/Article.ts | 69 +- src/types/Attachment.ts | 21 +- src/utils/Prism.ts | 19 + src/utils/PrismCodeBlock.ts | 123 +++ src/utils/TDesignMessage.ts | 27 + vite.config.ts | 18 +- 41 files changed, 5845 insertions(+), 35 deletions(-) create mode 100644 src/components/article-editor/index.ts create mode 100644 src/components/article-editor/index.vue create mode 100644 src/components/article-editor/shared.ts create mode 100644 src/components/article-view/index.ts create mode 100644 src/components/article-view/index.vue create mode 100644 src/components/code-editor/index.ts create mode 100644 src/components/code-editor/index.vue create mode 100644 src/components/code-editor/input.ts create mode 100644 src/components/code-editor/style.less create mode 100644 src/components/editor-search/index.vue create mode 100644 src/components/editor-search/search.ts create mode 100644 src/components/prism-code-block.less create mode 100644 src/components/tiptap-content.less create mode 100644 src/components/tiptap-editor/PrismCodeBlock.ts create mode 100644 src/components/tiptap-editor/TiptapCodeBlockButton.vue create mode 100644 src/components/tiptap-editor/TiptapIframeButton.vue create mode 100644 src/components/tiptap-editor/TiptapLinkButton.vue create mode 100644 src/components/tiptap-editor/TiptapMediaButton.vue create mode 100644 src/components/tiptap-editor/TiptapPaddingButton.vue create mode 100644 src/components/tiptap-editor/TiptapTableButton.vue create mode 100644 src/components/tiptap-editor/TiptapToolbar.vue create mode 100644 src/components/tiptap-editor/extensions.ts create mode 100644 src/components/tiptap-editor/index.ts create mode 100644 src/components/tiptap-editor/index.vue create mode 100644 src/components/tiptap-editor/shared.ts create mode 100644 src/components/tiptap-editor/useTiptapEditor.ts create mode 100644 src/components/tiptap-view/index.ts create mode 100644 src/components/tiptap-view/index.vue create mode 100644 src/utils/Prism.ts create mode 100644 src/utils/PrismCodeBlock.ts create mode 100644 src/utils/TDesignMessage.ts diff --git a/.eslintrc.js b/.eslintrc.js index d25856a..23fd20a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,7 +7,7 @@ module.exports = { "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", - "plugin:vue/vue3-essential", + "plugin:vue/essential", "./.eslintrc-auto-import.json" ], "overrides": [ diff --git a/package.json b/package.json index a4d66d5..fc22c32 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,24 @@ "node": ">=16.0.0" }, "dependencies": { + "@tiptap/extension-code-block": "^3.22.5", + "@tiptap/extension-color": "^3.22.4", + "@tiptap/extension-image": "^3.22.4", + "@tiptap/extension-link": "^3.22.4", + "@tiptap/extension-placeholder": "^3.22.4", + "@tiptap/extension-subscript": "^3.22.4", + "@tiptap/extension-superscript": "^3.22.4", + "@tiptap/extension-table": "^3.22.5", + "@tiptap/extension-table-cell": "^3.22.5", + "@tiptap/extension-table-header": "^3.22.5", + "@tiptap/extension-table-row": "^3.22.5", + "@tiptap/extension-text-align": "^3.22.4", + "@tiptap/extension-text-style": "^3.22.4", + "@tiptap/extension-underline": "^3.22.4", + "@tiptap/pm": "^3.30.2", + "@tiptap/starter-kit": "^3.22.4", + "@tiptap/vue-3": "^3.22.4", + "@marsio/vue-split-pane": "^1.0.0", "axios": "1.18.0", "less": "4.5.1", "pinia": "^3.0.2", @@ -50,6 +68,7 @@ "terser": "^5.44.1" }, "devDependencies": { + "@types/marked": "^6.0.0", "@types/node": "^25.0.8", "@types/prismjs": "1.26.5", "@typescript-eslint/eslint-plugin": "^8.53.0", @@ -74,6 +93,7 @@ "vitest": "^4.1.4", "vue": "^3.5.26", "vue-tsc": "^3.2.2", - "vue-eslint-parser": "^10.0.0" + "vue-eslint-parser": "^10.0.0", + "tdesign-vue-next": "^1.20.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 272f7ea..0b24b58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,60 @@ importers: .: dependencies: + '@marsio/vue-split-pane': + specifier: ^1.0.0 + version: 1.0.0(typescript@5.9.3) + '@tiptap/extension-code-block': + specifier: ^3.22.5 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-color': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/extension-text-style@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))) + '@tiptap/extension-image': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-link': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-placeholder': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/extensions@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-subscript': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-superscript': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-table': + specifier: ^3.22.5 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-table-cell': + specifier: ^3.22.5 + version: 3.30.2(@tiptap/extension-table@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-table-header': + specifier: ^3.22.5 + version: 3.30.2(@tiptap/extension-table@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-table-row': + specifier: ^3.22.5 + version: 3.30.2(@tiptap/extension-table@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-text-align': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-text-style': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-underline': + specifier: ^3.22.4 + version: 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/pm': + specifier: ^3.30.2 + version: 3.30.2 + '@tiptap/starter-kit': + specifier: ^3.22.4 + version: 3.30.2 + '@tiptap/vue-3': + specifier: ^3.22.4 + version: 3.30.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)(vue@3.5.26(typescript@5.9.3)) axios: specifier: 1.18.0 version: 1.18.0 @@ -36,6 +90,9 @@ importers: specifier: ^5.44.1 version: 5.44.1 devDependencies: + '@types/marked': + specifier: ^6.0.0 + version: 6.0.0 '@types/node': specifier: ^25.0.8 version: 25.0.8 @@ -78,6 +135,9 @@ importers: prettier: specifier: ^3.8.0 version: 3.8.0 + tdesign-vue-next: + specifier: ^1.20.6 + version: 1.20.6(vue@3.5.26(typescript@5.9.3)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -228,6 +288,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.24.0': resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} @@ -265,6 +329,36 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/css@11.13.5': + resolution: {integrity: sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -313,6 +407,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -364,6 +467,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@marsio/vue-draggable@1.0.11': + resolution: {integrity: sha512-UkQCPC34P1ODuqTKtmy1iINDhg4Svo93zbHose6UvU+Ukz/Sbx/dUJZlrjOGTnIzHUqJgTMfcWaznl8E+vWzWg==} + + '@marsio/vue-split-pane@1.0.0': + resolution: {integrity: sha512-7XEaVDjVhnoYSVcbgPvONT/U3F6n/oKbcNaj0i+lE2EfP2P38kIKyhHbSgbSGx+8i/DAP5vQtcjLIcleWb4N+A==} + '@microsoft/api-extractor-model@7.30.3': resolution: {integrity: sha512-yEAvq0F78MmStXdqz9TTT4PZ05Xu5R8nqgwI5xmUmQjWBQ9E6R2n8HB/iZMRciG4rf9iwI2mtuQwIzDXBvHn1w==} @@ -397,6 +506,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -670,6 +782,212 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tiptap/core@3.30.2': + resolution: {integrity: sha512-QbZC/s1OOqcoUdkhIY16TjR/gCtR0qAk9e4bJwUqOJqZuv5ozqCL5hzWm22jjTPp6c6Ei2tPd6t30VwfIKW4lQ==} + peerDependencies: + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-blockquote@3.30.2': + resolution: {integrity: sha512-BOkwhZenek7vzXBOgKppSrlx4YryBdAYu1p1MXKn0R9A9eNmE2HVhmm0gG49+E8BhsE/TG8wKVclwET42JJiIg==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-bold@3.30.2': + resolution: {integrity: sha512-MsvJhPgYejY2D9MhwYJv8AmscozvLBI8qtJ7YLdYZBWkMR4bgmxHq5+xqEfBsao9bOMMwBon9p3+P+/Tq5ReWA==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-bubble-menu@3.30.2': + resolution: {integrity: sha512-oS0WiWNXHKpiPYMkcnHm1j7iEvufTGGtLFtcJJd3olb5OS6V2acoXVDL0nNJDmRQ32K3QXut3fbRcMseMxT+lw==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-bullet-list@3.30.2': + resolution: {integrity: sha512-+awIL/TUz4aB3rL68igU1rWfaaoBIAkPcakkktkRq8gYf0bd9eSb48P6kHkpx/3q+JyK7g9vsnltLNHNh6twnA==} + peerDependencies: + '@tiptap/extension-list': 3.30.2 + + '@tiptap/extension-code-block@3.30.2': + resolution: {integrity: sha512-9otGKaQZmePHrLXFtCtz+BYDn5z4sSumTkUqQIQHz0gVxwPoTi7g51RedwxvViTb/zu2XV5ROXYLHIxKxypMPg==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-code@3.30.2': + resolution: {integrity: sha512-r8EZk3R9yGpF6v5xxafAU1HwrD/e+RpbfnmVi2TeB/ZHAsVO62fW96E32G0t6IdaCtOFtAd85hkDAaOfCT4yGg==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-color@3.30.2': + resolution: {integrity: sha512-VNct6qKePIoruJZ4PR30JtA3YfENOqESrUtDLF1YMJYf/oU26Bx4ePJGTy428IA0mouyE+uHruyUGfZXnT73Iw==} + peerDependencies: + '@tiptap/extension-text-style': 3.30.2 + + '@tiptap/extension-document@3.30.2': + resolution: {integrity: sha512-+xIv67V+/2L1uvz98FAT5W7kWEfHwfNV3MD7b4UsKPU0lhcCWuVOXy0JB8yYmdNExqpI7xT9g3MWzREoBvBQSg==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-dropcursor@3.30.2': + resolution: {integrity: sha512-nyRKUmItATnKI9AiRChmjhcBbCEsNxRu+AaCz+cx8EvnAcNHsVRdNYL5PmBs3WlNA/Et4Eb2DG0hVQDnNF61eg==} + peerDependencies: + '@tiptap/extensions': 3.30.2 + + '@tiptap/extension-floating-menu@3.30.2': + resolution: {integrity: sha512-A8PLvvh8W6PUMrqh+EpBerxm+Ucr0irGxJvwAnzYQmNGNIJ9U4OVgw4OcEU+9JH0gMmEzDcHzMPP2s/s1lIcyw==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-gapcursor@3.30.2': + resolution: {integrity: sha512-7Xk0ut6FM+RAsvKxDN3bAtk7zvYZ6Aa8pawJ6s7dLAmLR9JwrZevlcL4FSrj4bR7rqKOj92RYCFxzgTEbWimag==} + peerDependencies: + '@tiptap/extensions': 3.30.2 + + '@tiptap/extension-hard-break@3.30.2': + resolution: {integrity: sha512-IxSNgmG3d4OZdUTeebrOI7SxdIWXXJqlcGiSNDabWqxipUitfy3mZ3gDDE6G01koKxZRbhz4KIplAZlpxnTFSg==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-heading@3.30.2': + resolution: {integrity: sha512-PblDvgSJ05p1t6hzyPi02xeiBjB0M2abReoGEImqSWCy79UqnAGacgsZo4EeEawtJV1NEP8chhvmX+nRtzdT1A==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-horizontal-rule@3.30.2': + resolution: {integrity: sha512-j8aswLTsuEdJKC62DF+kw0EgvIRL7QMUyAVp2fdjR0qgM0ZVlEwCC4qIEq3kK9tFVU4kRtQ5BSj/jn6QwrlbCA==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-image@3.30.2': + resolution: {integrity: sha512-K/BPlWauHXI6Y4s7se2A1BLcZ2pnWmRQTEDkPJYe/8kmv1GyJzPu34sZed5Mvfu5chUH8u6aNZ/utZbvSbI/0Q==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-italic@3.30.2': + resolution: {integrity: sha512-pp8uaiuXsUbLm5rYzR1jlWbwm1mAahRajdHwAKBtthFRB2rDvC7ZWhKaCSoKhZvfIDRmu9/B67+uAHoutL0dCA==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-link@3.30.2': + resolution: {integrity: sha512-jwdcymKcrbFpj5hRAuGVLCq8FieVkGFnENyroYmvkad+XAt8ZLy/MTFYRN6SK3ukH6PZMY7H4iObGtciQaC5nw==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-list-item@3.30.2': + resolution: {integrity: sha512-HWgRCRlGxulE+hN1VUcnWD6P2NE08VBgGtcaxOfdXVqaI93BCK6AhRQZGpLsfKgajLk+5DXTBraaitwnBqzCxg==} + peerDependencies: + '@tiptap/extension-list': 3.30.2 + + '@tiptap/extension-list-keymap@3.30.2': + resolution: {integrity: sha512-TTve3WOlQaYu1ahMqsQ/T0wzaxfgZvcOl3/OuPyInOi8QtxXhqGhFjmYe5jOr56G9W2QDuFWVsZecVwfDte9zg==} + peerDependencies: + '@tiptap/extension-list': 3.30.2 + + '@tiptap/extension-list@3.30.2': + resolution: {integrity: sha512-MIUpo1Bd9Rf1Qg+TNYNwDZ4xsfFeQahjU9Xhy6UcaszKQzbAM7KCzn5BObNytK1NdcqNHsC8Wj5vFvMKEzrXdw==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-ordered-list@3.30.2': + resolution: {integrity: sha512-Z7OO1HcF0idda1n6vodXeQ3h2ylN9JR4IfIGUYkar5Xl9JusK8PDETTBQZQn//96p49I2d+GoWsD2LXPtjHXXg==} + peerDependencies: + '@tiptap/extension-list': 3.30.2 + + '@tiptap/extension-paragraph@3.30.2': + resolution: {integrity: sha512-ulEu3LNt+kPVAWEnrhoz13Fs8Q/v/8NUxQbAeteuBchQ8joxJXuWExhpy1fUfZir5+b+W5z7/NesgPjZQfv47w==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-placeholder@3.30.2': + resolution: {integrity: sha512-Bj1seUvPCoRrD/LpzMoKD+jQIjxuc+oq931GpPq4wobSUUbD4pF/0NMwpCLpiHO19QnTQz8+9p2dqPdlc44LHA==} + peerDependencies: + '@tiptap/extensions': 3.30.2 + + '@tiptap/extension-strike@3.30.2': + resolution: {integrity: sha512-fBLxMXz6hYIURzLOD+/L6aVATztsKham00ANWmGi13vN0hx2lQMYZffN+gR+QqiCDfMQxBXzrf5a7tJuDiQHLQ==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-subscript@3.30.2': + resolution: {integrity: sha512-rf16ZQiWTMMmsoG7c1AirGAjQ/gTabsleOLp+6JEwRQ/XFOO4XiPWVEgpbEXdwtzKNfyRp3sXAUzZvSKzGX+jg==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-superscript@3.30.2': + resolution: {integrity: sha512-jcfWyHAqq2wHlNGzNxpXp2SyBJoQYuGkj5LW/u2BuKm9uf/MJWRzczS2uSs43Gsc2kC1R4R+LZGHia3q473QcA==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-table-cell@3.30.2': + resolution: {integrity: sha512-fcFG6MsJV+x1ua27dJqIdpOa5wiTXaLSfXfZ0G+rhwchH8EtSM3Q64gT4KeHsa/vVt7pOEc6ChQhKMnm40fWdA==} + peerDependencies: + '@tiptap/extension-table': 3.30.2 + + '@tiptap/extension-table-header@3.30.2': + resolution: {integrity: sha512-qcpMr8x1XvevEG5QZdk4JGLzsjZal1zzNfy4xmEtwUYVYkXkWaXklQvxEXC6GUkVpTGTOSXtHl25/cSbnJ5qbg==} + peerDependencies: + '@tiptap/extension-table': 3.30.2 + + '@tiptap/extension-table-row@3.30.2': + resolution: {integrity: sha512-dlbkcmgb5eiMI9hLX5C6CaPGuGcpyyPxJCjIYcI1l4glimYcbgxxEeqtGIvySgDewxWgofY/V3CLOGHrfyBCkQ==} + peerDependencies: + '@tiptap/extension-table': 3.30.2 + + '@tiptap/extension-table@3.30.2': + resolution: {integrity: sha512-zWp0ehZnx6N5lwRa8anSTn17GBEF3wxHXfOJGbBugf4Vwmp5keUEAXrgeIS4j7gIdxjN3XawdwjuMImG93N+Fg==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-text-align@3.30.2': + resolution: {integrity: sha512-zexiz0uJlX2KzeAyYI/uEoBsl0Zw3Ua0CTV/kRKTnk7K9vCOnwvJXtXdkHA4WC12ACqUsPnyc4yMTwDR22fKFw==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-text-style@3.30.2': + resolution: {integrity: sha512-o3YMN3JNHg/rCCLZB3PUcAfl4bkrsXDoYoHRXvrbE9cl/RT+qAEFAAssI8kBC1fp9en9V2F4Z7Uy2uuzkbecKw==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-text@3.30.2': + resolution: {integrity: sha512-n/iZnirgRmXet6f97kolAnP3j8DsgLSiTbz/KLWc8eBYiFmkjRzkuisOm5xuGdfGIxwpB4x3tlSF4ef4DLnbRg==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extension-underline@3.30.2': + resolution: {integrity: sha512-SZiTMnvqXcnrtJX+X25ZbYsuDO83haGOVMBD/O+mAWYNYXhaSc5Rkph5czzItxrd+Yyp/vs4PiwD7XTNbfqmpA==} + peerDependencies: + '@tiptap/core': 3.30.2 + + '@tiptap/extensions@3.30.2': + resolution: {integrity: sha512-2LqAHXk26QDsryW+beECxYeBzv5Ylk4GuB3cOmfghS7/G37R2W+Te3TkUK7BT0EWoDryvBT57/5q0DEFIhfZZg==} + peerDependencies: + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + + '@tiptap/pm@3.30.2': + resolution: {integrity: sha512-BJN8tUx4ppFN3R3cV/FJfrJbJkvo1lj4uciq+nwpjwzdRvFzqIuglWf+HLcJ6CwlYpLOHp7ArgkBg4Q5e60Gog==} + + '@tiptap/starter-kit@3.30.2': + resolution: {integrity: sha512-fJSrhW1CyD4sjYA20evSP4Cp13B/HhbxCdM974K0xpHOVqvCtNU9w2s9hfq9mg2yGoU7MSNHKNYMkJjIi2/Xyw==} + + '@tiptap/vue-3@3.30.2': + resolution: {integrity: sha512-7TCahsK19ChY60FvgseTuzaz67yR1tkzTbKW6s3iXx9NTiU411C6WEQAS/EtPt4dsGDv/esFjSc9EMKilRhY5g==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.30.2 + '@tiptap/pm': 3.30.2 + vue: ^3.0.0 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -691,12 +1009,34 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + + '@types/marked@6.0.0': + resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==} + deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed. + '@types/node@25.0.8': resolution: {integrity: sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/sortablejs@1.15.9': + resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==} + + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -997,6 +1337,10 @@ packages: axios@1.18.0: resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==} + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + babel-plugin-prismjs@2.1.0: resolution: {integrity: sha512-ehzSKYfeAz4U78zi/sfwsjDPlq0LvDKxNefcZTJ/iKBu+plsHsLqZhUeGf1+82LAcA35UZGbU6ksEx2Utphc/g==} peerDependencies: @@ -1052,6 +1396,10 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1091,6 +1439,9 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1101,10 +1452,17 @@ packages: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1113,6 +1471,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -1191,6 +1552,9 @@ packages: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1235,6 +1599,7 @@ packages: eslint-define-config@2.1.0: resolution: {integrity: sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==} engines: {node: '>=18.0.0', npm: '>=9.0.0', pnpm: '>=8.6.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. eslint-plugin-prettier@5.5.5: resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} @@ -1351,6 +1716,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1468,6 +1836,9 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1500,6 +1871,12 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-prefixer@7.0.1: + resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} @@ -1573,6 +1950,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1679,6 +2059,12 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + local-pkg@1.1.1: resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} engines: {node: '>=14'} @@ -1691,6 +2077,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -1836,6 +2225,9 @@ packages: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1851,6 +2243,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -1873,6 +2269,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1952,6 +2352,45 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.2: + resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==} + + prosemirror-dropcursor@1.8.3: + resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.42.2: + resolution: {integrity: sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -2001,6 +2440,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2048,6 +2490,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sortablejs@1.15.7: + resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==} + source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -2059,6 +2504,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -2107,6 +2556,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} @@ -2131,6 +2583,17 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} + tdesign-icons-vue-next@0.4.10: + resolution: {integrity: sha512-TOmy4cP5tXiLk5/rC6ZheRw+qeokqTytl4HLE+7qZYc185kPq659ATyn9a1i+c5ZSU3eHPY79kIaJUEOl7OoyQ==} + peerDependencies: + vue: ^3.0.0 + + tdesign-vue-next@1.20.6: + resolution: {integrity: sha512-k9pKaWyyCPv1RGO8YEqhmOoAkS/WZ+CbS0CLWGMtuIltS+363dCZ4ggKhmu/y7Zvnv6Wdzoy8R5+ZIsuwsdvkw==} + engines: {node: '>= 18'} + peerDependencies: + vue: '>=3.1.0' + terser@5.44.1: resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} engines: {node: '>=10'} @@ -2139,6 +2602,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@1.1.1: resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} @@ -2237,6 +2703,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} + engines: {node: '>= 0.10'} + vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -2365,6 +2835,9 @@ packages: typescript: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -2412,6 +2885,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2543,6 +3020,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/runtime@7.29.7': {} + '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.23.5 @@ -2603,6 +3082,56 @@ snapshots: tslib: 2.8.1 optional: true + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.22.15 + '@babel/runtime': 7.29.7 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/css@11.13.5': + dependencies: + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + '@eslint-community/eslint-utils@4.4.0(eslint@9.39.2)': dependencies: eslint: 9.39.2 @@ -2656,6 +3185,17 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -2710,6 +3250,24 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@marsio/vue-draggable@1.0.11(typescript@5.9.3)': + dependencies: + clsx: 1.2.1 + lodash: 4.17.21 + vue: 3.5.26(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@marsio/vue-split-pane@1.0.0(typescript@5.9.3)': + dependencies: + '@emotion/css': 11.13.5 + '@marsio/vue-draggable': 1.0.11(typescript@5.9.3) + inline-style-prefixer: 7.0.1 + lodash: 4.17.21 + vue: 3.5.26(typescript@5.9.3) + transitivePeerDependencies: + - typescript + '@microsoft/api-extractor-model@7.30.3(@types/node@25.0.8)': dependencies: '@microsoft/tsdoc': 0.15.1 @@ -2761,6 +3319,8 @@ snapshots: '@pkgr/core@0.2.9': {} + '@popperjs/core@2.11.8': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true @@ -2934,6 +3494,219 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tiptap/core@3.30.2(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-blockquote@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-bold@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-bubble-menu@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + optional: true + + '@tiptap/extension-bullet-list@3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extension-list': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-code-block@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-code@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-color@3.30.2(@tiptap/extension-text-style@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)))': + dependencies: + '@tiptap/extension-text-style': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + + '@tiptap/extension-document@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-dropcursor@3.30.2(@tiptap/extensions@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extensions': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-floating-menu@3.30.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + optional: true + + '@tiptap/extension-gapcursor@3.30.2(@tiptap/extensions@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extensions': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-hard-break@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-heading@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-horizontal-rule@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-image@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-italic@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-link@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extension-list': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-list-keymap@3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extension-list': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-ordered-list@3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extension-list': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-paragraph@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-placeholder@3.30.2(@tiptap/extensions@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extensions': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-strike@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-subscript@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-superscript@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-table-cell@3.30.2(@tiptap/extension-table@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extension-table': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-table-header@3.30.2(@tiptap/extension-table@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extension-table': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-table-row@3.30.2(@tiptap/extension-table@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/extension-table': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + + '@tiptap/extension-table@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/extension-text-align@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-text-style@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-text@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extension-underline@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + + '@tiptap/extensions@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/pm@3.30.2': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.2 + prosemirror-dropcursor: 1.8.3 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + '@tiptap/starter-kit@3.30.2': + dependencies: + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/extension-blockquote': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-bold': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-bullet-list': 3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-code': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-code-block': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-document': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-dropcursor': 3.30.2(@tiptap/extensions@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-gapcursor': 3.30.2(@tiptap/extensions@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-hard-break': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-heading': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-horizontal-rule': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-italic': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-link': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-list': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-list-item': 3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-list-keymap': 3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-ordered-list': 3.30.2(@tiptap/extension-list@3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)) + '@tiptap/extension-paragraph': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-strike': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-text': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extension-underline': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2)) + '@tiptap/extensions': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + + '@tiptap/vue-3@3.30.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.30.2(@tiptap/pm@3.30.2) + '@tiptap/pm': 3.30.2 + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.30.2(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tiptap/extension-floating-menu': 3.30.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2) + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2954,12 +3727,30 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + + '@types/marked@6.0.0': + dependencies: + marked: 17.0.1 + '@types/node@25.0.8': dependencies: undici-types: 7.16.0 + '@types/parse-json@4.0.2': {} + '@types/prismjs@1.26.5': {} + '@types/sortablejs@1.15.9': {} + + '@types/tinycolor2@1.4.6': {} + + '@types/validator@13.15.10': {} + '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': @@ -3383,6 +4174,12 @@ snapshots: - debug - supports-color + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.7 + cosmiconfig: 7.1.0 + resolve: 1.22.8 + babel-plugin-prismjs@2.1.0(prismjs@1.30.0): dependencies: prismjs: 1.30.0 @@ -3437,6 +4234,8 @@ snapshots: dependencies: readdirp: 5.0.0 + clsx@1.2.1: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -3470,6 +4269,8 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} copy-anything@2.0.6: @@ -3480,16 +4281,30 @@ snapshots: dependencies: is-what: 5.5.0 + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + css-in-js-utils@3.1.0: + dependencies: + hyphenate-style-name: 1.1.0 + cssesc@3.0.0: {} csstype@3.2.3: {} + dayjs@1.11.23: {} + de-indent@1.0.2: {} debug@4.3.4: @@ -3542,6 +4357,10 @@ snapshots: prr: 1.0.1 optional: true + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -3693,6 +4512,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + find-root@1.1.0: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3813,6 +4634,8 @@ snapshots: transitivePeerDependencies: - supports-color + hyphenate-style-name@1.1.0: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -3836,6 +4659,12 @@ snapshots: ini@1.3.8: {} + inline-style-prefixer@7.0.1: + dependencies: + css-in-js-utils: 3.1.0 + + is-arrayish@0.2.1: {} + is-core-module@2.13.1: dependencies: hasown: 2.0.2 @@ -3899,6 +4728,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -3987,6 +4818,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lines-and-columns@1.2.4: {} + + linkifyjs@4.3.3: {} + local-pkg@1.1.1: dependencies: mlly: 1.7.4 @@ -4003,6 +4838,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.18.1: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -4144,6 +4981,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + orderedmap@2.1.1: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4158,6 +4997,13 @@ snapshots: dependencies: callsites: 3.1.0 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse-node-version@1.0.1: {} path-browserify@1.0.1: {} @@ -4173,6 +5019,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + path-type@4.0.0: {} + pathe@2.0.3: {} perfect-debounce@1.0.0: {} @@ -4248,6 +5096,80 @@ snapshots: prismjs@1.30.0: {} + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.3: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.2 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.11: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.11 + + prosemirror-view@1.42.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + proto-list@1.2.4: {} proxy-from-env@2.1.0: {} @@ -4331,6 +5253,8 @@ snapshots: fsevents: 2.3.3 optional: true + rope-sequence@1.3.4: {} + safer-buffer@2.1.2: optional: true @@ -4362,6 +5286,8 @@ snapshots: signal-exit@4.1.0: {} + sortablejs@1.15.7: {} + source-map-js@1.2.0: {} source-map-js@1.2.1: {} @@ -4371,6 +5297,8 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.5.7: {} + source-map@0.6.1: {} sourcemap-codec@1.4.8: {} @@ -4411,6 +5339,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + stylis@4.2.0: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 @@ -4433,6 +5363,28 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tdesign-icons-vue-next@0.4.10(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@babel/runtime': 7.29.7 + vue: 3.5.26(typescript@5.9.3) + + tdesign-vue-next@1.20.6(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@babel/runtime': 7.29.7 + '@popperjs/core': 2.11.8 + '@types/lodash-es': 4.17.12 + '@types/sortablejs': 1.15.9 + '@types/tinycolor2': 1.4.6 + '@types/validator': 13.15.10 + dayjs: 1.11.23 + lodash-es: 4.18.1 + mitt: 3.0.1 + sortablejs: 1.15.7 + tdesign-icons-vue-next: 0.4.10(vue@3.5.26(typescript@5.9.3)) + tinycolor2: 1.6.0 + validator: 13.15.35 + vue: 3.5.26(typescript@5.9.3) + terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.6 @@ -4442,6 +5394,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@1.1.1: {} tinyglobby@0.2.15: @@ -4538,6 +5492,8 @@ snapshots: util-deprecate@1.0.2: {} + validator@13.15.35: {} + vite-plugin-dts@4.5.4(@types/node@25.0.8)(rollup@4.55.1)(typescript@5.9.3)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.8)(less@4.5.1)(terser@5.44.1)): dependencies: '@microsoft/api-extractor': 7.50.1(@types/node@25.0.8) @@ -4648,6 +5604,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + w3c-keyname@2.2.8: {} + webpack-virtual-modules@0.6.2: {} whatwg-mimetype@3.0.0: {} @@ -4681,4 +5639,6 @@ snapshots: yallist@4.0.0: {} + yaml@1.10.3: {} + yocto-queue@0.1.0: {} diff --git a/src/api/ArticleAPI.ts b/src/api/ArticleAPI.ts index 9e03b27..3f32b68 100644 --- a/src/api/ArticleAPI.ts +++ b/src/api/ArticleAPI.ts @@ -1,4 +1,4 @@ -import { ArticleView } from "../types"; +import type { Article } from "../types"; import { axios } from "./BaseAPI"; /** @@ -7,7 +7,7 @@ import { axios } from "./BaseAPI"; * @param id 文章 ID * @returns 文章数据 */ -async function view(id: string): Promise> { +async function view(id: string): Promise
{ return axios.get(`/article/${id}`); } @@ -23,5 +23,5 @@ async function like(id: string): Promise { export default { view, - like, + like }; diff --git a/src/api/NotifyAPI.ts b/src/api/NotifyAPI.ts index a5aac64..da8db85 100644 --- a/src/api/NotifyAPI.ts +++ b/src/api/NotifyAPI.ts @@ -3,6 +3,13 @@ import type { NotifyDetail, Page, PageResult } from "../types"; const BASE_URI = "/notify/internal"; +/** 查询当前用户的站内通知详情。 */ +async function internalDetail(id: string): Promise { + return await axios.post(`${BASE_URI}/detail`, undefined, { + params: { id } + }); +} + /** 查询当前用户的站内通知。 */ async function listInternal(page?: Page, unreadOnly = false): Promise> { return await axios.post(`${BASE_URI}/internal`, page, { @@ -28,6 +35,7 @@ async function markAllInternalRead(): Promise { } export default { + internalDetail, listInternal, countUnreadInternal, markInternalRead, diff --git a/src/components/article-editor/index.ts b/src/components/article-editor/index.ts new file mode 100644 index 0000000..8147987 --- /dev/null +++ b/src/components/article-editor/index.ts @@ -0,0 +1,13 @@ +import view from "./index.vue"; +import Toolkit from "../../utils/Toolkit"; +import type { Article } from "../../types"; + +export * from "./shared"; +export type ArticleEditorExpose = { + submit: () => Promise
; + getPendingMediaCount: () => number; +}; + +export const ArticleEditor = Toolkit.withInstall(view); +export default ArticleEditor; + diff --git a/src/components/article-editor/index.vue b/src/components/article-editor/index.vue new file mode 100644 index 0000000..4e47cad --- /dev/null +++ b/src/components/article-editor/index.vue @@ -0,0 +1,289 @@ + + + diff --git a/src/components/article-editor/shared.ts b/src/components/article-editor/shared.ts new file mode 100644 index 0000000..a5cb84f --- /dev/null +++ b/src/components/article-editor/shared.ts @@ -0,0 +1,138 @@ +import type { ArticleContentType, Attachment } from "../../types"; +import { ATTACHMENT_ID_ATTR, TEMP_FILE_ID_ATTR, isLocalMediaId, normalizeContent } from "../tiptap-editor/shared"; + +export const ATTACHMENT_ID_PREFIX = "attachment-id:"; +export const TEMP_FILE_ID_PREFIX = "temp-file-id:"; +export const ARTICLE_IMAGE_ATTACH_TYPE = "CONTENT_IMAGE"; +export const ARTICLE_VIDEO_ATTACH_TYPE = "CONTENT_VIDEO"; +export const DEFAULT_ARTICLE_CONTENT_TYPE: ArticleContentType = "TIP_TAP"; +export const ARTICLE_CONTENT_TYPE_LIST: ArticleContentType[] = ["TEXT", "MARKDOWN", "TIP_TAP"]; + +export { ATTACHMENT_ID_ATTR, TEMP_FILE_ID_ATTR, normalizeContent }; + +export function normalizeArticleContentType(contentType?: string): ArticleContentType { + if (contentType === "TIPTAP") { + return "TIP_TAP"; + } + return ARTICLE_CONTENT_TYPE_LIST.includes(contentType as ArticleContentType) ? contentType as ArticleContentType : DEFAULT_ARTICLE_CONTENT_TYPE; +} + +export function normalizeRawArticleContent(content?: string) { + return content || ""; +} + +export function getAttachmentIdFromSrc(src?: string) { + if (!src?.startsWith(ATTACHMENT_ID_PREFIX)) { + return ""; + } + return src.slice(ATTACHMENT_ID_PREFIX.length); +} + +export function getTempFileIdFromSrc(src?: string) { + if (!src?.startsWith(TEMP_FILE_ID_PREFIX)) { + return ""; + } + return src.slice(TEMP_FILE_ID_PREFIX.length); +} + +export function buildAttachmentIdSrc(attachmentId: string) { + return `${ATTACHMENT_ID_PREFIX}${attachmentId}`; +} + +function getMediaAttachType(tagName: string) { + return tagName.toLowerCase() === "video" ? ARTICLE_VIDEO_ATTACH_TYPE : ARTICLE_IMAGE_ATTACH_TYPE; +} + +function buildAttachmentMetadata(src: string) { + if (!src.startsWith("blob:")) { + return undefined; + } + return { + previewUrl: src + }; +} + +function normalizeAttachmentTempFileId(tempFileId?: string) { + if (!tempFileId || isLocalMediaId(tempFileId)) { + return undefined; + } + return tempFileId; +} + +export function buildAttachmentListFromContent(content?: string) { + const rawContent = normalizeContent(content); + if (!rawContent) { + return [] as Attachment[]; + } + const parser = new DOMParser(); + const doc = parser.parseFromString(rawContent, "text/html"); + const mediaList = Array.from(doc.querySelectorAll("img, video")); + const attachmentMap = new Map(); + + for (const media of mediaList) { + const src = media.getAttribute("src") || ""; + const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src); + const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src); + const key = attachmentId || tempFileId; + if (!key || attachmentMap.has(key)) { + continue; + } + attachmentMap.set(key, { + id: attachmentId || key, + tempFileId: normalizeAttachmentTempFileId(tempFileId), + name: media.getAttribute("title") || media.getAttribute("alt") || "", + attachType: getMediaAttachType(media.tagName), + metadata: buildAttachmentMetadata(src) + }); + } + + return Array.from(attachmentMap.values()); +} + +export function normalizeContentForStore(content?: string) { + const rawContent = normalizeContent(content); + if (!rawContent) { + return ""; + } + const parser = new DOMParser(); + const doc = parser.parseFromString(rawContent, "text/html"); + const mediaList = Array.from(doc.querySelectorAll("img, video")); + for (const media of mediaList) { + const src = media.getAttribute("src") || ""; + const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src); + const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src); + const finalAttachmentId = attachmentId || tempFileId; + if (!finalAttachmentId) { + continue; + } + media.setAttribute(ATTACHMENT_ID_ATTR, finalAttachmentId); + media.removeAttribute(TEMP_FILE_ID_ATTR); + media.setAttribute("src", buildAttachmentIdSrc(finalAttachmentId)); + } + return normalizeContent(doc.body.innerHTML); +} + +export function sameAttachmentList(left?: Attachment[], right?: Attachment[]) { + const leftList = left || []; + const rightList = right || []; + if (leftList.length !== rightList.length) { + return false; + } + for (let i = 0; i < leftList.length; i += 1) { + const leftItem = leftList[i]; + const rightItem = rightList[i]; + if (!rightItem) { + return false; + } + if (leftItem.id !== rightItem.id) { + return false; + } + if ((leftItem.tempFileId || "") !== (rightItem.tempFileId || "")) { + return false; + } + if ((leftItem.accessKey?.value || "") !== (rightItem.accessKey?.value || "")) { + return false; + } + } + return true; +} diff --git a/src/components/article-view/index.ts b/src/components/article-view/index.ts new file mode 100644 index 0000000..3e31ab1 --- /dev/null +++ b/src/components/article-view/index.ts @@ -0,0 +1,6 @@ +import view from "./index.vue"; +import Toolkit from "../../utils/Toolkit"; + +export const ArticleView = Toolkit.withInstall(view); +export default ArticleView; + diff --git a/src/components/article-view/index.vue b/src/components/article-view/index.vue new file mode 100644 index 0000000..ffb6e17 --- /dev/null +++ b/src/components/article-view/index.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/components/code-editor/index.ts b/src/components/code-editor/index.ts new file mode 100644 index 0000000..be39c3d --- /dev/null +++ b/src/components/code-editor/index.ts @@ -0,0 +1,26 @@ +import view from "./index.vue"; +import Toolkit from "../../utils/Toolkit"; +import "./style.less"; + +export type CodeEditorScrollState = { + top: number; + left: number; +}; + +export type CodeEditorExpose = { + focus: () => void; + blur: () => void; + getScrollState: () => CodeEditorScrollState; + setScrollState: (state: Partial) => void; + findNext: () => boolean; + findPrevious: () => boolean; + replaceCurrent: () => boolean; + replaceAll: () => number; + openSearch: (showReplace: boolean) => void; + closeSearch: () => void; + textareaRef?: HTMLTextAreaElement; +}; + +export const CodeEditor = Toolkit.withInstall(view); +export default CodeEditor; + diff --git a/src/components/code-editor/index.vue b/src/components/code-editor/index.vue new file mode 100644 index 0000000..e3649a9 --- /dev/null +++ b/src/components/code-editor/index.vue @@ -0,0 +1,450 @@ + + + + + diff --git a/src/components/code-editor/input.ts b/src/components/code-editor/input.ts new file mode 100644 index 0000000..e06b97e --- /dev/null +++ b/src/components/code-editor/input.ts @@ -0,0 +1,208 @@ +type EditorSelection = { + start: number; + end: number; +}; + +type EditorEditResult = EditorSelection & { + value: string; +}; + +const TAB = "\t"; +const OPEN_CHAR_LIST = new Set(["(", "[", "{"]); +const WRAP_CHAR_MAP: Record = { + "(": ")", + "[": "]", + "{": "}", + "\"": "\"", + "'": "'", + "`": "`", + "<": ">" +}; +const CLOSE_CHAR_LIST = new Set(Object.values(WRAP_CHAR_MAP)); + +export const resolveCodeEditorKeydown = ( + value: string, + selection: EditorSelection, + event: KeyboardEvent +): EditorEditResult | undefined => { + if (event.isComposing || event.ctrlKey || event.altKey || event.metaKey) { + return undefined; + } + if (event.key === "Tab") { + return resolveTabInput(value, selection, event.shiftKey); + } + if (event.key === "Enter") { + return resolveEnterInput(value, selection); + } + if (event.key === "Backspace") { + return resolvePairBackspace(value, selection); + } + return resolvePairInput(value, selection, event.key); +}; + +// 处理 Tab / Shift+Tab 缩进 +const resolveTabInput = ( + value: string, + selection: EditorSelection, + shiftKey: boolean +): EditorEditResult => { + const { start, end } = selection; + + if (!shiftKey && start === end) { + return { + value: value.slice(0, start) + TAB + value.slice(end), + start: start + 1, + end: start + 1 + }; + } + + const lineStart = value.lastIndexOf("\n", Math.max(start - 1, 0)) + 1; + const lineEndIndex = value.indexOf("\n", end); + const blockEnd = lineEndIndex < 0 ? value.length : lineEndIndex; + const selectedBlock = value.slice(lineStart, blockEnd); + const lineList = selectedBlock.split("\n"); + + let nextStart = start; + let nextEnd = end; + const nextBlock = lineList.map((line, index) => { + if (shiftKey) { + if (line.startsWith(TAB)) { + if (index === 0) { + nextStart = Math.max(lineStart, nextStart - 1); + } + nextEnd = Math.max(nextStart, nextEnd - 1); + return line.slice(1); + } + return line; + } + if (index === 0) { + nextStart += 1; + } + nextEnd += 1; + return TAB + line; + }).join("\n"); + + return { + value: value.slice(0, lineStart) + nextBlock + value.slice(blockEnd), + start: nextStart, + end: nextEnd + }; +}; + +// 回车时继承当前行缩进,位于块级起始括号后时额外增加一层缩进 +const resolveEnterInput = ( + value: string, + selection: EditorSelection +): EditorEditResult => { + const { start, end } = selection; + const lineStart = value.lastIndexOf("\n", Math.max(start - 1, 0)) + 1; + const lineIndent = getLineIndent(value.slice(lineStart, start)); + const prevChar = value[start - 1] ?? ""; + const nextChar = value[end] ?? ""; + const isWrappedBlock = start === end + && OPEN_CHAR_LIST.has(prevChar) + && WRAP_CHAR_MAP[prevChar] === nextChar; + + if (isWrappedBlock) { + const nextValue = value.slice(0, start) + + `\n${lineIndent}${TAB}\n${lineIndent}` + + value.slice(end); + const nextCursor = start + 1 + lineIndent.length + TAB.length; + + return { + value: nextValue, + start: nextCursor, + end: nextCursor + }; + } + + const extraIndent = OPEN_CHAR_LIST.has(prevChar) ? TAB : ""; + const nextValue = value.slice(0, start) + + `\n${lineIndent}${extraIndent}` + + value.slice(end); + const nextCursor = start + 1 + lineIndent.length + extraIndent.length; + + return { + value: nextValue, + start: nextCursor, + end: nextCursor + }; +}; + +// 处理成对符号输入,选中文本时直接包裹 +const resolvePairInput = ( + value: string, + selection: EditorSelection, + key: string +): EditorEditResult | undefined => { + const wrapChar = WRAP_CHAR_MAP[key]; + if (wrapChar) { + return wrapSelection(value, selection, key, wrapChar); + } + + if (CLOSE_CHAR_LIST.has(key) && selection.start === selection.end && value[selection.start] === key) { + return { + value, + start: selection.start + 1, + end: selection.start + 1 + }; + } + + return undefined; +}; + +// 光标位于成对符号中间时,退格一次同时删除左右字符 +const resolvePairBackspace = ( + value: string, + selection: EditorSelection +): EditorEditResult | undefined => { + const { start, end } = selection; + if (start !== end || start < 1) { + return undefined; + } + + const prevChar = value[start - 1]; + const nextChar = value[start]; + if (WRAP_CHAR_MAP[prevChar] !== nextChar) { + return undefined; + } + + return { + value: value.slice(0, start - 1) + value.slice(start + 1), + start: start - 1, + end: start - 1 + }; +}; + +// 将当前选区替换为一对包裹字符,并把光标移动到合理位置 +const wrapSelection = ( + value: string, + selection: EditorSelection, + openChar: string, + closeChar: string +): EditorEditResult => { + const { start, end } = selection; + const selectedText = value.slice(start, end); + const nextValue = value.slice(0, start) + openChar + selectedText + closeChar + value.slice(end); + + if (start === end) { + return { + value: nextValue, + start: start + 1, + end: start + 1 + }; + } + + return { + value: nextValue, + start: start + 1, + end: end + 1 + }; +}; + +const getLineIndent = (value: string) => { + const match = value.match(/^[\t ]*/); + + return match?.[0] ?? ""; +}; + diff --git a/src/components/code-editor/style.less b/src/components/code-editor/style.less new file mode 100644 index 0000000..b9453f4 --- /dev/null +++ b/src/components/code-editor/style.less @@ -0,0 +1,202 @@ +.tui-code-editor { + width: calc(100% - 2px); + min-width: 0; + border: 1px solid var(--td-component-stroke, #B8BBC9); + display: flex; + overflow: hidden; + background: transparent; + border-radius: var(--td-radius-medium, 0); + + .gutter { + flex: none; + color: var(--td-text-color-placeholder, #999); + overflow: hidden; + text-align: right; + background: #F2F2F2E6; + border-right: 1px solid var(--td-component-stroke, #B8BBC9); + + > span { + width: 3.25rem; + height: 1.5rem; + display: block; + font-size: 1rem; + font-family: "JetBrains Mono", "Fira Code", Consolas, "Courier New", monospace; + line-height: 1.5rem; + padding-right: .25rem; + } + } + + .body { + flex: 1; + min-width: 0; + min-height: 0; + position: relative; + overflow: hidden; + } + + .search-bar { + top: .5rem; + right: .5rem; + gap: .5rem; + z-index: 3; + display: flex; + padding: .5rem; + position: absolute; + align-items: center; + border-radius: var(--td-radius-default, .375rem); + background: var(--td-bg-color-container, #fff); + border: 1px solid var(--td-component-stroke, #B8BBC9); + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .12); + } + + .search-input { + width: 10rem; + height: 2rem; + padding: 0 .5rem; + color: var(--td-text-color-primary, #111); + border: 1px solid var(--td-component-stroke, #B8BBC9); + background: var(--td-bg-color-container, #fff); + border-radius: var(--td-radius-default, .375rem); + font: inherit; + } + + .search-actions { + gap: .25rem; + display: flex; + align-items: center; + } + + .search-button { + height: 2rem; + padding: 0 .625rem; + color: var(--td-text-color-primary, #111); + border: 1px solid var(--td-component-stroke, #B8BBC9); + background: var(--td-bg-color-container, #fff); + border-radius: var(--td-radius-default, .375rem); + cursor: pointer; + + &:hover { + background: var(--td-bg-color-container-hover, #f6f6f6); + } + } + + .search-status { + color: var(--td-text-color-secondary, #666); + white-space: nowrap; + font-size: .875rem; + } + + .preview, + .input { + inset: 0; + width: 100%; + height: 100%; + margin: 0; + tab-size: 4; + overflow: auto; + max-width: 100%; + min-width: 0; + line-height: 1.5rem; + font-size: 1rem; + box-sizing: border-box; + font-family: "JetBrains Mono", "Fira Code", Consolas, "Courier New", monospace; + } + + .preview { + top: 0; + left: 0; + border: none; + position: absolute; + word-break: normal; + background: transparent; + transition: none; + white-space: pre; + border-radius: 0; + pointer-events: none; + + code { + padding: 0; + display: block; + width: max-content; + min-width: 100%; + min-height: 100%; + color: inherit; + background: transparent; + text-shadow: none; + font-size: inherit; + line-height: inherit; + font-family: inherit; + } + } + + .input { + top: 0; + left: 0; + color: transparent; + resize: none; + border: none; + outline: none; + position: absolute; + background: transparent; + white-space: pre; + word-break: normal; + overflow-wrap: normal; + caret-color: var(--td-text-color-primary, #111); + -webkit-text-fill-color: transparent; + } + + .input::placeholder { + color: var(--td-text-color-placeholder, #999); + -webkit-text-fill-color: var(--td-text-color-placeholder, #999); + } + + .input::selection { + background: rgb(0 122 255 / 18%); + } + + &.disabled { + opacity: .65; + } + + &.readonly { + .preview { + pointer-events: auto; + } + } +} + +@media (prefers-color-scheme: dark) { + .tui-code-editor { + border-color: var(--td-component-stroke, #434343); + + .gutter { + border-right-color: var(--td-component-stroke, #434343); + } + + .search-bar { + background: var(--td-bg-color-container, #1f1f1f); + border-color: var(--td-component-stroke, #434343); + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .28); + } + + .search-input, + .search-button { + color: var(--td-text-color-primary, #F5F5F5); + border-color: var(--td-component-stroke, #434343); + background: var(--td-bg-color-container, #1f1f1f); + } + + .search-button:hover { + background: var(--td-bg-color-container-hover, #2b2b2b); + } + + .input { + caret-color: var(--td-text-color-primary, #F5F5F5); + } + + .input::selection { + background: rgb(64 153 255 / 28%); + } + } +} + diff --git a/src/components/editor-search/index.vue b/src/components/editor-search/index.vue new file mode 100644 index 0000000..fb71e40 --- /dev/null +++ b/src/components/editor-search/index.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/src/components/editor-search/search.ts b/src/components/editor-search/search.ts new file mode 100644 index 0000000..2ae43bc --- /dev/null +++ b/src/components/editor-search/search.ts @@ -0,0 +1,134 @@ +export type TextSearchOptions = { + caseSensitive: boolean; + useRegex: boolean; +}; + +export type TextSearchMatch = { + start: number; + end: number; + text: string; +}; + +export type TextSearchResult = { + matches: TextSearchMatch[]; + error: string; +}; + +const buildFlags = (options: TextSearchOptions, global = true) => { + let flags = global ? "g" : ""; + if (!options.caseSensitive) { + flags += "i"; + } + return flags; +}; + +export const escapeSearchKeyword = (value: string) => ( + value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +); + +export const buildSearchRegex = ( + keyword: string, + options: TextSearchOptions, + global = true +) => { + if (!keyword) { + return { regex: undefined, error: "" }; + } + try { + const source = options.useRegex ? keyword : escapeSearchKeyword(keyword); + const regex = new RegExp(source, buildFlags(options, global)); + const emptyMatch = "".match(regex); + if (emptyMatch && emptyMatch[0] === "") { + return { + regex: undefined, + error: "正则不能匹配空文本" + }; + } + return { regex, error: "" }; + } catch { + return { + regex: undefined, + error: "正则表达式无效" + }; + } +}; + +export const findTextMatches = ( + value: string, + keyword: string, + options: TextSearchOptions +): TextSearchResult => { + const { regex, error } = buildSearchRegex(keyword, options, true); + if (!regex || error) { + return { + matches: [], + error + }; + } + const matches: TextSearchMatch[] = []; + let match = regex.exec(value); + while (match) { + if (!match[0]) { + return { + matches: [], + error: "正则不能匹配空文本" + }; + } + matches.push({ + start: match.index, + end: match.index + match[0].length, + text: match[0] + }); + match = regex.exec(value); + } + return { + matches, + error: "" + }; +}; + +export const replaceMatchText = ( + matchText: string, + keyword: string, + replaceText: string, + options: TextSearchOptions +) => { + if (!options.useRegex) { + return replaceText; + } + const { regex, error } = buildSearchRegex(keyword, options, false); + if (!regex || error) { + return matchText; + } + return matchText.replace(regex, replaceText); +}; + +export const replaceAllText = ( + value: string, + keyword: string, + replaceText: string, + options: TextSearchOptions +) => { + const { matches, error } = findTextMatches(value, keyword, options); + if (error || 1 > matches.length) { + return { + value, + count: 0, + error + }; + } + let nextValue = ""; + let lastEnd = 0; + matches.forEach((match) => { + nextValue += value.slice(lastEnd, match.start); + nextValue += replaceMatchText(match.text, keyword, replaceText, options); + lastEnd = match.end; + }); + nextValue += value.slice(lastEnd); + return { + value: nextValue, + count: matches.length, + error: "" + }; +}; + diff --git a/src/components/index.ts b/src/components/index.ts index e3ceda5..ee42ce7 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -11,6 +11,11 @@ import BEFlowerFall from "./background-effect/flower-fall"; import MarkdownEditor from "./markdown-editor"; import PassTimeLabel from "./passtime-label"; import ContactNumber from "./contact-number"; +import CodeEditor from "./code-editor"; +import TiptapEditor from "./tiptap-editor"; +import TiptapView from "./tiptap-view"; +import ArticleEditor from "./article-editor"; +import ArticleView from "./article-view"; export default [ Icon, @@ -24,7 +29,12 @@ export default [ BEFlowerFall, MarkdownEditor, PassTimeLabel, - ContactNumber + ContactNumber, + CodeEditor, + TiptapEditor, + TiptapView, + ArticleEditor, + ArticleView ]; export { @@ -39,5 +49,25 @@ export { BEFlowerFall, MarkdownEditor, PassTimeLabel, - ContactNumber + ContactNumber, + CodeEditor, + TiptapEditor, + TiptapView, + ArticleEditor, + ArticleView }; + +export type { + CodeEditorExpose +} from "./code-editor"; + +export type { + UploadResult, + AlignType, + TiptapEditorExpose, + TiptapEditorInstance +} from "./tiptap-editor"; + +export type { + ArticleEditorExpose +} from "./article-editor"; diff --git a/src/components/prism-code-block.less b/src/components/prism-code-block.less new file mode 100644 index 0000000..5c29f81 --- /dev/null +++ b/src/components/prism-code-block.less @@ -0,0 +1,181 @@ +.tui-markdown-view, +.tui-code-editor, +.tp-content-body { + img, + video, + iframe.tp-iframe { + max-width: 100%; + display: block; + margin: 1rem auto; + } + + img { + height: auto; + border-radius: var(--td-radius-default, .375rem); + } + + video, + iframe.tp-iframe { + width: min(100%, 56rem); + overflow: hidden; + border-radius: var(--td-radius-default, .375rem); + background: var(--td-bg-color-container, #fff); + border: 1px solid var(--td-component-border, #dcdcdc); + } + + iframe.tp-iframe { + height: 28rem; + } + + table { + width: 100%; + margin: 1rem 0; + border-spacing: 0; + border-collapse: collapse; + } + + th, + td { + padding: .5rem .75rem; + vertical-align: middle; + border: 1px solid var(--td-component-border, #dcdcdc); + } + + th { + background: var(--td-bg-color-container-hover, #f6f6f6); + } + + pre[class*="language-"] { + padding: 0; + border: 1px solid var(--td-component-stroke, #B8BBC9); + overflow: auto; + background: transparent; + transition: max-height .5s ease; + border-radius: var(--td-radius-medium, 0); + + code { + color: #333; + margin: 0; + padding: 0; + display: block; + font-size: 1rem; + background: transparent; + text-shadow: none; + line-height: 1.5; + font-family: "JetBrains Mono", "Fira Code", Consolas, "Courier New", monospace; + + .line-numbers-rows { + left: 0; + flex: none; + width: 3.25rem; + display: block; + text-align: right; + background: #F2F2F2E6; + border-right: 1px solid var(--td-component-stroke, #B8BBC9); + padding-right: .25rem; + + > span { + color: var(--td-text-color-placeholder, #999); + display: block; + } + + > span::before { + content: none; + } + } + + .codes { + display: block; + min-width: max-content; + padding-left: .25rem; + } + } + } + + pre[class*="language-"].has-line-numbers { + code { + padding: 0; + display: inline-flex; + min-width: 100%; + align-items: stretch; + } + + .line-numbers-rows { + left: 0; + z-index: 1; + position: sticky; + } + + .codes { + flex: 1; + } + } + + .token.namespace { + opacity: 1; + } + + .token.punctuation { + color: #333; + } + + .token.constant { + color: #FF7A9B; + } + + .token.annotation { + color: purple; + } + + .token.function { + color: #777; + } + + .token.class-name { + color: #FF461F; + } + + .token.generics .class-name { + color: #895532; + font-weight: bold; + } + + .token.comment { + color: #999; + } + + .token.string { + color: #55AA55; + } + + .token.number { + color: #EB9354; + } + + .token.keyword, + .token.boolean { + color: #177CB0; + } +} + +.tui-markdown-view pre[class*="language-"], +.tp-content-body pre[class*="language-"] { + position: relative; +} + +.tp-editor-body { + + pre[class*="language-"] { + max-height: 24rem; + } +} + +.tui-code-editor { + + pre[class*="language-"] { + border: none; + padding: 2px; + } +} + + diff --git a/src/components/tiptap-content.less b/src/components/tiptap-content.less new file mode 100644 index 0000000..73ffa63 --- /dev/null +++ b/src/components/tiptap-content.less @@ -0,0 +1,46 @@ +.tp-content-theme() { + color: var(--td-text-color-primary); + line-height: 1.6; + + p { + margin: 0 0 .75rem; + } + + p:empty:before { + content: "\00a0"; + } + + ul, + ol { + margin: 0 0 .75rem; + padding-left: 1.25rem; + } + + table { + width: 100%; + margin: .75rem 0; + table-layout: fixed; + border-collapse: collapse; + } + + th, + td { + border: 1px solid var(--td-component-border); + } + + th { + background: var(--td-bg-color-secondarycontainer); + } + + img, + video, + iframe { + margin: 0 auto; + max-width: 100%; + } +} + +.tp-content-body { + .tp-content-theme(); +} + diff --git a/src/components/tiptap-editor/PrismCodeBlock.ts b/src/components/tiptap-editor/PrismCodeBlock.ts new file mode 100644 index 0000000..571a860 --- /dev/null +++ b/src/components/tiptap-editor/PrismCodeBlock.ts @@ -0,0 +1,174 @@ +import CodeBlock from "@tiptap/extension-code-block"; +import { findChildren, mergeAttributes } from "@tiptap/vue-3"; +import type { Node as ProsemirrorNode } from "@tiptap/pm/model"; +import type { EditorState, Transaction } from "@tiptap/pm/state"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Decoration, DecorationSet } from "@tiptap/pm/view"; +import Prism from "prismjs"; +import { resolvePrismLanguage } from "../../utils/Prism"; + +type HighlightNode = { + text: string; + classes: string[]; +}; + +type PrismToken = { + type: string; + alias?: string | string[]; + content: string | PrismToken | Array; +}; + +type StepRange = { + from?: number; + to?: number; +}; + +function buildLanguageClassName(languageClassPrefix: string | null | undefined, language?: string | null) { + if (!languageClassPrefix || !language) { + return null; + } + return `${languageClassPrefix}${language}`; +} + +function buildTokenClassList(type: string, alias?: string | string[]) { + const aliasList = Array.isArray(alias) ? alias : alias ? [alias] : []; + return ["token", type, ...aliasList]; +} + +function parsePrismTokens(tokens: Array, className: string[] = []): HighlightNode[] { + return tokens.flatMap((token) => { + if (typeof token === "string") { + return [{ + text: token, + classes: className + }]; + } + + const classes = [...className, ...buildTokenClassList(token.type, token.alias)]; + if (typeof token.content === "string") { + return [{ + text: token.content, + classes + }]; + } + if (Array.isArray(token.content)) { + return parsePrismTokens(token.content, classes); + } + return parsePrismTokens([token.content], classes); + }); +} + +function getDecorations({ + doc, + name, + defaultLanguage +}: { + doc: ProsemirrorNode; + name: string; + defaultLanguage: string | null | undefined; +}) { + const decorations: Decoration[] = []; + + findChildren(doc, node => node.type.name === name).forEach((block) => { + let from = block.pos + 1; + const prismLanguage = resolvePrismLanguage(block.node.attrs.language || defaultLanguage); + const grammar = prismLanguage ? Prism.languages[prismLanguage] : null; + const tokens = grammar ? Prism.tokenize(block.node.textContent, grammar) : [block.node.textContent]; + + parsePrismTokens(tokens).forEach((node) => { + const to = from + node.text.length; + if (node.classes.length) { + decorations.push(Decoration.inline(from, to, { + class: node.classes.join(" ") + })); + } + from = to; + }); + }); + + return DecorationSet.create(doc, decorations); +} + +function createPrismPlugin({ + name, + defaultLanguage +}: { + name: string; + defaultLanguage: string | null | undefined; +}) { + const prismPlugin: Plugin = new Plugin({ + key: new PluginKey("prism"), + state: { + init: (_: unknown, { doc }: EditorState) => getDecorations({ + doc, + name, + defaultLanguage + }), + apply: (transaction: Transaction, decorationSet: DecorationSet, oldState: EditorState, newState: EditorState) => { + const oldNodeName = oldState.selection.$head.parent.type.name; + const newNodeName = newState.selection.$head.parent.type.name; + const oldNodes = findChildren(oldState.doc, node => node.type.name === name); + const newNodes = findChildren(newState.doc, node => node.type.name === name); + + if ( + transaction.docChanged + && ( + [oldNodeName, newNodeName].includes(name) + || newNodes.length !== oldNodes.length + || transaction.steps.some((step) => { + const range = step as StepRange; + return range.from !== undefined + && range.to !== undefined + && oldNodes.some(node => node.pos >= range.from! && node.pos + node.node.nodeSize <= range.to!); + }) + ) + ) { + return getDecorations({ + doc: transaction.doc, + name, + defaultLanguage + }); + } + + return decorationSet.map(transaction.mapping, transaction.doc); + } + }, + props: { + decorations(state: EditorState) { + return prismPlugin.getState(state); + } + } + }); + + return prismPlugin; +} + +export default CodeBlock.extend({ + renderHTML({ node, HTMLAttributes }: { node: ProsemirrorNode; HTMLAttributes: Record }) { + const languageClassName = buildLanguageClassName(this.options.languageClassPrefix, node.attrs.language); + + return [ + "pre", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + class: languageClassName + }), + [ + "code", + { + class: languageClassName + }, + 0 + ] + ]; + }, + + addProseMirrorPlugins() { + return [ + createPrismPlugin({ + name: this.name, + defaultLanguage: this.options.defaultLanguage + }) + ]; + } +}); + diff --git a/src/components/tiptap-editor/TiptapCodeBlockButton.vue b/src/components/tiptap-editor/TiptapCodeBlockButton.vue new file mode 100644 index 0000000..a72fb61 --- /dev/null +++ b/src/components/tiptap-editor/TiptapCodeBlockButton.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/components/tiptap-editor/TiptapIframeButton.vue b/src/components/tiptap-editor/TiptapIframeButton.vue new file mode 100644 index 0000000..9da1636 --- /dev/null +++ b/src/components/tiptap-editor/TiptapIframeButton.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/components/tiptap-editor/TiptapLinkButton.vue b/src/components/tiptap-editor/TiptapLinkButton.vue new file mode 100644 index 0000000..365e151 --- /dev/null +++ b/src/components/tiptap-editor/TiptapLinkButton.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/tiptap-editor/TiptapMediaButton.vue b/src/components/tiptap-editor/TiptapMediaButton.vue new file mode 100644 index 0000000..b96c0db --- /dev/null +++ b/src/components/tiptap-editor/TiptapMediaButton.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/src/components/tiptap-editor/TiptapPaddingButton.vue b/src/components/tiptap-editor/TiptapPaddingButton.vue new file mode 100644 index 0000000..de6e7a9 --- /dev/null +++ b/src/components/tiptap-editor/TiptapPaddingButton.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/components/tiptap-editor/TiptapTableButton.vue b/src/components/tiptap-editor/TiptapTableButton.vue new file mode 100644 index 0000000..d2d2e0b --- /dev/null +++ b/src/components/tiptap-editor/TiptapTableButton.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/tiptap-editor/TiptapToolbar.vue b/src/components/tiptap-editor/TiptapToolbar.vue new file mode 100644 index 0000000..3677e00 --- /dev/null +++ b/src/components/tiptap-editor/TiptapToolbar.vue @@ -0,0 +1,431 @@ + + + + + + diff --git a/src/components/tiptap-editor/extensions.ts b/src/components/tiptap-editor/extensions.ts new file mode 100644 index 0000000..0ebd371 --- /dev/null +++ b/src/components/tiptap-editor/extensions.ts @@ -0,0 +1,259 @@ +import { mergeAttributes, Node } from "@tiptap/vue-3"; +import Color from "@tiptap/extension-color"; +import Image from "@tiptap/extension-image"; +import Link from "@tiptap/extension-link"; +import Placeholder from "@tiptap/extension-placeholder"; +import Subscript from "@tiptap/extension-subscript"; +import Superscript from "@tiptap/extension-superscript"; +import { Table } from "@tiptap/extension-table"; +import TableCell from "@tiptap/extension-table-cell"; +import TableHeader from "@tiptap/extension-table-header"; +import TableRow from "@tiptap/extension-table-row"; +import TextAlign from "@tiptap/extension-text-align"; +import { TextStyle } from "@tiptap/extension-text-style"; +import Underline from "@tiptap/extension-underline"; +import type { Extensions } from "@tiptap/vue-3"; +import StarterKit from "@tiptap/starter-kit"; +import PrismCodeBlock from "./PrismCodeBlock"; +import { ATTACHMENT_ID_ATTR, TEMP_FILE_ID_ATTR } from "./shared"; + +function buildStyleString(styleList: Array) { + return styleList.filter(Boolean).join("; "); +} + +function buildNodeSizeAttributes(defaultHeight = "auto") { + return { + width: { + default: "100%", + parseHTML: (element: HTMLElement) => element.style.width || "100%" + }, + height: { + default: defaultHeight, + parseHTML: (element: HTMLElement) => element.style.height || defaultHeight + } + }; +} + +function buildNodeSizeStyle(attributes: Record) { + return buildStyleString([ + attributes.style, + attributes.width ? `width: ${attributes.width}` : null, + attributes.height ? `height: ${attributes.height}` : null + ]); +} + +const ExtTextStyle = TextStyle.extend({ + addGlobalAttributes() { + return [ + ...(this.parent?.() || []), + { + types: ["textStyle"], + attributes: { + backgroundColor: { + default: null, + parseHTML: (element: HTMLElement) => element.style.backgroundColor || null, + renderHTML: (attributes: Record) => { + if (!attributes.backgroundColor) { + return {}; + } + return { + style: `background-color: ${attributes.backgroundColor}` + }; + } + } + } + } + ]; + } +}); + +const ExtImage = Image.configure({ + allowBase64: true +}).extend({ + addAttributes() { + return { + ...this.parent?.(), + ...buildNodeSizeAttributes(), + attachmentId: { + default: null, + parseHTML: (element: HTMLElement) => element.getAttribute(ATTACHMENT_ID_ATTR), + renderHTML: (attributes: Record) => { + if (!attributes.attachmentId) { + return {}; + } + return { + [ATTACHMENT_ID_ATTR]: attributes.attachmentId + }; + } + }, + tempFileId: { + default: null, + parseHTML: (element: HTMLElement) => element.getAttribute(TEMP_FILE_ID_ATTR), + renderHTML: (attributes: Record) => { + if (!attributes.tempFileId) { + return {}; + } + return { + [TEMP_FILE_ID_ATTR]: attributes.tempFileId + }; + } + } + }; + }, + + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { + return [ + "img", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + style: buildNodeSizeStyle(HTMLAttributes) + }) + ]; + } +}); + +const ExtVideo = Node.create({ + name: "video", + group: "block", + atom: true, + draggable: true, + selectable: true, + + addAttributes() { + return { + src: { + default: null + }, + title: { + default: null + }, + ...buildNodeSizeAttributes(), + attachmentId: { + default: null, + parseHTML: (element: HTMLElement) => element.getAttribute(ATTACHMENT_ID_ATTR), + renderHTML: (attributes: Record) => { + if (!attributes.attachmentId) { + return {}; + } + return { + [ATTACHMENT_ID_ATTR]: attributes.attachmentId + }; + } + }, + tempFileId: { + default: null, + parseHTML: (element: HTMLElement) => element.getAttribute(TEMP_FILE_ID_ATTR), + renderHTML: (attributes: Record) => { + if (!attributes.tempFileId) { + return {}; + } + return { + [TEMP_FILE_ID_ATTR]: attributes.tempFileId + }; + } + } + }; + }, + + parseHTML() { + return [ + { + tag: "video" + } + ]; + }, + + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { + return [ + "video", + mergeAttributes( + { + controls: "true", + style: buildNodeSizeStyle(HTMLAttributes) + }, + HTMLAttributes + ) + ]; + } +}); + +const ExtIframe = Node.create({ + name: "iframe", + group: "block", + atom: true, + draggable: true, + selectable: true, + + addAttributes() { + return { + src: { + default: null + }, + title: { + default: null + }, + ...buildNodeSizeAttributes("28rem") + }; + }, + + parseHTML() { + return [ + { + tag: "iframe" + } + ]; + }, + + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { + const style = buildNodeSizeStyle(HTMLAttributes); + return [ + "iframe", + mergeAttributes( + { + class: "tp-iframe", + frameborder: "0", + allowfullscreen: "true", + referrerpolicy: "no-referrer-when-downgrade", + style + }, + HTMLAttributes + ) + ]; + } +}); + +export function buildTiptapExtensions(placeholder?: string): Extensions { + return [ + StarterKit.configure({ + codeBlock: false + }), + PrismCodeBlock.configure({ + languageClassPrefix: "language-" + }), + Placeholder.configure({ + placeholder: placeholder || "请输入内容" + }), + ExtImage, + ExtVideo, + ExtIframe, + Link.configure({ + openOnClick: false, + autolink: true, + defaultProtocol: "https" + }), + Underline, + ExtTextStyle, + Color, + Subscript, + Superscript, + Table.configure({ + resizable: true + }), + TableRow, + TableHeader, + TableCell, + TextAlign.configure({ + types: ["heading", "paragraph"] + }) + ]; +} + diff --git a/src/components/tiptap-editor/index.ts b/src/components/tiptap-editor/index.ts new file mode 100644 index 0000000..0f22e77 --- /dev/null +++ b/src/components/tiptap-editor/index.ts @@ -0,0 +1,15 @@ +import view from "./index.vue"; +import Toolkit from "../../utils/Toolkit"; +import type { TiptapEditorInstance } from "./useTiptapEditor"; + +export * from "./shared"; +export type { TiptapEditorInstance } from "./useTiptapEditor"; +export type TiptapEditorExpose = { + submit: () => Promise; + getPendingMediaCount: () => number; + editor?: TiptapEditorInstance; +}; + +export const TiptapEditor = Toolkit.withInstall(view); +export default TiptapEditor; + diff --git a/src/components/tiptap-editor/index.vue b/src/components/tiptap-editor/index.vue new file mode 100644 index 0000000..0ee8e77 --- /dev/null +++ b/src/components/tiptap-editor/index.vue @@ -0,0 +1,617 @@ + + + + + + + + diff --git a/src/components/tiptap-editor/shared.ts b/src/components/tiptap-editor/shared.ts new file mode 100644 index 0000000..75967b8 --- /dev/null +++ b/src/components/tiptap-editor/shared.ts @@ -0,0 +1,81 @@ +import { h } from "vue"; + +export type UploadResult = { + id: string; + url: string; + name?: string; +}; + +export type UploadMediaType = "image" | "video"; +export type PendingMediaItem = { + id: string; + type: UploadMediaType; + name: string; + file: File; + url: string; +}; + +export type AlignType = "left" | "center" | "right" | "justify"; + +export const ATTACHMENT_ID_ATTR = "data-attachment-id"; +export const TEMP_FILE_ID_ATTR = "data-temp-file-id"; +export const TABLE_PICKER_SIZE = 7; +export const LOCAL_MEDIA_ID_PREFIX = "local-media:"; + +export const headingMeta = [ + { label: "正文", value: "p", className: "p" }, + { label: "标题 1", value: "h1", className: "h1" }, + { label: "标题 2", value: "h2", className: "h2" }, + { label: "标题 3", value: "h3", className: "h3" }, + { label: "标题 4", value: "h4", className: "h4" }, + { label: "标题 5", value: "h5", className: "h5" }, + { label: "标题 6", value: "h6", className: "h6" } +] as const; + +export function buildHeadingOptions(currentHeadingValue: string) { + return headingMeta.map((item) => { + const active = currentHeadingValue === item.value; + return { + active, + value: item.value, + content: () => h( + "span", + { class: ["tp-heading-option", item.className] }, + item.label + ) + }; + }); +} + +export function normalizeContent(value?: string) { + const text = (value || "").trim(); + if (!text || text === "

") { + return ""; + } + const parser = new DOMParser(); + const doc = parser.parseFromString(text, "text/html"); + const normalized = doc.body.innerHTML.trim(); + return normalized === "

" ? "" : normalized; +} + +export function normalizePaddingValue(value: unknown, fallback: number) { + const nextValue = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(nextValue)) { + return fallback; + } + return Math.min(Math.max(nextValue, 0), 16); +} + +export function normalizeSliderValue(value: number | number[], fallback: number) { + const nextValue = Array.isArray(value) ? value[0] : value; + return normalizePaddingValue(nextValue, fallback); +} + +export function formatPaddingValue(value: number) { + return `${Number(value.toFixed(2))}rem`; +} + +export function isLocalMediaId(id?: string | null) { + return !!id && id.startsWith(LOCAL_MEDIA_ID_PREFIX); +} + diff --git a/src/components/tiptap-editor/useTiptapEditor.ts b/src/components/tiptap-editor/useTiptapEditor.ts new file mode 100644 index 0000000..d98c7c6 --- /dev/null +++ b/src/components/tiptap-editor/useTiptapEditor.ts @@ -0,0 +1,135 @@ +import type { Editor } from "@tiptap/vue-3"; +import { EditorContent, useEditor } from "@tiptap/vue-3"; +import { buildTiptapExtensions } from "./extensions"; +import { normalizeContent, normalizePaddingValue } from "./shared"; + +type TiptapEditorProps = { + modelValue: string; + paddingTB?: number; + paddingLR?: number; + placeholder?: string; +}; + +type UpdateModelValue = { + (event: "update:modelValue", value: string): void; + (event: "change", value: string): void; + (event: "update:paddingTB", value: number): void; + (event: "update:paddingLR", value: number): void; +}; + +export function useTiptapEditor(props: TiptapEditorProps, emit: UpdateModelValue) { + const articlePadding = reactive({ + paddingTB: normalizePaddingValue(props.paddingTB, 1), + paddingLR: normalizePaddingValue(props.paddingLR, 1.25) + }); + const lastEmittedContent = ref(""); + const pendingEmitContent = ref(null); + + // 代码块输入期间只在编辑器内部累积,离开代码块或失焦后再统一同步 + function flushPendingContent() { + if (pendingEmitContent.value === null) { + return; + } + lastEmittedContent.value = pendingEmitContent.value; + emit("change", pendingEmitContent.value); + emit("update:modelValue", pendingEmitContent.value); + pendingEmitContent.value = null; + } + + function applyExternalContent(nextContent: string) { + if (!editor.value) { + return; + } + editor.value.commands.setContent(nextContent || "

", { + emitUpdate: false + }); + lastEmittedContent.value = nextContent; + } + + const editorBodyStyle = computed(() => { + return { + "--tp-editor-padding-tb": `${articlePadding.paddingTB}rem`, + "--tp-editor-padding-lr": `${articlePadding.paddingLR}rem` + }; + }); + + const editor = useEditor({ + extensions: buildTiptapExtensions(props.placeholder), + content: "", + editorProps: { + attributes: { + class: "editor-body tp-content-body tp-editor-body" + } + }, + onBlur: () => { + flushPendingContent(); + }, + onSelectionUpdate: ({ editor: instance }) => { + if (instance.isActive("codeBlock")) { + return; + } + flushPendingContent(); + }, + onUpdate: ({ editor: instance }) => { + const nextContent = normalizeContent(instance.getHTML()); + lastEmittedContent.value = nextContent; + if (instance.isActive("codeBlock")) { + pendingEmitContent.value = nextContent; + return; + } + pendingEmitContent.value = null; + emit("change", nextContent); + emit("update:modelValue", nextContent); + } + }); + + watch(() => props.modelValue, (value) => { + if (!editor.value) { + return; + } + const nextContent = normalizeContent(value); + const currentContent = normalizeContent(editor.value.getHTML()); + if (nextContent === currentContent) { + lastEmittedContent.value = nextContent; + return; + } + if (nextContent === lastEmittedContent.value) { + return; + } + // 聚焦编辑时禁止外部回灌,避免重建选区后把光标拉回开头 + if (editor.value.isFocused) { + return; + } + applyExternalContent(nextContent); + }, { immediate: true }); + + watch(() => props.paddingTB, (value) => { + articlePadding.paddingTB = normalizePaddingValue(value, articlePadding.paddingTB); + }, { immediate: true }); + + watch(() => props.paddingLR, (value) => { + articlePadding.paddingLR = normalizePaddingValue(value, articlePadding.paddingLR); + }, { immediate: true }); + + watch(() => articlePadding.paddingTB, (value) => { + emit("update:paddingTB", value); + }); + + watch(() => articlePadding.paddingLR, (value) => { + emit("update:paddingLR", value); + }); + + onBeforeUnmount(() => { + editor.value?.destroy(); + }); + + return { + EditorContent, + articlePadding, + editor, + editorBodyStyle + }; +} + +export type TiptapEditorInstance = Editor; + diff --git a/src/components/tiptap-view/index.ts b/src/components/tiptap-view/index.ts new file mode 100644 index 0000000..1c26213 --- /dev/null +++ b/src/components/tiptap-view/index.ts @@ -0,0 +1,6 @@ +import view from "./index.vue"; +import Toolkit from "../../utils/Toolkit"; + +export const TiptapView = Toolkit.withInstall(view); +export default TiptapView; + diff --git a/src/components/tiptap-view/index.vue b/src/components/tiptap-view/index.vue new file mode 100644 index 0000000..3a64c27 --- /dev/null +++ b/src/components/tiptap-view/index.vue @@ -0,0 +1,93 @@ + + + + + + diff --git a/src/index.ts b/src/index.ts index de0cd11..52b805d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,11 @@ import components from "./components"; import "./assets/style/variable.less"; import "./assets/style/timi-web.less"; +import "prismjs/themes/prism.css"; +import "./components/prism-code-block.less"; +import "./components/tiptap-content.less"; import { Network } from "./utils"; +import VPopup from "./utils/directives/Popup"; export * from "./api"; export * from "./components"; @@ -18,6 +22,7 @@ const install = function (app: App) { components.forEach(component => { app.use(component as unknown as { install: () => any }); }); + app.directive("popup", VPopup); }; export default { diff --git a/src/types/Article.ts b/src/types/Article.ts index 6d9d7c4..9f3e76d 100644 --- a/src/types/Article.ts +++ b/src/types/Article.ts @@ -1,27 +1,52 @@ import type { Attachment } from "./Attachment"; +import type { Comment } from "./Comment"; import type { Model } from "./Model"; +import type { Setting } from "./Setting"; -// 文章 -export type Article = { +/** 文章业务类型 */ +export type ArticleBizType = "BLOG" | "FEEDBACK"; + +/** 文章正文类型 */ +export type ArticleContentType = "TEXT" | "MARKDOWN" | "TIP_TAP"; + +/** 文章状态 */ +export type ArticleStatus = "DART" | "NORMAL" | "HIDDEN"; + +/** 文章实体 */ +export type Article = { + /** 业务类型 */ + bizType?: ArticleBizType; + + /** 业务 ID */ + bizId?: string; + + /** 标题 */ title?: string; - type: ArticleType; - digest?: string; - data?: string; - args?: A; - reads: number; - likes: number; - showComment: boolean; - canComment: boolean; - canRanking: boolean; + + /** 描述 */ + description?: string; + + /** 正文类型 */ + contentType?: ArticleContentType; + + /** 正文内容 */ + content?: string; + + /** 阅读数量 */ + reads?: number; + + /** 喜欢数量 */ + likes?: number; + + /** 文章状态 */ + status?: ArticleStatus; + + /** 设置列表 */ + settingList?: Setting[]; + + /** 评论列表 */ + commentList?: Comment[]; + + /** 附件列表 */ + attachmentList?: Attachment[]; } & Model; - -export type ArticleView = { - comments?: number; - attachmentList: Attachment[]; -} & Article; - -export const ArticleType = "COMMON"; - -export type ArticleType = (typeof ArticleType)[keyof typeof ArticleType]; - -export const ArticleAttachType = "COVER"; diff --git a/src/types/Attachment.ts b/src/types/Attachment.ts index b849aba..902096f 100644 --- a/src/types/Attachment.ts +++ b/src/types/Attachment.ts @@ -8,7 +8,8 @@ export type Attachment = { title?: string; name?: string; mimeType?: string; - metadata?: unknown; + metadata?: Record; + accessKey?: AttachmentAccessKey; size?: number; md5?: string; uploaderIp?: string; @@ -28,4 +29,20 @@ export type BizIdUpdate = { items: Partial[]; }; -export type AttachmentBizType = | "USER" | "GAO_CUSTOMER" | "GAO_REGISTER_RECORD" | "TEMP_FILE" | "MIRROR"; +export type AttachmentBizType = + | "ARTICLE" + | "USER" + | "GAO_CUSTOMER" + | "GAO_EVENT_RECORD" + | "GAO_REGISTER_RECORD" + | "TEMP_FILE" + | "MIRROR"; + +/** 附件访问密钥 */ +export type AttachmentAccessKey = { + value: string; + attachmentId: string; + isDisposable: boolean; + expiredAt: number; + createdAt: number; +}; diff --git a/src/utils/Prism.ts b/src/utils/Prism.ts new file mode 100644 index 0000000..d14aa94 --- /dev/null +++ b/src/utils/Prism.ts @@ -0,0 +1,19 @@ +import Prism from "prismjs"; + +const prismLanguageAliasMap: Record = { + html: "markup", + xml: "markup", + js: "javascript", + ts: "typescript", + sh: "bash", + vue: "markup" +}; + +export function resolvePrismLanguage(language?: string | null) { + if (!language) { + return ""; + } + const nextLanguage = prismLanguageAliasMap[language] || language; + return Prism.languages[nextLanguage] ? nextLanguage : ""; +} + diff --git a/src/utils/PrismCodeBlock.ts b/src/utils/PrismCodeBlock.ts new file mode 100644 index 0000000..32f8ae1 --- /dev/null +++ b/src/utils/PrismCodeBlock.ts @@ -0,0 +1,123 @@ +export function enhancePrismCodeBlock(root: ParentNode) { + const codeList = Array.from(root.querySelectorAll("pre > code[class*='language-'], pre[class*='language-'] > code")); + + for (const code of codeList) { + if (!(code instanceof HTMLElement)) { + continue; + } + + const pre = code.parentElement; + if (!(pre instanceof HTMLElement)) { + continue; + } + + syncPreLanguageClass(pre, code); + + resetCodeBlock(code); + normalizeCodeContent(code); + + const maxHeight = resolveCodeBlockMaxHeight(pre); + if (maxHeight && maxHeight !== "auto") { + pre.style.maxHeight = maxHeight; + } else { + pre.style.removeProperty("max-height"); + } + + const lineCount = buildLineCount(code.textContent || ""); + const rows = buildLineNumberRows(lineCount); + const codes = document.createElement("span"); + codes.className = "codes"; + codes.innerHTML = code.innerHTML; + + pre.classList.add("has-line-numbers"); + code.replaceChildren(rows, codes); + bindCodeBlockToggle(pre, code, lineCount, maxHeight); + } +} + +function syncPreLanguageClass(pre: HTMLElement, code: HTMLElement) { + const classList = Array.from(code.classList).filter((item) => item.startsWith("language-")); + if (!classList.length) { + return; + } + for (const className of classList) { + if (!pre.classList.contains(className)) { + pre.classList.add(className); + } + } +} + +function resetCodeBlock(code: HTMLElement) { + code.parentElement?.classList.remove("has-line-numbers"); + const codes = code.querySelector(":scope > .codes"); + if (codes instanceof HTMLElement) { + code.innerHTML = codes.innerHTML; + } +} + +function normalizeCodeContent(code: HTMLElement) { + const firstNode = code.firstChild; + if (firstNode instanceof Text && firstNode.data.startsWith("\n")) { + firstNode.data = firstNode.data.slice(1); + if (!firstNode.data) { + firstNode.remove(); + } + } + + const lastNode = code.lastChild; + if (lastNode instanceof Text && lastNode.data.endsWith("\n")) { + lastNode.data = lastNode.data.replace(/\n+$/, ""); + if (!lastNode.data) { + lastNode.remove(); + } + } +} + +function resolveCodeBlockMaxHeight(pre: HTMLElement) { + const container = pre.closest("[data-max-height]"); + if (!(container instanceof HTMLElement)) { + return ""; + } + return container.dataset.maxHeight || ""; +} + +function buildLineCount(text: string) { + if (!text) { + return 1; + } + return Math.max(text.split("\n").length, 1); +} + +function buildLineNumberRows(lineCount: number) { + const rows = document.createElement("span"); + rows.className = "line-numbers-rows diselect"; + for (let i = 0; i < lineCount; i += 1) { + const row = document.createElement("span"); + row.textContent = `${i + 1}`; + rows.appendChild(row); + } + return rows; +} + +function bindCodeBlockToggle(pre: HTMLElement, code: HTMLElement, lineCount: number, maxHeight: string) { + pre.classList.remove("expand"); + pre.dataset.lineCount = `${lineCount}`; + pre.ondblclick = null; + + if (!maxHeight || maxHeight === "auto" || lineCount < 18) { + return; + } + pre.ondblclick = () => { + const isExpand = pre.classList.contains("expand"); + if (isExpand) { + pre.style.maxHeight = maxHeight; + pre.classList.remove("expand"); + } else { + const lineHeight = Number.parseFloat(getComputedStyle(code).lineHeight || "22") || 22; + pre.style.maxHeight = `${Math.ceil(lineCount * lineHeight + 2)}px`; + pre.classList.add("expand"); + } + window.getSelection()?.removeAllRanges(); + }; +} + diff --git a/src/utils/TDesignMessage.ts b/src/utils/TDesignMessage.ts new file mode 100644 index 0000000..11bc996 --- /dev/null +++ b/src/utils/TDesignMessage.ts @@ -0,0 +1,27 @@ +import type { AppContext } from "vue"; + +type MessageType = "info" | "success" | "warning" | "error"; + +type MessageHandler = (content: string) => unknown | Promise; + +type MessageApi = { + [type in MessageType]?: MessageHandler; +}; + +export const showTDesignMessage = async ( + appContext: AppContext | undefined, + type: MessageType, + content: string +) => { + const handler = (appContext?.config.globalProperties.$message as MessageApi | undefined)?.[type]; + if (handler) { + await handler(content); + return; + } + if ("error" === type) { + console.error(content); + return; + } + console.warn(content); +}; + diff --git a/vite.config.ts b/vite.config.ts index f07e878..5535ce5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -35,15 +35,24 @@ export default defineConfig({ }, rollupOptions: { external: [ - "vue" + "vue", + "tdesign-vue-next" ], output: { globals: { - vue: "Vue" + vue: "Vue", + "tdesign-vue-next": "TDesign" } } }, - minify: "terser", + commonjsOptions: { + esmExternals: [ + "vue", + "tdesign-vue-next" + ], + requireReturnsDefault: false + }, + minify: "terser" }, plugins: [ vue({ @@ -67,7 +76,8 @@ export default defineConfig({ "javascript", "typescript", "apacheconf", - "properties" + "properties", + "bash" ], plugins: [ "line-numbers"