Fix tiptap selection sync and toolbar event loop

This commit is contained in:
lix-2026
2026-04-19 21:03:25 +08:00
parent 111a87d4fd
commit 394e2a155c
87 changed files with 17415 additions and 527 deletions
@@ -0,0 +1,143 @@
use crate::editor::model::{BlockProps, ContentNode, EditorBlock, EditorBlockType, ReferenceToken};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorCommandKind {
ReplaceBlock,
InsertBlockAfter,
DeleteBlock,
SplitBlock,
MergeWithPrevious,
MoveBlock,
IndentBlock,
OutdentBlock,
ToggleHeadingCollapse,
AttachReferenceToken,
DetachReferenceToken,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorReplaceBlock {
pub block_id: String,
pub block_type: Option<EditorBlockType>,
pub props: Option<BlockProps>,
pub content_nodes: Option<Vec<ContentNode>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorInsertBlockAfter {
pub after_block_id: String,
pub block: EditorBlock,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorDeleteBlock {
pub block_id: String,
pub preserve_children: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorSplitBlock {
pub block_id: String,
pub content_node_index: u32,
pub text_offset: Option<u32>,
pub trailing_block_type: Option<EditorBlockType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorMergeWithPrevious {
pub block_id: String,
pub previous_block_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorMoveBlock {
pub block_id: String,
pub parent_block_id: Option<String>,
pub after_block_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorIndentBlock {
pub block_id: String,
pub max_depth: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorOutdentBlock {
pub block_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorToggleHeadingCollapse {
pub block_id: String,
pub collapsed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAttachReferenceToken {
pub block_id: String,
pub content_node_index: u32,
pub token: ReferenceToken,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorDetachReferenceToken {
pub block_id: String,
pub content_node_index: u32,
pub target_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EditorCommand {
ReplaceBlock(EditorReplaceBlock),
InsertBlockAfter(EditorInsertBlockAfter),
DeleteBlock(EditorDeleteBlock),
SplitBlock(EditorSplitBlock),
MergeWithPrevious(EditorMergeWithPrevious),
MoveBlock(EditorMoveBlock),
IndentBlock(EditorIndentBlock),
OutdentBlock(EditorOutdentBlock),
ToggleHeadingCollapse(EditorToggleHeadingCollapse),
AttachReferenceToken(EditorAttachReferenceToken),
DetachReferenceToken(EditorDetachReferenceToken),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorKernelCommandMapping {
pub editor_command: EditorCommandKind,
pub kernel_command_name: String,
pub lossy: bool,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorCommandDescriptor {
pub kind: EditorCommandKind,
pub label: String,
pub write_command: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct EditorCommandCatalog {
#[serde(default)]
pub commands: Vec<EditorCommandDescriptor>,
#[serde(default)]
pub kernel_command_mappings: Vec<EditorKernelCommandMapping>,
}
@@ -0,0 +1,91 @@
use crate::editor::model::{EditorBlockDocument, ReferenceTokenStrategy};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MarkdownFlavor {
Commonmark,
Gfm,
Mnote,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MarkdownImportMode {
ReplaceDocument,
ReplaceSelection,
MergeIntoDocument,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MarkdownExportMode {
FullDocument,
Selection,
SingleBlock,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownImportBoundary {
pub allow_front_matter: bool,
pub allow_html_blocks: bool,
pub allow_reference_tokens: bool,
pub unsupported_blocks_as_paragraph: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownExportBoundary {
pub preserve_heading_collapse: bool,
pub preserve_reference_tokens: bool,
pub allow_lossy_block_fallback: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownImportOptions {
pub mode: MarkdownImportMode,
pub flavor: MarkdownFlavor,
pub reference_token_strategy: ReferenceTokenStrategy,
pub boundary: MarkdownImportBoundary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownExportOptions {
pub mode: MarkdownExportMode,
pub flavor: MarkdownFlavor,
pub reference_token_strategy: ReferenceTokenStrategy,
pub boundary: MarkdownExportBoundary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownImportRequest {
pub markdown: String,
pub options: MarkdownImportOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownImportResult {
pub document: EditorBlockDocument,
#[serde(default)]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownExportRequest {
pub document: EditorBlockDocument,
pub options: MarkdownExportOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MarkdownExportResult {
pub markdown: String,
#[serde(default)]
pub warnings: Vec<String>,
}
@@ -0,0 +1,56 @@
pub mod command;
pub mod markdown;
pub mod model;
pub use command::{
EditorAttachReferenceToken, EditorCommand, EditorCommandCatalog, EditorCommandDescriptor,
EditorCommandKind, EditorDeleteBlock, EditorDetachReferenceToken, EditorIndentBlock,
EditorInsertBlockAfter, EditorKernelCommandMapping, EditorMergeWithPrevious, EditorMoveBlock,
EditorOutdentBlock, EditorReplaceBlock, EditorSplitBlock, EditorToggleHeadingCollapse,
};
pub use markdown::{
MarkdownExportBoundary, MarkdownExportMode, MarkdownExportOptions, MarkdownExportRequest,
MarkdownExportResult, MarkdownFlavor, MarkdownImportBoundary, MarkdownImportMode,
MarkdownImportOptions, MarkdownImportRequest, MarkdownImportResult,
};
pub use model::{
BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,
ReferenceToken, ReferenceTokenKind, ReferenceTokenStrategy, TextMark,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn editor_command_kind_uses_phase_one_names() {
assert_eq!(
serde_json::to_string(&EditorCommandKind::InsertBlockAfter).unwrap(),
"\"insert_block_after\""
);
assert_eq!(
serde_json::to_string(&EditorCommandKind::MergeWithPrevious).unwrap(),
"\"merge_with_previous\""
);
assert_eq!(
serde_json::to_string(&EditorCommandKind::ToggleHeadingCollapse).unwrap(),
"\"toggle_heading_collapse\""
);
}
#[test]
fn editor_block_type_uses_expected_snake_case_names() {
assert_eq!(
serde_json::to_string(&EditorBlockType::BulletListItem).unwrap(),
"\"bullet_list_item\""
);
assert_eq!(
serde_json::to_string(&EditorBlockType::PageReference).unwrap(),
"\"page_reference\""
);
assert_eq!(
serde_json::to_string(&EditorBlockType::BlockReference).unwrap(),
"\"block_reference\""
);
}
}
@@ -0,0 +1,112 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorBlockType {
Paragraph,
Heading,
BulletListItem,
NumberedListItem,
Quote,
Todo,
CodeBlock,
PageReference,
BlockReference,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ReferenceTokenKind {
PageReference,
BlockReference,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ReferenceTokenStrategy {
DoubleBracket,
DoubleParen,
InlineChip,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TextMark {
Bold,
Italic,
Underline,
Strike,
Code,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ReferenceToken {
pub kind: ReferenceTokenKind,
pub strategy: ReferenceTokenStrategy,
pub target_id: String,
pub label: Option<String>,
pub raw_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentNodePayload {
Text {
text: String,
#[serde(default)]
marks: Vec<TextMark>,
},
HardBreak,
ReferenceToken {
token: ReferenceToken,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ContentNode {
pub payload: ContentNodePayload,
#[serde(default)]
pub attrs: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct BlockProps {
pub indent: Option<u16>,
pub heading_level: Option<u8>,
pub checked: Option<bool>,
pub collapsed: Option<bool>,
pub language: Option<String>,
pub reference_target_id: Option<String>,
pub reference_label: Option<String>,
pub reference_token_strategy: Option<ReferenceTokenStrategy>,
#[serde(default)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorBlock {
pub block_id: String,
pub block_type: EditorBlockType,
#[serde(default)]
pub props: BlockProps,
#[serde(default)]
pub content_nodes: Vec<ContentNode>,
#[serde(default)]
pub child_block_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct EditorBlockDocument {
pub document_id: String,
#[serde(default)]
pub root_block_ids: Vec<String>,
#[serde(default)]
pub blocks: Vec<EditorBlock>,
}