use crate::parse::NotEmptyStr; use crate::parser::constant::*; use crate::parser::utils::{ convert_insert_delta_from_json, convert_nested_block_children_to_html, delta_to_html, delta_to_text, required_not_empty_str, serialize_color_attribute, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; use validator::Validate; #[derive(Default, ProtoBuf)] pub struct SelectionPB { #[pb(index = 1)] pub block_id: String, #[pb(index = 2)] pub index: u32, #[pb(index = 3)] pub length: u32, } #[derive(Default, ProtoBuf)] pub struct RangePB { #[pb(index = 1)] pub start: SelectionPB, #[pb(index = 2)] pub end: SelectionPB, } /** * ExportTypePB * @field json: bool // export json data * @field html: bool // export html data * @field text: bool // export text data */ #[derive(Default, ProtoBuf, Debug, Clone)] pub struct ParseTypePB { #[pb(index = 1)] pub json: bool, #[pb(index = 2)] pub html: bool, #[pb(index = 3)] pub text: bool, } /** * ConvertDocumentPayloadPB * @field document_id: String * @file range: Option - optional // if range is None, copy the whole document * @field parse_types: [ParseTypePB] */ #[derive(Default, ProtoBuf)] pub struct ConvertDocumentPayloadPB { #[pb(index = 1)] pub document_id: String, #[pb(index = 2, one_of)] pub range: Option, #[pb(index = 3)] pub parse_types: ParseTypePB, } #[derive(Default, ProtoBuf, Debug)] pub struct ConvertDocumentResponsePB { #[pb(index = 1, one_of)] pub json: Option, #[pb(index = 2, one_of)] pub html: Option, #[pb(index = 3, one_of)] pub text: Option, } pub struct Selection { pub block_id: String, pub index: u32, pub length: u32, } pub struct Range { pub start: Selection, pub end: Selection, } pub struct ParseType { pub json: bool, pub html: bool, pub text: bool, } pub struct ConvertDocumentParams { pub document_id: String, pub range: Option, pub parse_types: ParseType, } impl ParseType { pub fn any_enabled(&self) -> bool { self.json || self.html || self.text } } impl From for Selection { fn from(data: SelectionPB) -> Self { Selection { block_id: data.block_id, index: data.index, length: data.length, } } } impl From for Range { fn from(data: RangePB) -> Self { Range { start: data.start.into(), end: data.end.into(), } } } impl From for ParseType { fn from(data: ParseTypePB) -> Self { ParseType { json: data.json, html: data.html, text: data.text, } } } impl TryInto for ConvertDocumentPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; let range = self.range.map(|data| data.into()); Ok(ConvertDocumentParams { document_id: document_id.0, range, parse_types: self.parse_types.into(), }) } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InsertDelta { #[serde(default)] pub insert: String, #[serde(default)] pub attributes: Option>, } impl InsertDelta { pub fn to_text(&self) -> String { self.insert.clone() } pub fn to_html(&self) -> String { let mut html = String::new(); let mut style = String::new(); let mut html_attributes = String::new(); // If there are attributes, serialize them as a HashMap. if let Some(attrs) = &self.attributes { // Serialize the color attributes. style.push_str(&serialize_color_attribute(attrs, FONT_COLOR, COLOR)); // Serialize the background color attributes. style.push_str(&serialize_color_attribute( attrs, BG_COLOR, BACKGROUND_COLOR, )); // Serialize the href attributes. if let Some(href) = attrs.get(HREF) { html.push_str(&format!("<{} {}={}>", A_TAG_NAME, HREF, href)); } // Serialize the code attributes. if let Some(code) = attrs.get(CODE) { if code.as_bool().unwrap_or(false) { html.push_str(&format!("<{}>", CODE_TAG_NAME)); } } // Serialize the italic, underline, strikethrough, bold, formula attributes. if let Some(italic) = attrs.get(ITALIC) { if italic.as_bool().unwrap_or(false) { style.push_str(FONT_STYLE_ITALIC); } } if let Some(underline) = attrs.get(UNDERLINE) { if underline.as_bool().unwrap_or(false) { style.push_str(TEXT_DECORATION_UNDERLINE); } } if let Some(strikethrough) = attrs.get(STRIKETHROUGH) { if strikethrough.as_bool().unwrap_or(false) { style.push_str(TEXT_DECORATION_LINE_THROUGH); } } if let Some(bold) = attrs.get(BOLD) { if bold.as_bool().unwrap_or(false) { style.push_str(FONT_WEIGHT_BOLD); } } if let Some(formula) = attrs.get(FORMULA) { if formula.as_bool().unwrap_or(false) { style.push_str(FONT_FAMILY_FANTASY); } } if let Some(direction) = attrs.get(TEXT_DIRECTION) { html_attributes.push_str(&format!(" {}=\"{}\"", DIR_ATTR_NAME, direction)); } } if !style.is_empty() { 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. html.push_str(&self.insert); // Close the style tag. if !html_attributes.is_empty() { html.push_str(&format!("", SPAN_TAG_NAME)); } // Close the tags: , . if let Some(attrs) = &self.attributes { if attrs.contains_key(CODE) { html.push_str(&format!("", CODE_TAG_NAME)); } if attrs.contains_key(HREF) { html.push_str(&format!("", A_TAG_NAME)); } } html } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct NestedBlock { #[serde(default)] #[serde(rename = "type")] pub ty: String, #[serde(default)] pub data: HashMap, #[serde(default)] pub children: Vec, } impl Eq for NestedBlock {} impl PartialEq for NestedBlock { fn eq(&self, other: &Self) -> bool { self.ty == other.ty && self.data.iter().all(|(k, v)| { let other_v = other.data.get(k).unwrap_or(&Value::Null); if k == DELTA { let v = convert_insert_delta_from_json(v); let other_v = convert_insert_delta_from_json(other_v); return v == other_v; } v == other_v }) && self.children == other.children } } impl NestedBlock { pub fn new(ty: String, data: HashMap, children: Vec) -> Self { Self { ty, data, children } } pub fn add_child(&mut self, child: NestedBlock) { self.children.push(child); } pub fn convert_to_html(&self, params: ConvertBlockToHtmlParams) -> String { let mut html = String::new(); let text_html = self .data .get("delta") .and_then(convert_insert_delta_from_json) .map(|delta| delta_to_html(&delta)) .unwrap_or_default(); let prev_block_ty = params.prev_block_ty.unwrap_or_default(); let next_block_ty = params.next_block_ty.unwrap_or_default(); match self.ty.as_str() { //

Hello

HEADING => { let level = self.data.get(LEVEL).unwrap_or(&Value::Null); if level.as_u64().unwrap_or(0) > 6 { html.push_str(&format!("<{}>{}", H6_TAG_NAME, text_html, H6_TAG_NAME)); } else { html.push_str(&format!("{}", level, text_html, level)); } }, //

Hello

PARAGRAPH => { html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); }, // CALLOUT => { html.push_str(&format!( "<{}>{}{}", ASIDE_TAG_NAME, self .data .get(ICON) .unwrap_or(&Value::Null) .to_string() .trim_matches('\"'), text_html, ASIDE_TAG_NAME )); }, // Google Logo IMAGE => { html.push_str(&format!( "<{} src={} alt={} />", IMG_TAG_NAME, self.data.get(URL).unwrap(), "AppFlowy-Image" )); }, //
DIVIDER => { html.push_str(&format!("<{} />", HR_TAG_NAME)); }, //

