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.map(|rich_text| {
398 rich_text.iter().map(|t| t.to_string()).collect::<String>()
399 }),
400 ),
401 notionrs::object::file::File::Uploaded(f) => (
402 f.file.url,
403 f.caption.map(|rich_text| {
404 rich_text.iter().map(|t| t.to_string()).collect::<String>()
405 }),
406 ),
407 };
408
409 let props = crate::block::ElmBlockImageProps {
410 src: src.clone(),
411 alt,
412 enable_modal: true,
413 margin: "2rem".to_string(),
414 };
415
416 let image_block = crate::block::Block::ElmBlockImage(crate::block::ElmBlockImage {
417 props,
418 id: block.id.clone(),
419 });
420
421 blocks.push(image_block);
422 }
423 notionrs::object::block::Block::LinkPreview { link_preview: _ } => {}
424 notionrs::object::block::Block::NumberedListItem { numbered_list_item } => {
425 let mut list_item_children: Vec<crate::block::Block> = Vec::new();
426
427 let rich_text_block =
428 Client::convert_rich_text(numbered_list_item.rich_text).await?;
429 list_item_children.extend(rich_text_block);
430
431 if block.has_children {
432 let list_item_children_blocks = self.convert_block(&block.id).await?;
433 list_item_children.extend(list_item_children_blocks);
434 }
435
436 let list_item_block =
437 crate::block::Block::ElmListItem(crate::block::ElmListItem {
438 children: list_item_children,
439 id: block.id.clone(),
440 });
441
442 let last_item = blocks.last_mut();
443
444 match last_item {
445 Some(crate::block::Block::ElmNumberedList(elm_numbered_list)) => {
446 elm_numbered_list.children.push(list_item_block);
447 }
448 Some(_) | None => {
449 let new_ol = vec![list_item_block];
450 blocks.push(crate::block::Block::ElmNumberedList(
451 crate::block::ElmNumberedList {
452 children: new_ol,
453 id: block.id,
454 },
455 ));
456 }
457 };
458 }
459 notionrs::object::block::Block::Paragraph { paragraph } => {
460 let block = crate::block::Block::ElmParagraph(crate::block::ElmParagraph {
461 children: Client::convert_rich_text(paragraph.rich_text).await?,
462 id: block.id,
463 });
464
465 blocks.push(block);
466 }
467 notionrs::object::block::Block::Pdf { pdf: _ } => {}
468 notionrs::object::block::Block::Quote { quote } => {
469 let mut children = Vec::new();
470
471 let inline_text_block = Client::convert_rich_text(quote.rich_text).await?;
472
473 let children_block = self.convert_block(&block.id).await?;
474
475 children.extend(inline_text_block);
476 children.extend(children_block);
477
478 let block = crate::block::Block::ElmBlockQuote(crate::block::ElmBlockQuote {
479 children,
480 id: block.id,
481 });
482
483 blocks.push(block);
484 }
485 notionrs::object::block::Block::SyncedBlock { synced_block: _ } => {
486 let children = self.convert_block(&block.id).await?;
487 blocks.extend(children);
488 }
489 notionrs::object::block::Block::TableOfContents {
490 table_of_contents: _,
491 } => {}
492 notionrs::object::block::Block::Table { table: _ } => {
493 let rows = self
494 .notionrs_client
495 .get_block_children_all()
496 .block_id(block.id)
497 .send()
498 .await?;
499
500 if let Some((header_row, body_rows)) = rows.split_first() {
501 let table_header_block =
502 if let notionrs::object::block::Block::TableRow { table_row } =
503 &header_row.block
504 {
505 let cells_blocks = table_row
506 .cells
507 .iter()
508 .map(|cell| {
509 crate::block::Block::ElmTableCell(
510 crate::block::ElmTableCell {
511 props: crate::block::ElmTableCellProps {
512 has_header: true,
513 text: cell
514 .iter()
515 .map(|t| t.to_string())
516 .collect::<String>(),
517 },
518 },
519 )
520 })
521 .collect::<Vec<crate::block::Block>>();
522
523 let table_row_block =
524 crate::block::Block::ElmTableRow(crate::block::ElmTableRow {
525 children: cells_blocks,
526 });
527
528 crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
529 children: vec![table_row_block],
530 })
531 } else {
532 crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
533 children: vec![],
534 })
535 };
536
537 let table_body_row_blocks =
538 body_rows.iter().filter_map(|row| match &row.block {
539 notionrs::object::block::Block::TableRow { table_row } => {
540 let cells_blocks = table_row
541 .cells
542 .iter()
543 .map(|cell| {
544 crate::block::Block::ElmTableCell(
545 crate::block::ElmTableCell {
546 props: crate::block::ElmTableCellProps {
547 has_header: false,
548 text: cell
549 .iter()
550 .map(|t| t.to_string())
551 .collect::<String>(),
552 },
553 },
554 )
555 })
556 .collect::<Vec<crate::block::Block>>();
557
558 Some(crate::block::Block::ElmTableRow(
559 crate::block::ElmTableRow {
560 children: cells_blocks,
561 },
562 ))
563 }
564 _ => None,
565 });
566
567 let table_body_block =
568 crate::block::Block::ElmTableBody(crate::block::ElmTableBody {
569 children: table_body_row_blocks.collect(),
570 });
571
572 let table_block = crate::block::Block::ElmTable(crate::block::ElmTable {
573 children: vec![table_header_block, table_body_block],
574 });
575
576 blocks.push(table_block);
577 }
578 }
579 notionrs::object::block::Block::TableRow { table_row: _ } => {}
580 notionrs::object::block::Block::Template { template: _ } => {}
581 notionrs::object::block::Block::ToDo { to_do } => {
582 let props = crate::block::ElmCheckboxProps {
583 label: to_do
584 .rich_text
585 .iter()
586 .map(|t| t.to_string())
587 .collect::<String>(),
588 };
589
590 let block = crate::block::Block::ElmCheckbox(crate::block::ElmCheckbox {
591 props,
592 id: block.id,
593 });
594
595 blocks.push(block);
596 }
597 notionrs::object::block::Block::Toggle { toggle } => {
598 let summary = toggle
599 .rich_text
600 .iter()
601 .map(|t| t.to_string())
602 .collect::<String>();
603
604 let props = crate::block::ElmToggleProps {
605 summary,
606 margin: "2rem".to_string(),
607 };
608
609 let children = self.convert_block(&block.id).await?;
610
611 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
612 props,
613 children,
614 id: block.id,
615 });
616
617 blocks.push(block);
618 }
619 notionrs::object::block::Block::Video { video: _ } => {}
620 notionrs::object::block::Block::Unknown(_) => {}
621
622 #[allow(unreachable_patterns)]
623 _ => {}
624 };
625 }
626
627 Ok(blocks)
628 }
629
630 pub async fn convert_rich_text(
631 rich_text: Vec<notionrs::object::rich_text::RichText>,
632 ) -> Result<Vec<crate::block::Block>, crate::error::Error> {
633 let mut blocks: Vec<crate::block::Block> = Vec::new();
634
635 for r in rich_text {
636 let block: Result<crate::block::Block, crate::error::Error> = match r {
637 notionrs::object::rich_text::RichText::Mention { mention, .. } => match mention {
638 notionrs::object::rich_text::mention::Mention::LinkMention { link_mention } => {
639 let href = link_mention.href.as_str();
640
641 let mut props = crate::block::ElmInlineLinkProps {
642 href: link_mention.href.to_string(),
643 text: link_mention.to_string(),
644 favicon: None,
645 };
646
647 let response = reqwest::Client::new()
648 .get(href)
649 .header("user-agent", "Rust - reqwest")
650 .send()
651 .await?
652 .text()
653 .await?;
654
655 let document = scraper::Html::parse_document(&response);
656
657 let title = document
658 .select(&scraper::Selector::parse("title")?)
659 .next()
660 .map(|element| element.text().collect::<String>());
661
662 let og_title_selector =
663 scraper::Selector::parse("meta[property='og:title']")?;
664
665 if let Some(element) = document.select(&og_title_selector).next() {
666 if let Some(content) = element.value().attr("content") {
667 props.text = content.to_string();
668 }
669 }
670
671 if let Some(title) = title {
672 props.text = title;
673 }
674
675 let favicon_selector = scraper::Selector::parse(r#"link[rel="icon"]"#)?;
676
677 props.favicon = document
678 .select(&favicon_selector)
679 .next()
680 .and_then(|f| f.value().attr("href").map(String::from))
681 .and_then(|favicon_href| {
682 if favicon_href.starts_with("http://")
683 || favicon_href.starts_with("https://")
684 {
685 Some(favicon_href)
686 } else {
687 url::Url::parse(&href)
688 .and_then(|s| s.join(&favicon_href))
689 .map(|s| s.to_string())
690 .ok()
691 }
692 });
693
694 Ok(crate::block::Block::ElmInlineLink(
695 crate::block::ElmInlineLink { props },
696 ))
697 }
698 notionrs::object::rich_text::mention::Mention::User { user: _ } => continue,
699 notionrs::object::rich_text::mention::Mention::Date { date: _ } => continue,
700 notionrs::object::rich_text::mention::Mention::LinkPreview {
701 link_preview: _,
702 } => {
703 continue;
704 }
705 notionrs::object::rich_text::mention::Mention::TemplateMention {
706 template_mention: _,
707 } => continue,
708 notionrs::object::rich_text::mention::Mention::Page { page: _ } => continue,
709 notionrs::object::rich_text::mention::Mention::Database { database: _ } => {
710 continue;
711 }
712 notionrs::object::rich_text::mention::Mention::CustomEmoji { custom_emoji } => {
713 let props = crate::block::ElmInlineIconProps {
714 src: custom_emoji.url.to_string(),
715 alt: custom_emoji.name.to_string(),
716 };
717
718 let block =
719 crate::block::Block::ElmInlineIcon(crate::block::ElmInlineIcon {
720 id: custom_emoji.id.to_string(),
721 props,
722 });
723
724 Ok(block)
725 }
726 },
727 notionrs::object::rich_text::RichText::Text {
728 text: _text,
729 annotations,
730 plain_text,
731 href: _href,
732 } => {
733 let props = crate::block::ElmInlineTextProps {
734 text: plain_text.to_string(),
735 bold: annotations.bold,
736 italic: annotations.italic,
737 underline: annotations.underline,
738 strikethrough: annotations.strikethrough,
739 code: annotations.code,
740 color: match annotations.color {
741 notionrs::object::color::Color::Default => None,
742 notionrs::object::color::Color::Blue => Some(String::from("#6987b8")),
743 notionrs::object::color::Color::Brown => Some(String::from("#8b4c3f")),
744 notionrs::object::color::Color::Gray => Some(String::from("#868e9c")),
745 notionrs::object::color::Color::Green => Some(String::from("#59b57c")),
746 notionrs::object::color::Color::Orange => Some(String::from("#bf7e71")),
747 notionrs::object::color::Color::Pink => Some(String::from("#c9699e")),
748 notionrs::object::color::Color::Purple => Some(String::from("#9771bd")),
749 notionrs::object::color::Color::Red => Some(String::from("#b36472")),
750 notionrs::object::color::Color::Yellow => Some(String::from("#b8a36e")),
751 _ => None,
752 },
753 };
754
755 Ok(crate::block::Block::ElmInlineText(
756 crate::block::ElmInlineText { props },
757 ))
758 }
759 notionrs::object::rich_text::RichText::Equation {
760 equation,
761 annotations: _annotations,
762 plain_text: _plain_text,
763 href: _href,
764 } => {
765 let props = crate::block::ElmKatexProps {
766 block: false,
767 expression: equation.expression,
768 };
769
770 let block = crate::block::Block::ElmKatex(crate::block::ElmKatex { props });
771
772 Ok(block)
773 }
774 };
775
776 blocks.push(block.unwrap());
777 }
778
779 Ok(blocks)
780 }
781}