diff --git a/package-lock.json b/package-lock.json index ce4a2a9..bed5ac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0e4edb1..22dd901 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0f443f..fc21bfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index d07dea3..03f7f35 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -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" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d5967a3..08a98b6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, +} + +#[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 = 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 { } #[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 { + 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(); diff --git a/src/App.vue b/src/App.vue index 1ae73ce..5e31ffd 100644 --- a/src/App.vue +++ b/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 | null = null; let workspaceRefreshInFlight = false; let workspaceRefreshQueued = false; let syncInFlight = false; +let fileOpenRequest = 0; +let saveNoticeTimer: ReturnType | 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(loadDefaultDocumentMode()); const documentMode = ref(defaultDocumentMode.value); const appTheme = ref(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("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("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("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("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) }}% + +
+ + {{ saveNotice }} +
+
@@ -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 { diff --git a/src/components/DirectorySidebar.vue b/src/components/DirectorySidebar.vue index 1f8aed3..e8bbef6 100644 --- a/src/components/DirectorySidebar.vue +++ b/src/components/DirectorySidebar.vue @@ -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')" /> diff --git a/src/components/MarkdownCodeEditor.vue b/src/components/MarkdownCodeEditor.vue new file mode 100644 index 0000000..b4d189e --- /dev/null +++ b/src/components/MarkdownCodeEditor.vue @@ -0,0 +1,476 @@ + + + + + diff --git a/src/components/MarkdownEditor.vue b/src/components/MarkdownEditor.vue index 8d4ecc0..f153003 100644 --- a/src/components/MarkdownEditor.vue +++ b/src/components/MarkdownEditor.vue @@ -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) - .use(remarkLocalMedia) - .use(remarkRehype, { allowDangerousHtml: false }) - .use(rehypeSanitize, mediaSanitizeSchema) - .use(rehypeStringify); +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', + }" > 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; diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index db60b24..8cbeaff 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -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(createDefaultSyncConfig()); -const syncStatus = ref(""); -const syncError = ref(""); +const syncFeedback = ref(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("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("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("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((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(() => { +
+
+

单换行显示为换行

+

开启后,预览和混合模式会保留普通回车产生的换行,不修改 Markdown 原文。

+
+ +
@@ -669,41 +778,119 @@ onBeforeUnmount(() => {

{{ syncConfig.lastSyncAt ? `上次同步:${syncConfig.lastSyncAt}` : '尚未同步' }}

-
- {{ syncError || syncStatus }} -
+
+ + +
- -
+ + @@ -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; + } +} diff --git a/src/lib/codeHighlight.ts b/src/lib/codeHighlight.ts new file mode 100644 index 0000000..ac7b355 --- /dev/null +++ b/src/lib/codeHighlight.ts @@ -0,0 +1,100 @@ +import type { HighlighterCore, LanguageInput, ThemedToken } from "shiki/core"; + +type LanguageLoader = () => Promise<{ default: LanguageInput }>; + +const languageLoaders: Record = { + 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 | null = null; +const languagePromises = new Map>(); + +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 { + 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; +} diff --git a/src/lib/markdownLanguages.ts b/src/lib/markdownLanguages.ts new file mode 100644 index 0000000..0c45959 --- /dev/null +++ b/src/lib/markdownLanguages.ts @@ -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(); + +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), + }); + }, + })); +}