feat: add docx and html2pdf.js for document export functionality
This commit is contained in:
378
package-lock.json
generated
378
package-lock.json
generated
@@ -13,6 +13,8 @@
|
||||
"@milkdown/kit": "^7.18.0",
|
||||
"@milkdown/theme-nord": "^7.18.0",
|
||||
"@milkdown/vue": "^7.18.0",
|
||||
"docx": "^9.6.0",
|
||||
"html2pdf.js": "^0.14.0",
|
||||
"katex": "^0.16.9",
|
||||
"markdown-it": "^13.0.0",
|
||||
"markdown-it-math": "^3.0.2",
|
||||
@@ -73,6 +75,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
@@ -2209,6 +2220,28 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
|
||||
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
@@ -2834,6 +2867,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
|
||||
@@ -2935,6 +2977,38 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/canvg/node_modules/core-js": {
|
||||
"version": "3.48.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
|
||||
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
@@ -3292,8 +3366,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cose-base": {
|
||||
"version": "1.0.3",
|
||||
@@ -3309,6 +3382,15 @@
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -4005,6 +4087,41 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.6.0.tgz",
|
||||
"integrity": "sha512-y6EaJJMDvt4P7wgGQB9KsZf4wsRkQMJfkc9LlNufRshggI5BT35hGNkXBCAeEoI3MLMwApKguxzjdqqVcBCqNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^25.2.3",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
@@ -4192,6 +4309,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -4210,6 +4338,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
@@ -4488,6 +4622,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -4515,6 +4659,30 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2pdf.js": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.14.0.tgz",
|
||||
"integrity": "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dompurify": "^3.3.1",
|
||||
"html2canvas": "^1.0.0",
|
||||
"jspdf": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -4528,6 +4696,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@@ -4544,8 +4718,7 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
@@ -4566,6 +4739,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-accessor-descriptor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
|
||||
@@ -4802,8 +4981,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
"version": "2.1.0",
|
||||
@@ -4844,6 +5022,53 @@
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz",
|
||||
"integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf/node_modules/core-js": {
|
||||
"version": "3.48.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
|
||||
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip/node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.27",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz",
|
||||
@@ -4936,6 +5161,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||
@@ -5896,6 +6130,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz",
|
||||
@@ -6248,6 +6488,12 @@
|
||||
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parse-glob": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
|
||||
@@ -6313,6 +6559,13 @@
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -6453,8 +6706,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.3.1",
|
||||
@@ -6647,6 +6899,16 @@
|
||||
"teleport": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/randomatic": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
|
||||
@@ -6687,7 +6949,6 @@
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -7034,6 +7295,13 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/regex-cache": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
|
||||
@@ -7270,6 +7538,16 @@
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/right-align": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
|
||||
@@ -7361,8 +7639,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-regex": {
|
||||
"version": "1.1.0",
|
||||
@@ -7380,6 +7657,15 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
|
||||
"integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-value": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||
@@ -7409,6 +7695,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
@@ -7655,6 +7947,16 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/static-extend": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
||||
@@ -7701,7 +8003,6 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -7767,6 +8068,25 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
@@ -7921,6 +8241,12 @@
|
||||
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||
@@ -8116,8 +8442,16 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
@@ -8381,6 +8715,24 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"@milkdown/kit": "^7.18.0",
|
||||
"@milkdown/theme-nord": "^7.18.0",
|
||||
"@milkdown/vue": "^7.18.0",
|
||||
"docx": "^9.6.0",
|
||||
"html2pdf.js": "^0.14.0",
|
||||
"katex": "^0.16.9",
|
||||
"markdown-it": "^13.0.0",
|
||||
"markdown-it-math": "^3.0.2",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<div ref="root" class="milkdown-editor"></div>
|
||||
|
||||
@@ -48,20 +48,27 @@
|
||||
</button>
|
||||
<input type="file" ref="fileInputRef" @change="handleFileUpload" accept=".md,text/markdown,text/x-markdown" style="display:none">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="action-btn"
|
||||
:aria-label="t('exportMd')"
|
||||
:title="t('exportMd')"
|
||||
@click="exportMarkdown"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
<span class="btn-tooltip">{{ t('exportMd') }}</span>
|
||||
</button>
|
||||
<div class="export-btn-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
class="action-btn"
|
||||
:aria-label="t('exportMd')"
|
||||
:title="t('exportMd')"
|
||||
@click="toggleExportDropdown"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
<span class="btn-tooltip">{{ t('exportMd') }}</span>
|
||||
</button>
|
||||
<div v-if="showExportDropdown" class="export-dropdown">
|
||||
<button type="button" @click="exportMarkdown">{{ t('exportMd') }}</button>
|
||||
<button type="button" @click="exportToDocx">{{ t('exportDocx') }}</button>
|
||||
<button type="button" @click="exportToPdf">{{ t('exportPdf') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-btn-wrapper">
|
||||
<button
|
||||
@@ -128,6 +135,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showMermaidPreview" class="mermaid-preview-overlay" @click.self="closeMermaidPreview">
|
||||
<div class="mermaid-preview-dialog" role="dialog" aria-modal="true" aria-label="Mermaid Preview">
|
||||
<button type="button" class="mermaid-preview-close" @click="closeMermaidPreview" aria-label="Close preview">✕</button>
|
||||
<div class="mermaid-preview-scroll">
|
||||
<img :src="mermaidPreviewSrc" alt="Mermaid diagram preview">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -135,15 +151,19 @@
|
||||
import { onMounted, onUnmounted, ref, computed, watch } from 'vue'
|
||||
import { replaceAll } from '@milkdown/kit/utils'
|
||||
import { Crepe } from '@milkdown/crepe'
|
||||
import { editorViewCtx, serializerCtx } from '@milkdown/kit/core'
|
||||
import { editorViewCtx } from '@milkdown/kit/core'
|
||||
import { Selection } from '@milkdown/prose/state'
|
||||
import { undo, redo, undoDepth, redoDepth } from '@milkdown/prose/history'
|
||||
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, interruptCopilot, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit, clearGhostSuggestion } from '../plugins/copilotPlugin'
|
||||
import { mermaidRenderPreview, codeBlockConfig } from '../plugins/mermaidPlugin'
|
||||
import { mermaidRenderPreview, codeBlockConfig, refreshMermaidPreviews } from '../plugins/mermaidPlugin'
|
||||
import { fetchSuggestion } from '../utils/api.js'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { OCR_URL } from '../utils/config.js'
|
||||
import { setOcrCache, clearOcrCache, clearAllOcrCache, IMAGE_SIZE_LIMIT, calculateImageHash, getOcrByHash, setOcrByHash } from '../utils/ocrCache.js'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import katex from 'katex'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import html2pdf from 'html2pdf.js'
|
||||
|
||||
const emit = defineEmits(['update:markdown'])
|
||||
const settings = useSettingsStore()
|
||||
@@ -156,7 +176,10 @@ const cameraInputRef = ref(null)
|
||||
const aiEnabled = ref(true)
|
||||
const contentSize = ref(0)
|
||||
const showImageDropdown = ref(false)
|
||||
const showExportDropdown = ref(false)
|
||||
const showUrlDialog = ref(false)
|
||||
const showMermaidPreview = ref(false)
|
||||
const mermaidPreviewSrc = ref('')
|
||||
const imageUrl = ref('')
|
||||
const canUndo = ref(false)
|
||||
const canRedo = ref(false)
|
||||
@@ -178,6 +201,8 @@ const aiButtonLabel = computed(() => {
|
||||
let crepe = null
|
||||
let markdownSyncTimer = null
|
||||
let rootResizeObserver = null
|
||||
let themeObserver = null
|
||||
let mermaidResizeTimer = null
|
||||
const objectUrls = new Set()
|
||||
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
||||
const MARKDOWN_EXT_RE = /\.md$/i
|
||||
@@ -392,6 +417,170 @@ const prepareImageFile = async (file) => {
|
||||
return objectUrl
|
||||
}
|
||||
|
||||
const closeMermaidPreview = () => {
|
||||
showMermaidPreview.value = false
|
||||
mermaidPreviewSrc.value = ''
|
||||
}
|
||||
|
||||
const openMermaidPreview = (url) => {
|
||||
if (!url) return
|
||||
mermaidPreviewSrc.value = url
|
||||
showMermaidPreview.value = true
|
||||
}
|
||||
|
||||
const makeMermaidFilename = () => {
|
||||
const now = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
||||
const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||||
return `mermaid-${datePart}-${timePart}.png`
|
||||
}
|
||||
|
||||
const normalizeMermaidFilename = (filename = '') => {
|
||||
if (!filename) return makeMermaidFilename()
|
||||
if (/\.png$/i.test(filename)) return filename
|
||||
if (/\.[^./\\]+$/.test(filename)) return filename.replace(/\.[^./\\]+$/, '.png')
|
||||
return `${filename}.png`
|
||||
}
|
||||
|
||||
const parseSvgSize = (svg) => {
|
||||
const viewBox = svg.match(/viewBox\s*=\s*["']\s*[-\d.]+\s+[-\d.]+\s+([-\d.]+)\s+([-\d.]+)\s*["']/i)
|
||||
if (viewBox) {
|
||||
const width = Number(viewBox[1])
|
||||
const height = Number(viewBox[2])
|
||||
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
||||
return { width, height }
|
||||
}
|
||||
}
|
||||
const widthAttr = svg.match(/width\s*=\s*["']([-\d.]+)(px)?["']/i)
|
||||
const heightAttr = svg.match(/height\s*=\s*["']([-\d.]+)(px)?["']/i)
|
||||
const width = widthAttr ? Number(widthAttr[1]) : 960
|
||||
const height = heightAttr ? Number(heightAttr[1]) : 540
|
||||
return {
|
||||
width: Number.isFinite(width) && width > 0 ? width : 960,
|
||||
height: Number.isFinite(height) && height > 0 ? height : 540,
|
||||
}
|
||||
}
|
||||
|
||||
const getRasterDpr = (width, height) => {
|
||||
const rawDpr = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1
|
||||
const baseDpr = Math.min(3, Math.max(1, rawDpr))
|
||||
const maxCanvasEdge = 4096
|
||||
const edgeLimitedDpr = maxCanvasEdge / Math.max(width, height, 1)
|
||||
return Math.max(1, Math.min(baseDpr, edgeLimitedDpr))
|
||||
}
|
||||
|
||||
const decodeSvgDataUrl = (url) => {
|
||||
const commaIndex = url.indexOf(',')
|
||||
if (commaIndex === -1) throw new Error('Invalid SVG data URL')
|
||||
const header = url.slice(0, commaIndex)
|
||||
const payload = url.slice(commaIndex + 1)
|
||||
if (/;base64/i.test(header)) {
|
||||
const binary = atob(payload)
|
||||
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
|
||||
return new TextDecoder().decode(bytes)
|
||||
}
|
||||
return decodeURIComponent(payload)
|
||||
}
|
||||
|
||||
const svgTextToPngDataUrl = async (svgText) => {
|
||||
const fallback = parseSvgSize(svgText)
|
||||
let width = fallback.width
|
||||
let height = fallback.height
|
||||
|
||||
const maxEdge = 2400
|
||||
const scale = Math.min(1, maxEdge / Math.max(width, height))
|
||||
width = Math.max(1, Math.round(width * scale))
|
||||
height = Math.max(1, Math.round(height * scale))
|
||||
|
||||
const image = new Image()
|
||||
image.decoding = 'async'
|
||||
image.crossOrigin = 'anonymous'
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
image.onload = () => resolve()
|
||||
image.onerror = () => reject(new Error('Failed to load SVG image'))
|
||||
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`
|
||||
})
|
||||
|
||||
const dpr = getRasterDpr(width, height)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = Math.max(1, Math.round(width * dpr))
|
||||
canvas.height = Math.max(1, Math.round(height * dpr))
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Canvas context unavailable')
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.drawImage(image, 0, 0, width, height)
|
||||
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
const ensurePngDownloadUrl = async (url) => {
|
||||
if (!url) return ''
|
||||
if (/^data:image\/png/i.test(url)) return url
|
||||
if (/^data:image\/svg\+xml/i.test(url)) {
|
||||
const svgText = decodeSvgDataUrl(url)
|
||||
return await svgTextToPngDataUrl(svgText)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
const downloadMermaidImage = async (url, filename = '') => {
|
||||
if (!url) return
|
||||
let downloadUrl = url
|
||||
try {
|
||||
downloadUrl = await ensurePngDownloadUrl(url)
|
||||
} catch (error) {
|
||||
alert('PNG export failed for this diagram. Please remove external image resources and try again.')
|
||||
return
|
||||
}
|
||||
const a = document.createElement('a')
|
||||
a.href = downloadUrl
|
||||
a.download = normalizeMermaidFilename(filename)
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
}
|
||||
|
||||
const handleMermaidAction = async (event) => {
|
||||
const target = event.target instanceof Element ? event.target.closest('[data-mermaid-action]') : null
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
|
||||
const action = target.getAttribute('data-mermaid-action')
|
||||
if (!action) return
|
||||
|
||||
const block = target.closest('.mermaid-block')
|
||||
const url = target.getAttribute('data-mermaid-url') || block?.getAttribute('data-mermaid-url') || ''
|
||||
if (!url) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (action === 'zoom') {
|
||||
openMermaidPreview(url)
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'download') {
|
||||
const filename = target.getAttribute('data-mermaid-filename') || ''
|
||||
await downloadMermaidImage(url, filename)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMermaidViewportResize = () => {
|
||||
if (mermaidResizeTimer) {
|
||||
clearTimeout(mermaidResizeTimer)
|
||||
mermaidResizeTimer = null
|
||||
}
|
||||
mermaidResizeTimer = setTimeout(() => {
|
||||
mermaidResizeTimer = null
|
||||
refreshMermaidPreviews()
|
||||
}, 140)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!root.value) throw new Error('root.value is null')
|
||||
updateEditorTailSpace()
|
||||
@@ -401,10 +590,12 @@ onMounted(async () => {
|
||||
})
|
||||
rootResizeObserver.observe(root.value)
|
||||
}
|
||||
root.value.addEventListener('click', handleMermaidAction, true)
|
||||
window.addEventListener('resize', handleMermaidViewportResize)
|
||||
|
||||
crepe = new Crepe({
|
||||
root: root.value,
|
||||
defaultValue: '# 娆㈣繋鏉ュ埌LLM-IN-TEXT\n\n涓€涓嵆鏃禠LM绯荤粺\n\n鍦ㄤ笅闈㈠紑濮嬩綘鐨勫垱浣?..',
|
||||
defaultValue: '# 欢迎来到LLM-IN-TEXT\n\n一个即时LLM系统\n\n在下开始你的创作...',
|
||||
features: {
|
||||
[Crepe.Feature.Latex]: true,
|
||||
[Crepe.Feature.ImageBlock]: true,
|
||||
@@ -464,6 +655,7 @@ onMounted(async () => {
|
||||
|
||||
|
||||
await crepe.create()
|
||||
refreshMermaidPreviews()
|
||||
|
||||
crepe.on((listener) => {
|
||||
listener.updated((ctx, doc) => {
|
||||
@@ -481,6 +673,20 @@ onMounted(async () => {
|
||||
refreshSizeAndLimit(ctx)
|
||||
updateHistoryState(view)
|
||||
})
|
||||
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
themeObserver = new MutationObserver((mutations) => {
|
||||
const changed = mutations.some((mutation) => mutation.type === 'attributes' && mutation.attributeName === 'data-theme')
|
||||
if (changed) {
|
||||
refreshMermaidPreviews()
|
||||
}
|
||||
})
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme'],
|
||||
})
|
||||
}
|
||||
|
||||
scheduleMarkdownSync()
|
||||
})
|
||||
|
||||
@@ -508,6 +714,233 @@ const exportMarkdown = async () => {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 棰勫鐞?LaTeX 鍏紡
|
||||
const preprocessLatex = (text) => {
|
||||
// 澶勭悊 $$...$$ 鍧楃骇鍏紡
|
||||
text = text.replace(/\$\$([\s\S]*?)\$\$/g, (match, content) => {
|
||||
try {
|
||||
const html = katex.renderToString(content.trim(), {
|
||||
displayMode: true,
|
||||
throwOnError: false
|
||||
})
|
||||
return `<div class="math-block">${html}</div>`
|
||||
} catch (e) {
|
||||
return `<div class="math-error">$$${content}$$</div>`
|
||||
}
|
||||
})
|
||||
|
||||
// 澶勭悊 $...$ 琛屽唴鍏紡
|
||||
text = text.replace(/(?<!\$)\$(?!\$)([^\$\n]+?)\$(?!\$)/g, (match, content) => {
|
||||
try {
|
||||
const html = katex.renderToString(content.trim(), {
|
||||
displayMode: false,
|
||||
throwOnError: false
|
||||
})
|
||||
return `<span class="math-inline">${html}</span>`
|
||||
} catch (e) {
|
||||
return `<span class="math-error">${match}</span>`
|
||||
}
|
||||
})
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// 灏?markdown 杞崲涓?HTML 骞跺鐞?mermaid
|
||||
const decodeHtmlEntities = (input) => {
|
||||
if (!input) return ''
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.innerHTML = input
|
||||
return textarea.value
|
||||
}
|
||||
|
||||
const collectRenderedMermaidImages = () => {
|
||||
const imageMap = new Map()
|
||||
const blocks = document.querySelectorAll('.mermaid-block[data-mermaid-code][data-mermaid-url]')
|
||||
for (const block of blocks) {
|
||||
const encodedCode = block.getAttribute('data-mermaid-code') || ''
|
||||
const imageUrl = block.getAttribute('data-mermaid-url') || ''
|
||||
if (!encodedCode || !imageUrl) continue
|
||||
try {
|
||||
const code = normalizeMermaidCodeForExport(decodeURIComponent(encodedCode))
|
||||
if (code) imageMap.set(code, imageUrl)
|
||||
} catch (error) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return imageMap
|
||||
}
|
||||
|
||||
const normalizeMermaidCodeForExport = (code) => {
|
||||
return (code || '').replace(/\r\n/g, '\n').trim()
|
||||
}
|
||||
const markdownToHtml = async (markdown) => {
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
})
|
||||
|
||||
// 棰勫鐞?LaTeX
|
||||
let html = preprocessLatex(markdown)
|
||||
|
||||
// 娓叉煋 markdown
|
||||
html = md.render(html)
|
||||
|
||||
// Mermaid code block -> rendered image
|
||||
const mermaidImageMap = collectRenderedMermaidImages()
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = html
|
||||
const mermaidNodes = template.content.querySelectorAll('pre > code.language-mermaid')
|
||||
for (const node of mermaidNodes) {
|
||||
const pre = node.parentElement
|
||||
if (!(pre instanceof HTMLElement)) continue
|
||||
const normalizedCode = normalizeMermaidCodeForExport(decodeHtmlEntities(node.textContent || ''))
|
||||
const imgUrl = mermaidImageMap.get(normalizedCode)
|
||||
if (!imgUrl) {
|
||||
pre.remove()
|
||||
continue
|
||||
}
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'mermaid-export'
|
||||
const image = document.createElement('img')
|
||||
image.src = imgUrl
|
||||
image.alt = 'Mermaid diagram'
|
||||
wrapper.appendChild(image)
|
||||
pre.replaceWith(wrapper)
|
||||
}
|
||||
html = template.innerHTML
|
||||
|
||||
html = html.replace(/<pre class="language-(\w+)"><code>([\s\S]*?)<\/code><\/pre>/g,
|
||||
'<pre class="code-simple"><code>$2</code></pre>')
|
||||
|
||||
// Process remote images
|
||||
html = html.replace(/<img src="(http[s]?:\/\/[^"]+)"([^>]*)>/g, (match, src, attrs) => {
|
||||
return `<img src="${src}"${attrs} style="max-width: 100%; height: auto;" />`
|
||||
})
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
// 瀵煎嚭涓?DOCX (浣跨敤 HTML 鏍煎紡锛學ord 鍙互鐩存帴鎵撳紑)
|
||||
const exportToDocx = async () => {
|
||||
if (!crepe) return
|
||||
|
||||
showExportDropdown.value = false
|
||||
|
||||
try {
|
||||
const markdown = await crepe.getMarkdown()
|
||||
const html = await markdownToHtml(markdown)
|
||||
|
||||
// 鍖呰 HTML 涓哄畬鏁存枃妗o紝娣诲姞 Word 鍏煎鐨勫厓鏁版嵁
|
||||
const fullHtml = `
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Document</title>
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<w:WordDocument>
|
||||
<w:View>Print</w:View>
|
||||
</w:WordDocument>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<style>
|
||||
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; padding: 40px; }
|
||||
h1, h2, h3, h4, h5, h6 { margin-top: 1em; margin-bottom: 0.5em; font-weight: 600; }
|
||||
p { margin: 1em 0; }
|
||||
code { background-color: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; }
|
||||
pre { background-color: #f5f5f5; padding: 16px; border-radius: 6px; overflow-x: auto; }
|
||||
pre code { background-color: transparent; padding: 0; }
|
||||
blockquote { border-left: 4px solid #ddd; margin: 1em 0; padding-left: 16px; color: #666; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background-color: #f5f5f5; font-weight: 600; }
|
||||
.math-block { display: block; margin: 1em 0; text-align: center; overflow-x: auto; }
|
||||
.math-inline { padding: 0 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// 浣跨敤 HTML 鏍煎紡淇濆瓨涓?.doc锛學ord 鍙互鐩存帴鎵撳紑
|
||||
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
const now = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
||||
const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||||
a.href = url
|
||||
a.download = `document${datePart}${timePart}.doc`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('[Export DOCX] Error:', error)
|
||||
alert('瀵煎嚭 DOCX 澶辫触锛岃閲嶈瘯')
|
||||
}
|
||||
}
|
||||
|
||||
// 瀵煎嚭涓?PDF
|
||||
const exportToPdf = async () => {
|
||||
if (!crepe) return
|
||||
|
||||
showExportDropdown.value = false
|
||||
|
||||
try {
|
||||
const markdown = await crepe.getMarkdown()
|
||||
const html = await markdownToHtml(markdown)
|
||||
|
||||
const fullHtml = `
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; padding: 40px; color: #111; background: #fff; }
|
||||
h1, h2, h3, h4, h5, h6 { margin-top: 1em; margin-bottom: 0.5em; font-weight: 600; }
|
||||
p { margin: 1em 0; }
|
||||
code { background-color: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; }
|
||||
pre { background-color: #f5f5f5; padding: 16px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
|
||||
pre code { background-color: transparent; padding: 0; }
|
||||
blockquote { border-left: 4px solid #ddd; margin: 1em 0; padding-left: 16px; color: #666; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background-color: #f5f5f5; font-weight: 600; }
|
||||
.math-block { display: block; margin: 1em 0; text-align: center; overflow-x: auto; }
|
||||
.math-inline { padding: 0 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// 閰嶇疆 PDF 閫夐」
|
||||
const options = {
|
||||
margin: [10, 10, 10, 10],
|
||||
filename: `document${new Date().toISOString().slice(0, 10)}.pdf`,
|
||||
image: { type: 'jpeg', quality: 0.98 },
|
||||
html2canvas: { scale: 2, useCORS: true, backgroundColor: '#ffffff', windowWidth: 1200 },
|
||||
pagebreak: { mode: ['css', 'legacy'] },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||
}
|
||||
|
||||
// 鐢熸垚 PDF
|
||||
await html2pdf().set(options).from(fullHtml, 'string').save()
|
||||
} catch (error) {
|
||||
console.error('[Export PDF] Error:', error)
|
||||
alert('瀵煎嚭 PDF 澶辫触锛岃閲嶈瘯')
|
||||
}
|
||||
}
|
||||
|
||||
const triggerUpload = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
@@ -559,6 +992,10 @@ const toggleAI = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const toggleExportDropdown = () => {
|
||||
showExportDropdown.value = !showExportDropdown.value
|
||||
}
|
||||
|
||||
const toggleImageDropdown = () => {
|
||||
showImageDropdown.value = !showImageDropdown.value
|
||||
}
|
||||
@@ -630,6 +1067,21 @@ onUnmounted(() => {
|
||||
rootResizeObserver = null
|
||||
}
|
||||
|
||||
if (mermaidResizeTimer) {
|
||||
clearTimeout(mermaidResizeTimer)
|
||||
mermaidResizeTimer = null
|
||||
}
|
||||
|
||||
if (themeObserver) {
|
||||
themeObserver.disconnect()
|
||||
themeObserver = null
|
||||
}
|
||||
|
||||
if (root.value) {
|
||||
root.value.removeEventListener('click', handleMermaidAction, true)
|
||||
}
|
||||
window.removeEventListener('resize', handleMermaidViewportResize)
|
||||
|
||||
for (const url of Array.from(objectUrls)) {
|
||||
revokeObjectUrl(url)
|
||||
}
|
||||
@@ -791,6 +1243,40 @@ onUnmounted(() => {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.export-btn-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.export-dropdown {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
margin-bottom: 8px;
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--panel-shadow);
|
||||
overflow: hidden;
|
||||
z-index: 10000;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.export-dropdown button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.export-dropdown button:hover {
|
||||
background: var(--crepe-color-hover);
|
||||
}
|
||||
|
||||
.image-btn-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -900,6 +1386,60 @@ onUnmounted(() => {
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
|
||||
.mermaid-preview-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10002;
|
||||
background: var(--overlay-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mermaid-preview-dialog {
|
||||
width: min(96vw, 1280px);
|
||||
max-height: min(92vh, 920px);
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--panel-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mermaid-preview-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 8px;
|
||||
background: var(--btn-bg);
|
||||
color: var(--btn-fg);
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mermaid-preview-close:hover {
|
||||
background: var(--btn-hover-bg);
|
||||
color: var(--btn-hover-fg);
|
||||
border-color: var(--btn-hover-bg);
|
||||
}
|
||||
|
||||
.mermaid-preview-scroll {
|
||||
overflow: auto;
|
||||
padding: 40px 16px 16px;
|
||||
}
|
||||
|
||||
.mermaid-preview-scroll img {
|
||||
display: block;
|
||||
max-width: none;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.milkdown-editor {
|
||||
--editor-tail-space: calc(100vh - 32px);
|
||||
width: 100%;
|
||||
@@ -937,10 +1477,14 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.milkdown-editor :deep(.ProseMirror img) {
|
||||
max-width: 60%;
|
||||
max-width: 80%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.milkdown-editor :deep(.ProseMirror .mermaid-image) {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.milkdown-editor :deep(.ProseMirror > *:first-child) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
@@ -1046,3 +1590,6 @@ onUnmounted(() => {
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -183,9 +183,6 @@ function normalizeSuggestionText(raw: string): string {
|
||||
if (!text.includes('\n') && text.includes('\\n')) {
|
||||
text = text.replace(/\\n/g, '\n')
|
||||
}
|
||||
if (text.includes('\\t')) {
|
||||
text = text.replace(/\\t/g, '\t')
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -1,67 +1,354 @@
|
||||
import { codeBlockConfig } from '@milkdown/kit/component/code-block'
|
||||
import { codeBlockConfig } from '@milkdown/kit/component/code-block'
|
||||
import mermaid from 'mermaid'
|
||||
|
||||
// ── Mermaid init ────────────────────────────────────────────────────────────
|
||||
let mermaidReady = false
|
||||
let mermaidReadyTheme = ''
|
||||
let diagramCounter = 0
|
||||
let renderCounter = 0
|
||||
|
||||
type MermaidImagePayload = {
|
||||
previewUrl: string
|
||||
downloadUrl: string | null
|
||||
width: number
|
||||
height: number
|
||||
sourceWidth: number
|
||||
sourceHeight: number
|
||||
filename: string
|
||||
}
|
||||
|
||||
function getMermaidTheme() {
|
||||
const rootTheme = document.documentElement.getAttribute('data-theme')
|
||||
return rootTheme === 'dark' ? 'dark' : 'default'
|
||||
}
|
||||
|
||||
function ensureMermaid() {
|
||||
if (mermaidReady) return
|
||||
const theme = getMermaidTheme()
|
||||
if (mermaidReadyTheme === theme) return
|
||||
|
||||
const dark = window.matchMedia?.('(prefers-color-scheme: dark)').matches
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: dark ? 'dark' : 'default',
|
||||
theme: theme || (dark ? 'dark' : 'default'),
|
||||
securityLevel: 'loose',
|
||||
fontFamily: 'inherit',
|
||||
flowchart: {
|
||||
htmlLabels: false,
|
||||
},
|
||||
})
|
||||
mermaidReady = true
|
||||
mermaidReadyTheme = theme
|
||||
}
|
||||
|
||||
// ── renderPreview ───────────────────────────────────────────────────────────
|
||||
// Pass this function to codeBlockConfig.renderPreview via crepe.editor.config().
|
||||
// For non-mermaid languages, return null to use the default preview renderer.
|
||||
function encodeMermaidCode(code: string) {
|
||||
return encodeURIComponent(code)
|
||||
}
|
||||
|
||||
export async function mermaidRenderPreview(
|
||||
function decodeMermaidCode(code: string) {
|
||||
try {
|
||||
return decodeURIComponent(code)
|
||||
} catch {
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function nowFileStamp() {
|
||||
const now = new Date()
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||||
}
|
||||
|
||||
function makeMermaidFilename() {
|
||||
return `mermaid-${nowFileStamp()}.png`
|
||||
}
|
||||
|
||||
function buildMermaidPreviewMarkup(code: string, token: number) {
|
||||
const encoded = encodeMermaidCode(code)
|
||||
const filename = makeMermaidFilename()
|
||||
|
||||
return `
|
||||
<div class="mermaid-block" data-mermaid-code="${encoded}" data-mermaid-token="${token}">
|
||||
<div class="mermaid-controls">
|
||||
<button type="button" class="mermaid-action-btn" data-mermaid-action="zoom" disabled>Zoom</button>
|
||||
<button type="button" class="mermaid-action-btn" data-mermaid-action="download" data-mermaid-filename="${filename}" disabled>Download PNG</button>
|
||||
</div>
|
||||
<div class="mermaid-inner">
|
||||
<div class="mermaid-loading">...</div>
|
||||
</div>
|
||||
</div>`.trim()
|
||||
}
|
||||
|
||||
function setMermaidActionsState(block: HTMLElement, payload: MermaidImagePayload | null) {
|
||||
const actionNodes = block.querySelectorAll<HTMLElement>('[data-mermaid-action]')
|
||||
actionNodes.forEach((node) => {
|
||||
const action = node.getAttribute('data-mermaid-action')
|
||||
if (action === 'zoom') {
|
||||
if (payload) {
|
||||
node.removeAttribute('disabled')
|
||||
node.removeAttribute('title')
|
||||
node.setAttribute('data-mermaid-url', payload.previewUrl)
|
||||
} else {
|
||||
node.setAttribute('disabled', 'true')
|
||||
node.removeAttribute('title')
|
||||
node.removeAttribute('data-mermaid-url')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'download') {
|
||||
node.textContent = 'Download PNG'
|
||||
if (payload) {
|
||||
node.removeAttribute('disabled')
|
||||
node.setAttribute('data-mermaid-url', payload.downloadUrl || payload.previewUrl)
|
||||
node.setAttribute('data-mermaid-filename', payload.filename)
|
||||
if (payload.downloadUrl) {
|
||||
node.removeAttribute('title')
|
||||
} else {
|
||||
node.setAttribute('title', 'PNG will be generated on download')
|
||||
}
|
||||
} else {
|
||||
node.setAttribute('disabled', 'true')
|
||||
node.removeAttribute('data-mermaid-url')
|
||||
node.removeAttribute('title')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (payload) {
|
||||
node.removeAttribute('disabled')
|
||||
} else {
|
||||
node.setAttribute('disabled', 'true')
|
||||
node.removeAttribute('data-mermaid-url')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getSvgSize(svg: string) {
|
||||
const viewBox = svg.match(/viewBox\s*=\s*["']\s*[-\d.]+\s+[-\d.]+\s+([-\d.]+)\s+([-\d.]+)\s*["']/i)
|
||||
if (viewBox) {
|
||||
const width = Number(viewBox[1])
|
||||
const height = Number(viewBox[2])
|
||||
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
||||
return { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
const widthAttr = svg.match(/width\s*=\s*["']([-\d.]+)(px)?["']/i)
|
||||
const heightAttr = svg.match(/height\s*=\s*["']([-\d.]+)(px)?["']/i)
|
||||
const width = widthAttr ? Number(widthAttr[1]) : 960
|
||||
const height = heightAttr ? Number(heightAttr[1]) : 540
|
||||
return {
|
||||
width: Number.isFinite(width) && width > 0 ? width : 960,
|
||||
height: Number.isFinite(height) && height > 0 ? height : 540,
|
||||
}
|
||||
}
|
||||
|
||||
function svgToDataUrl(svg: string) {
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
||||
}
|
||||
|
||||
function stripExternalSvgResources(svg: string) {
|
||||
return svg
|
||||
.replace(/<image\b[^>]*(?:href|xlink:href)\s*=\s*["']https?:\/\/[^"']*["'][^>]*>/gi, '')
|
||||
.replace(/url\(\s*["']?https?:\/\/[^"')]*["']?\s*\)/gi, 'none')
|
||||
}
|
||||
|
||||
function getRasterDpr(width: number, height: number) {
|
||||
const rawDpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
|
||||
const baseDpr = Math.min(3, Math.max(1, rawDpr))
|
||||
const maxCanvasEdge = 4096
|
||||
const edgeLimitedDpr = maxCanvasEdge / Math.max(width, height, 1)
|
||||
return Math.max(1, Math.min(baseDpr, edgeLimitedDpr))
|
||||
}
|
||||
|
||||
async function rasterizeSvgToPngDataUrl(svg: string, width: number, height: number): Promise<string> {
|
||||
const image = new Image()
|
||||
image.decoding = 'async'
|
||||
image.crossOrigin = 'anonymous'
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => resolve()
|
||||
image.onerror = () => reject(new Error('Failed to load rendered SVG'))
|
||||
image.src = svgToDataUrl(svg)
|
||||
})
|
||||
|
||||
const dpr = getRasterDpr(width, height)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = Math.max(1, Math.round(width * dpr))
|
||||
canvas.height = Math.max(1, Math.round(height * dpr))
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
throw new Error('Canvas context unavailable')
|
||||
}
|
||||
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.drawImage(image, 0, 0, width, height)
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
async function svgToImageDataUrl(svg: string): Promise<MermaidImagePayload> {
|
||||
const fallback = getSvgSize(svg)
|
||||
const sourceWidth = fallback.width
|
||||
const sourceHeight = fallback.height
|
||||
|
||||
let width = sourceWidth
|
||||
let height = sourceHeight
|
||||
|
||||
const maxEdge = 2400
|
||||
const scale = Math.min(1, maxEdge / Math.max(width, height))
|
||||
width = Math.max(1, Math.round(width * scale))
|
||||
height = Math.max(1, Math.round(height * scale))
|
||||
|
||||
const normalizedSvg = stripExternalSvgResources(svg)
|
||||
const candidates = normalizedSvg === svg ? [svg] : [svg, normalizedSvg]
|
||||
let lastError: unknown = null
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const pngUrl = await rasterizeSvgToPngDataUrl(candidate, width, height)
|
||||
return {
|
||||
previewUrl: pngUrl,
|
||||
downloadUrl: pngUrl,
|
||||
width,
|
||||
height,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
filename: makeMermaidFilename(),
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err
|
||||
}
|
||||
}
|
||||
|
||||
const message = lastError instanceof Error ? lastError.message.toLowerCase() : String(lastError).toLowerCase()
|
||||
if (message.includes('tainted') || message.includes('security')) {
|
||||
return {
|
||||
previewUrl: svgToDataUrl(svg),
|
||||
downloadUrl: null,
|
||||
width,
|
||||
height,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
filename: makeMermaidFilename(),
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error('Failed to convert Mermaid diagram to PNG')
|
||||
}
|
||||
|
||||
function getViewportWidth() {
|
||||
const docWidth = document.documentElement?.clientWidth ?? 0
|
||||
const bodyWidth = document.body?.clientWidth ?? 0
|
||||
const winWidth = window.innerWidth ?? 0
|
||||
return Math.max(docWidth, bodyWidth, winWidth, 1)
|
||||
}
|
||||
|
||||
function getDisplayWidthPx(payload: MermaidImagePayload) {
|
||||
const threshold = Math.max(1, Math.floor(getViewportWidth() * 0.8))
|
||||
if (payload.sourceWidth > threshold) {
|
||||
return Math.min(payload.width, threshold)
|
||||
}
|
||||
return Math.min(payload.width, payload.sourceWidth)
|
||||
}
|
||||
|
||||
async function renderMermaidBlock(block: HTMLElement, token: number): Promise<void> {
|
||||
const tokenOnBlock = Number(block.getAttribute('data-mermaid-token') || '0')
|
||||
if (tokenOnBlock !== token) return
|
||||
|
||||
const inner = block.querySelector('.mermaid-inner')
|
||||
if (!(inner instanceof HTMLElement)) return
|
||||
|
||||
const encodedCode = block.getAttribute('data-mermaid-code') || ''
|
||||
const code = decodeMermaidCode(encodedCode).trim() || 'graph TD\nA-->B'
|
||||
|
||||
inner.innerHTML = '<div class="mermaid-loading">...</div>'
|
||||
setMermaidActionsState(block, null)
|
||||
|
||||
try {
|
||||
ensureMermaid()
|
||||
const id = `mermaid-render-${++diagramCounter}`
|
||||
const { svg } = await mermaid.render(id, code)
|
||||
const imagePayload = await svgToImageDataUrl(svg)
|
||||
|
||||
const latestToken = Number(block.getAttribute('data-mermaid-token') || '0')
|
||||
if (latestToken !== token) return
|
||||
|
||||
const displayWidth = Math.max(1, Math.round(getDisplayWidthPx(imagePayload)))
|
||||
const displayHeight = Math.max(1, Math.round((imagePayload.height / Math.max(1, imagePayload.width)) * displayWidth))
|
||||
|
||||
inner.innerHTML = `<img class="mermaid-image" src="${imagePayload.previewUrl}" alt="Mermaid diagram" width="${displayWidth}" height="${displayHeight}" style="width:${displayWidth}px;height:auto;">`
|
||||
block.setAttribute('data-mermaid-width', String(imagePayload.width))
|
||||
block.setAttribute('data-mermaid-height', String(imagePayload.height))
|
||||
block.setAttribute('data-mermaid-source-width', String(imagePayload.sourceWidth))
|
||||
block.setAttribute('data-mermaid-source-height', String(imagePayload.sourceHeight))
|
||||
block.setAttribute('data-mermaid-display-width', String(displayWidth))
|
||||
block.setAttribute('data-mermaid-url', imagePayload.previewUrl)
|
||||
block.style.removeProperty('width')
|
||||
block.style.removeProperty('max-width')
|
||||
setMermaidActionsState(block, imagePayload)
|
||||
} catch (err) {
|
||||
const latestToken = Number(block.getAttribute('data-mermaid-token') || '0')
|
||||
if (latestToken !== token) return
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
inner.innerHTML = `<pre class="mermaid-error">Mermaid error:\n${escapeHtml(message)}</pre>`
|
||||
setMermaidActionsState(block, null)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleMermaidRender(token: number) {
|
||||
const maxAttempts = 24
|
||||
const targetSelector = `.mermaid-block[data-mermaid-token="${token}"]`
|
||||
|
||||
const run = (attempt: number) => {
|
||||
const block = document.querySelector(targetSelector)
|
||||
if (block instanceof HTMLElement) {
|
||||
void renderMermaidBlock(block, token)
|
||||
return
|
||||
}
|
||||
if (attempt >= maxAttempts) return
|
||||
|
||||
if (typeof window.requestAnimationFrame === 'function') {
|
||||
window.requestAnimationFrame(() => run(attempt + 1))
|
||||
} else {
|
||||
window.setTimeout(() => run(attempt + 1), 16)
|
||||
}
|
||||
}
|
||||
|
||||
run(0)
|
||||
}
|
||||
|
||||
export function mermaidRenderPreview(
|
||||
language: string,
|
||||
content: string,
|
||||
applyPreview: (value: null | string | HTMLElement) => void,
|
||||
): Promise<void> {
|
||||
): void | null {
|
||||
if (language !== 'mermaid') {
|
||||
applyPreview(null)
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
ensureMermaid()
|
||||
|
||||
// Show a placeholder immediately
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'mermaid-block'
|
||||
const inner = document.createElement('div')
|
||||
inner.className = 'mermaid-inner'
|
||||
inner.innerHTML = '<div class="mermaid-loading">···</div>'
|
||||
wrapper.appendChild(inner)
|
||||
applyPreview(wrapper)
|
||||
|
||||
const id = `mermaid-render-${++diagramCounter}`
|
||||
const code = content.trim() || 'graph TD\nA-->B'
|
||||
|
||||
try {
|
||||
const { svg } = await mermaid.render(id, code)
|
||||
inner.innerHTML = svg
|
||||
applyPreview(wrapper)
|
||||
} catch (err) {
|
||||
const pre = document.createElement('pre')
|
||||
pre.className = 'mermaid-error'
|
||||
pre.textContent = `Mermaid error:\n${err instanceof Error ? err.message : String(err)}`
|
||||
inner.innerHTML = ''
|
||||
inner.appendChild(pre)
|
||||
applyPreview(wrapper)
|
||||
}
|
||||
const token = ++renderCounter
|
||||
applyPreview(buildMermaidPreviewMarkup(code, token))
|
||||
scheduleMermaidRender(token)
|
||||
}
|
||||
|
||||
export function refreshMermaidPreviews() {
|
||||
const blocks = document.querySelectorAll<HTMLElement>('.mermaid-block[data-mermaid-code]')
|
||||
blocks.forEach((block) => {
|
||||
const token = ++renderCounter
|
||||
block.setAttribute('data-mermaid-token', String(token))
|
||||
void renderMermaidBlock(block, token)
|
||||
})
|
||||
}
|
||||
|
||||
// ── Milkdown plugin helper ─────────────────────────────────────────────────
|
||||
// Call this inside a crepe.editor.config() callback:
|
||||
// ctx.update(codeBlockConfig.key, (prev) => ({ ...prev, renderPreview: mermaidRenderPreview }))
|
||||
//
|
||||
// Re-export the config key so callers don't need to import @milkdown/components directly.
|
||||
export { codeBlockConfig }
|
||||
|
||||
118
src/style.css
118
src/style.css
@@ -37,8 +37,16 @@
|
||||
--toggle-moon: #475569;
|
||||
--ghost-text: #7d8796;
|
||||
--ghost-code-bg: rgba(15, 23, 42, 0.06);
|
||||
--mermaid-max-width: 800px;
|
||||
--mermaid-max-height: 420px;
|
||||
--mermaid-mobile-max-height: 320px;
|
||||
--mermaid-action-bg: linear-gradient(180deg, #ffffff 0%, #f4f7fc 100%);
|
||||
--mermaid-action-hover-bg: linear-gradient(180deg, #ffffff 0%, #e9f2ff 100%);
|
||||
--mermaid-action-fg: #1f2937;
|
||||
--mermaid-action-border: #cfd8e6;
|
||||
--mermaid-action-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||
--mermaid-action-shadow-hover: 0 4px 10px rgba(37, 99, 235, 0.16);
|
||||
--mermaid-action-disabled-bg: rgba(148, 163, 184, 0.18);
|
||||
--mermaid-action-disabled-fg: #9aa4b2;
|
||||
|
||||
--crepe-color-background: #ffffff;
|
||||
--crepe-color-on-background: #000000;
|
||||
@@ -87,6 +95,14 @@
|
||||
--toggle-moon: #e2e8f0;
|
||||
--ghost-text: #95a0b4;
|
||||
--ghost-code-bg: rgba(226, 232, 240, 0.12);
|
||||
--mermaid-action-bg: linear-gradient(180deg, #30394b 0%, #242c3a 100%);
|
||||
--mermaid-action-hover-bg: linear-gradient(180deg, #3a4760 0%, #2a3445 100%);
|
||||
--mermaid-action-fg: #e5e7eb;
|
||||
--mermaid-action-border: #3e4a61;
|
||||
--mermaid-action-shadow: 0 1px 2px rgba(2, 6, 23, 0.45);
|
||||
--mermaid-action-shadow-hover: 0 5px 12px rgba(2, 6, 23, 0.55);
|
||||
--mermaid-action-disabled-bg: rgba(82, 93, 110, 0.3);
|
||||
--mermaid-action-disabled-fg: #9aa4b2;
|
||||
|
||||
--crepe-color-background: #1a1a1a;
|
||||
--crepe-color-on-background: #e6e6e6;
|
||||
@@ -195,31 +211,87 @@ body {
|
||||
/* ── Mermaid diagram blocks ─────────────────────────────────────────── */
|
||||
.mermaid-block {
|
||||
display: block;
|
||||
margin: 1em 0;
|
||||
padding: 16px;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin: 1em auto;
|
||||
padding: 12px;
|
||||
background: var(--crepe-color-surface, #f7f7f7);
|
||||
border: 1px solid var(--panel-border, #d7deea);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||
user-select: none;
|
||||
border-radius: 10px;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, background-color 160ms ease;
|
||||
}
|
||||
|
||||
.mermaid-block:hover {
|
||||
border-color: var(--focus-ring, #3b82f6);
|
||||
}
|
||||
|
||||
.mermaid-block.mermaid-selected {
|
||||
border-color: var(--focus-ring, #3b82f6);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus-ring, #3b82f6) 25%, transparent);
|
||||
.mermaid-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mermaid-action-btn {
|
||||
appearance: none;
|
||||
border: 1px solid var(--mermaid-action-border);
|
||||
border-radius: 999px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.2;
|
||||
background: var(--mermaid-action-bg);
|
||||
color: var(--mermaid-action-fg);
|
||||
cursor: pointer;
|
||||
box-shadow: var(--mermaid-action-shadow);
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
.mermaid-action-btn:hover:not([disabled]) {
|
||||
border-color: var(--focus-ring);
|
||||
background: var(--mermaid-action-hover-bg);
|
||||
box-shadow: var(--mermaid-action-shadow-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mermaid-action-btn:active:not([disabled]) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.mermaid-action-btn[disabled] {
|
||||
background: var(--mermaid-action-disabled-bg);
|
||||
color: var(--mermaid-action-disabled-fg);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mermaid-inner {
|
||||
display: block;
|
||||
max-width: min(100%, var(--mermaid-max-width));
|
||||
max-width: 100%;
|
||||
max-height: var(--mermaid-max-height);
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: color-mix(in srgb, var(--crepe-color-background, #fff) 88%, transparent);
|
||||
}
|
||||
|
||||
.mermaid-inner::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.mermaid-inner::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mermaid-image {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: none;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mermaid-inner svg {
|
||||
@@ -252,3 +324,25 @@ body {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .milkdown .katex {
|
||||
color: var(--crepe-color-on-background);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .milkdown .cm-editor,
|
||||
:root[data-theme='dark'] .milkdown .cm-scroller {
|
||||
background-color: color-mix(in srgb, var(--crepe-color-surface-low) 86%, transparent);
|
||||
color: var(--crepe-color-on-surface);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .milkdown .cm-gutters {
|
||||
background-color: color-mix(in srgb, var(--crepe-color-surface-low) 86%, transparent);
|
||||
color: var(--crepe-color-on-surface-variant);
|
||||
border-right-color: var(--panel-border);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mermaid-inner {
|
||||
max-height: var(--mermaid-mobile-max-height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ export const translations = {
|
||||
about: 'About Us',
|
||||
importMd: 'Import Markdown',
|
||||
exportMd: 'Export Markdown',
|
||||
exportDocx: 'Export DOCX',
|
||||
exportPdf: 'Export PDF',
|
||||
uploadImg: 'Upload Image',
|
||||
enableAI: 'Enable AI',
|
||||
disableAI: 'Disable AI',
|
||||
@@ -72,6 +74,8 @@ export const translations = {
|
||||
about: '关于我们',
|
||||
importMd: '导入 Markdown',
|
||||
exportMd: '导出 Markdown',
|
||||
exportDocx: '导出 DOCX',
|
||||
exportPdf: '导出 PDF',
|
||||
uploadImg: '上传图片',
|
||||
enableAI: '启用 AI',
|
||||
disableAI: '禁用 AI',
|
||||
@@ -113,6 +117,8 @@ export const translations = {
|
||||
about: '私たちについて',
|
||||
importMd: 'Markdownをインポート',
|
||||
exportMd: 'Markdownをエクスポート',
|
||||
exportDocx: 'DOCXをエクスポート',
|
||||
exportPdf: 'PDFをエクスポート',
|
||||
uploadImg: '画像をアップロード',
|
||||
enableAI: 'AIを有効化',
|
||||
disableAI: 'AIを無効化',
|
||||
@@ -154,6 +160,8 @@ export const translations = {
|
||||
about: '회사 소개',
|
||||
importMd: 'Markdown 가져오기',
|
||||
exportMd: 'Markdown 내보내기',
|
||||
exportDocx: 'DOCX 내보내기',
|
||||
exportPdf: 'PDF 내보내기',
|
||||
uploadImg: '이미지 업로드',
|
||||
enableAI: 'AI 활성화',
|
||||
disableAI: 'AI 비활성화',
|
||||
@@ -195,6 +203,8 @@ export const translations = {
|
||||
about: 'Über uns',
|
||||
importMd: 'Markdown importieren',
|
||||
exportMd: 'Markdown exportieren',
|
||||
exportDocx: 'DOCX exportieren',
|
||||
exportPdf: 'PDF exportieren',
|
||||
uploadImg: 'Bild hochladen',
|
||||
enableAI: 'KI aktivieren',
|
||||
disableAI: 'KI deaktivieren',
|
||||
@@ -236,6 +246,8 @@ export const translations = {
|
||||
about: 'À propos de nous',
|
||||
importMd: 'Importer Markdown',
|
||||
exportMd: 'Exporter Markdown',
|
||||
exportDocx: 'Exporter DOCX',
|
||||
exportPdf: 'Exporter PDF',
|
||||
uploadImg: 'Télécharger image',
|
||||
enableAI: 'Activer IA',
|
||||
disableAI: 'Désactiver IA',
|
||||
|
||||
Reference in New Issue
Block a user