refactor: prune block transform helpers
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 1. 目标
|
## 1. 目标
|
||||||
|
|
||||||
在 `3-19` 已归档的基础上,继续把仍留在 Rust inline / 主壳大 runtime 中的浏览器行为切成可审计模块。Codex 维护本文件作为事实源,Reasonix 只承接短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 2 个 worker。
|
在 `3-19` 已归档的基础上,继续把仍留在 Rust inline / 主壳大 runtime 中的浏览器行为切成可审计模块。Codex 维护本文件作为事实源,Reasonix 只承接短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 4 个 worker。
|
||||||
|
|
||||||
## 2. 本轮边界
|
## 2. 本轮边界
|
||||||
|
|
||||||
@@ -172,3 +172,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
|||||||
- 2026-05-25:Codex 独立复核并采纳 Batch Q Worker A 遗留候选 patch:`local_folder_events.rs` 在 `treeLive=true` 且 watcher subscribe 失败时不再返回 500,而是返回同一 SSE 响应并降级为 revision polling;初始仍发送 `snapshot`,revision 变化后通过既有 `rebuild_tree_resync_payload` 发送 `resync`。客户端 `tree:snapshot` / `tree:resync` consumer 不改,避免叠加 browser double-polling。该 patch 来自无 handoff 的失控 worker,未按 Reasonix 结果直接采纳,而是由 Codex 本地阅读 diff、跑验证后作为主控修复纳入。
|
- 2026-05-25:Codex 独立复核并采纳 Batch Q Worker A 遗留候选 patch:`local_folder_events.rs` 在 `treeLive=true` 且 watcher subscribe 失败时不再返回 500,而是返回同一 SSE 响应并降级为 revision polling;初始仍发送 `snapshot`,revision 变化后通过既有 `rebuild_tree_resync_payload` 发送 `resync`。客户端 `tree:snapshot` / `tree:resync` consumer 不改,避免叠加 browser double-polling。该 patch 来自无 handoff 的失控 worker,未按 Reasonix 结果直接采纳,而是由 Codex 本地阅读 diff、跑验证后作为主控修复纳入。
|
||||||
- 修改:`rust/crates/mnote-web/src/routes/local_folder_events.rs`。
|
- 修改:`rust/crates/mnote-web/src/routes/local_folder_events.rs`。
|
||||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_live_polling_resync_payload_tracks_revision_changes -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_events -- --test-threads=1`、`git diff --check`、`node --check scripts/task487-local-folder-tree-live-consumer-smoke.js`、临时 `MNOTE_WEB_BIND=127.0.0.1:3002 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3002 cargo run --manifest-path rust/Cargo.toml -p mnote-web` 后运行 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3002 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task487-local-folder-tree-live-consumer-smoke.js`,结果 `ok:true`,PageTree/FileTree 外部创建/删除均为 `treeLiveApplied=resync`,两个阶段 `navigationEvents=0`。
|
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_live_polling_resync_payload_tracks_revision_changes -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_events -- --test-threads=1`、`git diff --check`、`node --check scripts/task487-local-folder-tree-live-consumer-smoke.js`、临时 `MNOTE_WEB_BIND=127.0.0.1:3002 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3002 cargo run --manifest-path rust/Cargo.toml -p mnote-web` 后运行 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3002 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task487-local-folder-tree-live-consumer-smoke.js`,结果 `ok:true`,PageTree/FileTree 外部创建/删除均为 `treeLiveApplied=resync`,两个阶段 `navigationEvents=0`。
|
||||||
|
- 2026-05-25:Batch Z 曾一次派发 4 个 Reasonix worker,超过当前 goal 的“每批最多 2 个 worker”;其中 Worker C 负责 3-20 filetree refresh helper,Worker D 负责 3-20 tree live smoke;四个 run 均未产出 `final.md`、`result.json`、`process-handoff.md/json`,worktree diff 为空,全部拒收。
|
||||||
|
- 涉及 run:`reasonix-2026-05-24T22-02-32-495Z-dfd6bb89`、`reasonix-2026-05-24T22-02-32-511Z-ea50d6e0`、`reasonix-2026-05-24T22-02-32-635Z-4edc4f8a`、`reasonix-2026-05-24T22-02-32-644Z-1180a395`。
|
||||||
|
- 额外异常:Reasonix 仍创建 `.codex/reasonix-tasks/2026-05-25-batch-x-worker-a-5-29-dnd-dom-readonly.md` 草稿,违反叶子 worker 约束;该草稿不提交。
|
||||||
|
- 本批未采纳任何 3-20 diff。主工作区仅为 5-29 block transform 候选 patch 补跑 `task479-local-folder-markdown-resource-lifecycle-smoke.js` 作为资源生命周期回归,结果 `ok:true`,截图 `/tmp/mnote-task479-lifecycle-RJDsKO/screenshots-1779660971673/final-state.png`。
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 1. 目标
|
## 1. 目标
|
||||||
|
|
||||||
在 `5-28` 已完成附件链接、history-safe delete、command sync、persistence、attachment upload intent 首批拆分后,继续把 editor runtime 的 selection / overlay / block menu / mindmap node view 切成可索引模块。Codex 维护本文件作为事实源,Reasonix 只做短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 2 个 worker。
|
在 `5-28` 已完成附件链接、history-safe delete、command sync、persistence、attachment upload intent 首批拆分后,继续把 editor runtime 的 selection / overlay / block menu / mindmap node view 切成可索引模块。Codex 维护本文件作为事实源,Reasonix 只做短批次审计、窄范围实现或浏览器 smoke;当前 goal 下每批最多 4 个 worker。
|
||||||
|
|
||||||
## 2. 本轮边界
|
## 2. 本轮边界
|
||||||
|
|
||||||
@@ -217,3 +217,10 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
|||||||
- 未迁:`target_element` 仍被图片 toolbar 和事件 listener 共用;`hovered_block_from_target`、`drop_indicator_from_target`、`drop_indicator_from_point`、拖拽 listener、block reorder 行为和 DOM -> block 状态解析链仍留在 `lib.rs`。
|
- 未迁:`target_element` 仍被图片 toolbar 和事件 listener 共用;`hovered_block_from_target`、`drop_indicator_from_target`、`drop_indicator_from_point`、拖拽 listener、block reorder 行为和 DOM -> block 状态解析链仍留在 `lib.rs`。
|
||||||
- 修改:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_dnd.rs`、`rust/spikes/leptos-tiptap-spike/src/lib.rs`、generated island `js/wasm`。
|
- 修改:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_dnd.rs`、`rust/spikes/leptos-tiptap-spike/src/lib.rs`、generated island `js/wasm`。
|
||||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。
|
||||||
|
- 2026-05-25:Batch Z 曾一次派发 4 个 Reasonix worker,超过当前 goal 的“每批最多 2 个 worker”;四个 worker 均使用独立 worktree:A 只读审计 block transform、B 实现 block transform、C 实现 3-20 filetree refresh helper、D 运行 3-20 live smoke。
|
||||||
|
- Reasonix run `reasonix-2026-05-24T22-02-32-495Z-dfd6bb89` / `reasonix-2026-05-24T22-02-32-511Z-ea50d6e0` / `reasonix-2026-05-24T22-02-32-635Z-4edc4f8a` / `reasonix-2026-05-24T22-02-32-644Z-1180a395` 均只有 `prompt.md`、`memory-recall.json`、`reasonix-transcript.jsonl`,无 `final.md`、`result.json`、`process-handoff.md/json`;四个 worktree diff 为空,按采纳规则全部拒收。
|
||||||
|
- 额外异常:Reasonix 仍创建 `.codex/reasonix-tasks/2026-05-25-batch-x-worker-a-5-29-dnd-dom-readonly.md` 草稿,违反叶子 worker 约束;该草稿不纳入本文件事实源、不提交。
|
||||||
|
- Codex 随后在独立 worktree `/mnt/Data1T/mnote-worktrees/0525-x-worker-b-5-29-block-transform-impl` 产出并复核候选 patch:把仍有真实调用方的 `paragraph_node`、`collect_plain_text`、`mindmap_paragraph_node` 迁入 `editor_runtime/block_menu_document.rs`,删除 `lib.rs` 中无调用方的旧 JSON transform 死链 `list_item_node`、`task_item_node`、`inline_from_first_child`、`extract_inline_content`、`turn_into_block`、`replace_top_level_block_kind` 及图片占位常量。
|
||||||
|
- 修改:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_document.rs`、`rust/spikes/leptos-tiptap-spike/src/lib.rs`。
|
||||||
|
- 已验证:候选 worktree中 `git diff --check`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen` 通过;主工作区复跑 `git diff --check`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3001 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js` 通过。
|
||||||
|
- 未完成:`block_menu_document.rs` 中 `duplicate_top_level_block`、`delete_top_level_block`、`reorder_top_level_block` 仍显示无 runtime 调用;是否删除需另起只读审计确认历史测试 / future intent,不在本批扩大。
|
||||||
|
|||||||
+7
-7
@@ -1270,7 +1270,7 @@ function __wbg_get_imports() {
|
|||||||
},
|
},
|
||||||
__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: 973, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 973, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||||
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h19d53c3ed5a90a91);
|
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) {
|
||||||
@@ -1284,8 +1284,8 @@ function __wbg_get_imports() {
|
|||||||
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: 799, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 892, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha181c94e3b9968a5);
|
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) {
|
||||||
@@ -1359,8 +1359,8 @@ function wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5(arg0, arg
|
|||||||
wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5(arg0, arg1, arg2);
|
wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__h19d53c3ed5a90a91(arg0, arg1, arg2) {
|
function wasm_bindgen__convert__closures_____invoke__had771ddc65647798(arg0, arg1, arg2) {
|
||||||
wasm.wasm_bindgen__convert__closures_____invoke__h19d53c3ed5a90a91(arg0, arg1, arg2);
|
wasm.wasm_bindgen__convert__closures_____invoke__had771ddc65647798(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a(arg0, arg1, arg2) {
|
function wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a(arg0, arg1, arg2) {
|
||||||
@@ -1371,8 +1371,8 @@ function wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, a
|
|||||||
wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, arg1, arg2);
|
wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__ha181c94e3b9968a5(arg0, arg1, arg2) {
|
function wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2) {
|
||||||
wasm.wasm_bindgen__convert__closures_____invoke__ha181c94e3b9968a5(arg0, arg1, arg2);
|
wasm.wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12(arg0, arg1, arg2) {
|
function wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12(arg0, arg1, arg2) {
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
//! block menu 使用的顶层块 JSON helper。
|
//! block menu 使用的顶层块 JSON helper。
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
use crate::editor_runtime::block_hover_state::DropPlacement;
|
use crate::editor_runtime::block_hover_state::DropPlacement;
|
||||||
|
|
||||||
@@ -65,6 +65,57 @@ pub(crate) fn reorder_top_level_block(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn node_with_attrs(
|
||||||
|
type_name: &str,
|
||||||
|
attrs: Option<Map<String, Value>>,
|
||||||
|
content: Vec<Value>,
|
||||||
|
) -> Value {
|
||||||
|
let mut node = Map::new();
|
||||||
|
node.insert("type".to_string(), Value::String(type_name.to_string()));
|
||||||
|
if let Some(attrs) = attrs.filter(|attrs| !attrs.is_empty()) {
|
||||||
|
node.insert("attrs".to_string(), Value::Object(attrs));
|
||||||
|
}
|
||||||
|
if !content.is_empty() {
|
||||||
|
node.insert("content".to_string(), Value::Array(content));
|
||||||
|
}
|
||||||
|
Value::Object(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn paragraph_node(content: Vec<Value>) -> Value {
|
||||||
|
node_with_attrs("paragraph", None, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collect_plain_text(node: &Value) -> String {
|
||||||
|
match node.get("type").and_then(Value::as_str) {
|
||||||
|
Some("text") => node
|
||||||
|
.get("text")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
Some("hardBreak") => "\n".to_string(),
|
||||||
|
_ => node
|
||||||
|
.get("content")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.map(|children| {
|
||||||
|
children
|
||||||
|
.iter()
|
||||||
|
.map(collect_plain_text)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("")
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mindmap_paragraph_node(next_mindmap_id: impl FnOnce() -> String) -> Value {
|
||||||
|
let mut attrs = Map::new();
|
||||||
|
attrs.insert("mnoteBlockType".to_string(), json!("mindmap"));
|
||||||
|
attrs.insert("mindmapId".to_string(), json!(next_mindmap_id()));
|
||||||
|
attrs.insert("rootNodeId".to_string(), json!("root"));
|
||||||
|
attrs.insert("projectionVersion".to_string(), json!(1));
|
||||||
|
node_with_attrs("paragraph", Some(attrs), Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use leptos_tiptap::{
|
|||||||
};
|
};
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Value};
|
||||||
use std::{any::Any, cell::Cell, collections::HashMap, fmt::Display};
|
use std::{any::Any, cell::Cell, collections::HashMap, fmt::Display};
|
||||||
use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
|
use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
|
||||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||||
@@ -27,6 +27,9 @@ use editor_runtime::block_dnd::{
|
|||||||
HandleCorridorGeometry,
|
HandleCorridorGeometry,
|
||||||
};
|
};
|
||||||
use editor_runtime::block_menu_overlay;
|
use editor_runtime::block_menu_overlay;
|
||||||
|
use editor_runtime::block_menu_document::{
|
||||||
|
collect_plain_text, mindmap_paragraph_node, paragraph_node,
|
||||||
|
};
|
||||||
use editor_runtime::block_menu_legacy_html::{
|
use editor_runtime::block_menu_legacy_html::{
|
||||||
duplicate_top_level_block_html, reorder_top_level_block_html,
|
duplicate_top_level_block_html, reorder_top_level_block_html,
|
||||||
};
|
};
|
||||||
@@ -81,10 +84,6 @@ const MINDMAP_SHELL_MINIMAP_EVENT: &str = "mnote:mindmap-shell:minimap";
|
|||||||
const MINDMAP_SHELL_ZOOM_EVENT: &str = "mnote:mindmap-shell:zoom";
|
const MINDMAP_SHELL_ZOOM_EVENT: &str = "mnote:mindmap-shell:zoom";
|
||||||
const MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT: &str = "mnote:mindmap-shell:toolbar-overflow";
|
const MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT: &str = "mnote:mindmap-shell:toolbar-overflow";
|
||||||
const STANDALONE_ROOT_ID: &str = "mnote-leptos-tiptap-standalone-root";
|
const STANDALONE_ROOT_ID: &str = "mnote-leptos-tiptap-standalone-root";
|
||||||
const E24_IMAGE_PLACEHOLDER_SRC: &str = "/api/editor/image-placeholder.svg";
|
|
||||||
const E24_IMAGE_PLACEHOLDER_ALT: &str = "E24 图片占位";
|
|
||||||
const E24_IMAGE_PLACEHOLDER_TITLE: &str = "E24 图片";
|
|
||||||
|
|
||||||
mod editor_runtime;
|
mod editor_runtime;
|
||||||
|
|
||||||
#[wasm_bindgen(inline_js = r#"
|
#[wasm_bindgen(inline_js = r#"
|
||||||
@@ -5261,26 +5260,6 @@ fn document_content_mut(document: &mut Value) -> Result<&mut Vec<Value>, String>
|
|||||||
.ok_or_else(|| "文档 JSON 缺少顶层 content 数组".to_string())
|
.ok_or_else(|| "文档 JSON 缺少顶层 content 数组".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_with_attrs(
|
|
||||||
type_name: &str,
|
|
||||||
attrs: Option<Map<String, Value>>,
|
|
||||||
content: Vec<Value>,
|
|
||||||
) -> Value {
|
|
||||||
let mut node = Map::new();
|
|
||||||
node.insert("type".to_string(), Value::String(type_name.to_string()));
|
|
||||||
if let Some(attrs) = attrs.filter(|attrs| !attrs.is_empty()) {
|
|
||||||
node.insert("attrs".to_string(), Value::Object(attrs));
|
|
||||||
}
|
|
||||||
if !content.is_empty() {
|
|
||||||
node.insert("content".to_string(), Value::Array(content));
|
|
||||||
}
|
|
||||||
Value::Object(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paragraph_node(content: Vec<Value>) -> Value {
|
|
||||||
node_with_attrs("paragraph", None, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_mindmap_id() -> String {
|
fn next_mindmap_id() -> String {
|
||||||
let now = js_sys::Date::new_0();
|
let now = js_sys::Date::new_0();
|
||||||
format!(
|
format!(
|
||||||
@@ -5291,181 +5270,6 @@ fn next_mindmap_id() -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mindmap_paragraph_node() -> Value {
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("mnoteBlockType".to_string(), json!("mindmap"));
|
|
||||||
attrs.insert("mindmapId".to_string(), json!(next_mindmap_id()));
|
|
||||||
attrs.insert("rootNodeId".to_string(), json!("root"));
|
|
||||||
attrs.insert("projectionVersion".to_string(), json!(1));
|
|
||||||
node_with_attrs("paragraph", Some(attrs), Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_item_node(content: Vec<Value>) -> Value {
|
|
||||||
node_with_attrs("listItem", None, vec![paragraph_node(content)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_item_node(content: Vec<Value>) -> Value {
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("checked".to_string(), Value::Bool(false));
|
|
||||||
node_with_attrs("taskItem", Some(attrs), vec![paragraph_node(content)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_plain_text(node: &Value) -> String {
|
|
||||||
match node.get("type").and_then(Value::as_str) {
|
|
||||||
Some("text") => node
|
|
||||||
.get("text")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string(),
|
|
||||||
Some("hardBreak") => "\n".to_string(),
|
|
||||||
_ => node
|
|
||||||
.get("content")
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.map(|children| {
|
|
||||||
children
|
|
||||||
.iter()
|
|
||||||
.map(collect_plain_text)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("")
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inline_from_first_child(children: &[Value]) -> Vec<Value> {
|
|
||||||
children
|
|
||||||
.first()
|
|
||||||
.map(extract_inline_content)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_inline_content(node: &Value) -> Vec<Value> {
|
|
||||||
match node.get("type").and_then(Value::as_str) {
|
|
||||||
Some("paragraph") | Some("heading") => node
|
|
||||||
.get("content")
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
Some("blockquote") => node
|
|
||||||
.get("content")
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.map(|children| inline_from_first_child(children))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
Some("bulletList") | Some("orderedList") | Some("taskList") => node
|
|
||||||
.get("content")
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.and_then(|items| items.first())
|
|
||||||
.and_then(|item| item.get("content"))
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.map(|children| inline_from_first_child(children))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
Some("codeBlock") => {
|
|
||||||
let text = collect_plain_text(node);
|
|
||||||
if text.is_empty() {
|
|
||||||
Vec::new()
|
|
||||||
} else {
|
|
||||||
vec![json!({"type": "text", "text": text})]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("horizontalRule") => Vec::new(),
|
|
||||||
_ => node
|
|
||||||
.get("content")
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.map(|children| {
|
|
||||||
children
|
|
||||||
.iter()
|
|
||||||
.flat_map(extract_inline_content)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn turn_into_block(node: &Value, action: SlashActionKind) -> Value {
|
|
||||||
let inline = extract_inline_content(node);
|
|
||||||
match action {
|
|
||||||
SlashActionKind::AiAssistant
|
|
||||||
| SlashActionKind::AiWrite
|
|
||||||
| SlashActionKind::ContinueWriting
|
|
||||||
| SlashActionKind::Summarize
|
|
||||||
| SlashActionKind::MoreAi => paragraph_node(inline.clone()),
|
|
||||||
SlashActionKind::Paragraph => paragraph_node(inline),
|
|
||||||
SlashActionKind::Heading1 => {
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("level".to_string(), json!(1));
|
|
||||||
attrs.insert("collapsed".to_string(), Value::Null);
|
|
||||||
node_with_attrs("heading", Some(attrs), inline)
|
|
||||||
}
|
|
||||||
SlashActionKind::Heading2 => {
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("level".to_string(), json!(2));
|
|
||||||
attrs.insert("collapsed".to_string(), Value::Null);
|
|
||||||
node_with_attrs("heading", Some(attrs), inline)
|
|
||||||
}
|
|
||||||
SlashActionKind::Heading3 => {
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("level".to_string(), json!(3));
|
|
||||||
attrs.insert("collapsed".to_string(), Value::Null);
|
|
||||||
node_with_attrs("heading", Some(attrs), inline)
|
|
||||||
}
|
|
||||||
SlashActionKind::Heading4 => {
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("level".to_string(), json!(4));
|
|
||||||
attrs.insert("collapsed".to_string(), Value::Null);
|
|
||||||
node_with_attrs("heading", Some(attrs), inline)
|
|
||||||
}
|
|
||||||
SlashActionKind::BulletList => {
|
|
||||||
node_with_attrs("bulletList", None, vec![list_item_node(inline)])
|
|
||||||
}
|
|
||||||
SlashActionKind::OrderedList => {
|
|
||||||
node_with_attrs("orderedList", None, vec![list_item_node(inline)])
|
|
||||||
}
|
|
||||||
SlashActionKind::Todo | SlashActionKind::AdvancedTodo => {
|
|
||||||
node_with_attrs("taskList", None, vec![task_item_node(inline)])
|
|
||||||
}
|
|
||||||
SlashActionKind::Quote => node_with_attrs("blockquote", None, vec![paragraph_node(inline)]),
|
|
||||||
SlashActionKind::CodeBlock => {
|
|
||||||
let text = collect_plain_text(node);
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("language".to_string(), Value::String("rust".to_string()));
|
|
||||||
let content = if text.is_empty() {
|
|
||||||
Vec::new()
|
|
||||||
} else {
|
|
||||||
vec![json!({"type": "text", "text": text})]
|
|
||||||
};
|
|
||||||
node_with_attrs("codeBlock", Some(attrs), content)
|
|
||||||
}
|
|
||||||
SlashActionKind::Divider => node_with_attrs("horizontalRule", None, Vec::new()),
|
|
||||||
SlashActionKind::SimpleTable | SlashActionKind::UploadAttachment | SlashActionKind::Toc => {
|
|
||||||
paragraph_node(inline)
|
|
||||||
}
|
|
||||||
SlashActionKind::Mindmap => mindmap_paragraph_node(),
|
|
||||||
SlashActionKind::Image => {
|
|
||||||
let mut attrs = Map::new();
|
|
||||||
attrs.insert("src".to_string(), json!(E24_IMAGE_PLACEHOLDER_SRC));
|
|
||||||
attrs.insert("alt".to_string(), json!(E24_IMAGE_PLACEHOLDER_ALT));
|
|
||||||
attrs.insert("title".to_string(), json!(E24_IMAGE_PLACEHOLDER_TITLE));
|
|
||||||
node_with_attrs("image", Some(attrs), Vec::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_top_level_block_kind(
|
|
||||||
document: &mut Value,
|
|
||||||
index: usize,
|
|
||||||
action: SlashActionKind,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let content = document_content_mut(document)?;
|
|
||||||
let next = turn_into_block(
|
|
||||||
content
|
|
||||||
.get(index)
|
|
||||||
.ok_or_else(|| format!("找不到第 {index} 个块"))?,
|
|
||||||
action,
|
|
||||||
);
|
|
||||||
content[index] = next;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_html_update(
|
fn apply_html_update(
|
||||||
editor: TiptapEditorHandle,
|
editor: TiptapEditorHandle,
|
||||||
persisted_identity: &PersistedDocumentIdentity,
|
persisted_identity: &PersistedDocumentIdentity,
|
||||||
@@ -6185,7 +5989,10 @@ fn run_slash_action(
|
|||||||
editor.insert_table(4, 3, false)
|
editor.insert_table(4, 3, false)
|
||||||
}
|
}
|
||||||
SlashActionKind::Mindmap => {
|
SlashActionKind::Mindmap => {
|
||||||
editor.insert_content(TiptapContent::json(mindmap_paragraph_node()), None)
|
editor.insert_content(
|
||||||
|
TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
SlashActionKind::Image => {
|
SlashActionKind::Image => {
|
||||||
dispatch_editor_upload_request("image", "image/*")?;
|
dispatch_editor_upload_request("image", "image/*")?;
|
||||||
@@ -6272,7 +6079,11 @@ fn run_block_turn_into_action(
|
|||||||
.map(|_| "已把当前块转成分割线")
|
.map(|_| "已把当前块转成分割线")
|
||||||
.map_err(|err| format!("命令执行失败:{err}")),
|
.map_err(|err| format!("命令执行失败:{err}")),
|
||||||
SlashActionKind::Mindmap => editor
|
SlashActionKind::Mindmap => editor
|
||||||
.insert_content_at(range, TiptapContent::json(mindmap_paragraph_node()), None)
|
.insert_content_at(
|
||||||
|
range,
|
||||||
|
TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
.map(|_| "已把当前块转成思维导图")
|
.map(|_| "已把当前块转成思维导图")
|
||||||
.map_err(|err| format!("命令执行失败:{err}")),
|
.map_err(|err| format!("命令执行失败:{err}")),
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
Reference in New Issue
Block a user