refactor: extract editor action modules
This commit is contained in:
@@ -186,7 +186,7 @@ codegraph sync .
|
|||||||
|----|------|
|
|----|------|
|
||||||
| SIDEBAR_TREE_JS 外置 | 布局文件从 11,741 → ~1,100 行 |
|
| SIDEBAR_TREE_JS 外置 | 布局文件从 11,741 → ~1,100 行 |
|
||||||
| browser/ 模块 | 11 个 JS 文件,总计 ~11,000 行 |
|
| browser/ 模块 | 11 个 JS 文件,总计 ~11,000 行 |
|
||||||
| editor_runtime/ 模块 | 16 个 Rust 模块(另有 mod.rs) |
|
| editor_runtime/ 模块 | 19 个 Rust 模块(另有 mod.rs) |
|
||||||
| 浏览器 smoke | task479/488/489/451 全部 PASS |
|
| 浏览器 smoke | task479/488/489/451 全部 PASS |
|
||||||
| 测试 | editor 13/13, web sidebar 10/10, web full 531/549 |
|
| 测试 | editor 13/13, web sidebar 10/10, web full 531/549 |
|
||||||
| CodeGraph | 350+ 文件索引完成 |
|
| 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 回归与直接编辑器交互回归 |
|
||||||
|
|||||||
@@ -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/mindmap_node_view.rs` | — | 909 行 | 新建 |
|
||||||
| `editor_runtime/bridge_events.rs` | — | 149 行 | 新建 |
|
| `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_hover_state.rs` | HoveredBlockState 共享类型 | 5-29 |
|
||||||
| `block_menu_document.rs` | legacy JSON helper | 5-29 |
|
| `block_menu_document.rs` | legacy JSON helper | 5-29 |
|
||||||
| `block_menu_overlay.rs` | block menu 几何 | 5-29 |
|
| `block_menu_overlay.rs` | block menu 几何 | 5-29 |
|
||||||
|
| `bridge_dispatch.rs` | host command kind 解析 | **本批新建** |
|
||||||
| `bridge_events.rs` | 事件常量 + payload 类型 | **本批新建** |
|
| `bridge_events.rs` | 事件常量 + payload 类型 | **本批新建** |
|
||||||
| `command_sync.rs` | 命令同步 | 5-28 P2 |
|
| `command_sync.rs` | 命令同步 | 5-28 P2 |
|
||||||
| `content_layout.rs` | 内容布局 | 5-29 |
|
| `content_layout.rs` | 内容布局 | 5-29 |
|
||||||
@@ -430,6 +434,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
|||||||
| `mindmap_node_view.rs` | mindmap shell 完整实现 | **本批新建**(5-28 P5) |
|
| `mindmap_node_view.rs` | mindmap shell 完整实现 | **本批新建**(5-28 P5) |
|
||||||
| `overlays.rs` | overlay 管理 | 5-29 |
|
| `overlays.rs` | overlay 管理 | 5-29 |
|
||||||
| `persistence.rs` | 持久化 | 5-28 P2 |
|
| `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`。
|
- 拖拽重排两条路径与块菜单“拷贝副本”不再读取/写回整文 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 已重新生成。
|
- 删除本轮变成无调用方的 `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 + 块拖拽重排回归。
|
- 已验证:`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 回归。
|
||||||
|
|||||||
+9
-9
@@ -1244,47 +1244,47 @@ function __wbg_get_imports() {
|
|||||||
}
|
}
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
__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);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
__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);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
__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);
|
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
|
__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);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
|
__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);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
|
__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);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
|
__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);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000008: function(arg0, arg1) {
|
__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);
|
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000009: function(arg0, arg1) {
|
__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);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ pub(crate) mod block_dnd;
|
|||||||
pub(crate) mod block_hover_state;
|
pub(crate) mod block_hover_state;
|
||||||
pub(crate) mod block_menu_document;
|
pub(crate) mod block_menu_document;
|
||||||
pub(crate) mod block_menu_overlay;
|
pub(crate) mod block_menu_overlay;
|
||||||
|
pub(crate) mod bridge_dispatch;
|
||||||
pub(crate) mod bridge_events;
|
pub(crate) mod bridge_events;
|
||||||
pub(crate) mod command_sync;
|
pub(crate) mod command_sync;
|
||||||
pub(crate) mod content_layout;
|
pub(crate) mod content_layout;
|
||||||
@@ -14,3 +15,5 @@ pub(crate) mod history_safe_commands;
|
|||||||
pub(crate) mod mindmap_node_view;
|
pub(crate) mod mindmap_node_view;
|
||||||
pub(crate) mod overlays;
|
pub(crate) mod overlays;
|
||||||
pub(crate) mod persistence;
|
pub(crate) mod persistence;
|
||||||
|
pub(crate) mod slash_actions;
|
||||||
|
pub(crate) mod table_commands;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ use editor_runtime::block_menu_document::{
|
|||||||
use editor_runtime::block_hover_state::{
|
use editor_runtime::block_hover_state::{
|
||||||
BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
|
BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
|
||||||
};
|
};
|
||||||
|
use editor_runtime::bridge_dispatch::HostCommandKind;
|
||||||
use editor_runtime::bridge_events::{
|
use editor_runtime::bridge_events::{
|
||||||
BridgeEnvelope, BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload,
|
BridgeEnvelope, BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload,
|
||||||
HostCommandEnvelope, HostCommandPayload, HostStatusPayload, ReadyPayload, RuntimePageOptions,
|
HostCommandEnvelope, HostCommandPayload, HostStatusPayload, ReadyPayload, RuntimePageOptions,
|
||||||
@@ -70,6 +71,11 @@ use editor_runtime::persistence::{
|
|||||||
load_persisted_document, normalize_identity_value, persist_document_state,
|
load_persisted_document, normalize_identity_value, persist_document_state,
|
||||||
persisted_document_identity, PersistedDocumentIdentity,
|
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)]
|
#[cfg(test)]
|
||||||
use editor_runtime::persistence::persisted_document_storage_key;
|
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)]
|
#[derive(Clone, Copy)]
|
||||||
struct FoldedHeadingAction {
|
struct FoldedHeadingAction {
|
||||||
level: u8,
|
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)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct LegacyHostEnvelope {
|
struct LegacyHostEnvelope {
|
||||||
@@ -3045,90 +2813,10 @@ struct HostDocumentPayload {
|
|||||||
read_only: Option<bool>,
|
read_only: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
payload.command.as_deref().and_then(Self::from_event_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn resolves_embedded_mode_from_explicit_runtime_mode_first() {
|
fn resolves_embedded_mode_from_explicit_runtime_mode_first() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4292,184 +3980,6 @@ fn apply_image_align(
|
|||||||
.map_err(|err| format!("命令执行失败:{err}"))
|
.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(
|
fn run_slash_action(
|
||||||
editor: TiptapEditorHandle,
|
editor: TiptapEditorHandle,
|
||||||
action: SlashActionKind,
|
action: SlashActionKind,
|
||||||
|
|||||||
Reference in New Issue
Block a user