Compare commits

...

2 Commits

Author SHA1 Message Date
c5a5a4e55b 完善编辑功能 2026-07-16 16:40:00 +08:00
87e594be52 完善 2026-07-16 15:14:28 +08:00
8 changed files with 9821 additions and 366 deletions

View File

@@ -172,9 +172,38 @@ MarkdownEditor.vue
- **parseBlocks** 每次 modelValue 变化时调用,包含 N 次 `processSync`N = 块数) - **parseBlocks** 每次 modelValue 变化时调用,包含 N 次 `processSync`N = 块数)
- 典型文档50 块)约需 50-100ms在可接受范围内 - 典型文档50 块)约需 50-100ms在可接受范围内
- 后续可优化为增量更新(只重新渲染变化的块) - 后续可优化为增量更新(只重新渲染变化的块)
- **textarea auto-resize** 通过 `scrollHeight` 动态调整高度,无额外 DOM 操作 - **CodeMirror 实例数** 完整编辑模式保持一个实例;混合模式只为当前活动块创建实例
- **双栏滚动同步** 使用比例计算,避免频繁的绝对位置计算 - **双栏滚动同步** 使用比例计算,避免频繁的绝对位置计算
## 代码编辑体验
Markdown 源码编辑由 CodeMirror 6 提供,完整编辑模式与混合模式的活动块复用
`MarkdownCodeEditor.vue`。组件继续通过 `update:modelValue` 传递纯 Markdown 字符串,
不会改变外层的保存和文件切换数据流。
### 编辑能力
- Markdown 语法着色,以及围栏代码块内部的语言着色
- 括号、引号自动闭合列表续行Tab/Shift+Tab 缩进
- 输入三个反引号或波浪号后自动打开语言提示
- 语言提示支持继续输入筛选、方向键选择、Enter 确认和 Ctrl+Space 手动触发
- 围栏语言确认后按 Enter 自动补齐结束围栏,并将光标放在代码区域
- 最近使用的五种语言通过 localStorage 排在提示列表前面
语言名称、别名和 Shiki 映射集中定义在 `src/lib/markdownLanguages.ts`
无法识别的围栏语言按纯文本处理,不阻断 Markdown 渲染或编辑。
### 预览高亮
预览仍先经过 unified 和 rehype-sanitize。安全过滤完成后可信的 Shiki 高亮器
替换 `<pre><code>` 节点,因此不会放宽 Markdown 原始 HTML 的安全策略。
Shiki 核心和语言语法均按需加载:只有文档包含代码块时才初始化高亮器,只有某种
语言第一次出现在预览中时才加载对应语法。高亮器和已加载语言会在应用生命周期内
复用。Shiki 与 CodeMirror 共用 `--code-*` 语义色变量,每套应用主题同时定义界面色
和代码色CodeMirror 围栏内容还复用 Shiki 的 token 划分,确保混合模式点击前后逐词
颜色一致。切换主题时预览与活动编辑器会即时使用新颜色,无需重新解析文档。
## 样式体系 ## 样式体系
| 层级 | 设计 | | 层级 | 设计 |
@@ -284,8 +313,8 @@ MarkdownEditor.vue
paste 事件同时绑定在两种模式的 textarea 上: paste 事件同时绑定在两种模式的 textarea 上:
- **edit 模式**`@paste="onEditorPaste"`操作 `props.modelValue` + `editorTextareaRef` - **edit 模式**`@paste="onEditorPaste"`按 CodeMirror 当前选区替换 `modelValue`
- **mixed 模式**`@paste="onBlockPaste"`操作 `editingSource` + `blockTextareaRef`(包含空文档和 block 编辑两种场景) - **mixed 模式**`@paste="onBlockPaste"`按活动块 CodeMirror 选区替换 `editingSource`
粘贴后的 markdown 语法统一为: 粘贴后的 markdown 语法统一为:
```markdown ```markdown

8734
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,15 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "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", "@lucide/vue": "^1.23.0",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-dialog": "^2.7.1",
@@ -25,6 +34,7 @@
"remark-rehype": "^11.1.2", "remark-rehype": "^11.1.2",
"remixicon": "^4.9.1", "remixicon": "^4.9.1",
"shadcn-vue": "^2.7.4", "shadcn-vue": "^2.7.4",
"shiki": "^4.3.1",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"unified": "^11.0.0", "unified": "^11.0.0",

666
pnpm-lock.yaml generated
View File

