1#[derive(Debug)]
2pub struct Client {
3 pub notionrs_client: notionrs::Client,
4 pub images: Vec<Image>,
5}
6
7#[derive(Debug)]
8pub struct Image {
9 pub src: String,
10 pub id: String,
11}
12
13impl Client {
14 pub fn new<T>(secret: T) -> Self
15 where
16 T: AsRef<str>,
17 {
18 Client {
19 notionrs_client: notionrs::Client::new().secret(secret),
20 images: Vec::default(),
21 }
22 }
23
24 #[async_recursion::async_recursion]
25 pub async fn convert_block(
26 &mut self,
27 page_id: &str,
28 ) -> Result<Vec<crate::block::Block>, crate::error::Error> {
29 let mut blocks: Vec<crate::block::Block> = Vec::new();
30
31 let results = self
32 .notionrs_client
33 .get_block_children_all()
34 .block_id(page_id)
35 .send()
36 .await?;
37
38 for block in results {
39 match block.block {
40 notionrs::block::Block::Audio { audio: _ } => {}
41 notionrs::block::Block::Bookmark { bookmark } => {
42 let mut props = crate::block::ElmBookmarkProps {
43 url: bookmark.url.clone(),
44 margin: "2rem".to_string(),
45 ..Default::default()
46 };
47
48 let response = reqwest::Client::new()
49 .get(&bookmark.url)
50 .header("user-agent", "Rust - reqwest")
51 .send()
52 .await?
53 .text()
54 .await?;
55
56 let document = scraper::Html::parse_document(&response);
57
58 let title = document
61 .select(&scraper::Selector::parse("title")?)
62 .next()
63 .map(|element| element.text().collect::<String>());
64
65 let og_title_selector = scraper::Selector::parse("meta[property='og:title']")?;
66
67 if let Some(element) = document.select(&og_title_selector).next() {
68 if let Some(content) = element.value().attr("content") {
69 props.title = Some(content.to_string());
70 }
71 }
72
73 if let Some(title) = title {
74 props.title = Some(title);
75 }
76
77 let description = document
80 .select(&scraper::Selector::parse("meta[name='description']")?)
81 .next()
82 .map(|element| element.value().attr("content").unwrap().to_string());
83
84 if let Some(description) = description {
85 props.description = Some(description);
86 }
87
88 let og_description_selector =
89 scraper::Selector::parse("meta[property='og:description']")?;
90
91 if let Some(element) = document.select(&og_description_selector).next() {
92 if let Some(content) = element.value().attr("content") {
93 props.description = Some(content.to_string());
94 }
95 }
96
97 let og_image_selector = scraper::Selector::parse("meta[property='og:image']")?;
98
99 if let Some(element) = document.select(&og_image_selector).next() {
100 if let Some(content) = element.value().attr("content") {
101 props.image = Some(content.to_string());
102 }
103 }
104
105 let block =
106 crate::block::Block::ElmBookmark(crate::block::ElmBookmark { props });
107
108 blocks.push(block);
109 }
110 notionrs::block::Block::Breadcrumb { breadcrumb: _ } => {}
111 notionrs::block::Block::BulletedListItem { bulleted_list_item } => {
112 let mut list_item_children: Vec<crate::block::Block> = Vec::new();
113
114 let rich_text_block = Client::convert_rich_text(bulleted_list_item.rich_text);
115 list_item_children.extend(rich_text_block);
116
117 if block.has_children {
118 let list_item_children_blocks = self.convert_block(&block.id).await?;
119 list_item_children.extend(list_item_children_blocks);
120 }
121
122 let list_item_block =
123 crate::block::Block::ElmListItem(crate::block::ElmListItem {
124 children: list_item_children,
125 });
126
127 let last_item = blocks.last_mut();
128
129 match last_item {
130 Some(crate::block::Block::ElmBulletedList(elm_bulleted_list)) => {
131 elm_bulleted_list.children.push(list_item_block);
132 }
133 Some(_) | None => {
134 let new_ul = vec![list_item_block];
135 blocks.push(crate::block::Block::ElmBulletedList(
136 crate::block::ElmBulletedList { children: new_ul },
137 ));
138 }
139 };
140 }
141 notionrs::block::Block::Callout { callout } => {
142 let mut children: Vec<crate::block::Block> = Vec::new();
143 let text_blocks = Client::convert_rich_text(callout.rich_text);
144 let children_blocks = self.convert_block(&block.id).await?;
145 children.extend(text_blocks);
146 children.extend(children_blocks);
147
148 let r#type = match callout.color {
149 notionrs::Color::Default
150 | notionrs::Color::DefaultBackground
151 | notionrs::Color::Blue
152 | notionrs::Color::BlueBackground
153 | notionrs::Color::Gray
154 | notionrs::Color::GrayBackground => "note",
155 notionrs::Color::Green | notionrs::Color::GreenBackground => "tip",
156 notionrs::Color::Purple | notionrs::Color::PurpleBackground => "important",
157 notionrs::Color::Yellow
158 | notionrs::Color::YellowBackground
159 | notionrs::Color::Orange
160 | notionrs::Color::OrangeBackground
161 | notionrs::Color::Brown
162 | notionrs::Color::BrownBackground => "warning",
163 notionrs::Color::Red
164 | notionrs::Color::RedBackground
165 | notionrs::Color::Pink
166 | notionrs::Color::PinkBackground => "caution",
167 }
168 .to_string();
169
170 let props = crate::block::ElmCalloutProps { r#type };
171
172 let callout_block = crate::block::Block::ElmCallout(crate::block::ElmCallout {
173 props,
174 children,
175 });
176 blocks.push(callout_block);
177 }
178 notionrs::block::Block::ChildDatabase { child_database: _ } => {}
179 notionrs::block::Block::ChildPage { child_page: _ } => {}
180 notionrs::block::Block::Code { code } => {
181 let language = code.language.to_string();
182
183 let caption = code
184 .caption
185 .iter()
186 .map(|t| t.to_string())
187 .collect::<String>();
188
189 let props = crate::block::ElmCodeBlockProps {
190 code: code
191 .rich_text
192 .iter()
193 .map(|t| t.to_string())
194 .collect::<String>(),
195 language: language.to_string(),
196 caption: if caption.is_empty() {
197 language.to_string()
198 } else {
199 caption
200 },
201 margin: "2rem".to_string(),
202 };
203
204 blocks.push(crate::block::Block::ElmCodeBlock(
205 crate::block::ElmCodeBlock { props },
206 ));
207 }
208 notionrs::block::Block::ColumnList { column_list: _ } => {
209 let children = self.convert_block(&block.id).await?;
210 let block = crate::block::Block::ElmColumnList(crate::block::ElmColumnList {
211 children,
212 });
213 blocks.push(block);
214 }
215 notionrs::block::Block::Column { column: _ } => {
216 let children = self.convert_block(&block.id).await?;
217 let block =
218 crate::block::Block::ElmColumn(crate::block::ElmColumn { children });
219 blocks.push(block);
220 }
221 notionrs::block::Block::Divider { divider: _ } => {
222 blocks.push(crate::block::Block::ElmDivider(crate::block::ElmDivider {
223 props: crate::block::ElmDividerProps {
224 margin: "2rem".to_string(),
225 },
226 }));
227 }
228 notionrs::block::Block::Embed { embed: _ } => {}
229 notionrs::block::Block::Equation { equation } => {
230 let props = crate::block::ElmKatexProps {
231 expression: equation.expression,
232 block: true,
233 };
234
235 let block = crate::block::Block::ElmKatex(crate::block::ElmKatex { props });
236
237 blocks.push(block);
238 }
239 notionrs::block::Block::File { file } => {
240 let (name, src) = match file {
241 notionrs::File::External(f) => (f.name, f.external.url),
242 notionrs::File::Uploaded(f) => (f.name, f.file.url),
243 };
244
245 let props = crate::block::ElmFileProps {
246 name,
247 src,
248 margin: "2rem".to_string(),
249 };
250
251 let block = crate::block::Block::ElmFile(crate::block::ElmFile { props });
252
253 blocks.push(block);
254 }
255 notionrs::block::Block::Heading1 { heading_1 } => {
256 let heading = heading_1;
257 if heading.is_toggleable {
258 let summary = heading
259 .rich_text
260 .iter()
261 .map(|t| t.to_string())
262 .collect::<String>();
263
264 let props = crate::block::ElmToggleProps {
265 summary,
266 margin: "2rem".to_string(),
267 };
268
269 let children = self.convert_block(&block.id).await?;
270
271 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
272 props,
273 children,
274 });
275
276 blocks.push(block);
277 } else {
278 let props = crate::block::ElmHeadingProps {
279 text: heading
280 .rich_text
281 .iter()
282 .map(|t| t.to_string())
283 .collect::<String>(),
284 };
285
286 let block =
287 crate::block::Block::ElmHeading1(crate::block::ElmHeading1 { props });
288
289 blocks.push(block);
290 }
291 }
292 notionrs::block::Block::Heading2 { heading_2 } => {
293 let heading = heading_2;
294 if heading.is_toggleable {
295 let summary = heading
296 .rich_text
297 .iter()
298 .map(|t| t.to_string())
299 .collect::<String>();
300
301 let props = crate::block::ElmToggleProps {
302 summary,
303 margin: "2rem".to_string(),
304 };
305
306 let children = self.convert_block(&block.id).await?;
307
308 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
309 props,
310 children,
311 });
312
313 blocks.push(block);
314 } else {
315 let props = crate::block::ElmHeadingProps {
316 text: heading
317 .rich_text
318 .iter()
319 .map(|t| t.to_string())
320 .collect::<String>(),
321 };
322
323 let block =
324 crate::block::Block::ElmHeading1(crate::block::ElmHeading1 { props });
325
326 blocks.push(block);
327 }
328 }
329 notionrs::block::Block::Heading3 { heading_3 } => {
330 let heading = heading_3;
331 if heading.is_toggleable {
332 let summary = heading
333 .rich_text
334 .iter()
335 .map(|t| t.to_string())
336 .collect::<String>();
337
338 let props = crate::block::ElmToggleProps {
339 summary,
340 margin: "2rem".to_string(),
341 };
342
343 let children = self.convert_block(&block.id).await?;
344
345 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
346 props,
347 children,
348 });
349
350 blocks.push(block);
351 } else {
352 let props = crate::block::ElmHeadingProps {
353 text: heading
354 .rich_text
355 .iter()
356 .map(|t| t.to_string())
357 .collect::<String>(),
358 };
359
360 let block =
361 crate::block::Block::ElmHeading1(crate::block::ElmHeading1 { props });
362
363 blocks.push(block);
364 }
365 }
366 notionrs::block::Block::Image { image } => {
367 let (src, alt) = match image {
368 notionrs::File::External(f) => (
369 f.external.url,
370 f.caption.map(|rich_text| {
371 rich_text.iter().map(|t| t.to_string()).collect::<String>()
372 }),
373 ),
374 notionrs::File::Uploaded(f) => (
375 f.file.url,
376 f.caption.map(|rich_text| {
377 rich_text.iter().map(|t| t.to_string()).collect::<String>()
378 }),
379 ),
380 };
381
382 let props = crate::block::ElmImageProps {
383 src: src.clone(),
384 alt,
385 enable_modal: true,
386 margin: "2rem".to_string(),
387 };
388
389 let image_block =
390 crate::block::Block::ElmImage(crate::block::ElmImage { props });
391
392 blocks.push(image_block);
393 self.images.push(Image { src, id: block.id });
394 }
395 notionrs::block::Block::LinkPreview { link_preview: _ } => {}
396 notionrs::block::Block::NumberedListItem { numbered_list_item } => {
397 let mut list_item_children: Vec<crate::block::Block> = Vec::new();
398
399 let rich_text_block = Client::convert_rich_text(numbered_list_item.rich_text);
400 list_item_children.extend(rich_text_block);
401
402 if block.has_children {
403 let list_item_children_blocks = self.convert_block(&block.id).await?;
404 list_item_children.extend(list_item_children_blocks);
405 }
406
407 let list_item_block =
408 crate::block::Block::ElmListItem(crate::block::ElmListItem {
409 children: list_item_children,
410 });
411
412 let last_item = blocks.last_mut();
413
414 match last_item {
415 Some(crate::block::Block::ElmNumberedList(elm_numbered_list)) => {
416 elm_numbered_list.children.push(list_item_block);
417 }
418 Some(_) | None => {
419 let new_ol = vec![list_item_block];
420 blocks.push(crate::block::Block::ElmNumberedList(
421 crate::block::ElmNumberedList { children: new_ol },
422 ));
423 }
424 };
425 }
426 notionrs::block::Block::Paragraph { paragraph } => {
427 let block = crate::block::Block::ElmParagraph(crate::block::ElmParagraph {
428 children: Client::convert_rich_text(paragraph.rich_text),
429 });
430
431 blocks.push(block);
432 }
433 notionrs::block::Block::Pdf { pdf: _ } => {}
434 notionrs::block::Block::Quote { quote } => {
435 let mut children = Vec::new();
436
437 let inline_text_block = Client::convert_rich_text(quote.rich_text);
438
439 let children_block = self.convert_block(&block.id).await?;
440
441 children.extend(inline_text_block);
442 children.extend(children_block);
443
444 let block = crate::block::Block::ElmBlockQuote(crate::block::ElmBlockQuote {
445 children,
446 });
447
448 blocks.push(block);
449 }
450 notionrs::block::Block::SyncedBlock { synced_block: _ } => {
451 let children = self.convert_block(&block.id).await?;
452 blocks.extend(children);
453 }
454 notionrs::block::Block::Table { table: _ } => {
455 let rows = self
456 .notionrs_client
457 .get_block_children_all()
458 .block_id(block.id)
459 .send()
460 .await?;
461
462 if let Some((header_row, body_rows)) = rows.split_first() {
463 let table_header_block =
464 if let notionrs::block::Block::TableRow { table_row } =
465 &header_row.block
466 {
467 let cells_blocks = table_row
468 .cells
469 .iter()
470 .map(|cell| {
471 crate::block::Block::ElmTableCell(
472 crate::block::ElmTableCell {
473 props: crate::block::ElmTableCellProps {
474 has_header: true,
475 text: cell
476 .iter()
477 .map(|t| t.to_string())
478 .collect::<String>(),
479 },
480 },
481 )
482 })
483 .collect::<Vec<crate::block::Block>>();
484
485 let table_row_block =
486 crate::block::Block::ElmTableRow(crate::block::ElmTableRow {
487 children: cells_blocks,
488 });
489
490 crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
491 children: vec![table_row_block],
492 })
493 } else {
494 crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
495 children: vec![],
496 })
497 };
498
499 let table_body_row_blocks =
500 body_rows.iter().filter_map(|row| match &row.block {
501 notionrs::block::Block::TableRow { table_row } => {
502 let cells_blocks = table_row
503 .cells
504 .iter()
505 .map(|cell| {
506 crate::block::Block::ElmTableCell(
507 crate::block::ElmTableCell {
508 props: crate::block::ElmTableCellProps {
509 has_header: false,
510 text: cell
511 .iter()
512 .map(|t| t.to_string())
513 .collect::<String>(),
514 },
515 },
516 )
517 })
518 .collect::<Vec<crate::block::Block>>();
519
520 Some(crate::block::Block::ElmTableRow(
521 crate::block::ElmTableRow {
522 children: cells_blocks,
523 },
524 ))
525 }
526 _ => None,
527 });
528
529 let table_body_block =
530 crate::block::Block::ElmTableBody(crate::block::ElmTableBody {
531 children: table_body_row_blocks.collect(),
532 });
533
534 let table_block = crate::block::Block::ElmTable(crate::block::ElmTable {
535 children: vec![table_header_block, table_body_block],
536 });
537
538 blocks.push(table_block);
539 }
540 }
541 notionrs::block::Block::TableRow { table_row: _ } => {}
542 notionrs::block::Block::Template { template: _ } => {}
543 notionrs::block::Block::ToDo { to_do } => {
544 let props = crate::block::ElmCheckboxProps {
545 label: to_do
546 .rich_text
547 .iter()
548 .map(|t| t.to_string())
549 .collect::<String>(),
550 };
551
552 let block =
553 crate::block::Block::ElmCheckbox(crate::block::ElmCheckbox { props });
554
555 blocks.push(block);
556 }
557 notionrs::block::Block::Toggle { toggle } => {
558 let summary = toggle
559 .rich_text
560 .iter()
561 .map(|t| t.to_string())
562 .collect::<String>();
563
564 let props = crate::block::ElmToggleProps {
565 summary,
566 margin: "2rem".to_string(),
567 };
568
569 let children = self.convert_block(&block.id).await?;
570
571 let block =
572 crate::block::Block::ElmToggle(crate::block::ElmToggle { props, children });
573
574 blocks.push(block);
575 }
576 notionrs::block::Block::Video { video: _ } => {}
577 notionrs::block::Block::Unknown(_) => {}
578 };
579 }
580
581 Ok(blocks)
582 }
583
584 pub fn convert_rich_text(rich_text: Vec<notionrs::RichText>) -> Vec<crate::block::Block> {
585 let mut blocks: Vec<crate::block::Block> = Vec::new();
586
587 for r in rich_text {
588 let annotations = match r {
589 notionrs::RichText::Text { annotations, .. } => annotations,
590 notionrs::RichText::Mention { annotations, .. } => annotations,
591 notionrs::RichText::Equation { annotations, .. } => annotations,
592 };
593
594 let plain_text = match r {
595 notionrs::RichText::Text { plain_text, .. } => plain_text,
596 notionrs::RichText::Mention { plain_text, .. } => plain_text,
597 notionrs::RichText::Equation { plain_text, .. } => plain_text,
598 };
599
600 let props = crate::block::ElmInlineTextProps {
601 text: plain_text,
602 bold: annotations.bold,
603 italic: annotations.italic,
604 underline: annotations.underline,
605 strikethrough: annotations.strikethrough,
606 code: annotations.code,
607 color: match annotations.color {
608 notionrs::Color::Default => None,
609 notionrs::Color::Blue => Some(String::from("#6987b8")),
610 notionrs::Color::Brown => Some(String::from("#8b4c3f")),
611 notionrs::Color::Gray => Some(String::from("#868e9c")),
612 notionrs::Color::Green => Some(String::from("#59b57c")),
613 notionrs::Color::Orange => Some(String::from("#bf7e71")),
614 notionrs::Color::Pink => Some(String::from("#c9699e")),
615 notionrs::Color::Purple => Some(String::from("#9771bd")),
616 notionrs::Color::Red => Some(String::from("#b36472")),
617 notionrs::Color::Yellow => Some(String::from("#b8a36e")),
618 _ => None,
619 },
620 };
621
622 blocks.push(crate::block::Block::ElmInlineText(
623 crate::block::ElmInlineText { props },
624 ));
625 }
626
627 blocks
628 }
629}