mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support convert external data to nested json (#3848)
* feat: support convert external data to nested json * fix: add some comment * fix: code review * fix: code review * fix: code view * fix: code view * fix: update tauri cargo lock file * fix: remove reduant function * fix: parse dir attribute in element * fix: add comment about parse dir * fix: code review * fix: code review * fix: code review * fix: code review
This commit is contained in:
parent
1ad85416d8
commit
dc0af0f4c1
20
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
20
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -770,7 +770,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"realtime-entity",
|
"realtime-entity",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"scraper",
|
"scraper 0.17.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -2064,6 +2064,7 @@ dependencies = [
|
|||||||
"nanoid",
|
"nanoid",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
|
"scraper 0.18.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum_macros 0.21.1",
|
"strum_macros 0.21.1",
|
||||||
@ -2071,6 +2072,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5354,6 +5356,22 @@ dependencies = [
|
|||||||
"tendril",
|
"tendril",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scraper"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3693f9a0203d49a7ba8f38aa915316b3d535c1862d03dae7009cb71a3408b36a"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.8.3",
|
||||||
|
"cssparser 0.31.2",
|
||||||
|
"ego-tree",
|
||||||
|
"getopts",
|
||||||
|
"html5ever 0.26.0",
|
||||||
|
"once_cell",
|
||||||
|
"selectors 0.25.0",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
20
frontend/rust-lib/Cargo.lock
generated
20
frontend/rust-lib/Cargo.lock
generated
@ -668,7 +668,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"realtime-entity",
|
"realtime-entity",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"scraper",
|
"scraper 0.17.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -1885,6 +1885,7 @@ dependencies = [
|
|||||||
"nanoid",
|
"nanoid",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
|
"scraper 0.18.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum_macros 0.21.1",
|
"strum_macros 0.21.1",
|
||||||
@ -1894,6 +1895,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4701,6 +4703,22 @@ dependencies = [
|
|||||||
"tendril",
|
"tendril",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scraper"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3693f9a0203d49a7ba8f38aa915316b3d535c1862d03dae7009cb71a3408b36a"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.8.3",
|
||||||
|
"cssparser",
|
||||||
|
"ego-tree",
|
||||||
|
"getopts",
|
||||||
|
"html5ever",
|
||||||
|
"once_cell",
|
||||||
|
"selectors",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -5,7 +5,8 @@ use serde_json::Value;
|
|||||||
use flowy_document2::entities::*;
|
use flowy_document2::entities::*;
|
||||||
use flowy_document2::event_map::DocumentEvent;
|
use flowy_document2::event_map::DocumentEvent;
|
||||||
use flowy_document2::parser::parser_entities::{
|
use flowy_document2::parser::parser_entities::{
|
||||||
ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
|
ConvertDataToJsonPayloadPB, ConvertDataToJsonResponsePB, ConvertDocumentPayloadPB,
|
||||||
|
ConvertDocumentResponsePB,
|
||||||
};
|
};
|
||||||
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
||||||
use flowy_folder2::event_map::FolderEvent;
|
use flowy_folder2::event_map::FolderEvent;
|
||||||
@ -124,6 +125,20 @@ impl DocumentEventTest {
|
|||||||
.parse::<ConvertDocumentResponsePB>()
|
.parse::<ConvertDocumentResponsePB>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert data to json for document event test
|
||||||
|
pub async fn convert_data_to_json(
|
||||||
|
&self,
|
||||||
|
payload: ConvertDataToJsonPayloadPB,
|
||||||
|
) -> ConvertDataToJsonResponsePB {
|
||||||
|
let core = &self.inner;
|
||||||
|
EventBuilder::new(core.clone())
|
||||||
|
.event(DocumentEvent::ConvertDataToJSON)
|
||||||
|
.payload(payload)
|
||||||
|
.async_send()
|
||||||
|
.await
|
||||||
|
.parse::<ConvertDataToJsonResponsePB>()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_text(&self, payload: TextDeltaPayloadPB) {
|
pub async fn create_text(&self, payload: TextDeltaPayloadPB) {
|
||||||
let core = &self.inner;
|
let core = &self.inner;
|
||||||
EventBuilder::new(core.clone())
|
EventBuilder::new(core.clone())
|
||||||
|
@ -2,7 +2,9 @@ use collab_document::blocks::json_str_to_hashmap;
|
|||||||
use event_integration::document::document_event::DocumentEventTest;
|
use event_integration::document::document_event::DocumentEventTest;
|
||||||
use event_integration::document::utils::*;
|
use event_integration::document::utils::*;
|
||||||
use flowy_document2::entities::*;
|
use flowy_document2::entities::*;
|
||||||
use flowy_document2::parser::parser_entities::{ConvertDocumentPayloadPB, ExportTypePB};
|
use flowy_document2::parser::parser_entities::{
|
||||||
|
ConvertDataToJsonPayloadPB, ConvertDocumentPayloadPB, InputType, NestedBlock, ParseTypePB,
|
||||||
|
};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -125,7 +127,7 @@ async fn apply_text_delta_test() {
|
|||||||
macro_rules! generate_convert_document_test_cases {
|
macro_rules! generate_convert_document_test_cases {
|
||||||
($($json:ident, $text:ident, $html:ident),*) => {
|
($($json:ident, $text:ident, $html:ident),*) => {
|
||||||
[
|
[
|
||||||
$((ExportTypePB { json: $json, text: $text, html: $html }, ($json, $text, $html))),*
|
$((ParseTypePB { json: $json, text: $text, html: $html }, ($json, $text, $html))),*
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -145,7 +147,7 @@ async fn convert_document_test() {
|
|||||||
let copy_payload = ConvertDocumentPayloadPB {
|
let copy_payload = ConvertDocumentPayloadPB {
|
||||||
document_id: view.id.to_string(),
|
document_id: view.id.to_string(),
|
||||||
range: None,
|
range: None,
|
||||||
export_types: export_types.clone(),
|
parse_types: export_types.clone(),
|
||||||
};
|
};
|
||||||
let result = test.convert_document(copy_payload).await;
|
let result = test.convert_document(copy_payload).await;
|
||||||
assert_eq!(result.json.is_some(), *json_assert);
|
assert_eq!(result.json.is_some(), *json_assert);
|
||||||
@ -153,3 +155,53 @@ async fn convert_document_test() {
|
|||||||
assert_eq!(result.html.is_some(), *html_assert);
|
assert_eq!(result.html.is_some(), *html_assert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// test convert data to json
|
||||||
|
/// - input html: <p>Hello</p><p> World!</p>
|
||||||
|
/// - input plain text: Hello World!
|
||||||
|
/// - output json: { "type": "page", "data": {}, "children": [{ "type": "paragraph", "children": [], "data": { "delta": [{ "insert": "Hello" }] } }, { "type": "paragraph", "children": [], "data": { "delta": [{ "insert": " World!" }] } }] }
|
||||||
|
#[tokio::test]
|
||||||
|
async fn convert_data_to_json_test() {
|
||||||
|
let test = DocumentEventTest::new().await;
|
||||||
|
let _ = test.create_document().await;
|
||||||
|
|
||||||
|
let html = r#"<p>Hello</p><p>World!</p>"#;
|
||||||
|
let payload = ConvertDataToJsonPayloadPB {
|
||||||
|
data: html.to_string(),
|
||||||
|
input_type: InputType::Html,
|
||||||
|
};
|
||||||
|
let result = test.convert_data_to_json(payload).await;
|
||||||
|
let expect_json = json!({
|
||||||
|
"type": "page",
|
||||||
|
"data": {},
|
||||||
|
"children": [{
|
||||||
|
"type": "paragraph",
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [{ "insert": "Hello" }]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"type": "paragraph",
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [{ "insert": "World!" }]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
let expect_json = serde_json::from_value::<NestedBlock>(expect_json).unwrap();
|
||||||
|
assert!(serde_json::from_str::<NestedBlock>(&result.json)
|
||||||
|
.unwrap()
|
||||||
|
.eq(&expect_json));
|
||||||
|
|
||||||
|
let plain_text = "Hello\nWorld!";
|
||||||
|
let payload = ConvertDataToJsonPayloadPB {
|
||||||
|
data: plain_text.to_string(),
|
||||||
|
input_type: InputType::PlainText,
|
||||||
|
};
|
||||||
|
let result = test.convert_data_to_json(payload).await;
|
||||||
|
|
||||||
|
assert!(serde_json::from_str::<NestedBlock>(&result.json)
|
||||||
|
.unwrap()
|
||||||
|
.eq(&expect_json));
|
||||||
|
}
|
||||||
|
@ -18,7 +18,7 @@ flowy-notification = { workspace = true }
|
|||||||
flowy-error = { path = "../flowy-error", features = ["impl_from_serde", "impl_from_sqlite", "impl_from_dispatch_error", "impl_from_collab"] }
|
flowy-error = { path = "../flowy-error", features = ["impl_from_serde", "impl_from_sqlite", "impl_from_dispatch_error", "impl_from_collab"] }
|
||||||
lib-dispatch = { workspace = true }
|
lib-dispatch = { workspace = true }
|
||||||
lib-infra = { path = "../../../shared-lib/lib-infra" }
|
lib-infra = { path = "../../../shared-lib/lib-infra" }
|
||||||
|
validator = "0.16.0"
|
||||||
protobuf = {version = "2.28.0"}
|
protobuf = {version = "2.28.0"}
|
||||||
bytes = { version = "1.5" }
|
bytes = { version = "1.5" }
|
||||||
nanoid = "0.4.0"
|
nanoid = "0.4.0"
|
||||||
@ -33,6 +33,7 @@ indexmap = {version = "1.9.2", features = ["serde"]}
|
|||||||
uuid = { version = "1.3.3", features = ["v4"] }
|
uuid = { version = "1.3.3", features = ["v4"] }
|
||||||
futures = "0.3.26"
|
futures = "0.3.26"
|
||||||
tokio-stream = { version = "0.1.14", features = ["sync"] }
|
tokio-stream = { version = "0.1.14", features = ["sync"] }
|
||||||
|
scraper = "0.18.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.4.0"
|
tempfile = "3.4.0"
|
||||||
|
@ -319,6 +319,7 @@ pub struct ExportDataPB {
|
|||||||
#[pb(index = 2)]
|
#[pb(index = 2)]
|
||||||
pub export_type: ExportType,
|
pub export_type: ExportType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, ProtoBuf_Enum, Clone, Default)]
|
#[derive(PartialEq, Eq, Debug, ProtoBuf_Enum, Clone, Default)]
|
||||||
pub enum ConvertType {
|
pub enum ConvertType {
|
||||||
#[default]
|
#[default]
|
||||||
@ -337,6 +338,7 @@ impl From<i32> for ConvertType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// for convert data to document
|
||||||
/// for the json type
|
/// for the json type
|
||||||
/// the data is the json string
|
/// the data is the json string
|
||||||
#[derive(Default, ProtoBuf, Debug)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
|
@ -12,14 +12,18 @@ use collab_document::blocks::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
use lib_dispatch::prelude::{
|
||||||
|
data_result_ok, AFPluginData, AFPluginDataValidator, AFPluginState, DataResult,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::entities::*;
|
use crate::entities::*;
|
||||||
use crate::parser::document_data_parser::DocumentDataParser;
|
use crate::parser::document_data_parser::DocumentDataParser;
|
||||||
use crate::parser::parser_entities::{
|
use crate::parser::parser_entities::{
|
||||||
|
ConvertDataToJsonParams, ConvertDataToJsonPayloadPB, ConvertDataToJsonResponsePB,
|
||||||
ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
|
ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::parser::external::parser::ExternalDataToNestedJSONParser;
|
||||||
use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser};
|
use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser};
|
||||||
|
|
||||||
fn upgrade_document(
|
fn upgrade_document(
|
||||||
@ -309,16 +313,46 @@ impl From<(&Vec<BlockEvent>, bool)> for DocEventPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Handler for converting a document to a JSON string, HTML string, or plain text string.
|
||||||
* Handler for converting a document to a JSON string, HTML string, or plain text string.
|
///
|
||||||
|
/// ConvertDocumentPayloadPB is the input of this event.
|
||||||
* @param data: AFPluginData<[ConvertDocumentPayloadPB]>
|
/// ConvertDocumentResponsePB is the output of this event.
|
||||||
|
///
|
||||||
* @param manager: AFPluginState<Weak<DocumentManager>>
|
/// # Examples
|
||||||
|
///
|
||||||
* @return DataResult<[ConvertDocumentResponsePB], FlowyError>
|
/// Basic usage:
|
||||||
*/
|
///
|
||||||
pub async fn convert_document(
|
/// ```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,
|
||||||
|
/// }
|
||||||
|
/// }),
|
||||||
|
/// parse_types: ParseTypePB {
|
||||||
|
/// 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()));
|
||||||
|
/// ```
|
||||||
|
/// #
|
||||||
|
pub async fn convert_document_handler(
|
||||||
data: AFPluginData<ConvertDocumentPayloadPB>,
|
data: AFPluginData<ConvertDocumentPayloadPB>,
|
||||||
manager: AFPluginState<Weak<DocumentManager>>,
|
manager: AFPluginState<Weak<DocumentManager>>,
|
||||||
) -> DataResult<ConvertDocumentResponsePB, FlowyError> {
|
) -> DataResult<ConvertDocumentResponsePB, FlowyError> {
|
||||||
@ -329,7 +363,7 @@ pub async fn convert_document(
|
|||||||
let document_data = document.lock().get_document_data()?;
|
let document_data = document.lock().get_document_data()?;
|
||||||
let parser = DocumentDataParser::new(Arc::new(document_data), params.range);
|
let parser = DocumentDataParser::new(Arc::new(document_data), params.range);
|
||||||
|
|
||||||
if !params.export_types.any_enabled() {
|
if !params.parse_types.any_enabled() {
|
||||||
return data_result_ok(ConvertDocumentResponsePB::default());
|
return data_result_ok(ConvertDocumentResponsePB::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,16 +371,43 @@ pub async fn convert_document(
|
|||||||
|
|
||||||
data_result_ok(ConvertDocumentResponsePB {
|
data_result_ok(ConvertDocumentResponsePB {
|
||||||
json: params
|
json: params
|
||||||
.export_types
|
.parse_types
|
||||||
.json
|
.json
|
||||||
.then(|| serde_json::to_string(root).unwrap_or_default()),
|
.then(|| serde_json::to_string(root).unwrap_or_default()),
|
||||||
html: params
|
html: params
|
||||||
.export_types
|
.parse_types
|
||||||
.html
|
.html
|
||||||
.then(|| parser.to_html_with_json(root)),
|
.then(|| parser.to_html_with_json(root)),
|
||||||
text: params
|
text: params
|
||||||
.export_types
|
.parse_types
|
||||||
.text
|
.text
|
||||||
.then(|| parser.to_text_with_json(root)),
|
.then(|| parser.to_text_with_json(root)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for converting a string to a JSON string.
|
||||||
|
/// # Examples
|
||||||
|
/// Basic usage:
|
||||||
|
/// ```txt
|
||||||
|
/// let test = DocumentEventTest::new().await;
|
||||||
|
/// let payload = ConvertDataToJsonPayloadPB {
|
||||||
|
/// data: "<p>Hello</p><p> World!</p>".to_string(),
|
||||||
|
/// input_type: InputTypePB::Html,
|
||||||
|
/// };
|
||||||
|
/// let result: ConvertDataToJsonResponsePB = test.convert_data_to_json(payload).await;
|
||||||
|
/// let expect_json = json!({ "type": "page", "data": {}, "children": [{ "type": "paragraph", "children": [], "data": { "delta": [{ "insert": "Hello" }] } }, { "type": "paragraph", "children": [], "data": { "delta": [{ "insert": " World!" }] } }] });
|
||||||
|
/// assert!(serde_json::from_str::<NestedBlock>(&result.json).unwrap().eq(&serde_json::from_value::<NestedBlock>(expect_json).unwrap()));
|
||||||
|
/// ```
|
||||||
|
pub(crate) async fn convert_data_to_json_handler(
|
||||||
|
data: AFPluginData<ConvertDataToJsonPayloadPB>,
|
||||||
|
) -> DataResult<ConvertDataToJsonResponsePB, FlowyError> {
|
||||||
|
let payload: ConvertDataToJsonParams = data.validate()?.into_inner().try_into()?;
|
||||||
|
let parser = ExternalDataToNestedJSONParser::new(payload.data, payload.input_type);
|
||||||
|
|
||||||
|
let result = match parser.to_nested_block() {
|
||||||
|
Some(result) => serde_json::to_string(&result)?,
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
data_result_ok(ConvertDataToJsonResponsePB { json: result })
|
||||||
|
}
|
||||||
|
@ -5,7 +5,6 @@ use strum_macros::Display;
|
|||||||
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
||||||
use lib_dispatch::prelude::AFPlugin;
|
use lib_dispatch::prelude::AFPlugin;
|
||||||
|
|
||||||
use crate::event_handler::convert_document;
|
|
||||||
use crate::event_handler::get_snapshot_handler;
|
use crate::event_handler::get_snapshot_handler;
|
||||||
use crate::{event_handler::*, manager::DocumentManager};
|
use crate::{event_handler::*, manager::DocumentManager};
|
||||||
|
|
||||||
@ -28,7 +27,11 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin {
|
|||||||
.event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler)
|
.event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler)
|
||||||
.event(DocumentEvent::CreateText, create_text_handler)
|
.event(DocumentEvent::CreateText, create_text_handler)
|
||||||
.event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler)
|
.event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler)
|
||||||
.event(DocumentEvent::ConvertDocument, convert_document)
|
.event(DocumentEvent::ConvertDocument, convert_document_handler)
|
||||||
|
.event(
|
||||||
|
DocumentEvent::ConvertDataToJSON,
|
||||||
|
convert_data_to_json_handler,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
||||||
@ -79,48 +82,17 @@ pub enum DocumentEvent {
|
|||||||
#[event(input = "TextDeltaPayloadPB")]
|
#[event(input = "TextDeltaPayloadPB")]
|
||||||
ApplyTextDeltaEvent = 11,
|
ApplyTextDeltaEvent = 11,
|
||||||
|
|
||||||
/// Handler for converting a document to a JSON string, HTML string, or plain text string.
|
// document in event_handler.rs -> convert_document
|
||||||
///
|
|
||||||
/// 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(
|
#[event(
|
||||||
input = "ConvertDocumentPayloadPB",
|
input = "ConvertDocumentPayloadPB",
|
||||||
output = "ConvertDocumentResponsePB"
|
output = "ConvertDocumentResponsePB"
|
||||||
)]
|
)]
|
||||||
ConvertDocument = 12,
|
ConvertDocument = 12,
|
||||||
|
|
||||||
|
// document in event_handler.rs -> convert_data_to_json
|
||||||
|
#[event(
|
||||||
|
input = "ConvertDataToJsonPayloadPB",
|
||||||
|
output = "ConvertDataToJsonResponsePB"
|
||||||
|
)]
|
||||||
|
ConvertDataToJSON = 13,
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,92 @@ pub const CODE: &str = "code";
|
|||||||
pub const UNDERLINE: &str = "underline";
|
pub const UNDERLINE: &str = "underline";
|
||||||
pub const FONT_COLOR: &str = "font_color";
|
pub const FONT_COLOR: &str = "font_color";
|
||||||
pub const BG_COLOR: &str = "bg_color";
|
pub const BG_COLOR: &str = "bg_color";
|
||||||
pub const HREF: &str = "href";
|
|
||||||
pub const FORMULA: &str = "formula";
|
pub const FORMULA: &str = "formula";
|
||||||
pub const MENTION: &str = "mention";
|
pub const MENTION: &str = "mention";
|
||||||
|
|
||||||
|
pub const TEXT_DIRECTION: &str = "text_direction";
|
||||||
|
|
||||||
|
pub const HTML_TAG_NAME: &str = "html";
|
||||||
|
pub const HR_TAG_NAME: &str = "hr";
|
||||||
|
pub const META_TAG_NAME: &str = "meta";
|
||||||
|
pub const LINK_TAG_NAME: &str = "link";
|
||||||
|
pub const SCRIPT_TAG_NAME: &str = "script";
|
||||||
|
pub const STYLE_TAG_NAME: &str = "style";
|
||||||
|
pub const IFRAME_TAG_NAME: &str = "iframe";
|
||||||
|
pub const NOSCRIPT_TAG_NAME: &str = "noscript";
|
||||||
|
pub const HEAD_TAG_NAME: &str = "head";
|
||||||
|
pub const H1_TAG_NAME: &str = "h1";
|
||||||
|
pub const H2_TAG_NAME: &str = "h2";
|
||||||
|
pub const H3_TAG_NAME: &str = "h3";
|
||||||
|
pub const H4_TAG_NAME: &str = "h4";
|
||||||
|
pub const H5_TAG_NAME: &str = "h5";
|
||||||
|
pub const H6_TAG_NAME: &str = "h6";
|
||||||
|
pub const P_TAG_NAME: &str = "p";
|
||||||
|
pub const ASIDE_TAG_NAME: &str = "aside";
|
||||||
|
pub const ARTICLE_TAG_NAME: &str = "article";
|
||||||
|
pub const UL_TAG_NAME: &str = "ul";
|
||||||
|
pub const OL_TAG_NAME: &str = "ol";
|
||||||
|
pub const LI_TAG_NAME: &str = "li";
|
||||||
|
pub const BLOCKQUOTE_TAG_NAME: &str = "blockquote";
|
||||||
|
pub const PRE_TAG_NAME: &str = "pre";
|
||||||
|
pub const IMG_TAG_NAME: &str = "img";
|
||||||
|
pub const B_TAG_NAME: &str = "b";
|
||||||
|
pub const CODE_TAG_NAME: &str = "code";
|
||||||
|
pub const STRONG_TAG_NAME: &str = "strong";
|
||||||
|
pub const EM_TAG_NAME: &str = "em";
|
||||||
|
pub const U_TAG_NAME: &str = "u";
|
||||||
|
pub const S_TAG_NAME: &str = "s";
|
||||||
|
pub const SPAN_TAG_NAME: &str = "span";
|
||||||
|
pub const BR_TAG_NAME: &str = "br";
|
||||||
|
|
||||||
|
pub const A_TAG_NAME: &str = "a";
|
||||||
|
pub const BASE_TAG_NAME: &str = "base";
|
||||||
|
pub const ABBR_TAG_NAME: &str = "abbr";
|
||||||
|
pub const ADDRESS_TAG_NAME: &str = "address";
|
||||||
|
pub const DBO_TAG_NAME: &str = "bdo";
|
||||||
|
pub const DIR_ATTR_NAME: &str = "dir";
|
||||||
|
|
||||||
|
pub const RTL_ATTR_VALUE: &str = "rtl";
|
||||||
|
|
||||||
|
pub const CITE_TAG_NAME: &str = "cite";
|
||||||
|
|
||||||
|
pub const DEL_TAG_NAME: &str = "del";
|
||||||
|
|
||||||
|
pub const DETAILS_TAG_NAME: &str = "details";
|
||||||
|
|
||||||
|
pub const SUMMARY_TAG_NAME: &str = "summary";
|
||||||
|
|
||||||
|
pub const DFN_TAG_NAME: &str = "dfn";
|
||||||
|
|
||||||
|
pub const DL_TAG_NAME: &str = "dl";
|
||||||
|
|
||||||
|
pub const I_TAG_NAME: &str = "i";
|
||||||
|
pub const VAR_TAG_NAME: &str = "var";
|
||||||
|
|
||||||
|
pub const INS_TAG_NAME: &str = "ins";
|
||||||
|
pub const MENU_TAG_NAME: &str = "menu";
|
||||||
|
|
||||||
|
pub const MARK_TAG_NAME: &str = "mark";
|
||||||
|
|
||||||
|
pub const FONT_WEIGHT: &str = "font-weight";
|
||||||
|
pub const FONT_STYLE: &str = "font-style";
|
||||||
|
pub const TEXT_DECORATION: &str = "text-decoration";
|
||||||
|
|
||||||
|
pub const BACKGROUND_COLOR: &str = "background-color";
|
||||||
|
pub const COLOR: &str = "color";
|
||||||
|
pub const LINE_THROUGH: &str = "line-through";
|
||||||
|
|
||||||
|
pub const FONT_STYLE_ITALIC: &str = "font-style: italic;";
|
||||||
|
pub const TEXT_DECORATION_UNDERLINE: &str = "text-decoration: underline;";
|
||||||
|
pub const TEXT_DECORATION_LINE_THROUGH: &str = "text-decoration: line-through;";
|
||||||
|
pub const FONT_WEIGHT_BOLD: &str = "font-weight: bold;";
|
||||||
|
pub const FONT_FAMILY_FANTASY: &str = "font-family: fantasy;";
|
||||||
|
|
||||||
|
pub const SRC: &str = "src";
|
||||||
|
pub const HREF: &str = "href";
|
||||||
|
pub const ROLE: &str = "role";
|
||||||
|
pub const CHECKBOX: &str = "checkbox";
|
||||||
|
pub const ARIA_CHECKED: &str = "aria-checked";
|
||||||
|
pub const CLASS: &str = "class";
|
||||||
|
pub const STYLE: &str = "style";
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
use crate::parser::parser_entities::{ConvertBlockToHtmlParams, NestedBlock, Range};
|
use crate::parser::constant::DELTA;
|
||||||
use crate::parser::utils::{
|
use crate::parser::parser_entities::{ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Range};
|
||||||
block_to_nested_json, get_delta_for_block, get_delta_for_selection, get_flat_block_ids,
|
use crate::parser::utils::{get_delta_for_block, get_delta_for_selection};
|
||||||
ConvertBlockToJsonParams,
|
|
||||||
};
|
|
||||||
use collab_document::blocks::DocumentData;
|
use collab_document::blocks::DocumentData;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// DocumentDataParser is a struct for parsing a document's data and converting it to JSON, HTML, or text.
|
/// DocumentDataParser is a struct for parsing a document's data and converting it to JSON, HTML, or text.
|
||||||
@ -61,120 +58,94 @@ impl DocumentDataParser {
|
|||||||
/// Converts the document data to a nested JSON structure, considering the optional range.
|
/// Converts the document data to a nested JSON structure, considering the optional range.
|
||||||
pub fn to_json(&self) -> Option<NestedBlock> {
|
pub fn to_json(&self) -> Option<NestedBlock> {
|
||||||
let root_id = &self.document_data.page_id;
|
let root_id = &self.document_data.page_id;
|
||||||
// flatten the block id list.
|
let mut children = vec![];
|
||||||
let block_id_list = get_flat_block_ids(root_id, &self.document_data);
|
let mut start_found = false;
|
||||||
|
let mut end_found = false;
|
||||||
// collect the block ids in the range.
|
self.block_to_nested_block(root_id, &mut children, &mut start_found, &mut end_found)
|
||||||
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 block_to_nested_block(
|
||||||
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,
|
&self,
|
||||||
root: &mut NestedBlock,
|
block_id: &str,
|
||||||
convert_params: &ConvertBlockToJsonParams,
|
children: &mut Vec<NestedBlock>,
|
||||||
|
start_found: &mut bool,
|
||||||
|
end_found: &mut bool,
|
||||||
|
) -> Option<NestedBlock> {
|
||||||
|
let block = self.document_data.blocks.get(block_id)?;
|
||||||
|
let delta = self.get_delta(block_id);
|
||||||
|
|
||||||
|
// Prepare the data, including delta if available
|
||||||
|
let mut data = block.data.clone();
|
||||||
|
if let Some(delta) = delta {
|
||||||
|
if let Ok(delta_value) = serde_json::to_value(delta) {
|
||||||
|
data.insert(DELTA.to_string(), delta_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the child IDs for the current block
|
||||||
|
if let Some(block_children_ids) = self.document_data.meta.children_map.get(&block.children) {
|
||||||
|
for child_id in block_children_ids {
|
||||||
|
if let Some(range) = &self.range {
|
||||||
|
if child_id == &range.start.block_id {
|
||||||
|
*start_found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if child_id == &range.end.block_id {
|
||||||
|
*end_found = true;
|
||||||
|
// Process the "end" block recursively
|
||||||
|
self.process_child_block(child_id, children, start_found, end_found);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.range.is_some() {
|
||||||
|
if !*start_found {
|
||||||
|
// Don't insert children before the "start" block is found
|
||||||
|
self.block_to_nested_block(child_id, children, start_found, end_found);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if *end_found {
|
||||||
|
// Stop inserting children after the "end" block is found
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process child blocks recursively
|
||||||
|
self.process_child_block(child_id, children, start_found, end_found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(NestedBlock {
|
||||||
|
ty: block.ty.clone(),
|
||||||
|
children: children.to_owned(),
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_delta(&self, block_id: &str) -> Option<Vec<InsertDelta>> {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_child_block(
|
||||||
|
&self,
|
||||||
|
child_id: &str,
|
||||||
|
children: &mut Vec<NestedBlock>,
|
||||||
|
start_found: &mut bool,
|
||||||
|
end_found: &mut bool,
|
||||||
) {
|
) {
|
||||||
let start = &self.range.as_ref().unwrap().start;
|
let mut child_children = vec![];
|
||||||
if let Some(start_block_json) = block_to_nested_json(&start.block_id, convert_params) {
|
if let Some(child) =
|
||||||
root.children.insert(0, start_block_json);
|
self.block_to_nested_block(child_id, &mut child_children, start_found, end_found)
|
||||||
|
{
|
||||||
|
children.push(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
frontend/rust-lib/flowy-document2/src/parser/external/mod.rs
vendored
Normal file
2
frontend/rust-lib/flowy-document2/src/parser/external/mod.rs
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod parser;
|
||||||
|
mod utils;
|
40
frontend/rust-lib/flowy-document2/src/parser/external/parser.rs
vendored
Normal file
40
frontend/rust-lib/flowy-document2/src/parser/external/parser.rs
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use crate::parser::external::utils::{flatten_element_to_block, parse_plaintext_to_nested_block};
|
||||||
|
use crate::parser::parser_entities::{InputType, NestedBlock};
|
||||||
|
use scraper::Html;
|
||||||
|
|
||||||
|
/// External data to nested json parser.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ExternalDataToNestedJSONParser {
|
||||||
|
/// External data. for example: html string, plain text string.
|
||||||
|
external_data: String,
|
||||||
|
/// External data type. for example: [InputType]::Html, [InputType]::PlainText.
|
||||||
|
input_type: InputType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExternalDataToNestedJSONParser {
|
||||||
|
pub fn new(data: String, input_type: InputType) -> Self {
|
||||||
|
Self {
|
||||||
|
external_data: data,
|
||||||
|
input_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format to nested block.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// - input html: <p><strong>Hello</strong></p><p> World!</p>
|
||||||
|
/// - output json:
|
||||||
|
/// ```json
|
||||||
|
/// { "type": "page", "data": {}, "children": [{ "type": "paragraph", "children": [], "data": { "delta": [{ "insert": "Hello", attributes: { "bold": true } }] } }, { "type": "paragraph", "children": [], "data": { "delta": [{ "insert": " World!", attributes: null }] } }] }
|
||||||
|
/// ```
|
||||||
|
pub fn to_nested_block(&self) -> Option<NestedBlock> {
|
||||||
|
match self.input_type {
|
||||||
|
InputType::Html => {
|
||||||
|
let fragment = Html::parse_fragment(&self.external_data);
|
||||||
|
let root_element = fragment.root_element();
|
||||||
|
flatten_element_to_block(root_element)
|
||||||
|
},
|
||||||
|
InputType::PlainText => parse_plaintext_to_nested_block(&self.external_data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
559
frontend/rust-lib/flowy-document2/src/parser/external/utils.rs
vendored
Normal file
559
frontend/rust-lib/flowy-document2/src/parser/external/utils.rs
vendored
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
use crate::parser::constant::*;
|
||||||
|
use crate::parser::parser_entities::{InsertDelta, NestedBlock};
|
||||||
|
use scraper::node::Attrs;
|
||||||
|
use scraper::ElementRef;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
const INLINE_TAGS: [&str; 18] = [
|
||||||
|
A_TAG_NAME,
|
||||||
|
EM_TAG_NAME,
|
||||||
|
STRONG_TAG_NAME,
|
||||||
|
U_TAG_NAME,
|
||||||
|
S_TAG_NAME,
|
||||||
|
CODE_TAG_NAME,
|
||||||
|
SPAN_TAG_NAME,
|
||||||
|
ADDRESS_TAG_NAME,
|
||||||
|
BASE_TAG_NAME,
|
||||||
|
CITE_TAG_NAME,
|
||||||
|
DFN_TAG_NAME,
|
||||||
|
I_TAG_NAME,
|
||||||
|
VAR_TAG_NAME,
|
||||||
|
ABBR_TAG_NAME,
|
||||||
|
INS_TAG_NAME,
|
||||||
|
DEL_TAG_NAME,
|
||||||
|
MARK_TAG_NAME,
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
const LINK_TAGS: [&str; 2] = [A_TAG_NAME, BASE_TAG_NAME];
|
||||||
|
const ITALIC_TAGS: [&str; 6] = [
|
||||||
|
EM_TAG_NAME,
|
||||||
|
I_TAG_NAME,
|
||||||
|
VAR_TAG_NAME,
|
||||||
|
CITE_TAG_NAME,
|
||||||
|
DFN_TAG_NAME,
|
||||||
|
ADDRESS_TAG_NAME,
|
||||||
|
];
|
||||||
|
|
||||||
|
const BOLD_TAGS: [&str; 2] = [STRONG_TAG_NAME, B_TAG_NAME];
|
||||||
|
|
||||||
|
const UNDERLINE_TAGS: [&str; 3] = [U_TAG_NAME, ABBR_TAG_NAME, INS_TAG_NAME];
|
||||||
|
const STRIKETHROUGH_TAGS: [&str; 2] = [S_TAG_NAME, DEL_TAG_NAME];
|
||||||
|
const IGNORE_TAGS: [&str; 7] = [
|
||||||
|
META_TAG_NAME,
|
||||||
|
HEAD_TAG_NAME,
|
||||||
|
LINK_TAG_NAME,
|
||||||
|
SCRIPT_TAG_NAME,
|
||||||
|
STYLE_TAG_NAME,
|
||||||
|
NOSCRIPT_TAG_NAME,
|
||||||
|
IFRAME_TAG_NAME,
|
||||||
|
];
|
||||||
|
|
||||||
|
const HEADING_TAGS: [&str; 6] = [
|
||||||
|
H1_TAG_NAME,
|
||||||
|
H2_TAG_NAME,
|
||||||
|
H3_TAG_NAME,
|
||||||
|
H4_TAG_NAME,
|
||||||
|
H5_TAG_NAME,
|
||||||
|
H6_TAG_NAME,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SHOULD_EXPAND_TAGS: [&str; 4] = [UL_TAG_NAME, OL_TAG_NAME, DL_TAG_NAME, MENU_TAG_NAME];
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum JSONResult {
|
||||||
|
Block(NestedBlock),
|
||||||
|
Delta(InsertDelta),
|
||||||
|
BlockArray(Vec<NestedBlock>),
|
||||||
|
DeltaArray(Vec<InsertDelta>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flatten element to block
|
||||||
|
pub fn flatten_element_to_block(node: ElementRef) -> Option<NestedBlock> {
|
||||||
|
if let Some(JSONResult::Block(block)) = flatten_element_to_json(node, &None, &None) {
|
||||||
|
return Some(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse plaintext to nested block
|
||||||
|
pub fn parse_plaintext_to_nested_block(plaintext: &str) -> Option<NestedBlock> {
|
||||||
|
let lines: Vec<&str> = plaintext
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.collect();
|
||||||
|
let mut current_block = NestedBlock {
|
||||||
|
ty: PAGE.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
|
||||||
|
// Insert plaintext into delta
|
||||||
|
if let Ok(delta) = serde_json::to_value(vec![InsertDelta {
|
||||||
|
insert: line.to_string(),
|
||||||
|
attributes: None,
|
||||||
|
}]) {
|
||||||
|
data.insert(DELTA.to_string(), delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new block for each non-empty line
|
||||||
|
current_block.children.push(NestedBlock {
|
||||||
|
ty: PARAGRAPH.to_string(),
|
||||||
|
data,
|
||||||
|
children: Default::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_block.children.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(current_block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_element_to_json(
|
||||||
|
node: ElementRef,
|
||||||
|
list_type: &Option<String>,
|
||||||
|
attributes: &Option<HashMap<String, Value>>,
|
||||||
|
) -> Option<JSONResult> {
|
||||||
|
let tag_name = get_tag_name(node.to_owned());
|
||||||
|
|
||||||
|
if IGNORE_TAGS.contains(&tag_name.as_str()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if INLINE_TAGS.contains(&tag_name.as_str()) {
|
||||||
|
return process_inline_element(node, attributes.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
// insert dir into attrs when dir is rtl
|
||||||
|
// for example: <bdo dir="rtl">Right to left</bdo> -> { "attributes": { "text_direction": "rtl" }, "insert": "Right to left" }
|
||||||
|
if let Some(dir) = find_attribute_value(node.to_owned(), DIR_ATTR_NAME) {
|
||||||
|
data.insert(TEXT_DIRECTION.to_string(), Value::String(dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
if HEADING_TAGS.contains(&tag_name.as_str()) {
|
||||||
|
return process_heading_element(node, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if SHOULD_EXPAND_TAGS.contains(&tag_name.as_str()) {
|
||||||
|
return process_nested_element(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
match tag_name.as_str() {
|
||||||
|
LI_TAG_NAME => process_li_element(node, list_type.to_owned(), data),
|
||||||
|
BLOCKQUOTE_TAG_NAME | DETAILS_TAG_NAME => {
|
||||||
|
process_node_summary_and_details(QUOTE.to_string(), node, data)
|
||||||
|
},
|
||||||
|
PRE_TAG_NAME => process_code_element(node),
|
||||||
|
IMG_TAG_NAME => process_image_element(node),
|
||||||
|
B_TAG_NAME => {
|
||||||
|
// Compatible with Google Docs, <b id=xxx> is the document top level tag, so we need to process it's children
|
||||||
|
let id = find_attribute_value(node.to_owned(), "id");
|
||||||
|
if id.is_some() {
|
||||||
|
return process_nested_element(node);
|
||||||
|
}
|
||||||
|
process_inline_element(node, attributes.to_owned())
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => process_default_element(node, data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_default_element(
|
||||||
|
node: ElementRef,
|
||||||
|
mut data: HashMap<String, Value>,
|
||||||
|
) -> Option<JSONResult> {
|
||||||
|
let tag_name = get_tag_name(node.to_owned());
|
||||||
|
|
||||||
|
let ty = match tag_name.as_str() {
|
||||||
|
HTML_TAG_NAME => PAGE,
|
||||||
|
P_TAG_NAME => PARAGRAPH,
|
||||||
|
ASIDE_TAG_NAME | ARTICLE_TAG_NAME => CALLOUT,
|
||||||
|
HR_TAG_NAME => DIVIDER,
|
||||||
|
_ => PARAGRAPH,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (delta, children) = process_node_children(node, &None, None);
|
||||||
|
|
||||||
|
if !delta.is_empty() {
|
||||||
|
data.insert(DELTA.to_string(), delta_to_json(&delta));
|
||||||
|
}
|
||||||
|
Some(JSONResult::Block(NestedBlock {
|
||||||
|
ty: ty.to_string(),
|
||||||
|
children,
|
||||||
|
data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_image_element(node: ElementRef) -> Option<JSONResult> {
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
if let Some(src) = find_attribute_value(node, SRC) {
|
||||||
|
data.insert(URL.to_string(), Value::String(src));
|
||||||
|
}
|
||||||
|
Some(JSONResult::Block(NestedBlock {
|
||||||
|
ty: IMAGE.to_string(),
|
||||||
|
children: Default::default(),
|
||||||
|
data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_code_element(node: ElementRef) -> Option<JSONResult> {
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
|
||||||
|
// find code element and get language and delta, then insert into data
|
||||||
|
if let Some(code_child) = find_child_node(node.to_owned(), CODE_TAG_NAME.to_string()) {
|
||||||
|
// get language
|
||||||
|
if let Some(class) = find_attribute_value(code_child.to_owned(), CLASS) {
|
||||||
|
let lang = class.split('-').last().unwrap_or_default();
|
||||||
|
data.insert(LANGUAGE.to_string(), Value::String(lang.to_string()));
|
||||||
|
}
|
||||||
|
// get delta
|
||||||
|
let text = code_child.text().collect::<String>();
|
||||||
|
if let Ok(delta) = serde_json::to_value(vec![InsertDelta {
|
||||||
|
insert: text,
|
||||||
|
attributes: None,
|
||||||
|
}]) {
|
||||||
|
data.insert(DELTA.to_string(), delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(JSONResult::Block(NestedBlock {
|
||||||
|
ty: CODE.to_string(),
|
||||||
|
children: Default::default(),
|
||||||
|
data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process "ul" | "ol" | "dl" | "menu" element
|
||||||
|
fn process_nested_element(node: ElementRef) -> Option<JSONResult> {
|
||||||
|
let tag_name = get_tag_name(node.to_owned());
|
||||||
|
|
||||||
|
let ty = match tag_name.as_str() {
|
||||||
|
UL_TAG_NAME => BULLETED_LIST,
|
||||||
|
OL_TAG_NAME => NUMBERED_LIST,
|
||||||
|
_ => PARAGRAPH,
|
||||||
|
};
|
||||||
|
let (_, children) = process_node_children(node, &Some(ty.to_string()), None);
|
||||||
|
Some(JSONResult::BlockArray(children))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process <li> element, if it's a checkbox, then return a todo list, otherwise return a normal list.
|
||||||
|
fn process_li_element(
|
||||||
|
node: ElementRef,
|
||||||
|
list_type: Option<String>,
|
||||||
|
mut data: HashMap<String, Value>,
|
||||||
|
) -> Option<JSONResult> {
|
||||||
|
let mut ty = list_type.unwrap_or(BULLETED_LIST.to_string());
|
||||||
|
if let Some(role) = find_attribute_value(node.to_owned(), ROLE) {
|
||||||
|
if role == CHECKBOX {
|
||||||
|
if let Some(checked_attr) = find_attribute_value(node.to_owned(), ARIA_CHECKED) {
|
||||||
|
let checked = match checked_attr.as_str() {
|
||||||
|
"true" => true,
|
||||||
|
"false" => false,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
data.insert(
|
||||||
|
CHECKED.to_string(),
|
||||||
|
serde_json::to_value(checked).unwrap_or_default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
data.insert(
|
||||||
|
CHECKED.to_string(),
|
||||||
|
serde_json::to_value(false).unwrap_or_default(),
|
||||||
|
);
|
||||||
|
ty = TODO_LIST.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process_node_summary_and_details(ty, node, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process children and handle potential nesting
|
||||||
|
// <li>
|
||||||
|
// <p> title </p>
|
||||||
|
// <p> content </p>
|
||||||
|
// </li>
|
||||||
|
// Or Process children and handle potential consecutive arrangement
|
||||||
|
// <li>title<p>content</p></li>
|
||||||
|
// li | blockquote | details
|
||||||
|
fn process_node_summary_and_details(
|
||||||
|
ty: String,
|
||||||
|
node: ElementRef,
|
||||||
|
mut data: HashMap<String, Value>,
|
||||||
|
) -> Option<JSONResult> {
|
||||||
|
let (delta, children) = process_node_children(node, &Some(ty.to_string()), None);
|
||||||
|
if delta.is_empty() {
|
||||||
|
if let Some(first_child) = children.first() {
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
if let Some(first_child_delta) = first_child.data.get(DELTA) {
|
||||||
|
data.insert(DELTA.to_string(), first_child_delta.to_owned());
|
||||||
|
let rest_children = children.iter().skip(1).cloned().collect();
|
||||||
|
return Some(JSONResult::Block(NestedBlock {
|
||||||
|
ty,
|
||||||
|
children: rest_children,
|
||||||
|
data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.insert(DELTA.to_string(), delta_to_json(&delta));
|
||||||
|
}
|
||||||
|
Some(JSONResult::Block(NestedBlock {
|
||||||
|
ty,
|
||||||
|
children,
|
||||||
|
data: data.to_owned(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_heading_element(
|
||||||
|
node: ElementRef,
|
||||||
|
mut data: HashMap<String, Value>,
|
||||||
|
) -> Option<JSONResult> {
|
||||||
|
let tag_name = get_tag_name(node.to_owned());
|
||||||
|
let level = match tag_name.chars().last().unwrap_or_default() {
|
||||||
|
'1' => 1,
|
||||||
|
'2' => 2,
|
||||||
|
// default to h3 even if it's h4, h5, h6
|
||||||
|
_ => 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
data.insert(
|
||||||
|
LEVEL.to_string(),
|
||||||
|
serde_json::to_value(level).unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (delta, children) = process_node_children(node, &None, None);
|
||||||
|
if !delta.is_empty() {
|
||||||
|
data.insert(
|
||||||
|
DELTA.to_string(),
|
||||||
|
serde_json::to_value(delta).unwrap_or_default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(JSONResult::Block(NestedBlock {
|
||||||
|
ty: HEADING.to_string(),
|
||||||
|
children,
|
||||||
|
data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process <a> <em> <strong> <u> <s> <code> <span> <br>
|
||||||
|
fn process_inline_element(
|
||||||
|
node: ElementRef,
|
||||||
|
attributes: Option<HashMap<String, Value>>,
|
||||||
|
) -> Option<JSONResult> {
|
||||||
|
let tag_name = get_tag_name(node.to_owned());
|
||||||
|
|
||||||
|
let attributes = get_delta_attributes_for(&tag_name, &get_node_attrs(node), attributes);
|
||||||
|
let (delta, children) = process_node_children(node, &None, attributes);
|
||||||
|
Some(if !delta.is_empty() {
|
||||||
|
JSONResult::DeltaArray(delta)
|
||||||
|
} else {
|
||||||
|
JSONResult::BlockArray(children)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_node_children(
|
||||||
|
node: ElementRef,
|
||||||
|
list_type: &Option<String>,
|
||||||
|
attributes: Option<HashMap<String, Value>>,
|
||||||
|
) -> (Vec<InsertDelta>, Vec<NestedBlock>) {
|
||||||
|
let tag_name = get_tag_name(node.to_owned());
|
||||||
|
let mut delta = Vec::new();
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
for child in node.children() {
|
||||||
|
if let Some(child_element) = ElementRef::wrap(child) {
|
||||||
|
if let Some(child_json) = flatten_element_to_json(child_element, list_type, &attributes) {
|
||||||
|
match child_json {
|
||||||
|
JSONResult::Delta(op) => delta.push(op),
|
||||||
|
JSONResult::Block(block) => children.push(block),
|
||||||
|
JSONResult::BlockArray(blocks) => children.extend(blocks),
|
||||||
|
JSONResult::DeltaArray(ops) => delta.extend(ops),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// put text into delta while child is a text node
|
||||||
|
let text = child
|
||||||
|
.value()
|
||||||
|
.as_text()
|
||||||
|
.map(|text| text.text.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(op) = node_to_delta(&tag_name, text, &mut get_node_attrs(node), &attributes) {
|
||||||
|
delta.push(op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(delta, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get attributes from style
|
||||||
|
// for example: style="font-weight: bold; font-style: italic; text-decoration: underline; text-decoration: line-through;"
|
||||||
|
fn get_attributes_with_style(style: &str) -> HashMap<String, Value> {
|
||||||
|
let mut attributes = HashMap::new();
|
||||||
|
|
||||||
|
for property in style.split(';') {
|
||||||
|
let parts: Vec<&str> = property.split(':').map(|s| s.trim()).collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
if parts.len() != 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (key, value) = (parts[0], parts[1]);
|
||||||
|
|
||||||
|
match key {
|
||||||
|
FONT_WEIGHT if value.contains(BOLD) => {
|
||||||
|
attributes.insert(BOLD.to_string(), Value::Bool(true));
|
||||||
|
},
|
||||||
|
FONT_STYLE if value.contains(ITALIC) => {
|
||||||
|
attributes.insert(ITALIC.to_string(), Value::Bool(true));
|
||||||
|
},
|
||||||
|
TEXT_DECORATION if value.contains(UNDERLINE) => {
|
||||||
|
attributes.insert(UNDERLINE.to_string(), Value::Bool(true));
|
||||||
|
},
|
||||||
|
TEXT_DECORATION if value.contains(LINE_THROUGH) => {
|
||||||
|
attributes.insert(STRIKETHROUGH.to_string(), Value::Bool(true));
|
||||||
|
},
|
||||||
|
BACKGROUND_COLOR => {
|
||||||
|
attributes.insert(BG_COLOR.to_string(), Value::String(value.to_string()));
|
||||||
|
},
|
||||||
|
COLOR => {
|
||||||
|
attributes.insert(FONT_COLOR.to_string(), Value::String(value.to_string()));
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// get attributes from tag name
|
||||||
|
// input <a href="https://www.google.com">Google</a>
|
||||||
|
// export attributes: { "href": "https://www.google.com" }
|
||||||
|
// input <em>Italic</em>
|
||||||
|
// export attributes: { "italic": true }
|
||||||
|
// input <strong>Bold</strong>
|
||||||
|
// export attributes: { "bold": true }
|
||||||
|
// input <u>Underline</u>
|
||||||
|
// export attributes: { "underline": true }
|
||||||
|
// input <s>Strikethrough</s>
|
||||||
|
// export attributes: { "strikethrough": true }
|
||||||
|
// input <code>Code</code>
|
||||||
|
// export attributes: { "code": true }
|
||||||
|
fn get_delta_attributes_for(
|
||||||
|
tag_name: &str,
|
||||||
|
attrs: &Attrs,
|
||||||
|
parent_attributes: Option<HashMap<String, Value>>,
|
||||||
|
) -> Option<HashMap<String, Value>> {
|
||||||
|
let href = find_attribute_value_from_attrs(attrs, HREF);
|
||||||
|
|
||||||
|
let style = find_attribute_value_from_attrs(attrs, STYLE);
|
||||||
|
|
||||||
|
let mut attributes = get_attributes_with_style(&style);
|
||||||
|
if let Some(parent_attributes) = parent_attributes {
|
||||||
|
parent_attributes.iter().for_each(|(k, v)| {
|
||||||
|
attributes.insert(k.to_string(), v.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match tag_name {
|
||||||
|
CODE_TAG_NAME => {
|
||||||
|
attributes.insert(CODE.to_string(), Value::Bool(true));
|
||||||
|
},
|
||||||
|
MARK_TAG_NAME => {
|
||||||
|
attributes.insert(BG_COLOR.to_string(), Value::String("#FFFF00".to_string()));
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
if LINK_TAGS.contains(&tag_name) {
|
||||||
|
attributes.insert(HREF.to_string(), Value::String(href));
|
||||||
|
}
|
||||||
|
if ITALIC_TAGS.contains(&tag_name) {
|
||||||
|
attributes.insert(ITALIC.to_string(), Value::Bool(true));
|
||||||
|
}
|
||||||
|
if BOLD_TAGS.contains(&tag_name) {
|
||||||
|
attributes.insert(BOLD.to_string(), Value::Bool(true));
|
||||||
|
}
|
||||||
|
if UNDERLINE_TAGS.contains(&tag_name) {
|
||||||
|
attributes.insert(UNDERLINE.to_string(), Value::Bool(true));
|
||||||
|
}
|
||||||
|
if STRIKETHROUGH_TAGS.contains(&tag_name) {
|
||||||
|
attributes.insert(STRIKETHROUGH.to_string(), Value::Bool(true));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if attributes.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform text_node to delta
|
||||||
|
// input <a href="https://www.google.com">Google</a>
|
||||||
|
// export delta: [{ "insert": "Google", "attributes": { "href": "https://www.google.com" } }]
|
||||||
|
fn node_to_delta(
|
||||||
|
tag_name: &str,
|
||||||
|
text: String,
|
||||||
|
attrs: &mut Attrs,
|
||||||
|
parent_attributes: &Option<HashMap<String, Value>>,
|
||||||
|
) -> Option<InsertDelta> {
|
||||||
|
let attributes = get_delta_attributes_for(tag_name, attrs, parent_attributes.to_owned());
|
||||||
|
if text.trim().is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(InsertDelta {
|
||||||
|
insert: text,
|
||||||
|
attributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// get tag name from node
|
||||||
|
fn get_tag_name(node: ElementRef) -> String {
|
||||||
|
node.value().name().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_node_attrs(node: ElementRef) -> Attrs {
|
||||||
|
node.value().attrs()
|
||||||
|
}
|
||||||
|
// find attribute value from node
|
||||||
|
fn find_attribute_value(node: ElementRef, attr_name: &str) -> Option<String> {
|
||||||
|
node
|
||||||
|
.value()
|
||||||
|
.attrs()
|
||||||
|
.find(|(name, _)| *name == attr_name)
|
||||||
|
.map(|(_, value)| value.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_attribute_value_from_attrs(attrs: &Attrs, attr_name: &str) -> String {
|
||||||
|
// The attrs need to be mutable, because the find method will consume the attrs
|
||||||
|
// So we clone it and use the clone one
|
||||||
|
let mut attrs = attrs.clone();
|
||||||
|
attrs
|
||||||
|
.find(|(name, _)| *name == attr_name)
|
||||||
|
.map(|(_, value)| value.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_child_node(node: ElementRef, child_tag_name: String) -> Option<ElementRef> {
|
||||||
|
node
|
||||||
|
.children()
|
||||||
|
.find(|child| {
|
||||||
|
if let Some(child_element) = ElementRef::wrap(child.to_owned()) {
|
||||||
|
return get_tag_name(child_element) == child_tag_name;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.and_then(|child| ElementRef::wrap(child.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delta_to_json(delta: &Vec<InsertDelta>) -> Value {
|
||||||
|
serde_json::to_value(delta).unwrap_or_default()
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
pub mod constant;
|
pub mod constant;
|
||||||
pub mod document_data_parser;
|
pub mod document_data_parser;
|
||||||
|
pub mod external;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
pub mod parser_entities;
|
pub mod parser_entities;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
use crate::parse::NotEmptyStr;
|
use crate::parse::NotEmptyStr;
|
||||||
use crate::parser::constant::{
|
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::{
|
use crate::parser::utils::{
|
||||||
convert_insert_delta_from_json, convert_nested_block_children_to_html, delta_to_html,
|
convert_insert_delta_from_json, convert_nested_block_children_to_html, delta_to_html,
|
||||||
delta_to_text,
|
delta_to_text, required_not_empty_str, serialize_color_attribute,
|
||||||
};
|
};
|
||||||
use flowy_derive::ProtoBuf;
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
use flowy_error::ErrorCode;
|
use flowy_error::ErrorCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct SelectionPB {
|
pub struct SelectionPB {
|
||||||
@ -43,7 +40,7 @@ pub struct RangePB {
|
|||||||
* @field text: bool // export text data
|
* @field text: bool // export text data
|
||||||
*/
|
*/
|
||||||
#[derive(Default, ProtoBuf, Debug, Clone)]
|
#[derive(Default, ProtoBuf, Debug, Clone)]
|
||||||
pub struct ExportTypePB {
|
pub struct ParseTypePB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
|
|
||||||
@ -57,7 +54,7 @@ pub struct ExportTypePB {
|
|||||||
* ConvertDocumentPayloadPB
|
* ConvertDocumentPayloadPB
|
||||||
* @field document_id: String
|
* @field document_id: String
|
||||||
* @file range: Option<RangePB> - optional // if range is None, copy the whole document
|
* @file range: Option<RangePB> - optional // if range is None, copy the whole document
|
||||||
* @field export_types: [ExportTypePB]
|
* @field parse_types: [ParseTypePB]
|
||||||
*/
|
*/
|
||||||
#[derive(Default, ProtoBuf)]
|
#[derive(Default, ProtoBuf)]
|
||||||
pub struct ConvertDocumentPayloadPB {
|
pub struct ConvertDocumentPayloadPB {
|
||||||
@ -68,7 +65,7 @@ pub struct ConvertDocumentPayloadPB {
|
|||||||
pub range: Option<RangePB>,
|
pub range: Option<RangePB>,
|
||||||
|
|
||||||
#[pb(index = 3)]
|
#[pb(index = 3)]
|
||||||
pub export_types: ExportTypePB,
|
pub parse_types: ParseTypePB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf, Debug)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
@ -92,7 +89,7 @@ pub struct Range {
|
|||||||
pub end: Selection,
|
pub end: Selection,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExportType {
|
pub struct ParseType {
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
pub html: bool,
|
pub html: bool,
|
||||||
pub text: bool,
|
pub text: bool,
|
||||||
@ -101,10 +98,10 @@ pub struct ExportType {
|
|||||||
pub struct ConvertDocumentParams {
|
pub struct ConvertDocumentParams {
|
||||||
pub document_id: String,
|
pub document_id: String,
|
||||||
pub range: Option<Range>,
|
pub range: Option<Range>,
|
||||||
pub export_types: ExportType,
|
pub parse_types: ParseType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExportType {
|
impl ParseType {
|
||||||
pub fn any_enabled(&self) -> bool {
|
pub fn any_enabled(&self) -> bool {
|
||||||
self.json || self.html || self.text
|
self.json || self.html || self.text
|
||||||
}
|
}
|
||||||
@ -129,9 +126,9 @@ impl From<RangePB> for Range {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ExportTypePB> for ExportType {
|
impl From<ParseTypePB> for ParseType {
|
||||||
fn from(data: ExportTypePB) -> Self {
|
fn from(data: ParseTypePB) -> Self {
|
||||||
ExportType {
|
ParseType {
|
||||||
json: data.json,
|
json: data.json,
|
||||||
html: data.html,
|
html: data.html,
|
||||||
text: data.text,
|
text: data.text,
|
||||||
@ -148,7 +145,7 @@ impl TryInto<ConvertDocumentParams> for ConvertDocumentPayloadPB {
|
|||||||
Ok(ConvertDocumentParams {
|
Ok(ConvertDocumentParams {
|
||||||
document_id: document_id.0,
|
document_id: document_id.0,
|
||||||
range,
|
range,
|
||||||
export_types: self.export_types.into(),
|
parse_types: self.parse_types.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,88 +166,88 @@ impl InsertDelta {
|
|||||||
pub fn to_html(&self) -> String {
|
pub fn to_html(&self) -> String {
|
||||||
let mut html = String::new();
|
let mut html = String::new();
|
||||||
let mut style = String::new();
|
let mut style = String::new();
|
||||||
|
let mut html_attributes = String::new();
|
||||||
// If there are attributes, serialize them as a HashMap.
|
// If there are attributes, serialize them as a HashMap.
|
||||||
if let Some(attrs) = &self.attributes {
|
if let Some(attrs) = &self.attributes {
|
||||||
// Serialize the font color attributes.
|
// Serialize the color attributes.
|
||||||
if let Some(color) = attrs.get(FONT_COLOR) {
|
style.push_str(&serialize_color_attribute(attrs, FONT_COLOR, COLOR));
|
||||||
style.push_str(&format!(
|
|
||||||
"color: {};",
|
|
||||||
color.to_string().replace("0x", "#").trim_matches('\"')
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// Serialize the background color attributes.
|
// Serialize the background color attributes.
|
||||||
if let Some(color) = attrs.get(BG_COLOR) {
|
style.push_str(&serialize_color_attribute(
|
||||||
style.push_str(&format!(
|
attrs,
|
||||||
"background-color: {};",
|
BG_COLOR,
|
||||||
color.to_string().replace("0x", "#").trim_matches('\"')
|
BACKGROUND_COLOR,
|
||||||
));
|
));
|
||||||
}
|
|
||||||
// Serialize the href attributes.
|
// Serialize the href attributes.
|
||||||
if let Some(href) = attrs.get(HREF) {
|
if let Some(href) = attrs.get(HREF) {
|
||||||
html.push_str(&format!("<a href={}>", href));
|
html.push_str(&format!("<{} {}={}>", A_TAG_NAME, HREF, href));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize the code attributes.
|
// Serialize the code attributes.
|
||||||
if let Some(code) = attrs.get(CODE) {
|
if let Some(code) = attrs.get(CODE) {
|
||||||
if code.as_bool().unwrap_or(false) {
|
if code.as_bool().unwrap_or(false) {
|
||||||
html.push_str("<code>");
|
html.push_str(&format!("<{}>", CODE_TAG_NAME));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize the italic, underline, strikethrough, bold, formula attributes.
|
// Serialize the italic, underline, strikethrough, bold, formula attributes.
|
||||||
if let Some(italic) = attrs.get(ITALIC) {
|
if let Some(italic) = attrs.get(ITALIC) {
|
||||||
if italic.as_bool().unwrap_or(false) {
|
if italic.as_bool().unwrap_or(false) {
|
||||||
style.push_str("font-style: italic;");
|
style.push_str(FONT_STYLE_ITALIC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(underline) = attrs.get(UNDERLINE) {
|
if let Some(underline) = attrs.get(UNDERLINE) {
|
||||||
if underline.as_bool().unwrap_or(false) {
|
if underline.as_bool().unwrap_or(false) {
|
||||||
style.push_str("text-decoration: underline;");
|
style.push_str(TEXT_DECORATION_UNDERLINE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(strikethrough) = attrs.get(STRIKETHROUGH) {
|
if let Some(strikethrough) = attrs.get(STRIKETHROUGH) {
|
||||||
if strikethrough.as_bool().unwrap_or(false) {
|
if strikethrough.as_bool().unwrap_or(false) {
|
||||||
style.push_str("text-decoration: line-through;");
|
style.push_str(TEXT_DECORATION_LINE_THROUGH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(bold) = attrs.get(BOLD) {
|
if let Some(bold) = attrs.get(BOLD) {
|
||||||
if bold.as_bool().unwrap_or(false) {
|
if bold.as_bool().unwrap_or(false) {
|
||||||
style.push_str("font-weight: bold;");
|
style.push_str(FONT_WEIGHT_BOLD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(formula) = attrs.get(FORMULA) {
|
if let Some(formula) = attrs.get(FORMULA) {
|
||||||
if formula.as_bool().unwrap_or(false) {
|
if formula.as_bool().unwrap_or(false) {
|
||||||
style.push_str("font-family: fantasy;");
|
style.push_str(FONT_FAMILY_FANTASY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(direction) = attrs.get(TEXT_DIRECTION) {
|
||||||
|
html_attributes.push_str(&format!(" {}=\"{}\"", DIR_ATTR_NAME, direction));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Serialize the attributes to style.
|
|
||||||
if !style.is_empty() {
|
if !style.is_empty() {
|
||||||
html.push_str(&format!("<span style=\"{}\">", style));
|
html_attributes.push_str(&format!(" {}=\"{}\"", STYLE, style));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !html_attributes.is_empty() {
|
||||||
|
html.push_str(&format!("<{}{}>", SPAN_TAG_NAME, html_attributes));
|
||||||
}
|
}
|
||||||
// Serialize the insert field.
|
// Serialize the insert field.
|
||||||
html.push_str(&self.insert);
|
html.push_str(&self.insert);
|
||||||
|
|
||||||
// Close the style tag.
|
// Close the style tag.
|
||||||
if !style.is_empty() {
|
if !html_attributes.is_empty() {
|
||||||
html.push_str("</span>");
|
html.push_str(&format!("</{}>", SPAN_TAG_NAME));
|
||||||
}
|
}
|
||||||
// Close the tags: <a>, <code>.
|
// Close the tags: <a>, <code>.
|
||||||
if let Some(attrs) = &self.attributes {
|
if let Some(attrs) = &self.attributes {
|
||||||
if attrs.contains_key(HREF) {
|
|
||||||
html.push_str("</a>");
|
|
||||||
}
|
|
||||||
if attrs.contains_key(CODE) {
|
if attrs.contains_key(CODE) {
|
||||||
html.push_str("</code>");
|
html.push_str(&format!("</{}>", CODE_TAG_NAME));
|
||||||
|
}
|
||||||
|
if attrs.contains_key(HREF) {
|
||||||
|
html.push_str(&format!("</{}>", A_TAG_NAME));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
html
|
html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct NestedBlock {
|
pub struct NestedBlock {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub id: String,
|
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub ty: String,
|
pub ty: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -262,7 +259,6 @@ pub struct NestedBlock {
|
|||||||
impl Eq for NestedBlock {}
|
impl Eq for NestedBlock {}
|
||||||
|
|
||||||
impl PartialEq for NestedBlock {
|
impl PartialEq for NestedBlock {
|
||||||
// ignore the id field
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.ty == other.ty
|
self.ty == other.ty
|
||||||
&& self.data.iter().all(|(k, v)| {
|
&& self.data.iter().all(|(k, v)| {
|
||||||
@ -278,24 +274,9 @@ impl PartialEq for NestedBlock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConvertBlockToHtmlParams {
|
|
||||||
pub prev_block_ty: Option<String>,
|
|
||||||
pub next_block_ty: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NestedBlock {
|
impl NestedBlock {
|
||||||
pub fn new(
|
pub fn new(ty: String, data: HashMap<String, Value>, children: Vec<NestedBlock>) -> Self {
|
||||||
id: String,
|
Self { ty, data, children }
|
||||||
ty: String,
|
|
||||||
data: HashMap<String, Value>,
|
|
||||||
children: Vec<NestedBlock>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
ty,
|
|
||||||
data,
|
|
||||||
children,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_child(&mut self, child: NestedBlock) {
|
pub fn add_child(&mut self, child: NestedBlock) {
|
||||||
@ -316,115 +297,147 @@ impl NestedBlock {
|
|||||||
let next_block_ty = params.next_block_ty.unwrap_or_default();
|
let next_block_ty = params.next_block_ty.unwrap_or_default();
|
||||||
|
|
||||||
match self.ty.as_str() {
|
match self.ty.as_str() {
|
||||||
|
// <h1>Hello</h1>
|
||||||
HEADING => {
|
HEADING => {
|
||||||
let level = self.data.get(LEVEL).unwrap_or(&Value::Null);
|
let level = self.data.get(LEVEL).unwrap_or(&Value::Null);
|
||||||
if level.as_u64().unwrap_or(0) > 6 {
|
if level.as_u64().unwrap_or(0) > 6 {
|
||||||
html.push_str(&format!("<h6>{}</h6>", text_html));
|
html.push_str(&format!("<{}>{}</{}>", H6_TAG_NAME, text_html, H6_TAG_NAME));
|
||||||
} else {
|
} else {
|
||||||
html.push_str(&format!("<h{}>{}</h{}>", level, text_html, level));
|
html.push_str(&format!("<h{}>{}</h{}>", level, text_html, level));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// <p>Hello</p>
|
||||||
PARAGRAPH => {
|
PARAGRAPH => {
|
||||||
html.push_str(&format!("<p>{}</p>", text_html));
|
html.push_str(&format!("<{}>{}</{}>", P_TAG_NAME, text_html, P_TAG_NAME));
|
||||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
self.to_owned(),
|
self.to_owned(),
|
||||||
)));
|
)));
|
||||||
},
|
},
|
||||||
|
// <aside>😁Hello</aside>
|
||||||
CALLOUT => {
|
CALLOUT => {
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
"<p>{}{}</p>",
|
"<{}>{}{}</{}>",
|
||||||
|
ASIDE_TAG_NAME,
|
||||||
self
|
self
|
||||||
.data
|
.data
|
||||||
.get(ICON)
|
.get(ICON)
|
||||||
.unwrap_or(&Value::Null)
|
.unwrap_or(&Value::Null)
|
||||||
.to_string()
|
.to_string()
|
||||||
.trim_matches('\"'),
|
.trim_matches('\"'),
|
||||||
text_html
|
text_html,
|
||||||
|
ASIDE_TAG_NAME
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
// <img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt="Google Logo" />
|
||||||
IMAGE => {
|
IMAGE => {
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
"<img src={} alt={} />",
|
"<{} src={} alt={} />",
|
||||||
|
IMG_TAG_NAME,
|
||||||
self.data.get(URL).unwrap(),
|
self.data.get(URL).unwrap(),
|
||||||
"AppFlowy-Image"
|
"AppFlowy-Image"
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
// <hr />
|
||||||
DIVIDER => {
|
DIVIDER => {
|
||||||
html.push_str("<hr />");
|
html.push_str(&format!("<{} />", HR_TAG_NAME));
|
||||||
},
|
},
|
||||||
|
// <p>$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$</p>
|
||||||
MATH_EQUATION => {
|
MATH_EQUATION => {
|
||||||
let formula = self.data.get(FORMULA).unwrap_or(&Value::Null);
|
let formula = self.data.get(FORMULA).unwrap_or(&Value::Null);
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
"<p>{}</p>",
|
"<{}>{}</{}>",
|
||||||
formula.to_string().trim_matches('\"')
|
P_TAG_NAME,
|
||||||
|
formula.to_string().trim_matches('\"'),
|
||||||
|
P_TAG_NAME
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
// <pre><code class="language-js">console.log('Hello World!');</code></pre>
|
||||||
CODE => {
|
CODE => {
|
||||||
let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null);
|
let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null);
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
"<pre><code class=\"language-{}\">{}</code></pre>",
|
"<{}><{} {}=\"{}-{}\">{}</{}></{}>",
|
||||||
|
PRE_TAG_NAME,
|
||||||
|
CODE_TAG_NAME,
|
||||||
|
CLASS,
|
||||||
|
LANGUAGE,
|
||||||
language.to_string().trim_matches('\"'),
|
language.to_string().trim_matches('\"'),
|
||||||
text_html
|
text_html,
|
||||||
|
CODE_TAG_NAME,
|
||||||
|
PRE_TAG_NAME
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
BULLETED_LIST | NUMBERED_LIST | TODO_LIST | TOGGLE_LIST => {
|
// <details><summary>Hello</summary><p>World!</p></details>
|
||||||
let list_type = match self.ty.as_str() {
|
TOGGLE_LIST => {
|
||||||
BULLETED_LIST => "ul",
|
html.push_str(&format!("<{}>", DETAILS_TAG_NAME));
|
||||||
NUMBERED_LIST => "ol",
|
html.push_str(&format!(
|
||||||
TODO_LIST => "ul",
|
"<{}>{}</{}>",
|
||||||
TOGGLE_LIST => "ul",
|
SUMMARY_TAG_NAME, text_html, SUMMARY_TAG_NAME
|
||||||
_ => "ul", // Default to "ul" for unknown types
|
));
|
||||||
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
|
self.to_owned(),
|
||||||
|
)));
|
||||||
|
html.push_str(&format!("</{}>", DETAILS_TAG_NAME));
|
||||||
|
},
|
||||||
|
// <ul><li>Hello</li><li>World!</li></ul>
|
||||||
|
BULLETED_LIST | NUMBERED_LIST | TODO_LIST => {
|
||||||
|
let list_type = if self.ty == NUMBERED_LIST {
|
||||||
|
OL_TAG_NAME
|
||||||
|
} else {
|
||||||
|
UL_TAG_NAME
|
||||||
};
|
};
|
||||||
if prev_block_ty != self.ty {
|
if prev_block_ty != self.ty {
|
||||||
html.push_str(&format!("<{}>", list_type));
|
html.push_str(&format!("<{}>", list_type));
|
||||||
}
|
}
|
||||||
if self.ty == TODO_LIST {
|
if self.ty == TODO_LIST {
|
||||||
let checked_str = if self
|
let checked = self
|
||||||
.data
|
.data
|
||||||
.get(CHECKED)
|
.get(CHECKED)
|
||||||
.and_then(|checked| checked.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.unwrap_or(false)
|
.unwrap_or_default();
|
||||||
{
|
// <li role="checkbox" aria-checked="true">Hello</li>
|
||||||
"x"
|
html.push_str(&format!(
|
||||||
} else {
|
"<{} {}=\"{}\" {}=\"{}\">{}",
|
||||||
" "
|
LI_TAG_NAME, ROLE, CHECKBOX, ARIA_CHECKED, checked, text_html
|
||||||
};
|
));
|
||||||
html.push_str(&format!("<li>[{}] {}</li>", checked_str, text_html));
|
|
||||||
} else {
|
} else {
|
||||||
html.push_str(&format!("<li>{}</li>", text_html));
|
html.push_str(&format!("<{}>{}", LI_TAG_NAME, text_html));
|
||||||
}
|
}
|
||||||
|
|
||||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
self.to_owned(),
|
self.to_owned(),
|
||||||
)));
|
)));
|
||||||
|
html.push_str(&format!("</{}>", LI_TAG_NAME));
|
||||||
|
|
||||||
if next_block_ty != self.ty {
|
if next_block_ty != self.ty {
|
||||||
html.push_str(&format!("</{}>", list_type));
|
html.push_str(&format!("</{}>", list_type));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// <blockquote><p>Hello</p><p>World!</p></blockquote>
|
||||||
QUOTE => {
|
QUOTE => {
|
||||||
if prev_block_ty != self.ty {
|
if prev_block_ty != self.ty {
|
||||||
html.push_str("<blockquote>");
|
html.push_str(&format!("<{}>", BLOCKQUOTE_TAG_NAME));
|
||||||
}
|
}
|
||||||
html.push_str(&format!("<p>{}</p>", text_html));
|
html.push_str(&format!("<{}>{}</{}>", P_TAG_NAME, text_html, P_TAG_NAME));
|
||||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
self.to_owned(),
|
self.to_owned(),
|
||||||
)));
|
)));
|
||||||
if next_block_ty != self.ty {
|
if next_block_ty != self.ty {
|
||||||
html.push_str("</blockquote>");
|
html.push_str(&format!("</{}>", BLOCKQUOTE_TAG_NAME));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// <p>Hello</p>
|
||||||
PAGE => {
|
PAGE => {
|
||||||
if !text_html.is_empty() {
|
if !text_html.is_empty() {
|
||||||
html.push_str(&format!("<p>{}</p>", text_html));
|
html.push_str(&format!("<{}>{}</{}>", P_TAG_NAME, text_html, P_TAG_NAME));
|
||||||
}
|
}
|
||||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
self.to_owned(),
|
self.to_owned(),
|
||||||
)));
|
)));
|
||||||
},
|
},
|
||||||
|
// <p>Hello</p>
|
||||||
_ => {
|
_ => {
|
||||||
html.push_str(&format!("<p>{}</p>", text_html));
|
html.push_str(&format!("<{}>{}</{}>", P_TAG_NAME, text_html, P_TAG_NAME));
|
||||||
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
self.to_owned(),
|
self.to_owned(),
|
||||||
)));
|
)));
|
||||||
@ -439,7 +452,7 @@ impl NestedBlock {
|
|||||||
|
|
||||||
let delta_text = self
|
let delta_text = self
|
||||||
.data
|
.data
|
||||||
.get("delta")
|
.get(DELTA)
|
||||||
.and_then(convert_insert_delta_from_json)
|
.and_then(convert_insert_delta_from_json)
|
||||||
.map(|delta| delta_to_text(&delta))
|
.map(|delta| delta_to_text(&delta))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@ -479,3 +492,46 @@ impl NestedBlock {
|
|||||||
text
|
text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ConvertBlockToHtmlParams {
|
||||||
|
pub prev_block_ty: Option<String>,
|
||||||
|
pub next_block_ty: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, ProtoBuf_Enum, Clone, Default)]
|
||||||
|
pub enum InputType {
|
||||||
|
#[default]
|
||||||
|
Html = 0,
|
||||||
|
PlainText = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Debug, Validate)]
|
||||||
|
pub struct ConvertDataToJsonPayloadPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub data: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub input_type: InputType,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConvertDataToJsonParams {
|
||||||
|
pub data: String,
|
||||||
|
pub input_type: InputType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
|
pub struct ConvertDataToJsonResponsePB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub json: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<ConvertDataToJsonParams> for ConvertDataToJsonPayloadPB {
|
||||||
|
type Error = ErrorCode;
|
||||||
|
fn try_into(self) -> Result<ConvertDataToJsonParams, Self::Error> {
|
||||||
|
Ok(ConvertDataToJsonParams {
|
||||||
|
data: self.data,
|
||||||
|
input_type: self.input_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,74 +1,11 @@
|
|||||||
use crate::parser::constant::DELTA;
|
|
||||||
use crate::parser::parser_entities::{
|
use crate::parser::parser_entities::{
|
||||||
ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Selection,
|
ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Selection,
|
||||||
};
|
};
|
||||||
use collab_document::blocks::{Block, DocumentData};
|
use collab_document::blocks::DocumentData;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use validator::ValidationError;
|
||||||
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>> {
|
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
|
let text_map = data.meta.text_map.as_ref()?; // Retrieve the text_map reference
|
||||||
@ -165,3 +102,25 @@ pub fn convert_nested_block_children_to_html(block: Arc<NestedBlock>) -> String
|
|||||||
pub fn convert_insert_delta_from_json(delta_value: &Value) -> Option<Vec<InsertDelta>> {
|
pub fn convert_insert_delta_from_json(delta_value: &Value) -> Option<Vec<InsertDelta>> {
|
||||||
serde_json::from_value::<Vec<InsertDelta>>(delta_value.to_owned()).ok()
|
serde_json::from_value::<Vec<InsertDelta>>(delta_value.to_owned()).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn required_not_empty_str(s: &str) -> Result<(), ValidationError> {
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(ValidationError::new("should not be empty string"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize_color_attribute(
|
||||||
|
attrs: &HashMap<String, Value>,
|
||||||
|
attr_name: &str,
|
||||||
|
css_property: &str,
|
||||||
|
) -> String {
|
||||||
|
if let Some(color) = attrs.get(attr_name) {
|
||||||
|
return format!(
|
||||||
|
"{}: {};",
|
||||||
|
css_property,
|
||||||
|
color.to_string().replace("0x", "#").trim_matches('\"')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
<meta charset="UTF-8"><ul><li>Highlight</li><p>You can also</p><ul><li>nest</li></ul></ul>
|
<meta charset="UTF-8"><ul><li>Highlight<p>You can also</p><ul><li>nest</li></ul></li></ul>
|
@ -1,6 +1,6 @@
|
|||||||
<meta charset="UTF-8"><p>🥰
|
<meta charset="UTF-8"><aside>🥰
|
||||||
Like AppFlowy? Follow us:
|
Like AppFlowy? Follow us:
|
||||||
<a href="https://github.com/AppFlowy-IO/AppFlowy">GitHub</a>
|
<a href="https://github.com/AppFlowy-IO/AppFlowy">GitHub</a>
|
||||||
<a href="https://twitter.com/appflowy">Twitter</a>: @appflowy
|
<a href="https://twitter.com/appflowy">Twitter</a>: @appflowy
|
||||||
<a href="https://blog-appflowy.ghost.io/">Newsletter</a>
|
<a href="https://blog-appflowy.ghost.io/">Newsletter</a>
|
||||||
</p>
|
</aside>
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,34 @@
|
|||||||
|
<meta charset='utf-8'><h1>The Notion Document</h1>
|
||||||
|
<h1>Heading-1</h1>
|
||||||
|
<h2>Heading - 2</h2>
|
||||||
|
<h3>Heading - 3</h3>
|
||||||
|
<p>This is a paragraph</p>
|
||||||
|
<p>paragraph’s child</p>
|
||||||
|
<ul><li>This is a bulleted list - 1<ul><li>This is a bulleted list - 1 - 1</li></ul></li><li>This is a bulleted list - 2</li></ul>
|
||||||
|
<p>This is a paragraph</p>
|
||||||
|
<ul><li>[ ] This is a todo - 1<ul><li>[ ] This is a paragraph - 1-1</li></ul></li></ul>
|
||||||
|
<ol><li>This is a numbered list -1</li></ol>
|
||||||
|
<p>This is a paragraph</p>
|
||||||
|
<ul><li><p>This is a toggle list</p><p>This is a toggle child</p></li>
|
||||||
|
</ul>
|
||||||
|
<blockquote><p>This is a quote</p><p>This is a quote child</p></blockquote>
|
||||||
|
<p>This is a paragraph</p>
|
||||||
|
<hr>
|
||||||
|
<pre><code class="language-jsx">// This is the main function.
|
||||||
|
fn main() {
|
||||||
|
// Print text to the console.
|
||||||
|
**println**!("Hello World!");
|
||||||
|
}</code></pre>
|
||||||
|
<p>This is a paragraph</p>
|
||||||
|
<p><aside>
|
||||||
|
💡 callout</p>
|
||||||
|
<p></aside></p>
|
||||||
|
<p>This is a paragraph font-color bg-color <strong>bold</strong> <em>italic underline <s>strike-through</s> <code>inline-code</code> $inline-formula$ <a href="https://www.notion.so/The-Notion-Document-d4236da306b84f6199e4091705042d78?pvs=21">link</a></em></p>
|
||||||
|
<p>$$
|
||||||
|
|x| = \begin{cases}
|
||||||
|
x, &\quad x \geq 0 \\
|
||||||
|
-x, &\quad x < 0
|
||||||
|
\end{cases}
|
||||||
|
$$</p>
|
||||||
|
<p>End</p>
|
||||||
|
<!-- notionvc: 0b0229d7-b98a-4e36-8a64-f944de21ef0e -->
|
@ -1 +1 @@
|
|||||||
<meta charset="UTF-8"><ol><li>Highlight</li><p>You can also</p><ol><li>nest</li></ol></ol>
|
<meta charset="UTF-8"><ol><li>Highlight<p>You can also</p><ol><li>nest</li></ol></li></ol>
|
@ -1 +1 @@
|
|||||||
<meta charset="UTF-8"><ul><li>[x] Highlight</li><p>You can also</p><ul><li>[ ] nest</li></ul></ul>
|
<meta charset="UTF-8"><ul><li role="checkbox" aria-checked="true">Highlight<p>You can also</p><ul><li role="checkbox" aria-checked="false">nest</li></ul></li></ul>
|
@ -1 +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>
|
<meta charset="UTF-8"><details><summary>Click <code>?</code> at the bottom right for help and support.</summary><p>This is a paragraph</p><details><summary>This is a toggle list</summary></details></details>
|
@ -0,0 +1,351 @@
|
|||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "The Notion Document"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"level": 1,
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "heading"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "Heading-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"level": 1,
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "heading"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "Heading - 2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"level": 2,
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "heading"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "Heading - 3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"level": 3,
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "heading"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "Heading - 4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"level": 3,
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "heading"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "paragraph’s child"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a bulleted list - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "bulleted_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a bulleted list - 1 - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "bulleted_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a bulleted list - 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "bulleted_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII="
|
||||||
|
},
|
||||||
|
"type": "image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a todo - 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"checked": false,
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "todo_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII="
|
||||||
|
},
|
||||||
|
"type": "image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a todo - 1-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"checked": false,
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "todo_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a numbered list -1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "numbered_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a numbered list -2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "numbered_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a numbered list-1-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "numbered_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bg_color": "transparent",
|
||||||
|
"font_color": "#000000"
|
||||||
|
},
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text_direction": "ltr"
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {},
|
||||||
|
"type": "divider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {},
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {},
|
||||||
|
"type": "page"
|
||||||
|
}
|
371
frontend/rust-lib/flowy-document2/tests/assets/json/notion.json
Normal file
371
frontend/rust-lib/flowy-document2/tests/assets/json/notion.json
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"type": "page",
|
||||||
|
"data": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "The Notion Document"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"level": 1
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"data": {
|
||||||
|
"level": 1,
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "Heading-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"data": {
|
||||||
|
"level": 2,
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "Heading - 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"data": {
|
||||||
|
"level": 3,
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "Heading - 3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "paragraph’s child"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulleted_list",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a bulleted list - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "bulleted_list",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a bulleted list - 1 - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulleted_list",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a bulleted list - 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulleted_list",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "[ ] This is a todo - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "bulleted_list",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "[ ] This is a paragraph - 1-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "numbered_list",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a numbered list -1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulleted_list",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a toggle list"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a toggle child"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "quote",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a quote"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a quote child"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "divider",
|
||||||
|
"data": {},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "code",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n **println**!(\"Hello World!\");\n}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"language": "jsx"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "<aside>\n 💡 callout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "</aside>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph font-color bg-color "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"bold": true
|
||||||
|
},
|
||||||
|
"insert": "bold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"italic": true
|
||||||
|
},
|
||||||
|
"insert": "italic underline "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"italic": true,
|
||||||
|
"strikethrough": true
|
||||||
|
},
|
||||||
|
"insert": "strike-through"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"code": true,
|
||||||
|
"italic": true
|
||||||
|
},
|
||||||
|
"insert": "inline-code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"italic": true
|
||||||
|
},
|
||||||
|
"insert": " $inline-formula$ "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"href": "https://www.notion.so/The-Notion-Document-d4236da306b84f6199e4091705042d78?pvs=21",
|
||||||
|
"italic": true
|
||||||
|
},
|
||||||
|
"insert": "link"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "$$\n |x| = \\begin{cases}\n x, &\\quad x \\geq 0 \\\\\n -x, &\\quad x < 0\n \\end{cases}\n $$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,510 @@
|
|||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "# The Notion Document"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "# Heading-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "## Heading - 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "### Heading - 3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "paragraph’s child"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "- This is a bulleted list - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": " - This is a bulleted list - 1 - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "- This is a bulleted list - 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "- [ ] This is a todo - 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": " - [ ] This is a paragraph - 1-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "1. This is a numbered list -1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "- This is a toggle list"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": " This is a toggle child"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "> This is a quote"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": ">"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": ">"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "> This is a quote child"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": ">"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "---"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "```jsx"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "// This is the main function."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "fn main() {"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": " // Print text to the console."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": " **println**!(\"Hello World!\");"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "```"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "<aside>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "💡 callout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "</aside>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "This is a paragraph font-color bg-color **bold** *italic underline ~~strike-through~~ `inline-code` $inline-formula$ [link](https://www.notion.so/The-Notion-Document-d4236da306b84f6199e4091705042d78?pvs=21)*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "$$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "|x| = \\begin{cases} "
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": " x, &\\quad x \\geq 0 \\\\ "
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": " -x, &\\quad x < 0 "
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "\\end{cases}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "$$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [],
|
||||||
|
"data": {
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"attributes": null,
|
||||||
|
"insert": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {},
|
||||||
|
"type": "page"
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
# The Notion Document
|
||||||
|
|
||||||
|
# Heading-1
|
||||||
|
|
||||||
|
## Heading - 2
|
||||||
|
|
||||||
|
### Heading - 3
|
||||||
|
|
||||||
|
This is a paragraph
|
||||||
|
|
||||||
|
paragraph’s child
|
||||||
|
|
||||||
|
- This is a bulleted list - 1
|
||||||
|
- This is a bulleted list - 1 - 1
|
||||||
|
- This is a bulleted list - 2
|
||||||
|
|
||||||
|
This is a paragraph
|
||||||
|
|
||||||
|
- [ ] This is a todo - 1
|
||||||
|
- [ ] This is a paragraph - 1-1
|
||||||
|
1. This is a numbered list -1
|
||||||
|
|
||||||
|
This is a paragraph
|
||||||
|
|
||||||
|
- This is a toggle list
|
||||||
|
|
||||||
|
This is a toggle child
|
||||||
|
|
||||||
|
|
||||||
|
> This is a quote
|
||||||
|
>
|
||||||
|
>
|
||||||
|
> This is a quote child
|
||||||
|
>
|
||||||
|
|
||||||
|
This is a paragraph
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// This is the main function.
|
||||||
|
fn main() {
|
||||||
|
// Print text to the console.
|
||||||
|
**println**!("Hello World!");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a paragraph
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
💡 callout
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
This is a paragraph font-color bg-color **bold** *italic underline ~~strike-through~~ `inline-code` $inline-formula$ [link](https://www.notion.so/The-Notion-Document-d4236da306b84f6199e4091705042d78?pvs=21)*
|
||||||
|
|
||||||
|
$$
|
||||||
|
|x| = \begin{cases}
|
||||||
|
x, &\quad x \geq 0 \\
|
||||||
|
-x, &\quad x < 0
|
||||||
|
\end{cases}
|
||||||
|
$$
|
||||||
|
|
||||||
|
End
|
@ -0,0 +1 @@
|
|||||||
|
mod parser_test;
|
@ -0,0 +1,45 @@
|
|||||||
|
use flowy_document2::parser::external::parser::ExternalDataToNestedJSONParser;
|
||||||
|
use flowy_document2::parser::parser_entities::{InputType, NestedBlock};
|
||||||
|
|
||||||
|
macro_rules! generate_test_cases {
|
||||||
|
($($ty:ident),*) => {
|
||||||
|
[
|
||||||
|
$(
|
||||||
|
(
|
||||||
|
include_str!(concat!("../../assets/json/", stringify!($ty), ".json")),
|
||||||
|
include_str!(concat!("../../assets/html/", stringify!($ty), ".html")),
|
||||||
|
)
|
||||||
|
),*
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// test convert data to json
|
||||||
|
/// - input html: <p>Hello</p><p> World!</p>
|
||||||
|
#[tokio::test]
|
||||||
|
async fn html_to_document_test() {
|
||||||
|
let test_cases = generate_test_cases!(notion, google_docs);
|
||||||
|
|
||||||
|
for (json, html) in test_cases.iter() {
|
||||||
|
let parser = ExternalDataToNestedJSONParser::new(html.to_string(), InputType::Html);
|
||||||
|
let block = parser.to_nested_block();
|
||||||
|
assert!(block.is_some());
|
||||||
|
let block = block.unwrap();
|
||||||
|
let expect_block = serde_json::from_str::<NestedBlock>(json).unwrap();
|
||||||
|
assert_eq!(block, expect_block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// test convert data to json
|
||||||
|
/// - input plain text: Hello World!
|
||||||
|
#[tokio::test]
|
||||||
|
async fn plain_text_to_document_test() {
|
||||||
|
let plain_text = include_str!("../../assets/text/plain_text.txt");
|
||||||
|
let parser = ExternalDataToNestedJSONParser::new(plain_text.to_string(), InputType::PlainText);
|
||||||
|
let block = parser.to_nested_block();
|
||||||
|
assert!(block.is_some());
|
||||||
|
let block = block.unwrap();
|
||||||
|
let expect_json = include_str!("../../assets/json/plain_text.json");
|
||||||
|
let expect_block = serde_json::from_str::<NestedBlock>(expect_json).unwrap();
|
||||||
|
assert_eq!(block, expect_block);
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
mod document_data_parser_test;
|
mod document_data_parser_test;
|
||||||
mod html_text;
|
mod html;
|
||||||
mod json;
|
mod json;
|
||||||
|
mod parse_to_html_text;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::parser::html_text::utils::{assert_document_html_eq, assert_document_text_eq};
|
use crate::parser::parse_to_html_text::utils::{assert_document_html_eq, assert_document_text_eq};
|
||||||
|
|
||||||
macro_rules! generate_test_cases {
|
macro_rules! generate_test_cases {
|
||||||
($($block_ty:ident),*) => {
|
($($block_ty:ident),*) => {
|
Loading…
Reference in New Issue
Block a user