@@ -4,10 +4,40 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides:
'@codemirror/lang-markdown': 6.4.0
importers: importers:
.: .:
dependencies: dependencies:
'@codemirror/autocomplete':
specifier: ^6.20.3
version: 6.20.3
'@codemirror/commands':
specifier: ^6.10.4
version: 6.10.4
'@codemirror/lang-markdown':
specifier: 6.4.0
version: 6.4.0
'@codemirror/language':
specifier: ^6.12.4
version: 6.12.4
'@codemirror/language-data':
specifier: ^6.5.2
version: 6.5.2
'@codemirror/search':
specifier: ^6.7.1
version: 6.7.1
'@codemirror/state':
specifier: ^6.7.1
version: 6.7.1
'@codemirror/view':
specifier: ^6.43.6
version: 6.43.6
'@lezer/highlight':
specifier: ^1.2.3
version: 1.2.3
'@lucide/vue': '@lucide/vue':
specifier: ^1.23.0 specifier: ^1.23.0
version: 1.23.0(vue@3.5.39(typescript@5.6.3)) version: 1.23.0(vue@3.5.39(typescript@5.6.3))
@@ -53,6 +83,9 @@ importers:
shadcn-vue: shadcn-vue:
specifier: ^2.7.4 specifier: ^2.7.4
version: 2.7.4(eslint@10.6.0(jiti@2.7.0))(vue@3.5.39(typescript@5.6.3)) version: 2.7.4(eslint@10.6.0(jiti@2.7.0))(vue@3.5.39(typescript@5.6.3))
shiki:
specifier: ^4.3.1
version: 4.3.1
tailwind-merge: tailwind-merge:
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0 version: 3.6.0
@@ -224,6 +257,96 @@ packages:
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@codemirror/autocomplete@6.20.3':
resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==}
'@codemirror/commands@6.10.4':
resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==}
'@codemirror/lang-angular@0.1.4':
resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==}
'@codemirror/lang-cpp@6.0.3':
resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==}
'@codemirror/lang-css@6.3.1':
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
'@codemirror/lang-go@6.0.1':
resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==}
'@codemirror/lang-html@6.4.11':
resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
'@codemirror/lang-java@6.0.2':
resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==}
'@codemirror/lang-javascript@6.2.5':
resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==}
'@codemirror/lang-jinja@6.0.1':
resolution: {integrity: sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==}
'@codemirror/lang-json@6.0.2':
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
'@codemirror/lang-less@6.0.2':
resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==}
'@codemirror/lang-liquid@6.3.2':
resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==}
'@codemirror/lang-markdown@6.4.0':
resolution: {integrity: sha512-ZeArR54seh4laFbUTVy0ZmQgO+C/cxxlW4jEoQMhL3HALScBpZBeZcLzrQmJsTEx4is9GzOe0bFAke2B1KZqeA==}
'@codemirror/lang-php@6.0.2':
resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==}
'@codemirror/lang-python@6.2.1':
resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==}
'@codemirror/lang-rust@6.0.2':
resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==}
'@codemirror/lang-sass@6.0.2':
resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==}
'@codemirror/lang-sql@6.10.0':
resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
'@codemirror/lang-vue@0.1.3':
resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==}
'@codemirror/lang-wast@6.0.2':
resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==}
'@codemirror/lang-xml@6.1.0':
resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==}
'@codemirror/lang-yaml@6.1.3':
resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==}
'@codemirror/language-data@6.5.2':
resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==}
'@codemirror/language@6.12.4':
resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==}
'@codemirror/legacy-modes@6.5.3':
resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==}
'@codemirror/lint@6.9.7':
resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==}
'@codemirror/search@6.7.1':
resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==}
'@codemirror/state@6.7.1':
resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==}
'@codemirror/view@6.43.6':
resolution: {integrity: sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==}
'@dotenvx/dotenvx@1.75.1': '@dotenvx/dotenvx@1.75.1':
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
hasBin: true hasBin: true
@@ -485,11 +608,65 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@lezer/common@1.5.2':
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
'@lezer/cpp@1.1.6':
resolution: {integrity: sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==}
'@lezer/css@1.3.4':
resolution: {integrity: sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==}
'@lezer/go@1.0.1':
resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==}
'@lezer/highlight@1.2.3':
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
'@lezer/html@1.3.13':
resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
'@lezer/java@1.1.3':
resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==}
'@lezer/javascript@1.5.4':
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
'@lezer/json@1.0.3':
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
'@lezer/lr@1.4.10':
resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
'@lezer/markdown@1.7.1':
resolution: {integrity: sha512-MEBZeFSBxgteUjEC3Wxg2Dwld5/JxRKG267L3bMFdibm8KjqSdiJYBeFw1Nt1CM8+zKMpSIEHblY8FD9z38sJQ==}
'@lezer/php@1.0.5':
resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==}
'@lezer/python@1.1.19':
resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==}
'@lezer/rust@1.0.2':
resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==}
'@lezer/sass@1.1.0':
resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==}
'@lezer/xml@1.0.6':
resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==}
'@lezer/yaml@1.0.4':
resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==}
'@lucide/vue@1.23.0': '@lucide/vue@1.23.0':
resolution: {integrity: sha512-9SIYeY5K+R1iv8F8JUKMGSL7Pck/86BJ8djtZz/lnYsoHtKFUj1H2z6+PvKnC/8blZ8tqTDeDXAoTKobuX80hg==} resolution: {integrity: sha512-9SIYeY5K+R1iv8F8JUKMGSL7Pck/86BJ8djtZz/lnYsoHtKFUj1H2z6+PvKnC/8blZ8tqTDeDXAoTKobuX80hg==}
peerDependencies: peerDependencies:
vue: '>=3.0.1' vue: '>=3.0.1'
'@marijn/find-cluster-break@1.0.3':
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
'@modelcontextprotocol/sdk@1.29.0': '@modelcontextprotocol/sdk@1.29.0':
resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -654,6 +831,37 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@shikijs/core@4.3.1':
resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==}
engines: {node: '>=20'}
'@shikijs/engine-javascript@4.3.1':
resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==}
engines: {node: '>=20'}
'@shikijs/engine-oniguruma@4.3.1':
resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==}
engines: {node: '>=20'}
'@shikijs/langs@4.3.1':
resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==}
engines: {node: '>=20'}
'@shikijs/primitive@4.3.1':
resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==}
engines: {node: '>=20'}
'@shikijs/themes@4.3.1':
resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==}
engines: {node: '>=20'}
'@shikijs/types@4.3.1':
resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==}
engines: {node: '>=20'}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@swc/helpers@0.5.23': '@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
@@ -859,6 +1067,9 @@ packages:
'@types/hast@3.0.4': '@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/hast@3.0.5':
resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1210,6 +1421,9 @@ packages:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
crelt@1.0.7:
resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2223,6 +2437,12 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
oniguruma-parser@0.12.2:
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
oniguruma-to-es@4.3.6:
resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
open@10.2.0: open@10.2.0:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2410,6 +2630,15 @@ packages:
resolution: {integrity: sha512-CkfWKhQiYsMQYaWUkHdERXUxT2jJLBoa5y7zFv3dUAE7Ly5oU/0hsqrENyEfrCL03pDsQYbnoz17Cbagx/c2OA==} resolution: {integrity: sha512-CkfWKhQiYsMQYaWUkHdERXUxT2jJLBoa5y7zFv3dUAE7Ly5oU/0hsqrENyEfrCL03pDsQYbnoz17Cbagx/c2OA==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
regex-utilities@2.3.0:
resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
regex@6.1.0:
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
rehype-sanitize@6.0.0: rehype-sanitize@6.0.0:
resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
@@ -2514,6 +2743,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'} engines: {node: '>=8'}
shiki@4.3.1:
resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==}
engines: {node: '>=20'}
side-channel-list@1.0.1: side-channel-list@1.0.1:
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2606,6 +2839,9 @@ packages:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'} engines: {node: '>=6'}
style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
stylus@0.57.0: stylus@0.57.0:
resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==} resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==}
hasBin: true hasBin: true
@@ -2826,6 +3062,9 @@ packages:
typescript: typescript:
optional: true optional: true
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
web-worker@1.5.0: web-worker@1.5.0:
resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
@@ -3070,6 +3309,257 @@ snapshots:
'@babel/helper-string-parser': 7.29.7 '@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7 '@babel/helper-validator-identifier': 7.29.7
'@codemirror/autocomplete@6.20.3':
dependencies:
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@codemirror/commands@6.10.4':
dependencies:
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@codemirror/lang-angular@0.1.4':
dependencies:
'@codemirror/lang-html': 6.4.11
'@codemirror/lang-javascript': 6.2.5
'@codemirror/language': 6.12.4
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-cpp@6.0.3':
dependencies:
'@codemirror/language': 6.12.4
'@lezer/cpp': 1.1.6
'@codemirror/lang-css@6.3.1':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.2
'@lezer/css': 1.3.4
'@codemirror/lang-go@6.0.1':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.2
'@lezer/go': 1.0.1
'@codemirror/lang-html@6.4.11':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-javascript': 6.2.5
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@lezer/css': 1.3.4
'@lezer/html': 1.3.13
'@codemirror/lang-java@6.0.2':
dependencies:
'@codemirror/language': 6.12.4
'@lezer/java': 1.1.3
'@codemirror/lang-javascript@6.2.5':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/lint': 6.9.7
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@lezer/javascript': 1.5.4
'@codemirror/lang-jinja@6.0.1':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-json@6.0.2':
dependencies:
'@codemirror/language': 6.12.4
'@lezer/json': 1.0.3
'@codemirror/lang-less@6.0.2':
dependencies:
'@codemirror/lang-css': 6.3.1
'@codemirror/language': 6.12.4
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-liquid@6.3.2':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-markdown@6.4.0':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@lezer/markdown': 1.7.1
'@codemirror/lang-php@6.0.2':
dependencies:
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.2
'@lezer/php': 1.0.5
'@codemirror/lang-python@6.2.1':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.2
'@lezer/python': 1.1.19
'@codemirror/lang-rust@6.0.2':
dependencies:
'@codemirror/language': 6.12.4
'@lezer/rust': 1.0.2
'@codemirror/lang-sass@6.0.2':
dependencies:
'@codemirror/lang-css': 6.3.1
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.2
'@lezer/sass': 1.1.0
'@codemirror/lang-sql@6.10.0':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-vue@0.1.3':
dependencies:
'@codemirror/lang-html': 6.4.11
'@codemirror/lang-javascript': 6.2.5
'@codemirror/language': 6.12.4
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-wast@6.0.2':
dependencies:
'@codemirror/language': 6.12.4
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-xml@6.1.0':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@lezer/xml': 1.0.6
'@codemirror/lang-yaml@6.1.3':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/yaml': 1.0.4
'@codemirror/language-data@6.5.2':
dependencies:
'@codemirror/lang-angular': 0.1.4
'@codemirror/lang-cpp': 6.0.3
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-go': 6.0.1
'@codemirror/lang-html': 6.4.11
'@codemirror/lang-java': 6.0.2
'@codemirror/lang-javascript': 6.2.5
'@codemirror/lang-jinja': 6.0.1
'@codemirror/lang-json': 6.0.2
'@codemirror/lang-less': 6.0.2
'@codemirror/lang-liquid': 6.3.2
'@codemirror/lang-markdown': 6.4.0
'@codemirror/lang-php': 6.0.2
'@codemirror/lang-python': 6.2.1
'@codemirror/lang-rust': 6.0.2
'@codemirror/lang-sass': 6.0.2
'@codemirror/lang-sql': 6.10.0
'@codemirror/lang-vue': 0.1.3
'@codemirror/lang-wast': 6.0.2
'@codemirror/lang-xml': 6.1.0
'@codemirror/lang-yaml': 6.1.3
'@codemirror/language': 6.12.4
'@codemirror/legacy-modes': 6.5.3
'@codemirror/language@6.12.4':
dependencies:
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
style-mod: 4.1.3
'@codemirror/legacy-modes@6.5.3':
dependencies:
'@codemirror/language': 6.12.4
'@codemirror/lint@6.9.7':
dependencies:
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
crelt: 1.0.7
'@codemirror/search@6.7.1':
dependencies:
'@codemirror/state': 6.7.1
'@codemirror/view': 6.43.6
crelt: 1.0.7
'@codemirror/state@6.7.1':
dependencies:
'@marijn/find-cluster-break': 1.0.3
'@codemirror/view@6.43.6':
dependencies:
'@codemirror/state': 6.7.1
crelt: 1.0.7
style-mod: 4.1.3
w3c-keyname: 2.2.8
'@dotenvx/dotenvx@1.75.1': '@dotenvx/dotenvx@1.75.1':
dependencies: dependencies:
'@dotenvx/primitives': 0.8.0 '@dotenvx/primitives': 0.8.0
@@ -3277,10 +3767,105 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@lezer/common@1.5.2': {}
'@lezer/cpp@1.1.6':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/css@1.3.4':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/go@1.0.1':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/highlight@1.2.3':
dependencies:
'@lezer/common': 1.5.2
'@lezer/html@1.3.13':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/java@1.1.3':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/javascript@1.5.4':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/lr@1.4.10':
dependencies:
'@lezer/common': 1.5.2
'@lezer/markdown@1.7.1':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/php@1.0.5':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/python@1.1.19':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/rust@1.0.2':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/sass@1.1.0':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/xml@1.0.6':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/yaml@1.0.4':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lucide/vue@1.23.0(vue@3.5.39(typescript@5.6.3))': '@lucide/vue@1.23.0(vue@3.5.39(typescript@5.6.3))':
dependencies: dependencies:
vue: 3.5.39(typescript@5.6.3) vue: 3.5.39(typescript@5.6.3)
'@marijn/find-cluster-break@1.0.3': {}
'@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)':
dependencies: dependencies:
'@hono/node-server': 1.19.14(hono@4.12.28) '@hono/node-server': 1.19.14(hono@4.12.28)
@@ -3393,6 +3978,46 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.62.2': '@rollup/rollup-win32-x64-msvc@4.62.2':
optional: true optional: true
'@shikijs/core@4.3.1':
dependencies:
'@shikijs/primitive': 4.3.1
'@shikijs/types': 4.3.1
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.5
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@4.3.1':
dependencies:
'@shikijs/types': 4.3.1
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 4.3.6
'@shikijs/engine-oniguruma@4.3.1':
dependencies:
'@shikijs/types': 4.3.1
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@4.3.1':
dependencies:
'@shikijs/types': 4.3.1
'@shikijs/primitive@4.3.1':
dependencies:
'@shikijs/types': 4.3.1
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.5
'@shikijs/themes@4.3.1':
dependencies:
'@shikijs/types': 4.3.1
'@shikijs/types@4.3.1':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.5
'@shikijs/vscode-textmate@10.0.2': {}
'@swc/helpers@0.5.23': '@swc/helpers@0.5.23':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -3547,6 +4172,10 @@ snapshots:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/hast@3.0.5':
dependencies:
'@types/unist': 3.0.3
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
@@ -3907,6 +4536,8 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
vary: 1.1.2 vary: 1.1.2
crelt@1.0.7: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -4406,7 +5037,7 @@ snapshots:
hast-util-whitespace@3.0.0: hast-util-whitespace@3.0.0:
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.5
he@1.2.0: {} he@1.2.0: {}
@@ -5048,6 +5679,14 @@ snapshots:
dependencies: dependencies:
mimic-function: 5.0.1 mimic-function: 5.0.1
oniguruma-parser@0.12.2: {}
oniguruma-to-es@4.3.6:
dependencies:
oniguruma-parser: 0.12.2
regex: 6.1.0
regex-recursion: 6.0.2
open@10.2.0: open@10.2.0:
dependencies: dependencies:
default-browser: 5.5.0 default-browser: 5.5.0
@@ -5235,6 +5874,16 @@ snapshots:
tiny-invariant: 1.3.3 tiny-invariant: 1.3.3
tslib: 2.8.1 tslib: 2.8.1
regex-recursion@6.0.2:
dependencies:
regex-utilities: 2.3.0
regex-utilities@2.3.0: {}
regex@6.1.0:
dependencies:
regex-utilities: 2.3.0
rehype-sanitize@6.0.0: rehype-sanitize@6.0.0:
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
@@ -5449,6 +6098,17 @@ snapshots:
shebang-regex@3.0.0: {} shebang-regex@3.0.0: {}
shiki@4.3.1:
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.5
side-channel-list@1.0.1: side-channel-list@1.0.1:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -5551,6 +6211,8 @@ snapshots:
strip-final-newline@2.0.0: {} strip-final-newline@2.0.0: {}
style-mod@4.1.3: {}
stylus@0.57.0: stylus@0.57.0:
dependencies: dependencies:
css: 3.0.0 css: 3.0.0
@@ -5771,6 +6433,8 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.6.3 typescript: 5.6.3
w3c-keyname@2.2.8: {}
web-worker@1.5.0: {} web-worker@1.5.0: {}
which@2.0.1: which@2.0.1:

