Compare commits
10 Commits
6a66c7867a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 81a0484654 | |||
| 63d1f0b729 | |||
| abd9f2912f | |||
| 0f75a3a6af | |||
| 05898a14e0 | |||
| f725261370 | |||
| 3c6b142098 | |||
| 164e658e5b | |||
| c5a5a4e55b | |||
| 87e594be52 |
@@ -58,5 +58,23 @@
|
|||||||
"userTurns": 4,
|
"userTurns": 4,
|
||||||
"basisHash": "aaae120b1f33c2f2",
|
"basisHash": "aaae120b1f33c2f2",
|
||||||
"updatedAt": 1783674217586
|
"updatedAt": 1783674217586
|
||||||
|
},
|
||||||
|
"topic_20260710-090003_60717bd90da5ec68": {
|
||||||
|
"stage": 3,
|
||||||
|
"userTurns": 3,
|
||||||
|
"basisHash": "de6e56c00454cedd",
|
||||||
|
"updatedAt": 1784540365461
|
||||||
|
},
|
||||||
|
"topic_20260720-095943_8dd2c8de3f143692": {
|
||||||
|
"stage": 1,
|
||||||
|
"userTurns": 1,
|
||||||
|
"basisHash": "b2acebe8a17318f4",
|
||||||
|
"updatedAt": 1784541735771
|
||||||
|
},
|
||||||
|
"topic_20260722-163413_8ebd34b08091d260": {
|
||||||
|
"stage": 1,
|
||||||
|
"userTurns": 1,
|
||||||
|
"basisHash": "c61a582d55782664",
|
||||||
|
"updatedAt": 1784738151003
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
{
|
{
|
||||||
"topic_20260630-144952_fd0584fe8f266d6a": 1782830992641,
|
"topic_20260707-131941_481a70d2be9881dd": 1783430381000,
|
||||||
"topic_20260703-022850_50330b4eb93f116d": 1783045730616,
|
"topic_20260720-095943_8dd2c8de3f143692": 1784541583642,
|
||||||
"topic_20260706-145507_076a32df5f4276b5": 1783349707873,
|
"topic_20260722-163413_8ebd34b08091d260": 1784738053267
|
||||||
"topic_20260706-153317_238f65a7a470703c": 1783351997029,
|
|
||||||
"topic_20260710-024855_5c4b1833149d66bb": 1783651735446,
|
|
||||||
"topic_20260710-030936_cedad2907c3954fe": 1783652976310,
|
|
||||||
"topic_20260710-032907_600adc6b00791607": 1783654147796,
|
|
||||||
"topic_20260710-034033_6432ee6566f7b432": 1783654833341,
|
|
||||||
"topic_20260710-034724_3c6fb29d8d0a4119": 1783655244536,
|
|
||||||
"topic_20260710-054543_5e7c427932aecd7b": 1783662343716,
|
|
||||||
"topic_20260710-063053_353d579222d3b70b": 1783665053391,
|
|
||||||
"topic_20260710-090003_60717bd90da5ec68": 1783674003004
|
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"topic_20260707-093602_5c75f77654cab0c3": "manual",
|
"topic_20260707-093602_5c75f77654cab0c3": "manual",
|
||||||
"topic_20260707-094645_05255aaa45d5f673": "manual",
|
"topic_20260707-094645_05255aaa45d5f673": "manual",
|
||||||
"topic_20260707-095503_bf879b65e4666516": "auto",
|
"topic_20260707-095503_bf879b65e4666516": "auto",
|
||||||
|
"topic_20260707-131941_481a70d2be9881dd": "manual",
|
||||||
"topic_20260708-020616_9ab54d98daf65053": "auto",
|
"topic_20260708-020616_9ab54d98daf65053": "auto",
|
||||||
"topic_20260708-093419_71d1fea2e4b5a40f": "auto",
|
"topic_20260708-093419_71d1fea2e4b5a40f": "auto",
|
||||||
"topic_20260708-095428_990617acd4168b40": "auto",
|
"topic_20260708-095428_990617acd4168b40": "auto",
|
||||||
@@ -48,5 +49,7 @@
|
|||||||
"topic_20260710-034724_3c6fb29d8d0a4119": "auto",
|
"topic_20260710-034724_3c6fb29d8d0a4119": "auto",
|
||||||
"topic_20260710-054543_5e7c427932aecd7b": "auto",
|
"topic_20260710-054543_5e7c427932aecd7b": "auto",
|
||||||
"topic_20260710-063053_353d579222d3b70b": "auto",
|
"topic_20260710-063053_353d579222d3b70b": "auto",
|
||||||
"topic_20260710-090003_60717bd90da5ec68": "auto"
|
"topic_20260710-090003_60717bd90da5ec68": "auto",
|
||||||
|
"topic_20260720-095943_8dd2c8de3f143692": "auto",
|
||||||
|
"topic_20260722-163413_8ebd34b08091d260": "auto"
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,8 @@
|
|||||||
"topic_20260707-093602_5c75f77654cab0c3": "隐藏单栏和双栏的切换功能,把mark…",
|
"topic_20260707-093602_5c75f77654cab0c3": "隐藏单栏和双栏的切换功能,把mark…",
|
||||||
"topic_20260707-094645_05255aaa45d5f673": "帮我移除markdownEditor…",
|
"topic_20260707-094645_05255aaa45d5f673": "帮我移除markdownEditor…",
|
||||||
"topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…",
|
"topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…",
|
||||||
|
"topic_20260707-131941_481a70d2be9881dd": "新的会话",
|
||||||
|
"topic_20260707-132824_49b096d569ffa3d3": "现在帮我把这个markdownEdi…",
|
||||||
"topic_20260708-020616_9ab54d98daf65053": "帮我分析一下现在的markdownE…",
|
"topic_20260708-020616_9ab54d98daf65053": "帮我分析一下现在的markdownE…",
|
||||||
"topic_20260708-093419_71d1fea2e4b5a40f": "src/components/Dir…",
|
"topic_20260708-093419_71d1fea2e4b5a40f": "src/components/Dir…",
|
||||||
"topic_20260708-095428_990617acd4168b40": "帮我检查一下这个应用在进行放大缩小的…",
|
"topic_20260708-095428_990617acd4168b40": "帮我检查一下这个应用在进行放大缩小的…",
|
||||||
@@ -48,5 +50,7 @@
|
|||||||
"topic_20260710-034724_3c6fb29d8d0a4119": "目录树中鼠标悬浮弹框dropmenu…",
|
"topic_20260710-034724_3c6fb29d8d0a4119": "目录树中鼠标悬浮弹框dropmenu…",
|
||||||
"topic_20260710-054543_5e7c427932aecd7b": "现在在markdown的编辑区域中…",
|
"topic_20260710-054543_5e7c427932aecd7b": "现在在markdown的编辑区域中…",
|
||||||
"topic_20260710-063053_353d579222d3b70b": "这个应用里有一个全局搜索的功能,帮我…",
|
"topic_20260710-063053_353d579222d3b70b": "这个应用里有一个全局搜索的功能,帮我…",
|
||||||
"topic_20260710-090003_60717bd90da5ec68": "新的会话"
|
"topic_20260710-090003_60717bd90da5ec68": "D:\\code\\yurou\\src…",
|
||||||
|
"topic_20260720-095943_8dd2c8de3f143692": "帮我调整一下项目的logo,用D…",
|
||||||
|
"topic_20260722-163413_8ebd34b08091d260": "markdownEditor底部区域…"
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,6 @@ This template should help get you started developing with Vue 3 and TypeScript i
|
|||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$env:HTTPS_PROXY="http://127.0.0.1:10808"
|
$env:HTTPS_PROXY="http://127.0.0.1:10808"
|
||||||
|
|
||||||
|
hello
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/yurou-logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Markdown Editor</title>
|
<title>Markdown Editor</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
9531
package-lock.json
generated
Normal file
12
package.json
@@ -10,6 +10,16 @@
|
|||||||
"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",
|
||||||
|
"@fancyapps/ui": "^6.1.14",
|
||||||
|
"@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",
|
||||||
@@ -20,11 +30,13 @@
|
|||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"reka-ui": "^2.10.1",
|
"reka-ui": "^2.10.1",
|
||||||
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"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",
|
||||||
|
|||||||
709
pnpm-lock.yaml
generated
@@ -4,10 +4,43 @@ 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
|
||||||
|
'@fancyapps/ui':
|
||||||
|
specifier: ^6.1.14
|
||||||
|
version: 6.1.14
|
||||||
|
'@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))
|
||||||
@@ -38,6 +71,9 @@ importers:
|
|||||||
reka-ui:
|
reka-ui:
|
||||||
specifier: ^2.10.1
|
specifier: ^2.10.1
|
||||||
version: 2.10.1(vue@3.5.39(typescript@5.6.3))
|
version: 2.10.1(vue@3.5.39(typescript@5.6.3))
|
||||||
|
remark-breaks:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.0.0
|
||||||
remark-gfm:
|
remark-gfm:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@@ -53,6 +89,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 +263,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
|
||||||
@@ -417,6 +546,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
|
resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@fancyapps/ui@6.1.14':
|
||||||
|
resolution: {integrity: sha512-cBV73XXHrc0qMca+PJJ6qay3xweIgsoYse13rGRstgUs0xA5WAv1tgbSzPCIOOjiLs2S2jiVFJxyszgvgyfxMg==}
|
||||||
|
|
||||||
'@floating-ui/core@1.7.5':
|
'@floating-ui/core@1.7.5':
|
||||||
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||||
|
|
||||||
@@ -485,11 +617,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 +840,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 +1076,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 +1430,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'}
|
||||||
@@ -1624,6 +1847,7 @@ packages:
|
|||||||
glob@11.0.0:
|
glob@11.0.0:
|
||||||
resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
|
resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@7.2.0:
|
glob@7.2.0:
|
||||||
@@ -1997,6 +2221,9 @@ packages:
|
|||||||
mdast-util-gfm@3.1.0:
|
mdast-util-gfm@3.1.0:
|
||||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||||
|
|
||||||
|
mdast-util-newline-to-break@2.0.0:
|
||||||
|
resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==}
|
||||||
|
|
||||||
mdast-util-phrasing@4.1.0:
|
mdast-util-phrasing@4.1.0:
|
||||||
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
||||||
|
|
||||||
@@ -2223,6 +2450,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 +2643,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==}
|
||||||
|
|
||||||
@@ -2421,6 +2663,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: '>= 3.4.0'
|
vue: '>= 3.4.0'
|
||||||
|
|
||||||
|
remark-breaks@4.0.0:
|
||||||
|
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
|
||||||
|
|
||||||
remark-gfm@4.0.1:
|
remark-gfm@4.0.1:
|
||||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||||
|
|
||||||
@@ -2514,6 +2759,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'}
|
||||||
@@ -2571,10 +2820,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==}
|
resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
string-width@4.2.0:
|
|
||||||
resolution: {integrity: sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2606,6 +2851,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 +3074,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 +3321,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
|
||||||
@@ -3199,6 +3701,8 @@ snapshots:
|
|||||||
'@eslint/core': 1.2.1
|
'@eslint/core': 1.2.1
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@fancyapps/ui@6.1.14': {}
|
||||||
|
|
||||||
'@floating-ui/core@1.7.5':
|
'@floating-ui/core@1.7.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/utils': 0.2.11
|
'@floating-ui/utils': 0.2.11
|
||||||
@@ -3250,7 +3754,7 @@ snapshots:
|
|||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 5.1.2
|
string-width: 5.1.2
|
||||||
string-width-cjs: string-width@4.2.0
|
string-width-cjs: string-width@4.2.3
|
||||||
strip-ansi: 7.2.0
|
strip-ansi: 7.2.0
|
||||||
strip-ansi-cjs: strip-ansi@6.0.1
|
strip-ansi-cjs: strip-ansi@6.0.1
|
||||||
wrap-ansi: 8.1.0
|
wrap-ansi: 8.1.0
|
||||||
@@ -3277,10 +3781,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 +3992,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 +4186,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 +4550,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 +5051,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: {}
|
||||||
|
|
||||||
@@ -4721,6 +5366,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
mdast-util-phrasing@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
@@ -5048,6 +5698,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 +5893,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
|
||||||
@@ -5262,6 +5930,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@vue/composition-api'
|
- '@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:
|
remark-gfm@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
@@ -5449,6 +6123,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
|
||||||
@@ -5506,12 +6191,6 @@ snapshots:
|
|||||||
|
|
||||||
stdin-discarder@0.3.2: {}
|
stdin-discarder@0.3.2: {}
|
||||||
|
|
||||||
string-width@4.2.0:
|
|
||||||
dependencies:
|
|
||||||
emoji-regex: 8.0.0
|
|
||||||
is-fullwidth-code-point: 3.0.0
|
|
||||||
strip-ansi: 6.0.1
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
@@ -5551,6 +6230,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 +6452,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:
|
||||||
@@ -5786,7 +6469,7 @@ snapshots:
|
|||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
string-width: 4.2.0
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
wrap-ansi@8.1.0:
|
wrap-ansi@8.1.0:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
allowBuilds:
|
allowBuilds:
|
||||||
esbuild: true
|
esbuild: true
|
||||||
vue-demi: true
|
vue-demi: true
|
||||||
|
overrides:
|
||||||
|
'@codemirror/lang-markdown': 6.4.0
|
||||||
|
|||||||
BIN
public/yurou-logo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
@@ -8,6 +8,7 @@
|
|||||||
"core:window:allow-minimize",
|
"core:window:allow-minimize",
|
||||||
"core:window:allow-toggle-maximize",
|
"core:window:allow-toggle-maximize",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-destroy",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"dialog:default"
|
"dialog:default"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 131 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
BIN
src-tauri/icons/file-icon.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 181 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 690 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 618 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
53
src-tauri/nsis-hooks.nsh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
; NSIS installer hooks for yurou
|
||||||
|
; Override file association DefaultIcon to use custom file icon (yurou-sys.png → file-icon.ico)
|
||||||
|
|
||||||
|
!macro NSIS_HOOK_POSTINSTALL
|
||||||
|
; Read the file class (ProgID) registered by Tauri's APP_ASSOCIATE,
|
||||||
|
; then overwrite its DefaultIcon to point to our custom file-icon.ico
|
||||||
|
; instead of the application exe icon.
|
||||||
|
;
|
||||||
|
; This repeats for every extension we registered.
|
||||||
|
; If the extension was not associated (backup value is empty) we skip it.
|
||||||
|
|
||||||
|
!define YUROU_FILE_ICON '"$INSTDIR\file-icon.ico"'
|
||||||
|
|
||||||
|
; .md
|
||||||
|
ReadRegStr $0 SHCTX "Software\Classes\.md" ""
|
||||||
|
StrCmp $0 "" +2 +1
|
||||||
|
WriteRegStr SHCTX "Software\Classes\$0\DefaultIcon" "" ${YUROU_FILE_ICON}
|
||||||
|
|
||||||
|
; .markdown
|
||||||
|
ReadRegStr $0 SHCTX "Software\Classes\.markdown" ""
|
||||||
|
StrCmp $0 "" +2 +1
|
||||||
|
WriteRegStr SHCTX "Software\Classes\$0\DefaultIcon" "" ${YUROU_FILE_ICON}
|
||||||
|
|
||||||
|
; .mdown
|
||||||
|
ReadRegStr $0 SHCTX "Software\Classes\.mdown" ""
|
||||||
|
StrCmp $0 "" +2 +1
|
||||||
|
WriteRegStr SHCTX "Software\Classes\$0\DefaultIcon" "" ${YUROU_FILE_ICON}
|
||||||
|
|
||||||
|
; .mkdn
|
||||||
|
ReadRegStr $0 SHCTX "Software\Classes\.mkdn" ""
|
||||||
|
StrCmp $0 "" +2 +1
|
||||||
|
WriteRegStr SHCTX "Software\Classes\$0\DefaultIcon" "" ${YUROU_FILE_ICON}
|
||||||
|
|
||||||
|
; .mkd
|
||||||
|
ReadRegStr $0 SHCTX "Software\Classes\.mkd" ""
|
||||||
|
StrCmp $0 "" +2 +1
|
||||||
|
WriteRegStr SHCTX "Software\Classes\$0\DefaultIcon" "" ${YUROU_FILE_ICON}
|
||||||
|
|
||||||
|
; .mdwn
|
||||||
|
ReadRegStr $0 SHCTX "Software\Classes\.mdwn" ""
|
||||||
|
StrCmp $0 "" +2 +1
|
||||||
|
WriteRegStr SHCTX "Software\Classes\$0\DefaultIcon" "" ${YUROU_FILE_ICON}
|
||||||
|
|
||||||
|
!undef YUROU_FILE_ICON
|
||||||
|
|
||||||
|
; Notify Windows shell about the association change
|
||||||
|
System::Call "shell32::SHChangeNotify(i,i,i,i) (0x08000000, 0x1000, 0, 0)"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro NSIS_HOOK_PREUNINSTALL
|
||||||
|
; Cleanup is handled automatically by APP_UNASSOCIATE which deletes the
|
||||||
|
; entire ProgID key — our custom DefaultIcon is removed along with it.
|
||||||
|
!macroend
|
||||||
@@ -552,12 +552,37 @@ struct SyncStats {
|
|||||||
skipped: usize,
|
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)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SyncResult {
|
struct SyncResult {
|
||||||
message: String,
|
message: String,
|
||||||
last_sync_at: String,
|
last_sync_at: String,
|
||||||
stats: SyncStats,
|
stats: SyncStats,
|
||||||
|
files: Vec<SyncFileDetail>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SyncConfigSaveResult {
|
||||||
|
config_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
@@ -1173,6 +1198,7 @@ async fn sync_workspace(
|
|||||||
let remote_files = scan_remote_files(&op).await?;
|
let remote_files = scan_remote_files(&op).await?;
|
||||||
let mut history = load_sync_history(app, &key)?;
|
let mut history = load_sync_history(app, &key)?;
|
||||||
let mut stats = SyncStats::default();
|
let mut stats = SyncStats::default();
|
||||||
|
let mut file_details = Vec::new();
|
||||||
|
|
||||||
let all_paths: BTreeSet<String> = local_files
|
let all_paths: BTreeSet<String> = local_files
|
||||||
.keys()
|
.keys()
|
||||||
@@ -1199,6 +1225,11 @@ async fn sync_workspace(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
stats.uploaded += 1;
|
stats.uploaded += 1;
|
||||||
|
file_details.push(SyncFileDetail {
|
||||||
|
path: relative.clone(),
|
||||||
|
action: SyncFileAction::Upload,
|
||||||
|
size: local.map(|item| item.size).unwrap_or(0),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
SyncAction::Download => {
|
SyncAction::Download => {
|
||||||
let local_path = local_path_for(workspace, &relative);
|
let local_path = local_path_for(workspace, &relative);
|
||||||
@@ -1216,6 +1247,11 @@ async fn sync_workspace(
|
|||||||
fs::write(&local_path, bytes)
|
fs::write(&local_path, bytes)
|
||||||
.map_err(|e| format!("无法写入本地文件 {}: {}", local_path.display(), e))?;
|
.map_err(|e| format!("无法写入本地文件 {}: {}", local_path.display(), e))?;
|
||||||
stats.downloaded += 1;
|
stats.downloaded += 1;
|
||||||
|
file_details.push(SyncFileDetail {
|
||||||
|
path: relative.clone(),
|
||||||
|
action: SyncFileAction::Download,
|
||||||
|
size: remote.map(|item| item.size).unwrap_or(0),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
SyncAction::DeleteLocal => {
|
SyncAction::DeleteLocal => {
|
||||||
let local_path = local_path_for(workspace, &relative);
|
let local_path = local_path_for(workspace, &relative);
|
||||||
@@ -1225,6 +1261,11 @@ async fn sync_workspace(
|
|||||||
}
|
}
|
||||||
history.files.remove(&relative);
|
history.files.remove(&relative);
|
||||||
stats.deleted_local += 1;
|
stats.deleted_local += 1;
|
||||||
|
file_details.push(SyncFileDetail {
|
||||||
|
path: relative.clone(),
|
||||||
|
action: SyncFileAction::DeleteLocal,
|
||||||
|
size: local.map(|item| item.size).unwrap_or(0),
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
SyncAction::DeleteRemote => {
|
SyncAction::DeleteRemote => {
|
||||||
@@ -1236,10 +1277,23 @@ async fn sync_workspace(
|
|||||||
.await?;
|
.await?;
|
||||||
history.files.remove(&relative);
|
history.files.remove(&relative);
|
||||||
stats.deleted_remote += 1;
|
stats.deleted_remote += 1;
|
||||||
|
file_details.push(SyncFileDetail {
|
||||||
|
path: relative.clone(),
|
||||||
|
action: SyncFileAction::DeleteRemote,
|
||||||
|
size: remote.map(|item| item.size).unwrap_or(0),
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
SyncAction::Skip => {
|
SyncAction::Skip => {
|
||||||
stats.skipped += 1;
|
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(),
|
message: "同步完成".to_string(),
|
||||||
last_sync_at,
|
last_sync_at,
|
||||||
stats,
|
stats,
|
||||||
|
files: file_details,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1291,8 +1346,13 @@ fn load_sync_config(app: tauri::AppHandle) -> Result<SyncConfig, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn save_sync_config(app: tauri::AppHandle, config: SyncConfig) -> Result<(), String> {
|
fn save_sync_config(
|
||||||
save_sync_config_to_disk(&app, &config)
|
app: tauri::AppHandle,
|
||||||
|
config: SyncConfig,
|
||||||
|
) -> Result<SyncConfigSaveResult, String> {
|
||||||
|
save_sync_config_to_disk(&app, &config)?;
|
||||||
|
let config_path = sync_config_path(&app)?.to_string_lossy().into_owned();
|
||||||
|
Ok(SyncConfigSaveResult { config_path })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1430,7 +1490,7 @@ pub fn run() {
|
|||||||
let id = event.id().0.as_str();
|
let id = event.id().0.as_str();
|
||||||
match id {
|
match id {
|
||||||
"quit" => {
|
"quit" => {
|
||||||
std::process::exit(0);
|
let _ = app_handle.emit("menu-quit", ());
|
||||||
}
|
}
|
||||||
"open" => {
|
"open" => {
|
||||||
let handle = app_handle.clone();
|
let handle = app_handle.clone();
|
||||||
|
|||||||
@@ -34,10 +34,28 @@
|
|||||||
"targets": ["nsis"],
|
"targets": ["nsis"],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
|
"icons/64x64.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
]
|
],
|
||||||
|
"resources": {
|
||||||
|
"icons/file-icon.ico": "file-icon.ico"
|
||||||
|
},
|
||||||
|
"fileAssociations": [
|
||||||
|
{
|
||||||
|
"ext": ["md", "markdown", "mdown", "mkdn", "mkd", "mdwn"],
|
||||||
|
"name": "Markdown",
|
||||||
|
"description": "Markdown Document",
|
||||||
|
"role": "Editor",
|
||||||
|
"mimeType": "text/markdown"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"nsis": {
|
||||||
|
"installerHooks": "nsis-hooks.nsh"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
365
src/App.vue
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref, watch, onMounted, onUnmounted, nextTick} from "vue";
|
import {computed, ref, onMounted, onUnmounted, nextTick} from "vue";
|
||||||
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
||||||
import {convertFileSrc, invoke, isTauri} from "@tauri-apps/api/core";
|
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 MarkdownEditor from "./components/MarkdownEditor.vue";
|
||||||
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
||||||
import OutlinePanel from "./components/OutlinePanel.vue";
|
import OutlinePanel from "./components/OutlinePanel.vue";
|
||||||
@@ -17,6 +17,8 @@ type WorkspaceFileType = "markdown" | "image" | "video";
|
|||||||
type SyncProvider = "none" | "webdav" | "s3";
|
type SyncProvider = "none" | "webdav" | "s3";
|
||||||
const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode";
|
const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode";
|
||||||
const APP_THEME_KEY = "yurou:app-theme";
|
const APP_THEME_KEY = "yurou:app-theme";
|
||||||
|
const RENDER_SINGLE_LINE_BREAKS_KEY = "yurou:render-single-line-breaks";
|
||||||
|
const PARAGRAPH_INDENT_KEY = "yurou:paragraph-indent";
|
||||||
const MARKDOWN_EXTENSIONS = new Set(["md"]);
|
const MARKDOWN_EXTENSIONS = new Set(["md"]);
|
||||||
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif"]);
|
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif"]);
|
||||||
const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "ogv"]);
|
const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "ogv"]);
|
||||||
@@ -79,7 +81,10 @@ const mediaPreviewSrc = ref("");
|
|||||||
const mediaPreviewError = ref("");
|
const mediaPreviewError = ref("");
|
||||||
const mediaPreviewKey = ref(0);
|
const mediaPreviewKey = ref(0);
|
||||||
const saveStatus = ref("");
|
const saveStatus = ref("");
|
||||||
|
const saveNotice = ref("");
|
||||||
|
const saveNoticeType = ref<"success" | "error">("success");
|
||||||
const hasDraftDocument = ref(false);
|
const hasDraftDocument = ref(false);
|
||||||
|
let lastSavedContent = "";
|
||||||
|
|
||||||
// Folder state
|
// Folder state
|
||||||
interface DirEntry {
|
interface DirEntry {
|
||||||
@@ -105,6 +110,36 @@ interface WorkspaceWatchErrorPayload {
|
|||||||
message: string;
|
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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadParagraphIndent() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(PARAGRAPH_INDENT_KEY) === "true";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveParagraphIndent(value: boolean) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(PARAGRAPH_INDENT_KEY, String(value));
|
||||||
|
} catch { /* noop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface SyncConfig {
|
interface SyncConfig {
|
||||||
provider: SyncProvider;
|
provider: SyncProvider;
|
||||||
webdav: {
|
webdav: {
|
||||||
@@ -151,7 +186,10 @@ const WORKSPACE_REFRESH_DELAY_MS = 140;
|
|||||||
let workspaceRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
let workspaceRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let workspaceRefreshInFlight = false;
|
let workspaceRefreshInFlight = false;
|
||||||
let workspaceRefreshQueued = false;
|
let workspaceRefreshQueued = false;
|
||||||
let syncInFlight = false;
|
const syncInFlight = ref(false);
|
||||||
|
let fileOpenRequest = 0;
|
||||||
|
let saveNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let closeSaveInFlight = false;
|
||||||
|
|
||||||
// ---- editor zoom (Ctrl+scroll) ----
|
// ---- editor zoom (Ctrl+scroll) ----
|
||||||
const ZOOM_MIN = 0.7;
|
const ZOOM_MIN = 0.7;
|
||||||
@@ -207,7 +245,8 @@ const showOutline = ref(false);
|
|||||||
const defaultDocumentMode = ref<DocumentMode>(loadDefaultDocumentMode());
|
const defaultDocumentMode = ref<DocumentMode>(loadDefaultDocumentMode());
|
||||||
const documentMode = ref<DocumentMode>(defaultDocumentMode.value);
|
const documentMode = ref<DocumentMode>(defaultDocumentMode.value);
|
||||||
const appTheme = ref<AppTheme>(loadAppTheme());
|
const appTheme = ref<AppTheme>(loadAppTheme());
|
||||||
const showMoreMenu = ref(false);
|
const renderSingleLineBreaks = ref(loadRenderSingleLineBreaks());
|
||||||
|
const paragraphIndent = ref(loadParagraphIndent());
|
||||||
const settingsRequest = ref(0);
|
const settingsRequest = ref(0);
|
||||||
const settingsInitialPage = ref<"about" | "editor" | "sync">("about");
|
const settingsInitialPage = ref<"about" | "editor" | "sync">("about");
|
||||||
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined; scrollToLine: (line: number) => void } | null>(null);
|
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined; scrollToLine: (line: number) => void } | null>(null);
|
||||||
@@ -230,6 +269,7 @@ type TitlebarDragState = {
|
|||||||
const TITLEBAR_DRAG_THRESHOLD = 4;
|
const TITLEBAR_DRAG_THRESHOLD = 4;
|
||||||
let titlebarDragState: TitlebarDragState | null = null;
|
let titlebarDragState: TitlebarDragState | null = null;
|
||||||
let unlistenWindowResize: (() => void) | null = null;
|
let unlistenWindowResize: (() => void) | null = null;
|
||||||
|
let unlistenWindowCloseRequested: (() => void) | null = null;
|
||||||
|
|
||||||
function isInteractiveTitlebarTarget(target: EventTarget | null) {
|
function isInteractiveTitlebarTarget(target: EventTarget | null) {
|
||||||
if (!(target instanceof Element)) return false;
|
if (!(target instanceof Element)) return false;
|
||||||
@@ -321,6 +361,26 @@ function openSettings(page: "about" | "editor" | "sync" = "about") {
|
|||||||
settingsRequest.value += 1;
|
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) {
|
function setDocumentMode(mode: DocumentMode) {
|
||||||
documentMode.value = mode;
|
documentMode.value = mode;
|
||||||
}
|
}
|
||||||
@@ -337,23 +397,6 @@ function updateAppTheme(theme: AppTheme) {
|
|||||||
saveAppTheme(theme);
|
saveAppTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExportPdf() {
|
|
||||||
showMoreMenu.value = false;
|
|
||||||
// TODO: implement PDF export
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeMoreMenu() {
|
|
||||||
showMoreMenu.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(showMoreMenu, (val) => {
|
|
||||||
if (val) {
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener("click", closeMoreMenu, {once: true});
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
applyAppTheme(appTheme.value);
|
applyAppTheme(appTheme.value);
|
||||||
applyZoomCssVars();
|
applyZoomCssVars();
|
||||||
@@ -367,6 +410,11 @@ onMounted(() => {
|
|||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("Failed to listen for window resize:", 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();
|
void restoreLastWorkspace();
|
||||||
});
|
});
|
||||||
@@ -378,7 +426,12 @@ onUnmounted(() => {
|
|||||||
unlistenWindowResize();
|
unlistenWindowResize();
|
||||||
unlistenWindowResize = null;
|
unlistenWindowResize = null;
|
||||||
}
|
}
|
||||||
|
if (unlistenWindowCloseRequested) {
|
||||||
|
unlistenWindowCloseRequested();
|
||||||
|
unlistenWindowCloseRequested = null;
|
||||||
|
}
|
||||||
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
|
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
|
||||||
|
if (saveNoticeTimer) clearTimeout(saveNoticeTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
function stripMarkdown(md: string): string {
|
function stripMarkdown(md: string): string {
|
||||||
@@ -422,7 +475,9 @@ function setDocumentContent(md: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetMarkdownEditorState() {
|
function resetMarkdownEditorState() {
|
||||||
|
fileOpenRequest += 1;
|
||||||
setDocumentContent("");
|
setDocumentContent("");
|
||||||
|
lastSavedContent = "";
|
||||||
currentFilePath.value = null;
|
currentFilePath.value = null;
|
||||||
currentFileType.value = "markdown";
|
currentFileType.value = "markdown";
|
||||||
resetMediaPreviewState();
|
resetMediaPreviewState();
|
||||||
@@ -430,7 +485,6 @@ function resetMarkdownEditorState() {
|
|||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
showOutline.value = false;
|
showOutline.value = false;
|
||||||
documentMode.value = defaultDocumentMode.value;
|
documentMode.value = defaultDocumentMode.value;
|
||||||
showMoreMenu.value = false;
|
|
||||||
editorResetKey.value += 1;
|
editorResetKey.value += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,8 +655,19 @@ async function saveCurrentWorkspaceDocumentBeforeSwitch() {
|
|||||||
|
|
||||||
const contentToSave = getLatestEditorContent();
|
const contentToSave = getLatestEditorContent();
|
||||||
if (!currentFilePath.value && !contentToSave.trim()) return true;
|
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 updateParagraphIndent(value: boolean) {
|
||||||
|
paragraphIndent.value = value;
|
||||||
|
saveParagraphIndent(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWorkspaceAndEditorState(options: { stopWatcher?: boolean } = {}) {
|
function resetWorkspaceAndEditorState(options: { stopWatcher?: boolean } = {}) {
|
||||||
@@ -696,6 +761,7 @@ async function refreshCurrentOpenFileFromDisk() {
|
|||||||
if (isMarkdownDocument.value) {
|
if (isMarkdownDocument.value) {
|
||||||
const fileContent = await invoke<string>("read_file", { path: currentFilePath.value });
|
const fileContent = await invoke<string>("read_file", { path: currentFilePath.value });
|
||||||
setDocumentContent(fileContent);
|
setDocumentContent(fileContent);
|
||||||
|
lastSavedContent = fileContent;
|
||||||
hasDraftDocument.value = false;
|
hasDraftDocument.value = false;
|
||||||
editorResetKey.value += 1;
|
editorResetKey.value += 1;
|
||||||
return;
|
return;
|
||||||
@@ -713,36 +779,41 @@ async function refreshCurrentOpenFileFromDisk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runConfiguredSync(trigger: "startup" | "save" | "manual") {
|
async function runConfiguredSync(trigger: "startup" | "save" | "manual") {
|
||||||
if (!folderPath.value || syncInFlight) return false;
|
if (!folderPath.value || syncInFlight.value) return false;
|
||||||
|
|
||||||
const config = await loadSyncConfig();
|
const config = await loadSyncConfig();
|
||||||
if (trigger !== "manual" && !shouldRunSync(config, trigger)) return false;
|
if (trigger !== "manual" && !shouldRunSync(config, trigger)) return false;
|
||||||
if (!config || config.provider === "none") return false;
|
if (!config || config.provider === "none") {
|
||||||
|
if (trigger === "manual") {
|
||||||
|
showSaveNotice("请先在设置中配置同步方式", "error");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (trigger !== "save") {
|
if (trigger !== "save") {
|
||||||
const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
|
const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
|
||||||
if (!saved) return false;
|
if (!saved) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
syncInFlight = true;
|
syncInFlight.value = true;
|
||||||
|
showSaveNotice("正在同步...", "success");
|
||||||
try {
|
try {
|
||||||
const result = await invoke<SyncResult>("sync_now", {
|
const result = await invoke<SyncResult>("sync_now", {
|
||||||
workspace: folderPath.value,
|
workspace: folderPath.value,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
saveStatus.value = describeSyncResult(result);
|
saveStatus.value = describeSyncResult(result);
|
||||||
setTimeout(() => {
|
showSaveNotice(describeSyncResult(result), "success");
|
||||||
saveStatus.value = "";
|
|
||||||
}, 3600);
|
|
||||||
await refreshWorkspaceTree();
|
await refreshWorkspaceTree();
|
||||||
await refreshCurrentOpenFileFromDisk();
|
await refreshCurrentOpenFileFromDisk();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Sync failed:", e);
|
console.error("Sync failed:", e);
|
||||||
saveStatus.value = `同步失败: ${e}`;
|
saveStatus.value = `同步失败: ${e}`;
|
||||||
|
showSaveNotice(`同步失败: ${e}`, "error");
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
syncInFlight = false;
|
syncInFlight.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,10 +897,10 @@ function openMediaPreview(path: string, fileType: Extract<WorkspaceFileType, "im
|
|||||||
mediaPreviewError.value = "";
|
mediaPreviewError.value = "";
|
||||||
mediaPreviewKey.value += 1;
|
mediaPreviewKey.value += 1;
|
||||||
setDocumentContent("");
|
setDocumentContent("");
|
||||||
|
lastSavedContent = "";
|
||||||
hasDraftDocument.value = false;
|
hasDraftDocument.value = false;
|
||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
showOutline.value = false;
|
showOutline.value = false;
|
||||||
showMoreMenu.value = false;
|
|
||||||
editorResetKey.value += 1;
|
editorResetKey.value += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,6 +1055,15 @@ async function onRenameEntry(path: string, newName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onOpenFile(path: string) {
|
async function onOpenFile(path: string) {
|
||||||
|
const request = ++fileOpenRequest;
|
||||||
|
|
||||||
|
if (currentFilePath.value && getWorkspaceIdentity(currentFilePath.value) === getWorkspaceIdentity(path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = await saveCurrentWorkspaceDocumentBeforeSwitch();
|
||||||
|
if (!saved || request !== fileOpenRequest) return;
|
||||||
|
|
||||||
const fileType = getWorkspaceFileType(path);
|
const fileType = getWorkspaceFileType(path);
|
||||||
if (fileType === "image" || fileType === "video") {
|
if (fileType === "image" || fileType === "video") {
|
||||||
openMediaPreview(path, fileType);
|
openMediaPreview(path, fileType);
|
||||||
@@ -992,9 +1072,11 @@ async function onOpenFile(path: string) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const fileContent = await invoke<string>("read_file", {path});
|
const fileContent = await invoke<string>("read_file", {path});
|
||||||
|
if (request !== fileOpenRequest) return;
|
||||||
currentFileType.value = "markdown";
|
currentFileType.value = "markdown";
|
||||||
resetMediaPreviewState();
|
resetMediaPreviewState();
|
||||||
setDocumentContent(fileContent);
|
setDocumentContent(fileContent);
|
||||||
|
lastSavedContent = fileContent;
|
||||||
currentFilePath.value = path;
|
currentFilePath.value = path;
|
||||||
hasDraftDocument.value = false;
|
hasDraftDocument.value = false;
|
||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
@@ -1009,7 +1091,29 @@ function getLatestEditorContent(contentOverride?: string) {
|
|||||||
return markdownEditorRef.value?.flushPendingChanges() ?? content.value;
|
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;
|
if (!isMarkdownDocument.value) return false;
|
||||||
|
|
||||||
const contentToSave = getLatestEditorContent(contentOverride);
|
const contentToSave = getLatestEditorContent(contentOverride);
|
||||||
@@ -1018,17 +1122,25 @@ async function doSave(contentOverride?: string, options: { syncAfterSave?: boole
|
|||||||
// No file path yet — prompt user to pick a save location
|
// No file path yet — prompt user to pick a save location
|
||||||
try {
|
try {
|
||||||
const chosen = await invoke<string | null>("pick_save_file");
|
const chosen = await invoke<string | null>("pick_save_file");
|
||||||
if (!chosen) return false; // user cancelled
|
if (!chosen) {
|
||||||
|
showSaveNotice(options.trigger === "automatic" ? "已取消自动保存" : "已取消保存", "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
currentFilePath.value = chosen;
|
currentFilePath.value = chosen;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Save dialog failed:", e);
|
console.error("Save dialog failed:", e);
|
||||||
|
showSaveNotice(`保存失败: ${e}`, "error");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
|
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(() => {
|
setTimeout(() => {
|
||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
}, 3000);
|
}, 3000);
|
||||||
@@ -1040,6 +1152,7 @@ async function doSave(contentOverride?: string, options: { syncAfterSave?: boole
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Save failed:", e);
|
console.error("Save failed:", e);
|
||||||
saveStatus.value = `保存失败: ${e}`;
|
saveStatus.value = `保存失败: ${e}`;
|
||||||
|
showSaveNotice(saveStatus.value, "error");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1051,10 +1164,15 @@ async function doSaveAs() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const chosen = await invoke<string | null>("pick_save_file");
|
const chosen = await invoke<string | null>("pick_save_file");
|
||||||
if (!chosen) return; // user cancelled
|
if (!chosen) {
|
||||||
|
showSaveNotice("已取消另存为", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentFilePath.value = chosen;
|
currentFilePath.value = chosen;
|
||||||
await invoke("write_file", {path: currentFilePath.value, content: contentToSave});
|
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(() => {
|
setTimeout(() => {
|
||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
}, 3000);
|
}, 3000);
|
||||||
@@ -1063,13 +1181,19 @@ async function doSaveAs() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Save failed:", e);
|
console.error("Save failed:", e);
|
||||||
saveStatus.value = `保存失败: ${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";
|
currentFileType.value = "markdown";
|
||||||
resetMediaPreviewState();
|
resetMediaPreviewState();
|
||||||
setDocumentContent("");
|
setDocumentContent("");
|
||||||
|
lastSavedContent = "";
|
||||||
currentFilePath.value = null;
|
currentFilePath.value = null;
|
||||||
hasDraftDocument.value = true;
|
hasDraftDocument.value = true;
|
||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
@@ -1112,7 +1236,11 @@ registerTauriListener("menu-save-as", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerTauriListener("menu-new", () => {
|
registerTauriListener("menu-new", () => {
|
||||||
doNew();
|
void doNew();
|
||||||
|
});
|
||||||
|
|
||||||
|
registerTauriListener("menu-quit", () => {
|
||||||
|
void closeWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
registerTauriListener("show-about", () => {
|
registerTauriListener("show-about", () => {
|
||||||
@@ -1147,6 +1275,8 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
:settings-initial-page="settingsInitialPage"
|
:settings-initial-page="settingsInitialPage"
|
||||||
:default-document-mode="defaultDocumentMode"
|
:default-document-mode="defaultDocumentMode"
|
||||||
:app-theme="appTheme"
|
:app-theme="appTheme"
|
||||||
|
:render-single-line-breaks="renderSingleLineBreaks"
|
||||||
|
:paragraph-indent="paragraphIndent"
|
||||||
:recent-workspaces="recentWorkspaces"
|
:recent-workspaces="recentWorkspaces"
|
||||||
@open-folder="onOpenFolder"
|
@open-folder="onOpenFolder"
|
||||||
@switch-workspace="(path: string) => switchWorkspace(path)"
|
@switch-workspace="(path: string) => switchWorkspace(path)"
|
||||||
@@ -1160,6 +1290,8 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
@search-result-click="handleSearchResultClick"
|
@search-result-click="handleSearchResultClick"
|
||||||
@update-default-document-mode="updateDefaultDocumentMode"
|
@update-default-document-mode="updateDefaultDocumentMode"
|
||||||
@update-app-theme="updateAppTheme"
|
@update-app-theme="updateAppTheme"
|
||||||
|
@update-render-single-line-breaks="updateRenderSingleLineBreaks"
|
||||||
|
@update-paragraph-indent="updateParagraphIndent"
|
||||||
@prepare-workspace-sync="prepareWorkspaceSync"
|
@prepare-workspace-sync="prepareWorkspaceSync"
|
||||||
@workspace-synced="handleWorkspaceSynced"
|
@workspace-synced="handleWorkspaceSynced"
|
||||||
/>
|
/>
|
||||||
@@ -1188,8 +1320,8 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
<i class="ri-menu-line text-base"></i>
|
<i class="ri-menu-line text-base"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="titlebar-brand app-muted-text shrink-0 pointer-events-none">
|
<div v-else class="titlebar-brand shrink-0 pointer-events-none">
|
||||||
yurou
|
<img src="/yurou-logo.png" alt="yurou" class="titlebar-logo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Center: Filename -->
|
<!-- Center: Filename -->
|
||||||
@@ -1233,34 +1365,16 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- More menu -->
|
<!-- Sync button -->
|
||||||
<div v-if="isMarkdownDocument" class="relative">
|
<button
|
||||||
<button
|
v-if="folderPath"
|
||||||
class="toolbar-btn"
|
class="toolbar-btn"
|
||||||
title="更多"
|
:title="syncInFlight ? '正在同步...' : '同步工作目录'"
|
||||||
@click="showMoreMenu = !showMoreMenu"
|
:disabled="syncInFlight"
|
||||||
>
|
@click="runConfiguredSync('manual')"
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
>
|
||||||
<circle cx="3" cy="8" r="1.5"/>
|
<i :class="syncInFlight ? 'ri-loader-4-line sync-spinning' : 'ri-loop-left-line'"></i>
|
||||||
<circle cx="8" cy="8" r="1.5"/>
|
</button>
|
||||||
<circle cx="13" cy="8" r="1.5"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="showMoreMenu"
|
|
||||||
class="dropdown-menu"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<button class="dropdown-item" @click="handleExportPdf">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
||||||
<path d="M5 1h6l4 4v9a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1z"/>
|
|
||||||
<path d="M5 1v4h4"/>
|
|
||||||
<path d="M5 9h6M5 12h4"/>
|
|
||||||
</svg>
|
|
||||||
导出 PDF
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Window controls -->
|
<!-- Window controls -->
|
||||||
@@ -1307,6 +1421,8 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
:model-value="content"
|
:model-value="content"
|
||||||
:document-mode="documentMode"
|
:document-mode="documentMode"
|
||||||
:file-path="currentFilePath"
|
:file-path="currentFilePath"
|
||||||
|
:render-single-line-breaks="renderSingleLineBreaks"
|
||||||
|
:paragraph-indent="paragraphIndent"
|
||||||
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等"
|
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等"
|
||||||
@update:model-value="onUpdate"
|
@update:model-value="onUpdate"
|
||||||
@save-shortcut="doSave"
|
@save-shortcut="doSave"
|
||||||
@@ -1379,6 +1495,18 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
{{ Math.round(editorZoom * 100) }}%
|
{{ Math.round(editorZoom * 100) }}%
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
<Transition name="save-notice">
|
||||||
|
<div
|
||||||
|
v-if="saveNotice"
|
||||||
|
class="save-notice fixed top-12 right-4 z-50 pointer-events-none select-none"
|
||||||
|
:class="{ 'save-notice-error': saveNoticeType === 'error' }"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<i :class="saveNoticeType === 'error' ? 'ri-error-warning-line' : 'ri-checkbox-circle-line'"></i>
|
||||||
|
<span>{{ saveNotice }}</span>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<!-- <footer class="app-footer backdrop-blur-sm">-->
|
<!-- <footer class="app-footer backdrop-blur-sm">-->
|
||||||
@@ -1416,6 +1544,17 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
opacity: 0;
|
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-enter-active,
|
||||||
.outline-slide-leave-active {
|
.outline-slide-leave-active {
|
||||||
transition:
|
transition:
|
||||||
@@ -1465,6 +1604,48 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
color: var(--app-control-contrast);
|
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 ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
.media-preview {
|
.media-preview {
|
||||||
@@ -1550,6 +1731,11 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titlebar-logo {
|
||||||
|
height: 26px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.titlebar-file-title {
|
.titlebar-file-title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
@@ -1658,38 +1844,6 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
box-shadow: 0 1px 3px var(--app-shadow);
|
box-shadow: 0 1px 3px var(--app-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 100%;
|
|
||||||
margin-top: 4px;
|
|
||||||
background: var(--app-surface);
|
|
||||||
border: 1px solid var(--app-border-strong);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
||||||
min-width: 140px;
|
|
||||||
padding: 4px;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--app-text);
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:hover {
|
|
||||||
background-color: var(--app-surface-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-actions {
|
.start-actions {
|
||||||
background:
|
background:
|
||||||
@@ -1720,4 +1874,15 @@ async function handleSearchResultClick(filePath: string, lineNumber: number) {
|
|||||||
text-decoration-color: currentColor;
|
text-decoration-color: currentColor;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sync button spin animation */
|
||||||
|
@keyframes sync-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-spinning {
|
||||||
|
display: inline-block;
|
||||||
|
animation: sync-spin 1s linear infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
BIN
src/assets/yurou-logo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/yurou-sys.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
@@ -21,6 +21,11 @@ interface WorkspaceEntry {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TreeExpansionCommand {
|
||||||
|
id: number;
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -31,6 +36,8 @@ const props = withDefaults(
|
|||||||
settingsInitialPage?: SettingsPage;
|
settingsInitialPage?: SettingsPage;
|
||||||
defaultDocumentMode?: DocumentMode;
|
defaultDocumentMode?: DocumentMode;
|
||||||
appTheme?: AppTheme;
|
appTheme?: AppTheme;
|
||||||
|
renderSingleLineBreaks?: boolean;
|
||||||
|
paragraphIndent?: boolean;
|
||||||
recentWorkspaces?: WorkspaceEntry[];
|
recentWorkspaces?: WorkspaceEntry[];
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -39,6 +46,8 @@ const props = withDefaults(
|
|||||||
settingsInitialPage: "about",
|
settingsInitialPage: "about",
|
||||||
defaultDocumentMode: "edit",
|
defaultDocumentMode: "edit",
|
||||||
appTheme: "warm",
|
appTheme: "warm",
|
||||||
|
renderSingleLineBreaks: false,
|
||||||
|
paragraphIndent: false,
|
||||||
recentWorkspaces: () => [],
|
recentWorkspaces: () => [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -55,6 +64,8 @@ const emit = defineEmits<{
|
|||||||
renameEntry: [path: string, newName: string];
|
renameEntry: [path: string, newName: string];
|
||||||
updateDefaultDocumentMode: [mode: DocumentMode];
|
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||||
updateAppTheme: [theme: AppTheme];
|
updateAppTheme: [theme: AppTheme];
|
||||||
|
updateRenderSingleLineBreaks: [value: boolean];
|
||||||
|
updateParagraphIndent: [value: boolean];
|
||||||
prepareWorkspaceSync: [done: (saved: boolean) => void];
|
prepareWorkspaceSync: [done: (saved: boolean) => void];
|
||||||
workspaceSynced: [];
|
workspaceSynced: [];
|
||||||
"search-result-click": [filePath: string, lineNumber: number];
|
"search-result-click": [filePath: string, lineNumber: number];
|
||||||
@@ -455,6 +466,7 @@ watch(showWorkspaceMenu, (open) => {
|
|||||||
|
|
||||||
watch(() => props.folderPath, () => {
|
watch(() => props.folderPath, () => {
|
||||||
closeWorkspaceMenu();
|
closeWorkspaceMenu();
|
||||||
|
treeExpansionCommand.value = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -532,6 +544,16 @@ function handleCreateRootFile() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const treeExpansionCommand = ref<TreeExpansionCommand | null>(null);
|
||||||
|
let treeExpansionCommandId = 0;
|
||||||
|
|
||||||
|
function setAllFoldersExpanded(expanded: boolean) {
|
||||||
|
treeExpansionCommand.value = {
|
||||||
|
id: ++treeExpansionCommandId,
|
||||||
|
expanded,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -597,6 +619,25 @@ function handleCreateRootFile() {
|
|||||||
>
|
>
|
||||||
<i class="ri-file-add-line"></i>
|
<i class="ri-file-add-line"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<span class="tree-toolbar-separator" aria-hidden="true"></span>
|
||||||
|
<button
|
||||||
|
class="tree-toolbar-btn"
|
||||||
|
type="button"
|
||||||
|
title="全部展开"
|
||||||
|
aria-label="全部展开工作目录文件夹"
|
||||||
|
@click="setAllFoldersExpanded(true)"
|
||||||
|
>
|
||||||
|
<i class="ri-expand-vertical-line"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tree-toolbar-btn"
|
||||||
|
type="button"
|
||||||
|
title="全部折叠"
|
||||||
|
aria-label="全部折叠工作目录文件夹"
|
||||||
|
@click="setAllFoldersExpanded(false)"
|
||||||
|
>
|
||||||
|
<i class="ri-collapse-vertical-line"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- File tree -->
|
<!-- File tree -->
|
||||||
<div
|
<div
|
||||||
@@ -609,6 +650,7 @@ function handleCreateRootFile() {
|
|||||||
:entry="entry"
|
:entry="entry"
|
||||||
:depth="0"
|
:depth="0"
|
||||||
:currentFilePath="currentFilePath"
|
:currentFilePath="currentFilePath"
|
||||||
|
:expansion-command="treeExpansionCommand"
|
||||||
@toggle-folder="(path: string) => emit('toggleFolder', path)"
|
@toggle-folder="(path: string) => emit('toggleFolder', path)"
|
||||||
@open-file="(path: string) => emit('openFile', path)"
|
@open-file="(path: string) => emit('openFile', path)"
|
||||||
@create-folder="(path: string) => emit('createFolder', path)"
|
@create-folder="(path: string) => emit('createFolder', path)"
|
||||||
@@ -842,8 +884,12 @@ function handleCreateRootFile() {
|
|||||||
:workspace-path="folderPath"
|
:workspace-path="folderPath"
|
||||||
:default-document-mode="defaultDocumentMode"
|
:default-document-mode="defaultDocumentMode"
|
||||||
:app-theme="appTheme"
|
:app-theme="appTheme"
|
||||||
|
:render-single-line-breaks="renderSingleLineBreaks"
|
||||||
|
:paragraph-indent="paragraphIndent"
|
||||||
@update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
|
@update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
|
||||||
@update-app-theme="(theme: AppTheme) => emit('updateAppTheme', theme)"
|
@update-app-theme="(theme: AppTheme) => emit('updateAppTheme', theme)"
|
||||||
|
@update-render-single-line-breaks="(value: boolean) => emit('updateRenderSingleLineBreaks', value)"
|
||||||
|
@update-paragraph-indent="(value: boolean) => emit('updateParagraphIndent', value)"
|
||||||
@prepare-workspace-sync="forwardPrepareWorkspaceSync"
|
@prepare-workspace-sync="forwardPrepareWorkspaceSync"
|
||||||
@workspace-synced="emit('workspaceSynced')"
|
@workspace-synced="emit('workspaceSynced')"
|
||||||
/>
|
/>
|
||||||
@@ -1090,6 +1136,13 @@ function handleCreateRootFile() {
|
|||||||
color: var(--app-text);
|
color: var(--app-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-toolbar-separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0 2px;
|
||||||
|
background: var(--app-border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-search-input {
|
.sidebar-search-input {
|
||||||
border: 1px solid var(--app-border-strong);
|
border: 1px solid var(--app-border-strong);
|
||||||
background: var(--app-hover);
|
background: var(--app-hover);
|
||||||
|
|||||||
496
src/components/MarkdownCodeEditor.vue
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
|
import {
|
||||||
|
acceptCompletion,
|
||||||
|
autocompletion,
|
||||||
|
closeBrackets,
|
||||||
|
closeBracketsKeymap,
|
||||||
|
completionKeymap,
|
||||||
|
pickedCompletion,
|
||||||
|
startCompletion,
|
||||||
|
type CompletionContext,
|
||||||
|
type CompletionResult,
|
||||||
|
} from "@codemirror/autocomplete";
|
||||||
|
import {
|
||||||
|
defaultKeymap,
|
||||||
|
history,
|
||||||
|
historyKeymap,
|
||||||
|
indentWithTab,
|
||||||
|
} from "@codemirror/commands";
|
||||||
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
|
import { languages } from "@codemirror/language-data";
|
||||||
|
import {
|
||||||
|
bracketMatching,
|
||||||
|
defaultHighlightStyle,
|
||||||
|
foldGutter,
|
||||||
|
HighlightStyle,
|
||||||
|
indentOnInput,
|
||||||
|
syntaxHighlighting,
|
||||||
|
} from "@codemirror/language";
|
||||||
|
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
||||||
|
import {
|
||||||
|
EditorSelection,
|
||||||
|
EditorState,
|
||||||
|
StateEffect,
|
||||||
|
StateField,
|
||||||
|
type Text,
|
||||||
|
} from "@codemirror/state";
|
||||||
|
import {
|
||||||
|
crosshairCursor,
|
||||||
|
Decoration,
|
||||||
|
type DecorationSet,
|
||||||
|
drawSelection,
|
||||||
|
dropCursor,
|
||||||
|
EditorView,
|
||||||
|
highlightActiveLine,
|
||||||
|
highlightActiveLineGutter,
|
||||||
|
highlightSpecialChars,
|
||||||
|
keymap,
|
||||||
|
placeholder as placeholderExtension,
|
||||||
|
rectangularSelection,
|
||||||
|
} from "@codemirror/view";
|
||||||
|
import { tags } from "@lezer/highlight";
|
||||||
|
import { highlightCodeToTokens } from "@/lib/codeHighlight";
|
||||||
|
import { getMarkdownLanguageCompletions } from "@/lib/markdownLanguages";
|
||||||
|
import { resolveMarkdownCodeLanguage } from "@/lib/markdownLanguages";
|
||||||
|
|
||||||
|
interface PastePayload {
|
||||||
|
event: ClipboardEvent;
|
||||||
|
value: string;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: string;
|
||||||
|
placeholder?: string;
|
||||||
|
autofocus?: boolean;
|
||||||
|
minHeight?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
placeholder: "",
|
||||||
|
autofocus: false,
|
||||||
|
minHeight: "1.625em",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: string];
|
||||||
|
blur: [];
|
||||||
|
escape: [];
|
||||||
|
paste: [payload: PastePayload];
|
||||||
|
"backspace-at-start": [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const hostRef = ref<HTMLDivElement>();
|
||||||
|
let editorView: EditorView | null = null;
|
||||||
|
let highlightRequestId = 0;
|
||||||
|
|
||||||
|
const setCodeDecorations = StateEffect.define<DecorationSet>();
|
||||||
|
const codeDecorationField = StateField.define<DecorationSet>({
|
||||||
|
create: () => Decoration.none,
|
||||||
|
update(value, transaction) {
|
||||||
|
let next = value.map(transaction.changes);
|
||||||
|
for (const effect of transaction.effects) {
|
||||||
|
if (effect.is(setCodeDecorations)) next = effect.value;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
},
|
||||||
|
provide: (field) => EditorView.decorations.from(field),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface FencedCodeBlock {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
code: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFencedCodeBlocks(doc: Text): FencedCodeBlock[] {
|
||||||
|
const blocks: FencedCodeBlock[] = [];
|
||||||
|
|
||||||
|
for (let lineNumber = 1; lineNumber <= doc.lines; lineNumber += 1) {
|
||||||
|
const line = doc.line(lineNumber);
|
||||||
|
const opener = /^(\s*)(`{3,}|~{3,})\s*([\w.+#-]*)/.exec(line.text);
|
||||||
|
if (!opener) continue;
|
||||||
|
|
||||||
|
const fenceChar = opener[2][0];
|
||||||
|
const fenceLength = opener[2].length;
|
||||||
|
const closingPattern = new RegExp(`^\\s*${fenceChar === "`" ? "`" : "~"}{${fenceLength},}\\s*$`);
|
||||||
|
const contentFrom = Math.min(doc.length, line.to + 1);
|
||||||
|
|
||||||
|
for (let closingLineNumber = lineNumber + 1; closingLineNumber <= doc.lines; closingLineNumber += 1) {
|
||||||
|
const closingLine = doc.line(closingLineNumber);
|
||||||
|
if (!closingPattern.test(closingLine.text)) continue;
|
||||||
|
|
||||||
|
const requestedLanguage = opener[3] || "text";
|
||||||
|
blocks.push({
|
||||||
|
from: contentFrom,
|
||||||
|
to: closingLine.from,
|
||||||
|
code: doc.sliceString(contentFrom, closingLine.from),
|
||||||
|
language: resolveMarkdownCodeLanguage(requestedLanguage)?.shiki || "text",
|
||||||
|
});
|
||||||
|
lineNumber = closingLineNumber;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCodeDecorations(view: EditorView) {
|
||||||
|
const requestId = ++highlightRequestId;
|
||||||
|
const documentText = view.state.doc.toString();
|
||||||
|
const blocks = findFencedCodeBlocks(view.state.doc);
|
||||||
|
const ranges: ReturnType<Decoration["range"]>[] = [];
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const tokenLines = await highlightCodeToTokens(block.code, block.language);
|
||||||
|
if (!tokenLines || requestId !== highlightRequestId) return;
|
||||||
|
|
||||||
|
for (const tokens of tokenLines) {
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (!token.content || !token.color) continue;
|
||||||
|
const from = block.from + token.offset;
|
||||||
|
const to = Math.min(block.to, from + token.content.length);
|
||||||
|
if (to <= from) continue;
|
||||||
|
ranges.push(
|
||||||
|
Decoration.mark({
|
||||||
|
class: "cm-shiki-token",
|
||||||
|
attributes: { style: `color: ${token.color}` },
|
||||||
|
}).range(from, to),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestId !== highlightRequestId ||
|
||||||
|
!editorView ||
|
||||||
|
editorView !== view ||
|
||||||
|
view.state.doc.toString() !== documentText
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.dispatch({ effects: setCodeDecorations.of(Decoration.set(ranges, true)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fenceLanguageCompletion(context: CompletionContext): CompletionResult | null {
|
||||||
|
const line = context.state.doc.lineAt(context.pos);
|
||||||
|
const beforeCursor = context.state.sliceDoc(line.from, context.pos);
|
||||||
|
const match = /^(\s*)(`{3,}|~{3,})([\w.+#-]*)$/.exec(beforeCursor);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: line.from + match[1].length + match[2].length,
|
||||||
|
options: getMarkdownLanguageCompletions(),
|
||||||
|
validFor: /^[\w.+#-]*$/,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertClosedCodeFence(view: EditorView) {
|
||||||
|
const selection = view.state.selection.main;
|
||||||
|
if (!selection.empty) return false;
|
||||||
|
|
||||||
|
const line = view.state.doc.lineAt(selection.head);
|
||||||
|
const beforeCursor = view.state.sliceDoc(line.from, selection.head);
|
||||||
|
const afterCursor = view.state.sliceDoc(selection.head, line.to);
|
||||||
|
const match = /^(\s*)(`{3,}|~{3,})[\w.+#-]*\s*$/.exec(beforeCursor);
|
||||||
|
if (!match || afterCursor.trim()) return false;
|
||||||
|
|
||||||
|
const nextLine = line.number < view.state.doc.lines
|
||||||
|
? view.state.doc.line(line.number + 1).text
|
||||||
|
: "";
|
||||||
|
const fence = match[2][0].repeat(match[2].length);
|
||||||
|
if (nextLine.trimStart().startsWith(fence)) return false;
|
||||||
|
|
||||||
|
const insert = `\n${match[1]}\n${match[1]}${fence}`;
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: selection.head, insert },
|
||||||
|
selection: { anchor: selection.head + 1 + match[1].length },
|
||||||
|
scrollIntoView: true,
|
||||||
|
userEvent: "input",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function acceptCompletionOrCloseFence(view: EditorView) {
|
||||||
|
return acceptCompletion(view) || insertClosedCodeFence(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: tags.heading, color: "var(--app-text-strong)", fontWeight: "700" },
|
||||||
|
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||||
|
{ tag: tags.strong, fontWeight: "700" },
|
||||||
|
{ tag: tags.link, color: "var(--code-token-link)", textDecoration: "underline" },
|
||||||
|
{ tag: tags.url, color: "var(--code-token-link)" },
|
||||||
|
{
|
||||||
|
tag: tags.monospace,
|
||||||
|
color: "var(--code-token-string)",
|
||||||
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||||
|
},
|
||||||
|
{ tag: [tags.keyword, tags.operatorKeyword], color: "var(--code-token-keyword)" },
|
||||||
|
{ tag: [tags.string, tags.regexp], color: "var(--code-token-string-expression)" },
|
||||||
|
{ tag: [tags.number, tags.bool, tags.null, tags.atom], color: "var(--code-token-constant)" },
|
||||||
|
{ tag: [tags.comment, tags.meta], color: "var(--code-token-comment)" },
|
||||||
|
{
|
||||||
|
tag: [tags.function(tags.variableName), tags.labelName],
|
||||||
|
color: "var(--code-token-function)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: [tags.typeName, tags.className, tags.namespace, tags.tagName],
|
||||||
|
color: "var(--code-token-function)",
|
||||||
|
},
|
||||||
|
{ tag: tags.propertyName, color: "var(--code-token-constant)" },
|
||||||
|
{ tag: tags.attributeName, color: "var(--code-token-parameter)" },
|
||||||
|
{ tag: [tags.punctuation, tags.separator, tags.bracket], color: "var(--code-token-punctuation)" },
|
||||||
|
{ tag: tags.inserted, color: "var(--code-token-inserted)" },
|
||||||
|
{ tag: tags.deleted, color: "var(--code-token-deleted)" },
|
||||||
|
{ tag: tags.changed, color: "var(--code-token-changed)" },
|
||||||
|
{ tag: tags.invalid, color: "var(--code-token-deleted)" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const editorTheme = EditorView.theme({
|
||||||
|
"&": {
|
||||||
|
color: "var(--code-foreground)",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
fontSize: "inherit",
|
||||||
|
minHeight: "var(--markdown-code-editor-min-height)",
|
||||||
|
},
|
||||||
|
"&.cm-focused": { outline: "none" },
|
||||||
|
".cm-scroller": {
|
||||||
|
fontFamily: "inherit",
|
||||||
|
lineHeight: "1.625",
|
||||||
|
overflow: "visible",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
padding: "0",
|
||||||
|
minHeight: "var(--markdown-code-editor-min-height)",
|
||||||
|
caretColor: "var(--app-accent)",
|
||||||
|
},
|
||||||
|
".cm-line": { padding: "0" },
|
||||||
|
".cm-shiki-token > span": { color: "inherit !important" },
|
||||||
|
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--app-accent)" },
|
||||||
|
".cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection": {
|
||||||
|
backgroundColor: "var(--app-selection) !important",
|
||||||
|
},
|
||||||
|
".cm-placeholder": { color: "var(--app-subtle)" },
|
||||||
|
".cm-activeLine": { backgroundColor: "transparent" },
|
||||||
|
".cm-gutters": { display: "none" },
|
||||||
|
".cm-tooltip": {
|
||||||
|
border: "1px solid var(--app-border-strong)",
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: "var(--app-surface)",
|
||||||
|
color: "var(--app-text)",
|
||||||
|
boxShadow: "0 10px 28px var(--app-shadow)",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
".cm-tooltip-autocomplete > ul": {
|
||||||
|
fontFamily: '"Geist", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||||
|
maxHeight: "240px",
|
||||||
|
},
|
||||||
|
".cm-tooltip-autocomplete > ul > li": {
|
||||||
|
minWidth: "220px",
|
||||||
|
padding: "6px 10px",
|
||||||
|
},
|
||||||
|
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
|
||||||
|
backgroundColor: "var(--app-active-bg)",
|
||||||
|
color: "var(--app-text-strong)",
|
||||||
|
},
|
||||||
|
".cm-completionLabel": { fontWeight: "600" },
|
||||||
|
".cm-completionDetail": { color: "var(--app-muted)", fontStyle: "normal" },
|
||||||
|
});
|
||||||
|
|
||||||
|
function createEditor() {
|
||||||
|
const host = hostRef.value;
|
||||||
|
if (!host) return;
|
||||||
|
|
||||||
|
editorView = new EditorView({
|
||||||
|
parent: host,
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: props.modelValue,
|
||||||
|
extensions: [
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
foldGutter(),
|
||||||
|
drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
|
EditorState.allowMultipleSelections.of(true),
|
||||||
|
indentOnInput(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
bracketMatching(),
|
||||||
|
rectangularSelection(),
|
||||||
|
crosshairCursor(),
|
||||||
|
highlightActiveLine(),
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
highlightSelectionMatches(),
|
||||||
|
markdown({ codeLanguages: languages }),
|
||||||
|
syntaxHighlighting(editorHighlightStyle),
|
||||||
|
codeDecorationField,
|
||||||
|
closeBrackets(),
|
||||||
|
autocompletion({ override: [fenceLanguageCompletion], activateOnTyping: true }),
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
editorTheme,
|
||||||
|
placeholderExtension(props.placeholder),
|
||||||
|
keymap.of([
|
||||||
|
{ key: "Enter", run: acceptCompletionOrCloseFence },
|
||||||
|
...completionKeymap,
|
||||||
|
{
|
||||||
|
key: "Escape",
|
||||||
|
run() {
|
||||||
|
emit("escape");
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
indentWithTab,
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
{
|
||||||
|
key: "Backspace",
|
||||||
|
run(view) {
|
||||||
|
const selection = view.state.selection.main;
|
||||||
|
if (selection.empty && selection.head === 0) {
|
||||||
|
emit("backspace-at-start");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...defaultKeymap,
|
||||||
|
...searchKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
]),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
emit("update:modelValue", update.state.doc.toString());
|
||||||
|
void updateCodeDecorations(update.view);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!update.docChanged ||
|
||||||
|
update.transactions.some((transaction) => transaction.annotation(pickedCompletion))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = update.state.selection.main;
|
||||||
|
if (!selection.empty) return;
|
||||||
|
const line = update.state.doc.lineAt(selection.head);
|
||||||
|
const beforeCursor = update.state.sliceDoc(line.from, selection.head);
|
||||||
|
if (/^\s*(`{3,}|~{3,})[\w.+#-]*$/.test(beforeCursor)) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (editorView) startCompletion(editorView);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
EditorView.domEventHandlers({
|
||||||
|
blur() {
|
||||||
|
emit("blur");
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
paste(event, view) {
|
||||||
|
const selection = view.state.selection.main;
|
||||||
|
emit("paste", {
|
||||||
|
event,
|
||||||
|
value: view.state.doc.toString(),
|
||||||
|
from: selection.from,
|
||||||
|
to: selection.to,
|
||||||
|
});
|
||||||
|
return event.defaultPrevented;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.autofocus) {
|
||||||
|
requestAnimationFrame(() => focusAtEnd());
|
||||||
|
}
|
||||||
|
void updateCodeDecorations(editorView);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value) => {
|
||||||
|
const view = editorView;
|
||||||
|
if (!view || value === view.state.doc.toString()) return;
|
||||||
|
|
||||||
|
const cursor = Math.min(view.state.selection.main.head, value.length);
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: 0, to: view.state.doc.length, insert: value },
|
||||||
|
selection: EditorSelection.cursor(cursor),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
editorView?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusAtEnd() {
|
||||||
|
const view = editorView;
|
||||||
|
if (!view) return;
|
||||||
|
const end = view.state.doc.length;
|
||||||
|
view.dispatch({ selection: EditorSelection.cursor(end), scrollIntoView: true });
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceRange(text: string, from?: number, to?: number) {
|
||||||
|
const view = editorView;
|
||||||
|
if (!view) return;
|
||||||
|
const selection = view.state.selection.main;
|
||||||
|
const replaceFrom = from ?? selection.from;
|
||||||
|
const replaceTo = to ?? selection.to;
|
||||||
|
const cursor = replaceFrom + text.length;
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: replaceFrom, to: replaceTo, insert: text },
|
||||||
|
selection: EditorSelection.cursor(cursor),
|
||||||
|
scrollIntoView: true,
|
||||||
|
userEvent: "input.paste",
|
||||||
|
});
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToLine(lineNumber: number) {
|
||||||
|
const view = editorView;
|
||||||
|
if (!view) return;
|
||||||
|
const safeLine = Math.max(1, Math.min(lineNumber, view.state.doc.lines));
|
||||||
|
const line = view.state.doc.line(safeLine);
|
||||||
|
view.dispatch({ selection: EditorSelection.cursor(line.from), scrollIntoView: true });
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCursorPosition(pos: number) {
|
||||||
|
const view = editorView;
|
||||||
|
if (!view) return;
|
||||||
|
const safePos = Math.max(0, Math.min(pos, view.state.doc.length));
|
||||||
|
view.dispatch({ selection: EditorSelection.cursor(safePos), scrollIntoView: true });
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ focus, focusAtEnd, replaceRange, scrollToLine, setCursorPosition });
|
||||||
|
|
||||||
|
onMounted(createEditor);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
highlightRequestId += 1;
|
||||||
|
editorView?.destroy();
|
||||||
|
editorView = null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="hostRef"
|
||||||
|
class="markdown-code-editor"
|
||||||
|
:style="{ '--markdown-code-editor-min-height': props.minHeight }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-code-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,17 +3,23 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
ref,
|
ref,
|
||||||
watch,
|
watch,
|
||||||
type ComponentPublicInstance,
|
|
||||||
} from "vue";
|
} from "vue";
|
||||||
|
import { Fancybox } from "@fancyapps/ui";
|
||||||
|
import "@fancyapps/ui/dist/fancybox/fancybox.css";
|
||||||
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";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkBreaks from "remark-breaks";
|
||||||
import remarkRehype from "remark-rehype";
|
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 +35,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 +58,21 @@ 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;
|
||||||
|
setCursorPosition: (pos: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
@@ -64,6 +80,8 @@ const props = withDefaults(
|
|||||||
documentMode?: DocumentMode;
|
documentMode?: DocumentMode;
|
||||||
previewOnly?: boolean;
|
previewOnly?: boolean;
|
||||||
filePath?: string | null;
|
filePath?: string | null;
|
||||||
|
renderSingleLineBreaks?: boolean;
|
||||||
|
paragraphIndent?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
@@ -71,6 +89,8 @@ const props = withDefaults(
|
|||||||
documentMode: undefined,
|
documentMode: undefined,
|
||||||
previewOnly: false,
|
previewOnly: false,
|
||||||
filePath: null,
|
filePath: null,
|
||||||
|
renderSingleLineBreaks: false,
|
||||||
|
paragraphIndent: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -209,14 +229,23 @@ const mediaSanitizeSchema = {
|
|||||||
tagNames: [...(defaultSchema.tagNames || []), "video"],
|
tagNames: [...(defaultSchema.tagNames || []), "video"],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Full pipeline: markdown → HTML */
|
function createMarkdownProcessor(renderSingleLineBreaks: boolean) {
|
||||||
const md2html = unified()
|
const processor = unified().use(remarkParse).use(remarkGfm);
|
||||||
.use(remarkParse)
|
|
||||||
.use(remarkGfm)
|
if (renderSingleLineBreaks) {
|
||||||
.use(remarkLocalMedia)
|
processor.use(remarkBreaks);
|
||||||
.use(remarkRehype, { allowDangerousHtml: false })
|
}
|
||||||
.use(rehypeSanitize, mediaSanitizeSchema)
|
|
||||||
.use(rehypeStringify);
|
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);
|
const mdParser = unified().use(remarkParse).use(remarkGfm);
|
||||||
|
|
||||||
@@ -230,9 +259,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 +274,12 @@ 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;
|
const previewParagraphStyle = computed(() => ({
|
||||||
|
"--paragraph-indent": props.paragraphIndent ? "2em" : "0",
|
||||||
|
}));
|
||||||
let modeScrollRestoreFrame: number | null = null;
|
let modeScrollRestoreFrame: number | null = null;
|
||||||
|
let renderRequestId = 0;
|
||||||
|
const previewImageSelector = ".markdown-preview:not(.markdown-mixed) img";
|
||||||
|
|
||||||
interface ScrollSnapshot {
|
interface ScrollSnapshot {
|
||||||
ratio: number;
|
ratio: number;
|
||||||
@@ -276,7 +309,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 +318,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 +329,63 @@ 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 processor = props.renderSingleLineBreaks ? singleLineBreaksMd2html : standardMd2html;
|
||||||
|
const file = processor.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 +400,16 @@ function escapeHtml(value: string) {
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +418,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 +434,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 +485,6 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
scheduleEditorTextareaResize();
|
|
||||||
// 外部切换文件时重置滚动位置;本组件内编辑时保留当前位置。
|
// 外部切换文件时重置滚动位置;本组件内编辑时保留当前位置。
|
||||||
if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0;
|
if (shouldResetScroll && scrollRef.value) scrollRef.value.scrollTop = 0;
|
||||||
});
|
});
|
||||||
@@ -415,40 +492,18 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Editor textarea handlers ───────────────────────────────────────
|
watch(
|
||||||
|
() => props.renderSingleLineBreaks,
|
||||||
|
() => {
|
||||||
|
syncRenderedState(props.modelValue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function setBlockTextareaRef(el: Element | ComponentPublicInstance | null) {
|
// ── Editor handlers ────────────────────────────────────────────────
|
||||||
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 +516,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 +533,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,57 +592,61 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function mergeCurrentBlockWithPrevious() {
|
||||||
|
if (activeDocumentMode.value !== "mixed") return;
|
||||||
|
const blockId = editingBlockId.value;
|
||||||
|
if (!blockId) return;
|
||||||
|
|
||||||
|
const blockIndex = markdownBlocks.value.findIndex((b) => b.id === blockId);
|
||||||
|
if (blockIndex <= 0) return;
|
||||||
|
|
||||||
|
const previousBlock = markdownBlocks.value[blockIndex - 1];
|
||||||
|
const currentBlock = markdownBlocks.value[blockIndex];
|
||||||
|
|
||||||
|
// 将当前编辑中的源码与前一个块的源码合并
|
||||||
|
const mergedSource = previousBlock.source + editingSource.value;
|
||||||
|
|
||||||
|
// 构建新文档:用合并后的源码替换两个块的范围
|
||||||
|
const nextValue =
|
||||||
|
props.modelValue.slice(0, previousBlock.startOffset) +
|
||||||
|
mergedSource +
|
||||||
|
props.modelValue.slice(currentBlock.endOffset);
|
||||||
|
|
||||||
|
// 记录合并点位置(上一个块的原末尾),用于后续光标定位
|
||||||
|
const cursorOffset = previousBlock.source.length;
|
||||||
|
|
||||||
|
// 取消当前编辑状态
|
||||||
|
cancelBlockEdit();
|
||||||
|
|
||||||
|
// 更新文档
|
||||||
|
emitModelValue(nextValue);
|
||||||
|
|
||||||
|
// 等待重新解析块结构
|
||||||
|
await parseBlocks(nextValue);
|
||||||
|
|
||||||
|
// 找到合并后的新块(startOffset 仍为 previousBlock.startOffset)
|
||||||
|
const newBlock = markdownBlocks.value.find(
|
||||||
|
(b) => b.startOffset === previousBlock.startOffset,
|
||||||
|
);
|
||||||
|
if (newBlock) {
|
||||||
|
// 进入编辑模式,光标定位在合并点
|
||||||
|
editingBlockId.value = newBlock.id;
|
||||||
|
editingSource.value = newBlock.source;
|
||||||
|
editingBaseValue.value = nextValue;
|
||||||
|
editingRange.value = {
|
||||||
|
startOffset: newBlock.startOffset,
|
||||||
|
endOffset: newBlock.endOffset,
|
||||||
|
};
|
||||||
|
await nextTick();
|
||||||
|
blockCodeRef.value?.setCursorPosition(cursorOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(activeDocumentMode, (mode, oldMode) => {
|
watch(activeDocumentMode, (mode, oldMode) => {
|
||||||
const scrollSnapshot = captureScrollSnapshot();
|
const scrollSnapshot = captureScrollSnapshot();
|
||||||
let committedValue: string | undefined;
|
let committedValue: string | undefined;
|
||||||
@@ -675,17 +656,20 @@ 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);
|
onMounted(() => {
|
||||||
|
Fancybox.bind(scrollRef.value ?? null, previewImageSelector, {
|
||||||
|
groupAll: true,
|
||||||
|
placeFocusBack: true,
|
||||||
|
theme: "auto",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (textareaResizeFrame !== null) {
|
Fancybox.unbind(scrollRef.value ?? null, previewImageSelector);
|
||||||
cancelAnimationFrame(textareaResizeFrame);
|
|
||||||
}
|
|
||||||
if (modeScrollRestoreFrame !== null) {
|
if (modeScrollRestoreFrame !== null) {
|
||||||
cancelAnimationFrame(modeScrollRestoreFrame);
|
cancelAnimationFrame(modeScrollRestoreFrame);
|
||||||
}
|
}
|
||||||
@@ -948,47 +932,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 +972,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) {
|
||||||
@@ -1045,25 +1008,21 @@ defineExpose({
|
|||||||
@keydown.capture="onRootKeydown"
|
@keydown.capture="onRootKeydown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx-auto px-8 py-4 h-full"
|
class="mx-auto px-8 pt-4 pb-10 min-h-full"
|
||||||
:style="{
|
:style="{
|
||||||
maxWidth: `calc(700px * var(--editor-zoom, 1))`,
|
maxWidth: `calc(700px * var(--editor-zoom, 1))`,
|
||||||
fontSize: `calc(1rem * var(--editor-zoom, 1))`,
|
fontSize: `calc(1rem * var(--editor-zoom, 1))`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- 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,23 +1031,24 @@ 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" :style="previewParagraphStyle" v-html="renderedHtml" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mixed mode -->
|
<!-- Mixed mode -->
|
||||||
<div v-else class="min-h-[60vh] markdown-preview markdown-mixed">
|
<div v-else class="min-h-[60vh] markdown-preview markdown-mixed" :style="previewParagraphStyle">
|
||||||
<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"
|
||||||
/>
|
/>
|
||||||
@@ -1108,18 +1068,25 @@ defineExpose({
|
|||||||
v-for="block in markdownBlocks"
|
v-for="block in markdownBlocks"
|
||||||
:key="block.id"
|
:key="block.id"
|
||||||
class="markdown-block"
|
class="markdown-block"
|
||||||
:class="{ 'markdown-block-editing': editingBlockId === block.id }"
|
:class="{
|
||||||
|
'markdown-block-editing': editingBlockId === block.id,
|
||||||
|
'markdown-block-paragraph': block.type === 'paragraph',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
|
@backspace-at-start="mergeCurrentBlockWithPrevious"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -1157,14 +1124,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;
|
||||||
}
|
}
|
||||||
@@ -1186,7 +1148,7 @@ defineExpose({
|
|||||||
.markdown-preview h3 { font-size: 1.125em; }
|
.markdown-preview h3 { font-size: 1.125em; }
|
||||||
.markdown-preview h4 { font-size: 1em; }
|
.markdown-preview h4 { font-size: 1em; }
|
||||||
|
|
||||||
.markdown-preview p { margin: 0.5em 0; }
|
.markdown-preview p { margin: 0.8em 0; text-indent: var(--paragraph-indent, 0); }
|
||||||
|
|
||||||
.markdown-preview strong { font-weight: 700; color: var(--app-text-strong); }
|
.markdown-preview strong { font-weight: 700; color: var(--app-text-strong); }
|
||||||
.markdown-preview em { font-style: italic; }
|
.markdown-preview em { font-style: italic; }
|
||||||
@@ -1209,10 +1171,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;
|
||||||
@@ -1269,6 +1251,10 @@ defineExpose({
|
|||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-preview:not(.markdown-mixed) img {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Mixed mode block editing ────────────────────────────────────── */
|
/* ── Mixed mode block editing ────────────────────────────────────── */
|
||||||
|
|
||||||
.markdown-block {
|
.markdown-block {
|
||||||
@@ -1286,15 +1272,16 @@ defineExpose({
|
|||||||
box-shadow 0.15s ease;
|
box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-block:not(.markdown-block-editing):hover > .markdown-block-preview > * {
|
|
||||||
background-color: var(--app-surface-muted);
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-block-editing {
|
.markdown-block-editing {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-mixed
|
||||||
|
> .markdown-block-paragraph
|
||||||
|
+ .markdown-block-paragraph.markdown-block-editing {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown-block-editing,
|
.markdown-block-editing,
|
||||||
.markdown-block-editing:hover {
|
.markdown-block-editing:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -1315,20 +1302,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,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface SyncResult {
|
|||||||
deletedRemote: number;
|
deletedRemote: number;
|
||||||
skipped: number;
|
skipped: number;
|
||||||
};
|
};
|
||||||
|
files: SyncFileDetail[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncConnectionTestResult {
|
interface SyncConnectionTestResult {
|
||||||
@@ -48,6 +49,27 @@ interface SyncConnectionTestResult {
|
|||||||
details: string[];
|
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(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -55,12 +77,16 @@ const props = withDefaults(
|
|||||||
workspacePath?: string;
|
workspacePath?: string;
|
||||||
defaultDocumentMode?: DocumentMode;
|
defaultDocumentMode?: DocumentMode;
|
||||||
appTheme?: AppTheme;
|
appTheme?: AppTheme;
|
||||||
|
renderSingleLineBreaks?: boolean;
|
||||||
|
paragraphIndent?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
initialPage: "about",
|
initialPage: "about",
|
||||||
workspacePath: "",
|
workspacePath: "",
|
||||||
defaultDocumentMode: "edit",
|
defaultDocumentMode: "edit",
|
||||||
appTheme: "warm",
|
appTheme: "warm",
|
||||||
|
renderSingleLineBreaks: false,
|
||||||
|
paragraphIndent: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -68,6 +94,8 @@ const emit = defineEmits<{
|
|||||||
"update:show": [value: boolean];
|
"update:show": [value: boolean];
|
||||||
updateDefaultDocumentMode: [mode: DocumentMode];
|
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||||
updateAppTheme: [theme: AppTheme];
|
updateAppTheme: [theme: AppTheme];
|
||||||
|
updateRenderSingleLineBreaks: [value: boolean];
|
||||||
|
updateParagraphIndent: [value: boolean];
|
||||||
prepareWorkspaceSync: [done: (saved: boolean) => void];
|
prepareWorkspaceSync: [done: (saved: boolean) => void];
|
||||||
workspaceSynced: [];
|
workspaceSynced: [];
|
||||||
}>();
|
}>();
|
||||||
@@ -83,8 +111,7 @@ const autoUpdate = ref(loadBooleanPref(AUTO_UPDATE_KEY, true));
|
|||||||
const language = ref(loadStringPref(LANGUAGE_KEY, "zh-Hans"));
|
const language = ref(loadStringPref(LANGUAGE_KEY, "zh-Hans"));
|
||||||
const updateStatus = ref("");
|
const updateStatus = ref("");
|
||||||
const syncConfig = ref<SyncConfig>(createDefaultSyncConfig());
|
const syncConfig = ref<SyncConfig>(createDefaultSyncConfig());
|
||||||
const syncStatus = ref("");
|
const syncFeedback = ref<SyncFeedback | null>(null);
|
||||||
const syncError = ref("");
|
|
||||||
const syncSaving = ref(false);
|
const syncSaving = ref(false);
|
||||||
const syncTesting = ref(false);
|
const syncTesting = ref(false);
|
||||||
const syncRunning = ref(false);
|
const syncRunning = ref(false);
|
||||||
@@ -98,6 +125,30 @@ const pageTitle = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isSyncBusy = computed(() => syncSaving.value || syncTesting.value || syncRunning.value);
|
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
|
||||||
|
.filter((file) => file.action !== "skip")
|
||||||
|
.sort((left, right) => left.path.localeCompare(right.path));
|
||||||
|
});
|
||||||
|
|
||||||
const menuItems: Array<{
|
const menuItems: Array<{
|
||||||
id: SettingsPage;
|
id: SettingsPage;
|
||||||
@@ -122,14 +173,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,42 +321,62 @@ function onDefaultDocumentModeChange(event: Event) {
|
|||||||
emit("updateDefaultDocumentMode", mode);
|
emit("updateDefaultDocumentMode", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onThemeChange(event: Event) {
|
function setSyncError(message: string, title = "操作失败") {
|
||||||
const theme = (event.target as HTMLSelectElement).value as AppTheme;
|
syncFeedback.value = {
|
||||||
emit("updateAppTheme", theme);
|
kind: "error",
|
||||||
|
title,
|
||||||
|
summary: message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetTheme() {
|
function syncActionLabel(action: SyncFileAction) {
|
||||||
emit("updateAppTheme", "warm");
|
if (action === "upload") return "上传";
|
||||||
|
if (action === "download") return "下载";
|
||||||
|
if (action === "deleteLocal") return "删除本地";
|
||||||
|
if (action === "deleteRemote") return "删除远端";
|
||||||
|
return "未变更";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSyncStatus(message: string, isError = false) {
|
function formatFileSize(bytes: number) {
|
||||||
if (isError) {
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
syncError.value = message;
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)} KB`;
|
||||||
syncStatus.value = "";
|
if (bytes < 1024 * 1024 * 1024) {
|
||||||
} else {
|
return `${(bytes / (1024 * 1024)).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
|
||||||
syncStatus.value = message;
|
|
||||||
syncError.value = "";
|
|
||||||
}
|
}
|
||||||
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSyncConfig() {
|
async function loadSyncConfig() {
|
||||||
try {
|
try {
|
||||||
const config = await invoke<SyncConfig>("load_sync_config");
|
const config = await invoke<SyncConfig>("load_sync_config");
|
||||||
syncConfig.value = mergeSyncConfig(config);
|
syncConfig.value = mergeSyncConfig(config);
|
||||||
syncError.value = "";
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSyncStatus(`读取同步配置失败: ${error}`, true);
|
setSyncError(`读取同步配置失败: ${error}`, "无法读取同步配置");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSyncConfig() {
|
async function saveSyncConfig() {
|
||||||
syncSaving.value = true;
|
syncSaving.value = true;
|
||||||
|
syncFeedback.value = {
|
||||||
|
kind: "progress",
|
||||||
|
title: "正在保存配置",
|
||||||
|
summary: `正在保存 ${syncProviderLabel.value} 配置。`,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
await invoke("save_sync_config", { config: syncConfig.value });
|
const result = await invoke<SyncConfigSaveResult>("save_sync_config", { config: syncConfig.value });
|
||||||
setSyncStatus("同步配置已保存");
|
syncFeedback.value = {
|
||||||
|
kind: "save",
|
||||||
|
title: "同步配置已保存",
|
||||||
|
summary: `已保存 ${syncProviderLabel.value} 配置。`,
|
||||||
|
configPath: result.configPath,
|
||||||
|
details: [
|
||||||
|
syncTargetDescription.value,
|
||||||
|
`启动时自动同步:${syncConfig.value.autoSyncOnStartup ? "开启" : "关闭"}`,
|
||||||
|
`保存后自动同步:${syncConfig.value.autoSyncAfterSave ? "开启" : "关闭"}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSyncStatus(`保存失败: ${error}`, true);
|
setSyncError(String(error), "保存同步配置失败");
|
||||||
} finally {
|
} finally {
|
||||||
syncSaving.value = false;
|
syncSaving.value = false;
|
||||||
}
|
}
|
||||||
@@ -282,36 +384,45 @@ async function saveSyncConfig() {
|
|||||||
|
|
||||||
async function testSyncConnection() {
|
async function testSyncConnection() {
|
||||||
syncTesting.value = true;
|
syncTesting.value = true;
|
||||||
|
syncFeedback.value = {
|
||||||
|
kind: "progress",
|
||||||
|
title: "正在测试连接",
|
||||||
|
summary: `正在检查 ${syncProviderLabel.value} 服务的读取、写入和删除权限。`,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const result = await invoke<SyncConnectionTestResult>("test_sync_connection", { config: syncConfig.value });
|
const result = await invoke<SyncConnectionTestResult>("test_sync_connection", { config: syncConfig.value });
|
||||||
setSyncStatus([result.message, ...result.details.map((item) => `- ${item}`)].join("\n"));
|
syncFeedback.value = {
|
||||||
|
kind: "test",
|
||||||
|
title: "连接测试通过",
|
||||||
|
summary: result.message,
|
||||||
|
details: result.details,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSyncStatus(String(error), true);
|
setSyncError(String(error), "连接测试失败");
|
||||||
} finally {
|
} finally {
|
||||||
syncTesting.value = false;
|
syncTesting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSyncResult(result: SyncResult) {
|
|
||||||
const { stats } = result;
|
|
||||||
return `同步完成:上传 ${stats.uploaded},下载 ${stats.downloaded},删除本地 ${stats.deletedLocal},删除远端 ${stats.deletedRemote},跳过 ${stats.skipped}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runSyncNow() {
|
async function runSyncNow() {
|
||||||
if (!props.workspacePath) {
|
if (!props.workspacePath) {
|
||||||
setSyncStatus("请先打开一个工作目录", true);
|
setSyncError("请先打开一个工作目录", "无法开始同步");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
syncRunning.value = true;
|
syncRunning.value = true;
|
||||||
try {
|
try {
|
||||||
setSyncStatus("正在同步:扫描本地和远端文件,远端列表请求最长等待 120 秒。");
|
syncFeedback.value = {
|
||||||
|
kind: "progress",
|
||||||
|
title: "正在同步",
|
||||||
|
summary: "正在扫描本地和远端文件,远端列表请求最长等待 120 秒。",
|
||||||
|
};
|
||||||
await invoke("save_sync_config", { config: syncConfig.value });
|
await invoke("save_sync_config", { config: syncConfig.value });
|
||||||
const prepared = await new Promise<boolean>((resolve) => {
|
const prepared = await new Promise<boolean>((resolve) => {
|
||||||
emit("prepareWorkspaceSync", resolve);
|
emit("prepareWorkspaceSync", resolve);
|
||||||
});
|
});
|
||||||
if (!prepared) {
|
if (!prepared) {
|
||||||
setSyncStatus("同步已取消,当前文档尚未保存", true);
|
setSyncError("当前文档尚未保存,同步已取消。", "同步未开始");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +431,16 @@ async function runSyncNow() {
|
|||||||
config: syncConfig.value,
|
config: syncConfig.value,
|
||||||
});
|
});
|
||||||
syncConfig.value.lastSyncAt = result.lastSyncAt;
|
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");
|
emit("workspaceSynced");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSyncStatus(`同步失败: ${error}`, true);
|
setSyncError(String(error), "同步失败");
|
||||||
} finally {
|
} finally {
|
||||||
syncRunning.value = false;
|
syncRunning.value = false;
|
||||||
}
|
}
|
||||||
@@ -335,6 +452,7 @@ watch(
|
|||||||
if (!show) return;
|
if (!show) return;
|
||||||
activePage.value = props.initialPage;
|
activePage.value = props.initialPage;
|
||||||
updateStatus.value = "";
|
updateStatus.value = "";
|
||||||
|
syncFeedback.value = null;
|
||||||
void loadAppVersion();
|
void loadAppVersion();
|
||||||
void loadSyncConfig();
|
void loadSyncConfig();
|
||||||
},
|
},
|
||||||
@@ -399,6 +517,9 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<div v-if="activePage === 'about'" class="settings-content">
|
<div v-if="activePage === 'about'" class="settings-content">
|
||||||
<section class="settings-about-card" aria-label="关于 yurou">
|
<section class="settings-about-card" aria-label="关于 yurou">
|
||||||
|
<div class="settings-about-logo">
|
||||||
|
<img src="/yurou-logo.png" alt="yurou logo" class="settings-logo-img" />
|
||||||
|
</div>
|
||||||
<div class="settings-row settings-row-top">
|
<div class="settings-row settings-row-top">
|
||||||
<div class="settings-copy">
|
<div class="settings-copy">
|
||||||
<h3>版本 {{ appVersion }}</h3>
|
<h3>版本 {{ appVersion }}</h3>
|
||||||
@@ -475,6 +596,34 @@ onBeforeUnmount(() => {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-copy">
|
||||||
|
<h3>单换行显示为换行</h3>
|
||||||
|
<p>开启后,预览和混合模式会保留普通回车产生的换行,不修改 Markdown 原文。</p>
|
||||||
|
</div>
|
||||||
|
<label class="settings-switch" title="单换行显示为换行">
|
||||||
|
<input
|
||||||
|
:checked="renderSingleLineBreaks"
|
||||||
|
type="checkbox"
|
||||||
|
@change="emit('updateRenderSingleLineBreaks', ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-copy">
|
||||||
|
<h3>段落首行缩进</h3>
|
||||||
|
<p>开启后,预览和混合模式下段落首行会缩进两个字符,更符合中文排版习惯。</p>
|
||||||
|
</div>
|
||||||
|
<label class="settings-switch" title="段落首行缩进">
|
||||||
|
<input
|
||||||
|
:checked="paragraphIndent"
|
||||||
|
type="checkbox"
|
||||||
|
@change="emit('updateParagraphIndent', ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -647,41 +796,119 @@ onBeforeUnmount(() => {
|
|||||||
<p>
|
<p>
|
||||||
{{ syncConfig.lastSyncAt ? `上次同步:${syncConfig.lastSyncAt}` : '尚未同步' }}
|
{{ syncConfig.lastSyncAt ? `上次同步:${syncConfig.lastSyncAt}` : '尚未同步' }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
|
||||||
v-if="syncStatus || syncError"
|
|
||||||
class="settings-sync-log"
|
|
||||||
:class="{ 'settings-sync-log-error': Boolean(syncError) }"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
{{ syncError || syncStatus }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-action-stack">
|
<div class="settings-action-stack">
|
||||||
|
<div class="settings-config-actions" aria-label="同步配置操作">
|
||||||
|
<button
|
||||||
|
class="settings-secondary-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="isSyncBusy"
|
||||||
|
@click="saveSyncConfig"
|
||||||
|
>
|
||||||
|
<i :class="syncSaving ? 'ri-loader-4-line settings-spin' : 'ri-save-3-line'"></i>
|
||||||
|
{{ syncSaving ? '保存中' : '保存配置' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="settings-secondary-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="isSyncBusy || syncConfig.provider === 'none'"
|
||||||
|
@click="testSyncConnection"
|
||||||
|
>
|
||||||
|
<i :class="syncTesting ? 'ri-loader-4-line settings-spin' : 'ri-pulse-line'"></i>
|
||||||
|
{{ syncTesting ? '测试中' : '测试连接' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="settings-secondary-btn"
|
class="settings-primary-btn settings-sync-now-btn"
|
||||||
type="button"
|
|
||||||
:disabled="isSyncBusy"
|
|
||||||
@click="saveSyncConfig"
|
|
||||||
>
|
|
||||||
{{ syncSaving ? '保存中' : '保存配置' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="settings-secondary-btn"
|
|
||||||
type="button"
|
|
||||||
:disabled="isSyncBusy || syncConfig.provider === 'none'"
|
|
||||||
@click="testSyncConnection"
|
|
||||||
>
|
|
||||||
{{ syncTesting ? '测试中' : '测试连接' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="settings-primary-btn"
|
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="isSyncBusy || syncConfig.provider === 'none' || !workspacePath"
|
:disabled="isSyncBusy || syncConfig.provider === 'none' || !workspacePath"
|
||||||
@click="runSyncNow"
|
@click="runSyncNow"
|
||||||
>
|
>
|
||||||
|
<i :class="syncRunning ? 'ri-loader-4-line settings-spin' : 'ri-loop-left-line'"></i>
|
||||||
{{ syncRunning ? '同步中' : '立即同步' }}
|
{{ syncRunning ? '同步中' : '立即同步' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
v-if="syncFeedback"
|
||||||
|
class="settings-sync-feedback"
|
||||||
|
:class="{
|
||||||
|
'settings-sync-feedback-error': syncFeedback.kind === 'error',
|
||||||
|
'settings-sync-feedback-progress': syncFeedback.kind === 'progress',
|
||||||
|
}"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<div class="settings-sync-feedback-header">
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
syncFeedback.kind === 'error'
|
||||||
|
? 'ri-error-warning-line'
|
||||||
|
: syncFeedback.kind === 'progress'
|
||||||
|
? 'ri-loader-4-line settings-spin'
|
||||||
|
: 'ri-checkbox-circle-line'
|
||||||
|
"
|
||||||
|
></i>
|
||||||
|
<div>
|
||||||
|
<strong>{{ syncFeedback.title }}</strong>
|
||||||
|
<p>{{ syncFeedback.summary }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="syncFeedback.configPath" class="settings-config-path">
|
||||||
|
<span>配置保存位置</span>
|
||||||
|
<code :title="syncFeedback.configPath">{{ syncFeedback.configPath }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul v-if="syncFeedback.details?.length" class="settings-feedback-details">
|
||||||
|
<li v-for="detail in syncFeedback.details" :key="detail">
|
||||||
|
<i class="ri-check-line"></i>
|
||||||
|
<span>{{ detail }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<template v-if="syncFeedback.result">
|
||||||
|
<dl class="settings-sync-stats" aria-label="同步数量汇总">
|
||||||
|
<div>
|
||||||
|
<dt>上传</dt>
|
||||||
|
<dd>{{ syncFeedback.result.stats.uploaded }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>下载</dt>
|
||||||
|
<dd>{{ syncFeedback.result.stats.downloaded }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>删除本地</dt>
|
||||||
|
<dd>{{ syncFeedback.result.stats.deletedLocal }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>删除远端</dt>
|
||||||
|
<dd>{{ syncFeedback.result.stats.deletedRemote }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>未变更</dt>
|
||||||
|
<dd>{{ syncFeedback.result.stats.skipped }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="settings-sync-file-section">
|
||||||
|
<div class="settings-sync-file-heading">
|
||||||
|
<strong>变更文件</strong>
|
||||||
|
<span>{{ syncFileDetails.length }} 个文件</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="syncFileDetails.length" class="settings-sync-file-list">
|
||||||
|
<div v-for="file in syncFileDetails" :key="`${file.action}:${file.path}`" class="settings-sync-file">
|
||||||
|
<span class="settings-sync-file-action" :data-action="file.action">
|
||||||
|
{{ syncActionLabel(file.action) }}
|
||||||
|
</span>
|
||||||
|
<code :title="file.path">{{ file.path }}</code>
|
||||||
|
<span class="settings-sync-file-size">{{ formatFileSize(file.size) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="settings-sync-file-empty">本次同步没有文件变更。</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -690,51 +917,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>
|
||||||
@@ -886,6 +1097,20 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: inset 0 0 0 1px var(--app-border);
|
box-shadow: inset 0 0 0 1px var(--app-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-about-logo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 28px 20px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-logo-img {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-row {
|
.settings-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
@@ -942,9 +1167,16 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.settings-action-stack {
|
.settings-action-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
width: 244px;
|
||||||
|
align-items: stretch;
|
||||||
flex-direction: column;
|
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,
|
.settings-primary-btn,
|
||||||
@@ -957,6 +1189,7 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
gap: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.15s ease,
|
background-color 0.15s ease,
|
||||||
@@ -977,7 +1210,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-secondary-btn {
|
.settings-secondary-btn {
|
||||||
min-width: 50px;
|
min-width: 0;
|
||||||
border: 1px solid var(--app-border-strong);
|
border: 1px solid var(--app-border-strong);
|
||||||
background: var(--app-surface);
|
background: var(--app-surface);
|
||||||
color: var(--app-text);
|
color: var(--app-text);
|
||||||
@@ -1143,68 +1376,288 @@ onBeforeUnmount(() => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-sync-log {
|
.settings-sync-now-btn {
|
||||||
width: min(100%, 420px);
|
width: 100%;
|
||||||
max-height: 138px;
|
}
|
||||||
margin-top: 10px;
|
|
||||||
overflow: auto;
|
.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: 1px solid color-mix(in srgb, var(--app-accent) 26%, var(--app-border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: color-mix(in srgb, var(--app-accent) 7%, var(--app-surface));
|
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);
|
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;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: 9px 10px;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-sync-log-error {
|
.settings-config-path {
|
||||||
border-color: color-mix(in srgb, #dc2626 32%, var(--app-border));
|
display: grid;
|
||||||
background: color-mix(in srgb, #dc2626 7%, var(--app-surface));
|
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;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-swatch-group {
|
.settings-sync-file-action[data-action="skip"] {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-reset-btn,
|
|
||||||
.theme-swatch {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-reset-btn {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--app-muted);
|
color: var(--app-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-reset-btn:hover {
|
.settings-sync-file code {
|
||||||
background: var(--app-hover);
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
color: var(--app-text);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-swatch {
|
.settings-spin {
|
||||||
width: 22px;
|
animation: settings-spin 0.85s linear infinite;
|
||||||
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 {
|
@keyframes settings-spin {
|
||||||
outline: 2px solid var(--app-accent);
|
to {
|
||||||
outline-offset: 3px;
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
padding: 4px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 48px minmax(0, 1fr) 18px;
|
||||||
|
gap: 11px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 64px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--app-border-strong);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--app-surface);
|
||||||
|
color: var(--app-text);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option:hover {
|
||||||
|
background: var(--app-surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option-active {
|
||||||
|
border-color: var(--app-accent);
|
||||||
|
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 {
|
||||||
@@ -1259,11 +1712,45 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-action-stack {
|
.settings-action-stack {
|
||||||
|
width: 100%;
|
||||||
align-items: flex-start;
|
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 {
|
.settings-switch-group {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.settings-spin {
|
||||||
|
animation-duration: 1.8s;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ interface DirEntry {
|
|||||||
children?: DirEntry[];
|
children?: DirEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TreeExpansionCommand {
|
||||||
|
id: number;
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const TREE_CONTEXT_MENU_CLOSE_EVENT = "yurou-tree-context-menu-close";
|
const TREE_CONTEXT_MENU_CLOSE_EVENT = "yurou-tree-context-menu-close";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
entry: DirEntry;
|
entry: DirEntry;
|
||||||
depth: number;
|
depth: number;
|
||||||
currentFilePath?: string | null;
|
currentFilePath?: string | null;
|
||||||
|
expansionCommand?: TreeExpansionCommand | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -33,6 +39,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const expanded = ref(true);
|
const expanded = ref(true);
|
||||||
|
const handledExpansionCommandId = ref(0);
|
||||||
const contextMenuOpen = ref(false);
|
const contextMenuOpen = ref(false);
|
||||||
const contextMenuPoint = ref<{ x: number; y: number } | null>(null);
|
const contextMenuPoint = ref<{ x: number; y: number } | null>(null);
|
||||||
const contextMenuStyle = computed(() => {
|
const contextMenuStyle = computed(() => {
|
||||||
@@ -76,12 +83,35 @@ function onContextMenuKeydown(event: KeyboardEvent) {
|
|||||||
watch(
|
watch(
|
||||||
() => props.entry.children,
|
() => props.entry.children,
|
||||||
(children, oldChildren) => {
|
(children, oldChildren) => {
|
||||||
if (children !== undefined && oldChildren === undefined) {
|
if (
|
||||||
|
children !== undefined &&
|
||||||
|
oldChildren === undefined &&
|
||||||
|
props.expansionCommand?.expanded !== false
|
||||||
|
) {
|
||||||
expanded.value = true;
|
expanded.value = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.expansionCommand,
|
||||||
|
(command) => {
|
||||||
|
if (
|
||||||
|
!command ||
|
||||||
|
!props.entry.is_dir ||
|
||||||
|
command.id <= handledExpansionCommandId.value
|
||||||
|
) return;
|
||||||
|
|
||||||
|
handledExpansionCommandId.value = command.id;
|
||||||
|
expanded.value = command.expanded;
|
||||||
|
|
||||||
|
if (command.expanded && props.entry.children === undefined) {
|
||||||
|
emit("toggleFolder", props.entry.path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
const isActive = computed(() => {
|
const isActive = computed(() => {
|
||||||
return props.currentFilePath != null && props.currentFilePath === props.entry.path;
|
return props.currentFilePath != null && props.currentFilePath === props.entry.path;
|
||||||
});
|
});
|
||||||
@@ -307,6 +337,7 @@ function onContextMenu(event: MouseEvent) {
|
|||||||
:entry="child"
|
:entry="child"
|
||||||
:depth="depth + 1"
|
:depth="depth + 1"
|
||||||
:currentFilePath="currentFilePath"
|
:currentFilePath="currentFilePath"
|
||||||
|
:expansion-command="expansionCommand"
|
||||||
@toggle-folder="(path: string) => emit('toggleFolder', path)"
|
@toggle-folder="(path: string) => emit('toggleFolder', path)"
|
||||||
@open-file="(path: string) => emit('openFile', path)"
|
@open-file="(path: string) => emit('openFile', path)"
|
||||||
@create-folder="(path: string) => emit('createFolder', path)"
|
@create-folder="(path: string) => emit('createFolder', path)"
|
||||||
|
|||||||
100
src/lib/codeHighlight.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import type { HighlighterCore, LanguageInput, ThemedToken } from "shiki/core";
|
||||||
|
|
||||||
|
type LanguageLoader = () => Promise<{ default: LanguageInput }>;
|
||||||
|
|
||||||
|
const languageLoaders: Record<string, LanguageLoader> = {
|
||||||
|
bash: () => import("shiki/langs/bash.mjs"),
|
||||||
|
c: () => import("shiki/langs/c.mjs"),
|
||||||
|
cpp: () => import("shiki/langs/cpp.mjs"),
|
||||||
|
csharp: () => import("shiki/langs/csharp.mjs"),
|
||||||
|
css: () => import("shiki/langs/css.mjs"),
|
||||||
|
dockerfile: () => import("shiki/langs/dockerfile.mjs"),
|
||||||
|
go: () => import("shiki/langs/go.mjs"),
|
||||||
|
html: () => import("shiki/langs/html.mjs"),
|
||||||
|
java: () => import("shiki/langs/java.mjs"),
|
||||||
|
javascript: () => import("shiki/langs/javascript.mjs"),
|
||||||
|
json: () => import("shiki/langs/json.mjs"),
|
||||||
|
jsx: () => import("shiki/langs/jsx.mjs"),
|
||||||
|
kotlin: () => import("shiki/langs/kotlin.mjs"),
|
||||||
|
markdown: () => import("shiki/langs/markdown.mjs"),
|
||||||
|
php: () => import("shiki/langs/php.mjs"),
|
||||||
|
powershell: () => import("shiki/langs/powershell.mjs"),
|
||||||
|
python: () => import("shiki/langs/python.mjs"),
|
||||||
|
ruby: () => import("shiki/langs/ruby.mjs"),
|
||||||
|
rust: () => import("shiki/langs/rust.mjs"),
|
||||||
|
scss: () => import("shiki/langs/scss.mjs"),
|
||||||
|
sql: () => import("shiki/langs/sql.mjs"),
|
||||||
|
swift: () => import("shiki/langs/swift.mjs"),
|
||||||
|
toml: () => import("shiki/langs/toml.mjs"),
|
||||||
|
tsx: () => import("shiki/langs/tsx.mjs"),
|
||||||
|
typescript: () => import("shiki/langs/typescript.mjs"),
|
||||||
|
vue: () => import("shiki/langs/vue.mjs"),
|
||||||
|
xml: () => import("shiki/langs/xml.mjs"),
|
||||||
|
yaml: () => import("shiki/langs/yaml.mjs"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let highlighterPromise: Promise<HighlighterCore> | null = null;
|
||||||
|
const languagePromises = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
|
async function getCodeHighlighter() {
|
||||||
|
if (!highlighterPromise) {
|
||||||
|
highlighterPromise = Promise.all([
|
||||||
|
import("shiki/core"),
|
||||||
|
import("shiki/engine/javascript"),
|
||||||
|
]).then(([core, engine]) =>
|
||||||
|
core.createHighlighterCore({
|
||||||
|
themes: [
|
||||||
|
core.createCssVariablesTheme({
|
||||||
|
name: "yurou-code",
|
||||||
|
variablePrefix: "--code-",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
langs: [],
|
||||||
|
engine: engine.createJavaScriptRegexEngine(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return highlighterPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCodeLanguage(highlighter: HighlighterCore, language: string) {
|
||||||
|
if (highlighter.getLoadedLanguages().includes(language)) return;
|
||||||
|
|
||||||
|
let promise = languagePromises.get(language);
|
||||||
|
if (!promise) {
|
||||||
|
const loader = languageLoaders[language];
|
||||||
|
if (!loader) return;
|
||||||
|
promise = loader().then((module) => highlighter.loadLanguage(module.default));
|
||||||
|
languagePromises.set(language, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function highlightCodeToHtml(code: string, language: string) {
|
||||||
|
const highlighter = await getCodeHighlighter();
|
||||||
|
await loadCodeLanguage(highlighter, language);
|
||||||
|
|
||||||
|
if (!highlighter.getLoadedLanguages().includes(language)) return null;
|
||||||
|
|
||||||
|
return highlighter.codeToHtml(code, {
|
||||||
|
lang: language,
|
||||||
|
theme: "yurou-code",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function highlightCodeToTokens(
|
||||||
|
code: string,
|
||||||
|
language: string,
|
||||||
|
): Promise<ThemedToken[][] | null> {
|
||||||
|
const highlighter = await getCodeHighlighter();
|
||||||
|
await loadCodeLanguage(highlighter, language);
|
||||||
|
|
||||||
|
if (!highlighter.getLoadedLanguages().includes(language)) return null;
|
||||||
|
|
||||||
|
return highlighter.codeToTokens(code, {
|
||||||
|
lang: language,
|
||||||
|
theme: "yurou-code",
|
||||||
|
}).tokens;
|
||||||
|
}
|
||||||
96
src/lib/markdownLanguages.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { pickedCompletion, type Completion } from "@codemirror/autocomplete";
|
||||||
|
|
||||||
|
export interface MarkdownCodeLanguage {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
aliases: string[];
|
||||||
|
shiki: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const markdownCodeLanguages: MarkdownCodeLanguage[] = [
|
||||||
|
{ label: "text", name: "Plain text", aliases: ["txt", "plaintext"], shiki: "text" },
|
||||||
|
{ label: "javascript", name: "JavaScript", aliases: ["js", "node"], shiki: "javascript" },
|
||||||
|
{ label: "typescript", name: "TypeScript", aliases: ["ts"], shiki: "typescript" },
|
||||||
|
{ label: "jsx", name: "JSX", aliases: [], shiki: "jsx" },
|
||||||
|
{ label: "tsx", name: "TSX", aliases: [], shiki: "tsx" },
|
||||||
|
{ label: "vue", name: "Vue", aliases: ["vuejs"], shiki: "vue" },
|
||||||
|
{ label: "html", name: "HTML", aliases: ["htm"], shiki: "html" },
|
||||||
|
{ label: "css", name: "CSS", aliases: [], shiki: "css" },
|
||||||
|
{ label: "scss", name: "SCSS", aliases: ["sass"], shiki: "scss" },
|
||||||
|
{ label: "json", name: "JSON", aliases: ["jsonc"], shiki: "json" },
|
||||||
|
{ label: "yaml", name: "YAML", aliases: ["yml"], shiki: "yaml" },
|
||||||
|
{ label: "markdown", name: "Markdown", aliases: ["md"], shiki: "markdown" },
|
||||||
|
{ label: "python", name: "Python", aliases: ["py"], shiki: "python" },
|
||||||
|
{ label: "java", name: "Java", aliases: [], shiki: "java" },
|
||||||
|
{ label: "kotlin", name: "Kotlin", aliases: ["kt"], shiki: "kotlin" },
|
||||||
|
{ label: "c", name: "C", aliases: [], shiki: "c" },
|
||||||
|
{ label: "cpp", name: "C++", aliases: ["c++"], shiki: "cpp" },
|
||||||
|
{ label: "csharp", name: "C#", aliases: ["cs", "c#"], shiki: "csharp" },
|
||||||
|
{ label: "go", name: "Go", aliases: ["golang"], shiki: "go" },
|
||||||
|
{ label: "rust", name: "Rust", aliases: ["rs"], shiki: "rust" },
|
||||||
|
{ label: "php", name: "PHP", aliases: [], shiki: "php" },
|
||||||
|
{ label: "ruby", name: "Ruby", aliases: ["rb"], shiki: "ruby" },
|
||||||
|
{ label: "swift", name: "Swift", aliases: [], shiki: "swift" },
|
||||||
|
{ label: "sql", name: "SQL", aliases: [], shiki: "sql" },
|
||||||
|
{ label: "bash", name: "Bash", aliases: ["sh", "shell", "zsh"], shiki: "bash" },
|
||||||
|
{ label: "powershell", name: "PowerShell", aliases: ["ps1", "pwsh"], shiki: "powershell" },
|
||||||
|
{ label: "dockerfile", name: "Dockerfile", aliases: ["docker"], shiki: "dockerfile" },
|
||||||
|
{ label: "xml", name: "XML", aliases: [], shiki: "xml" },
|
||||||
|
{ label: "toml", name: "TOML", aliases: [], shiki: "toml" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const languageLookup = new Map<string, MarkdownCodeLanguage>();
|
||||||
|
|
||||||
|
for (const language of markdownCodeLanguages) {
|
||||||
|
languageLookup.set(language.label, language);
|
||||||
|
for (const alias of language.aliases) {
|
||||||
|
languageLookup.set(alias, language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMarkdownCodeLanguage(value: string | null | undefined) {
|
||||||
|
const normalized = value?.trim().toLowerCase().split(/\s+/, 1)[0] || "";
|
||||||
|
return languageLookup.get(normalized) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECENT_LANGUAGES_KEY = "markdown-recent-code-languages";
|
||||||
|
|
||||||
|
function readRecentLanguages() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(RECENT_LANGUAGES_KEY);
|
||||||
|
const value = stored ? JSON.parse(stored) : [];
|
||||||
|
return Array.isArray(value)
|
||||||
|
? value.filter((item): item is string => typeof item === "string").slice(0, 5)
|
||||||
|
: [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rememberMarkdownCodeLanguage(label: string) {
|
||||||
|
const next = [label, ...readRecentLanguages().filter((item) => item !== label)].slice(0, 5);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(RECENT_LANGUAGES_KEY, JSON.stringify(next));
|
||||||
|
} catch {
|
||||||
|
// Completion remains available when storage is disabled.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMarkdownLanguageCompletions(): Completion[] {
|
||||||
|
const recent = readRecentLanguages();
|
||||||
|
|
||||||
|
return markdownCodeLanguages.map((language, index) => ({
|
||||||
|
label: language.label,
|
||||||
|
displayLabel: language.name,
|
||||||
|
detail: language.aliases.length > 0 ? language.aliases.join(", ") : language.label,
|
||||||
|
type: "keyword",
|
||||||
|
boost: recent.includes(language.label) ? 100 - recent.indexOf(language.label) : -index,
|
||||||
|
apply(view, completion, from, to) {
|
||||||
|
rememberMarkdownCodeLanguage(language.label);
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: language.label },
|
||||||
|
annotations: pickedCompletion.of(completion),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||