Skip to content

Commit ad7c317

Browse files
authored
feat: support link preview in markdown importer (#420)
* feat: support link preview in markdown importer * fix: cargo clippy * fix: ios ci * chore: remove unused clone
1 parent 864d03b commit ad7c317

File tree

6 files changed

+106
-8
lines changed

6 files changed

+106
-8
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ jobs:
120120
run: brew install protobuf
121121

122122
- name: Build
123-
run: IPHONEOS_DEPLOYMENT_TARGET=10.0 cargo build --target ${{ matrix.target }} --verbose
123+
run: BINDGEN_EXTRA_CLANG_ARGS="--target=arm64-apple-ios10.0.0-simulator" IPHONEOS_DEPLOYMENT_TARGET=10.0 cargo build --target ${{ matrix.target }} --verbose
124124

125125
# Android Build
126126
android-build:

collab-document/src/importer/md_importer.rs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ pub struct MDImporter {
2020
/// - math text, math flow, autolink features.
2121
/// - default Markdown features.
2222
pub parse_options: ParseOptions,
23+
24+
/// If true, paragraphs containing only inline linked text will be converted to link_preview blocks.
25+
///
26+
/// [link](https://example.com) -> link_preview block
27+
pub parse_link_as_link_preview: bool,
2328
}
2429

2530
impl MDImporter {
26-
pub fn new(parse_options: Option<ParseOptions>) -> Self {
31+
pub fn new(parse_options: Option<ParseOptions>, parse_link_as_link_preview: bool) -> Self {
2732
let parse_options = parse_options.unwrap_or_else(|| ParseOptions {
2833
gfm_strikethrough_single_tilde: true,
2934
constructs: Constructs {
@@ -35,7 +40,10 @@ impl MDImporter {
3540
..ParseOptions::gfm()
3641
});
3742

38-
Self { parse_options }
43+
Self {
44+
parse_options,
45+
parse_link_as_link_preview,
46+
}
3947
}
4048

4149
pub fn import(&self, document_id: &str, md: String) -> Result<DocumentData, DocumentError> {
@@ -58,12 +66,22 @@ impl MDImporter {
5866
Some(document_id.to_string()),
5967
None,
6068
None,
69+
self.parse_link_as_link_preview,
6170
);
6271

6372
Ok(document_data)
6473
}
6574
}
6675

76+
fn is_paragraph_with_only_link(para: &mdast::Paragraph) -> Option<String> {
77+
if para.children.len() == 1 {
78+
if let mdast::Node::Link(link) = &para.children[0] {
79+
return Some(link.url.clone());
80+
}
81+
}
82+
None
83+
}
84+
6785
/// This function will recursively process the mdast node and convert it to document blocks
6886
/// The document blocks will be stored in the document data
6987
fn process_mdast_node(
@@ -73,6 +91,7 @@ fn process_mdast_node(
7391
block_id: Option<String>,
7492
list_type: Option<&str>,
7593
start_number: Option<u32>,
94+
parse_link_as_link_preview: bool,
7695
) {
7796
// If the node is an inline node, process it as an inline node
7897
if is_inline_node(node) {
@@ -90,6 +109,7 @@ fn process_mdast_node(
90109
children,
91110
Some(&list_type),
92111
start_number,
112+
parse_link_as_link_preview,
93113
);
94114
return;
95115
}
@@ -119,7 +139,7 @@ fn process_mdast_node(
119139

120140
document_data.blocks.insert(id.clone(), block);
121141

122-
update_children_map(document_data, parent_id, &id);
142+
update_children_map(document_data, parent_id.clone(), &id);
123143

124144
match node {
125145
mdast::Node::Root(root) => {
@@ -129,16 +149,28 @@ fn process_mdast_node(
129149
&root.children,
130150
None,
131151
start_number,
152+
parse_link_as_link_preview,
132153
);
133154
},
134155
mdast::Node::Paragraph(para) => {
156+
if let Some(parent_id) = parent_id {
157+
if parse_link_as_link_preview {
158+
if let Some(url) = is_paragraph_with_only_link(para) {
159+
let link_preview_block = create_link_preview_block(&id, url, &parent_id);
160+
document_data.blocks.insert(id.clone(), link_preview_block);
161+
return;
162+
}
163+
}
164+
}
165+
135166
// Process paragraph as before
136167
process_mdast_node_children(
137168
document_data,
138169
Some(id.clone()),
139170
&para.children,
140171
None,
141172
start_number,
173+
parse_link_as_link_preview,
142174
);
143175
},
144176
mdast::Node::Heading(heading) => {
@@ -148,6 +180,7 @@ fn process_mdast_node(
148180
&heading.children,
149181
None,
150182
start_number,
183+
parse_link_as_link_preview,
151184
);
152185
},
153186
// handle the blockquote and list item node
@@ -166,6 +199,7 @@ fn process_mdast_node(
166199
&para.children,
167200
None,
168201
start_number,
202+
parse_link_as_link_preview,
169203
);
170204
}
171205

@@ -176,6 +210,7 @@ fn process_mdast_node(
176210
rest,
177211
list_type,
178212
start_number,
213+
parse_link_as_link_preview,
179214
);
180215
}
181216
}
@@ -189,7 +224,14 @@ fn process_mdast_node(
189224
// Process each row and create SimpleTableRow blocks
190225
for (row_index, row) in table.children.iter().enumerate() {
191226
if let mdast::Node::TableRow(row_node) = row {
192-
process_table_row(document_data, row_node, row_index, &id, &table.align);
227+
process_table_row(
228+
document_data,
229+
row_node,
230+
row_index,
231+
&id,
232+
&table.align,
233+
parse_link_as_link_preview,
234+
);
193235
}
194236
}
195237
},
@@ -257,6 +299,7 @@ fn process_table_row(
257299
row_index: usize,
258300
table_id: &str,
259301
align: &[AlignKind],
302+
parse_link_as_link_preview: bool,
260303
) {
261304
let row_id = generate_id();
262305
let row_block = create_simple_table_row_block(&row_id, table_id);
@@ -279,6 +322,7 @@ fn process_table_row(
279322
&cell_node.children,
280323
None,
281324
None,
325+
parse_link_as_link_preview,
282326
);
283327
}
284328
}
@@ -326,6 +370,20 @@ pub fn create_image_block(block_id: &str, url: String, parent_id: &str) -> Block
326370
}
327371
}
328372

373+
fn create_link_preview_block(block_id: &str, url: String, parent_id: &str) -> Block {
374+
let mut data = BlockData::new();
375+
data.insert(URL_FIELD.to_string(), url.into());
376+
Block {
377+
id: block_id.to_string(),
378+
ty: BlockType::LinkPreview.to_string(),
379+
data,
380+
parent: parent_id.to_string(),
381+
children: "".to_string(),
382+
external_id: None,
383+
external_type: None,
384+
}
385+
}
386+
329387
fn create_simple_table_row_block(id: &str, parent_id: &str) -> Block {
330388
Block {
331389
id: id.to_string(),
@@ -379,6 +437,7 @@ fn process_mdast_node_children(
379437
children: &[mdast::Node],
380438
list_type: Option<&str>,
381439
start_number: Option<u32>,
440+
parse_link_as_link_preview: bool,
382441
) {
383442
for child in children {
384443
process_mdast_node(
@@ -388,6 +447,7 @@ fn process_mdast_node_children(
388447
None,
389448
list_type,
390449
start_number,
450+
parse_link_as_link_preview,
391451
);
392452
}
393453
}

collab-document/tests/importer/md_importer_test.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::importer::util::{
55
use assert_json_diff::assert_json_eq;
66
use collab::core::collab::default_client_id;
77
use collab_document::document::Document;
8+
use collab_document::importer::md_importer::MDImporter;
89
use serde_json::json;
910

1011
#[test]
@@ -532,3 +533,40 @@ fn test_aside() {
532533
]);
533534
assert_eq!(delta_json, expected_delta);
534535
}
536+
537+
#[test]
538+
fn test_link_preview_with_false() {
539+
let markdown = "[AppFlowy.IO](https://appflowy.io)";
540+
541+
let importer = MDImporter::new(None, false);
542+
let result = importer
543+
.import("test_document", markdown.to_string())
544+
.unwrap();
545+
546+
let page = get_page_block(&result);
547+
let children = get_children_blocks(&result, &page.id);
548+
549+
assert_eq!(children.len(), 1);
550+
let first_child = &children[0];
551+
assert_eq!(first_child.ty, "paragraph");
552+
}
553+
554+
#[test]
555+
fn test_link_preview_with_true() {
556+
let markdown = "[AppFlowy.IO](https://appflowy.io)";
557+
558+
let importer = MDImporter::new(None, true);
559+
let result = importer
560+
.import("test_document", markdown.to_string())
561+
.unwrap();
562+
563+
let page = get_page_block(&result);
564+
let children = get_children_blocks(&result, &page.id);
565+
566+
assert_eq!(children.len(), 1);
567+
let first_child = &children[0];
568+
assert_eq!(first_child.ty, "link_preview");
569+
570+
let url = first_child.data.get("url").unwrap();
571+
assert_eq!(url.as_str().unwrap(), "https://appflowy.io");
572+
}

collab-document/tests/importer/util.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use collab_document::importer::md_importer::MDImporter;
33
use serde_json::Value;
44

55
pub(crate) fn markdown_to_document_data<T: ToString>(md: T) -> DocumentData {
6-
let importer = MDImporter::new(None);
6+
let importer = MDImporter::new(None, false);
77
let result = importer.import("test_document", md.to_string());
88
result.unwrap()
99
}

collab-importer/src/notion/page.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ impl NotionPage {
139139
match &self.notion_file {
140140
NotionFile::Markdown { file_path, .. } => {
141141
let resource_paths = self.notion_file.upload_files();
142-
let md_importer = MDImporter::new(None);
142+
let md_importer = MDImporter::new(None, false);
143143
let content = fs::read_to_string(file_path).await?;
144144
let document_data = md_importer.import(&self.view_id, content)?;
145145
let mut document = Document::create(&self.view_id, document_data, default_client_id())?;

collab-plugins/src/local_storage/kv/doc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::local_storage::kv::snapshot::SnapshotAction;
33
use crate::local_storage::kv::*;
44
use smallvec::{SmallVec, smallvec};
55
use std::collections::HashSet;
6-
use tracing::{error, info};
6+
use tracing::error;
77
use uuid::Uuid;
88
use yrs::updates::decoder::Decode;
99
use yrs::updates::encoder::Encode;

0 commit comments

Comments
 (0)