View File

@@ -1,3 +1,5 @@
allowBuilds: allowBuilds:
esbuild: true esbuild: true
vue-demi: true vue-demi: true
overrides:
'@codemirror/lang-markdown': 6.4.0

View File

@@ -5,7 +5,6 @@ import {
onBeforeUnmount, onBeforeUnmount,
ref, ref,
watch, watch,
type ComponentPublicInstance,
} from "vue"; } from "vue";
import { unified, type Plugin, type Transformer } from "unified"; import { unified, type Plugin, type Transformer } from "unified";
import remarkParse from "remark-parse"; import remarkParse from "remark-parse";
@@ -14,6 +13,9 @@ import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify"; import rehypeStringify from "rehype-stringify";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import { convertFileSrc, invoke, isTauri } from "@tauri-apps/api/core"; import { convertFileSrc, invoke, isTauri } from "@tauri-apps/api/core";
import MarkdownCodeEditor from "./MarkdownCodeEditor.vue";
import { highlightCodeToHtml } from "@/lib/codeHighlight";
import { resolveMarkdownCodeLanguage } from "@/lib/markdownLanguages";
type DocumentMode = "preview" | "edit" | "mixed"; type DocumentMode = "preview" | "edit" | "mixed";
type ClipboardMediaKind = "image" | "video"; type ClipboardMediaKind = "image" | "video";
@@ -29,6 +31,7 @@ const VIDEO_MIME_TYPES: Record<string, string> = {
}; };
interface MarkdownAstNode { interface MarkdownAstNode {
type?: string;
position?: { position?: {
start?: { offset?: number }; start?: { offset?: number };
end?: { offset?: number }; end?: { offset?: number };
@@ -51,12 +54,20 @@ interface MarkdownTree {
interface MarkdownBlock { interface MarkdownBlock {
id: string; id: string;
type: string;
source: string; source: string;
html: string; html: string;
startOffset: number; startOffset: number;
endOffset: number; endOffset: number;
} }
interface MarkdownCodeEditorApi {
focus: () => void;
focusAtEnd: () => void;
replaceRange: (text: string, from?: number, to?: number) => void;
scrollToLine: (lineNumber: number) => void;
}
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue?: string; modelValue?: string;
@@ -230,9 +241,9 @@ const renderedHtml = ref("");
const renderError = ref(false); const renderError = ref(false);
const markdownBlocks = ref<MarkdownBlock[]>([]); const markdownBlocks = ref<MarkdownBlock[]>([]);
const editorTextareaRef = ref<HTMLTextAreaElement>(); const editorCodeRef = ref<MarkdownCodeEditorApi>();
const scrollRef = ref<HTMLDivElement>(); const scrollRef = ref<HTMLDivElement>();
const blockTextareaRef = ref<HTMLTextAreaElement | null>(null); const blockCodeRef = ref<MarkdownCodeEditorApi>();
const EMPTY_BLOCK_ID = "__empty__"; const EMPTY_BLOCK_ID = "__empty__";
const editingBlockId = ref<string | null>(null); const editingBlockId = ref<string | null>(null);
@@ -245,8 +256,8 @@ const pendingLocalModelUpdate = ref(false);
const isEditingEmptyDocument = computed( const isEditingEmptyDocument = computed(
() => editingBlockId.value === EMPTY_BLOCK_ID, () => editingBlockId.value === EMPTY_BLOCK_ID,
); );
let textareaResizeFrame: number | null = null;
let modeScrollRestoreFrame: number | null = null; let modeScrollRestoreFrame: number | null = null;
let renderRequestId = 0;
interface ScrollSnapshot { interface ScrollSnapshot {
ratio: number; ratio: number;
@@ -276,7 +287,6 @@ function restoreScrollSnapshot(snapshot: ScrollSnapshot | null) {
function restoreScrollAfterModeChange( function restoreScrollAfterModeChange(
snapshot: ScrollSnapshot | null, snapshot: ScrollSnapshot | null,
mode: DocumentMode,
) { ) {
if (!snapshot) return; if (!snapshot) return;
@@ -286,17 +296,10 @@ function restoreScrollAfterModeChange(
} }
void nextTick(() => { void nextTick(() => {
if (mode === "edit" && editorTextareaRef.value) {
resizeTextarea(editorTextareaRef.value);
}
restoreScrollSnapshot(snapshot); restoreScrollSnapshot(snapshot);
modeScrollRestoreFrame = requestAnimationFrame(() => { modeScrollRestoreFrame = requestAnimationFrame(() => {
modeScrollRestoreFrame = null; modeScrollRestoreFrame = null;
if (mode === "edit" && editorTextareaRef.value) {
resizeTextarea(editorTextareaRef.value);
}
restoreScrollSnapshot(snapshot); restoreScrollSnapshot(snapshot);
}); });
}); });
@@ -304,21 +307,62 @@ function restoreScrollAfterModeChange(
function syncRenderedState(md: string, mode = activeDocumentMode.value) { function syncRenderedState(md: string, mode = activeDocumentMode.value) {
if (mode === "preview") { if (mode === "preview") {
renderFullHtml(md); void renderFullHtml(md);
return; return;
} }
if (mode === "mixed") { if (mode === "mixed") {
parseBlocks(md); void parseBlocks(md);
} }
} }
function renderFullHtml(md: string) { async function highlightCodeBlocks(html: string) {
if (!html.includes("<pre><code")) return html;
const document = new DOMParser().parseFromString(html, "text/html");
const codeBlocks = Array.from(document.querySelectorAll("pre > code"));
await Promise.all(
codeBlocks.map(async (code) => {
const languageClass = Array.from(code.classList).find((className) =>
className.startsWith("language-"),
);
const requestedLanguage = languageClass?.slice("language-".length) || "text";
const language = resolveMarkdownCodeLanguage(requestedLanguage)?.shiki || "text";
code.parentElement?.setAttribute("data-language", requestedLanguage);
try {
const highlighted = await highlightCodeToHtml(code.textContent || "", language);
if (!highlighted) return;
const fragment = new DOMParser().parseFromString(highlighted, "text/html");
const highlightedPre = fragment.body.firstElementChild;
if (highlightedPre) {
highlightedPre.setAttribute("data-language", requestedLanguage);
code.parentElement?.replaceWith(highlightedPre);
}
} catch {
code.parentElement?.setAttribute("data-language", requestedLanguage);
}
}),
);
return document.body.innerHTML;
}
async function renderMarkdownHtml(md: string) {
const file = md2html.processSync(md);
return highlightCodeBlocks(String(file.value));
}
async function renderFullHtml(md: string) {
const requestId = ++renderRequestId;
try { try {
const file = md2html.processSync(md); const html = await renderMarkdownHtml(md);
renderedHtml.value = String(file.value); if (requestId !== renderRequestId || activeDocumentMode.value !== "preview") return;
renderedHtml.value = html;
renderError.value = false; renderError.value = false;
} catch { } catch {
if (requestId !== renderRequestId) return;
renderedHtml.value = ""; renderedHtml.value = "";
renderError.value = true; renderError.value = true;
} }
@@ -333,16 +377,16 @@ function escapeHtml(value: string) {
.replace(/'/g, "&#39;"); .replace(/'/g, "&#39;");
} }
function renderBlockHtml(source: string) { async function renderBlockHtml(source: string) {
try { try {
const file = md2html.processSync(source); return await renderMarkdownHtml(source);
return String(file.value);
} catch { } catch {
return `<pre><code>${escapeHtml(source)}</code></pre>`; return `<pre><code>${escapeHtml(source)}</code></pre>`;
} }
} }
function parseBlocks(md: string) { async function parseBlocks(md: string) {
const requestId = ++renderRequestId;
if (!md.trim()) { if (!md.trim()) {
markdownBlocks.value = []; markdownBlocks.value = [];
renderError.value = false; renderError.value = false;
@@ -351,8 +395,8 @@ function parseBlocks(md: string) {
try { try {
const tree = mdParser.parse(md) as { children?: MarkdownAstNode[] }; const tree = mdParser.parse(md) as { children?: MarkdownAstNode[] };
const blocks = (tree.children ?? []) const blockCandidates = (tree.children ?? [])
.map((node, index): MarkdownBlock | null => { .map((node, index): Omit<MarkdownBlock, "html"> | null => {
const startOffset = node.position?.start?.offset; const startOffset = node.position?.start?.offset;
const endOffset = node.position?.end?.offset; const endOffset = node.position?.end?.offset;
@@ -367,22 +411,33 @@ function parseBlocks(md: string) {
const source = md.slice(startOffset, endOffset); const source = md.slice(startOffset, endOffset);
return { return {
id: `${index}-${startOffset}-${endOffset}`, id: `${index}-${startOffset}-${endOffset}`,
type: node.type || "unknown",
source, source,
html: renderBlockHtml(source),
startOffset, startOffset,
endOffset, endOffset,
}; };
}) })
.filter((block): block is MarkdownBlock => block !== null); .filter((block): block is Omit<MarkdownBlock, "html"> => block !== null);
const blocks = await Promise.all(
blockCandidates.map(async (block) => ({
...block,
html: await renderBlockHtml(block.source),
})),
);
if (requestId !== renderRequestId || activeDocumentMode.value !== "mixed") return;
markdownBlocks.value = blocks; markdownBlocks.value = blocks;
renderError.value = false; renderError.value = false;
} catch { } catch {
const html = await renderBlockHtml(md);
if (requestId !== renderRequestId || activeDocumentMode.value !== "mixed") return;
markdownBlocks.value = [ markdownBlocks.value = [
{ {
id: "fallback-0", id: "fallback-0",
type: "unknown",
source: md, source: md,
html: renderBlockHtml(md), html,
startOffset: 0, startOffset: 0,
endOffset: md.length, endOffset: md.length,
}, },
@@ -407,7 +462,6 @@ watch(
} }
nextTick(() => { nextTick(() => {
scheduleEditorTextareaResize();
// 外部切换文件时重置滚动位置;本组件内编辑时保留当前位置。 // 外部切换文件时重置滚动位置;本组件内编辑时保留当前位置。
if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0; if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0;
}); });
@@ -415,40 +469,11 @@ watch(
{ immediate: true }, { immediate: true },
); );
// ── Editor textarea handlers ─────────────────────────────────────── // ── Editor handlers ────────────────────────────────────────────────
function setBlockTextareaRef(el: Element | ComponentPublicInstance | null) {
blockTextareaRef.value = el instanceof HTMLTextAreaElement ? el : null;
}
function resizeTextarea(ta: HTMLTextAreaElement) {
ta.style.height = "auto";
ta.style.height = `${ta.scrollHeight}px`;
}
function scheduleTextareaResize(ta: HTMLTextAreaElement | undefined | null) {
if (!ta) return;
if (textareaResizeFrame !== null) {
cancelAnimationFrame(textareaResizeFrame);
}
textareaResizeFrame = requestAnimationFrame(() => {
textareaResizeFrame = null;
if (ta.isConnected) resizeTextarea(ta);
});
}
function scheduleEditorTextareaResize() {
scheduleTextareaResize(editorTextareaRef.value);
}
function focusBlockEditor() { function focusBlockEditor() {
void nextTick(() => { void nextTick(() => {
const ta = blockTextareaRef.value; blockCodeRef.value?.focusAtEnd();
if (!ta) return;
resizeTextarea(ta);
ta.focus();
ta.selectionStart = ta.selectionEnd = ta.value.length;
}); });
} }
@@ -461,54 +486,6 @@ function applyFullEditorValue(value: string) {
emitModelValue(value); emitModelValue(value);
} }
function onEditorInput(e: Event) {
const ta = e.target as HTMLTextAreaElement;
const val = ta.value;
applyFullEditorValue(val);
scheduleTextareaResize(ta);
}
function tryAutoPairForValue(
ta: HTMLTextAreaElement,
key: string,
value: string,
onChange: (nextValue: string) => void,
): boolean {
const pairs: Record<string, string> = {
"(": ")",
"[": "]",
"{": "}",
'"': '"',
"'": "'",
"`": "`",
};
const close = pairs[key];
if (!close) return false;
const start = ta.selectionStart;
const end = ta.selectionEnd;
const selected = value.substring(start, end);
const newVal =
value.substring(0, start) +
key +
selected +
close +
value.substring(end);
onChange(newVal);
void nextTick(() => {
ta.selectionStart = start + 1;
ta.selectionEnd = start + 1 + selected.length;
ta.focus();
});
return true;
}
function tryAutoPair(ta: HTMLTextAreaElement, key: string): boolean {
return tryAutoPairForValue(ta, key, props.modelValue, applyFullEditorValue);
}
function isSaveShortcut(e: KeyboardEvent) { function isSaveShortcut(e: KeyboardEvent) {
return (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s"; return (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s";
} }
@@ -526,36 +503,6 @@ function onRootKeydown(e: KeyboardEvent) {
} }
} }
function onEditorKeydown(e: KeyboardEvent) {
if (isSaveShortcut(e)) {
requestSave(e);
return;
}
const ta = e.target as HTMLTextAreaElement;
if (e.key === "Tab") {
e.preventDefault();
const start = ta.selectionStart;
const end = ta.selectionEnd;
const newVal =
props.modelValue.substring(0, start) +
" " +
props.modelValue.substring(end);
applyFullEditorValue(newVal);
void nextTick(() => {
ta.selectionStart = ta.selectionEnd = start + 2;
ta.focus();
});
return;
}
if (tryAutoPair(ta, e.key)) {
e.preventDefault();
return;
}
}
// ── Mixed mode block editing ─────────────────────────────────────── // ── Mixed mode block editing ───────────────────────────────────────
function startBlockEdit(block: MarkdownBlock) { function startBlockEdit(block: MarkdownBlock) {
@@ -615,52 +562,6 @@ function finalizeBlockEdit(): string | undefined {
return nextValue; return nextValue;
} }
function onBlockInput(e: Event) {
const ta = e.target as HTMLTextAreaElement;
editingSource.value = ta.value;
scheduleTextareaResize(ta);
}
function onBlockKeydown(e: KeyboardEvent) {
const ta = e.target as HTMLTextAreaElement;
if (isSaveShortcut(e)) {
requestSave(e);
return;
}
if (e.key === "Escape") {
e.preventDefault();
cancelBlockEdit();
return;
}
if (e.key === "Tab") {
e.preventDefault();
const start = ta.selectionStart;
const end = ta.selectionEnd;
editingSource.value =
editingSource.value.substring(0, start) +
" " +
editingSource.value.substring(end);
void nextTick(() => {
ta.selectionStart = ta.selectionEnd = start + 2;
scheduleTextareaResize(ta);
ta.focus();
});
return;
}
if (
tryAutoPairForValue(ta, e.key, editingSource.value, (nextValue) => {
editingSource.value = nextValue;
void nextTick(() => scheduleTextareaResize(ta));
})
) {
e.preventDefault();
}
}
function flushPendingChanges(): string | undefined { function flushPendingChanges(): string | undefined {
if (!editingBlockId.value) return undefined; if (!editingBlockId.value) return undefined;
return finalizeBlockEdit(); return finalizeBlockEdit();
@@ -675,17 +576,10 @@ watch(activeDocumentMode, (mode, oldMode) => {
syncRenderedState(committedValue ?? props.modelValue, mode); syncRenderedState(committedValue ?? props.modelValue, mode);
if (mode === "edit") { restoreScrollAfterModeChange(scrollSnapshot);
void nextTick(scheduleEditorTextareaResize);
}
restoreScrollAfterModeChange(scrollSnapshot, mode);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (textareaResizeFrame !== null) {
cancelAnimationFrame(textareaResizeFrame);
}
if (modeScrollRestoreFrame !== null) { if (modeScrollRestoreFrame !== null) {
cancelAnimationFrame(modeScrollRestoreFrame); cancelAnimationFrame(modeScrollRestoreFrame);
} }
@@ -948,47 +842,38 @@ async function processPaste(
applyValue(newValue, cursorPos); applyValue(newValue, cursorPos);
} }
async function onEditorPaste(e: ClipboardEvent) { async function onEditorPaste(payload: {
const ta = editorTextareaRef.value; event: ClipboardEvent;
if (!ta) return; value: string;
from: number;
to: number;
}) {
await processPaste( await processPaste(
e, payload.event,
props.modelValue, payload.value,
ta.selectionStart, payload.from,
ta.selectionEnd, payload.to,
(newValue, cursorPos) => { (newValue, cursorPos) => {
applyFullEditorValue(newValue); const insertedText = newValue.slice(payload.from, cursorPos);
void nextTick(() => { editorCodeRef.value?.replaceRange(insertedText, payload.from, payload.to);
const ta = editorTextareaRef.value;
if (ta) {
ta.selectionStart = ta.selectionEnd = cursorPos;
ta.focus();
}
});
}, },
); );
} }
async function onBlockPaste(e: ClipboardEvent) { async function onBlockPaste(payload: {
const ta = blockTextareaRef.value; event: ClipboardEvent;
if (!ta) return; value: string;
from: number;
to: number;
}) {
await processPaste( await processPaste(
e, payload.event,
editingSource.value, payload.value,
ta.selectionStart, payload.from,
ta.selectionEnd, payload.to,
(newValue, cursorPos) => { (newValue, cursorPos) => {
editingSource.value = newValue; const insertedText = newValue.slice(payload.from, cursorPos);
void nextTick(() => { blockCodeRef.value?.replaceRange(insertedText, payload.from, payload.to);
const ta = blockTextareaRef.value;
if (ta) {
ta.selectionStart = ta.selectionEnd = cursorPos;
ta.focus();
scheduleTextareaResize(ta);
}
});
}, },
); );
} }
@@ -997,19 +882,7 @@ function scrollToLine(lineNumber: number) {
if (!scrollRef.value) return; if (!scrollRef.value) return;
if (activeDocumentMode.value === "edit") { if (activeDocumentMode.value === "edit") {
const ta = editorTextareaRef.value; editorCodeRef.value?.scrollToLine(lineNumber);
if (ta) {
const lineHeight = 24;
const targetScroll = (lineNumber - 1) * lineHeight;
ta.scrollTop = Math.max(0, targetScroll - 80);
ta.focus();
const lines = ta.value.split("\n");
let pos = 0;
for (let i = 0; i < Math.min(lineNumber - 1, lines.length); i++) {
pos += lines[i].length + 1;
}
ta.setSelectionRange(pos, pos);
}
} else { } else {
const previewEl = scrollRef.value.querySelector(".markdown-preview") as HTMLElement; const previewEl = scrollRef.value.querySelector(".markdown-preview") as HTMLElement;
if (previewEl) { if (previewEl) {
@@ -1052,18 +925,14 @@ defineExpose({
}" }"
> >
<!-- Editor mode --> <!-- Editor mode -->
<textarea <MarkdownCodeEditor
v-if="activeDocumentMode === 'edit'" v-if="activeDocumentMode === 'edit'"
ref="editorTextareaRef" ref="editorCodeRef"
:value="props.modelValue" :model-value="props.modelValue"
:placeholder="props.placeholder" :placeholder="props.placeholder"
class=" min-height="60vh"
markdown-source-editor class="markdown-source-editor w-full font-serif leading-relaxed"
w-full min-h-[60vh] resize-none overflow-hidden bg-transparent outline-none @update:model-value="applyFullEditorValue"
font-serif leading-relaxed
"
@input="onEditorInput"
@keydown="onEditorKeydown"
@paste="onEditorPaste" @paste="onEditorPaste"
/> />
@@ -1072,7 +941,7 @@ defineExpose({
<div v-if="renderError" class="text-red-500 text-sm"> <div v-if="renderError" class="text-red-500 text-sm">
Markdown 解析错误 Markdown 解析错误
</div> </div>
<div v-else class="markdown-preview py-2" v-html="renderedHtml" /> <div v-else class="markdown-preview" v-html="renderedHtml" />
</div> </div>
<!-- Mixed mode --> <!-- Mixed mode -->
@@ -1080,15 +949,16 @@ defineExpose({
<div v-if="renderError" class="text-red-500 text-sm"> <div v-if="renderError" class="text-red-500 text-sm">
Markdown 解析错误 Markdown 解析错误
</div> </div>
<textarea <MarkdownCodeEditor
v-else-if="isEditingEmptyDocument" v-else-if="isEditingEmptyDocument"
:ref="setBlockTextareaRef" ref="blockCodeRef"
:value="editingSource" :model-value="editingSource"
:placeholder="props.placeholder" :placeholder="props.placeholder"
rows="1" autofocus
min-height="12rem"
class="markdown-block-editor" class="markdown-block-editor"
@input="onBlockInput" @update:model-value="editingSource = $event"
@keydown="onBlockKeydown" @escape="cancelBlockEdit"
@blur="finalizeBlockEdit" @blur="finalizeBlockEdit"
@paste="onBlockPaste" @paste="onBlockPaste"
/> />
@@ -1110,14 +980,17 @@ defineExpose({
class="markdown-block" class="markdown-block"
:class="{ 'markdown-block-editing': editingBlockId === block.id }" :class="{ 'markdown-block-editing': editingBlockId === block.id }"
> >
<textarea <MarkdownCodeEditor
v-if="editingBlockId === block.id" v-if="editingBlockId === block.id"
:ref="setBlockTextareaRef" ref="blockCodeRef"
:value="editingSource" :model-value="editingSource"
rows="1" autofocus
class="markdown-block-editor" class="markdown-block-editor"
@input="onBlockInput" :class="{
@keydown="onBlockKeydown" 'markdown-block-editor-soft-wrap': block.type === 'paragraph',
}"
@update:model-value="editingSource = $event"
@escape="cancelBlockEdit"
@blur="finalizeBlockEdit" @blur="finalizeBlockEdit"
@paste="onBlockPaste" @paste="onBlockPaste"
/> />
@@ -1157,14 +1030,9 @@ defineExpose({
.markdown-source-editor { .markdown-source-editor {
display: block; display: block;
height: auto;
color: var(--app-text); color: var(--app-text);
} }
.markdown-source-editor::placeholder {
color: var(--app-subtle);
}
.markdown-mixed > .markdown-block:first-child .markdown-block-preview > *:first-child { .markdown-mixed > .markdown-block:first-child .markdown-block-preview > *:first-child {
margin-top: 0; margin-top: 0;
} }
@@ -1209,10 +1077,30 @@ defineExpose({
overflow-x: auto; overflow-x: auto;
margin: 0.75em 0; margin: 0.75em 0;
} }
.markdown-preview pre.shiki {
position: relative;
background-color: var(--code-background) !important;
color: var(--code-foreground);
border: 1px solid var(--code-border);
}
.markdown-preview pre.shiki[data-language]::before {
content: attr(data-language);
display: block;
margin-bottom: 0.65rem;
color: var(--app-muted);
font-family: "Geist", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 0.7rem;
font-weight: 600;
line-height: 1;
text-transform: lowercase;
}
.markdown-preview pre.shiki span {
color: inherit;
}
.markdown-preview pre code { .markdown-preview pre code {
background: none; background: none;
padding: 0; padding: 0;
color: var(--app-text); color: var(--code-foreground);
font-size: 0.875em; font-size: 0.875em;
border-radius: 0; border-radius: 0;
word-break: normal; word-break: normal;
@@ -1315,20 +1203,28 @@ defineExpose({
.markdown-block-editor { .markdown-block-editor {
display: block; display: block;
width: 100%; width: 100%;
min-height: calc((1em * 1.6) + 1.5rem + 2px); min-height: 1.625em;
resize: none; resize: none;
overflow: hidden; overflow: visible;
border: 1px solid var(--app-accent-muted); border: 0;
border-radius: 0.375rem; border-radius: 0;
background-color: var(--app-surface); background: transparent;
color: var(--app-text); color: var(--app-text);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font: inherit;
font-size: 0.92em; line-height: inherit;
line-height: 1.6; margin: 0;
margin: 0.375rem 0; padding: 0;
padding: 0.75rem 0.875rem;
outline: none; outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-accent) 12%, transparent); box-shadow: none;
caret-color: var(--app-accent);
}
.markdown-block-editor .cm-tooltip {
font-size: calc(0.875rem * var(--editor-zoom, 1));
}
.markdown-block-editor-soft-wrap {
white-space: normal;
} }
.markdown-block-editor::placeholder, .markdown-block-editor::placeholder,

View File

@@ -122,14 +122,45 @@ const documentModeOptions: Array<{
const themeOptions: Array<{ const themeOptions: Array<{
value: AppTheme; value: AppTheme;
label: string; label: string;
swatch: string; description: string;
swatches: [string, string, string];
}> = [ }> = [
{ value: "system", label: "跟随系统", swatch: "linear-gradient(135deg, #ffffff 0 50%, #17191d 50% 100%)" }, {
{ value: "warm", label: "柔和暖色", swatch: "#bf6a3b" }, value: "system",
{ value: "white", label: "白色", swatch: "#2563eb" }, label: "跟随系统",
{ value: "black", label: "黑色", swatch: "#8ab4ff" }, description: "随操作系统在浅色与深色之间切换",
{ value: "gruvbox", label: "Gruvbox", swatch: "#fe8019" }, swatches: ["#ffffff", "#101113", "#7c9cff"],
{ value: "gray", label: "灰色", swatch: "#4f6f93" }, },
{
value: "warm",
label: "柔和暖色",
description: "暖白纸张、陶土强调色",
swatches: ["#faf9f6", "#bf6a3b", "#7a55d9"],
},
{
value: "white",
label: "明亮白色",
description: "纯白背景、清晰蓝色强调",
swatches: ["#ffffff", "#2563eb", "#8250df"],
},
{
value: "gray",
label: "中性灰色",
description: "低饱和界面、沉静蓝灰强调",
swatches: ["#f3f4f6", "#4f6f93", "#7c5c99"],
},
{
value: "black",
label: "深色",
description: "近黑背景、冷调高对比代码色",
swatches: ["#101113", "#8ab4ff", "#c58af9"],
},
{
value: "gruvbox",
label: "Gruvbox",
description: "复古深色、暖橙与青绿色代码色",
swatches: ["#282828", "#fe8019", "#8ec07c"],
},
]; ];
function loadBooleanPref(key: string, fallback: boolean) { function loadBooleanPref(key: string, fallback: boolean) {
@@ -239,15 +270,6 @@ function onDefaultDocumentModeChange(event: Event) {
emit("updateDefaultDocumentMode", mode); emit("updateDefaultDocumentMode", mode);
} }
function onThemeChange(event: Event) {
const theme = (event.target as HTMLSelectElement).value as AppTheme;
emit("updateAppTheme", theme);
}
function resetTheme() {
emit("updateAppTheme", "warm");
}
function setSyncStatus(message: string, isError = false) { function setSyncStatus(message: string, isError = false) {
if (isError) { if (isError) {
syncError.value = message; syncError.value = message;
@@ -690,51 +712,35 @@ onBeforeUnmount(() => {
<section class="settings-about-card" aria-label="外观设置"> <section class="settings-about-card" aria-label="外观设置">
<div class="settings-row settings-row-top"> <div class="settings-row settings-row-top">
<div class="settings-copy"> <div class="settings-copy">
<h3>基础颜色</h3> <h3>外观主题</h3>
<p>设置 {{ APP_NAME }} 的基础颜色</p> <p>主题同时应用于界面Markdown 预览和代码编辑</p>
</div> </div>
<select
class="settings-select"
aria-label="基础颜色"
:value="appTheme"
@change="onThemeChange"
>
<option
v-for="option in themeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div> </div>
<div class="settings-row"> <div class="theme-option-grid" role="radiogroup" aria-label="外观主题">
<div class="settings-copy">
<h3>主题色</h3>
<p> {{ APP_NAME }} 选择一个主题色主题色将影响选中链接等元素的颜色</p>
</div>
<div class="theme-swatch-group" aria-label="主题色">
<button <button
class="theme-reset-btn" v-for="option in themeOptions"
type="button"
title="重置主题色"
@click="resetTheme"
>
<i class="ri-reset-left-line text-lg"></i>
</button>
<button
v-for="option in themeOptions.filter((item) => item.value !== 'system')"
:key="option.value" :key="option.value"
class="theme-swatch" class="theme-option"
:class="{ 'theme-swatch-active': appTheme === option.value }" :class="{ 'theme-option-active': appTheme === option.value }"
type="button" type="button"
:title="option.label" role="radio"
:aria-label="option.label" :aria-checked="appTheme === option.value"
:style="{ background: option.swatch }"
@click="emit('updateAppTheme', option.value)" @click="emit('updateAppTheme', option.value)"
></button> >
</div> <span class="theme-option-preview" aria-hidden="true">
<span
v-for="(swatch, index) in option.swatches"
:key="swatch"
:style="{ background: swatch, flex: index === 0 ? 2 : 1 }"
></span>
</span>
<span class="theme-option-copy">
<strong>{{ option.label }}</strong>
<small>{{ option.description }}</small>
</span>
<i v-if="appTheme === option.value" class="ri-check-line"></i>
</button>
</div> </div>
</section> </section>
</div> </div>
@@ -1165,46 +1171,70 @@ onBeforeUnmount(() => {
color: #dc2626; color: #dc2626;
} }
.theme-swatch-group { .theme-option-grid {
display: inline-flex; display: grid;
align-items: center; grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px; gap: 10px;
padding: 4px 20px 20px;
} }
.theme-reset-btn, .theme-option {
.theme-swatch { display: grid;
display: inline-flex; grid-template-columns: 48px minmax(0, 1fr) 18px;
gap: 11px;
align-items: center; align-items: center;
justify-content: center; min-height: 64px;
border: none; padding: 10px;
cursor: pointer; border: 1px solid var(--app-border-strong);
} border-radius: 7px;
background: var(--app-surface);
.theme-reset-btn {
width: 30px;
height: 30px;
border-radius: 6px;
background: transparent;
color: var(--app-muted);
}
.theme-reset-btn:hover {
background: var(--app-hover);
color: var(--app-text); color: var(--app-text);
text-align: left;
cursor: pointer;
transition: border-color 0.15s ease, background-color 0.15s ease;
} }
.theme-swatch { .theme-option:hover {
width: 22px; background: var(--app-surface-muted);
height: 22px;
border-radius: 999px;
box-shadow:
0 0 0 1px color-mix(in srgb, var(--app-text) 18%, transparent),
0 2px 5px color-mix(in srgb, var(--app-shadow) 70%, transparent);
} }
.theme-swatch-active { .theme-option-active {
outline: 2px solid var(--app-accent); border-color: var(--app-accent);
outline-offset: 3px; background: var(--app-active-bg);
}
.theme-option-preview {
display: flex;
width: 48px;
height: 38px;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--app-text) 18%, transparent);
border-radius: 5px;
box-shadow: 0 2px 5px color-mix(in srgb, var(--app-shadow) 55%, transparent);
}
.theme-option-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.theme-option-copy strong {
color: var(--app-text-strong);
font-size: 13px;
line-height: 1.2;
}
.theme-option-copy small {
color: var(--app-muted);
font-size: 11px;
line-height: 1.35;
}
.theme-option > i {
color: var(--app-accent);
font-size: 16px;
} }
.settings-empty-page { .settings-empty-page {

View File

@@ -44,6 +44,21 @@
--app-overlay: rgba(56, 52, 46, 0.22); --app-overlay: rgba(56, 52, 46, 0.22);
--app-toast-bg: rgba(56, 52, 46, 0.8); --app-toast-bg: rgba(56, 52, 46, 0.8);
--app-shadow: rgba(56, 52, 46, 0.16); --app-shadow: rgba(56, 52, 46, 0.16);
--code-background: #f4f1ea;
--code-foreground: #38342e;
--code-border: #e0dbcf;
--code-token-comment: #8c877d;
--code-token-constant: #8c4f2b;
--code-token-keyword: #7651a8;
--code-token-parameter: #9f4f28;
--code-token-function: #315f78;
--code-token-string: #6f641f;
--code-token-string-expression: #6f641f;
--code-token-punctuation: #6e685f;
--code-token-link: #6342bd;
--code-token-inserted: #3d7a4d;
--code-token-deleted: #b34242;
--code-token-changed: #9a691f;
} }
:root[data-theme="white"] { :root[data-theme="white"] {
@@ -74,6 +89,21 @@
--app-overlay: rgba(17, 24, 39, 0.2); --app-overlay: rgba(17, 24, 39, 0.2);
--app-toast-bg: rgba(17, 24, 39, 0.82); --app-toast-bg: rgba(17, 24, 39, 0.82);
--app-shadow: rgba(17, 24, 39, 0.15); --app-shadow: rgba(17, 24, 39, 0.15);
--code-background: #f4f6f8;
--code-foreground: #24292f;
--code-border: #d7dbe0;
--code-token-comment: #6e7781;
--code-token-constant: #953800;
--code-token-keyword: #8250df;
--code-token-parameter: #953800;
--code-token-function: #0550ae;
--code-token-string: #0a3069;
--code-token-string-expression: #0a3069;
--code-token-punctuation: #57606a;
--code-token-link: #0969da;
--code-token-inserted: #1a7f37;
--code-token-deleted: #cf222e;
--code-token-changed: #9a6700;
} }
:root[data-theme="gray"] { :root[data-theme="gray"] {
@@ -104,6 +134,21 @@
--app-overlay: rgba(31, 35, 40, 0.24); --app-overlay: rgba(31, 35, 40, 0.24);
--app-toast-bg: rgba(31, 35, 40, 0.82); --app-toast-bg: rgba(31, 35, 40, 0.82);
--app-shadow: rgba(31, 35, 40, 0.14); --app-shadow: rgba(31, 35, 40, 0.14);
--code-background: #eceff2;
--code-foreground: #2f3337;
--code-border: #cfd5dc;
--code-token-comment: #737a82;
--code-token-constant: #8b572a;
--code-token-keyword: #7c5c99;
--code-token-parameter: #8b572a;
--code-token-function: #3b668c;
--code-token-string: #537238;
--code-token-string-expression: #537238;
--code-token-punctuation: #656c74;
--code-token-link: #3b668c;
--code-token-inserted: #4e7a52;
--code-token-deleted: #a94a4a;
--code-token-changed: #8b6b2f;
} }
:root[data-theme="black"] { :root[data-theme="black"] {
@@ -134,6 +179,21 @@
--app-overlay: rgba(0, 0, 0, 0.48); --app-overlay: rgba(0, 0, 0, 0.48);
--app-toast-bg: rgba(0, 0, 0, 0.82); --app-toast-bg: rgba(0, 0, 0, 0.82);
--app-shadow: rgba(0, 0, 0, 0.36); --app-shadow: rgba(0, 0, 0, 0.36);
--code-background: #1b1e23;
--code-foreground: #e7e9ed;
--code-border: #383d45;
--code-token-comment: #8b949e;
--code-token-constant: #ffa657;
--code-token-keyword: #ff7b72;
--code-token-parameter: #ffa657;
--code-token-function: #d2a8ff;
--code-token-string: #a5d6ff;
--code-token-string-expression: #a5d6ff;
--code-token-punctuation: #b1bac4;
--code-token-link: #79c0ff;
--code-token-inserted: #7ee787;
--code-token-deleted: #ff7b72;
--code-token-changed: #e3b341;
} }
:root[data-theme="gruvbox"] { :root[data-theme="gruvbox"] {
@@ -164,6 +224,21 @@
--app-overlay: rgba(29, 32, 33, 0.52); --app-overlay: rgba(29, 32, 33, 0.52);
--app-toast-bg: rgba(29, 32, 33, 0.9); --app-toast-bg: rgba(29, 32, 33, 0.9);
--app-shadow: rgba(0, 0, 0, 0.38); --app-shadow: rgba(0, 0, 0, 0.38);
--code-background: #1d2021;
--code-foreground: #ebdbb2;
--code-border: #504945;
--code-token-comment: #928374;
--code-token-constant: #d3869b;
--code-token-keyword: #fb4934;
--code-token-parameter: #fe8019;
--code-token-function: #fabd2f;
--code-token-string: #b8bb26;
--code-token-string-expression: #b8bb26;
--code-token-punctuation: #bdae93;
--code-token-link: #83a598;
--code-token-inserted: #b8bb26;
--code-token-deleted: #fb4934;
--code-token-changed: #fabd2f;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -195,6 +270,21 @@
--app-overlay: rgba(0, 0, 0, 0.48); --app-overlay: rgba(0, 0, 0, 0.48);
--app-toast-bg: rgba(0, 0, 0, 0.82); --app-toast-bg: rgba(0, 0, 0, 0.82);
--app-shadow: rgba(0, 0, 0, 0.36); --app-shadow: rgba(0, 0, 0, 0.36);
--code-background: #1b1e23;
--code-foreground: #e7e9ed;
--code-border: #383d45;
--code-token-comment: #8b949e;
--code-token-constant: #ffa657;
--code-token-keyword: #ff7b72;
--code-token-parameter: #ffa657;
--code-token-function: #d2a8ff;
--code-token-string: #a5d6ff;
--code-token-string-expression: #a5d6ff;
--code-token-punctuation: #b1bac4;
--code-token-link: #79c0ff;
--code-token-inserted: #7ee787;
--code-token-deleted: #ff7b72;
--code-token-changed: #e3b341;
} }
} }
@@ -374,4 +464,4 @@ body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
@apply font-sans; @apply font-sans;
} }
} }