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