完善编辑功能
This commit is contained in:
802
package-lock.json
generated
802
package-lock.json
generated
@@ -8,6 +8,15 @@
|
||||
"name": "yurou",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.3",
|
||||
"@codemirror/commands": "^6.10.4",
|
||||
"@codemirror/lang-markdown": "6.4.0",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/search": "^6.7.1",
|
||||
"@codemirror/state": "^6.7.1",
|
||||
"@codemirror/view": "^6.43.6",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lucide/vue": "^1.23.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
@@ -18,11 +27,13 @@
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"reka-ui": "^2.10.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"remixicon": "^4.9.1",
|
||||
"shadcn-vue": "^2.7.4",
|
||||
"shiki": "^4.3.1",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"unified": "^11.0.0",
|
||||
@@ -470,6 +481,407 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.10.4.tgz",
|
||||
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-angular": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz",
|
||||
"integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-cpp": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
|
||||
"integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/cpp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-go": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
|
||||
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/go": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-java": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
|
||||
"integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/java": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-jinja": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-jinja/-/lang-jinja-6.0.1.tgz",
|
||||
"integrity": "sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@lezer/lr": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
|
||||
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-less": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-less/-/lang-less-6.0.2.tgz",
|
||||
"integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.2.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-liquid": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-liquid/-/lang-liquid-6.3.2.tgz",
|
||||
"integrity": "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-markdown/-/lang-markdown-6.4.0.tgz",
|
||||
"integrity": "sha512-ZeArR54seh4laFbUTVy0ZmQgO+C/cxxlW4jEoQMhL3HALScBpZBeZcLzrQmJsTEx4is9GzOe0bFAke2B1KZqeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-php": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
|
||||
"integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/php": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-python": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
|
||||
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.3.2",
|
||||
"@codemirror/language": "^6.8.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/python": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-rust": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
|
||||
"integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/rust": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-sass": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz",
|
||||
"integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.2.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/sass": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-sql": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
|
||||
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-vue": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz",
|
||||
"integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-wast": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz",
|
||||
"integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-xml": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
||||
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/xml": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-yaml": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz",
|
||||
"integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"@lezer/yaml": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.4",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.4.tgz",
|
||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language-data": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/language-data/-/language-data-6.5.2.tgz",
|
||||
"integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-angular": "^0.1.0",
|
||||
"@codemirror/lang-cpp": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-go": "^6.0.0",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-java": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/lang-jinja": "^6.0.0",
|
||||
"@codemirror/lang-json": "^6.0.0",
|
||||
"@codemirror/lang-less": "^6.0.0",
|
||||
"@codemirror/lang-liquid": "^6.0.0",
|
||||
"@codemirror/lang-markdown": "^6.0.0",
|
||||
"@codemirror/lang-php": "^6.0.0",
|
||||
"@codemirror/lang-python": "^6.0.0",
|
||||
"@codemirror/lang-rust": "^6.0.0",
|
||||
"@codemirror/lang-sass": "^6.0.0",
|
||||
"@codemirror/lang-sql": "^6.0.0",
|
||||
"@codemirror/lang-vue": "^0.1.1",
|
||||
"@codemirror/lang-wast": "^6.0.0",
|
||||
"@codemirror/lang-xml": "^6.0.0",
|
||||
"@codemirror/lang-yaml": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/legacy-modes": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/legacy-modes": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz",
|
||||
"integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.42.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.7.1.tgz",
|
||||
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.7.1.tgz",
|
||||
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.6",
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.43.6.tgz",
|
||||
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@dotenvx/dotenvx": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@dotenvx/dotenvx/-/dotenvx-2.11.1.tgz",
|
||||
@@ -1260,6 +1672,183 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.2.tgz",
|
||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/cpp": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/cpp/-/cpp-1.1.6.tgz",
|
||||
"integrity": "sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/css/-/css-1.3.4.tgz",
|
||||
"integrity": "sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/go": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/go/-/go-1.0.1.tgz",
|
||||
"integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/java": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/java/-/java-1.1.3.tgz",
|
||||
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.10.tgz",
|
||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.7.2.tgz",
|
||||
"integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/php": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/php/-/php-1.0.5.tgz",
|
||||
"integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/python": {
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/python/-/python-1.1.19.tgz",
|
||||
"integrity": "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/rust": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/rust/-/rust-1.0.2.tgz",
|
||||
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/sass": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/sass/-/sass-1.1.0.tgz",
|
||||
"integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/xml": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/xml/-/xml-1.0.6.tgz",
|
||||
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/yaml": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.4.tgz",
|
||||
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lucide/vue": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmmirror.com/@lucide/vue/-/vue-1.24.0.tgz",
|
||||
@@ -1269,6 +1858,12 @@
|
||||
"vue": ">=3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
|
||||
@@ -1733,6 +2328,106 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-4.3.1.tgz",
|
||||
"integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/primitive": "4.3.1",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-javascript": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz",
|
||||
"integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"oniguruma-to-es": "^4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz",
|
||||
"integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-4.3.1.tgz",
|
||||
"integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/primitive": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/primitive/-/primitive-4.3.1.tgz",
|
||||
"integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-4.3.1.tgz",
|
||||
"integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/types": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-4.3.1.tgz",
|
||||
"integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/vscode-textmate": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
|
||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.23",
|
||||
"resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.23.tgz",
|
||||
@@ -3374,6 +4069,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.7.tgz",
|
||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -5647,6 +6348,20 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-newline-to-break": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz",
|
||||
"integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-find-and-replace": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-phrasing": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||
@@ -6567,6 +7282,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/oniguruma-parser": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
|
||||
"integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oniguruma-to-es": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
|
||||
"integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"oniguruma-parser": "^0.12.2",
|
||||
"regex": "^6.1.0",
|
||||
"regex-recursion": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/open/-/open-11.0.0.tgz",
|
||||
@@ -7128,6 +7860,30 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/regex/-/regex-6.1.0.tgz",
|
||||
"integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-recursion": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-6.0.2.tgz",
|
||||
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-utilities": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz",
|
||||
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rehype-sanitize": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
|
||||
@@ -7182,6 +7938,21 @@
|
||||
"vue": ">= 3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-breaks": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/remark-breaks/-/remark-breaks-4.0.0.tgz",
|
||||
"integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-newline-to-break": "^2.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-gfm": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||
@@ -7548,6 +8319,25 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/shiki/-/shiki-4.3.1.tgz",
|
||||
"integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "4.3.1",
|
||||
"@shikijs/engine-javascript": "4.3.1",
|
||||
"@shikijs/engine-oniguruma": "4.3.1",
|
||||
"@shikijs/langs": "4.3.1",
|
||||
"@shikijs/themes": "4.3.1",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz",
|
||||
@@ -7779,6 +8569,12 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stylus": {
|
||||
"version": "0.57.0",
|
||||
"resolved": "https://registry.npmmirror.com/stylus/-/stylus-0.57.0.tgz",
|
||||
@@ -8603,6 +9399,12 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/web-worker": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/web-worker/-/web-worker-1.5.0.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"reka-ui": "^2.10.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
|
||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -68,6 +68,9 @@ importers:
|
||||
reka-ui:
|
||||
specifier: ^2.10.1
|
||||
version: 2.10.1(vue@3.5.39(typescript@5.6.3))
|
||||
remark-breaks:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -2211,6 +2214,9 @@ packages:
|
||||
mdast-util-gfm@3.1.0:
|
||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||
|
||||
mdast-util-newline-to-break@2.0.0:
|
||||
resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==}
|
||||
|
||||
mdast-util-phrasing@4.1.0:
|
||||
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
||||
|
||||
@@ -2650,6 +2656,9 @@ packages:
|
||||
peerDependencies:
|
||||
vue: '>= 3.4.0'
|
||||
|
||||
remark-breaks@4.0.0:
|
||||
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
@@ -5352,6 +5361,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-newline-to-break@2.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
|
||||
mdast-util-phrasing@4.1.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -5911,6 +5925,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
remark-breaks@4.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-newline-to-break: 2.0.0
|
||||
unified: 11.0.0
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-start-dragging",
|
||||
"opener:default",
|
||||
"dialog:default"
|
||||
|
||||
@@ -552,12 +552,37 @@ struct SyncStats {
|
||||
skipped: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum SyncFileAction {
|
||||
Upload,
|
||||
Download,
|
||||
DeleteLocal,
|
||||
DeleteRemote,
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SyncFileDetail {
|
||||
path: String,
|
||||
action: SyncFileAction,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SyncResult {
|
||||
message: String,
|
||||
last_sync_at: String,
|
||||
stats: SyncStats,
|
||||
files: Vec<SyncFileDetail>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SyncConfigSaveResult {
|
||||
config_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
@@ -1173,6 +1198,7 @@ async fn sync_workspace(
|
||||
let remote_files = scan_remote_files(&op).await?;
|
||||
let mut history = load_sync_history(app, &key)?;
|
||||
let mut stats = SyncStats::default();
|
||||
let mut file_details = Vec::new();
|
||||
|
||||
let all_paths: BTreeSet<String> = local_files
|
||||
.keys()
|
||||
@@ -1199,6 +1225,11 @@ async fn sync_workspace(
|
||||
)
|
||||
.await?;
|
||||
stats.uploaded += 1;
|
||||
file_details.push(SyncFileDetail {
|
||||
path: relative.clone(),
|
||||
action: SyncFileAction::Upload,
|
||||
size: local.map(|item| item.size).unwrap_or(0),
|
||||
});
|
||||
}
|
||||
SyncAction::Download => {
|
||||
let local_path = local_path_for(workspace, &relative);
|
||||
@@ -1216,6 +1247,11 @@ async fn sync_workspace(
|
||||
fs::write(&local_path, bytes)
|
||||
.map_err(|e| format!("无法写入本地文件 {}: {}", local_path.display(), e))?;
|
||||
stats.downloaded += 1;
|
||||
file_details.push(SyncFileDetail {
|
||||
path: relative.clone(),
|
||||
action: SyncFileAction::Download,
|
||||
size: remote.map(|item| item.size).unwrap_or(0),
|
||||
});
|
||||
}
|
||||
SyncAction::DeleteLocal => {
|
||||
let local_path = local_path_for(workspace, &relative);
|
||||
@@ -1225,6 +1261,11 @@ async fn sync_workspace(
|
||||
}
|
||||
history.files.remove(&relative);
|
||||
stats.deleted_local += 1;
|
||||
file_details.push(SyncFileDetail {
|
||||
path: relative.clone(),
|
||||
action: SyncFileAction::DeleteLocal,
|
||||
size: local.map(|item| item.size).unwrap_or(0),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
SyncAction::DeleteRemote => {
|
||||
@@ -1236,10 +1277,23 @@ async fn sync_workspace(
|
||||
.await?;
|
||||
history.files.remove(&relative);
|
||||
stats.deleted_remote += 1;
|
||||
file_details.push(SyncFileDetail {
|
||||
path: relative.clone(),
|
||||
action: SyncFileAction::DeleteRemote,
|
||||
size: remote.map(|item| item.size).unwrap_or(0),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
SyncAction::Skip => {
|
||||
stats.skipped += 1;
|
||||
file_details.push(SyncFileDetail {
|
||||
path: relative.clone(),
|
||||
action: SyncFileAction::Skip,
|
||||
size: local
|
||||
.map(|item| item.size)
|
||||
.or_else(|| remote.map(|item| item.size))
|
||||
.unwrap_or(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1282,6 +1336,7 @@ async fn sync_workspace(
|
||||
message: "同步完成".to_string(),
|
||||
last_sync_at,
|
||||
stats,
|
||||
files: file_details,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1291,8 +1346,13 @@ fn load_sync_config(app: tauri::AppHandle) -> Result<SyncConfig, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_sync_config(app: tauri::AppHandle, config: SyncConfig) -> Result<(), String> {
|
||||
save_sync_config_to_disk(&app, &config)
|
||||
fn save_sync_config(
|
||||
app: tauri::AppHandle,
|
||||
config: SyncConfig,
|
||||
) -> Result<SyncConfigSaveResult, String> {
|
||||
save_sync_config_to_disk(&app, &config)?;
|
||||
let config_path = sync_config_path(&app)?.to_string_lossy().into_owned();
|
||||
Ok(SyncConfigSaveResult { config_path })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1430,7 +1490,7 @@ pub fn run() {
|
||||
let id = event.id().0.as_str();
|
||||
match id {
|
||||
"quit" => {
|
||||
std::process::exit(0);
|
||||
let _ = app_handle.emit("menu-quit", ());
|
||||
}
|
||||
"open" => {
|
||||
let handle = app_handle.clone();
|
||||
|
||||
207
src/App.vue
207
src/App.vue
@@ -2,7 +2,7 @@
|
||||
import {computed, ref, watch, onMounted, onUnmounted, nextTick} from "vue";
|
||||
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
||||
import {convertFileSrc, invoke, isTauri} from "@tauri-apps/api/core";
|
||||
import {getCurrentWindow} from "@tauri-apps/api/window";
|
||||
import {getCurrentWindow, type CloseRequestedEvent} from "@tauri-apps/api/window";
|
||||
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
||||
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
||||
import OutlinePanel from "./components/OutlinePanel.vue";
|
||||
@@ -17,6 +17,7 @@ type WorkspaceFileType = "markdown" | "image" | "video";
|
||||
type SyncProvider = "none" | "webdav" | "s3";
|
||||
const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode";
|
||||
const APP_THEME_KEY = "yurou:app-theme";
|
||||
const RENDER_SINGLE_LINE_BREAKS_KEY = "yurou:render-single-line-breaks";
|
||||
const MARKDOWN_EXTENSIONS = new Set(["md"]);
|
||||
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif"]);
|
||||
const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "ogv"]);
|
||||
@@ -79,7 +80,10 @@ const mediaPreviewSrc = ref("");
|
||||
const mediaPreviewError = ref("");
|
||||
const mediaPreviewKey = ref(0);
|
||||
const saveStatus = ref("");
|
||||
const saveNotice = ref("");
|
||||
const saveNoticeType = ref<"success" | "error">("success");
|
||||
const hasDraftDocument = ref(false);
|
||||
let lastSavedContent = "";
|
||||
|
||||
// Folder state
|
||||
interface DirEntry {
|
||||
@@ -105,6 +109,21 @@ interface WorkspaceWatchErrorPayload {
|
||||
message: string;
|
||||
}
|
||||
|
||||
function loadRenderSingleLineBreaks() {
|
||||
try {
|
||||
return localStorage.getItem(RENDER_SINGLE_LINE_BREAKS_KEY) === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveRenderSingleLineBreaks(value: boolean) {
|
||||
try {
|
||||
localStorage.setItem(RENDER_SINGLE_LINE_BREAKS_KEY, String(value));
|
||||
} catch { /* noop */
|
||||
}
|
||||
}
|
||||
|
||||
interface SyncConfig {
|
||||
provider: SyncProvider;
|
||||
webdav: {
|
||||
@@ -152,6 +171,9 @@ let workspaceRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let workspaceRefreshInFlight = false;
|
||||
let workspaceRefreshQueued = false;
|
||||
let syncInFlight = false;
|
||||
let fileOpenRequest = 0;
|
||||
let saveNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let closeSaveInFlight = false;
|
||||
|
||||
// ---- editor zoom (Ctrl+scroll) ----
|
||||
const ZOOM_MIN = 0.7;
|
||||
@@ -207,6 +229,7 @@ const showOutline = ref(false);
|
||||
const defaultDocumentMode = ref<DocumentMode>(loadDefaultDocumentMode());
|
||||
const documentMode = ref<DocumentMode>(defaultDocumentMode.value);
|
||||
const appTheme = ref<AppTheme>(loadAppTheme());
|
||||
const renderSingleLineBreaks = ref(loadRenderSingleLineBreaks());
|
||||
const showMoreMenu = ref(false);
|
||||
const settingsRequest = ref(0);
|
||||
const settingsInitialPage = ref<"about" | "editor" | "sync">("about");
|
||||
@@ -230,6 +253,7 @@ type TitlebarDragState = {
|
||||
const TITLEBAR_DRAG_THRESHOLD = 4;
|
||||
let titlebarDragState: TitlebarDragState | null = null;
|
||||
let unlistenWindowResize: (() => void) | null = null;
|
||||
let unlistenWindowCloseRequested: (() => void) | null = null;
|
||||
|
||||
function isInteractiveTitlebarTarget(target: EventTarget | null) {
|
||||
if (!(target instanceof Element)) return false;
|
||||
@@ -321,6 +345,26 @@ function openSettings(page: "about" | "editor" | "sync" = "about") {
|
||||
settingsRequest.value += 1;
|
||||
}
|
||||
|
||||
async function onWindowCloseRequested(event: CloseRequestedEvent) {
|
||||
if (closeSaveInFlight) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
closeSaveInFlight = true;
|
||||
const shouldShowClosingSave = isCurrentMarkdownDocumentDirty();
|
||||
const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
|
||||
if (!saved) {
|
||||
closeSaveInFlight = false;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldShowClosingSave) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
}
|
||||
}
|
||||
|
||||
function setDocumentMode(mode: DocumentMode) {
|
||||
documentMode.value = mode;
|
||||
}
|
||||
@@ -367,6 +411,11 @@ onMounted(() => {
|
||||
}).catch((error) => {
|
||||
console.error("Failed to listen for window resize:", error);
|
||||
});
|
||||
void getCurrentWindow().onCloseRequested(onWindowCloseRequested).then((unlisten) => {
|
||||
unlistenWindowCloseRequested = unlisten;
|
||||
}).catch((error) => {
|
||||
console.error("Failed to listen for window close requests:", error);
|
||||
});
|
||||
}
|
||||
void restoreLastWorkspace();
|
||||
});
|
||||
@@ -378,7 +427,12 @@ onUnmounted(() => {
|
||||
unlistenWindowResize();
|
||||
unlistenWindowResize = null;
|
||||
}
|
||||
if (unlistenWindowCloseRequested) {
|
||||
unlistenWindowCloseRequested();
|
||||
unlistenWindowCloseRequested = null;
|
||||
}
|
||||
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
|
||||
if (saveNoticeTimer) clearTimeout(saveNoticeTimer);
|
||||
});
|
||||
|
||||
function stripMarkdown(md: string): string {
|
||||
@@ -422,7 +476,9 @@ function setDocumentContent(md: string) {
|
||||
}
|
||||
|
||||
function resetMarkdownEditorState() {
|
||||
fileOpenRequest += 1;
|
||||
setDocumentContent("");
|
||||
lastSavedContent = "";
|
||||
currentFilePath.value = null;
|
||||
currentFileType.value = "markdown";
|
||||
resetMediaPreviewState();
|
||||
@@ -601,8 +657,14 @@ async function saveCurrentWorkspaceDocumentBeforeSwitch() {
|
||||
|
||||
const contentToSave = getLatestEditorContent();
|
||||
if (!currentFilePath.value && !contentToSave.trim()) return true;
|
||||
if (contentToSave === lastSavedContent) return true;
|
||||
|
||||
return doSave(contentToSave, { syncAfterSave: false });
|
||||
return doSave(contentToSave, { syncAfterSave: false, trigger: "automatic" });
|
||||
}
|
||||
|
||||
function updateRenderSingleLineBreaks(value: boolean) {
|
||||
renderSingleLineBreaks.value = value;
|
||||
saveRenderSingleLineBreaks(value);
|
||||
}
|
||||
|
||||
function resetWorkspaceAndEditorState(options: { stopWatcher?: boolean } = {}) {
|
||||
@@ -696,6 +758,7 @@ async function refreshCurrentOpenFileFromDisk() {
|
||||
if (isMarkdownDocument.value) {
|
||||
const fileContent = await invoke<string>("read_file", { path: currentFilePath.value });
|
||||
setDocumentContent(fileContent);
|
||||
lastSavedContent = fileContent;
|
||||
hasDraftDocument.value = false;
|
||||
editorResetKey.value += 1;
|
||||
return;
|
||||
@@ -826,6 +889,7 @@ function openMediaPreview(path: string, fileType: Extract<WorkspaceFileType, "im
|
||||
mediaPreviewError.value = "";
|
||||
mediaPreviewKey.value += 1;
|
||||
setDocumentContent("");
|
||||
lastSavedContent = "";
|
||||
hasDraftDocument.value = false;
|
||||
saveStatus.value = "";
|
||||
showOutline.value = false;
|
||||
@@ -984,6 +1048,15 @@ async function onRenameEntry(path: string, newName: string) {
|
||||
}
|
||||
|
||||
async function onOpenFile(path: string) {
|
||||
const request = ++fileOpenRequest;
|
||||
|
||||
if (currentFilePath.value && getWorkspaceIdentity(currentFilePath.value) === getWorkspaceIdentity(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
|
||||
if (!saved || request !== fileOpenRequest) return;
|
||||
|
||||
const fileType = getWorkspaceFileType(path);
|
||||
if (fileType === "image" || fileType === "video") {
|
||||
openMediaPreview(path, fileType);
|
||||
@@ -992,9 +1065,11 @@ async function onOpenFile(path: string) {
|
||||
|
||||
try {
|
||||
const fileContent = await invoke<string>("read_file", {path});
|
||||
if (request !== fileOpenRequest) return;
|
||||
currentFileType.value = "markdown";
|
||||
resetMediaPreviewState();
|
||||
setDocumentContent(fileContent);
|
||||
lastSavedContent = fileContent;
|
||||
currentFilePath.value = path;
|
||||
hasDraftDocument.value = false;
|
||||
saveStatus.value = "";
|
||||
@@ -1009,7 +1084,29 @@ function getLatestEditorContent(contentOverride?: string) {
|
||||
return markdownEditorRef.value?.flushPendingChanges() ?? content.value;
|
||||
}
|
||||
|
||||
async function doSave(contentOverride?: string, options: { syncAfterSave?: boolean } = {}) {
|
||||
function isCurrentMarkdownDocumentDirty() {
|
||||
if (!isMarkdownDocument.value) return false;
|
||||
if (!currentFilePath.value && !hasDraftDocument.value) return false;
|
||||
|
||||
const latestContent = getLatestEditorContent();
|
||||
if (!currentFilePath.value && !latestContent.trim()) return false;
|
||||
return latestContent !== lastSavedContent;
|
||||
}
|
||||
|
||||
function showSaveNotice(message: string, type: "success" | "error" = "success") {
|
||||
if (saveNoticeTimer) clearTimeout(saveNoticeTimer);
|
||||
saveNotice.value = message;
|
||||
saveNoticeType.value = type;
|
||||
saveNoticeTimer = setTimeout(() => {
|
||||
saveNotice.value = "";
|
||||
saveNoticeTimer = null;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function doSave(
|
||||
contentOverride?: string,
|
||||
options: { syncAfterSave?: boolean; trigger?: "manual" | "automatic" } = {},
|
||||
) {
|
||||
if (!isMarkdownDocument.value) return false;
|
||||
|
||||
const contentToSave = getLatestEditorContent(contentOverride);
|
||||
@@ -1018,17 +1115,25 @@ async function doSave(contentOverride?: string, options: { syncAfterSave?: boole
|
||||
// No file path yet — prompt user to pick a save location
|
||||
try {
|
||||
const chosen = await invoke<string | null>("pick_save_file");
|
||||
if (!chosen) return false; // user cancelled
|
||||
if (!chosen) {
|
||||
showSaveNotice(options.trigger === "automatic" ? "已取消自动保存" : "已取消保存", "error");
|
||||
return false;
|
||||
}
|
||||
currentFilePath.value = chosen;
|
||||
} catch (e) {
|
||||
console.error("Save dialog failed:", e);
|
||||
showSaveNotice(`保存失败: ${e}`, "error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
|
||||
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
|
||||
const fileName = getFileName(currentFilePath.value);
|
||||
const message = options.trigger === "automatic" ? `已自动保存: ${fileName}` : `已手动保存: ${fileName}`;
|
||||
saveStatus.value = message;
|
||||
showSaveNotice(message);
|
||||
lastSavedContent = contentToSave;
|
||||
setTimeout(() => {
|
||||
saveStatus.value = "";
|
||||
}, 3000);
|
||||
@@ -1040,6 +1145,7 @@ async function doSave(contentOverride?: string, options: { syncAfterSave?: boole
|
||||
} catch (e) {
|
||||
console.error("Save failed:", e);
|
||||
saveStatus.value = `保存失败: ${e}`;
|
||||
showSaveNotice(saveStatus.value, "error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1051,10 +1157,15 @@ async function doSaveAs() {
|
||||
|
||||
try {
|
||||
const chosen = await invoke<string | null>("pick_save_file");
|
||||
if (!chosen) return; // user cancelled
|
||||
if (!chosen) {
|
||||
showSaveNotice("已取消另存为", "error");
|
||||
return;
|
||||
}
|
||||
currentFilePath.value = chosen;
|
||||
await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
|
||||
saveStatus.value = `已保存: ${getFileName(currentFilePath.value)}`;
|
||||
saveStatus.value = `已手动保存: ${getFileName(currentFilePath.value)}`;
|
||||
showSaveNotice(saveStatus.value);
|
||||
lastSavedContent = contentToSave;
|
||||
setTimeout(() => {
|
||||
saveStatus.value = "";
|
||||
}, 3000);
|
||||
@@ -1063,13 +1174,19 @@ async function doSaveAs() {
|
||||
} catch (e) {
|
||||
console.error("Save failed:", e);
|
||||
saveStatus.value = `保存失败: ${e}`;
|
||||
showSaveNotice(saveStatus.value, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function doNew() {
|
||||
async function doNew() {
|
||||
fileOpenRequest += 1;
|
||||
const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
|
||||
if (!saved) return;
|
||||
|
||||
currentFileType.value = "markdown";
|
||||
resetMediaPreviewState();
|
||||
setDocumentContent("");
|
||||
lastSavedContent = "";
|
||||
currentFilePath.value = null;
|
||||
hasDraftDocument.value = true;
|
||||
saveStatus.value = "";
|
||||
@@ -1112,7 +1229,11 @@ registerTauriListener("menu-save-as", () => {
|
||||
});
|
||||
|
||||
registerTauriListener("menu-new", () => {
|
||||
doNew();
|
||||
void doNew();
|
||||
});
|
||||
|
||||
registerTauriListener("menu-quit", () => {
|
||||
void closeWindow();
|
||||
});
|
||||
|
||||
registerTauriListener("show-about", () => {
|
||||
@@ -1147,6 +1268,7 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
||||
:settings-initial-page="settingsInitialPage"
|
||||
:default-document-mode="defaultDocumentMode"
|
||||
:app-theme="appTheme"
|
||||
:render-single-line-breaks="renderSingleLineBreaks"
|
||||
:recent-workspaces="recentWorkspaces"
|
||||
@open-folder="onOpenFolder"
|
||||
@switch-workspace="(path: string) => switchWorkspace(path)"
|
||||
@@ -1160,6 +1282,7 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
||||
@search-result-click="handleSearchResultClick"
|
||||
@update-default-document-mode="updateDefaultDocumentMode"
|
||||
@update-app-theme="updateAppTheme"
|
||||
@update-render-single-line-breaks="updateRenderSingleLineBreaks"
|
||||
@prepare-workspace-sync="prepareWorkspaceSync"
|
||||
@workspace-synced="handleWorkspaceSynced"
|
||||
/>
|
||||
@@ -1307,6 +1430,7 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
||||
:model-value="content"
|
||||
:document-mode="documentMode"
|
||||
:file-path="currentFilePath"
|
||||
:render-single-line-breaks="renderSingleLineBreaks"
|
||||
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等"
|
||||
@update:model-value="onUpdate"
|
||||
@save-shortcut="doSave"
|
||||
@@ -1379,6 +1503,18 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
||||
{{ Math.round(editorZoom * 100) }}%
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="save-notice">
|
||||
<div
|
||||
v-if="saveNotice"
|
||||
class="save-notice fixed top-12 right-4 z-50 pointer-events-none select-none"
|
||||
:class="{ 'save-notice-error': saveNoticeType === 'error' }"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<i :class="saveNoticeType === 'error' ? 'ri-error-warning-line' : 'ri-checkbox-circle-line'"></i>
|
||||
<span>{{ saveNotice }}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</main>
|
||||
</div>
|
||||
<!-- <footer class="app-footer backdrop-blur-sm">-->
|
||||
@@ -1416,6 +1552,17 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.save-notice-enter-active,
|
||||
.save-notice-leave-active {
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.save-notice-enter-from,
|
||||
.save-notice-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
.outline-slide-enter-active,
|
||||
.outline-slide-leave-active {
|
||||
transition:
|
||||
@@ -1465,6 +1612,48 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
||||
color: var(--app-control-contrast);
|
||||
}
|
||||
|
||||
.save-notice {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
max-width: min(360px, calc(100vw - 32px));
|
||||
min-height: 32px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 6px;
|
||||
background: #f0fdf4;
|
||||
color: #14532d;
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 0, 0, 0.28),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.save-notice i {
|
||||
flex: 0 0 auto;
|
||||
color: #15803d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-notice span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.save-notice-error {
|
||||
border-color: #fca5a5;
|
||||
background: #fef2f2;
|
||||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
.save-notice-error i {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
/* ── Media preview ──────────────────────────────────────────────── */
|
||||
|
||||
.media-preview {
|
||||
|
||||
@@ -31,6 +31,7 @@ const props = withDefaults(
|
||||
settingsInitialPage?: SettingsPage;
|
||||
defaultDocumentMode?: DocumentMode;
|
||||
appTheme?: AppTheme;
|
||||
renderSingleLineBreaks?: boolean;
|
||||
recentWorkspaces?: WorkspaceEntry[];
|
||||
}>(),
|
||||
{
|
||||
@@ -39,6 +40,7 @@ const props = withDefaults(
|
||||
settingsInitialPage: "about",
|
||||
defaultDocumentMode: "edit",
|
||||
appTheme: "warm",
|
||||
renderSingleLineBreaks: false,
|
||||
recentWorkspaces: () => [],
|
||||
},
|
||||
);
|
||||
@@ -55,6 +57,7 @@ const emit = defineEmits<{
|
||||
renameEntry: [path: string, newName: string];
|
||||
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||
updateAppTheme: [theme: AppTheme];
|
||||
updateRenderSingleLineBreaks: [value: boolean];
|
||||
prepareWorkspaceSync: [done: (saved: boolean) => void];
|
||||
workspaceSynced: [];
|
||||
"search-result-click": [filePath: string, lineNumber: number];
|
||||
@@ -842,8 +845,10 @@ function handleCreateRootFile() {
|
||||
:workspace-path="folderPath"
|
||||
:default-document-mode="defaultDocumentMode"
|
||||
:app-theme="appTheme"
|
||||
:render-single-line-breaks="renderSingleLineBreaks"
|
||||
@update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
|
||||
@update-app-theme="(theme: AppTheme) => emit('updateAppTheme', theme)"
|
||||
@update-render-single-line-breaks="(value: boolean) => emit('updateRenderSingleLineBreaks', value)"
|
||||
@prepare-workspace-sync="forwardPrepareWorkspaceSync"
|
||||
@workspace-synced="emit('workspaceSynced')"
|
||||
/>
|
||||
|
||||
476
src/components/MarkdownCodeEditor.vue
Normal file
476
src/components/MarkdownCodeEditor.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import {
|
||||
acceptCompletion,
|
||||
autocompletion,
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
pickedCompletion,
|
||||
startCompletion,
|
||||
type CompletionContext,
|
||||
type CompletionResult,
|
||||
} from "@codemirror/autocomplete";
|
||||
import {
|
||||
defaultKeymap,
|
||||
history,
|
||||
historyKeymap,
|
||||
indentWithTab,
|
||||
} from "@codemirror/commands";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { languages } from "@codemirror/language-data";
|
||||
import {
|
||||
bracketMatching,
|
||||
defaultHighlightStyle,
|
||||
foldGutter,
|
||||
HighlightStyle,
|
||||
indentOnInput,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language";
|
||||
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
||||
import {
|
||||
EditorSelection,
|
||||
EditorState,
|
||||
StateEffect,
|
||||
StateField,
|
||||
type Text,
|
||||
} from "@codemirror/state";
|
||||
import {
|
||||
crosshairCursor,
|
||||
Decoration,
|
||||
type DecorationSet,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
placeholder as placeholderExtension,
|
||||
rectangularSelection,
|
||||
} from "@codemirror/view";
|
||||
import { tags } from "@lezer/highlight";
|
||||
import { highlightCodeToTokens } from "@/lib/codeHighlight";
|
||||
import { getMarkdownLanguageCompletions } from "@/lib/markdownLanguages";
|
||||
import { resolveMarkdownCodeLanguage } from "@/lib/markdownLanguages";
|
||||
|
||||
interface PastePayload {
|
||||
event: ClipboardEvent;
|
||||
value: string;
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string;
|
||||
placeholder?: string;
|
||||
autofocus?: boolean;
|
||||
minHeight?: string;
|
||||
}>(),
|
||||
{
|
||||
placeholder: "",
|
||||
autofocus: false,
|
||||
minHeight: "1.625em",
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
blur: [];
|
||||
escape: [];
|
||||
paste: [payload: PastePayload];
|
||||
}>();
|
||||
|
||||
const hostRef = ref<HTMLDivElement>();
|
||||
let editorView: EditorView | null = null;
|
||||
let highlightRequestId = 0;
|
||||
|
||||
const setCodeDecorations = StateEffect.define<DecorationSet>();
|
||||
const codeDecorationField = StateField.define<DecorationSet>({
|
||||
create: () => Decoration.none,
|
||||
update(value, transaction) {
|
||||
let next = value.map(transaction.changes);
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(setCodeDecorations)) next = effect.value;
|
||||
}
|
||||
return next;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
|
||||
interface FencedCodeBlock {
|
||||
from: number;
|
||||
to: number;
|
||||
code: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
function findFencedCodeBlocks(doc: Text): FencedCodeBlock[] {
|
||||
const blocks: FencedCodeBlock[] = [];
|
||||
|
||||
for (let lineNumber = 1; lineNumber <= doc.lines; lineNumber += 1) {
|
||||
const line = doc.line(lineNumber);
|
||||
const opener = /^(\s*)(`{3,}|~{3,})\s*([\w.+#-]*)/.exec(line.text);
|
||||
if (!opener) continue;
|
||||
|
||||
const fenceChar = opener[2][0];
|
||||
const fenceLength = opener[2].length;
|
||||
const closingPattern = new RegExp(`^\\s*${fenceChar === "`" ? "`" : "~"}{${fenceLength},}\\s*$`);
|
||||
const contentFrom = Math.min(doc.length, line.to + 1);
|
||||
|
||||
for (let closingLineNumber = lineNumber + 1; closingLineNumber <= doc.lines; closingLineNumber += 1) {
|
||||
const closingLine = doc.line(closingLineNumber);
|
||||
if (!closingPattern.test(closingLine.text)) continue;
|
||||
|
||||
const requestedLanguage = opener[3] || "text";
|
||||
blocks.push({
|
||||
from: contentFrom,
|
||||
to: closingLine.from,
|
||||
code: doc.sliceString(contentFrom, closingLine.from),
|
||||
language: resolveMarkdownCodeLanguage(requestedLanguage)?.shiki || "text",
|
||||
});
|
||||
lineNumber = closingLineNumber;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
async function updateCodeDecorations(view: EditorView) {
|
||||
const requestId = ++highlightRequestId;
|
||||
const documentText = view.state.doc.toString();
|
||||
const blocks = findFencedCodeBlocks(view.state.doc);
|
||||
const ranges: ReturnType<Decoration["range"]>[] = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
const tokenLines = await highlightCodeToTokens(block.code, block.language);
|
||||
if (!tokenLines || requestId !== highlightRequestId) return;
|
||||
|
||||
for (const tokens of tokenLines) {
|
||||
for (const token of tokens) {
|
||||
if (!token.content || !token.color) continue;
|
||||
const from = block.from + token.offset;
|
||||
const to = Math.min(block.to, from + token.content.length);
|
||||
if (to <= from) continue;
|
||||
ranges.push(
|
||||
Decoration.mark({
|
||||
class: "cm-shiki-token",
|
||||
attributes: { style: `color: ${token.color}` },
|
||||
}).range(from, to),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
requestId !== highlightRequestId ||
|
||||
!editorView ||
|
||||
editorView !== view ||
|
||||
view.state.doc.toString() !== documentText
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.dispatch({ effects: setCodeDecorations.of(Decoration.set(ranges, true)) });
|
||||
}
|
||||
|
||||
function fenceLanguageCompletion(context: CompletionContext): CompletionResult | null {
|
||||
const line = context.state.doc.lineAt(context.pos);
|
||||
const beforeCursor = context.state.sliceDoc(line.from, context.pos);
|
||||
const match = /^(\s*)(`{3,}|~{3,})([\w.+#-]*)$/.exec(beforeCursor);
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
from: line.from + match[1].length + match[2].length,
|
||||
options: getMarkdownLanguageCompletions(),
|
||||
validFor: /^[\w.+#-]*$/,
|
||||
};
|
||||
}
|
||||
|
||||
function insertClosedCodeFence(view: EditorView) {
|
||||
const selection = view.state.selection.main;
|
||||
if (!selection.empty) return false;
|
||||
|
||||
const line = view.state.doc.lineAt(selection.head);
|
||||
const beforeCursor = view.state.sliceDoc(line.from, selection.head);
|
||||
const afterCursor = view.state.sliceDoc(selection.head, line.to);
|
||||
const match = /^(\s*)(`{3,}|~{3,})[\w.+#-]*\s*$/.exec(beforeCursor);
|
||||
if (!match || afterCursor.trim()) return false;
|
||||
|
||||
const nextLine = line.number < view.state.doc.lines
|
||||
? view.state.doc.line(line.number + 1).text
|
||||
: "";
|
||||
const fence = match[2][0].repeat(match[2].length);
|
||||
if (nextLine.trimStart().startsWith(fence)) return false;
|
||||
|
||||
const insert = `\n${match[1]}\n${match[1]}${fence}`;
|
||||
view.dispatch({
|
||||
changes: { from: selection.head, insert },
|
||||
selection: { anchor: selection.head + 1 + match[1].length },
|
||||
scrollIntoView: true,
|
||||
userEvent: "input",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function acceptCompletionOrCloseFence(view: EditorView) {
|
||||
return acceptCompletion(view) || insertClosedCodeFence(view);
|
||||
}
|
||||
|
||||
const editorHighlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.heading, color: "var(--app-text-strong)", fontWeight: "700" },
|
||||
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||
{ tag: tags.strong, fontWeight: "700" },
|
||||
{ tag: tags.link, color: "var(--code-token-link)", textDecoration: "underline" },
|
||||
{ tag: tags.url, color: "var(--code-token-link)" },
|
||||
{
|
||||
tag: tags.monospace,
|
||||
color: "var(--code-token-string)",
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
},
|
||||
{ tag: [tags.keyword, tags.operatorKeyword], color: "var(--code-token-keyword)" },
|
||||
{ tag: [tags.string, tags.regexp], color: "var(--code-token-string-expression)" },
|
||||
{ tag: [tags.number, tags.bool, tags.null, tags.atom], color: "var(--code-token-constant)" },
|
||||
{ tag: [tags.comment, tags.meta], color: "var(--code-token-comment)" },
|
||||
{
|
||||
tag: [tags.function(tags.variableName), tags.labelName],
|
||||
color: "var(--code-token-function)",
|
||||
},
|
||||
{
|
||||
tag: [tags.typeName, tags.className, tags.namespace, tags.tagName],
|
||||
color: "var(--code-token-function)",
|
||||
},
|
||||
{ tag: tags.propertyName, color: "var(--code-token-constant)" },
|
||||
{ tag: tags.attributeName, color: "var(--code-token-parameter)" },
|
||||
{ tag: [tags.punctuation, tags.separator, tags.bracket], color: "var(--code-token-punctuation)" },
|
||||
{ tag: tags.inserted, color: "var(--code-token-inserted)" },
|
||||
{ tag: tags.deleted, color: "var(--code-token-deleted)" },
|
||||
{ tag: tags.changed, color: "var(--code-token-changed)" },
|
||||
{ tag: tags.invalid, color: "var(--code-token-deleted)" },
|
||||
]);
|
||||
|
||||
const editorTheme = EditorView.theme({
|
||||
"&": {
|
||||
color: "var(--code-foreground)",
|
||||
backgroundColor: "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "inherit",
|
||||
minHeight: "var(--markdown-code-editor-min-height)",
|
||||
},
|
||||
"&.cm-focused": { outline: "none" },
|
||||
".cm-scroller": {
|
||||
fontFamily: "inherit",
|
||||
lineHeight: "1.625",
|
||||
overflow: "visible",
|
||||
},
|
||||
".cm-content": {
|
||||
padding: "0",
|
||||
minHeight: "var(--markdown-code-editor-min-height)",
|
||||
caretColor: "var(--app-accent)",
|
||||
},
|
||||
".cm-line": { padding: "0" },
|
||||
".cm-shiki-token > span": { color: "inherit !important" },
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--app-accent)" },
|
||||
".cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection": {
|
||||
backgroundColor: "var(--app-selection) !important",
|
||||
},
|
||||
".cm-placeholder": { color: "var(--app-subtle)" },
|
||||
".cm-activeLine": { backgroundColor: "transparent" },
|
||||
".cm-gutters": { display: "none" },
|
||||
".cm-tooltip": {
|
||||
border: "1px solid var(--app-border-strong)",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "var(--app-surface)",
|
||||
color: "var(--app-text)",
|
||||
boxShadow: "0 10px 28px var(--app-shadow)",
|
||||
overflow: "hidden",
|
||||
},
|
||||
".cm-tooltip-autocomplete > ul": {
|
||||
fontFamily: '"Geist", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
maxHeight: "240px",
|
||||
},
|
||||
".cm-tooltip-autocomplete > ul > li": {
|
||||
minWidth: "220px",
|
||||
padding: "6px 10px",
|
||||
},
|
||||
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
|
||||
backgroundColor: "var(--app-active-bg)",
|
||||
color: "var(--app-text-strong)",
|
||||
},
|
||||
".cm-completionLabel": { fontWeight: "600" },
|
||||
".cm-completionDetail": { color: "var(--app-muted)", fontStyle: "normal" },
|
||||
});
|
||||
|
||||
function createEditor() {
|
||||
const host = hostRef.value;
|
||||
if (!host) return;
|
||||
|
||||
editorView = new EditorView({
|
||||
parent: host,
|
||||
state: EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions: [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSelectionMatches(),
|
||||
markdown({ codeLanguages: languages }),
|
||||
syntaxHighlighting(editorHighlightStyle),
|
||||
codeDecorationField,
|
||||
closeBrackets(),
|
||||
autocompletion({ override: [fenceLanguageCompletion], activateOnTyping: true }),
|
||||
EditorView.lineWrapping,
|
||||
editorTheme,
|
||||
placeholderExtension(props.placeholder),
|
||||
keymap.of([
|
||||
{ key: "Enter", run: acceptCompletionOrCloseFence },
|
||||
...completionKeymap,
|
||||
{
|
||||
key: "Escape",
|
||||
run() {
|
||||
emit("escape");
|
||||
return true;
|
||||
},
|
||||
},
|
||||
indentWithTab,
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
emit("update:modelValue", update.state.doc.toString());
|
||||
void updateCodeDecorations(update.view);
|
||||
}
|
||||
if (
|
||||
!update.docChanged ||
|
||||
update.transactions.some((transaction) => transaction.annotation(pickedCompletion))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = update.state.selection.main;
|
||||
if (!selection.empty) return;
|
||||
const line = update.state.doc.lineAt(selection.head);
|
||||
const beforeCursor = update.state.sliceDoc(line.from, selection.head);
|
||||
if (/^\s*(`{3,}|~{3,})[\w.+#-]*$/.test(beforeCursor)) {
|
||||
queueMicrotask(() => {
|
||||
if (editorView) startCompletion(editorView);
|
||||
});
|
||||
}
|
||||
}),
|
||||
EditorView.domEventHandlers({
|
||||
blur() {
|
||||
emit("blur");
|
||||
return false;
|
||||
},
|
||||
paste(event, view) {
|
||||
const selection = view.state.selection.main;
|
||||
emit("paste", {
|
||||
event,
|
||||
value: view.state.doc.toString(),
|
||||
from: selection.from,
|
||||
to: selection.to,
|
||||
});
|
||||
return event.defaultPrevented;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
if (props.autofocus) {
|
||||
requestAnimationFrame(() => focusAtEnd());
|
||||
}
|
||||
void updateCodeDecorations(editorView);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const view = editorView;
|
||||
if (!view || value === view.state.doc.toString()) return;
|
||||
|
||||
const cursor = Math.min(view.state.selection.main.head, value.length);
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: value },
|
||||
selection: EditorSelection.cursor(cursor),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
function focus() {
|
||||
editorView?.focus();
|
||||
}
|
||||
|
||||
function focusAtEnd() {
|
||||
const view = editorView;
|
||||
if (!view) return;
|
||||
const end = view.state.doc.length;
|
||||
view.dispatch({ selection: EditorSelection.cursor(end), scrollIntoView: true });
|
||||
view.focus();
|
||||
}
|
||||
|
||||
function replaceRange(text: string, from?: number, to?: number) {
|
||||
const view = editorView;
|
||||
if (!view) return;
|
||||
const selection = view.state.selection.main;
|
||||
const replaceFrom = from ?? selection.from;
|
||||
const replaceTo = to ?? selection.to;
|
||||
const cursor = replaceFrom + text.length;
|
||||
view.dispatch({
|
||||
changes: { from: replaceFrom, to: replaceTo, insert: text },
|
||||
selection: EditorSelection.cursor(cursor),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input.paste",
|
||||
});
|
||||
view.focus();
|
||||
}
|
||||
|
||||
function scrollToLine(lineNumber: number) {
|
||||
const view = editorView;
|
||||
if (!view) return;
|
||||
const safeLine = Math.max(1, Math.min(lineNumber, view.state.doc.lines));
|
||||
const line = view.state.doc.line(safeLine);
|
||||
view.dispatch({ selection: EditorSelection.cursor(line.from), scrollIntoView: true });
|
||||
view.focus();
|
||||
}
|
||||
|
||||
defineExpose({ focus, focusAtEnd, replaceRange, scrollToLine });
|
||||
|
||||
onMounted(createEditor);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
highlightRequestId += 1;
|
||||
editorView?.destroy();
|
||||
editorView = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="hostRef"
|
||||
class="markdown-code-editor"
|
||||
:style="{ '--markdown-code-editor-min-height': props.minHeight }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.markdown-code-editor {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { unified, type Plugin, type Transformer } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
||||
@@ -75,6 +76,7 @@ const props = withDefaults(
|
||||
documentMode?: DocumentMode;
|
||||
previewOnly?: boolean;
|
||||
filePath?: string | null;
|
||||
renderSingleLineBreaks?: boolean;
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@@ -82,6 +84,7 @@ const props = withDefaults(
|
||||
documentMode: undefined,
|
||||
previewOnly: false,
|
||||
filePath: null,
|
||||
renderSingleLineBreaks: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -220,14 +223,23 @@ const mediaSanitizeSchema = {
|
||||
tagNames: [...(defaultSchema.tagNames || []), "video"],
|
||||
};
|
||||
|
||||
/** Full pipeline: markdown → HTML */
|
||||
const md2html = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
function createMarkdownProcessor(renderSingleLineBreaks: boolean) {
|
||||
const processor = unified().use(remarkParse).use(remarkGfm);
|
||||
|
||||
if (renderSingleLineBreaks) {
|
||||
processor.use(remarkBreaks);
|
||||
}
|
||||
|
||||
return processor
|
||||
.use(remarkLocalMedia)
|
||||
.use(remarkRehype, { allowDangerousHtml: false })
|
||||
.use(rehypeSanitize, mediaSanitizeSchema)
|
||||
.use(rehypeStringify);
|
||||
}
|
||||
|
||||
/** Full pipelines: standard Markdown and optional single-line breaks. */
|
||||
const standardMd2html = createMarkdownProcessor(false);
|
||||
const singleLineBreaksMd2html = createMarkdownProcessor(true);
|
||||
|
||||
const mdParser = unified().use(remarkParse).use(remarkGfm);
|
||||
|
||||
@@ -350,7 +362,8 @@ async function highlightCodeBlocks(html: string) {
|
||||
}
|
||||
|
||||
async function renderMarkdownHtml(md: string) {
|
||||
const file = md2html.processSync(md);
|
||||
const processor = props.renderSingleLineBreaks ? singleLineBreaksMd2html : standardMd2html;
|
||||
const file = processor.processSync(md);
|
||||
return highlightCodeBlocks(String(file.value));
|
||||
}
|
||||
|
||||
@@ -469,6 +482,13 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.renderSingleLineBreaks,
|
||||
() => {
|
||||
syncRenderedState(props.modelValue);
|
||||
},
|
||||
);
|
||||
|
||||
// ── Editor handlers ────────────────────────────────────────────────
|
||||
|
||||
function focusBlockEditor() {
|
||||
@@ -978,7 +998,10 @@ defineExpose({
|
||||
v-for="block in markdownBlocks"
|
||||
:key="block.id"
|
||||
class="markdown-block"
|
||||
:class="{ 'markdown-block-editing': editingBlockId === block.id }"
|
||||
:class="{
|
||||
'markdown-block-editing': editingBlockId === block.id,
|
||||
'markdown-block-paragraph': block.type === 'paragraph',
|
||||
}"
|
||||
>
|
||||
<MarkdownCodeEditor
|
||||
v-if="editingBlockId === block.id"
|
||||
@@ -1054,7 +1077,16 @@ defineExpose({
|
||||
.markdown-preview h3 { font-size: 1.125em; }
|
||||
.markdown-preview h4 { font-size: 1em; }
|
||||
|
||||
.markdown-preview p { margin: 0.5em 0; }
|
||||
.markdown-preview p { margin: 0; }
|
||||
|
||||
.markdown-preview:not(.markdown-mixed) > p + p,
|
||||
.markdown-mixed
|
||||
> .markdown-block-paragraph
|
||||
+ .markdown-block-paragraph
|
||||
> .markdown-block-preview
|
||||
> p {
|
||||
margin-top: 1.625em;
|
||||
}
|
||||
|
||||
.markdown-preview strong { font-weight: 700; color: var(--app-text-strong); }
|
||||
.markdown-preview em { font-style: italic; }
|
||||
@@ -1183,6 +1215,12 @@ defineExpose({
|
||||
display: block;
|
||||
}
|
||||
|
||||
.markdown-mixed
|
||||
> .markdown-block-paragraph
|
||||
+ .markdown-block-paragraph.markdown-block-editing {
|
||||
margin-top: 1.625em;
|
||||
}
|
||||
|
||||
.markdown-block-editing,
|
||||
.markdown-block-editing:hover {
|
||||
background-color: transparent;
|
||||
|
||||
@@ -41,6 +41,7 @@ interface SyncResult {
|
||||
deletedRemote: number;
|
||||
skipped: number;
|
||||
};
|
||||
files: SyncFileDetail[];
|
||||
}
|
||||
|
||||
interface SyncConnectionTestResult {
|
||||
@@ -48,6 +49,27 @@ interface SyncConnectionTestResult {
|
||||
details: string[];
|
||||
}
|
||||
|
||||
interface SyncConfigSaveResult {
|
||||
configPath: string;
|
||||
}
|
||||
|
||||
type SyncFileAction = "upload" | "download" | "deleteLocal" | "deleteRemote" | "skip";
|
||||
|
||||
interface SyncFileDetail {
|
||||
path: string;
|
||||
action: SyncFileAction;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface SyncFeedback {
|
||||
kind: "progress" | "save" | "test" | "sync" | "error";
|
||||
title: string;
|
||||
summary: string;
|
||||
details?: string[];
|
||||
configPath?: string;
|
||||
result?: SyncResult;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
@@ -55,12 +77,14 @@ const props = withDefaults(
|
||||
workspacePath?: string;
|
||||
defaultDocumentMode?: DocumentMode;
|
||||
appTheme?: AppTheme;
|
||||
renderSingleLineBreaks?: boolean;
|
||||
}>(),
|
||||
{
|
||||
initialPage: "about",
|
||||
workspacePath: "",
|
||||
defaultDocumentMode: "edit",
|
||||
appTheme: "warm",
|
||||
renderSingleLineBreaks: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -68,6 +92,7 @@ const emit = defineEmits<{
|
||||
"update:show": [value: boolean];
|
||||
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||
updateAppTheme: [theme: AppTheme];
|
||||
updateRenderSingleLineBreaks: [value: boolean];
|
||||
prepareWorkspaceSync: [done: (saved: boolean) => void];
|
||||
workspaceSynced: [];
|
||||
}>();
|
||||
@@ -83,8 +108,7 @@ const autoUpdate = ref(loadBooleanPref(AUTO_UPDATE_KEY, true));
|
||||
const language = ref(loadStringPref(LANGUAGE_KEY, "zh-Hans"));
|
||||
const updateStatus = ref("");
|
||||
const syncConfig = ref<SyncConfig>(createDefaultSyncConfig());
|
||||
const syncStatus = ref("");
|
||||
const syncError = ref("");
|
||||
const syncFeedback = ref<SyncFeedback | null>(null);
|
||||
const syncSaving = ref(false);
|
||||
const syncTesting = ref(false);
|
||||
const syncRunning = ref(false);
|
||||
@@ -98,6 +122,32 @@ const pageTitle = computed(() => {
|
||||
});
|
||||
|
||||
const isSyncBusy = computed(() => syncSaving.value || syncTesting.value || syncRunning.value);
|
||||
const syncProviderLabel = computed(() => {
|
||||
if (syncConfig.value.provider === "webdav") return "WebDAV";
|
||||
if (syncConfig.value.provider === "s3") return "S3 兼容";
|
||||
return "关闭同步";
|
||||
});
|
||||
const syncTargetDescription = computed(() => {
|
||||
if (syncConfig.value.provider === "webdav") {
|
||||
const endpoint = syncConfig.value.webdav.endpoint.trim() || "未填写服务器地址";
|
||||
const remoteDir = syncConfig.value.webdav.remoteDir.trim() || "/";
|
||||
return `远端目标:${endpoint} · ${remoteDir}`;
|
||||
}
|
||||
if (syncConfig.value.provider === "s3") {
|
||||
const bucket = syncConfig.value.s3.bucket.trim() || "未填写 Bucket";
|
||||
const prefix = syncConfig.value.s3.remotePrefix.trim() || "/";
|
||||
return `远端目标:Bucket ${bucket} · ${prefix}`;
|
||||
}
|
||||
return "同步已关闭";
|
||||
});
|
||||
const syncFileDetails = computed(() => {
|
||||
const files = syncFeedback.value?.result?.files || [];
|
||||
return [...files].sort((left, right) => {
|
||||
if (left.action === "skip" && right.action !== "skip") return 1;
|
||||
if (left.action !== "skip" && right.action === "skip") return -1;
|
||||
return left.path.localeCompare(right.path);
|
||||
});
|
||||
});
|
||||
|
||||
const menuItems: Array<{
|
||||
id: SettingsPage;
|
||||
@@ -270,33 +320,62 @@ function onDefaultDocumentModeChange(event: Event) {
|
||||
emit("updateDefaultDocumentMode", mode);
|
||||
}
|
||||
|
||||
function setSyncStatus(message: string, isError = false) {
|
||||
if (isError) {
|
||||
syncError.value = message;
|
||||
syncStatus.value = "";
|
||||
} else {
|
||||
syncStatus.value = message;
|
||||
syncError.value = "";
|
||||
function setSyncError(message: string, title = "操作失败") {
|
||||
syncFeedback.value = {
|
||||
kind: "error",
|
||||
title,
|
||||
summary: message,
|
||||
};
|
||||
}
|
||||
|
||||
function syncActionLabel(action: SyncFileAction) {
|
||||
if (action === "upload") return "上传";
|
||||
if (action === "download") return "下载";
|
||||
if (action === "deleteLocal") return "删除本地";
|
||||
if (action === "deleteRemote") return "删除远端";
|
||||
return "未变更";
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number) {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
|
||||
}
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
async function loadSyncConfig() {
|
||||
try {
|
||||
const config = await invoke<SyncConfig>("load_sync_config");
|
||||
syncConfig.value = mergeSyncConfig(config);
|
||||
syncError.value = "";
|
||||
} catch (error) {
|
||||
setSyncStatus(`读取同步配置失败: ${error}`, true);
|
||||
setSyncError(`读取同步配置失败: ${error}`, "无法读取同步配置");
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSyncConfig() {
|
||||
syncSaving.value = true;
|
||||
syncFeedback.value = {
|
||||
kind: "progress",
|
||||
title: "正在保存配置",
|
||||
summary: `正在保存 ${syncProviderLabel.value} 配置。`,
|
||||
};
|
||||
try {
|
||||
await invoke("save_sync_config", { config: syncConfig.value });
|
||||
setSyncStatus("同步配置已保存");
|
||||
const result = await invoke<SyncConfigSaveResult>("save_sync_config", { config: syncConfig.value });
|
||||
syncFeedback.value = {
|
||||
kind: "save",
|
||||
title: "同步配置已保存",
|
||||
summary: `已保存 ${syncProviderLabel.value} 配置。`,
|
||||
configPath: result.configPath,
|
||||
details: [
|
||||
syncTargetDescription.value,
|
||||
`启动时自动同步:${syncConfig.value.autoSyncOnStartup ? "开启" : "关闭"}`,
|
||||
`保存后自动同步:${syncConfig.value.autoSyncAfterSave ? "开启" : "关闭"}`,
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
setSyncStatus(`保存失败: ${error}`, true);
|
||||
setSyncError(String(error), "保存同步配置失败");
|
||||
} finally {
|
||||
syncSaving.value = false;
|
||||
}
|
||||
@@ -304,36 +383,45 @@ async function saveSyncConfig() {
|
||||
|
||||
async function testSyncConnection() {
|
||||
syncTesting.value = true;
|
||||
syncFeedback.value = {
|
||||
kind: "progress",
|
||||
title: "正在测试连接",
|
||||
summary: `正在检查 ${syncProviderLabel.value} 服务的读取、写入和删除权限。`,
|
||||
};
|
||||
try {
|
||||
const result = await invoke<SyncConnectionTestResult>("test_sync_connection", { config: syncConfig.value });
|
||||
setSyncStatus([result.message, ...result.details.map((item) => `- ${item}`)].join("\n"));
|
||||
syncFeedback.value = {
|
||||
kind: "test",
|
||||
title: "连接测试通过",
|
||||
summary: result.message,
|
||||
details: result.details,
|
||||
};
|
||||
} catch (error) {
|
||||
setSyncStatus(String(error), true);
|
||||
setSyncError(String(error), "连接测试失败");
|
||||
} finally {
|
||||
syncTesting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSyncResult(result: SyncResult) {
|
||||
const { stats } = result;
|
||||
return `同步完成:上传 ${stats.uploaded},下载 ${stats.downloaded},删除本地 ${stats.deletedLocal},删除远端 ${stats.deletedRemote},跳过 ${stats.skipped}`;
|
||||
}
|
||||
|
||||
async function runSyncNow() {
|
||||
if (!props.workspacePath) {
|
||||
setSyncStatus("请先打开一个工作目录", true);
|
||||
setSyncError("请先打开一个工作目录", "无法开始同步");
|
||||
return;
|
||||
}
|
||||
|
||||
syncRunning.value = true;
|
||||
try {
|
||||
setSyncStatus("正在同步:扫描本地和远端文件,远端列表请求最长等待 120 秒。");
|
||||
syncFeedback.value = {
|
||||
kind: "progress",
|
||||
title: "正在同步",
|
||||
summary: "正在扫描本地和远端文件,远端列表请求最长等待 120 秒。",
|
||||
};
|
||||
await invoke("save_sync_config", { config: syncConfig.value });
|
||||
const prepared = await new Promise<boolean>((resolve) => {
|
||||
emit("prepareWorkspaceSync", resolve);
|
||||
});
|
||||
if (!prepared) {
|
||||
setSyncStatus("同步已取消,当前文档尚未保存", true);
|
||||
setSyncError("当前文档尚未保存,同步已取消。", "同步未开始");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,10 +430,16 @@ async function runSyncNow() {
|
||||
config: syncConfig.value,
|
||||
});
|
||||
syncConfig.value.lastSyncAt = result.lastSyncAt;
|
||||
setSyncStatus(formatSyncResult(result));
|
||||
const changedCount = result.stats.uploaded + result.stats.downloaded + result.stats.deletedLocal + result.stats.deletedRemote;
|
||||
syncFeedback.value = {
|
||||
kind: "sync",
|
||||
title: "同步完成",
|
||||
summary: changedCount > 0 ? `本次同步处理了 ${changedCount} 个变更文件。` : "本地与远端内容已一致,没有文件需要变更。",
|
||||
result,
|
||||
};
|
||||
emit("workspaceSynced");
|
||||
} catch (error) {
|
||||
setSyncStatus(`同步失败: ${error}`, true);
|
||||
setSyncError(String(error), "同步失败");
|
||||
} finally {
|
||||
syncRunning.value = false;
|
||||
}
|
||||
@@ -357,6 +451,7 @@ watch(
|
||||
if (!show) return;
|
||||
activePage.value = props.initialPage;
|
||||
updateStatus.value = "";
|
||||
syncFeedback.value = null;
|
||||
void loadAppVersion();
|
||||
void loadSyncConfig();
|
||||
},
|
||||
@@ -497,6 +592,20 @@ onBeforeUnmount(() => {
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-copy">
|
||||
<h3>单换行显示为换行</h3>
|
||||
<p>开启后,预览和混合模式会保留普通回车产生的换行,不修改 Markdown 原文。</p>
|
||||
</div>
|
||||
<label class="settings-switch" title="单换行显示为换行">
|
||||
<input
|
||||
:checked="renderSingleLineBreaks"
|
||||
type="checkbox"
|
||||
@change="emit('updateRenderSingleLineBreaks', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -669,22 +778,16 @@ onBeforeUnmount(() => {
|
||||
<p>
|
||||
{{ syncConfig.lastSyncAt ? `上次同步:${syncConfig.lastSyncAt}` : '尚未同步' }}
|
||||
</p>
|
||||
<div
|
||||
v-if="syncStatus || syncError"
|
||||
class="settings-sync-log"
|
||||
:class="{ 'settings-sync-log-error': Boolean(syncError) }"
|
||||
role="status"
|
||||
>
|
||||
{{ syncError || syncStatus }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-action-stack">
|
||||
<div class="settings-config-actions" aria-label="同步配置操作">
|
||||
<button
|
||||
class="settings-secondary-btn"
|
||||
type="button"
|
||||
:disabled="isSyncBusy"
|
||||
@click="saveSyncConfig"
|
||||
>
|
||||
<i :class="syncSaving ? 'ri-loader-4-line settings-spin' : 'ri-save-3-line'"></i>
|
||||
{{ syncSaving ? '保存中' : '保存配置' }}
|
||||
</button>
|
||||
<button
|
||||
@@ -693,17 +796,101 @@ onBeforeUnmount(() => {
|
||||
:disabled="isSyncBusy || syncConfig.provider === 'none'"
|
||||
@click="testSyncConnection"
|
||||
>
|
||||
<i :class="syncTesting ? 'ri-loader-4-line settings-spin' : 'ri-pulse-line'"></i>
|
||||
{{ syncTesting ? '测试中' : '测试连接' }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="settings-primary-btn"
|
||||
class="settings-primary-btn settings-sync-now-btn"
|
||||
type="button"
|
||||
:disabled="isSyncBusy || syncConfig.provider === 'none' || !workspacePath"
|
||||
@click="runSyncNow"
|
||||
>
|
||||
<i :class="syncRunning ? 'ri-loader-4-line settings-spin' : 'ri-loop-left-line'"></i>
|
||||
{{ syncRunning ? '同步中' : '立即同步' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="syncFeedback"
|
||||
class="settings-sync-feedback"
|
||||
:class="{
|
||||
'settings-sync-feedback-error': syncFeedback.kind === 'error',
|
||||
'settings-sync-feedback-progress': syncFeedback.kind === 'progress',
|
||||
}"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="settings-sync-feedback-header">
|
||||
<i
|
||||
:class="
|
||||
syncFeedback.kind === 'error'
|
||||
? 'ri-error-warning-line'
|
||||
: syncFeedback.kind === 'progress'
|
||||
? 'ri-loader-4-line settings-spin'
|
||||
: 'ri-checkbox-circle-line'
|
||||
"
|
||||
></i>
|
||||
<div>
|
||||
<strong>{{ syncFeedback.title }}</strong>
|
||||
<p>{{ syncFeedback.summary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="syncFeedback.configPath" class="settings-config-path">
|
||||
<span>配置保存位置</span>
|
||||
<code :title="syncFeedback.configPath">{{ syncFeedback.configPath }}</code>
|
||||
</div>
|
||||
|
||||
<ul v-if="syncFeedback.details?.length" class="settings-feedback-details">
|
||||
<li v-for="detail in syncFeedback.details" :key="detail">
|
||||
<i class="ri-check-line"></i>
|
||||
<span>{{ detail }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<template v-if="syncFeedback.result">
|
||||
<dl class="settings-sync-stats" aria-label="同步数量汇总">
|
||||
<div>
|
||||
<dt>上传</dt>
|
||||
<dd>{{ syncFeedback.result.stats.uploaded }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>下载</dt>
|
||||
<dd>{{ syncFeedback.result.stats.downloaded }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>删除本地</dt>
|
||||
<dd>{{ syncFeedback.result.stats.deletedLocal }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>删除远端</dt>
|
||||
<dd>{{ syncFeedback.result.stats.deletedRemote }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>未变更</dt>
|
||||
<dd>{{ syncFeedback.result.stats.skipped }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="settings-sync-file-section">
|
||||
<div class="settings-sync-file-heading">
|
||||
<strong>文件明细</strong>
|
||||
<span>{{ syncFileDetails.length }} 个文件</span>
|
||||
</div>
|
||||
<div v-if="syncFileDetails.length" class="settings-sync-file-list">
|
||||
<div v-for="file in syncFileDetails" :key="`${file.action}:${file.path}`" class="settings-sync-file">
|
||||
<span class="settings-sync-file-action" :data-action="file.action">
|
||||
{{ syncActionLabel(file.action) }}
|
||||
</span>
|
||||
<code :title="file.path">{{ file.path }}</code>
|
||||
<span class="settings-sync-file-size">{{ formatFileSize(file.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="settings-sync-file-empty">工作目录与远端都没有文件。</p>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -948,9 +1135,16 @@ onBeforeUnmount(() => {
|
||||
|
||||
.settings-action-stack {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
width: 244px;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-config-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-primary-btn,
|
||||
@@ -963,6 +1157,7 @@ onBeforeUnmount(() => {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
@@ -983,7 +1178,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.settings-secondary-btn {
|
||||
min-width: 50px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: var(--app-surface);
|
||||
color: var(--app-text);
|
||||
@@ -1149,28 +1344,224 @@ onBeforeUnmount(() => {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.settings-sync-log {
|
||||
width: min(100%, 420px);
|
||||
max-height: 138px;
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
.settings-sync-now-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-sync-feedback {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
grid-column: 1 / -1;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--app-accent) 26%, var(--app-border));
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--app-accent) 7%, var(--app-surface));
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.settings-sync-feedback-error {
|
||||
border-color: color-mix(in srgb, #dc2626 32%, var(--app-border));
|
||||
background: color-mix(in srgb, #dc2626 7%, var(--app-surface));
|
||||
}
|
||||
|
||||
.settings-sync-feedback-progress {
|
||||
border-color: var(--app-border-strong);
|
||||
background: var(--app-surface-muted);
|
||||
}
|
||||
|
||||
.settings-sync-feedback-header {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.settings-sync-feedback-header > i {
|
||||
margin-top: 1px;
|
||||
color: var(--app-accent);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.settings-sync-feedback-error .settings-sync-feedback-header > i,
|
||||
.settings-sync-feedback-error .settings-sync-feedback-header strong {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.settings-sync-feedback-header strong,
|
||||
.settings-sync-file-heading strong {
|
||||
display: block;
|
||||
color: var(--app-text-strong);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.settings-sync-feedback-header p,
|
||||
.settings-sync-file-empty {
|
||||
margin: 3px 0 0;
|
||||
color: var(--app-text-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 9px 10px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.settings-sync-log-error {
|
||||
border-color: color-mix(in srgb, #dc2626 32%, var(--app-border));
|
||||
background: color-mix(in srgb, #dc2626 7%, var(--app-surface));
|
||||
.settings-config-path {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--app-border-strong);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.settings-config-path span,
|
||||
.settings-sync-file-heading span,
|
||||
.settings-sync-file-size {
|
||||
color: var(--app-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.settings-config-path code {
|
||||
min-width: 0;
|
||||
color: var(--app-text);
|
||||
font-family: var(--app-font-mono, ui-monospace, SFMono-Regular, Consolas, monospace);
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.settings-feedback-details {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.settings-feedback-details li {
|
||||
display: grid;
|
||||
grid-template-columns: 16px minmax(0, 1fr);
|
||||
gap: 5px;
|
||||
color: var(--app-text-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.settings-feedback-details i {
|
||||
color: var(--app-accent);
|
||||
}
|
||||
|
||||
.settings-sync-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
border-top: 1px solid var(--app-border-strong);
|
||||
border-bottom: 1px solid var(--app-border-strong);
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.settings-sync-stats > div {
|
||||
min-width: 0;
|
||||
padding: 0 9px;
|
||||
border-left: 1px solid var(--app-border-strong);
|
||||
}
|
||||
|
||||
.settings-sync-stats > div:first-child {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.settings-sync-stats dt {
|
||||
color: var(--app-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.settings-sync-stats dd {
|
||||
margin: 3px 0 0;
|
||||
color: var(--app-text-strong);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-sync-file-section {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-sync-file-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-sync-file-list {
|
||||
display: flex;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--app-border-strong);
|
||||
border-radius: 5px;
|
||||
background: color-mix(in srgb, var(--app-surface) 78%, transparent);
|
||||
}
|
||||
|
||||
.settings-sync-file {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr) auto;
|
||||
gap: 9px;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 6px 9px;
|
||||
border-top: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.settings-sync-file:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.settings-sync-file-action {
|
||||
color: var(--app-accent);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.settings-sync-file-action[data-action="deleteLocal"],
|
||||
.settings-sync-file-action[data-action="deleteRemote"] {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.settings-sync-file-action[data-action="skip"] {
|
||||
color: var(--app-muted);
|
||||
}
|
||||
|
||||
.settings-sync-file code {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--app-text);
|
||||
font-family: var(--app-font-mono, ui-monospace, SFMono-Regular, Consolas, monospace);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-spin {
|
||||
animation: settings-spin 0.85s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes settings-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-option-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -1289,11 +1680,45 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.settings-action-stack {
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.settings-config-actions,
|
||||
.settings-sync-now-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-config-path {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.settings-sync-stats {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.settings-sync-stats > div:nth-child(4) {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.settings-sync-file {
|
||||
grid-template-columns: 58px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.settings-sync-file-size {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-switch-group {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.settings-spin {
|
||||
animation-duration: 1.8s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
100
src/lib/codeHighlight.ts
Normal file
100
src/lib/codeHighlight.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { HighlighterCore, LanguageInput, ThemedToken } from "shiki/core";
|
||||
|
||||
type LanguageLoader = () => Promise<{ default: LanguageInput }>;
|
||||
|
||||
const languageLoaders: Record<string, LanguageLoader> = {
|
||||
bash: () => import("shiki/langs/bash.mjs"),
|
||||
c: () => import("shiki/langs/c.mjs"),
|
||||
cpp: () => import("shiki/langs/cpp.mjs"),
|
||||
csharp: () => import("shiki/langs/csharp.mjs"),
|
||||
css: () => import("shiki/langs/css.mjs"),
|
||||
dockerfile: () => import("shiki/langs/dockerfile.mjs"),
|
||||
go: () => import("shiki/langs/go.mjs"),
|
||||
html: () => import("shiki/langs/html.mjs"),
|
||||
java: () => import("shiki/langs/java.mjs"),
|
||||
javascript: () => import("shiki/langs/javascript.mjs"),
|
||||
json: () => import("shiki/langs/json.mjs"),
|
||||
jsx: () => import("shiki/langs/jsx.mjs"),
|
||||
kotlin: () => import("shiki/langs/kotlin.mjs"),
|
||||
markdown: () => import("shiki/langs/markdown.mjs"),
|
||||
php: () => import("shiki/langs/php.mjs"),
|
||||
powershell: () => import("shiki/langs/powershell.mjs"),
|
||||
python: () => import("shiki/langs/python.mjs"),
|
||||
ruby: () => import("shiki/langs/ruby.mjs"),
|
||||
rust: () => import("shiki/langs/rust.mjs"),
|
||||
scss: () => import("shiki/langs/scss.mjs"),
|
||||
sql: () => import("shiki/langs/sql.mjs"),
|
||||
swift: () => import("shiki/langs/swift.mjs"),
|
||||
toml: () => import("shiki/langs/toml.mjs"),
|
||||
tsx: () => import("shiki/langs/tsx.mjs"),
|
||||
typescript: () => import("shiki/langs/typescript.mjs"),
|
||||
vue: () => import("shiki/langs/vue.mjs"),
|
||||
xml: () => import("shiki/langs/xml.mjs"),
|
||||
yaml: () => import("shiki/langs/yaml.mjs"),
|
||||
};
|
||||
|
||||
let highlighterPromise: Promise<HighlighterCore> | null = null;
|
||||
const languagePromises = new Map<string, Promise<void>>();
|
||||
|
||||
async function getCodeHighlighter() {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = Promise.all([
|
||||
import("shiki/core"),
|
||||
import("shiki/engine/javascript"),
|
||||
]).then(([core, engine]) =>
|
||||
core.createHighlighterCore({
|
||||
themes: [
|
||||
core.createCssVariablesTheme({
|
||||
name: "yurou-code",
|
||||
variablePrefix: "--code-",
|
||||
}),
|
||||
],
|
||||
langs: [],
|
||||
engine: engine.createJavaScriptRegexEngine(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
async function loadCodeLanguage(highlighter: HighlighterCore, language: string) {
|
||||
if (highlighter.getLoadedLanguages().includes(language)) return;
|
||||
|
||||
let promise = languagePromises.get(language);
|
||||
if (!promise) {
|
||||
const loader = languageLoaders[language];
|
||||
if (!loader) return;
|
||||
promise = loader().then((module) => highlighter.loadLanguage(module.default));
|
||||
languagePromises.set(language, promise);
|
||||
}
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
export async function highlightCodeToHtml(code: string, language: string) {
|
||||
const highlighter = await getCodeHighlighter();
|
||||
await loadCodeLanguage(highlighter, language);
|
||||
|
||||
if (!highlighter.getLoadedLanguages().includes(language)) return null;
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: language,
|
||||
theme: "yurou-code",
|
||||
});
|
||||
}
|
||||
|
||||
export async function highlightCodeToTokens(
|
||||
code: string,
|
||||
language: string,
|
||||
): Promise<ThemedToken[][] | null> {
|
||||
const highlighter = await getCodeHighlighter();
|
||||
await loadCodeLanguage(highlighter, language);
|
||||
|
||||
if (!highlighter.getLoadedLanguages().includes(language)) return null;
|
||||
|
||||
return highlighter.codeToTokens(code, {
|
||||
lang: language,
|
||||
theme: "yurou-code",
|
||||
}).tokens;
|
||||
}
|
||||
96
src/lib/markdownLanguages.ts
Normal file
96
src/lib/markdownLanguages.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { pickedCompletion, type Completion } from "@codemirror/autocomplete";
|
||||
|
||||
export interface MarkdownCodeLanguage {
|
||||
label: string;
|
||||
name: string;
|
||||
aliases: string[];
|
||||
shiki: string;
|
||||
}
|
||||
|
||||
export const markdownCodeLanguages: MarkdownCodeLanguage[] = [
|
||||
{ label: "text", name: "Plain text", aliases: ["txt", "plaintext"], shiki: "text" },
|
||||
{ label: "javascript", name: "JavaScript", aliases: ["js", "node"], shiki: "javascript" },
|
||||
{ label: "typescript", name: "TypeScript", aliases: ["ts"], shiki: "typescript" },
|
||||
{ label: "jsx", name: "JSX", aliases: [], shiki: "jsx" },
|
||||
{ label: "tsx", name: "TSX", aliases: [], shiki: "tsx" },
|
||||
{ label: "vue", name: "Vue", aliases: ["vuejs"], shiki: "vue" },
|
||||
{ label: "html", name: "HTML", aliases: ["htm"], shiki: "html" },
|
||||
{ label: "css", name: "CSS", aliases: [], shiki: "css" },
|
||||
{ label: "scss", name: "SCSS", aliases: ["sass"], shiki: "scss" },
|
||||
{ label: "json", name: "JSON", aliases: ["jsonc"], shiki: "json" },
|
||||
{ label: "yaml", name: "YAML", aliases: ["yml"], shiki: "yaml" },
|
||||
{ label: "markdown", name: "Markdown", aliases: ["md"], shiki: "markdown" },
|
||||
{ label: "python", name: "Python", aliases: ["py"], shiki: "python" },
|
||||
{ label: "java", name: "Java", aliases: [], shiki: "java" },
|
||||
{ label: "kotlin", name: "Kotlin", aliases: ["kt"], shiki: "kotlin" },
|
||||
{ label: "c", name: "C", aliases: [], shiki: "c" },
|
||||
{ label: "cpp", name: "C++", aliases: ["c++"], shiki: "cpp" },
|
||||
{ label: "csharp", name: "C#", aliases: ["cs", "c#"], shiki: "csharp" },
|
||||
{ label: "go", name: "Go", aliases: ["golang"], shiki: "go" },
|
||||
{ label: "rust", name: "Rust", aliases: ["rs"], shiki: "rust" },
|
||||
{ label: "php", name: "PHP", aliases: [], shiki: "php" },
|
||||
{ label: "ruby", name: "Ruby", aliases: ["rb"], shiki: "ruby" },
|
||||
{ label: "swift", name: "Swift", aliases: [], shiki: "swift" },
|
||||
{ label: "sql", name: "SQL", aliases: [], shiki: "sql" },
|
||||
{ label: "bash", name: "Bash", aliases: ["sh", "shell", "zsh"], shiki: "bash" },
|
||||
{ label: "powershell", name: "PowerShell", aliases: ["ps1", "pwsh"], shiki: "powershell" },
|
||||
{ label: "dockerfile", name: "Dockerfile", aliases: ["docker"], shiki: "dockerfile" },
|
||||
{ label: "xml", name: "XML", aliases: [], shiki: "xml" },
|
||||
{ label: "toml", name: "TOML", aliases: [], shiki: "toml" },
|
||||
];
|
||||
|
||||
const languageLookup = new Map<string, MarkdownCodeLanguage>();
|
||||
|
||||
for (const language of markdownCodeLanguages) {
|
||||
languageLookup.set(language.label, language);
|
||||
for (const alias of language.aliases) {
|
||||
languageLookup.set(alias, language);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMarkdownCodeLanguage(value: string | null | undefined) {
|
||||
const normalized = value?.trim().toLowerCase().split(/\s+/, 1)[0] || "";
|
||||
return languageLookup.get(normalized) ?? null;
|
||||
}
|
||||
|
||||
const RECENT_LANGUAGES_KEY = "markdown-recent-code-languages";
|
||||
|
||||
function readRecentLanguages() {
|
||||
try {
|
||||
const stored = localStorage.getItem(RECENT_LANGUAGES_KEY);
|
||||
const value = stored ? JSON.parse(stored) : [];
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === "string").slice(0, 5)
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function rememberMarkdownCodeLanguage(label: string) {
|
||||
const next = [label, ...readRecentLanguages().filter((item) => item !== label)].slice(0, 5);
|
||||
try {
|
||||
localStorage.setItem(RECENT_LANGUAGES_KEY, JSON.stringify(next));
|
||||
} catch {
|
||||
// Completion remains available when storage is disabled.
|
||||
}
|
||||
}
|
||||
|
||||
export function getMarkdownLanguageCompletions(): Completion[] {
|
||||
const recent = readRecentLanguages();
|
||||
|
||||
return markdownCodeLanguages.map((language, index) => ({
|
||||
label: language.label,
|
||||
displayLabel: language.name,
|
||||
detail: language.aliases.length > 0 ? language.aliases.join(", ") : language.label,
|
||||
type: "keyword",
|
||||
boost: recent.includes(language.label) ? 100 - recent.indexOf(language.label) : -index,
|
||||
apply(view, completion, from, to) {
|
||||
rememberMarkdownCodeLanguage(language.label);
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: language.label },
|
||||
annotations: pickedCompletion.of(completion),
|
||||
});
|
||||
},
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user