mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support converting documents to JSON, HTML, or TEXT. (#3811)
* feat: support converting documents to JSON, HTML, or TEXT * fix: modify the comment * fix: modify the comment
This commit is contained in:
parent
e08a1a6974
commit
dd9b1fb78f
@ -36,7 +36,7 @@ export const turnToBlockThunk = createAsyncThunk(
|
||||
let caretId,
|
||||
caretIndex = caret?.index || 0;
|
||||
const deltaOperator = new BlockDeltaOperator(documentState, controller);
|
||||
let delta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||
let delta = deltaOperator.getDeltaWithBlockId(node.id) || new Delta([{ insert: '' }]);
|
||||
// insert new block after current block
|
||||
const insertActions = [];
|
||||
|
||||
@ -44,14 +44,14 @@ export const turnToBlockThunk = createAsyncThunk(
|
||||
delta = new Delta([{ insert: node.data.formula }]);
|
||||
}
|
||||
|
||||
if (delta && type === BlockType.EquationBlock) {
|
||||
if (type === BlockType.EquationBlock) {
|
||||
data.formula = deltaOperator.getDeltaText(delta);
|
||||
const block = newBlock<any>(type, parent.id, data);
|
||||
|
||||
insertActions.push(controller.getInsertAction(block, node.id));
|
||||
caretId = block.id;
|
||||
caretIndex = 0;
|
||||
} else if (delta && type === BlockType.DividerBlock) {
|
||||
} else if (type === BlockType.DividerBlock) {
|
||||
const block = newBlock<any>(type, parent.id, data);
|
||||
|
||||
insertActions.push(controller.getInsertAction(block, node.id));
|
||||
@ -68,7 +68,7 @@ export const turnToBlockThunk = createAsyncThunk(
|
||||
caretId = nodeId;
|
||||
caretIndex = 0;
|
||||
insertActions.push(...actions);
|
||||
} else if (delta) {
|
||||
} else {
|
||||
caretId = generateId();
|
||||
|
||||
const actions = deltaOperator.getNewTextLineActions({
|
||||
|
@ -4,6 +4,9 @@ use serde_json::Value;
|
||||
|
||||
use flowy_document2::entities::*;
|
||||
use flowy_document2::event_map::DocumentEvent;
|
||||
use flowy_document2::parser::parser_entities::{
|
||||
ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
|
||||
};
|
||||
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
||||
use flowy_folder2::event_map::FolderEvent;
|
||||
|
||||
@ -108,6 +111,19 @@ impl DocumentEventTest {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn convert_document(
|
||||
&self,
|
||||
payload: ConvertDocumentPayloadPB,
|
||||
) -> ConvertDocumentResponsePB {
|
||||
let core = &self.inner;
|
||||
EventBuilder::new(core.clone())
|
||||
.event(DocumentEvent::ConvertDocument)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<ConvertDocumentResponsePB>()
|
||||
}
|
||||
|
||||
pub async fn create_text(&self, payload: TextDeltaPayloadPB) {
|
||||
let core = &self.inner;
|
||||
EventBuilder::new(core.clone())
|
||||
|
@ -2,6 +2,7 @@ use collab_document::blocks::json_str_to_hashmap;
|
||||
use event_integration::document::document_event::DocumentEventTest;
|
||||
use event_integration::document::utils::*;
|
||||
use flowy_document2::entities::*;
|
||||
use flowy_document2::parser::parser_entities::{ConvertDocumentPayloadPB, ExportTypePB};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@ -120,3 +121,35 @@ async fn apply_text_delta_test() {
|
||||
json!([{ "insert": "Hello! World" }]).to_string()
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! generate_convert_document_test_cases {
|
||||
($($json:ident, $text:ident, $html:ident),*) => {
|
||||
[
|
||||
$((ExportTypePB { json: $json, text: $text, html: $html }, ($json, $text, $html))),*
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn convert_document_test() {
|
||||
let test = DocumentEventTest::new().await;
|
||||
let view = test.create_document().await;
|
||||
|
||||
let test_cases = generate_convert_document_test_cases! {
|
||||
true, true, true,
|
||||
false, true, true,
|
||||
false, false, false
|
||||
};
|
||||
|
||||
for (export_types, (json_assert, text_assert, html_assert)) in test_cases.iter() {
|
||||
let copy_payload = ConvertDocumentPayloadPB {
|
||||
document_id: view.id.to_string(),
|
||||
range: None,
|
||||
export_types: export_types.clone(),
|
||||
};
|
||||
let result = test.convert_document(copy_payload).await;
|
||||
assert_eq!(result.json.is_some(), *json_assert);
|
||||
assert_eq!(result.text.is_some(), *text_assert);
|
||||
assert_eq!(result.html.is_some(), *html_assert);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
# Check out the FlowyConfig (located in flowy_toml.rs) for more details.
|
||||
proto_input = ["src/event_map.rs", "src/entities.rs", "src/notification.rs"]
|
||||
proto_input = ["src/event_map.rs", "src/entities.rs", "src/notification.rs", "src/parser/parser_entities.rs"]
|
||||
event_files = ["src/event_map.rs"]
|
@ -15,6 +15,11 @@ use flowy_error::{FlowyError, FlowyResult};
|
||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||
|
||||
use crate::entities::*;
|
||||
use crate::parser::document_data_parser::DocumentDataParser;
|
||||
use crate::parser::parser_entities::{
|
||||
ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
|
||||
};
|
||||
|
||||
use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser};
|
||||
|
||||
fn upgrade_document(
|
||||
@ -303,3 +308,45 @@ impl From<(&Vec<BlockEvent>, bool)> for DocEventPB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for converting a document to a JSON string, HTML string, or plain text string.
|
||||
|
||||
* @param data: AFPluginData<[ConvertDocumentPayloadPB]>
|
||||
|
||||
* @param manager: AFPluginState<Weak<DocumentManager>>
|
||||
|
||||
* @return DataResult<[ConvertDocumentResponsePB], FlowyError>
|
||||
*/
|
||||
pub async fn convert_document(
|
||||
data: AFPluginData<ConvertDocumentPayloadPB>,
|
||||
manager: AFPluginState<Weak<DocumentManager>>,
|
||||
) -> DataResult<ConvertDocumentResponsePB, FlowyError> {
|
||||
let manager = upgrade_document(manager)?;
|
||||
let params: ConvertDocumentParams = data.into_inner().try_into()?;
|
||||
|
||||
let document = manager.get_document(¶ms.document_id).await?;
|
||||
let document_data = document.lock().get_document_data()?;
|
||||
let parser = DocumentDataParser::new(Arc::new(document_data), params.range);
|
||||
|
||||
if !params.export_types.any_enabled() {
|
||||
return data_result_ok(ConvertDocumentResponsePB::default());
|
||||
}
|
||||
|
||||
let root = &parser.to_json();
|
||||
|
||||
data_result_ok(ConvertDocumentResponsePB {
|
||||
json: params
|
||||
.export_types
|
||||
.json
|
||||
.then(|| serde_json::to_string(root).unwrap_or_default()),
|
||||
html: params
|
||||
.export_types
|
||||
.html
|
||||
.then(|| parser.to_html_with_json(root)),
|
||||
text: params
|
||||
.export_types
|
||||
.text
|
||||
.then(|| parser.to_text_with_json(root)),
|
||||
})
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use strum_macros::Display;
|
||||
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
||||
use lib_dispatch::prelude::AFPlugin;
|
||||
|
||||
use crate::event_handler::convert_document;
|
||||
use crate::event_handler::get_snapshot_handler;
|
||||
use crate::{event_handler::*, manager::DocumentManager};
|
||||
|
||||
@ -27,6 +28,7 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin {
|
||||
.event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler)
|
||||
.event(DocumentEvent::CreateText, create_text_handler)
|
||||
.event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler)
|
||||
.event(DocumentEvent::ConvertDocument, convert_document)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
||||
@ -76,4 +78,49 @@ pub enum DocumentEvent {
|
||||
|
||||
#[event(input = "TextDeltaPayloadPB")]
|
||||
ApplyTextDeltaEvent = 11,
|
||||
|
||||
/// Handler for converting a document to a JSON string, HTML string, or plain text string.
|
||||
///
|
||||
/// ConvertDocumentPayloadPB is the input of this event.
|
||||
/// ConvertDocumentResponsePB is the output of this event.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
///
|
||||
/// ```txt
|
||||
/// // document: [{ "block_id": "1", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } }, { "block_id": "2", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] }
|
||||
/// let test = DocumentEventTest::new().await;
|
||||
/// let view = test.create_document().await;
|
||||
/// let payload = ConvertDocumentPayloadPB {
|
||||
/// document_id: view.id,
|
||||
/// range: Some(RangePB {
|
||||
/// start: SelectionPB {
|
||||
/// block_id: "1".to_string(),
|
||||
/// index: 0,
|
||||
/// length: 5,
|
||||
/// },
|
||||
/// end: SelectionPB {
|
||||
/// block_id: "2".to_string(),
|
||||
/// index: 5,
|
||||
/// length: 7,
|
||||
/// }
|
||||
/// }),
|
||||
/// export_types: ConvertTypePB {
|
||||
/// json: true,
|
||||
/// text: true,
|
||||
/// html: true,
|
||||
/// },
|
||||
/// };
|
||||
/// let result = test.convert_document(payload).await;
|
||||
/// assert_eq!(result.json, Some("[{ \"block_id\": \"1\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \"Hello\" }] } }, { \"block_id\": \"2\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \" World!\" }] } }".to_string()));
|
||||
/// assert_eq!(result.text, Some("Hello\n World!".to_string()));
|
||||
/// assert_eq!(result.html, Some("<p>Hello</p><p> World!</p>".to_string()));
|
||||
/// ```
|
||||
/// #
|
||||
#[event(
|
||||
input = "ConvertDocumentPayloadPB",
|
||||
output = "ConvertDocumentResponsePB"
|
||||
)]
|
||||
ConvertDocument = 12,
|
||||
}
|
||||
|
37
frontend/rust-lib/flowy-document2/src/parser/constant.rs
Normal file
37
frontend/rust-lib/flowy-document2/src/parser/constant.rs
Normal file
@ -0,0 +1,37 @@
|
||||
pub const DELTA: &str = "delta";
|
||||
pub const LEVEL: &str = "level";
|
||||
pub const NUMBER: &str = "number";
|
||||
pub const CHECKED: &str = "checked";
|
||||
|
||||
pub const COLLAPSED: &str = "collapsed";
|
||||
pub const LANGUAGE: &str = "language";
|
||||
|
||||
pub const ICON: &str = "icon";
|
||||
pub const WIDTH: &str = "width";
|
||||
pub const HEIGHT: &str = "height";
|
||||
pub const URL: &str = "url";
|
||||
pub const CAPTION: &str = "caption";
|
||||
pub const ALIGN: &str = "align";
|
||||
|
||||
pub const PAGE: &str = "page";
|
||||
pub const HEADING: &str = "heading";
|
||||
pub const PARAGRAPH: &str = "paragraph";
|
||||
pub const NUMBERED_LIST: &str = "numbered_list";
|
||||
pub const BULLETED_LIST: &str = "bulleted_list";
|
||||
pub const TODO_LIST: &str = "todo_list";
|
||||
pub const TOGGLE_LIST: &str = "toggle_list";
|
||||
pub const QUOTE: &str = "quote";
|
||||
pub const CALLOUT: &str = "callout";
|
||||
pub const IMAGE: &str = "image";
|
||||
pub const DIVIDER: &str = "divider";
|
||||
pub const MATH_EQUATION: &str = "math_equation";
|
||||
pub const BOLD: &str = "bold";
|
||||
pub const ITALIC: &str = "italic";
|
||||
pub const STRIKETHROUGH: &str = "strikethrough";
|
||||
pub const CODE: &str = "code";
|
||||
pub const UNDERLINE: &str = "underline";
|
||||
pub const FONT_COLOR: &str = "font_color";
|
||||
pub const BG_COLOR: &str = "bg_color";
|
||||
pub const HREF: &str = "href";
|
||||
pub const FORMULA: &str = "formula";
|
||||
pub const MENTION: &str = "mention";
|
@ -0,0 +1,180 @@
|
||||
use crate::parser::parser_entities::{ConvertBlockToHtmlParams, NestedBlock, Range};
|
||||
use crate::parser::utils::{
|
||||
block_to_nested_json, get_delta_for_block, get_delta_for_selection, get_flat_block_ids,
|
||||
ConvertBlockToJsonParams,
|
||||
};
|
||||
use collab_document::blocks::DocumentData;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// DocumentDataParser is a struct for parsing a document's data and converting it to JSON, HTML, or text.
|
||||
pub struct DocumentDataParser {
|
||||
/// The document data to parse.
|
||||
pub document_data: Arc<DocumentData>,
|
||||
/// The range of the document data to parse. If the range is None, the entire document data will be parsed.
|
||||
pub range: Option<Range>,
|
||||
}
|
||||
|
||||
impl DocumentDataParser {
|
||||
pub fn new(document_data: Arc<DocumentData>, range: Option<Range>) -> Self {
|
||||
Self {
|
||||
document_data,
|
||||
range,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the JSON to an HTML representation.
|
||||
pub fn to_html_with_json(&self, json: &Option<NestedBlock>) -> String {
|
||||
let mut html = String::new();
|
||||
html.push_str("<meta charset=\"UTF-8\">");
|
||||
if let Some(json) = json {
|
||||
let params = ConvertBlockToHtmlParams {
|
||||
prev_block_ty: None,
|
||||
next_block_ty: None,
|
||||
};
|
||||
html.push_str(json.convert_to_html(params).as_str());
|
||||
}
|
||||
html
|
||||
}
|
||||
|
||||
/// Converts the JSON to plain text.
|
||||
pub fn to_text_with_json(&self, json: &Option<NestedBlock>) -> String {
|
||||
if let Some(json) = json {
|
||||
json.convert_to_text()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the document data to HTML.
|
||||
pub fn to_html(&self) -> String {
|
||||
let json = self.to_json();
|
||||
self.to_html_with_json(&json)
|
||||
}
|
||||
|
||||
/// Converts the document data to plain text.
|
||||
pub fn to_text(&self) -> String {
|
||||
let json = self.to_json();
|
||||
self.to_text_with_json(&json)
|
||||
}
|
||||
|
||||
/// Converts the document data to a nested JSON structure, considering the optional range.
|
||||
pub fn to_json(&self) -> Option<NestedBlock> {
|
||||
let root_id = &self.document_data.page_id;
|
||||
// flatten the block id list.
|
||||
let block_id_list = get_flat_block_ids(root_id, &self.document_data);
|
||||
|
||||
// collect the block ids in the range.
|
||||
let mut in_range_block_ids = self.collect_in_range_block_ids(&block_id_list);
|
||||
// insert the root block id if it is not in the in-range block ids.
|
||||
if !in_range_block_ids.contains(root_id) {
|
||||
in_range_block_ids.push(root_id.to_string());
|
||||
}
|
||||
|
||||
// build the parameters for converting the block to JSON with the in-range block ids.
|
||||
let convert_params = self.build_convert_json_params(&in_range_block_ids);
|
||||
// convert the root block to JSON.
|
||||
let mut root = block_to_nested_json(root_id, &convert_params)?;
|
||||
|
||||
// If the start block's parent is outside the in-range selection, we need to insert the start block.
|
||||
if self.should_insert_start_block() {
|
||||
self.insert_start_block_json(&mut root, &convert_params);
|
||||
}
|
||||
|
||||
Some(root)
|
||||
}
|
||||
|
||||
/// Collects the block ids in the range.
|
||||
fn collect_in_range_block_ids(&self, block_id_list: &Vec<String>) -> Vec<String> {
|
||||
if let Some(range) = &self.range {
|
||||
// Find the positions of start and end block IDs in the list
|
||||
let mut start_index = block_id_list
|
||||
.iter()
|
||||
.position(|id| id == &range.start.block_id)
|
||||
.unwrap_or(0);
|
||||
let mut end_index = block_id_list
|
||||
.iter()
|
||||
.position(|id| id == &range.end.block_id)
|
||||
.unwrap_or(0);
|
||||
|
||||
if start_index > end_index {
|
||||
// Swap start and end if they are in reverse order
|
||||
std::mem::swap(&mut start_index, &mut end_index);
|
||||
}
|
||||
|
||||
// Slice the block IDs based on the positions of start and end
|
||||
block_id_list[start_index..=end_index].to_vec()
|
||||
} else {
|
||||
// If no range is specified, return the entire list
|
||||
block_id_list.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the parameters for converting the block to JSON.
|
||||
/// ConvertBlockToJsonParams format:
|
||||
/// {
|
||||
/// blocks: HashMap<String, Arc<Block>>, // in-range blocks
|
||||
/// relation_map: HashMap<String, Arc<Vec<String>>>, // in-range blocks' children
|
||||
/// delta_map: HashMap<String, String>, // in-range blocks' delta
|
||||
/// }
|
||||
fn build_convert_json_params(&self, block_id_list: &[String]) -> ConvertBlockToJsonParams {
|
||||
let mut delta_map = HashMap::new();
|
||||
let mut in_range_blocks = HashMap::new();
|
||||
let mut relation_map = HashMap::new();
|
||||
|
||||
for block_id in block_id_list {
|
||||
if let Some(block) = self.document_data.blocks.get(block_id) {
|
||||
// Insert the block into the in-range block map.
|
||||
in_range_blocks.insert(block_id.to_string(), Arc::new(block.to_owned()));
|
||||
|
||||
// If the block has children, insert the children into the relation map.
|
||||
if let Some(children) = self.document_data.meta.children_map.get(&block.children) {
|
||||
relation_map.insert(block_id.to_string(), Arc::new(children.to_owned()));
|
||||
}
|
||||
|
||||
let delta = match &self.range {
|
||||
Some(range) if block_id == &range.start.block_id => {
|
||||
get_delta_for_selection(&range.start, &self.document_data)
|
||||
},
|
||||
Some(range) if block_id == &range.end.block_id => {
|
||||
get_delta_for_selection(&range.end, &self.document_data)
|
||||
},
|
||||
_ => get_delta_for_block(block_id, &self.document_data),
|
||||
};
|
||||
|
||||
// If the delta exists, insert it into the delta map.
|
||||
if let Some(delta) = delta {
|
||||
delta_map.insert(block_id.to_string(), delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConvertBlockToJsonParams {
|
||||
blocks: in_range_blocks,
|
||||
relation_map,
|
||||
delta_map,
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the start block should be inserted whether the start block's parent is outside the in-range selection.
|
||||
fn should_insert_start_block(&self) -> bool {
|
||||
if let Some(range) = &self.range {
|
||||
if let Some(start_block) = self.document_data.blocks.get(&range.start.block_id) {
|
||||
return start_block.parent != self.document_data.page_id;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Inserts the start block JSON to the root JSON.
|
||||
fn insert_start_block_json(
|
||||
&self,
|
||||
root: &mut NestedBlock,
|
||||
convert_params: &ConvertBlockToJsonParams,
|
||||
) {
|
||||
let start = &self.range.as_ref().unwrap().start;
|
||||
if let Some(start_block_json) = block_to_nested_json(&start.block_id, convert_params) {
|
||||
root.children.insert(0, start_block_json);
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1,5 @@
|
||||
pub mod constant;
|
||||
pub mod document_data_parser;
|
||||
pub mod json;
|
||||
pub mod parser_entities;
|
||||
pub mod utils;
|
||||
|
481
frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs
Normal file
481
frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs
Normal file
@ -0,0 +1,481 @@
|
||||
use crate::parse::NotEmptyStr;
|
||||
use crate::parser::constant::{
|
||||
BG_COLOR, BOLD, BULLETED_LIST, CALLOUT, CHECKED, CODE, DELTA, DIVIDER, FONT_COLOR, FORMULA,
|
||||
HEADING, HREF, ICON, IMAGE, ITALIC, LANGUAGE, LEVEL, MATH_EQUATION, NUMBERED_LIST, PAGE,
|
||||
PARAGRAPH, QUOTE, STRIKETHROUGH, TODO_LIST, TOGGLE_LIST, UNDERLINE, URL,
|
||||
};
|
||||
use crate::parser::utils::{
|
||||
convert_insert_delta_from_json, convert_nested_block_children_to_html, delta_to_html,
|
||||
delta_to_text,
|
||||
};
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::ErrorCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct SelectionPB {
|
||||
#[pb(index = 1)]
|
||||
pub block_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub index: u32,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub length: u32,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct RangePB {
|
||||
#[pb(index = 1)]
|
||||
pub start: SelectionPB,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub end: SelectionPB,
|
||||
}
|
||||
|
||||
/**
|
||||
* ExportTypePB
|
||||
* @field json: bool // export json data
|
||||
* @field html: bool // export html data
|
||||
* @field text: bool // export text data
|
||||
*/
|
||||
#[derive(Default, ProtoBuf, Debug, Clone)]
|
||||
pub struct ExportTypePB {
|
||||
#[pb(index = 1)]
|
||||
pub json: bool,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub html: bool,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub text: bool,
|
||||
}
|
||||
/**
|
||||
* ConvertDocumentPayloadPB
|
||||
* @field document_id: String
|
||||
* @file range: Option<RangePB> - optional // if range is None, copy the whole document
|
||||
* @field export_types: [ExportTypePB]
|
||||
*/
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct ConvertDocumentPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub range: Option<RangePB>,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub export_types: ExportTypePB,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf, Debug)]
|
||||
pub struct ConvertDocumentResponsePB {
|
||||
#[pb(index = 1, one_of)]
|
||||
pub json: Option<String>,
|
||||
#[pb(index = 2, one_of)]
|
||||
pub html: Option<String>,
|
||||
#[pb(index = 3, one_of)]
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Selection {
|
||||
pub block_id: String,
|
||||
pub index: u32,
|
||||
pub length: u32,
|
||||
}
|
||||
|
||||
pub struct Range {
|
||||
pub start: Selection,
|
||||
pub end: Selection,
|
||||
}
|
||||
|
||||
pub struct ExportType {
|
||||
pub json: bool,
|
||||
pub html: bool,
|
||||
pub text: bool,
|
||||
}
|
||||
|
||||
pub struct ConvertDocumentParams {
|
||||
pub document_id: String,
|
||||
pub range: Option<Range>,
|
||||
pub export_types: ExportType,
|
||||
}
|
||||
|
||||
impl ExportType {
|
||||
pub fn any_enabled(&self) -> bool {
|
||||
self.json || self.html || self.text
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SelectionPB> for Selection {
|
||||
fn from(data: SelectionPB) -> Self {
|
||||
Selection {
|
||||
block_id: data.block_id,
|
||||
index: data.index,
|
||||
length: data.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangePB> for Range {
|
||||
fn from(data: RangePB) -> Self {
|
||||
Range {
|
||||
start: data.start.into(),
|
||||
end: data.end.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExportTypePB> for ExportType {
|
||||
fn from(data: ExportTypePB) -> Self {
|
||||
ExportType {
|
||||
json: data.json,
|
||||
html: data.html,
|
||||
text: data.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TryInto<ConvertDocumentParams> for ConvertDocumentPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<ConvertDocumentParams, Self::Error> {
|
||||
let document_id =
|
||||
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
||||
let range = self.range.map(|data| data.into());
|
||||
|
||||
Ok(ConvertDocumentParams {
|
||||
document_id: document_id.0,
|
||||
range,
|
||||
export_types: self.export_types.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct InsertDelta {
|
||||
#[serde(default)]
|
||||
pub insert: String,
|
||||
#[serde(default)]
|
||||
pub attributes: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
impl InsertDelta {
|
||||
pub fn to_text(&self) -> String {
|
||||
self.insert.clone()
|
||||
}
|
||||
|
||||
pub fn to_html(&self) -> String {
|
||||
let mut html = String::new();
|
||||
let mut style = String::new();
|
||||
// If there are attributes, serialize them as a HashMap.
|
||||
if let Some(attrs) = &self.attributes {
|
||||
// Serialize the font color attributes.
|
||||
if let Some(color) = attrs.get(FONT_COLOR) {
|
||||
style.push_str(&format!(
|
||||
"color: {};",
|
||||
color.to_string().replace("0x", "#").trim_matches('\"')
|
||||
));
|
||||
}
|
||||
// Serialize the background color attributes.
|
||||
if let Some(color) = attrs.get(BG_COLOR) {
|
||||
style.push_str(&format!(
|
||||
"background-color: {};",
|
||||
color.to_string().replace("0x", "#").trim_matches('\"')
|
||||
));
|
||||
}
|
||||
// Serialize the href attributes.
|
||||
if let Some(href) = attrs.get(HREF) {
|
||||
html.push_str(&format!("<a href={}>", href));
|
||||
}
|
||||
|
||||
// Serialize the code attributes.
|
||||
if let Some(code) = attrs.get(CODE) {
|
||||
if code.as_bool().unwrap_or(false) {
|
||||
html.push_str("<code>");
|
||||
}
|
||||
}
|
||||
// Serialize the italic, underline, strikethrough, bold, formula attributes.
|
||||
if let Some(italic) = attrs.get(ITALIC) {
|
||||
if italic.as_bool().unwrap_or(false) {
|
||||
style.push_str("font-style: italic;");
|
||||
}
|
||||
}
|
||||
if let Some(underline) = attrs.get(UNDERLINE) {
|
||||
if underline.as_bool().unwrap_or(false) {
|
||||
style.push_str("text-decoration: underline;");
|
||||
}
|
||||
}
|
||||
if let Some(strikethrough) = attrs.get(STRIKETHROUGH) {
|
||||
if strikethrough.as_bool().unwrap_or(false) {
|
||||
style.push_str("text-decoration: line-through;");
|
||||
}
|
||||
}
|
||||
if let Some(bold) = attrs.get(BOLD) {
|
||||
if bold.as_bool().unwrap_or(false) {
|
||||
style.push_str("font-weight: bold;");
|
||||
}
|
||||
}
|
||||
if let Some(formula) = attrs.get(FORMULA) {
|
||||
if formula.as_bool().unwrap_or(false) {
|
||||
style.push_str("font-family: fantasy;");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Serialize the attributes to style.
|
||||
if !style.is_empty() {
|
||||
html.push_str(&format!("<span style=\"{}\">", style));
|
||||
}
|
||||
// Serialize the insert field.
|
||||
html.push_str(&self.insert);
|
||||
|
||||
// Close the style tag.
|
||||
if !style.is_empty() {
|
||||
html.push_str("</span>");
|
||||
}
|
||||
// Close the tags: <a>, <code>.
|
||||
if let Some(attrs) = &self.attributes {
|
||||
if attrs.contains_key(HREF) {
|
||||
html.push_str("</a>");
|
||||
}
|
||||
if attrs.contains_key(CODE) {
|
||||
html.push_str("</code>");
|
||||
}
|
||||
}
|
||||
html
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NestedBlock {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: String,
|
||||
#[serde(default)]
|
||||
pub data: HashMap<String, Value>,
|
||||
#[serde(default)]
|
||||
pub children: Vec<NestedBlock>,
|
||||
}
|
||||
|
||||
impl Eq for NestedBlock {}
|
||||
|
||||
impl PartialEq for NestedBlock {
|
||||
// ignore the id field
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.ty == other.ty
|
||||
&& self.data.iter().all(|(k, v)| {
|
||||
let other_v = other.data.get(k).unwrap_or(&Value::Null);
|
||||
if k == DELTA {
|
||||
let v = convert_insert_delta_from_json(v);
|
||||
let other_v = convert_insert_delta_from_json(other_v);
|
||||
return v == other_v;
|
||||
}
|
||||
v == other_v
|
||||
})
|
||||
&& self.children == other.children
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConvertBlockToHtmlParams {
|
||||
pub prev_block_ty: Option<String>,
|
||||
pub next_block_ty: Option<String>,
|
||||
}
|
||||
|
||||
impl NestedBlock {
|
||||
pub fn new(
|
||||
id: String,
|
||||
ty: String,
|
||||
data: HashMap<String, Value>,
|
||||
children: Vec<NestedBlock>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
ty,
|
||||
data,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_child(&mut self, child: NestedBlock) {
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
pub fn convert_to_html(&self, params: ConvertBlockToHtmlParams) -> String {
|
||||
let mut html = String::new();
|
||||
|
||||
let text_html = self
|
||||
.data
|
||||
.get("delta")
|
||||
.and_then(convert_insert_delta_from_json)
|
||||
.map(|delta| delta_to_html(&delta))
|
||||
.unwrap_or_default();
|
||||
|
||||
let prev_block_ty = params.prev_block_ty.unwrap_or_default();
|
||||
let next_block_ty = params.next_block_ty.unwrap_or_default();
|
||||
|
||||
match self.ty.as_str() {
|
||||
HEADING => {
|
||||
let level = self.data.get(LEVEL).unwrap_or(&Value::Null);
|
||||
if level.as_u64().unwrap_or(0) > 6 {
|
||||
html.push_str(&format!("<h6>{}</h6>", text_html));
|
||||
} else {
|
||||
html.push_str(&format!("<h{}>{}</h{}>", level, text_html, level));
|
||||
}
|
||||
},
|
||||
PARAGRAPH => {
|
||||
html.push_str(&format!("<p>{}</p>", text_html));
|
||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||
self.to_owned(),
|
||||
)));
|
||||
},
|
||||
CALLOUT => {
|
||||
html.push_str(&format!(
|
||||
"<p>{}{}</p>",
|
||||
self
|
||||
.data
|
||||
.get(ICON)
|
||||
.unwrap_or(&Value::Null)
|
||||
.to_string()
|
||||
.trim_matches('\"'),
|
||||
text_html
|
||||
));
|
||||
},
|
||||
IMAGE => {
|
||||
html.push_str(&format!(
|
||||
"<img src={} alt={} />",
|
||||
self.data.get(URL).unwrap(),
|
||||
"AppFlowy-Image"
|
||||
));
|
||||
},
|
||||
DIVIDER => {
|
||||
html.push_str("<hr />");
|
||||
},
|
||||
MATH_EQUATION => {
|
||||
let formula = self.data.get(FORMULA).unwrap_or(&Value::Null);
|
||||
html.push_str(&format!(
|
||||
"<p>{}</p>",
|
||||
formula.to_string().trim_matches('\"')
|
||||
));
|
||||
},
|
||||
CODE => {
|
||||
let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null);
|
||||
html.push_str(&format!(
|
||||
"<pre><code class=\"language-{}\">{}</code></pre>",
|
||||
language.to_string().trim_matches('\"'),
|
||||
text_html
|
||||
));
|
||||
},
|
||||
BULLETED_LIST | NUMBERED_LIST | TODO_LIST | TOGGLE_LIST => {
|
||||
let list_type = match self.ty.as_str() {
|
||||
BULLETED_LIST => "ul",
|
||||
NUMBERED_LIST => "ol",
|
||||
TODO_LIST => "ul",
|
||||
TOGGLE_LIST => "ul",
|
||||
_ => "ul", // Default to "ul" for unknown types
|
||||
};
|
||||
if prev_block_ty != self.ty {
|
||||
html.push_str(&format!("<{}>", list_type));
|
||||
}
|
||||
if self.ty == TODO_LIST {
|
||||
let checked_str = if self
|
||||
.data
|
||||
.get(CHECKED)
|
||||
.and_then(|checked| checked.as_bool())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"x"
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
html.push_str(&format!("<li>[{}] {}</li>", checked_str, text_html));
|
||||
} else {
|
||||
html.push_str(&format!("<li>{}</li>", text_html));
|
||||
}
|
||||
|
||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||
self.to_owned(),
|
||||
)));
|
||||
|
||||
if next_block_ty != self.ty {
|
||||
html.push_str(&format!("</{}>", list_type));
|
||||
}
|
||||
},
|
||||
|
||||
QUOTE => {
|
||||
if prev_block_ty != self.ty {
|
||||
html.push_str("<blockquote>");
|
||||
}
|
||||
html.push_str(&format!("<p>{}</p>", text_html));
|
||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||
self.to_owned(),
|
||||
)));
|
||||
if next_block_ty != self.ty {
|
||||
html.push_str("</blockquote>");
|
||||
}
|
||||
},
|
||||
PAGE => {
|
||||
if !text_html.is_empty() {
|
||||
html.push_str(&format!("<p>{}</p>", text_html));
|
||||
}
|
||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||
self.to_owned(),
|
||||
)));
|
||||
},
|
||||
_ => {
|
||||
html.push_str(&format!("<p>{}</p>", text_html));
|
||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||
self.to_owned(),
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
pub fn convert_to_text(&self) -> String {
|
||||
let mut text = String::new();
|
||||
|
||||
let delta_text = self
|
||||
.data
|
||||
.get("delta")
|
||||
.and_then(convert_insert_delta_from_json)
|
||||
.map(|delta| delta_to_text(&delta))
|
||||
.unwrap_or_default();
|
||||
|
||||
match self.ty.as_str() {
|
||||
CALLOUT => {
|
||||
text.push_str(&format!(
|
||||
"{}{}\n",
|
||||
self
|
||||
.data
|
||||
.get(ICON)
|
||||
.unwrap_or(&Value::Null)
|
||||
.to_string()
|
||||
.trim_matches('\"'),
|
||||
delta_text
|
||||
));
|
||||
},
|
||||
MATH_EQUATION => {
|
||||
let formula = self.data.get(FORMULA).unwrap_or(&Value::Null);
|
||||
text.push_str(&format!("{}\n", formula.to_string().trim_matches('\"')));
|
||||
},
|
||||
PAGE => {
|
||||
if !delta_text.is_empty() {
|
||||
text.push_str(&format!("{}\n", delta_text));
|
||||
}
|
||||
for child in &self.children {
|
||||
text.push_str(&child.convert_to_text());
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
text.push_str(&format!("{}\n", delta_text));
|
||||
for child in &self.children {
|
||||
text.push_str(&child.convert_to_text());
|
||||
}
|
||||
},
|
||||
};
|
||||
text
|
||||
}
|
||||
}
|
167
frontend/rust-lib/flowy-document2/src/parser/utils.rs
Normal file
167
frontend/rust-lib/flowy-document2/src/parser/utils.rs
Normal file
@ -0,0 +1,167 @@
|
||||
use crate::parser::constant::DELTA;
|
||||
use crate::parser::parser_entities::{
|
||||
ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Selection,
|
||||
};
|
||||
use collab_document::blocks::{Block, DocumentData};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ConvertBlockToJsonParams {
|
||||
pub(crate) blocks: HashMap<String, Arc<Block>>,
|
||||
pub(crate) relation_map: HashMap<String, Arc<Vec<String>>>,
|
||||
pub(crate) delta_map: HashMap<String, Vec<InsertDelta>>,
|
||||
}
|
||||
pub fn block_to_nested_json(
|
||||
block_id: &str,
|
||||
convert_params: &ConvertBlockToJsonParams,
|
||||
) -> Option<NestedBlock> {
|
||||
let blocks = &convert_params.blocks;
|
||||
let relation_map = &convert_params.relation_map;
|
||||
let delta_map = &convert_params.delta_map;
|
||||
// Attempt to retrieve the block using the block_id
|
||||
let block = blocks.get(block_id)?;
|
||||
|
||||
// Retrieve the children for this block from the relation map
|
||||
let children = relation_map.get(&block.id)?;
|
||||
|
||||
// Recursively convert children blocks to JSON
|
||||
let children: Vec<_> = children
|
||||
.iter()
|
||||
.filter_map(|child_id| block_to_nested_json(child_id, convert_params))
|
||||
.collect();
|
||||
|
||||
// Clone block data
|
||||
let mut data = block.data.clone();
|
||||
|
||||
// Insert delta into data if available
|
||||
if let Some(delta) = delta_map.get(&block.id) {
|
||||
if let Ok(delta_value) = serde_json::to_value(delta) {
|
||||
data.insert(DELTA.to_string(), delta_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Create and return the NestedBlock
|
||||
Some(NestedBlock {
|
||||
id: block.id.to_string(),
|
||||
ty: block.ty.to_string(),
|
||||
children,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_flat_block_ids(block_id: &str, data: &DocumentData) -> Vec<String> {
|
||||
let blocks = &data.blocks;
|
||||
let children_map = &data.meta.children_map;
|
||||
|
||||
if let Some(block) = blocks.get(block_id) {
|
||||
let mut result = vec![block.id.clone()];
|
||||
|
||||
if let Some(child_ids) = children_map.get(&block.children) {
|
||||
for child_id in child_ids {
|
||||
let child_blocks = get_flat_block_ids(child_id, data);
|
||||
result.extend(child_blocks);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
vec![]
|
||||
}
|
||||
|
||||
pub fn get_delta_for_block(block_id: &str, data: &DocumentData) -> Option<Vec<InsertDelta>> {
|
||||
let text_map = data.meta.text_map.as_ref()?; // Retrieve the text_map reference
|
||||
|
||||
data.blocks.get(block_id).and_then(|block| {
|
||||
let text_id = block.external_id.as_ref()?;
|
||||
let delta_str = text_map.get(text_id)?;
|
||||
serde_json::from_str::<Vec<InsertDelta>>(delta_str).ok()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_delta_for_selection(
|
||||
selection: &Selection,
|
||||
data: &DocumentData,
|
||||
) -> Option<Vec<InsertDelta>> {
|
||||
let delta = get_delta_for_block(&selection.block_id, data)?;
|
||||
let start = selection.index as usize;
|
||||
let end = (selection.index + selection.length) as usize;
|
||||
Some(slice_delta(&delta, start, end))
|
||||
}
|
||||
|
||||
pub fn slice_delta(delta: &Vec<InsertDelta>, start: usize, end: usize) -> Vec<InsertDelta> {
|
||||
let mut result = vec![];
|
||||
let mut index = 0;
|
||||
for d in delta {
|
||||
let content = &d.insert;
|
||||
let text_len = content.len();
|
||||
// skip if index is not reached
|
||||
if index + text_len <= start {
|
||||
index += text_len;
|
||||
continue;
|
||||
}
|
||||
// break if index is over end
|
||||
if index >= end {
|
||||
break;
|
||||
}
|
||||
// slice content, and push to result
|
||||
let start_offset = std::cmp::max(0, start as isize - index as isize) as usize;
|
||||
let end_offset = std::cmp::min(end - index, text_len);
|
||||
let content = content[start_offset..end_offset].to_string();
|
||||
result.push(InsertDelta {
|
||||
insert: content,
|
||||
attributes: d.attributes.clone(),
|
||||
});
|
||||
|
||||
index += text_len;
|
||||
}
|
||||
result
|
||||
}
|
||||
pub fn delta_to_text(delta: &Vec<InsertDelta>) -> String {
|
||||
let mut result = String::new();
|
||||
for d in delta {
|
||||
result.push_str(d.to_text().as_str());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn delta_to_html(delta: &Vec<InsertDelta>) -> String {
|
||||
let mut result = String::new();
|
||||
for d in delta {
|
||||
result.push_str(d.to_html().as_str());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn convert_nested_block_children_to_html(block: Arc<NestedBlock>) -> String {
|
||||
let children = &block.children;
|
||||
let mut html = String::new();
|
||||
let num_children = children.len();
|
||||
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let prev_block_ty = if i > 0 {
|
||||
Some(children[i - 1].ty.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let next_block_ty = if i + 1 < num_children {
|
||||
Some(children[i + 1].ty.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let child_html = child.convert_to_html(ConvertBlockToHtmlParams {
|
||||
prev_block_ty,
|
||||
next_block_ty,
|
||||
});
|
||||
|
||||
html.push_str(&child_html);
|
||||
}
|
||||
html
|
||||
}
|
||||
|
||||
pub fn convert_insert_delta_from_json(delta_value: &Value) -> Option<Vec<InsertDelta>> {
|
||||
serde_json::from_value::<Vec<InsertDelta>>(delta_value.to_owned()).ok()
|
||||
}
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><ul><li>Highlight</li><p>You can also</p><ul><li>nest</li></ul></ul>
|
@ -0,0 +1,6 @@
|
||||
<meta charset="UTF-8"><p>🥰
|
||||
Like AppFlowy? Follow us:
|
||||
<a href="https://github.com/AppFlowy-IO/AppFlowy">GitHub</a>
|
||||
<a href="https://twitter.com/appflowy">Twitter</a>: @appflowy
|
||||
<a href="https://blog-appflowy.ghost.io/">Newsletter</a>
|
||||
</p>
|
@ -0,0 +1,5 @@
|
||||
<meta charset="UTF-8"><pre><code class="language-rust">// This is the main function.
|
||||
fn main() {
|
||||
// Print text to the console.
|
||||
println!("Hello World!");
|
||||
}</code></pre>
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><hr />
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><h1>Heading1</h1><h2>Heading2</h2><h3>Heading3</h3>
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt=AppFlowy-Image />
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><p>E = MC^2</p>
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><ol><li>Highlight</li><p>You can also</p><ol><li>nest</li></ol></ol>
|
@ -0,0 +1,6 @@
|
||||
<meta charset="UTF-8"><p>
|
||||
Like AppFlowy? Follow us:
|
||||
<a href="https://github.com/AppFlowy-IO/AppFlowy">GitHub</a>
|
||||
<a href="https://twitter.com/appflowy">Twitter</a>: @appflowy
|
||||
<a href="https://blog-appflowy.ghost.io/">Newsletter</a>
|
||||
</p><p>Click <code>?</code> at the bottom right for help and support.</p><p><span style="background-color: #4dffeb3b;">Highlight </span>any text, and use the editing menu to <span style="font-style: italic;">style</span> <span style="font-weight: bold;">your</span> <span style="text-decoration: underline;">writing</span> <code>however</code><span style="color: #4dffeb3b;"> you </span><span style="text-decoration: line-through;">like.</span><span style="font-family: fantasy;">1+1=2</span></p>
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><blockquote><p>This is a quote</p><p>This is a paragraph</p></blockquote>
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><ul><li>[x] Highlight</li><p>You can also</p><ul><li>[ ] nest</li></ul></ul>
|
@ -0,0 +1 @@
|
||||
<meta charset="UTF-8"><ul><li>Click <code>?</code> at the bottom right for help and support.</li><p>This is a paragraph</p><ul><li>This is a toggle list</li></ul></ul>
|
@ -0,0 +1,37 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "bulleted_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Highlight"
|
||||
}
|
||||
]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "You can also"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bulleted_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "nest"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "callout",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "\nLike AppFlowy? Follow us:\n" },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||
},
|
||||
"insert": "GitHub"
|
||||
},
|
||||
{ "insert": "\n" },
|
||||
{
|
||||
"attributes": { "href": "https://twitter.com/appflowy" },
|
||||
"insert": "Twitter"
|
||||
},
|
||||
{ "insert": ": @appflowy\n" },
|
||||
{
|
||||
"attributes": { "href": "https://blog-appflowy.ghost.io/" },
|
||||
"insert": "Newsletter"
|
||||
},
|
||||
{ "insert": "\n" }
|
||||
],
|
||||
"icon": "🥰"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [{
|
||||
"type": "code",
|
||||
"data": {
|
||||
"language": "rust",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "divider",
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "heading",
|
||||
"data": {
|
||||
"level": 1,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Heading1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"data": {
|
||||
"level": 2,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Heading2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"data": {
|
||||
"level": 3,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Heading3"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "image",
|
||||
"data": {
|
||||
"url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png",
|
||||
"width": 272,
|
||||
"height": 92,
|
||||
"align": "center"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,267 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "delta": [{ "insert": "Welcome to AppFlowy!" }], "level": 1 }
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "delta": [{ "insert": "Here are the basics" }], "level": 2 }
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "delta": [{ "insert": "Here is H3" }], "level": 3 }
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Click anywhere and just start typing." }],
|
||||
"checked": false
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "Enter" },
|
||||
{ "insert": " to create a new line." }
|
||||
],
|
||||
"checked": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{
|
||||
"attributes": { "bg_color": "0x4dffeb3b" },
|
||||
"insert": "Highlight "
|
||||
},
|
||||
{ "insert": "any text, and use the editing menu to " },
|
||||
{ "attributes": { "italic": true }, "insert": "style" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "bold": true }, "insert": "your" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "underline": true }, "insert": "writing" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "code": true }, "insert": "however" },
|
||||
{ "insert": " you " },
|
||||
{ "attributes": { "strikethrough": true }, "insert": "like." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{ "insert": "As soon as you type " },
|
||||
{
|
||||
"attributes": { "code": true, "font_color": "0xff00b5ff" },
|
||||
"insert": "/"
|
||||
},
|
||||
{ "insert": " a menu will pop up. Select " },
|
||||
{
|
||||
"attributes": { "bg_color": "0x4d9c27b0" },
|
||||
"insert": "different types"
|
||||
},
|
||||
{ "insert": " of content blocks you can add." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "attributes": { "code": true }, "insert": "/" },
|
||||
{ "insert": " followed by " },
|
||||
{ "attributes": { "code": true }, "insert": "/bullet" },
|
||||
{ "insert": " or " },
|
||||
{ "attributes": { "code": true }, "insert": "/num" },
|
||||
{ "attributes": { "code": false }, "insert": " to create a list." }
|
||||
],
|
||||
"checked": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "+ New Page " },
|
||||
{
|
||||
"insert": "button at the bottom of your sidebar to add a new page."
|
||||
}
|
||||
],
|
||||
"checked": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "+" },
|
||||
{ "insert": " next to any page title in the sidebar to " },
|
||||
{
|
||||
"attributes": { "font_color": "0xff8427e0" },
|
||||
"insert": "quickly"
|
||||
},
|
||||
{ "insert": " add a new subpage, " },
|
||||
{ "attributes": { "code": true }, "insert": "Document" },
|
||||
{ "attributes": { "code": false }, "insert": ", " },
|
||||
{ "attributes": { "code": true }, "insert": "Grid" },
|
||||
{ "attributes": { "code": false }, "insert": ", or " },
|
||||
{ "attributes": { "code": true }, "insert": "Kanban Board" },
|
||||
{ "attributes": { "code": false }, "insert": "." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{ "type": "divider" },
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{
|
||||
"type": "heading",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }],
|
||||
"level": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Keyboard shortcuts " },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"
|
||||
},
|
||||
"insert": "guide"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Markdown " },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown"
|
||||
},
|
||||
"insert": "reference"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "attributes": { "code": true }, "insert": "/code" },
|
||||
{
|
||||
"attributes": { "code": false },
|
||||
"insert": " to insert a code block"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "code",
|
||||
"data": {
|
||||
"language": "rust",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [] },
|
||||
"children": [{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [{ "insert": "This is a paragraph" }] },
|
||||
"children": [{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [{ "insert": "This is a paragraph" }] }
|
||||
}]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "level": 2, "delta": [{ "insert": "Have a question❓" }] }
|
||||
},
|
||||
{
|
||||
"type": "toggle_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "?" },
|
||||
{ "insert": " at the bottom right for help and support." }
|
||||
]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [{ "insert": "This is a paragraph" }] }
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [{ "insert": "This is a paragraph" }] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "quote",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "?" },
|
||||
{ "insert": " at the bottom right for help and support." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{
|
||||
"type": "callout",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "\nLike AppFlowy? Follow us:\n" },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||
},
|
||||
"insert": "GitHub"
|
||||
},
|
||||
{ "insert": "\n" },
|
||||
{
|
||||
"attributes": { "href": "https://twitter.com/appflowy" },
|
||||
"insert": "Twitter"
|
||||
},
|
||||
{ "insert": ": @appflowy\n" },
|
||||
{
|
||||
"attributes": { "href": "https://blog-appflowy.ghost.io/" },
|
||||
"insert": "Newsletter"
|
||||
},
|
||||
{ "insert": "\n" }
|
||||
],
|
||||
"icon": "🥰"
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{ "type": "paragraph", "data": { "delta": [] } }
|
||||
]
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [{
|
||||
"type": "math_equation",
|
||||
"data": {
|
||||
"formula": "E = MC^2"
|
||||
}
|
||||
}]
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Highlight"
|
||||
}
|
||||
]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "You can also"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "nest"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [
|
||||
{ "insert": "\nLike AppFlowy? Follow us:\n" },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||
},
|
||||
"insert": "GitHub"
|
||||
},
|
||||
{ "insert": "\n" },
|
||||
{
|
||||
"attributes": { "href": "https://twitter.com/appflowy" },
|
||||
"insert": "Twitter"
|
||||
},
|
||||
{ "insert": ": @appflowy\n" },
|
||||
{
|
||||
"attributes": { "href": "https://blog-appflowy.ghost.io/" },
|
||||
"insert": "Newsletter"
|
||||
},
|
||||
{ "insert": "\n" }
|
||||
]},
|
||||
"children": [{
|
||||
"type": "paragraph",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "?" },
|
||||
{ "insert": " at the bottom right for help and support." }
|
||||
]
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [
|
||||
{
|
||||
"attributes": { "bg_color": "0x4dffeb3b" },
|
||||
"insert": "Highlight "
|
||||
},
|
||||
{ "insert": "any text, and use the editing menu to " },
|
||||
{ "attributes": { "italic": true }, "insert": "style" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "bold": true }, "insert": "your" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "underline": true }, "insert": "writing" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "code": true }, "insert": "however" },
|
||||
{ "insert": " you ", "attributes": { "font_color": "0x4dffeb3b" } },
|
||||
{ "attributes": { "strikethrough": true }, "insert": "like." },
|
||||
{ "attributes": { "formula": true }, "insert": "1+1=2" }
|
||||
] }
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "quote",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "This is a quote"
|
||||
}
|
||||
]
|
||||
},
|
||||
"children": [{
|
||||
"type": "paragraph",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "This is a paragraph"
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
100
frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json
Normal file
100
frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json
Normal file
@ -0,0 +1,100 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "delta": [{ "insert": " are the basics" }], "level": 2 }
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"data": { "delta": [{ "insert": "Here is H3" }], "level": 3 }
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Click anywhere and just start typing." }],
|
||||
"checked": false
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "Enter" },
|
||||
{ "insert": " to create a new line." }
|
||||
],
|
||||
"checked": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{
|
||||
"attributes": { "bg_color": "0x4dffeb3b" },
|
||||
"insert": "Highlight "
|
||||
},
|
||||
{ "insert": "any text, and use the editing menu to " },
|
||||
{ "attributes": { "italic": true }, "insert": "style" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "bold": true }, "insert": "your" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "underline": true }, "insert": "writing" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "code": true }, "insert": "however" },
|
||||
{ "insert": " you " },
|
||||
{ "attributes": { "strikethrough": true }, "insert": "like." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{ "insert": "As soon as you type " },
|
||||
{
|
||||
"attributes": { "code": true, "font_color": "0xff00b5ff" },
|
||||
"insert": "/"
|
||||
},
|
||||
{ "insert": " a menu will pop up. Select " },
|
||||
{
|
||||
"attributes": { "bg_color": "0x4d9c27b0" },
|
||||
"insert": "different types"
|
||||
},
|
||||
{ "insert": " of content blocks you can add." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "attributes": { "code": true }, "insert": "/" },
|
||||
{ "insert": " followed by " },
|
||||
{ "attributes": { "code": true }, "insert": "/bullet" },
|
||||
{ "insert": " or " },
|
||||
{ "attributes": { "code": true }, "insert": "/num" },
|
||||
{ "attributes": { "code": false }, "insert": " to create a list." }
|
||||
],
|
||||
"checked": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "+ New" }
|
||||
],
|
||||
"checked": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
178
frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json
Normal file
178
frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json
Normal file
@ -0,0 +1,178 @@
|
||||
{
|
||||
"type": "page",
|
||||
"data": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "attributes": { "code": true }, "insert": "Enter" },
|
||||
{ "insert": " to create a new line." }
|
||||
],
|
||||
"checked": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{
|
||||
"attributes": { "bg_color": "0x4dffeb3b" },
|
||||
"insert": "Highlight "
|
||||
},
|
||||
{ "insert": "any text, and use the editing menu to " },
|
||||
{ "attributes": { "italic": true }, "insert": "style" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "bold": true }, "insert": "your" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "underline": true }, "insert": "writing" },
|
||||
{ "insert": " " },
|
||||
{ "attributes": { "code": true }, "insert": "however" },
|
||||
{ "insert": " you " },
|
||||
{ "attributes": { "strikethrough": true }, "insert": "like." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{ "insert": "As soon as you type " },
|
||||
{
|
||||
"attributes": { "code": true, "font_color": "0xff00b5ff" },
|
||||
"insert": "/"
|
||||
},
|
||||
{ "insert": " a menu will pop up. Select " },
|
||||
{
|
||||
"attributes": { "bg_color": "0x4d9c27b0" },
|
||||
"insert": "different types"
|
||||
},
|
||||
{ "insert": " of content blocks you can add." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "attributes": { "code": true }, "insert": "/" },
|
||||
{ "insert": " followed by " },
|
||||
{ "attributes": { "code": true }, "insert": "/bullet" },
|
||||
{ "insert": " or " },
|
||||
{ "attributes": { "code": true }, "insert": "/num" },
|
||||
{ "attributes": { "code": false }, "insert": " to create a list." }
|
||||
],
|
||||
"checked": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "+ New Page " },
|
||||
{
|
||||
"insert": "button at the bottom of your sidebar to add a new page."
|
||||
}
|
||||
],
|
||||
"checked": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": false,
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "+" },
|
||||
{ "insert": " next to any page title in the sidebar to " },
|
||||
{
|
||||
"attributes": { "font_color": "0xff8427e0" },
|
||||
"insert": "quickly"
|
||||
},
|
||||
{ "insert": " add a new subpage, " },
|
||||
{ "attributes": { "code": true }, "insert": "Document" },
|
||||
{ "attributes": { "code": false }, "insert": ", " },
|
||||
{ "attributes": { "code": true }, "insert": "Grid" },
|
||||
{ "attributes": { "code": false }, "insert": ", or " },
|
||||
{ "attributes": { "code": true }, "insert": "Kanban Board" },
|
||||
{ "attributes": { "code": false }, "insert": "." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{ "type": "divider" },
|
||||
{ "type": "paragraph", "data": { "delta": [] } },
|
||||
{
|
||||
"type": "heading",
|
||||
"data": {
|
||||
"delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }],
|
||||
"level": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Keyboard shortcuts " },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"
|
||||
},
|
||||
"insert": "guide"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Markdown " },
|
||||
{
|
||||
"attributes": {
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown"
|
||||
},
|
||||
"insert": "reference"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "attributes": { "code": true }, "insert": "/code" },
|
||||
{
|
||||
"attributes": { "code": false },
|
||||
"insert": " to insert a code block"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "code",
|
||||
"data": {
|
||||
"language": "rust",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [] },
|
||||
"children": [{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [{ "insert": "This is a p" }] },
|
||||
"children": []
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "todo_list",
|
||||
"data": {
|
||||
"checked": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Highlight"
|
||||
}
|
||||
]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "You can also"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "todo_list",
|
||||
"checked": false,
|
||||
"data": {
|
||||
"delta": [
|
||||
{
|
||||
"insert": "nest"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"type": "page",
|
||||
"children": [
|
||||
{
|
||||
"type": "toggle_list",
|
||||
"data": {
|
||||
"delta": [
|
||||
{ "insert": "Click " },
|
||||
{ "attributes": { "code": true }, "insert": "?" },
|
||||
{ "insert": " at the bottom right for help and support." }
|
||||
]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"data": { "delta": [{ "insert": "This is a paragraph" }] }
|
||||
},
|
||||
{
|
||||
"type": "toggle_list",
|
||||
"data": { "delta": [{ "insert": "This is a toggle list" }] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
Highlight
|
||||
You can also
|
||||
nest
|
@ -0,0 +1,6 @@
|
||||
🥰
|
||||
Like AppFlowy? Follow us:
|
||||
GitHub
|
||||
Twitter: @appflowy
|
||||
Newsletter
|
||||
|
@ -0,0 +1,5 @@
|
||||
// This is the main function.
|
||||
fn main() {
|
||||
// Print text to the console.
|
||||
println!("Hello World!");
|
||||
}
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,3 @@
|
||||
Heading1
|
||||
Heading2
|
||||
Heading3
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1 @@
|
||||
E = MC^2
|
@ -0,0 +1,3 @@
|
||||
Highlight
|
||||
You can also
|
||||
nest
|
@ -0,0 +1,8 @@
|
||||
|
||||
Like AppFlowy? Follow us:
|
||||
GitHub
|
||||
Twitter: @appflowy
|
||||
Newsletter
|
||||
|
||||
Click ? at the bottom right for help and support.
|
||||
Highlight any text, and use the editing menu to style your writing however you like.1+1=2
|
@ -0,0 +1,2 @@
|
||||
This is a quote
|
||||
This is a paragraph
|
@ -0,0 +1,3 @@
|
||||
Highlight
|
||||
You can also
|
||||
nest
|
@ -0,0 +1,3 @@
|
||||
Click ? at the bottom right for help and support.
|
||||
This is a paragraph
|
||||
This is a toggle list
|
@ -2,4 +2,4 @@ mod document_insert_test;
|
||||
mod document_redo_undo_test;
|
||||
mod document_test;
|
||||
mod event_handler_test;
|
||||
mod util;
|
||||
pub mod util;
|
||||
|
@ -0,0 +1,105 @@
|
||||
use collab_document::blocks::DocumentData;
|
||||
use flowy_document2::parser::document_data_parser::DocumentDataParser;
|
||||
use flowy_document2::parser::json::parser::JsonToDocumentParser;
|
||||
use flowy_document2::parser::parser_entities::{NestedBlock, Range, Selection};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_data_parse_json_test() {
|
||||
let initial_json_str = include_str!("../assets/json/initial_document.json");
|
||||
let document_data = JsonToDocumentParser::json_str_to_document(initial_json_str)
|
||||
.unwrap()
|
||||
.into();
|
||||
let parser = DocumentDataParser::new(Arc::new(document_data), None);
|
||||
let read_me_json = serde_json::from_str::<NestedBlock>(initial_json_str).unwrap();
|
||||
let json = parser.to_json().unwrap();
|
||||
assert_eq!(read_me_json, json);
|
||||
}
|
||||
|
||||
// range_1 is a range from the 2nd block to the 8th block
|
||||
#[tokio::test]
|
||||
async fn document_data_to_json_with_range_1_test() {
|
||||
let initial_json_str = include_str!("../assets/json/initial_document.json");
|
||||
let document_data: DocumentData = JsonToDocumentParser::json_str_to_document(initial_json_str)
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
let children_map = &document_data.meta.children_map;
|
||||
let page_block_id = &document_data.page_id;
|
||||
let blocks = &document_data.blocks;
|
||||
let page_block = blocks.get(page_block_id).unwrap();
|
||||
let children = children_map.get(page_block.children.as_str()).unwrap();
|
||||
|
||||
let range = Range {
|
||||
start: Selection {
|
||||
block_id: children.get(1).unwrap().to_string(),
|
||||
index: 4,
|
||||
length: 15,
|
||||
},
|
||||
end: Selection {
|
||||
block_id: children.get(7).unwrap().to_string(),
|
||||
index: 0,
|
||||
length: 11,
|
||||
},
|
||||
};
|
||||
let parser = DocumentDataParser::new(Arc::new(document_data), Some(range));
|
||||
let json = parser.to_json().unwrap();
|
||||
let part_1 = include_str!("../assets/json/range_1.json");
|
||||
let part_1_json = serde_json::from_str::<NestedBlock>(part_1).unwrap();
|
||||
assert_eq!(part_1_json, json);
|
||||
}
|
||||
|
||||
// range_2 is a range from the 4th block's first child to the 18th block's first child
|
||||
#[tokio::test]
|
||||
async fn document_data_to_json_with_range_2_test() {
|
||||
let initial_json_str = include_str!("../assets/json/initial_document.json");
|
||||
let document_data: DocumentData = JsonToDocumentParser::json_str_to_document(initial_json_str)
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
let children_map = &document_data.meta.children_map;
|
||||
let page_block_id = &document_data.page_id;
|
||||
let blocks = &document_data.blocks;
|
||||
let page_block = blocks.get(page_block_id).unwrap();
|
||||
|
||||
let start_block_parent_id = children_map
|
||||
.get(page_block.children.as_str())
|
||||
.unwrap()
|
||||
.get(3)
|
||||
.unwrap();
|
||||
let start_block_parent = blocks.get(start_block_parent_id).unwrap();
|
||||
let start_block_id = children_map
|
||||
.get(start_block_parent.children.as_str())
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.unwrap();
|
||||
|
||||
let start = Selection {
|
||||
block_id: start_block_id.to_string(),
|
||||
index: 6,
|
||||
length: 27,
|
||||
};
|
||||
|
||||
let end_block_parent_id = children_map
|
||||
.get(page_block.children.as_str())
|
||||
.unwrap()
|
||||
.get(17)
|
||||
.unwrap();
|
||||
let end_block_parent = blocks.get(end_block_parent_id).unwrap();
|
||||
let end_block_children = children_map
|
||||
.get(end_block_parent.children.as_str())
|
||||
.unwrap();
|
||||
let end_block_id = end_block_children.get(0).unwrap();
|
||||
let end = Selection {
|
||||
block_id: end_block_id.to_string(),
|
||||
index: 0,
|
||||
length: 11,
|
||||
};
|
||||
|
||||
let range = Range { start, end };
|
||||
let parser = DocumentDataParser::new(Arc::new(document_data), Some(range));
|
||||
let json = parser.to_json().unwrap();
|
||||
let part_2 = include_str!("../assets/json/range_2.json");
|
||||
let part_2_json = serde_json::from_str::<NestedBlock>(part_2).unwrap();
|
||||
assert_eq!(part_2_json, json);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
mod test;
|
||||
mod utils;
|
@ -0,0 +1,37 @@
|
||||
use crate::parser::html_text::utils::{assert_document_html_eq, assert_document_text_eq};
|
||||
|
||||
macro_rules! generate_test_cases {
|
||||
($($block_ty:ident),*) => {
|
||||
[
|
||||
$(
|
||||
(
|
||||
include_str!(concat!("../../assets/json/", stringify!($block_ty), ".json")),
|
||||
include_str!(concat!("../../assets/html/", stringify!($block_ty), ".html")),
|
||||
include_str!(concat!("../../assets/text/", stringify!($block_ty), ".txt")),
|
||||
)
|
||||
),*
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn block_tests() {
|
||||
let test_cases = generate_test_cases!(
|
||||
heading,
|
||||
callout,
|
||||
paragraph,
|
||||
divider,
|
||||
image,
|
||||
math_equation,
|
||||
code,
|
||||
bulleted_list,
|
||||
numbered_list,
|
||||
todo_list,
|
||||
toggle_list,
|
||||
quote
|
||||
);
|
||||
for (json_data, expect_html, expect_text) in test_cases.iter() {
|
||||
assert_document_html_eq(json_data, expect_html);
|
||||
assert_document_text_eq(json_data, expect_text);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
use flowy_document2::parser::document_data_parser::DocumentDataParser;
|
||||
use flowy_document2::parser::json::parser::JsonToDocumentParser;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn assert_document_html_eq(source: &str, expect: &str) {
|
||||
let document_data = JsonToDocumentParser::json_str_to_document(source)
|
||||
.unwrap()
|
||||
.into();
|
||||
let parser = DocumentDataParser::new(Arc::new(document_data), None);
|
||||
let html = parser.to_html();
|
||||
assert_eq!(expect, html);
|
||||
}
|
||||
|
||||
pub fn assert_document_text_eq(source: &str, expect: &str) {
|
||||
let document_data = JsonToDocumentParser::json_str_to_document(source)
|
||||
.unwrap()
|
||||
.into();
|
||||
let parser = DocumentDataParser::new(Arc::new(document_data), None);
|
||||
let text = parser.to_text();
|
||||
assert_eq!(expect, text);
|
||||
}
|
@ -1 +1,3 @@
|
||||
mod document_data_parser_test;
|
||||
mod html_text;
|
||||
mod json;
|
||||
|
Loading…
Reference in New Issue
Block a user