diff --git a/.kilo/plans/1775304798427-lucky-panda.md b/.kilo/plans/1775304798427-lucky-panda.md new file mode 100644 index 0000000..a86e7fe --- /dev/null +++ b/.kilo/plans/1775304798427-lucky-panda.md @@ -0,0 +1,52 @@ +# 导出按钮缺失修复计划 + +## 问题分析 +当前 `action-buttons` 区域只有以下按钮可见: +- 上传文件 +- 导入 Markdown +- 导出 Markdown +- 上传图片 +- AI 切换按钮 + +**缺失功能**:DOCX 和 PDF 导出按钮 + +## 调查结果 +1. ✅ 翻译文件中已存在 `exportDocx` 和 `exportPdf` 键名(src/utils/i18n.js) +2. ❌ 模板中**完全缺失**这两个按钮的 HTML 代码 +3. ❓ 导出功能后端已实现,前端只需要添加调用接口的按钮 +4. ✅ 相关 CSS 样式已存在,按钮外观无需额外调整 + +## 实施计划 + +### 1. 添加 UI 按钮 +在 `src/components/MilkdownEditor.vue:79` 之后添加两个新按钮: +- DOCX 导出按钮 +- PDF 导出按钮 + +按钮位置: +``` +导出 Markdown → 导出 DOCX → 导出 PDF → 上传图片 +``` + +### 2. 实现前端导出功能 +使用已安装的依赖库: +- `docx` 库:用于 DOCX 导出 +- `html2pdf.js` 库:用于 PDF 导出 + +需要添加的函数: +```javascript +const exportDocx = async () => { + // 使用 docx 库实现导出 +} + +const exportPdf = async () => { + // 使用 html2pdf.js 实现导出 +} +``` + +### 3. 按钮图标 +- DOCX:使用文档图标 +- PDF:使用 PDF 专用图标 + +### 4. 状态管理 +添加加载状态和错误处理,与现有按钮保持一致风格 diff --git a/README.md b/README.md index 405a3f0..c4ed81c 100644 --- a/README.md +++ b/README.md @@ -1,264 +1,93 @@ -# LLM in Text - 智能写作助手 +# LLM in Text - 智能写作助手 -基于 Vue3 和 FastAPI 的智能 Markdown 编辑器,集成大语言模型(LLM)实时补全建议功能,提供类似 GitHub Copilot 的 Ghost Text 体验。 +基于 Vue3 和 FastAPI 的智能 Markdown 编辑器,集成大语言模型(LLM)实时补全建议功能。 ## 功能特性 ### Markdown 编辑器 - 基于 Milkdown Crepe 的所见即所得编辑体验 -- 支持完整 Markdown 语法和 LaTeX 公式 +- 支持 Markdown 语法和 LaTeX 公式 +- 支持 Mermaid 图表渲染 - 导入/导出 Markdown 文件 +- 导出 DOCX 和 PDF 格式 ### AI 智能补全 - 实时生成文本补全建议(灰色显示) - 流式响应,低延迟体验 -- 多种交互方式: - - **Tab 键**:接受建议 - - **Esc 键**:拒绝建议 - - **点击灰色文本**:接受建议 - - **继续输入**:自动拒绝建议 +- 多种交互方式:Tab接受、Esc拒绝、点击接受 -### AI 开关控制 -- 右下角 AI 开关按钮 -- 白色 = AI 启用,黑色 = AI 禁用 -- 禁用时自动清除灰色文本并停止 API 调用 +### 文档处理 +- OCR 图片识别:上传图片自动识别文字 +- 文档转换:PDF、DOCX、PPTX、TXT 转 Markdown +- 文档块嵌入:可折叠的文档预览块 +- 智能大小限制:32KB自动禁用AI + +### 设置面板 +- 外观主题:亮色/暗色/跟随系统 +- 背景模式:默认/暖色/阅读灯/自定义图片 +- 模型智能:低/中/高思考级别 +- 隐私控制:隐私模式防止发送IP +- 多语言界面:中英日韩德法 + +### 语音功能 +- TTS文字转语音(macOS) +- STT语音转文字 ## 技术架构 -```mermaid -flowchart TB - subgraph Frontend["前端 (Vue3 + Vite)"] - A[App.vue] --> B[MilkdownEditor.vue] - B --> C[Crepe Editor] - C --> D[ProseMirror] - D --> E[copilotPlugin.ts] - E --> F[copilotGhostMark] - E --> G[api.js] - end - - subgraph Backend["后端 (FastAPI + Python)"] - H[main.pyFastAPI Server] --> I[prompt.pyPrompt 构建] - H --> J[llm.pyOllama 调用] - J --> K[Ollama API] - end - - G -->|POST /v1/completionsSSE 流式响应| H - K -->|LLM 响应| J -``` - -## 项目结构 - -``` -llm-in-text/ -├── src/ -│ ├── components/ -│ │ └── MilkdownEditor.vue # 主编辑器组件 -│ ├── plugins/ -│ │ ├── copilotPlugin.ts # ProseMirror AI 补全插件 -│ │ ├── types.ts # 类型定义 -│ │ └── index.ts # 插件导出 -│ ├── utils/ -│ │ ├── api.js # API 调用封装 -│ │ ├── config.js # 配置文件 -│ │ └── ocrCache.js # OCR 缓存管理 -│ ├── App.vue -│ └── main.js -├── backend/ -│ ├── main.py # FastAPI 服务器 -│ ├── llm.py # LLM API 调用 -│ ├── prompt.py # Prompt 构建 -│ └── requirements.txt -└── README.md -``` +前端: Vue3 + Vite + Milkdown + ProseMirror +后端: FastAPI + Python + Ollama ## 快速开始 -### 环境要求 -- Node.js 18+ -- Python 3.8+ -- Ollama 服务(或其他兼容 OpenAI API 的服务) +环境: Node.js 18+、Python 3.8+、Ollama -### 安装 +安装: +- 前端: npm install +- 后端: pip install -r backend/requirements.txt -```bash -# 前端 -npm install +启动: +- 后端: python backend/main.py (端口8001) +- 前端: npm run dev (端口5173) -# 后端 -cd backend -pip install -r requirements.txt -``` +## API接口 -### 配置 - -在 `backend/.env` 中配置: - -```env -OLLAMA_MODEL=gpt-oss:20b -OLLAMA_HOST=http://localhost:11434 -``` - -### 启动 - -```bash -# 后端(端口 8000) -cd backend -python main.py - -# 前端(端口 5173) -npm run dev -``` - -访问 http://localhost:5173 - -## API 接口 - -### POST /v1/completions - -流式获取补全建议 - -**请求:** -```json -{ - "prefix": "# Title\n\nContent ", - "suffix": "", - "languageId": "markdown" -} -``` - -**响应(SSE):** -``` -data: {"content": "here"} -data: {"content": "here is"} -data: {"done": true} -``` +- POST /v1/completions 流式补全建议 +- POST /v1/ocr 图片文字识别 +- POST /v1/convert 文档转换 +- POST /v1/completions/cancel 取消请求 ## 核心实现 -### 后端设计 +### 后端 +- main.py: FastAPI服务器、SSE流式响应 +- llm.py: 异步Ollama调用、超时控制 +- prompt.py: 7条Prompt规则 +- tts_asr.py: macOS 语音处理 -#### main.py - FastAPI 服务器 -- 定义 `/v1/completions` 端点 -- 使用 `StreamingResponse` 返回 SSE 流式响应 -- CORS 配置允许跨域请求 - -#### llm.py - LLM 调用封装 -- 使用 `ollama.AsyncClient` 异步调用 -- 支持 `think='high'` 思考模式 -- 返回 `content` 和 `thinking` 字段 - -#### prompt.py - Prompt 工程 -精心设计的 Prompt 模板,包含 7 条核心规则: - -| 规则 | 说明 | -|------|------| -| RULE #1 | 无缝连接 - 不重复 suffix 内容,避免"复读机"错误 | -| RULE #2 | 空白处理 - 避免双空格,正确对接标点 | -| RULE #3 | 缩进对齐 - 匹配当前缩进级别和类型 | -| RULE #4 | 列表维护 - 识别并继续任务列表、有序列表、无序列表 | -| RULE #5 | 语法闭合 - 自动闭合未完成的 Markdown 语法 | -| RULE #6 | 输出格式 - 仅输出续写文本,无解释无注释 | -| RULE #7 | 必须输出 - 始终提供有用的续写建议 | - -### 前端设计 - -#### ProseMirror Mark 系统 - -使用 ProseMirror 的 Mark 系统实现灰色建议文本: - -```typescript -// 定义 ghost mark -export const copilotGhostMark = $markSchema('copilot_ghost', () => ({ - excludes: '_', - inclusive: true, - toDOM: () => ['span', { - 'data-copilot-ghost': '', - class: 'copilot-ghost-text' - }, 0] -})) - -// CSS 样式 -.copilot-ghost-text { - color: #999; - opacity: 0.6; -} -``` - -#### copilotPlugin 核心逻辑 - -```mermaid -flowchart LR - A[用户输入] --> B{文档变化?} - B -->|是| C[清除旧建议] - C --> D[防抖 1000ms] - D --> E[发送 API 请求] - E --> F[收到建议] - F --> G[插入 Ghost Text] - - G --> H{用户操作} - H -->|Tab| I[接受建议移除 mark] - H -->|Esc| J[拒绝建议删除文本] - H -->|点击 Ghost| I - H -->|继续输入| J -``` - -#### 关键函数 - -| 函数 | 作用 | -|------|------| -| `scheduleFetch` | 防抖调度 API 请求 | -| `insertGhostText` | 插入带 mark 的建议文本 | -| `acceptSuggestion` | Tab 接受建议 | -| `rejectSuggestion` | Esc 拒绝建议 | -| `clearGhostText` | 清除当前建议 | - -### 数据流 - -```mermaid -sequenceDiagram - participant U as 用户 - participant E as Editor (ProseMirror) - participant P as copilotPlugin - participant A as api.js - participant B as Backend - participant L as LLM - - U->>E: 输入文本 - E->>P: view.update() - P->>P: 清除旧建议 - P->>P: 防抖 1000ms - P->>A: fetchSuggestion(prefix, suffix) - A->>B: POST /v1/completions - B->>B: build_prompt() - B->>L: ollama.chat() - L-->>B: {content, thinking} - B-->>A: SSE stream - A-->>P: suggestion text - P->>E: insertGhostText() - E-->>U: 显示灰色建议 - - alt Tab 键 - U->>P: Tab - P->>E: acceptSuggestion() - E-->>U: 建议变为正常文本 - else Esc 键 - U->>P: Esc - P->>E: rejectSuggestion() - E-->>U: 建议消失 - else 继续输入 - U->>E: 输入其他字符 - E->>P: handleKeyDown() - P->>E: clearGhostText() - end -``` +### 前端 +- copilotPlugin.ts: ProseMirror Mark系统 +- 关键函数: scheduleFetch、insertGhostText +- Pinia Store状态管理 ## 设计亮点 -1. **前后端分离**:前端只负责渲染和数据回传,后端负责 LLM 调用、Prompt 构建和数据解析 -2. **低延迟优化**:防抖机制 (1000ms) + SSE 流式响应 + AbortController 取消过期请求 -3. **ProseMirror Mark 系统**:与编辑器状态完美集成,支持 Undo/Redo -4. **多种交互方式**:Tab/Esc/点击/输入,用户体验友好 -5. **智能大小限制**:文档超过 32KB 自动禁用 AI 功能 +1. 前后端分离 +2. 低延迟优化:防抖+SSE+AbortController +3. ProseMirror Mark系统 +4. 多种交互方式 +5. 智能大小限制 +6. 隐私保护 +7. 多语言支持 +8. 主题定制 +9. 文档处理 +10. 语音功能 + +## 开发指南 + +代码风格: Python(4空格,snake_case) JS/TS(2空格,camelCase) +测试: pytest +构建: npm run build ## 许可证 diff --git a/README.md.fixed b/README.md.fixed deleted file mode 100644 index 20c3741..0000000 Binary files a/README.md.fixed and /dev/null differ diff --git a/README.md.original b/README.md.original deleted file mode 100644 index 20c3741..0000000 Binary files a/README.md.original and /dev/null differ diff --git a/README.md.tmp b/README.md.tmp deleted file mode 100644 index 405a3f0..0000000 --- a/README.md.tmp +++ /dev/null @@ -1,265 +0,0 @@ -# LLM in Text - 智能写作助手 - -基于 Vue3 和 FastAPI 的智能 Markdown 编辑器,集成大语言模型(LLM)实时补全建议功能,提供类似 GitHub Copilot 的 Ghost Text 体验。 - -## 功能特性 - -### Markdown 编辑器 -- 基于 Milkdown Crepe 的所见即所得编辑体验 -- 支持完整 Markdown 语法和 LaTeX 公式 -- 导入/导出 Markdown 文件 - -### AI 智能补全 -- 实时生成文本补全建议(灰色显示) -- 流式响应,低延迟体验 -- 多种交互方式: - - **Tab 键**:接受建议 - - **Esc 键**:拒绝建议 - - **点击灰色文本**:接受建议 - - **继续输入**:自动拒绝建议 - -### AI 开关控制 -- 右下角 AI 开关按钮 -- 白色 = AI 启用,黑色 = AI 禁用 -- 禁用时自动清除灰色文本并停止 API 调用 - -## 技术架构 - -```mermaid -flowchart TB - subgraph Frontend["前端 (Vue3 + Vite)"] - A[App.vue] --> B[MilkdownEditor.vue] - B --> C[Crepe Editor] - C --> D[ProseMirror] - D --> E[copilotPlugin.ts] - E --> F[copilotGhostMark] - E --> G[api.js] - end - - subgraph Backend["后端 (FastAPI + Python)"] - H[main.pyFastAPI Server] --> I[prompt.pyPrompt 构建] - H --> J[llm.pyOllama 调用] - J --> K[Ollama API] - end - - G -->|POST /v1/completionsSSE 流式响应| H - K -->|LLM 响应| J -``` - -## 项目结构 - -``` -llm-in-text/ -├── src/ -│ ├── components/ -│ │ └── MilkdownEditor.vue # 主编辑器组件 -│ ├── plugins/ -│ │ ├── copilotPlugin.ts # ProseMirror AI 补全插件 -│ │ ├── types.ts # 类型定义 -│ │ └── index.ts # 插件导出 -│ ├── utils/ -│ │ ├── api.js # API 调用封装 -│ │ ├── config.js # 配置文件 -│ │ └── ocrCache.js # OCR 缓存管理 -│ ├── App.vue -│ └── main.js -├── backend/ -│ ├── main.py # FastAPI 服务器 -│ ├── llm.py # LLM API 调用 -│ ├── prompt.py # Prompt 构建 -│ └── requirements.txt -└── README.md -``` - -## 快速开始 - -### 环境要求 -- Node.js 18+ -- Python 3.8+ -- Ollama 服务(或其他兼容 OpenAI API 的服务) - -### 安装 - -```bash -# 前端 -npm install - -# 后端 -cd backend -pip install -r requirements.txt -``` - -### 配置 - -在 `backend/.env` 中配置: - -```env -OLLAMA_MODEL=gpt-oss:20b -OLLAMA_HOST=http://localhost:11434 -``` - -### 启动 - -```bash -# 后端(端口 8000) -cd backend -python main.py - -# 前端(端口 5173) -npm run dev -``` - -访问 http://localhost:5173 - -## API 接口 - -### POST /v1/completions - -流式获取补全建议 - -**请求:** -```json -{ - "prefix": "# Title\n\nContent ", - "suffix": "", - "languageId": "markdown" -} -``` - -**响应(SSE):** -``` -data: {"content": "here"} -data: {"content": "here is"} -data: {"done": true} -``` - -## 核心实现 - -### 后端设计 - -#### main.py - FastAPI 服务器 -- 定义 `/v1/completions` 端点 -- 使用 `StreamingResponse` 返回 SSE 流式响应 -- CORS 配置允许跨域请求 - -#### llm.py - LLM 调用封装 -- 使用 `ollama.AsyncClient` 异步调用 -- 支持 `think='high'` 思考模式 -- 返回 `content` 和 `thinking` 字段 - -#### prompt.py - Prompt 工程 -精心设计的 Prompt 模板,包含 7 条核心规则: - -| 规则 | 说明 | -|------|------| -| RULE #1 | 无缝连接 - 不重复 suffix 内容,避免"复读机"错误 | -| RULE #2 | 空白处理 - 避免双空格,正确对接标点 | -| RULE #3 | 缩进对齐 - 匹配当前缩进级别和类型 | -| RULE #4 | 列表维护 - 识别并继续任务列表、有序列表、无序列表 | -| RULE #5 | 语法闭合 - 自动闭合未完成的 Markdown 语法 | -| RULE #6 | 输出格式 - 仅输出续写文本,无解释无注释 | -| RULE #7 | 必须输出 - 始终提供有用的续写建议 | - -### 前端设计 - -#### ProseMirror Mark 系统 - -使用 ProseMirror 的 Mark 系统实现灰色建议文本: - -```typescript -// 定义 ghost mark -export const copilotGhostMark = $markSchema('copilot_ghost', () => ({ - excludes: '_', - inclusive: true, - toDOM: () => ['span', { - 'data-copilot-ghost': '', - class: 'copilot-ghost-text' - }, 0] -})) - -// CSS 样式 -.copilot-ghost-text { - color: #999; - opacity: 0.6; -} -``` - -#### copilotPlugin 核心逻辑 - -```mermaid -flowchart LR - A[用户输入] --> B{文档变化?} - B -->|是| C[清除旧建议] - C --> D[防抖 1000ms] - D --> E[发送 API 请求] - E --> F[收到建议] - F --> G[插入 Ghost Text] - - G --> H{用户操作} - H -->|Tab| I[接受建议移除 mark] - H -->|Esc| J[拒绝建议删除文本] - H -->|点击 Ghost| I - H -->|继续输入| J -``` - -#### 关键函数 - -| 函数 | 作用 | -|------|------| -| `scheduleFetch` | 防抖调度 API 请求 | -| `insertGhostText` | 插入带 mark 的建议文本 | -| `acceptSuggestion` | Tab 接受建议 | -| `rejectSuggestion` | Esc 拒绝建议 | -| `clearGhostText` | 清除当前建议 | - -### 数据流 - -```mermaid -sequenceDiagram - participant U as 用户 - participant E as Editor (ProseMirror) - participant P as copilotPlugin - participant A as api.js - participant B as Backend - participant L as LLM - - U->>E: 输入文本 - E->>P: view.update() - P->>P: 清除旧建议 - P->>P: 防抖 1000ms - P->>A: fetchSuggestion(prefix, suffix) - A->>B: POST /v1/completions - B->>B: build_prompt() - B->>L: ollama.chat() - L-->>B: {content, thinking} - B-->>A: SSE stream - A-->>P: suggestion text - P->>E: insertGhostText() - E-->>U: 显示灰色建议 - - alt Tab 键 - U->>P: Tab - P->>E: acceptSuggestion() - E-->>U: 建议变为正常文本 - else Esc 键 - U->>P: Esc - P->>E: rejectSuggestion() - E-->>U: 建议消失 - else 继续输入 - U->>E: 输入其他字符 - E->>P: handleKeyDown() - P->>E: clearGhostText() - end -``` - -## 设计亮点 - -1. **前后端分离**:前端只负责渲染和数据回传,后端负责 LLM 调用、Prompt 构建和数据解析 -2. **低延迟优化**:防抖机制 (1000ms) + SSE 流式响应 + AbortController 取消过期请求 -3. **ProseMirror Mark 系统**:与编辑器状态完美集成,支持 Undo/Redo -4. **多种交互方式**:Tab/Esc/点击/输入,用户体验友好 -5. **智能大小限制**:文档超过 32KB 自动禁用 AI 功能 - -## 许可证 - -MIT License diff --git a/README_clean.md b/README_clean.md deleted file mode 100644 index 20c3741..0000000 Binary files a/README_clean.md and /dev/null differ diff --git a/README_correct.md b/README_correct.md deleted file mode 100644 index 20c3741..0000000 Binary files a/README_correct.md and /dev/null differ diff --git a/README_fixed.md b/README_fixed.md deleted file mode 100644 index 20c3741..0000000 Binary files a/README_fixed.md and /dev/null differ diff --git a/README_original.txt b/README_original.txt deleted file mode 100644 index 405a3f0..0000000 --- a/README_original.txt +++ /dev/null @@ -1,265 +0,0 @@ -# LLM in Text - 智能写作助手 - -基于 Vue3 和 FastAPI 的智能 Markdown 编辑器,集成大语言模型(LLM)实时补全建议功能,提供类似 GitHub Copilot 的 Ghost Text 体验。 - -## 功能特性 - -### Markdown 编辑器 -- 基于 Milkdown Crepe 的所见即所得编辑体验 -- 支持完整 Markdown 语法和 LaTeX 公式 -- 导入/导出 Markdown 文件 - -### AI 智能补全 -- 实时生成文本补全建议(灰色显示) -- 流式响应,低延迟体验 -- 多种交互方式: - - **Tab 键**:接受建议 - - **Esc 键**:拒绝建议 - - **点击灰色文本**:接受建议 - - **继续输入**:自动拒绝建议 - -### AI 开关控制 -- 右下角 AI 开关按钮 -- 白色 = AI 启用,黑色 = AI 禁用 -- 禁用时自动清除灰色文本并停止 API 调用 - -## 技术架构 - -```mermaid -flowchart TB - subgraph Frontend["前端 (Vue3 + Vite)"] - A[App.vue] --> B[MilkdownEditor.vue] - B --> C[Crepe Editor] - C --> D[ProseMirror] - D --> E[copilotPlugin.ts] - E --> F[copilotGhostMark] - E --> G[api.js] - end - - subgraph Backend["后端 (FastAPI + Python)"] - H[main.pyFastAPI Server] --> I[prompt.pyPrompt 构建] - H --> J[llm.pyOllama 调用] - J --> K[Ollama API] - end - - G -->|POST /v1/completionsSSE 流式响应| H - K -->|LLM 响应| J -``` - -## 项目结构 - -``` -llm-in-text/ -├── src/ -│ ├── components/ -│ │ └── MilkdownEditor.vue # 主编辑器组件 -│ ├── plugins/ -│ │ ├── copilotPlugin.ts # ProseMirror AI 补全插件 -│ │ ├── types.ts # 类型定义 -│ │ └── index.ts # 插件导出 -│ ├── utils/ -│ │ ├── api.js # API 调用封装 -│ │ ├── config.js # 配置文件 -│ │ └── ocrCache.js # OCR 缓存管理 -│ ├── App.vue -│ └── main.js -├── backend/ -│ ├── main.py # FastAPI 服务器 -│ ├── llm.py # LLM API 调用 -│ ├── prompt.py # Prompt 构建 -│ └── requirements.txt -└── README.md -``` - -## 快速开始 - -### 环境要求 -- Node.js 18+ -- Python 3.8+ -- Ollama 服务(或其他兼容 OpenAI API 的服务) - -### 安装 - -```bash -# 前端 -npm install - -# 后端 -cd backend -pip install -r requirements.txt -``` - -### 配置 - -在 `backend/.env` 中配置: - -```env -OLLAMA_MODEL=gpt-oss:20b -OLLAMA_HOST=http://localhost:11434 -``` - -### 启动 - -```bash -# 后端(端口 8000) -cd backend -python main.py - -# 前端(端口 5173) -npm run dev -``` - -访问 http://localhost:5173 - -## API 接口 - -### POST /v1/completions - -流式获取补全建议 - -**请求:** -```json -{ - "prefix": "# Title\n\nContent ", - "suffix": "", - "languageId": "markdown" -} -``` - -**响应(SSE):** -``` -data: {"content": "here"} -data: {"content": "here is"} -data: {"done": true} -``` - -## 核心实现 - -### 后端设计 - -#### main.py - FastAPI 服务器 -- 定义 `/v1/completions` 端点 -- 使用 `StreamingResponse` 返回 SSE 流式响应 -- CORS 配置允许跨域请求 - -#### llm.py - LLM 调用封装 -- 使用 `ollama.AsyncClient` 异步调用 -- 支持 `think='high'` 思考模式 -- 返回 `content` 和 `thinking` 字段 - -#### prompt.py - Prompt 工程 -精心设计的 Prompt 模板,包含 7 条核心规则: - -| 规则 | 说明 | -|------|------| -| RULE #1 | 无缝连接 - 不重复 suffix 内容,避免"复读机"错误 | -| RULE #2 | 空白处理 - 避免双空格,正确对接标点 | -| RULE #3 | 缩进对齐 - 匹配当前缩进级别和类型 | -| RULE #4 | 列表维护 - 识别并继续任务列表、有序列表、无序列表 | -| RULE #5 | 语法闭合 - 自动闭合未完成的 Markdown 语法 | -| RULE #6 | 输出格式 - 仅输出续写文本,无解释无注释 | -| RULE #7 | 必须输出 - 始终提供有用的续写建议 | - -### 前端设计 - -#### ProseMirror Mark 系统 - -使用 ProseMirror 的 Mark 系统实现灰色建议文本: - -```typescript -// 定义 ghost mark -export const copilotGhostMark = $markSchema('copilot_ghost', () => ({ - excludes: '_', - inclusive: true, - toDOM: () => ['span', { - 'data-copilot-ghost': '', - class: 'copilot-ghost-text' - }, 0] -})) - -// CSS 样式 -.copilot-ghost-text { - color: #999; - opacity: 0.6; -} -``` - -#### copilotPlugin 核心逻辑 - -```mermaid -flowchart LR - A[用户输入] --> B{文档变化?} - B -->|是| C[清除旧建议] - C --> D[防抖 1000ms] - D --> E[发送 API 请求] - E --> F[收到建议] - F --> G[插入 Ghost Text] - - G --> H{用户操作} - H -->|Tab| I[接受建议移除 mark] - H -->|Esc| J[拒绝建议删除文本] - H -->|点击 Ghost| I - H -->|继续输入| J -``` - -#### 关键函数 - -| 函数 | 作用 | -|------|------| -| `scheduleFetch` | 防抖调度 API 请求 | -| `insertGhostText` | 插入带 mark 的建议文本 | -| `acceptSuggestion` | Tab 接受建议 | -| `rejectSuggestion` | Esc 拒绝建议 | -| `clearGhostText` | 清除当前建议 | - -### 数据流 - -```mermaid -sequenceDiagram - participant U as 用户 - participant E as Editor (ProseMirror) - participant P as copilotPlugin - participant A as api.js - participant B as Backend - participant L as LLM - - U->>E: 输入文本 - E->>P: view.update() - P->>P: 清除旧建议 - P->>P: 防抖 1000ms - P->>A: fetchSuggestion(prefix, suffix) - A->>B: POST /v1/completions - B->>B: build_prompt() - B->>L: ollama.chat() - L-->>B: {content, thinking} - B-->>A: SSE stream - A-->>P: suggestion text - P->>E: insertGhostText() - E-->>U: 显示灰色建议 - - alt Tab 键 - U->>P: Tab - P->>E: acceptSuggestion() - E-->>U: 建议变为正常文本 - else Esc 键 - U->>P: Esc - P->>E: rejectSuggestion() - E-->>U: 建议消失 - else 继续输入 - U->>E: 输入其他字符 - E->>P: handleKeyDown() - P->>E: clearGhostText() - end -``` - -## 设计亮点 - -1. **前后端分离**:前端只负责渲染和数据回传,后端负责 LLM 调用、Prompt 构建和数据解析 -2. **低延迟优化**:防抖机制 (1000ms) + SSE 流式响应 + AbortController 取消过期请求 -3. **ProseMirror Mark 系统**:与编辑器状态完美集成,支持 Undo/Redo -4. **多种交互方式**:Tab/Esc/点击/输入,用户体验友好 -5. **智能大小限制**:文档超过 32KB 自动禁用 AI 功能 - -## 许可证 - -MIT License diff --git a/backend/docx2pdf_bridge.cjs b/backend/docx2pdf_bridge.cjs new file mode 100644 index 0000000..063c6b1 --- /dev/null +++ b/backend/docx2pdf_bridge.cjs @@ -0,0 +1,20 @@ +const path = require('path') +const { convert } = require('docx2pdf-converter') + +function main() { + const inputPath = process.argv[2] + const outputPath = process.argv[3] + + if (!inputPath || !outputPath) { + throw new Error('缺少 DOCX 或 PDF 路径') + } + + convert(path.resolve(inputPath), path.resolve(outputPath)) +} + +try { + main() +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)) + process.exit(1) +} diff --git a/backend/main.py b/backend/main.py index 14e68ac..f3bded9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,13 +3,16 @@ import base64 import json import logging import os +import re +import shutil +import subprocess import tempfile import uuid from typing import Optional -from fastapi import FastAPI, HTTPException, Request, Security +from fastapi import FastAPI, HTTPException, Request, Security, File, UploadFile from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import JSONResponse, StreamingResponse, Response from fastapi.security import APIKeyHeader from pydantic import BaseModel @@ -81,6 +84,32 @@ class ConvertRequest(BaseModel): filename: str = "document.pdf" +ALLOWED_CONVERT_EXTENSIONS = {".txt", ".docx", ".pptx", ".pdf"} +IMAGE_MARKDOWN_RE = re.compile(r"!\[[^\]]*]\([^)]+\)") +IMAGE_HTML_RE = re.compile(r"]*>", re.IGNORECASE) + + +def _convert_docx_to_pdf(input_path: str, output_path: str) -> None: + node_executable = shutil.which("node") + if not node_executable: + raise RuntimeError("未找到 Node.js,无法转换 DOCX 为 PDF") + + bridge_path = os.path.join(os.path.dirname(__file__), "docx2pdf_bridge.cjs") + if not os.path.exists(bridge_path): + raise RuntimeError("缺少 DOCX 转 PDF 桥接脚本") + + result = subprocess.run( + [node_executable, bridge_path, input_path, output_path], + cwd=os.path.dirname(os.path.dirname(__file__)), + capture_output=True, + text=True, + ) + + if result.returncode != 0: + error_text = (result.stderr or result.stdout or "DOCX 转 PDF 失败").strip() + raise RuntimeError(error_text) + + def _preview(text: str, limit: int = 80) -> str: value = (text or "").replace("\n", "\\n") if len(value) <= limit: @@ -88,6 +117,14 @@ def _preview(text: str, limit: int = 80) -> str: return value[:limit] + "..." +def _sanitize_converted_markdown(text: str) -> str: + value = (text or "").replace("\r\n", "\n").replace("\r", "\n") + value = IMAGE_MARKDOWN_RE.sub("", value) + value = IMAGE_HTML_RE.sub("", value) + value = re.sub(r"\n{3,}", "\n\n", value) + return value.strip() + + def _sse_payload(payload: dict) -> str: return f"data: {json.dumps(payload)}\n\n" @@ -253,9 +290,9 @@ async def ocr_image(request: OCRRequest, api_key: str = Security(get_api_key)): @app.post("/v1/convert") async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(get_api_key)): - """鐏忓棙鏋冩禒鎯版祮閹诡澀璐烳arkdown閺嶇厧绱?"" + """Convert file to markdown""" request_id = str(uuid.uuid4())[:8] - + try: logger.info( "[%s] /v1/convert filename=%s file_base64_chars=%d", @@ -263,53 +300,106 @@ async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(g request.filename, len(request.file or ""), ) - - # 鐟欙絿鐖淏ase64閺傚洣娆㈤崘鍛啇 + + # Decode base64 file_bytes = base64.b64decode(request.file) logger.info("[%s] /v1/convert decoded file_bytes=%d", request_id, len(file_bytes)) - - # 閼惧嘲褰囬弬鍥︽閹碘晛鐫嶉崥? + + # Get file extension ext = os.path.splitext(request.filename)[1].lower() - - # 閸掓稑缂撴稉瀛樻閺傚洣娆? + + if ext not in ALLOWED_CONVERT_EXTENSIONS: + raise ValueError("仅支持 txt、docx、pptx、pdf 格式") + + if ext == ".txt": + markdown_text = _sanitize_converted_markdown(file_bytes.decode("utf-8", errors="ignore")) + return { + "markdown": markdown_text, + "filename": request.filename + } + + # Create temporary file with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp: tmp.write(file_bytes) tmp_path = tmp.name - + try: - # 娴h法鏁arkItDown鏉烆剚宕叉稉绡梐rkdown + # Convert using MarkItDown md = markitdown.MarkItDown() result = md.convert(tmp_path) - markdown_text = result.text_content - + markdown_text = _sanitize_converted_markdown(result.text_content) + logger.info( "[%s] /v1/convert success text_chars=%d text_preview='%s'", request_id, len(markdown_text or ""), _preview(markdown_text, 120), ) - + return { "markdown": markdown_text, "filename": request.filename } finally: - # 濞撳懐鎮婃稉瀛樻閺傚洣娆? + # Clean up temporary file if os.path.exists(tmp_path): os.unlink(tmp_path) - + except Exception as e: logger.exception("[%s] /v1/convert failed: %s", request_id, e) return JSONResponse(content={"error": str(e)}, status_code=500) + + + +@app.post("/v1/export/pdf") +async def export_pdf(file: UploadFile = File(...), api_key: str = Security(get_api_key)): + request_id = str(uuid.uuid4())[:8] + original_name = file.filename or "document.docx" + base_name = os.path.splitext(original_name)[0] or "document" + + try: + file_bytes = await file.read() + logger.info( + "[%s] /v1/export/pdf filename=%s file_bytes=%d", + request_id, + original_name, + len(file_bytes), + ) + + with tempfile.TemporaryDirectory() as temp_dir: + input_path = os.path.join(temp_dir, f"{base_name}.docx") + output_path = os.path.join(temp_dir, f"{base_name}.pdf") + + with open(input_path, "wb") as tmp_file: + tmp_file.write(file_bytes) + + await asyncio.to_thread(_convert_docx_to_pdf, input_path, output_path) + + if not os.path.exists(output_path): + raise RuntimeError("PDF 转换后未生成输出文件") + + with open(output_path, "rb") as pdf_file: + pdf_bytes = pdf_file.read() + + logger.info("[%s] /v1/export/pdf success pdf_bytes=%d", request_id, len(pdf_bytes)) + headers = { + "Content-Disposition": f'attachment; filename="{base_name}.pdf"', + } + return Response(content=pdf_bytes, media_type="application/pdf", headers=headers) + except Exception as e: + logger.exception("[%s] /v1/export/pdf failed: %s", request_id, e) + return JSONResponse(content={"error": str(e)}, status_code=500) + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001) -# TTS and STT routes +# TTS and ASR routes from tts_asr import register_tts_asr_routes register_tts_asr_routes(app) diff --git a/backend/tts_asr.py b/backend/tts_asr.py new file mode 100644 index 0000000..0acc06f --- /dev/null +++ b/backend/tts_asr.py @@ -0,0 +1,255 @@ +# TTS and ASR API for macOS Silicon with HuggingFace transformers +import asyncio +import base64 +import logging +import os +import platform + +from fastapi import APIRouter, HTTPException, Security +from pydantic import BaseModel +import numpy as np + +router = APIRouter() +logger = logging.getLogger("tts_asr") + +_tts_pipeline = None +_asr_pipeline = None +_device = None + + +def _get_device(): + global _device + if _device is not None: + return _device + + import torch + + if platform.system() == "Darwin" and hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + _device = "mps" + logger.info("[Device] 使用 MPS 加速") + elif torch.cuda.is_available(): + _device = "cuda" + logger.info("[Device] 使用 CUDA 加速") + else: + _device = "cpu" + logger.info("[Device] 使用 CPU") + return _device + + +def _device_arg(): + device = _get_device() + if device == "cuda": + return "cuda:0" + return device + + +def _get_tts_pipeline(): + global _tts_pipeline + if _tts_pipeline is not None: + return _tts_pipeline + + import torch + from transformers import pipeline + + logger.info("[TTS] 加载 Kokoro-82M 模型...") + _tts_pipeline = pipeline( + "text-to-speech", + model="hexgrad/Kokoro-82M", + trust_remote_code=True, + device=_device_arg(), + torch_dtype=torch.float16 if _get_device() != "cpu" else torch.float32, + ) + logger.info("[TTS] Kokoro-82M 模型加载完成") + return _tts_pipeline + + +def _get_asr_pipeline(): + global _asr_pipeline + if _asr_pipeline is not None: + return _asr_pipeline + + import torch + from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline + + logger.info("[ASR] 加载 Whisper large-v3-turbo 模型...") + model_id = "openai/whisper-large-v3-turbo" + model = AutoModelForSpeechSeq2Seq.from_pretrained( + model_id, + torch_dtype=torch.float16 if _get_device() != "cpu" else torch.float32, + low_cpu_mem_usage=True, + use_safetensors=True, + ) + processor = AutoProcessor.from_pretrained(model_id) + _asr_pipeline = pipeline( + "automatic-speech-recognition", + model=model, + tokenizer=processor.tokenizer, + feature_extractor=processor.feature_extractor, + torch_dtype=torch.float16 if _get_device() != "cpu" else torch.float32, + device=_device_arg(), + ) + logger.info("[ASR] Whisper large-v3-turbo 模型加载完成") + return _asr_pipeline + + +def _save_audio_to_wav(audio_data: bytes, sample_rate: int = 16000) -> str: + import tempfile + import wave + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False, mode="wb") as tmp: + with wave.open(tmp.name, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(audio_data) + return tmp.name + + +def _tts_sync(text: str, voice: str = "af_bella", rate: float = 1.0) -> tuple[bytes, int]: + tts = _get_tts_pipeline() + result = tts(text, voice=voice) + audio = None + sample_rate = 24000 + if isinstance(result, dict): + audio = result.get("audio") + sample_rate = int(result.get("sampling_rate", sample_rate)) + elif isinstance(result, (list, tuple)) and result: + audio = result[0] + + if audio is None: + raise RuntimeError("Kokoro 未返回音频数据") + + if hasattr(audio, "cpu"): + audio = audio.cpu().numpy() + + duration_ms = int(len(audio) * 1000 / sample_rate) + + if audio.dtype != np.int16: + audio = (audio * 32767).astype(np.int16) + + import tempfile + import wave + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + output_path = tmp.name + try: + with wave.open(output_path, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(audio.tobytes()) + with open(output_path, "rb") as f: + return f.read(), duration_ms + finally: + if os.path.exists(output_path): + os.unlink(output_path) + + +async def _text_to_speech(text: str, voice: str = "af_bella", rate: float = 1.0) -> tuple[bytes, int]: + return await asyncio.to_thread(_tts_sync, text, voice, rate) + + +def _asr_sync(audio_data: bytes, language: str = "zh") -> str: + import soundfile as sf + + asr = _get_asr_pipeline() + audio_path = _save_audio_to_wav(audio_data) + try: + audio_array, sample_rate = sf.read(audio_path) + result = asr( + audio_array, + sampling_rate=sample_rate, + generate_kwargs={"language": language, "task": "transcribe"}, + ) + if isinstance(result, dict): + return result.get("text", "").strip() + return str(result).strip() + finally: + if os.path.exists(audio_path): + os.unlink(audio_path) + + +async def _speech_to_text(audio_data: bytes, language: str = "zh") -> str: + return await asyncio.to_thread(_asr_sync, audio_data, language) + + +class TTSRequest(BaseModel): + text: str + voice: str = "af_bella" + rate: float = 1.0 + format: str = "wav" + + +class TTSResponse(BaseModel): + audio_base64: str + format: str + duration_ms: int + + +class ASRRequest(BaseModel): + audio_base64: str + language: str = "zh-CN" + + +class ASRResponse(BaseModel): + text: str + language: str + + +def get_api_key(api_key: str): + from backend.main import API_KEY + + if api_key != API_KEY: + raise HTTPException(status_code=403, detail="API Key 无效") + return api_key + + +@router.post("/tts", response_model=TTSResponse) +async def text_to_speech(req: TTSRequest, api_key: str = Security(get_api_key)): + request_id = str(hash(req.text))[:8] + try: + logger.info("[TTS][%s] text_chars=%d voice=%s format=%s", request_id, len(req.text), req.voice, req.format) + audio_data, duration_ms = await _text_to_speech(req.text, req.voice, req.rate) + if req.format.lower() == "mp3": + import subprocess + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_in: + tmp_in.write(audio_data) + input_path = tmp_in.name + with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_out: + output_path = tmp_out.name + try: + cmd = ["ffmpeg", "-i", input_path, "-acodec", "libmp3lame", "-ab", "128k", output_path] + result = await asyncio.to_thread(lambda: subprocess.run(cmd, capture_output=True, text=True, timeout=30)) + if result.returncode != 0: + raise RuntimeError(f"MP3 转换失败: {result.stderr}") + with open(output_path, "rb") as f: + audio_data = f.read() + finally: + for path in [input_path, output_path]: + if os.path.exists(path): + os.unlink(path) + logger.info("[TTS][%s] success duration_ms=%d", request_id, duration_ms) + return TTSResponse(audio_base64=base64.b64encode(audio_data).decode(), format=req.format, duration_ms=duration_ms) + except Exception as e: + logger.exception("[TTS] failed: %s", e) + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/asr", response_model=ASRResponse) +async def speech_to_text(req: ASRRequest, api_key: str = Security(get_api_key)): + request_id = str(hash(req.audio_base64))[:8] + try: + logger.info("[ASR][%s] audio_base64_chars=%d language=%s", request_id, len(req.audio_base64), req.language) + audio_data = base64.b64decode(req.audio_base64) + text = await _speech_to_text(audio_data, req.language[:2]) + logger.info("[ASR][%s] success text_chars=%d", request_id, len(text)) + return ASRResponse(text=text, language=req.language) + except Exception as e: + logger.exception("[ASR] failed: %s", e) + raise HTTPException(status_code=500, detail=str(e)) + + +def register_tts_asr_routes(app): + app.include_router(router, prefix="/v1/tts-asr") diff --git a/backend/tts_stt.py b/backend/tts_stt.py deleted file mode 100644 index 33b0426..0000000 --- a/backend/tts_stt.py +++ /dev/null @@ -1,141 +0,0 @@ -# TTS and Speech Recognition API for macOS Silicon -import os -import asyncio -import logging -import base64 -from typing import Optional -from fastapi import APIRouter, UploadFile, File, HTTPException, Security -from pydantic import BaseModel -from fastapi.security import APIKeyHeader - -router = APIRouter() -api_key_header = APIKeyHeader(name="X-API-Key") -logger = logging.getLogger("tts_stt") - - -def _speak_text_macos(text: str, voice: str = "meijia", rate: float = 0.5) -> bytes: - import subprocess - import tempfile - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: - output_path = tmp.name - try: - cmd = ["say", "-v", voice, "-r", str(rate * 10), "--output-format", "WAVE", "-o", output_path, text] - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - if result.returncode != 0: - raise Exception(f"TTS failed: {result.stderr}") - with open(output_path, "rb") as f: - audio_data = f.read() - return audio_data - finally: - if os.path.exists(output_path): - os.unlink(output_path) - - -async def _speak_text_macos_async(text: str, voice: str = "meijia", rate: float = 0.5) -> bytes: - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, _speak_text_macos, text, voice, rate) - - -def _recognize_speech_macos(audio_data: bytes, language: str = "zh-CN") -> str: - import tempfile - try: - import whisper - model = whisper.load_model("tiny") - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp2: - tmp2.write(audio_data) - audio_for_whisper = tmp2.name - try: - result = model.transcribe(audio_for_whisper, language=language[:2]) - return result["text"] - finally: - if os.path.exists(audio_for_whisper): - os.unlink(audio_for_whisper) - except ImportError: - raise Exception("Whisper is required for speech recognition on macOS") - - -async def _recognize_speech_macos_async(audio_data: bytes, language: str = "zh-CN") -> str: - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, _recognize_speech_macos, audio_data, language) - - -class TTSRequest(BaseModel): - text: str - voice: str = "meijia" - rate: float = 0.5 - format: str = "wav" - - -class TTSResponse(BaseModel): - audio_base64: str - format: str - duration_ms: int - - -class STTRequest(BaseModel): - audio_base64: str - language: str = "zh-CN" - - -class STTResponse(BaseModel): - text: str - language: str - - -@router.post("/tts", response_model=TTSResponse) -async def text_to_speech(req: TTSRequest, api_key: str = Security(get_api_key)): - request_id = str(hash(req.text))[:8] - try: - logger.info("[TTS][%s] text_chars=%d voice=%s", request_id, len(req.text), req.voice) - audio_data = await _speak_text_macos_async(req.text, req.voice, req.rate) - if req.format.lower() == "mp3": - import tempfile - import subprocess - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_in: - tmp_in.write(audio_data) - input_path = tmp_in.name - with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_out: - output_path = tmp_out.name - try: - cmd = ["ffmpeg", "-i", input_path, "-acodec", "libmp3lame", output_path] - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - if result.returncode != 0: - raise Exception(f"MP3 conversion failed: {result.stderr}") - with open(output_path, "rb") as f: - audio_data = f.read() - finally: - for p in [input_path, output_path]: - if os.path.exists(p): - os.unlink(p) - duration_ms = len(audio_data) * 1000 // 16000 - logger.info("[TTS][%s] success duration_ms=%d", request_id, duration_ms) - return TTSResponse(audio_base64=base64.b64encode(audio_data).decode(), format=req.format, duration_ms=duration_ms) - except Exception as e: - logger.exception("[TTS] failed: %s", e) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/stt", response_model=STTResponse) -async def speech_to_text(req: STTRequest, api_key: str = Security(get_api_key)): - request_id = str(hash(req.audio_base64))[:8] - try: - logger.info("[STT][%s] audio_base64_chars=%d language=%s", request_id, len(req.audio_base64), req.language) - audio_data = base64.b64decode(req.audio_base64) - text = await _recognize_speech_macos_async(audio_data, req.language) - logger.info("[STT][%s] success text_chars=%d", request_id, len(text)) - return STTResponse(text=text, language=req.language) - except Exception as e: - logger.exception("[STT] failed: %s", e) - raise HTTPException(status_code=500, detail=str(e)) - - -def get_api_key(api_key: str): - from backend.main import API_KEY - if api_key != API_KEY: - from fastapi import HTTPException - raise HTTPException(status_code=403, detail="Could not validate credentials") - return api_key - - -def register_tts_stt_routes(app): - app.include_router(router, prefix="/v1/tts-stt") \ No newline at end of file diff --git a/original_readme.md b/original_readme.md deleted file mode 100644 index 20c3741..0000000 Binary files a/original_readme.md and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 7c0df9c..eb1a7f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,17 @@ "name": "llm-in-text", "version": "0.0.0", "dependencies": { + "@blocknote/xl-docx-exporter": "^0.47.3", "@milkdown/core": "^7.18.0", "@milkdown/crepe": "^7.18.0", "@milkdown/kit": "^7.18.0", "@milkdown/theme-nord": "^7.18.0", "@milkdown/vue": "^7.18.0", "docx": "^9.6.0", + "docx-preview": "^0.3.7", + "docx2pdf-converter": "^2.1.1", "html2pdf.js": "^0.14.0", + "jspdf": "^4.2.1", "katex": "^0.16.9", "markdown-it": "^13.0.0", "markdown-it-math": "^3.0.2", @@ -97,6 +101,126 @@ "node": ">=6.9.0" } }, + "node_modules/@blocknote/core": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/core/-/core-0.47.3.tgz", + "integrity": "sha512-+YIOEXmmRXzDULYlQaycaFDofwQ/W79CWsU3GwNuRzp62QC+WpRLFLkgkoU4Pb1Vo7RvmzU1+udESdNUGhO2KA==", + "license": "MPL-2.0", + "dependencies": { + "@emoji-mart/data": "^1.2.1", + "@handlewithcare/prosemirror-inputrules": "^0.1.4", + "@shikijs/types": "^3", + "@tanstack/store": "^0.7.7", + "@tiptap/core": "^3.13.0", + "@tiptap/extension-bold": "^3.13.0", + "@tiptap/extension-code": "^3.13.0", + "@tiptap/extension-horizontal-rule": "^3.13.0", + "@tiptap/extension-italic": "^3.13.0", + "@tiptap/extension-link": "^3.13.0", + "@tiptap/extension-paragraph": "^3.13.0", + "@tiptap/extension-strike": "^3.13.0", + "@tiptap/extension-text": "^3.13.0", + "@tiptap/extension-underline": "^3.13.0", + "@tiptap/extensions": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "emoji-mart": "^5.6.0", + "fast-deep-equal": "^3.1.3", + "hast-util-from-dom": "^5.0.1", + "prosemirror-highlight": "^0.13.0", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.3", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4", + "rehype-format": "^5.0.1", + "rehype-parse": "^9.0.1", + "rehype-remark": "^10.0.1", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "uuid": "^8.3.2", + "y-prosemirror": "^1.3.7", + "y-protocols": "^1.0.6", + "yjs": "^13.6.27" + } + }, + "node_modules/@blocknote/core/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@blocknote/react": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/react/-/react-0.47.3.tgz", + "integrity": "sha512-aWDgfnf/ufgqYfXtIy8QepfrgdVaC5DfU/BAWbazb/O5ucEFfCr6lK0SUVvswK1CkxL/KBpHpdAd+uT+rfgkmg==", + "license": "MPL-2.0", + "dependencies": { + "@blocknote/core": "0.47.3", + "@emoji-mart/data": "^1.2.1", + "@floating-ui/react": "^0.27.18", + "@floating-ui/utils": "^0.2.10", + "@tanstack/react-store": "0.7.7", + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@types/use-sync-external-store": "1.5.0", + "emoji-mart": "^5.6.0", + "fast-deep-equal": "^3.1.3", + "lodash.merge": "^4.6.2", + "react-icons": "^5.5.0", + "use-sync-external-store": "1.6.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } + }, + "node_modules/@blocknote/xl-docx-exporter": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/xl-docx-exporter/-/xl-docx-exporter-0.47.3.tgz", + "integrity": "sha512-HE8JkQQ+G7wBiejFmyNlk04uUfSg6WMsKXPIHtrXK38kwXzlRTq/hu0lziZ990xc/G02/yETZNm/K1qpV9xmzA==", + "license": "GPL-3.0 OR PROPRIETARY", + "dependencies": { + "@blocknote/core": "0.47.3", + "@blocknote/xl-multi-column": "0.47.3", + "buffer": "^6.0.3", + "docx": "^9.5.1", + "image-meta": "^0.2.2" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } + }, + "node_modules/@blocknote/xl-multi-column": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/xl-multi-column/-/xl-multi-column-0.47.3.tgz", + "integrity": "sha512-gK0dOXJFvwVQi3Esee0vNbk+DsSxDub0BKLDwdUBDyfk+OkS6I82MUSAmEyvCiJG0G9Vo+hSaCMduX0YdgzYAQ==", + "license": "GPL-3.0 OR PROPRIETARY", + "dependencies": { + "@blocknote/core": "0.47.3", + "@blocknote/react": "0.47.3", + "@tiptap/core": "^3.13.0", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.3", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4", + "react-icons": "^5.5.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", @@ -567,6 +691,12 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@emoji-mart/data": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", + "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz", @@ -1023,17 +1153,61 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/utils": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@handlewithcare/prosemirror-inputrules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@handlewithcare/prosemirror-inputrules/-/prosemirror-inputrules-0.1.4.tgz", + "integrity": "sha512-GMqlBeG2MKM+tXEFd2N+wIv5z4VvJTg8JtfJUrdjvFq2W6v+AW8oTgiWyFw8L3iEQwvtQcVJxU873iB0LXUNNw==", + "license": "MIT", + "dependencies": { + "prosemirror-history": "^1.4.1", + "prosemirror-transform": "^1.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", @@ -1602,6 +1776,12 @@ "url": "https://github.com/sponsors/ocavue" } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.2", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", @@ -1959,6 +2139,299 @@ "win32" ] }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tanstack/react-store": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.7.tgz", + "integrity": "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.7", + "use-sync-external-store": "^1.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz", + "integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tiptap/core": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.2.tgz", + "integrity": "sha512-atq35NkpeEphH6vNYJ0pTLLBA73FAbvTV9Ovd3AaTC5s99/KF5Q86zVJXvml8xPRcMGM6dLp+eSSd06oTscMSA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.2.tgz", + "integrity": "sha512-bqsPJyKcT/RWse4e16U2EKhraR8a2+98TUuk1amG3yCyFJZStoO/j+pN0IqZdZZjr3WtxFyvwWp7Kc59UN+jUA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.2.tgz", + "integrity": "sha512-5hbyDOSkJwA2uh0v9Mm0Dd9bb9inx6tHBEDSH2tCB9Rm23poz3yOreB7SNX8xDMe5L0/PQesfWC14RitcmhKPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.2.tgz", + "integrity": "sha512-iYFY+yzfYA9MKt7nupyW/PzqL9XC2D0mC8l1z2Y10i0/fGL8NbqIYjhNUAyXGqH3QWcI+DirI66842y2OadPOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.2.tgz", + "integrity": "sha512-r0ZTeh9rNtj9Api+G0YyaB+tAKPDn7aYWg+qSrmAC5EyUPee6Zjn3zlw0q4renCeQflvNRK20xHM8zokC41jOA==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.2.tgz", + "integrity": "sha512-Oz8KN5KJAWV1mFNE9UIWXdMD6xa5zPf/0yLsT8V4sgaRm+VsdFKllN58BY9qCZf/kIZbaOez5KkaoeAcm0MAZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.2.tgz", + "integrity": "sha512-fmtQu2HDnV3sOZPdz0+1lOLI7UtrIhusohJj2UwOLQxG8qqhLwbvWx2OQTlfblgY0z+CjLRr6ANbNDxOTIblfg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.2.tgz", + "integrity": "sha512-TXfSoKmng5pecvQUZqdsx6ICeob5V5hhYOj2vCEtjfcjWsyCndqFIl1w+Nt/yI5ehrFNOVPyj3ZvcELuuAW6pw==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.2.tgz", + "integrity": "sha512-EHZZzxVhvzEPDPWtRBF1YKhB+WCUjd1C2NhjHfL3Dl71PBqM3ZWA6qN7NDGPyNyGGWauui/NR/4X+5AfPqlHyA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.2.tgz", + "integrity": "sha512-YFC3elKU1L8PiGbcB6tqd/7vWPF5IbydJz0POJpHzSjstX+VfT8VsvS7ubxVuSIWQ11kGkH3mzX6LX8JHsHZxg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.2.tgz", + "integrity": "sha512-J1w7JwijfSD7ah0WfiwZ/DVWCIGT9x369RM4RJc57i44mIBElj7tl1dh+N5KPGOXKUup4gr7sSJAE38lgeaDMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.2.tgz", + "integrity": "sha512-BaV6WOowxdkGTLWiU7DdZ3Twh633O4RGqwUM5dDas5LvaqL8AMWGTO8Wg9yAaaKXzd9MtKI1ZCqS/+MtzusgkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.2.tgz", + "integrity": "sha512-s7MZmm2Xdq+8feIXgY3v7gVpQ5ClqBZi20KheouS7KSbBlrY4fu2irYR1EGc6r1UUVaHMxEa+cx5knhx+mIPUw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.2.tgz", + "integrity": "sha512-G2ENwIazoSKkAnN5MN5yN91TIZNFm6TxB74kPf3Empr2k9W51Hkcier70jHGpArhgcEaL4BVreuU1PRDRwCeGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.22.2.tgz", + "integrity": "sha512-tyGKG69e/MkpoD/JTpVPz0XydEHxh1MSAYnLb3gRvyvBDv2r/veLea+cApkmjQaCfkKC/CWwTFXBYlOB0caSBA==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.22.2", + "@tiptap/extension-floating-menu": "^3.22.2" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/react/node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -2249,6 +2722,12 @@ "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", @@ -2264,6 +2743,16 @@ "@types/lodash": "*" } }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2273,6 +2762,12 @@ "@types/unist": "*" } }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2301,6 +2796,26 @@ "license": "MIT", "optional": true }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2314,6 +2829,18 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-5dyB8nLC/qogMrlCizZnYWQTA4lnb/v+It+sqNl5YnSRAPMlIqY/X0Xn+gZw8vOL+TgTTr28VEbn3uf8fUtAkw==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@upsetjs/venn.js": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", @@ -2471,6 +2998,15 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -2950,6 +3486,26 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -3010,6 +3566,30 @@ "license": "MIT", "optional": true }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -3122,6 +3702,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chevrotain": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", @@ -3277,6 +3877,16 @@ "node": ">=0.10.0" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -4163,6 +4773,24 @@ "node": ">=10" } }, + "node_modules/docx-preview": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.3.7.tgz", + "integrity": "sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==", + "license": "Apache-2.0", + "dependencies": { + "jszip": ">=3.0.0" + } + }, + "node_modules/docx2pdf-converter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/docx2pdf-converter/-/docx2pdf-converter-2.1.1.tgz", + "integrity": "sha512-XUxY4D6HGuWVdOXuEygko1TyA1y2vejqY6iv7KrKT2St99UxreokeP/ic70XcGQuZRXTwWo5GY0haiq9HCHU4Q==", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.16" + } + }, "node_modules/dompurify": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", @@ -4172,6 +4800,12 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", + "license": "MIT" + }, "node_modules/entities": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", @@ -4351,6 +4985,21 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-png": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", @@ -4691,6 +5340,273 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-mdast": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.2.tgz", + "integrity": "sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/home-or-tmp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-1.0.0.tgz", @@ -4705,6 +5621,26 @@ "node": ">=0.10.0" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -4741,6 +5677,32 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-meta": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.2.tgz", + "integrity": "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==", + "license": "MIT" + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -5042,6 +6004,16 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/js-tokens": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.1.tgz", @@ -5196,6 +6168,27 @@ "node": ">=0.10.0" } }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -5214,6 +6207,12 @@ "uc.micro": "^1.0.1" } }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/lodash": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", @@ -5227,6 +6226,12 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -5534,6 +6539,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", @@ -6604,6 +7630,30 @@ "node": ">=0.10.0" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -6820,6 +7870,16 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/prosemirror-changeset": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", @@ -6829,6 +7889,15 @@ "prosemirror-transform": "^1.0.0" } }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, "node_modules/prosemirror-commands": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", @@ -6878,6 +7947,59 @@ "prosemirror-view": "^1.0.0" } }, + "node_modules/prosemirror-highlight": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/prosemirror-highlight/-/prosemirror-highlight-0.13.1.tgz", + "integrity": "sha512-41EwMJDUeFBxizPP1/msQBjDke1YyaTy40w3CGoc7fjXboDBgyhz2LWThwaygL9LkDvBqn4pwBJg1PNtrNilGg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ocavue" + }, + "peerDependencies": { + "@shikijs/types": "^1.29.2 || ^2.0.0 || ^3.0.0", + "@types/hast": "^3.0.0", + "highlight.js": "^11.9.0", + "lowlight": "^3.1.0", + "prosemirror-model": "^1.19.3", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.8.0", + "prosemirror-view": "^1.32.4", + "refractor": "^5.0.0", + "sugar-high": "^0.6.1 || ^0.7.0 || ^0.8.0 || ^0.9.0" + }, + "peerDependenciesMeta": { + "@shikijs/types": { + "optional": true + }, + "@types/hast": { + "optional": true + }, + "highlight.js": { + "optional": true + }, + "lowlight": { + "optional": true + }, + "prosemirror-model": { + "optional": true + }, + "prosemirror-state": { + "optional": true + }, + "prosemirror-transform": { + "optional": true + }, + "prosemirror-view": { + "optional": true + }, + "refractor": { + "optional": true + }, + "sugar-high": { + "optional": true + } + } + }, "node_modules/prosemirror-history": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", @@ -6910,6 +8032,79 @@ "w3c-keyname": "^2.2.0" } }, + "node_modules/prosemirror-markdown": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-markdown/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/prosemirror-markdown/node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/prosemirror-markdown/node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/prosemirror-markdown/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/prosemirror-markdown/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/prosemirror-menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz", + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, "node_modules/prosemirror-model": { "version": "1.25.4", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", @@ -6933,6 +8128,15 @@ "url": "https://github.com/sponsors/ocavue" } }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, "node_modules/prosemirror-schema-list": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", @@ -6969,6 +8173,33 @@ "prosemirror-view": "^1.41.4" } }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-trailing-node/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prosemirror-transform": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", @@ -7015,6 +8246,15 @@ } } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -7072,6 +8312,38 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -7477,6 +8749,81 @@ "regjsparser": "bin/parser" } }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-minify-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.2.tgz", + "integrity": "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.1.tgz", + "integrity": "sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", @@ -7558,6 +8905,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", @@ -7799,6 +9163,12 @@ "node": ">=11.0.0" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -8042,6 +9412,16 @@ "license": "MIT", "optional": true }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -8123,6 +9503,20 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringmap": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz", @@ -8195,6 +9589,12 @@ "node": ">=12.0.0" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -8303,6 +9703,16 @@ "node": ">=0.10.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", @@ -8313,6 +9723,16 @@ "node": ">=0.10.0" } }, + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -8400,6 +9820,20 @@ "node": ">=0.10.0" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -8413,6 +9847,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-remove-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", @@ -8549,6 +9996,15 @@ "node": ">=0.10.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/user-home": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", @@ -8604,6 +10060,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", @@ -8827,6 +10297,16 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/window-size": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", @@ -8875,6 +10355,51 @@ "xml-js": "bin/cli.js" } }, + "node_modules/y-prosemirror": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.3.7.tgz", + "integrity": "sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.109" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" + } + }, + "node_modules/y-protocols": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", + "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y18n": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", @@ -8897,6 +10422,24 @@ "y18n": "^3.2.0" } }, + "node_modules/yjs": { + "version": "13.6.30", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz", + "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index e8f7817..a7dd887 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,17 @@ "check": "npm run build" }, "dependencies": { + "@blocknote/xl-docx-exporter": "^0.47.3", "@milkdown/core": "^7.18.0", "@milkdown/crepe": "^7.18.0", "@milkdown/kit": "^7.18.0", "@milkdown/theme-nord": "^7.18.0", "@milkdown/vue": "^7.18.0", "docx": "^9.6.0", + "docx-preview": "^0.3.7", + "docx2pdf-converter": "^2.1.1", "html2pdf.js": "^0.14.0", + "jspdf": "^4.2.1", "katex": "^0.16.9", "markdown-it": "^13.0.0", "markdown-it-math": "^3.0.2", diff --git a/src/components/DocBlockCrepe.vue b/src/components/DocBlockCrepe.vue index 88e9583..e1e7622 100644 --- a/src/components/DocBlockCrepe.vue +++ b/src/components/DocBlockCrepe.vue @@ -1,250 +1,329 @@ - + + - - + + - + - - - + - - - - + + + + + - - + + - {{ docName }} + + {{ docName }} + {{ typeLabel }} · {{ displayTime }} + - - - - - - - - - + + + + + + + + + + + - + diff --git a/src/components/MilkdownEditor.vue b/src/components/MilkdownEditor.vue index 794f912..91e1124 100644 --- a/src/components/MilkdownEditor.vue +++ b/src/components/MilkdownEditor.vue @@ -35,8 +35,9 @@ @@ -45,7 +46,7 @@ {{ t('uploadFile') }} - + - - - - - - - {{ t('exportMd') }} - - + + + + + + + + + {{ t('exportMd') }} + + + { exportMarkdown(); showExportDropdown = false; }">{{ t('exportMd') }} + { exportDocx(); showExportDropdown = false; }">{{ t('exportDocx') }} + { exportPdf(); showExportDropdown = false; }">{{ t('exportPdf') }} + + contentSize.value > SIZE_LIMIT) const sizeInKB = computed(() => Math.floor(contentSize.value / 1024)) const undoLabel = computed(() => t('undo') || 'Undo') const redoLabel = computed(() => t('redo') || 'Redo') const cameraUploadLabel = computed(() => t('cameraUpload') || 'Use Camera') +const API_KEY = 'your-secret-key-here' const supportsCameraCapture = computed(() => { if (typeof navigator === 'undefined') return false const ua = navigator.userAgent || '' @@ -192,47 +206,111 @@ const aiButtonLabel = computed(() => { if (isOverLimit.value) return t('docTooLarge') return aiEnabled.value ? t('disableAI') : t('enableAI') }) +const docUploadButtonTitle = computed(() => { + if (isDocUploadDisabled.value) return t('uploadDocInBlockWarning') || '当前光标位置不能插入文件' + return t('uploadFile') +}) let crepe = null let markdownSyncTimer = null let rootResizeObserver = null +let editorCopyHandler = null const objectUrls = new Set() const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock']) const MARKDOWN_EXT_RE = /\.md$/i const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|bmp|svg|heic|heif|avif)$/i -const CONVERT_EXT_RE = /\.(docx?|pptx?|pdf|zip)$/i -const TEXT_EXT_RE = /\.(txt|json)$/i -const TEXT_MIME_TYPES = new Set(['text/plain', 'application/json']) +const CONVERT_EXT_RE = /\.(docx|pptx|pdf)$/i +const TEXT_EXT_RE = /\.txt$/i +const TEXT_MIME_TYPES = new Set(['text/plain']) const CONVERT_MIME_TYPES = new Set([ - 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/pdf', - 'application/zip', - 'application/x-zip-compressed', ]) -let lastInitialMarkdown = initialMarkdown.value +let lastInitialMarkdown = transformSpecialDocBlocksToLegacy(initialMarkdown.value) const normalizeTrailingWhitespace = (value) => (value || '').replace(/\s+$/, '') +const padTimePart = (value) => String(value).padStart(2, '0') + +const createExportName = () => { + const now = new Date() + const datePart = `${now.getFullYear()}${padTimePart(now.getMonth() + 1)}${padTimePart(now.getDate())}` + const timePart = `${padTimePart(now.getHours())}${padTimePart(now.getMinutes())}${padTimePart(now.getSeconds())}` + return `save${datePart}${timePart}` +} + +const buildDocxBlob = async (markdown) => { + const { Document, Packer, Paragraph, HeadingLevel } = await import('docx') + const children = [] + + for (const line of markdown.split('\n')) { + if (line.startsWith('# ')) { + children.push(new Paragraph({ text: line.slice(2), heading: HeadingLevel.HEADING_1 })) + continue + } + if (line.startsWith('## ')) { + children.push(new Paragraph({ text: line.slice(3), heading: HeadingLevel.HEADING_2 })) + continue + } + if (line.startsWith('### ')) { + children.push(new Paragraph({ text: line.slice(4), heading: HeadingLevel.HEADING_3 })) + continue + } + if (line.startsWith('---')) { + children.push(new Paragraph({ text: '----------' })) + continue + } + children.push(line.trim() === '' ? new Paragraph({}) : new Paragraph({ text: line })) + } + + return Packer.toBlob(new Document({ sections: [{ properties: {}, children }] })) +} + +const downloadBlob = (blob, filename) => { + const url = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = url + anchor.download = filename + anchor.style.display = 'none' + document.body.appendChild(anchor) + anchor.click() + anchor.remove() + setTimeout(() => URL.revokeObjectURL(url), 0) +} + +const getExportMarkdown = async () => { + if (!crepe) { + throw new Error('编辑器未初始化,请稍后重试') + } + + crepe.editor.action((ctx) => { + const view = ctx.get(editorViewCtx) + clearCurrentSuggestion(view) + }) + + const markdown = await crepe.getMarkdown() + return transformLegacyDocBlocksForExport(markdown) +} + const syncInitialMarkdown = async (nextValue) => { if (!crepe) { - lastInitialMarkdown = nextValue + lastInitialMarkdown = transformSpecialDocBlocksToLegacy(nextValue) return } + const normalizedNextValue = transformSpecialDocBlocksToLegacy(nextValue) try { const current = await crepe.getMarkdown() const normalizedCurrent = normalizeTrailingWhitespace(current) const normalizedLast = normalizeTrailingWhitespace(lastInitialMarkdown) if (!normalizedCurrent || normalizedCurrent === normalizedLast) { - crepe.editor.action(replaceAll(nextValue)) + crepe.editor.action(replaceAll(normalizedNextValue)) } } catch { // Ignore sync errors } finally { - lastInitialMarkdown = nextValue + lastInitialMarkdown = normalizedNextValue } } @@ -338,6 +416,58 @@ const updateHistoryState = (view) => { canRedo.value = redoDepth(view.state) > 0 } +const serializeSelectionToMarkdown = (view, from, to) => { + const state = view.state + const slice = state.doc.slice(from, to) + const doc = state.schema.topNodeType.createAndFill(undefined, slice.content) + if (!doc) return state.doc.textBetween(from, to, '\n', '\n') + return crepe?.editor?.action((ctx) => { + const serializer = ctx.get(serializerCtx) + return serializer(doc) + }) || state.doc.textBetween(from, to, '\n', '\n') +} + +const selectionIncludesDocBlock = (state) => { + const { from, to } = state.selection + let hasDocBlock = false + state.doc.nodesBetween(from, to, (node) => { + if (node.type?.name === DOC_BLOCK_NODE_TYPE) { + hasDocBlock = true + return false + } + return true + }) + return hasDocBlock +} + +const getCursorContext = (view) => { + const { $from } = view.state.selection + let inDocBlock = false + let fenceLanguage = '' + for (let depth = $from.depth; depth > 0; depth -= 1) { + const node = $from.node(depth) + const typeName = node.type?.name || '' + if (typeName === DOC_BLOCK_NODE_TYPE) { + inDocBlock = true + break + } + if (typeName === 'code_block' || typeName === 'codeBlock' || typeName === 'code_fence' || typeName === 'fence') { + fenceLanguage = String(node.attrs?.language || node.attrs?.lang || node.attrs?.info || '').trim().toLowerCase() + break + } + } + const disabledByFence = fenceLanguage === 'mermaid' || fenceLanguage === 'tex' || fenceLanguage === 'latex' || fenceLanguage === 'katex' + return { + disabled: inDocBlock || disabledByFence, + inDocBlock, + fenceLanguage, + } +} + +const refreshDocUploadState = (view) => { + isDocUploadDisabled.value = getCursorContext(view).disabled +} + const runHistoryCommand = (command) => { if (!crepe) return crepe.editor.action((ctx) => { @@ -480,6 +610,15 @@ const prepareImageFile = async (file) => { } onMounted(async () => { + document.addEventListener('click', (e) => { + if (!e.target.closest('.export-btn-wrapper')) { + showExportDropdown.value = false + } + if (!e.target.closest('.image-btn-wrapper')) { + showImageDropdown.value = false + } + }) + if (!root.value) throw new Error('root.value is null') updateEditorTailSpace() if (typeof ResizeObserver !== 'undefined') { @@ -491,7 +630,7 @@ onMounted(async () => { crepe = new Crepe({ root: root.value, - defaultValue: initialMarkdown.value || '', + defaultValue: transformSpecialDocBlocksToLegacy(initialMarkdown.value || ''), features: { [Crepe.Feature.Latex]: true, [Crepe.Feature.ImageBlock]: true, @@ -547,6 +686,9 @@ onMounted(async () => { crepe.editor.use(copilotConfigCtx) crepe.editor.use(copilotGhostMark) crepe.editor.use(copilotPlugin) + crepe.editor.use(docBlockRemark) + crepe.editor.use(docBlockNode) + crepe.editor.use(docBlockView) await crepe.create() @@ -557,6 +699,7 @@ onMounted(async () => { syncObjectUrls(doc) refreshSizeAndLimit(ctx) updateHistoryState(view) + refreshDocUploadState(view) scheduleMarkdownSync() }) }) @@ -566,32 +709,79 @@ onMounted(async () => { setCopilotEnabled(view, aiEnabled.value) refreshSizeAndLimit(ctx) updateHistoryState(view) + refreshDocUploadState(view) + const editorDom = view.dom + editorCopyHandler = (event) => { + const state = view.state + if (!selectionIncludesDocBlock(state)) return + const { from, to } = state.selection + const rawMarkdown = serializeSelectionToMarkdown(view, from, to) + const clipboardMarkdown = transformDocBlockMarkdownForClipboard(rawMarkdown || '') + if (!clipboardMarkdown) return + event.preventDefault() + event.clipboardData?.setData('text/plain', clipboardMarkdown) + } + editorDom.addEventListener('copy', editorCopyHandler) }) scheduleMarkdownSync() }) const exportMarkdown = async () => { - if (!crepe) return + try { + const markdown = await getExportMarkdown() + const exportName = createExportName() + const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' }) + downloadBlob(blob, `${exportName}.md`) + } catch (error) { + console.error('Markdown export failed:', error) + alert(`Markdown 导出失败: ${error.message}`) + } +} - crepe.editor.action((ctx) => { - const view = ctx.get(editorViewCtx) - clearCurrentSuggestion(view) - }) - - const markdown = await crepe.getMarkdown() - const blob = new Blob([markdown], { type: 'text/markdown' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - const now = new Date() - const pad = (n) => String(n).padStart(2, '0') - const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` - const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` - a.href = url - a.download = `save${datePart}${timePart}.md` - document.body.appendChild(a) - a.click() - a.remove() - URL.revokeObjectURL(url) +const exportDocx = async () => { + try { + console.log('Exporting DOCX...') + const markdown = await getExportMarkdown() + const blob = await buildDocxBlob(markdown) + const exportName = createExportName() + downloadBlob(blob, `${exportName}.docx`) + console.log('DOCX export completed') + } catch (error) { + console.error('DOCX export failed:', error) + alert(`DOCX导出失败: ${error.message}`) + } +} + +const exportPdf = async () => { + try { + console.log('Exporting PDF via DOCX...') + const markdown = await getExportMarkdown() + const docxBlob = await buildDocxBlob(markdown) + const exportName = createExportName() + const formData = new FormData() + formData.append('file', docxBlob, `${exportName}.docx`) + + const res = await fetch(EXPORT_PDF_URL, { + method: 'POST', + headers: { + 'X-API-Key': API_KEY, + }, + body: formData, + }) + + if (!res.ok) { + const errorText = await res.text() + throw new Error(`HTTP ${res.status}: ${errorText}`) + } + + const pdfBlob = await res.blob() + downloadBlob(pdfBlob, `${exportName}.pdf`) + console.log('PDF export completed successfully') + alert('PDF导出成功!') + } catch (error) { + console.error('PDF export failed:', error) + alert(`PDF导出失败: ${error.message}`) + } } const triggerUpload = () => { @@ -622,7 +812,7 @@ const handleFileUpload = async (event) => { try { const text = await file.text() if (crepe && crepe.editor) { - crepe.editor.action(replaceAll(text)) + crepe.editor.action(replaceAll(transformSpecialDocBlocksToLegacy(text))) } } catch { // File upload error, ignore @@ -647,6 +837,12 @@ const toggleAI = async () => { const toggleImageDropdown = () => { showImageDropdown.value = !showImageDropdown.value + showExportDropdown.value = false +} + +const toggleExportDropdown = () => { + showExportDropdown.value = !showExportDropdown.value + showImageDropdown.value = false } const triggerImageUpload = () => { @@ -689,13 +885,14 @@ const insertMarkdownAtCursor = (markdown) => { }) } -const buildCodeBlock = (file, text) => { - const name = (file?.name || '').toLowerCase() - const lang = name.endsWith('.json') ? 'json' : 'text' - return `\n\`\`\`${lang}\n${text}\n\`\`\`\n` +const insertDocBlockAtCursor = (attrs) => { + if (!crepe) return + const markdown = buildLegacyDocBlock(attrs) + insertMarkdownAtCursor(`\n${markdown}\n`) } const triggerFileUpload = () => { + if (isDocUploadDisabled.value) return uploadFileInputRef.value?.click() } @@ -704,42 +901,44 @@ const handleUploadFile = async (event) => { const file = input.files?.[0] if (!file) return - const convertible = isConvertibleFile(file) try { - if (isImageFile(file)) { - const objectUrl = await prepareImageFile(file) - if (objectUrl) { - clearCurrentGhost() - insertImageAtCursor(objectUrl) - } + if (!isSupportedDocFile(file)) { + alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档') return } + if (isDocUploadDisabled.value || !crepe) { + alert(t('uploadDocInBlockWarning') || '当前光标位置不能插入文件') + return + } + + const docType = getDocTypeFromFilename(file.name) + let content = '' + if (isTextFile(file)) { - const text = await file.text() - clearCurrentGhost() - insertMarkdownAtCursor(buildCodeBlock(file, text)) + content = await file.text() + } else if (isConvertibleFile(file)) { + content = await convertFileToMarkdown(file) + } else { + alert(t('uploadDocTypeWarning') || '仅支持 txt、docx、pptx、pdf 格式的文档') return } - if (convertible) { - const markdown = await convertFileToMarkdown(file) - if (!markdown) { - throw new Error('No markdown returned') - } - clearCurrentGhost() - insertMarkdownAtCursor(markdown) - return + if (!content) { + throw new Error('文档解析结果为空') } - warnUnsupportedInsertType() + clearCurrentGhost() + insertDocBlockAtCursor({ + docType, + docName: file.name || `document.${docType}`, + uploadTime: new Date().toISOString(), + collapsed: false, + content, + }) } catch (e) { const message = e instanceof Error ? e.message : '' - if (convertible) { - warnConvertError(message) - } else { - warnUploadError(message) - } + warnConvertError(message) } finally { input.value = '' } @@ -788,6 +987,12 @@ onUnmounted(() => { clearAllOcrCache() if (crepe) { + if (editorCopyHandler) { + crepe.editor.action((ctx) => { + ctx.get(editorViewCtx).dom.removeEventListener('copy', editorCopyHandler) + }) + editorCopyHandler = null + } crepe.destroy() crepe = null } @@ -799,7 +1004,6 @@ onUnmounted(() => { position: relative; width: 100vw; height: 100vh; - overflow: hidden; } .history-buttons { @@ -850,7 +1054,8 @@ onUnmounted(() => { display: flex; flex-direction: column; gap: 8px; - z-index: 9999; + z-index: 99999; + transform: translateZ(0); } .action-btn { @@ -977,6 +1182,40 @@ onUnmounted(() => { background: var(--crepe-color-hover); } +.export-btn-wrapper { + position: relative; +} + +.export-dropdown { + position: absolute; + bottom: 100%; + right: 0; + margin-bottom: 8px; + background: var(--panel-bg); + border: 1px solid var(--panel-border); + border-radius: 8px; + box-shadow: var(--panel-shadow); + overflow: hidden; + z-index: 10000; + min-width: 160px; +} + +.export-dropdown button { + display: block; + width: 100%; + padding: 10px 16px; + border: none; + background: none; + text-align: left; + cursor: pointer; + font-size: 14px; + color: var(--app-text); +} + +.export-dropdown button:hover { + background: var(--crepe-color-hover); +} + .url-dialog-overlay { position: fixed; top: 0; diff --git a/src/plugins/docBlockPlugin.ts b/src/plugins/docBlockPlugin.ts new file mode 100644 index 0000000..2ee28d8 --- /dev/null +++ b/src/plugins/docBlockPlugin.ts @@ -0,0 +1,249 @@ +import { createApp, reactive } from 'vue' +import { serializerCtx } from '@milkdown/kit/core' +import { $node, $remark, $view } from '@milkdown/kit/utils' +import type { Node as ProseNode, Schema } from '@milkdown/prose/model' +import type { EditorView, NodeView } from '@milkdown/prose/view' +import DocBlockCrepe from '../components/DocBlockCrepe.vue' +import { + DOC_BLOCK_FENCE_LANG, + DOC_BLOCK_NODE_TYPE, + DOC_CONTEXT_LIMIT, + buildLegacyDocBlock, + buildDocContextFence, + normalizeDocType, + parseLegacyDocBlock, + parseDocBlockValue, + stripDocBlockMarkdown, +} from '../utils/docBlock.js' + +function serializeRangeToMarkdown( + doc: ProseNode, + from: number, + to: number, + schema: Schema, + serializer: (content: ProseNode) => string +): string { + if (from >= to) return '' + const slice = doc.slice(from, to) + if (slice.content.size <= 0) return '' + const sliceDoc = schema.topNodeType.createAndFill(undefined, slice.content) + return sliceDoc ? serializer(sliceDoc) : doc.textBetween(from, to, '\n', '\n') +} + +function buildDocContext(doc: ProseNode, excludePos?: number) { + const blocks: string[] = [] + doc.descendants((node, pos) => { + if (node.type.name !== DOC_BLOCK_NODE_TYPE) return true + if (excludePos !== undefined && pos === excludePos) return false + blocks.push( + buildDocContextFence({ + docType: node.attrs.docType, + content: node.attrs.content, + }) + ) + return false + }) + return blocks.join('\n\n') +} + +class DocBlockNodeView implements NodeView { + node: ProseNode + view: EditorView + getPos: () => number | undefined + dom: HTMLElement + app: ReturnType | null = null + props: Record + serializer: (content: ProseNode) => string + + constructor(node: ProseNode, view: EditorView, getPos: () => number | undefined, serializer: (content: ProseNode) => string) { + this.node = node + this.view = view + this.getPos = getPos + this.serializer = serializer + this.dom = document.createElement('div') + this.dom.className = 'doc-block-node-view' + this.props = reactive({ + docType: node.attrs.docType, + docName: node.attrs.docName, + uploadTime: node.attrs.uploadTime, + content: node.attrs.content, + collapsed: node.attrs.collapsed, + onUpdateContent: (content: string) => this.updateAttrs({ content }), + onUpdateCollapsed: (collapsed: boolean) => this.updateAttrs({ collapsed }), + onDelete: () => this.deleteNode(), + resolveSuggestionRequest: (payload: { prefix: string; suffix: string; languageId: string }) => this.resolveSuggestionRequest(payload), + }) + this.mount() + } + + mount() { + this.app = createApp(DocBlockCrepe, this.props) + this.app.mount(this.dom) + } + + getPosValue() { + const pos = this.getPos() + return typeof pos === 'number' ? pos : undefined + } + + updateAttrs(patch: Record) { + const pos = this.getPosValue() + if (pos === undefined) return + const nextAttrs = { ...this.node.attrs, ...patch } + this.view.dispatch(this.view.state.tr.setNodeMarkup(pos, undefined, nextAttrs)) + } + + deleteNode() { + const pos = this.getPosValue() + if (pos === undefined) return + const tr = this.view.state.tr.delete(pos, pos + this.node.nodeSize).scrollIntoView() + this.view.dispatch(tr) + this.view.focus() + } + + resolveSuggestionRequest(payload: { prefix: string; suffix: string; languageId: string }) { + const pos = this.getPosValue() + if (pos === undefined) return payload + const doc = this.view.state.doc + const schema = this.view.state.schema + const before = stripDocBlockMarkdown(serializeRangeToMarkdown(doc, 0, pos, schema, this.serializer)) + const after = stripDocBlockMarkdown(serializeRangeToMarkdown(doc, pos + this.node.nodeSize, doc.content.size, schema, this.serializer)) + const docContext = buildDocContext(doc, pos) + const mergedPrefix = [docContext, before, payload.prefix].filter(Boolean).join('\n\n') + const mergedSuffix = [payload.suffix, after].filter(Boolean).join('\n\n') + if (mergedPrefix.length + mergedSuffix.length > DOC_CONTEXT_LIMIT) { + return { + prefix: mergedPrefix.slice(0, DOC_CONTEXT_LIMIT), + suffix: '', + languageId: payload.languageId, + blocked: true, + } + } + return { + prefix: mergedPrefix, + suffix: mergedSuffix, + languageId: payload.languageId, + blocked: false, + } + } + + update(node: ProseNode) { + if (node.type !== this.node.type) return false + this.node = node + this.props.docType = node.attrs.docType + this.props.docName = node.attrs.docName + this.props.uploadTime = node.attrs.uploadTime + this.props.content = node.attrs.content + this.props.collapsed = node.attrs.collapsed + return true + } + + stopEvent(event: Event) { + const target = event.target as Node | null + return Boolean(target && this.dom.contains(target)) + } + + ignoreMutation() { + return true + } + + destroy() { + this.app?.unmount() + this.app = null + } +} + +function visitChildren(node: any, visitor: (child: any) => any) { + if (!node || !Array.isArray(node.children)) return + node.children = node.children.map((child: any) => { + const next = visitor(child) + if (next && next !== child) return next + visitChildren(child, visitor) + return child + }) +} + +export const docBlockRemark = $remark('docBlockRemark', () => () => { + return (tree: any) => { + visitChildren(tree, (node) => { + if (node?.type === 'code' && node.lang === DOC_BLOCK_FENCE_LANG) { + return { + type: 'docBlock', + value: String(node.value || ''), + sourceType: 'code', + } + } + if (node?.type === 'html' && typeof node.value === 'string' && node.value.includes(' ({ + group: 'block', + atom: true, + isolating: true, + selectable: true, + draggable: false, + marks: '', + attrs: { + docType: { default: 'txt' }, + docName: { default: 'document.txt' }, + uploadTime: { default: '' }, + content: { default: '' }, + collapsed: { default: false }, + }, + parseDOM: [ + { + tag: 'div[data-doc-block="true"]', + getAttrs: (dom) => ({ + docType: normalizeDocType((dom as HTMLElement).getAttribute('data-doc-type') || ''), + docName: (dom as HTMLElement).getAttribute('data-doc-name') || 'document.txt', + uploadTime: (dom as HTMLElement).getAttribute('data-doc-upload-time') || '', + collapsed: ((dom as HTMLElement).getAttribute('data-doc-collapsed') || '') === 'true', + content: '', + }), + }, + ], + toDOM: (node) => [ + 'div', + { + 'data-doc-block': 'true', + 'data-doc-type': node.attrs.docType, + 'data-doc-name': node.attrs.docName, + 'data-doc-upload-time': node.attrs.uploadTime, + 'data-doc-collapsed': String(Boolean(node.attrs.collapsed)), + }, + ], + parseMarkdown: { + match: (node) => node.type === 'docBlock', + runner: (state, node, type) => { + const attrs = node.sourceType === 'code' + ? parseDocBlockValue(String(node.value || '')) + : parseLegacyDocBlock(String(node.value || '')) + if (!attrs) return + state.addNode(type, attrs) + }, + }, + toMarkdown: { + match: (node) => node.type.name === DOC_BLOCK_NODE_TYPE, + runner: (state, node) => { + state.addNode('html', undefined, buildLegacyDocBlock(node.attrs)) + }, + }, +})) + +export const docBlockView = $view(docBlockNode, (ctx) => { + const serializer = ctx.get(serializerCtx) + return (node, view, getPos) => new DocBlockNodeView(node, view, getPos, serializer) +}) + +export function buildDocContextFromDoc(doc: ProseNode, excludePos?: number) { + return buildDocContext(doc, excludePos) +} diff --git a/src/style.css b/src/style.css index 2d056c4..b049e4d 100644 --- a/src/style.css +++ b/src/style.css @@ -273,7 +273,7 @@ body { overflow: auto; border-radius: 8px; padding: 8px; - background: color-mix(in srgb, var(--crepe-color-background, #fff) 88%, transparent); + background: rgba(255, 255, 255, 0.88); } .mermaid-inner::-webkit-scrollbar { @@ -315,7 +315,7 @@ body { .mermaid-error { padding: 12px 16px; margin: 0; - background: color-mix(in srgb, var(--danger-text, #dc2626) 8%, transparent); + background: rgba(220, 38, 38, 0.08); border: 1px solid var(--danger-text, #dc2626); border-radius: 6px; color: var(--danger-text, #dc2626); @@ -331,12 +331,12 @@ body { :root[data-theme='dark'] .milkdown .cm-editor, :root[data-theme='dark'] .milkdown .cm-scroller { - background-color: color-mix(in srgb, var(--crepe-color-surface-low) 86%, transparent); + background-color: rgba(237, 237, 237, 0.86); color: var(--crepe-color-on-surface); } :root[data-theme='dark'] .milkdown .cm-gutters { - background-color: color-mix(in srgb, var(--crepe-color-surface-low) 86%, transparent); + background-color: rgba(237, 237, 237, 0.86); color: var(--crepe-color-on-surface-variant); border-right-color: var(--panel-border); } diff --git a/src/utils/config.js b/src/utils/config.js index 52ad943..f9460c2 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -5,3 +5,4 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.imageteac export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions` export const OCR_URL = import.meta.env.VITE_OCR_URL || `${API_BASE_URL}/v1/ocr` export const CONVERT_URL = import.meta.env.VITE_CONVERT_URL || `${API_BASE_URL}/v1/convert` +export const EXPORT_PDF_URL = import.meta.env.VITE_EXPORT_PDF_URL || '/v1/export/pdf' diff --git a/src/utils/convert.js b/src/utils/convert.js index df77519..6ce2cfb 100644 --- a/src/utils/convert.js +++ b/src/utils/convert.js @@ -23,6 +23,7 @@ export async function convertFileToMarkdown(file) { method: 'POST', headers: { 'Content-Type': 'application/json', + 'X-API-Key': 'your-secret-key-here', }, body: JSON.stringify({ file: base64, diff --git a/src/utils/docBlock.js b/src/utils/docBlock.js new file mode 100644 index 0000000..3d829d5 --- /dev/null +++ b/src/utils/docBlock.js @@ -0,0 +1,180 @@ +export const DOC_BLOCK_NODE_TYPE = 'doc_block' +export const DOC_BLOCK_FENCE_LANG = 'llm-file' +export const DOC_CONTEXT_LIMIT = 32 * 1024 + +const IMAGE_MD_RE = /!\[[^\]]*]\([^)]+\)/g +const IMAGE_HTML_RE = /]*>/gi +const HEADER_SEPARATOR = '\n---\n' + +export function normalizeDocType(value = '') { + const lower = String(value || '').trim().toLowerCase() + if (lower === 'txt' || lower === 'text' || lower === 'plain') return 'txt' + if (lower === 'doc' || lower === 'docx' || lower === 'word') return 'docx' + if (lower === 'ppt' || lower === 'pptx' || lower === 'powerpoint') return 'pptx' + if (lower === 'pdf') return 'pdf' + return 'txt' +} + +export function getDocTypeFromFilename(name = '') { + const lower = String(name || '').toLowerCase() + if (lower.endsWith('.docx')) return 'docx' + if (lower.endsWith('.pptx')) return 'pptx' + if (lower.endsWith('.pdf')) return 'pdf' + return 'txt' +} + +export function isSupportedDocFile(file) { + if (!file) return false + const name = String(file.name || '').toLowerCase() + const type = String(file.type || '').toLowerCase() + return ( + name.endsWith('.txt') || + name.endsWith('.docx') || + name.endsWith('.pptx') || + name.endsWith('.pdf') || + type === 'text/plain' || + type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || + type === 'application/pdf' + ) +} + +export function sanitizeDocContent(markdown = '') { + return String(markdown || '') + .replace(/\r\n?/g, '\n') + .replace(IMAGE_MD_RE, '') + .replace(IMAGE_HTML_RE, '') + .replace(/\n{3,}/g, '\n\n') + .trim() +} + +function quoteMeta(value = '') { + return JSON.stringify(String(value ?? '')) +} + +function parseMetaLine(line = '') { + const idx = line.indexOf(':') + if (idx < 0) return null + const key = line.slice(0, idx).trim() + const rawValue = line.slice(idx + 1).trim() + if (!key) return null + try { + return [key, JSON.parse(rawValue)] + } catch { + return [key, rawValue] + } +} + +function pickFence(content = '') { + const matches = String(content || '').match(/`{3,}/g) || [] + const maxLen = matches.reduce((max, item) => Math.max(max, item.length), 2) + return '`'.repeat(maxLen + 1) +} + +export function buildDocBlockValue(attrs = {}) { + const docType = normalizeDocType(attrs.docType) + const docName = String(attrs.docName || `document.${docType}`) + const uploadTime = String(attrs.uploadTime || new Date().toISOString()) + const collapsed = Boolean(attrs.collapsed) + const content = sanitizeDocContent(attrs.content || '') + return [ + `type: ${quoteMeta(docType)}`, + `name: ${quoteMeta(docName)}`, + `uploadTime: ${quoteMeta(uploadTime)}`, + `collapsed: ${collapsed ? 'true' : 'false'}`, + '---', + content, + ].join('\n') +} + +export function parseDocBlockValue(raw = '') { + const normalized = String(raw || '').replace(/\r\n?/g, '\n') + const separatorIndex = normalized.indexOf(HEADER_SEPARATOR) + const headerText = separatorIndex >= 0 ? normalized.slice(0, separatorIndex) : '' + const bodyText = separatorIndex >= 0 ? normalized.slice(separatorIndex + HEADER_SEPARATOR.length) : normalized + const attrs = { + docType: 'txt', + docName: 'document.txt', + uploadTime: '', + collapsed: false, + content: sanitizeDocContent(bodyText), + } + + for (const line of headerText.split('\n')) { + const parsed = parseMetaLine(line) + if (!parsed) continue + const [key, value] = parsed + if (key === 'type') attrs.docType = normalizeDocType(value) + if (key === 'name' && value) attrs.docName = String(value) + if (key === 'uploadTime' && value) attrs.uploadTime = String(value) + if (key === 'collapsed') attrs.collapsed = value === true || value === 'true' + } + + if (!attrs.docName) attrs.docName = `document.${attrs.docType}` + return attrs +} + +export function buildDocBlockMarkdown(attrs = {}) { + const value = buildDocBlockValue(attrs) + const fence = pickFence(value) + return `${fence}${DOC_BLOCK_FENCE_LANG}\n${value}\n${fence}` +} + +export function buildDocContextFence(attrs = {}) { + const docType = normalizeDocType(attrs.docType) + const content = sanitizeDocContent(attrs.content || '') + const fence = pickFence(content) + return `${fence}${docType}\n${content}\n${fence}` +} + +export function buildLegacyDocBlock(attrs = {}) { + const docType = normalizeDocType(attrs.docType) + const docName = String(attrs.docName || `document.${docType}`) + const uploadTime = String(attrs.uploadTime || new Date().toISOString()) + const content = sanitizeDocContent(attrs.content || '') + return `\n${content}\n` +} + +export function parseLegacyDocBlock(raw = '') { + const match = String(raw || '').match(/^\n?([\s\S]*?)\n?<\/doc_end>$/) + if (!match) return null + return { + docType: normalizeDocType(match[1]), + docName: match[2] || 'document.txt', + uploadTime: match[3] || '', + collapsed: match[4] === 'true', + content: sanitizeDocContent(match[5] || ''), + } +} + +export function transformDocBlockMarkdownForClipboard(markdown = '') { + const pattern = /(^|\n)(`{3,})llm-file[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/g + const replacedFence = String(markdown || '').replace(pattern, (full, prefix, _fence, value) => { + const attrs = parseDocBlockValue(value) + return `${prefix}${buildDocContextFence(attrs)}` + }) + return replacedFence.replace(/[\s\S]*?<\/doc_end>/g, (full) => { + const attrs = parseLegacyDocBlock(full) + return attrs ? buildDocContextFence(attrs) : full + }) +} + +export function stripDocBlockMarkdown(markdown = '') { + const pattern = /(^|\n)(`{3,})llm-file[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g + return String(markdown || '').replace(pattern, '$1').replace(/\n{3,}/g, '\n\n').trim() +} + +export function transformLegacyDocBlocksForExport(markdown = '') { + return String(markdown || '').replace(/[\s\S]*?<\/doc_end>/g, (full) => { + const attrs = parseLegacyDocBlock(full) + return attrs ? buildDocBlockMarkdown(attrs) : full + }) +} + +export function transformSpecialDocBlocksToLegacy(markdown = '') { + const pattern = /(^|\n)(`{3,})llm-file[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/g + return String(markdown || '').replace(pattern, (full, prefix, _fence, value) => { + const attrs = parseDocBlockValue(value) + return `${prefix}${buildLegacyDocBlock(attrs)}` + }) +} diff --git a/test.txt b/test.txt deleted file mode 100644 index e69de29..0000000 diff --git a/update_readme.py b/update_readme.py deleted file mode 100644 index 5b3da0c..0000000 Binary files a/update_readme.py and /dev/null differ