refactor: prune block transform helpers
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! block menu 使用的顶层块 JSON helper。
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::editor_runtime::block_hover_state::DropPlacement;
|
||||
|
||||
@@ -65,6 +65,57 @@ pub(crate) fn reorder_top_level_block(
|
||||
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)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -11,7 +11,7 @@ use leptos_tiptap::{
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
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 wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
@@ -27,6 +27,9 @@ use editor_runtime::block_dnd::{
|
||||
HandleCorridorGeometry,
|
||||
};
|
||||
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::{
|
||||
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_TOOLBAR_OVERFLOW_EVENT: &str = "mnote:mindmap-shell:toolbar-overflow";
|
||||
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;
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
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 {
|
||||
let now = js_sys::Date::new_0();
|
||||
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(
|
||||
editor: TiptapEditorHandle,
|
||||
persisted_identity: &PersistedDocumentIdentity,
|
||||
@@ -6185,7 +5989,10 @@ fn run_slash_action(
|
||||
editor.insert_table(4, 3, false)
|
||||
}
|
||||
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 => {
|
||||
dispatch_editor_upload_request("image", "image/*")?;
|
||||
@@ -6272,7 +6079,11 @@ fn run_block_turn_into_action(
|
||||
.map(|_| "已把当前块转成分割线")
|
||||
.map_err(|err| format!("命令执行失败:{err}")),
|
||||
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_err(|err| format!("命令执行失败:{err}")),
|
||||
_ => {
|
||||
|
||||
Reference in New Issue
Block a user