主编辑区改造准备

This commit is contained in:
lix-2026
2026-04-21 06:26:35 +08:00
parent 1e686bfa3c
commit 5d1c94eb9e
49 changed files with 6730 additions and 2565 deletions
@@ -0,0 +1,216 @@
use core_protocol::editor::{
BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument,
EditorBlockDocumentTiptapBridge, EditorBlockType, TextMark, TiptapCodeBlockAttrs,
TiptapHeadingAttrs, TiptapNode, TiptapParagraphAttrs, TiptapTaskItemAttrs,
};
use std::collections::BTreeMap;
#[test]
fn p05_blocks_round_trip_through_tiptap_doc() {
let document = EditorBlockDocument {
document_id: "doc_1".into(),
root_block_ids: vec![
"p1".into(),
"h1".into(),
"b1".into(),
"n1".into(),
"t1".into(),
"q1".into(),
"c1".into(),
],
blocks: vec![
EditorBlock {
block_id: "p1".into(),
block_type: EditorBlockType::Paragraph,
props: BlockProps::default(),
content_nodes: vec![ContentNode {
payload: ContentNodePayload::Text {
text: "段落".into(),
marks: vec![TextMark::Bold],
},
attrs: BTreeMap::new(),
}],
child_block_ids: vec![],
},
EditorBlock {
block_id: "h1".into(),
block_type: EditorBlockType::Heading,
props: BlockProps {
heading_level: Some(2),
..BlockProps::default()
},
content_nodes: vec![ContentNode {
payload: ContentNodePayload::Text {
text: "标题".into(),
marks: vec![],
},
attrs: BTreeMap::new(),
}],
child_block_ids: vec![],
},
EditorBlock {
block_id: "b1".into(),
block_type: EditorBlockType::BulletListItem,
props: BlockProps::default(),
content_nodes: vec![ContentNode {
payload: ContentNodePayload::Text {
text: "项目".into(),
marks: vec![],
},
attrs: BTreeMap::new(),
}],
child_block_ids: vec![],
},
EditorBlock {
block_id: "n1".into(),
block_type: EditorBlockType::NumberedListItem,
props: BlockProps::default(),
content_nodes: vec![ContentNode {
payload: ContentNodePayload::Text {
text: "序号".into(),
marks: vec![],
},
attrs: BTreeMap::new(),
}],
child_block_ids: vec![],
},
EditorBlock {
block_id: "t1".into(),
block_type: EditorBlockType::Todo,
props: BlockProps {
checked: Some(true),
..BlockProps::default()
},
content_nodes: vec![ContentNode {
payload: ContentNodePayload::Text {
text: "待办".into(),
marks: vec![],
},
attrs: BTreeMap::new(),
}],
child_block_ids: vec![],
},
EditorBlock {
block_id: "q1".into(),
block_type: EditorBlockType::Quote,
props: BlockProps::default(),
content_nodes: vec![ContentNode {
payload: ContentNodePayload::Text {
text: "引用".into(),
marks: vec![],
},
attrs: BTreeMap::new(),
}],
child_block_ids: vec![],
},
EditorBlock {
block_id: "c1".into(),
block_type: EditorBlockType::CodeBlock,
props: BlockProps {
language: Some("rust".into()),
..BlockProps::default()
},
content_nodes: vec![ContentNode {
payload: ContentNodePayload::Text {
text: "fn main() {}".into(),
marks: vec![TextMark::Code],
},
attrs: BTreeMap::new(),
}],
child_block_ids: vec![],
},
],
};
let doc =
EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document).expect("export should work");
let round_trip = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_2", &doc)
.expect("import should work");
assert_eq!(
round_trip.root_block_ids,
vec!["p1", "h1", "b1", "n1", "t1", "q1", "c1"]
);
assert_eq!(round_trip.blocks[0].block_id, "p1");
assert_eq!(round_trip.blocks[0].content_nodes.len(), 1);
match &round_trip.blocks[0].content_nodes[0].payload {
ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Bold]),
other => panic!("unexpected payload: {other:?}"),
}
assert_eq!(round_trip.blocks[1].props.heading_level, Some(2));
assert_eq!(round_trip.blocks[4].props.checked, Some(true));
assert_eq!(round_trip.blocks[6].props.language.as_deref(), Some("rust"));
}
#[test]
fn tiptap_import_preserves_explicit_block_ids_and_marks() {
let doc = TiptapNode::Doc {
content: vec![
TiptapNode::Paragraph {
attrs: TiptapParagraphAttrs {
block_id: Some("p-1".into()),
},
content: vec![TiptapNode::Text {
text: "Hello".into(),
marks: vec![core_protocol::editor::TiptapMark {
r#type: "italic".into(),
}],
}],
},
TiptapNode::Heading {
attrs: TiptapHeadingAttrs {
level: 3,
block_id: Some("h-1".into()),
},
content: vec![TiptapNode::Text {
text: "H".into(),
marks: vec![],
}],
},
TiptapNode::TaskList {
attrs: core_protocol::editor::TiptapListAttrs {
block_id: Some("t-1".into()),
},
content: vec![TiptapNode::TaskItem {
attrs: TiptapTaskItemAttrs {
checked: true,
block_id: Some("t-1".into()),
},
content: vec![TiptapNode::Paragraph {
attrs: TiptapParagraphAttrs {
block_id: Some("t-1".into()),
},
content: vec![TiptapNode::Text {
text: "todo".into(),
marks: vec![],
}],
}],
}],
},
TiptapNode::CodeBlock {
attrs: TiptapCodeBlockAttrs {
language: Some("ts".into()),
block_id: Some("c-1".into()),
},
content: vec![TiptapNode::Text {
text: "let x = 1;".into(),
marks: vec![core_protocol::editor::TiptapMark {
r#type: "code".into(),
}],
}],
},
],
};
let imported = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_3", &doc).unwrap();
assert_eq!(imported.root_block_ids, vec!["p-1", "h-1", "t-1", "c-1"]);
assert_eq!(imported.blocks[0].block_id, "p-1");
assert_eq!(imported.blocks[1].props.heading_level, Some(3));
assert_eq!(imported.blocks[2].props.checked, Some(true));
assert_eq!(imported.blocks[3].props.language.as_deref(), Some("ts"));
match &imported.blocks[0].content_nodes[0].payload {
ContentNodePayload::Text { marks, .. } => assert_eq!(marks, &vec![TextMark::Italic]),
other => panic!("unexpected payload: {other:?}"),
}
}