$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$

MATH_EQUATION => { let formula = self.data.get(FORMULA).unwrap_or(&Value::Null); html.push_str(&format!( "<{}>{}", P_TAG_NAME, formula.to_string().trim_matches('\"'), P_TAG_NAME )); }, //
console.log('Hello World!');
CODE => { let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null); html.push_str(&format!( "<{}><{} {}=\"{}-{}\">{}", PRE_TAG_NAME, CODE_TAG_NAME, CLASS, LANGUAGE, language.to_string().trim_matches('\"'), text_html, CODE_TAG_NAME, PRE_TAG_NAME )); }, //
Hello

World!

TOGGLE_LIST => { html.push_str(&format!("<{}>", DETAILS_TAG_NAME)); html.push_str(&format!( "<{}>{}", SUMMARY_TAG_NAME, text_html, SUMMARY_TAG_NAME )); html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); html.push_str(&format!("", DETAILS_TAG_NAME)); }, //
  • Hello
  • World!
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 { html.push_str(&format!("<{}>", list_type)); } if self.ty == TODO_LIST { let checked = self .data .get(CHECKED) .and_then(|v| v.as_bool()) .unwrap_or_default(); //
  • Hello
  • html.push_str(&format!( "<{} {}=\"{}\" {}=\"{}\">{}", LI_TAG_NAME, ROLE, CHECKBOX, ARIA_CHECKED, checked, text_html )); } else { html.push_str(&format!("<{}>{}", LI_TAG_NAME, text_html)); } html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); html.push_str(&format!("", LI_TAG_NAME)); if next_block_ty != self.ty { html.push_str(&format!("", list_type)); } }, //

    Hello

    World!

    QUOTE => { if prev_block_ty != self.ty { html.push_str(&format!("<{}>", BLOCKQUOTE_TAG_NAME)); } html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); if next_block_ty != self.ty { html.push_str(&format!("", BLOCKQUOTE_TAG_NAME)); } }, //

    Hello

    PAGE => { if !text_html.is_empty() { html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); } html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); }, //

    Hello

    _ => { html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); }, }; html } pub fn convert_to_text(&self) -> String { let mut text = String::new(); let delta_text = self .data .get(DELTA) .and_then(convert_insert_delta_from_json) .map(|delta| delta_to_text(&delta)) .unwrap_or_default(); match self.ty.as_str() { CALLOUT => { text.push_str(&format!( "{}{}\n", self .data .get(ICON) .unwrap_or(&Value::Null) .to_string() .trim_matches('\"'), delta_text )); }, MATH_EQUATION => { let formula = self.data.get(FORMULA).unwrap_or(&Value::Null); text.push_str(&format!("{}\n", formula.to_string().trim_matches('\"'))); }, PAGE => { if !delta_text.is_empty() { text.push_str(&format!("{}\n", delta_text)); } for child in &self.children { text.push_str(&child.convert_to_text()); } }, _ => { text.push_str(&format!("{}\n", delta_text)); for child in &self.children { text.push_str(&child.convert_to_text()); } }, }; text } } pub struct ConvertBlockToHtmlParams { pub prev_block_ty: Option, pub next_block_ty: Option, } #[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 for ConvertDataToJsonPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { Ok(ConvertDataToJsonParams { data: self.data, input_type: self.input_type, }) } }