mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
5facb61e23
* chore: rename flowy-folder2 to flowy-folder * chore: rename flowy-document2 to flowy-document * chore: fix test * chore: move lib-infra crate * chore: remove shared-lib * chore: fix clippy
538 lines
14 KiB
Rust
538 lines
14 KiB
Rust
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<RangePB> - 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<RangePB>,
|
|
|
|
#[pb(index = 3)]
|
|
pub parse_types: ParseTypePB,
|
|
}
|
|
|
|
#[derive(Default, ProtoBuf, Debug)]
|
|
pub struct ConvertDocumentResponsePB {
|
|
#[pb(index = 1, one_of)]
|
|
pub json: Option<String>,
|
|
#[pb(index = 2, one_of)]
|
|
pub html: Option<String>,
|
|
#[pb(index = 3, one_of)]
|
|
pub text: Option<String>,
|
|
}
|
|
|
|
pub struct Selection {
|
|
pub block_id: String,
|
|
pub index: u32,
|
|
pub length: u32,
|
|
}
|
|
|
|
pub struct Range {
|
|
pub start: Selection,
|
|
pub end: Selection,
|
|
}
|
|
|
|
pub struct ParseType {
|
|
pub json: bool,
|
|
pub html: bool,
|
|
pub text: bool,
|
|
}
|
|
|
|
pub struct ConvertDocumentParams {
|
|
pub document_id: String,
|
|
pub range: Option<Range>,
|
|
pub parse_types: ParseType,
|
|
}
|
|
|
|
impl ParseType {
|
|
pub fn any_enabled(&self) -> bool {
|
|
self.json || self.html || self.text
|
|
}
|
|
}
|
|
|
|
impl From<SelectionPB> for Selection {
|
|
fn from(data: SelectionPB) -> Self {
|
|
Selection {
|
|
block_id: data.block_id,
|
|
index: data.index,
|
|
length: data.length,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RangePB> for Range {
|
|
fn from(data: RangePB) -> Self {
|
|
Range {
|
|
start: data.start.into(),
|
|
end: data.end.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ParseTypePB> for ParseType {
|
|
fn from(data: ParseTypePB) -> Self {
|
|
ParseType {
|
|
json: data.json,
|
|
html: data.html,
|
|
text: data.text,
|
|
}
|
|
}
|
|
}
|
|
impl TryInto<ConvertDocumentParams> for ConvertDocumentPayloadPB {
|
|
type Error = ErrorCode;
|
|
fn try_into(self) -> Result<ConvertDocumentParams, Self::Error> {
|
|
let document_id =
|
|
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
|
let range = self.range.map(|data| data.into());
|
|
|
|
Ok(ConvertDocumentParams {
|
|
document_id: document_id.0,
|
|
range,
|
|
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<HashMap<String, Value>>,
|
|
}
|
|
|
|
impl InsertDelta {
|
|
pub fn to_text(&self) -> String {
|
|
self.insert.clone()
|
|
}
|
|
|
|
pub fn to_html(&self) -> String {
|
|
let mut html = String::new();
|
|
let mut style = String::new();
|
|
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: <a>, <code>.
|
|
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<String, Value>,
|
|
#[serde(default)]
|
|
pub children: Vec<NestedBlock>,
|
|
}
|
|
|
|
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<String, Value>, children: Vec<NestedBlock>) -> 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() {
|
|
// <h1>Hello</h1>
|
|
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!("<h{}>{}</h{}>", level, text_html, level));
|
|
}
|
|
},
|
|
// <p>Hello</p>
|
|
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(),
|
|
)));
|
|
},
|
|
// <aside>😁Hello</aside>
|
|
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
|
|
));
|
|
},
|
|
// <img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt="Google Logo" />
|
|
IMAGE => {
|
|
html.push_str(&format!(
|
|
"<{} src={} alt={} />",
|
|
IMG_TAG_NAME,
|
|
self.data.get(URL).unwrap(),
|
|
"AppFlowy-Image"
|
|
));
|
|
},
|
|
// <hr />
|
|
DIVIDER => {
|
|
html.push_str(&format!("<{} />", HR_TAG_NAME));
|
|
},
|
|
// <p>$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$</p>
|
|
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
|
|
));
|
|
},
|
|
// <pre><code class="language-js">console.log('Hello World!');</code></pre>
|
|
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
|
|
));
|
|
},
|
|
// <details><summary>Hello</summary><p>World!</p></details>
|
|
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));
|
|
},
|
|
// <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 {
|
|
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();
|
|
// <li role="checkbox" aria-checked="true">Hello</li>
|
|
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));
|
|
}
|
|
},
|
|
|
|
// <blockquote><p>Hello</p><p>World!</p></blockquote>
|
|
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));
|
|
}
|
|
},
|
|
// <p>Hello</p>
|
|
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(),
|
|
)));
|
|
},
|
|
// <p>Hello</p>
|
|
_ => {
|
|
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<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,
|
|
})
|
|
}
|
|
}
|