diff --git a/design/03-rust-web/process/3-21-final-sidebar-runtime-extraction-v1.md b/design/03-rust-web/process/3-21-final-sidebar-runtime-extraction-v1.md index d96ea4c4..ef8b6516 100644 --- a/design/03-rust-web/process/3-21-final-sidebar-runtime-extraction-v1.md +++ b/design/03-rust-web/process/3-21-final-sidebar-runtime-extraction-v1.md @@ -186,7 +186,7 @@ codegraph sync . |----|------| | SIDEBAR_TREE_JS 外置 | 布局文件从 11,741 → ~1,100 行 | | browser/ 模块 | 11 个 JS 文件,总计 ~11,000 行 | -| editor_runtime/ 模块 | 16 个 Rust 模块(另有 mod.rs) | +| editor_runtime/ 模块 | 19 个 Rust 模块(另有 mod.rs) | | 浏览器 smoke | task479/488/489/451 全部 PASS | | 测试 | editor 13/13, web sidebar 10/10, web full 531/549 | | CodeGraph | 350+ 文件索引完成 | @@ -195,4 +195,4 @@ codegraph sync . | 项 | 说明 | |----|------| -| Batch C 全量验证 | 尚未运行 `cargo test -p mnote-web --lib` / `cargo test -p mnote-leptos-tiptap-spike` / task451 全套;本轮已完成 spike check/build、wasm-bindgen、task479/task488/task489 与直接编辑器交互回归 | +| Batch C 全量验证 | 尚未运行 `cargo test -p mnote-web --lib` / `cargo test -p mnote-leptos-tiptap-spike` / task451 全套;本轮已完成 spike check/build、wasm-bindgen、task479/task488/task489、临时 slash/table Playwright 回归与直接编辑器交互回归 | diff --git a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md index 24d3d725..cbed1a2c 100644 --- a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md +++ b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md @@ -406,11 +406,14 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 | 文件 | 原大小 | 现大小 | 变化 | |------|--------|--------|------| -| `lib.rs` | 9,873 行 | 8,671 行 | **-12%** | +| `lib.rs` | 9,873 行 | 8,181 行 | **-17%** | | `editor_runtime/mindmap_node_view.rs` | — | 909 行 | 新建 | | `editor_runtime/bridge_events.rs` | — | 149 行 | 新建 | +| `editor_runtime/slash_actions.rs` | — | 237 行 | 新建 | +| `editor_runtime/table_commands.rs` | — | 180 行 | 新建 | +| `editor_runtime/bridge_dispatch.rs` | — | 86 行 | 新建 | -### editor_runtime/ 最终模块清单(16 模块,另有 `mod.rs`) +### editor_runtime/ 最终模块清单(19 模块,另有 `mod.rs`) | 模块 | 说明 | 来源 | |------|------|------| @@ -420,6 +423,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 | `block_hover_state.rs` | HoveredBlockState 共享类型 | 5-29 | | `block_menu_document.rs` | legacy JSON helper | 5-29 | | `block_menu_overlay.rs` | block menu 几何 | 5-29 | +| `bridge_dispatch.rs` | host command kind 解析 | **本批新建** | | `bridge_events.rs` | 事件常量 + payload 类型 | **本批新建** | | `command_sync.rs` | 命令同步 | 5-28 P2 | | `content_layout.rs` | 内容布局 | 5-29 | @@ -430,6 +434,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 | `mindmap_node_view.rs` | mindmap shell 完整实现 | **本批新建**(5-28 P5) | | `overlays.rs` | overlay 管理 | 5-29 | | `persistence.rs` | 持久化 | 5-28 P2 | +| `slash_actions.rs` | slash 菜单 action 数据 | **本批新建** | +| `table_commands.rs` | table toolbar / option 命令 | **本批新建** | ### 未完成(后续独立任务) @@ -448,3 +454,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 拖拽重排两条路径与块菜单“拷贝副本”不再读取/写回整文 HTML,改为 `move_top_level_block_with_history` / `duplicate_top_level_block_with_history` + `sync_persisted_editor_command`。 - 删除本轮变成无调用方的 `block_menu_legacy_html.rs` 与 `apply_html_update`,generated island JS/WASM 已重新生成。 - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、`cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`、`wasm-bindgen --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm`、`task489`、`task488`、`task479`、临时 Playwright 拷贝副本 undo/redo + 块拖拽重排回归。 +- 2026-05-25:Codex 继续完成低风险模块提取。 + - 新增 `slash_actions.rs`,迁出 `SlashActionKind` / `SlashAction` / `SLASH_ACTIONS`,`lib.rs` 继续持有 slash 命令执行与 Leptos view。 + - 新增 `table_commands.rs`,迁出 table toolbar / option action、命令执行和 checked 状态计算。 + - 新增 `bridge_dispatch.rs`,迁出 `HostCommandKind` 及 host command payload 解析;真实 command 执行 match 仍留在 `lib.rs`。 + - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、`cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml command -- --test-threads=1`、wasm release build、`wasm-bindgen`、task489、task488、task479、临时 slash 插入简单表格 + 表格行 aux 命令 Playwright 回归。 diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js index fb0616b2..65cc8fbd 100644 --- a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js +++ b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js @@ -1244,47 +1244,47 @@ function __wbg_get_imports() { } }, arguments); }, __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1143, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5); return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1196, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1195, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12); return ret; }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 972, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 971, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798); return ret; }, __wbindgen_cast_0000000000000004: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1094, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1093, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a); return ret; }, __wbindgen_cast_0000000000000005: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1143, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4); return ret; }, __wbindgen_cast_0000000000000006: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 598, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 879, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf); return ret; }, __wbindgen_cast_0000000000000007: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1096, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1095, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f); return ret; }, __wbindgen_cast_0000000000000008: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1111, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1110, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5); return ret; }, __wbindgen_cast_0000000000000009: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1147, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1146, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c); return ret; }, diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm index 572ae85d..233a5689 100644 Binary files a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm and b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm differ diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/bridge_dispatch.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/bridge_dispatch.rs new file mode 100644 index 00000000..4c2f0838 --- /dev/null +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/bridge_dispatch.rs @@ -0,0 +1,86 @@ +use crate::editor_runtime::bridge_events::HostCommandPayload; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum HostCommandKind { + Undo, + Redo, + ReplaceContent, + SetPageOptions, + InsertInlineReference, + InsertEmbedReference, + RequestCurrentBlockId, + SetEditable, + Focus, + Bootstrap, +} + +impl HostCommandKind { + pub(crate) fn from_event_name(event_name: &str) -> Option { + match event_name { + "undo" => Some(Self::Undo), + "redo" => Some(Self::Redo), + "replaceContent" | "replace-document" => Some(Self::ReplaceContent), + "setPageOptions" | "set-page-options" => Some(Self::SetPageOptions), + "insertInlineReference" => Some(Self::InsertInlineReference), + "insertEmbedReference" => Some(Self::InsertEmbedReference), + "requestCurrentBlockId" => Some(Self::RequestCurrentBlockId), + "setEditable" | "set-editable" => Some(Self::SetEditable), + "focus" => Some(Self::Focus), + "bootstrap" => Some(Self::Bootstrap), + _ => None, + } + } + + pub(crate) fn from_payload(payload: &HostCommandPayload) -> Option { + payload.command.as_deref().and_then(Self::from_event_name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_command_aliases_and_canonical_names() { + assert_eq!( + HostCommandKind::from_event_name("replace-document"), + Some(HostCommandKind::ReplaceContent) + ); + assert_eq!( + HostCommandKind::from_event_name("replaceContent"), + Some(HostCommandKind::ReplaceContent) + ); + assert_eq!( + HostCommandKind::from_event_name("requestCurrentBlockId"), + Some(HostCommandKind::RequestCurrentBlockId) + ); + } + + #[test] + fn reads_command_from_payload_field() { + let payload = HostCommandPayload { + command: Some("setEditable".to_string()), + document_id: None, + workspace_id: None, + title: None, + content: None, + editable: Some(true), + page_options: None, + block_id: None, + block_index: None, + text: None, + reference_document_id: None, + reference_block_id: None, + current_block_id: None, + selection: None, + revision: None, + conflict_detection_key: None, + read_only: None, + }; + + assert_eq!( + HostCommandKind::from_payload(&payload), + Some(HostCommandKind::SetEditable) + ); + } +} diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs index 2e5d48ce..491547c4 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod block_dnd; pub(crate) mod block_hover_state; pub(crate) mod block_menu_document; pub(crate) mod block_menu_overlay; +pub(crate) mod bridge_dispatch; pub(crate) mod bridge_events; pub(crate) mod command_sync; pub(crate) mod content_layout; @@ -14,3 +15,5 @@ pub(crate) mod history_safe_commands; pub(crate) mod mindmap_node_view; pub(crate) mod overlays; pub(crate) mod persistence; +pub(crate) mod slash_actions; +pub(crate) mod table_commands; diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/slash_actions.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/slash_actions.rs new file mode 100644 index 00000000..36e0b238 --- /dev/null +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/slash_actions.rs @@ -0,0 +1,237 @@ +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum SlashActionKind { + AiAssistant, + AiWrite, + ContinueWriting, + Summarize, + MoreAi, + Paragraph, + Heading1, + Heading2, + Heading3, + Heading4, + BulletList, + OrderedList, + Todo, + AdvancedTodo, + Quote, + CodeBlock, + Divider, + SimpleTable, + Mindmap, + Image, + UploadAttachment, + Toc, +} + +#[derive(Clone, Copy)] +pub(crate) struct SlashAction { + pub(crate) kind: SlashActionKind, + pub(crate) id: &'static str, + pub(crate) category: &'static str, + pub(crate) icon: &'static str, + pub(crate) label: &'static str, + pub(crate) description: &'static str, + pub(crate) shortcut: &'static str, +} + +pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [ + SlashAction { + kind: SlashActionKind::AiAssistant, + id: "ai-assistant", + category: "AI 助理", + icon: "✦", + label: "AI 助理", + description: "按 Wolai 基线保留 AI 入口", + shortcut: "/ai", + }, + SlashAction { + kind: SlashActionKind::AiWrite, + id: "ai-write", + category: "AI 助理", + icon: "✎", + label: "用 AI 写作", + description: "唤起写作辅助入口", + shortcut: "/yaixz", + }, + SlashAction { + kind: SlashActionKind::ContinueWriting, + id: "continue-writing", + category: "AI 助理", + icon: "↪", + label: "续写", + description: "按当前上下文继续写作", + shortcut: "/xx", + }, + SlashAction { + kind: SlashActionKind::Summarize, + id: "summarize", + category: "AI 助理", + icon: "≡", + label: "总结", + description: "对当前内容生成摘要", + shortcut: "/zj", + }, + SlashAction { + kind: SlashActionKind::MoreAi, + id: "more-ai", + category: "AI 助理", + icon: "…", + label: "更多", + description: "更多 AI 命令", + shortcut: "›", + }, + SlashAction { + kind: SlashActionKind::Paragraph, + id: "paragraph", + category: "基础块列表", + icon: "Aa", + label: "文本", + description: "普通正文块", + shortcut: "/wb", + }, + SlashAction { + kind: SlashActionKind::Todo, + id: "todo", + category: "基础块列表", + icon: "☑", + label: "待办列表", + description: "创建可勾选任务", + shortcut: "/dblb", + }, + SlashAction { + kind: SlashActionKind::AdvancedTodo, + id: "advanced-todo", + category: "基础块列表", + icon: "☑", + label: "高级待办列表", + description: "保留 Wolai 高级待办入口", + shortcut: "/gjdblb", + }, + SlashAction { + kind: SlashActionKind::Heading1, + id: "heading-1", + category: "基础块列表", + icon: "H1", + label: "主标题", + description: "一级标题", + shortcut: "/h1", + }, + SlashAction { + kind: SlashActionKind::Heading2, + id: "heading-2", + category: "基础块列表", + icon: "H2", + label: "大标题", + description: "二级标题", + shortcut: "/h2", + }, + SlashAction { + kind: SlashActionKind::Heading3, + id: "heading-3", + category: "基础块列表", + icon: "H3", + label: "中标题", + description: "三级标题", + shortcut: "/h3", + }, + SlashAction { + kind: SlashActionKind::Heading4, + id: "heading-4", + category: "基础块列表", + icon: "H4", + label: "小标题", + description: "四级标题", + shortcut: "/h4", + }, + SlashAction { + kind: SlashActionKind::BulletList, + id: "bullet", + category: "基础块列表", + icon: "•", + label: "列表", + description: "记录普通要点", + shortcut: "/lb", + }, + SlashAction { + kind: SlashActionKind::OrderedList, + id: "ordered", + category: "基础块列表", + icon: "1.", + label: "数字列表", + description: "记录步骤顺序", + shortcut: "/szlb", + }, + SlashAction { + kind: SlashActionKind::Quote, + id: "quote", + category: "基础块列表", + icon: "❝", + label: "引述文字", + description: "包住注释与摘录", + shortcut: "/ys", + }, + SlashAction { + kind: SlashActionKind::CodeBlock, + id: "code-block", + category: "基础块列表", + icon: "<> ", + label: "代码片段", + description: "插入带语义的代码块", + shortcut: "/dm", + }, + SlashAction { + kind: SlashActionKind::Divider, + id: "divider", + category: "基础块列表", + icon: "—", + label: "分割线", + description: "插入分隔线", + shortcut: "/fgx", + }, + SlashAction { + kind: SlashActionKind::SimpleTable, + id: "simple-table", + category: "进阶块列表", + icon: "▦", + label: "简单表格", + description: "插入基础表格", + shortcut: "/jdbg", + }, + SlashAction { + kind: SlashActionKind::Mindmap, + id: "mindmap", + category: "进阶块列表", + icon: "⌘", + label: "思维导图", + description: "插入 kernel projection 导图块", + shortcut: "/dt", + }, + SlashAction { + kind: SlashActionKind::Image, + id: "image", + category: "媒体与附件", + icon: "▧", + label: "图片", + description: "上传并插入图片", + shortcut: "/tp", + }, + SlashAction { + kind: SlashActionKind::UploadAttachment, + id: "upload-attachment", + category: "媒体与附件", + icon: "↥", + label: "上传附件", + description: "上传 Office、PDF 或其他文件", + shortcut: "/fj", + }, + SlashAction { + kind: SlashActionKind::Toc, + id: "toc", + category: "进阶块列表", + icon: "☰", + label: "页面目录", + description: "根据当前标题生成目录", + shortcut: "/toc", + }, +]; diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/table_commands.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/table_commands.rs new file mode 100644 index 00000000..f0ed1a34 --- /dev/null +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/table_commands.rs @@ -0,0 +1,180 @@ +use leptos_tiptap::TiptapEditorHandle; +use serde_json::Value; + +#[derive(Clone, Copy)] +pub(crate) enum TableToolbarAction { + AddRowAfter, + AddColumnAfter, + DeleteRow, + DeleteColumn, + ClearCell, + DeleteTable, +} + +#[derive(Clone, Copy)] +pub(crate) enum TableOptionAction { + ToggleHeaderRow, + ToggleHeaderColumn, + ToggleHiddenBorders, +} + +impl TableToolbarAction { + pub(crate) fn id(self) -> &'static str { + match self { + Self::AddRowAfter => "add-row-after", + Self::AddColumnAfter => "add-column-after", + Self::DeleteRow => "delete-row", + Self::DeleteColumn => "delete-column", + Self::ClearCell => "clear-cell", + Self::DeleteTable => "delete-table", + } + } + + pub(crate) fn icon(self) -> &'static str { + match self { + Self::AddRowAfter => "+R", + Self::AddColumnAfter => "+C", + Self::DeleteRow => "-R", + Self::DeleteColumn => "-C", + Self::ClearCell => "⌫", + Self::DeleteTable => "×", + } + } + + pub(crate) fn title(self) -> &'static str { + match self { + Self::AddRowAfter => "下方插入行", + Self::AddColumnAfter => "右侧插入列", + Self::DeleteRow => "删除当前行", + Self::DeleteColumn => "删除当前列", + Self::ClearCell => "清空当前单元格", + Self::DeleteTable => "删除表格", + } + } +} + +pub(crate) const TABLE_TOOLBAR_ACTIONS: [TableToolbarAction; 6] = [ + TableToolbarAction::AddRowAfter, + TableToolbarAction::AddColumnAfter, + TableToolbarAction::ClearCell, + TableToolbarAction::DeleteRow, + TableToolbarAction::DeleteColumn, + TableToolbarAction::DeleteTable, +]; + +impl TableOptionAction { + pub(crate) fn id(self) -> &'static str { + match self { + Self::ToggleHeaderRow => "toggle-header-row", + Self::ToggleHeaderColumn => "toggle-header-column", + Self::ToggleHiddenBorders => "toggle-hidden-borders", + } + } + + pub(crate) fn title(self) -> &'static str { + match self { + Self::ToggleHeaderRow => "标题行", + Self::ToggleHeaderColumn => "标题列", + Self::ToggleHiddenBorders => "隐藏边框线", + } + } +} + +pub(crate) const TABLE_OPTION_ACTIONS: [TableOptionAction; 3] = [ + TableOptionAction::ToggleHeaderRow, + TableOptionAction::ToggleHeaderColumn, + TableOptionAction::ToggleHiddenBorders, +]; + +pub(crate) fn run_table_toolbar_action( + editor: TiptapEditorHandle, + action: TableToolbarAction, +) -> Result<&'static str, String> { + let result = match action { + TableToolbarAction::AddRowAfter => editor.add_table_row_after(), + TableToolbarAction::AddColumnAfter => editor.add_table_column_after(), + TableToolbarAction::DeleteRow => editor.delete_table_row(), + TableToolbarAction::DeleteColumn => editor.delete_table_column(), + TableToolbarAction::ClearCell => editor.clear_table_cell(), + TableToolbarAction::DeleteTable => editor.delete_table(), + }; + result + .map(|_| action.title()) + .map_err(|err| format!("命令执行失败:{err}")) +} + +pub(crate) fn run_table_option_action( + editor: TiptapEditorHandle, + action: TableOptionAction, +) -> Result<&'static str, String> { + let result = match action { + TableOptionAction::ToggleHeaderRow => editor.toggle_table_header_row(), + TableOptionAction::ToggleHeaderColumn => editor.toggle_table_header_column(), + TableOptionAction::ToggleHiddenBorders => editor.toggle_table_hidden_borders(), + }; + result + .map(|_| action.title()) + .map_err(|err| format!("命令执行失败:{err}")) +} + +fn first_table_node(value: &Value) -> Option<&Value> { + if value.get("type").and_then(Value::as_str) == Some("table") { + return Some(value); + } + if let Some(table) = value + .get("props") + .and_then(|props| props.get("tiptapTable")) + .and_then(first_table_node) + { + return Some(table); + } + for key in ["content", "children"] { + if let Some(children) = value.get(key).and_then(Value::as_array) { + if let Some(table) = children.iter().find_map(first_table_node) { + return Some(table); + } + } + } + None +} + +pub(crate) fn table_option_checked(document: &Value, action: TableOptionAction) -> bool { + let Some(table) = first_table_node(document) else { + return false; + }; + match action { + TableOptionAction::ToggleHiddenBorders => table + .get("attrs") + .and_then(|attrs| attrs.get("hiddenBorders")) + .and_then(Value::as_bool) + .unwrap_or(false), + TableOptionAction::ToggleHeaderRow => table + .get("content") + .and_then(Value::as_array) + .and_then(|rows| rows.first()) + .and_then(|row| row.get("content")) + .and_then(Value::as_array) + .map(|cells| { + !cells.is_empty() + && cells + .iter() + .all(|cell| cell.get("type").and_then(Value::as_str) == Some("tableHeader")) + }) + .unwrap_or(false), + TableOptionAction::ToggleHeaderColumn => table + .get("content") + .and_then(Value::as_array) + .map(|rows| { + !rows.is_empty() + && rows.iter().all(|row| { + row.get("content") + .and_then(Value::as_array) + .and_then(|cells| cells.first()) + .and_then(|cell| cell.get("type")) + .and_then(Value::as_str) + == Some("tableHeader") + }) + }) + .unwrap_or(false), + } +} diff --git a/rust/spikes/leptos-tiptap-spike/src/lib.rs b/rust/spikes/leptos-tiptap-spike/src/lib.rs index f28ecb2b..d5a1ba22 100644 --- a/rust/spikes/leptos-tiptap-spike/src/lib.rs +++ b/rust/spikes/leptos-tiptap-spike/src/lib.rs @@ -34,6 +34,7 @@ use editor_runtime::block_menu_document::{ use editor_runtime::block_hover_state::{ BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState, }; +use editor_runtime::bridge_dispatch::HostCommandKind; use editor_runtime::bridge_events::{ BridgeEnvelope, BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload, HostCommandEnvelope, HostCommandPayload, HostStatusPayload, ReadyPayload, RuntimePageOptions, @@ -70,6 +71,11 @@ use editor_runtime::persistence::{ load_persisted_document, normalize_identity_value, persist_document_state, persisted_document_identity, PersistedDocumentIdentity, }; +use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS}; +use editor_runtime::table_commands::{ + run_table_option_action, run_table_toolbar_action, table_option_checked, TableToolbarAction, + TABLE_OPTION_ACTIONS, TABLE_TOOLBAR_ACTIONS, +}; #[cfg(test)] use editor_runtime::persistence::persisted_document_storage_key; @@ -2749,43 +2755,6 @@ const SPIKE_STYLE: &str = r#" } "#; -#[derive(Clone, Copy, PartialEq, Eq)] -enum SlashActionKind { - AiAssistant, - AiWrite, - ContinueWriting, - Summarize, - MoreAi, - Paragraph, - Heading1, - Heading2, - Heading3, - Heading4, - BulletList, - OrderedList, - Todo, - AdvancedTodo, - Quote, - CodeBlock, - Divider, - SimpleTable, - Mindmap, - Image, - UploadAttachment, - Toc, -} - -#[derive(Clone, Copy)] -struct SlashAction { - kind: SlashActionKind, - id: &'static str, - category: &'static str, - icon: &'static str, - label: &'static str, - description: &'static str, - shortcut: &'static str, -} - #[derive(Clone, Copy)] struct FoldedHeadingAction { level: u8, @@ -2821,207 +2790,6 @@ const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [ }, ]; -const SLASH_ACTIONS: [SlashAction; 22] = [ - SlashAction { - kind: SlashActionKind::AiAssistant, - id: "ai-assistant", - category: "AI 助理", - icon: "✦", - label: "AI 助理", - description: "按 Wolai 基线保留 AI 入口", - shortcut: "/ai", - }, - SlashAction { - kind: SlashActionKind::AiWrite, - id: "ai-write", - category: "AI 助理", - icon: "✎", - label: "用 AI 写作", - description: "唤起写作辅助入口", - shortcut: "/yaixz", - }, - SlashAction { - kind: SlashActionKind::ContinueWriting, - id: "continue-writing", - category: "AI 助理", - icon: "↪", - label: "续写", - description: "按当前上下文继续写作", - shortcut: "/xx", - }, - SlashAction { - kind: SlashActionKind::Summarize, - id: "summarize", - category: "AI 助理", - icon: "≡", - label: "总结", - description: "对当前内容生成摘要", - shortcut: "/zj", - }, - SlashAction { - kind: SlashActionKind::MoreAi, - id: "more-ai", - category: "AI 助理", - icon: "…", - label: "更多", - description: "更多 AI 命令", - shortcut: "›", - }, - SlashAction { - kind: SlashActionKind::Paragraph, - id: "paragraph", - category: "基础块列表", - icon: "Aa", - label: "文本", - description: "普通正文块", - shortcut: "/wb", - }, - SlashAction { - kind: SlashActionKind::Todo, - id: "todo", - category: "基础块列表", - icon: "☑", - label: "待办列表", - description: "创建可勾选任务", - shortcut: "/dblb", - }, - SlashAction { - kind: SlashActionKind::AdvancedTodo, - id: "advanced-todo", - category: "基础块列表", - icon: "☑", - label: "高级待办列表", - description: "保留 Wolai 高级待办入口", - shortcut: "/gjdblb", - }, - SlashAction { - kind: SlashActionKind::Heading1, - id: "heading-1", - category: "基础块列表", - icon: "H1", - label: "主标题", - description: "一级标题", - shortcut: "/h1", - }, - SlashAction { - kind: SlashActionKind::Heading2, - id: "heading-2", - category: "基础块列表", - icon: "H2", - label: "大标题", - description: "二级标题", - shortcut: "/h2", - }, - SlashAction { - kind: SlashActionKind::Heading3, - id: "heading-3", - category: "基础块列表", - icon: "H3", - label: "中标题", - description: "三级标题", - shortcut: "/h3", - }, - SlashAction { - kind: SlashActionKind::Heading4, - id: "heading-4", - category: "基础块列表", - icon: "H4", - label: "小标题", - description: "四级标题", - shortcut: "/h4", - }, - SlashAction { - kind: SlashActionKind::BulletList, - id: "bullet", - category: "基础块列表", - icon: "•", - label: "列表", - description: "记录普通要点", - shortcut: "/lb", - }, - SlashAction { - kind: SlashActionKind::OrderedList, - id: "ordered", - category: "基础块列表", - icon: "1.", - label: "数字列表", - description: "记录步骤顺序", - shortcut: "/szlb", - }, - SlashAction { - kind: SlashActionKind::Quote, - id: "quote", - category: "基础块列表", - icon: "❝", - label: "引述文字", - description: "包住注释与摘录", - shortcut: "/ys", - }, - SlashAction { - kind: SlashActionKind::CodeBlock, - id: "code-block", - category: "基础块列表", - icon: "<> ", - label: "代码片段", - description: "插入带语义的代码块", - shortcut: "/dm", - }, - SlashAction { - kind: SlashActionKind::Divider, - id: "divider", - category: "基础块列表", - icon: "—", - label: "分割线", - description: "插入分隔线", - shortcut: "/fgx", - }, - SlashAction { - kind: SlashActionKind::SimpleTable, - id: "simple-table", - category: "进阶块列表", - icon: "▦", - label: "简单表格", - description: "插入基础表格", - shortcut: "/jdbg", - }, - SlashAction { - kind: SlashActionKind::Mindmap, - id: "mindmap", - category: "进阶块列表", - icon: "⌘", - label: "思维导图", - description: "插入 kernel projection 导图块", - shortcut: "/dt", - }, - SlashAction { - kind: SlashActionKind::Image, - id: "image", - category: "媒体与附件", - icon: "▧", - label: "图片", - description: "上传并插入图片", - shortcut: "/tp", - }, - SlashAction { - kind: SlashActionKind::UploadAttachment, - id: "upload-attachment", - category: "媒体与附件", - icon: "↥", - label: "上传附件", - description: "上传 Office、PDF 或其他文件", - shortcut: "/fj", - }, - SlashAction { - kind: SlashActionKind::Toc, - id: "toc", - category: "进阶块列表", - icon: "☰", - label: "页面目录", - description: "根据当前标题生成目录", - shortcut: "/toc", - }, -]; - #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct LegacyHostEnvelope { @@ -3045,90 +2813,10 @@ struct HostDocumentPayload { read_only: Option, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum HostCommandKind { - Undo, - Redo, - ReplaceContent, - SetPageOptions, - InsertInlineReference, - InsertEmbedReference, - RequestCurrentBlockId, - SetEditable, - Focus, - Bootstrap, -} - -impl HostCommandKind { - fn from_event_name(event_name: &str) -> Option { - match event_name { - "undo" => Some(Self::Undo), - "redo" => Some(Self::Redo), - "replaceContent" | "replace-document" => Some(Self::ReplaceContent), - "setPageOptions" | "set-page-options" => Some(Self::SetPageOptions), - "insertInlineReference" => Some(Self::InsertInlineReference), - "insertEmbedReference" => Some(Self::InsertEmbedReference), - "requestCurrentBlockId" => Some(Self::RequestCurrentBlockId), - "setEditable" | "set-editable" => Some(Self::SetEditable), - "focus" => Some(Self::Focus), - "bootstrap" => Some(Self::Bootstrap), - _ => None, - } - } - - fn from_payload(payload: &HostCommandPayload) -> Option { - payload.command.as_deref().and_then(Self::from_event_name) - } -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn parses_command_aliases_and_canonical_names() { - assert_eq!( - HostCommandKind::from_event_name("replace-document"), - Some(HostCommandKind::ReplaceContent) - ); - assert_eq!( - HostCommandKind::from_event_name("replaceContent"), - Some(HostCommandKind::ReplaceContent) - ); - assert_eq!( - HostCommandKind::from_event_name("requestCurrentBlockId"), - Some(HostCommandKind::RequestCurrentBlockId) - ); - } - - #[test] - fn reads_command_from_payload_field() { - let payload = HostCommandPayload { - command: Some("setEditable".to_string()), - document_id: None, - workspace_id: None, - title: None, - content: None, - editable: Some(true), - page_options: None, - block_id: None, - block_index: None, - text: None, - reference_document_id: None, - reference_block_id: None, - current_block_id: None, - selection: None, - revision: None, - conflict_detection_key: None, - read_only: None, - }; - - assert_eq!( - HostCommandKind::from_payload(&payload), - Some(HostCommandKind::SetEditable) - ); - } - #[test] fn resolves_embedded_mode_from_explicit_runtime_mode_first() { assert_eq!( @@ -4292,184 +3980,6 @@ fn apply_image_align( .map_err(|err| format!("命令执行失败:{err}")) } -#[derive(Clone, Copy)] -enum TableToolbarAction { - AddRowAfter, - AddColumnAfter, - DeleteRow, - DeleteColumn, - ClearCell, - DeleteTable, -} - -#[derive(Clone, Copy)] -enum TableOptionAction { - ToggleHeaderRow, - ToggleHeaderColumn, - ToggleHiddenBorders, -} - -impl TableToolbarAction { - fn id(self) -> &'static str { - match self { - Self::AddRowAfter => "add-row-after", - Self::AddColumnAfter => "add-column-after", - Self::DeleteRow => "delete-row", - Self::DeleteColumn => "delete-column", - Self::ClearCell => "clear-cell", - Self::DeleteTable => "delete-table", - } - } - - fn icon(self) -> &'static str { - match self { - Self::AddRowAfter => "+R", - Self::AddColumnAfter => "+C", - Self::DeleteRow => "-R", - Self::DeleteColumn => "-C", - Self::ClearCell => "⌫", - Self::DeleteTable => "×", - } - } - - fn title(self) -> &'static str { - match self { - Self::AddRowAfter => "下方插入行", - Self::AddColumnAfter => "右侧插入列", - Self::DeleteRow => "删除当前行", - Self::DeleteColumn => "删除当前列", - Self::ClearCell => "清空当前单元格", - Self::DeleteTable => "删除表格", - } - } -} - -const TABLE_TOOLBAR_ACTIONS: [TableToolbarAction; 6] = [ - TableToolbarAction::AddRowAfter, - TableToolbarAction::AddColumnAfter, - TableToolbarAction::ClearCell, - TableToolbarAction::DeleteRow, - TableToolbarAction::DeleteColumn, - TableToolbarAction::DeleteTable, -]; - -impl TableOptionAction { - fn id(self) -> &'static str { - match self { - Self::ToggleHeaderRow => "toggle-header-row", - Self::ToggleHeaderColumn => "toggle-header-column", - Self::ToggleHiddenBorders => "toggle-hidden-borders", - } - } - - fn title(self) -> &'static str { - match self { - Self::ToggleHeaderRow => "标题行", - Self::ToggleHeaderColumn => "标题列", - Self::ToggleHiddenBorders => "隐藏边框线", - } - } -} - -const TABLE_OPTION_ACTIONS: [TableOptionAction; 3] = [ - TableOptionAction::ToggleHeaderRow, - TableOptionAction::ToggleHeaderColumn, - TableOptionAction::ToggleHiddenBorders, -]; - -fn run_table_toolbar_action( - editor: TiptapEditorHandle, - action: TableToolbarAction, -) -> Result<&'static str, String> { - let result = match action { - TableToolbarAction::AddRowAfter => editor.add_table_row_after(), - TableToolbarAction::AddColumnAfter => editor.add_table_column_after(), - TableToolbarAction::DeleteRow => editor.delete_table_row(), - TableToolbarAction::DeleteColumn => editor.delete_table_column(), - TableToolbarAction::ClearCell => editor.clear_table_cell(), - TableToolbarAction::DeleteTable => editor.delete_table(), - }; - result - .map(|_| action.title()) - .map_err(|err| format!("命令执行失败:{err}")) -} - -fn run_table_option_action( - editor: TiptapEditorHandle, - action: TableOptionAction, -) -> Result<&'static str, String> { - let result = match action { - TableOptionAction::ToggleHeaderRow => editor.toggle_table_header_row(), - TableOptionAction::ToggleHeaderColumn => editor.toggle_table_header_column(), - TableOptionAction::ToggleHiddenBorders => editor.toggle_table_hidden_borders(), - }; - result - .map(|_| action.title()) - .map_err(|err| format!("命令执行失败:{err}")) -} - -fn first_table_node(value: &Value) -> Option<&Value> { - if value.get("type").and_then(Value::as_str) == Some("table") { - return Some(value); - } - if let Some(table) = value - .get("props") - .and_then(|props| props.get("tiptapTable")) - .and_then(first_table_node) - { - return Some(table); - } - for key in ["content", "children"] { - if let Some(children) = value.get(key).and_then(Value::as_array) { - if let Some(table) = children.iter().find_map(first_table_node) { - return Some(table); - } - } - } - None -} - -fn table_option_checked(document: &Value, action: TableOptionAction) -> bool { - let Some(table) = first_table_node(document) else { - return false; - }; - match action { - TableOptionAction::ToggleHiddenBorders => table - .get("attrs") - .and_then(|attrs| attrs.get("hiddenBorders")) - .and_then(Value::as_bool) - .unwrap_or(false), - TableOptionAction::ToggleHeaderRow => table - .get("content") - .and_then(Value::as_array) - .and_then(|rows| rows.first()) - .and_then(|row| row.get("content")) - .and_then(Value::as_array) - .map(|cells| { - !cells.is_empty() - && cells - .iter() - .all(|cell| cell.get("type").and_then(Value::as_str) == Some("tableHeader")) - }) - .unwrap_or(false), - TableOptionAction::ToggleHeaderColumn => table - .get("content") - .and_then(Value::as_array) - .map(|rows| { - !rows.is_empty() - && rows.iter().all(|row| { - row.get("content") - .and_then(Value::as_array) - .and_then(|cells| cells.first()) - .and_then(|cell| cell.get("type")) - .and_then(Value::as_str) - == Some("tableHeader") - }) - }) - .unwrap_or(false), - } -} - fn run_slash_action( editor: TiptapEditorHandle, action: SlashActionKind,