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 = 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 id: block.id.clone(),
126 });
127
128 let last_item = blocks.last_mut();
129
130 match last_item {
131 Some(crate::block::Block::ElmBulletedList(elm_bulleted_list)) => {
132 elm_bulleted_list.children.push(list_item_block);
133 }
134 Some(_) | None => {
135 let new_ul = vec![list_item_block];
136 blocks.push(crate::block::Block::ElmBulletedList(
137 crate::block::ElmBulletedList {
138 children: new_ul,
139 id: block.id,
140 },
141 ));
142 }
143 };
144 }
145 notionrs::object::block::Block::Callout { callout } => {
146 let mut children: Vec<crate::block::Block> = Vec::new();
147 let text_blocks = Client::convert_rich_text(callout.rich_text);
148 let children_blocks = self.convert_block(&block.id).await?;
149 children.extend(text_blocks);
150 children.extend(children_blocks);
151
152 let r#type = match callout.color {
153 notionrs::object::color::Color::Default
154 | notionrs::object::color::Color::DefaultBackground
155 | notionrs::object::color::Color::Blue
156 | notionrs::object::color::Color::BlueBackground
157 | notionrs::object::color::Color::Gray
158 | notionrs::object::color::Color::GrayBackground => "note",
159 notionrs::object::color::Color::Green
160 | notionrs::object::color::Color::GreenBackground => "tip",
161 notionrs::object::color::Color::Purple
162 | notionrs::object::color::Color::PurpleBackground => "important",
163 notionrs::object::color::Color::Yellow
164 | notionrs::object::color::Color::YellowBackground
165 | notionrs::object::color::Color::Orange
166 | notionrs::object::color::Color::OrangeBackground
167 | notionrs::object::color::Color::Brown
168 | notionrs::object::color::Color::BrownBackground => "warning",
169 notionrs::object::color::Color::Red
170 | notionrs::object::color::Color::RedBackground
171 | notionrs::object::color::Color::Pink
172 | notionrs::object::color::Color::PinkBackground => "caution",
173 }
174 .to_string();
175
176 let props = crate::block::ElmCalloutProps { r#type };
177
178 let callout_block = crate::block::Block::ElmCallout(crate::block::ElmCallout {
179 props,
180 children,
181 id: block.id,
182 });
183 blocks.push(callout_block);
184 }
185 notionrs::object::block::Block::ChildDatabase { child_database: _ } => {}
186 notionrs::object::block::Block::ChildPage { child_page: _ } => {}
187 notionrs::object::block::Block::Code { code } => {
188 let language = code.language.to_string();
189
190 let caption = code
191 .caption
192 .iter()
193 .map(|t| t.to_string())
194 .collect::<String>();
195
196 let props = crate::block::ElmCodeBlockProps {
197 code: code
198 .rich_text
199 .iter()
200 .map(|t| t.to_string())
201 .collect::<String>(),
202 language: language.to_string(),
203 caption: if caption.is_empty() {
204 language.to_string()
205 } else {
206 caption
207 },
208 margin: "2rem".to_string(),
209 };
210
211 blocks.push(crate::block::Block::ElmCodeBlock(
212 crate::block::ElmCodeBlock {
213 props,
214 id: block.id,
215 },
216 ));
217 }
218 notionrs::object::block::Block::ColumnList { column_list: _ } => {
219 let children = self.convert_block(&block.id).await?;
220 let block = crate::block::Block::ElmColumnList(crate::block::ElmColumnList {
221 children,
222 id: block.id,
223 });
224 blocks.push(block);
225 }
226 notionrs::object::block::Block::Column { column: _ } => {
227 let children = self.convert_block(&block.id).await?;
228 let block = crate::block::Block::ElmColumn(crate::block::ElmColumn {
229 children,
230 id: block.id,
231 });
232 blocks.push(block);
233 }
234 notionrs::object::block::Block::Divider { divider: _ } => {
235 blocks.push(crate::block::Block::ElmDivider(crate::block::ElmDivider {
236 props: crate::block::ElmDividerProps {
237 margin: "2rem".to_string(),
238 },
239 id: block.id,
240 }));
241 }
242 notionrs::object::block::Block::Embed { embed: _ } => {}
243 notionrs::object::block::Block::Equation { equation } => {
244 let props = crate::block::ElmKatexProps {
245 expression: equation.expression,
246 block: true,
247 };
248
249 let block = crate::block::Block::ElmKatex(crate::block::ElmKatex {
250 props,
251 id: block.id,
252 });
253
254 blocks.push(block);
255 }
256 notionrs::object::block::Block::File { file } => {
257 let (name, src) = match file {
258 notionrs::object::file::File::External(f) => (f.name, f.external.url),
259 notionrs::object::file::File::Uploaded(f) => (f.name, f.file.url),
260 };
261
262 let props = crate::block::ElmFileProps {
263 name,
264 src,
265 margin: "2rem".to_string(),
266 };
267
268 let block = crate::block::Block::ElmFile(crate::block::ElmFile {
269 props,
270 id: block.id,
271 });
272
273 blocks.push(block);
274 }
275 notionrs::object::block::Block::Heading1 { heading_1 } => {
276 let heading = heading_1;
277 if heading.is_toggleable {
278 let summary = heading
279 .rich_text
280 .iter()
281 .map(|t| t.to_string())
282 .collect::<String>();
283
284 let props = crate::block::ElmToggleProps {
285 summary,
286 margin: "2rem".to_string(),
287 };
288
289 let children = self.convert_block(&block.id).await?;
290
291 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
292 props,
293 children,
294 id: block.id,
295 });
296
297 blocks.push(block);
298 } else {
299 let props = crate::block::ElmHeadingProps {
300 text: heading
301 .rich_text
302 .iter()
303 .map(|t| t.to_string())
304 .collect::<String>(),
305 };
306
307 let block = crate::block::Block::ElmHeading1(crate::block::ElmHeading1 {
308 props,
309 id: block.id,
310 });
311
312 blocks.push(block);
313 }
314 }
315 notionrs::object::block::Block::Heading2 { heading_2 } => {
316 let heading = heading_2;
317 if heading.is_toggleable {
318 let summary = heading
319 .rich_text
320 .iter()
321 .map(|t| t.to_string())
322 .collect::<String>();
323
324 let props = crate::block::ElmToggleProps {
325 summary,
326 margin: "2rem".to_string(),
327 };
328
329 let children = self.convert_block(&block.id).await?;
330
331 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
332 props,
333 children,
334 id: block.id,
335 });
336
337 blocks.push(block);
338 } else {
339 let props = crate::block::ElmHeadingProps {
340 text: heading
341 .rich_text
342 .iter()
343 .map(|t| t.to_string())
344 .collect::<String>(),
345 };
346
347 let block = crate::block::Block::ElmHeading1(crate::block::ElmHeading1 {
348 props,
349 id: block.id,
350 });
351
352 blocks.push(block);
353 }
354 }
355 notionrs::object::block::Block::Heading3 { heading_3 } => {
356 let heading = heading_3;
357 if heading.is_toggleable {
358 let summary = heading
359 .rich_text
360 .iter()
361 .map(|t| t.to_string())
362 .collect::<String>();
363
364 let props = crate::block::ElmToggleProps {
365 summary,
366 margin: "2rem".to_string(),
367 };
368
369 let children = self.convert_block(&block.id).await?;
370
371 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
372 props,
373 children,
374 id: block.id,
375 });
376
377 blocks.push(block);
378 } else {
379 let props = crate::block::ElmHeadingProps {
380 text: heading
381 .rich_text
382 .iter()
383 .map(|t| t.to_string())
384 .collect::<String>(),
385 };
386
387 let block = crate::block::Block::ElmHeading1(crate::block::ElmHeading1 {
388 props,
389 id: block.id,
390 });
391
392 blocks.push(block);
393 }
394 }
395 notionrs::object::block::Block::Image { image } => {
396 let (src, alt) = match image {
397 notionrs::object::file::File::External(f) => (
398 f.external.url,
399 f.caption.map(|rich_text| {
400 rich_text.iter().map(|t| t.to_string()).collect::<String>()
401 }),
402 ),
403 notionrs::object::file::File::Uploaded(f) => (
404 f.file.url,
405 f.caption.map(|rich_text| {
406 rich_text.iter().map(|t| t.to_string()).collect::<String>()
407 }),
408 ),
409 };
410
411 let props = crate::block::ElmImageProps {
412 src: src.clone(),
413 alt,
414 enable_modal: true,
415 margin: "2rem".to_string(),
416 };
417
418 let image_block = crate::block::Block::ElmImage(crate::block::ElmImage {
419 props,
420 id: block.id.clone(),
421 });
422
423 blocks.push(image_block);
424 }
425 notionrs::object::block::Block::LinkPreview { link_preview: _ } => {}
426 notionrs::object::block::Block::NumberedListItem { numbered_list_item } => {
427 let mut list_item_children: Vec<crate::block::Block> = Vec::new();
428
429 let rich_text_block = Client::convert_rich_text(numbered_list_item.rich_text);
430 list_item_children.extend(rich_text_block);
431
432 if block.has_children {
433 let list_item_children_blocks = self.convert_block(&block.id).await?;
434 list_item_children.extend(list_item_children_blocks);
435 }
436
437 let list_item_block =
438 crate::block::Block::ElmListItem(crate::block::ElmListItem {
439 children: list_item_children,
440 id: block.id.clone(),
441 });
442
443 let last_item = blocks.last_mut();
444
445 match last_item {
446 Some(crate::block::Block::ElmNumberedList(elm_numbered_list)) => {
447 elm_numbered_list.children.push(list_item_block);
448 }
449 Some(_) | None => {
450 let new_ol = vec![list_item_block];
451 blocks.push(crate::block::Block::ElmNumberedList(
452 crate::block::ElmNumberedList {
453 children: new_ol,
454 id: block.id,
455 },
456 ));
457 }
458 };
459 }
460 notionrs::object::block::Block::Paragraph { paragraph } => {
461 let block = crate::block::Block::ElmParagraph(crate::block::ElmParagraph {
462 children: Client::convert_rich_text(paragraph.rich_text),
463 id: block.id,
464 });
465
466 blocks.push(block);
467 }
468 notionrs::object::block::Block::Pdf { pdf: _ } => {}
469 notionrs::object::block::Block::Quote { quote } => {
470 let mut children = Vec::new();
471
472 let inline_text_block = Client::convert_rich_text(quote.rich_text);
473
474 let children_block = self.convert_block(&block.id).await?;
475
476 children.extend(inline_text_block);
477 children.extend(children_block);
478
479 let block = crate::block::Block::ElmBlockQuote(crate::block::ElmBlockQuote {
480 children,
481 id: block.id,
482 });
483
484 blocks.push(block);
485 }
486 notionrs::object::block::Block::SyncedBlock { synced_block: _ } => {
487 let children = self.convert_block(&block.id).await?;
488 blocks.extend(children);
489 }
490 notionrs::object::block::Block::Table { table: _ } => {
491 let rows = self
492 .notionrs_client
493 .get_block_children_all()
494 .block_id(block.id)
495 .send()
496 .await?;
497
498 if let Some((header_row, body_rows)) = rows.split_first() {
499 let table_header_block =
500 if let notionrs::object::block::Block::TableRow { table_row } =
501 &header_row.block
502 {
503 let cells_blocks = table_row
504 .cells
505 .iter()
506 .map(|cell| {
507 crate::block::Block::ElmTableCell(
508 crate::block::ElmTableCell {
509 props: crate::block::ElmTableCellProps {
510 has_header: true,
511 text: cell
512 .iter()
513 .map(|t| t.to_string())
514 .collect::<String>(),
515 },
516 },
517 )
518 })
519 .collect::<Vec<crate::block::Block>>();
520
521 let table_row_block =
522 crate::block::Block::ElmTableRow(crate::block::ElmTableRow {
523 children: cells_blocks,
524 });
525
526 crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
527 children: vec![table_row_block],
528 })
529 } else {
530 crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
531 children: vec![],
532 })
533 };
534
535 let table_body_row_blocks =
536 body_rows.iter().filter_map(|row| match &row.block {
537 notionrs::object::block::Block::TableRow { table_row } => {
538 let cells_blocks = table_row
539 .cells
540 .iter()
541 .map(|cell| {
542 crate::block::Block::ElmTableCell(
543 crate::block::ElmTableCell {
544 props: crate::block::ElmTableCellProps {
545 has_header: false,
546 text: cell
547 .iter()
548 .map(|t| t.to_string())
549 .collect::<String>(),
550 },
551 },
552 )
553 })
554 .collect::<Vec<crate::block::Block>>();
555
556 Some(crate::block::Block::ElmTableRow(
557 crate::block::ElmTableRow {
558 children: cells_blocks,
559 },
560 ))
561 }
562 _ => None,
563 });
564
565 let table_body_block =
566 crate::block::Block::ElmTableBody(crate::block::ElmTableBody {
567 children: table_body_row_blocks.collect(),
568 });
569
570 let table_block = crate::block::Block::ElmTable(crate::block::ElmTable {
571 children: vec![table_header_block, table_body_block],
572 });
573
574 blocks.push(table_block);
575 }
576 }
577 notionrs::object::block::Block::TableRow { table_row: _ } => {}
578 notionrs::object::block::Block::Template { template: _ } => {}
579 notionrs::object::block::Block::ToDo { to_do } => {
580 let props = crate::block::ElmCheckboxProps {
581 label: to_do
582 .rich_text
583 .iter()
584 .map(|t| t.to_string())
585 .collect::<String>(),
586 };
587
588 let block = crate::block::Block::ElmCheckbox(crate::block::ElmCheckbox {
589 props,
590 id: block.id,
591 });
592
593 blocks.push(block);
594 }
595 notionrs::object::block::Block::Toggle { toggle } => {
596 let summary = toggle
597 .rich_text
598 .iter()
599 .map(|t| t.to_string())
600 .collect::<String>();
601
602 let props = crate::block::ElmToggleProps {
603 summary,
604 margin: "2rem".to_string(),
605 };
606
607 let children = self.convert_block(&block.id).await?;
608
609 let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
610 props,
611 children,
612 id: block.id,
613 });
614
615 blocks.push(block);
616 }
617 notionrs::object::block::Block::Video { video: _ } => {}
618 notionrs::object::block::Block::Unknown(_) => {}
619 };
620 }
621
622 Ok(blocks)
623 }
624
625 pub fn convert_rich_text(
626 rich_text: Vec<notionrs::object::rich_text::RichText>,
627 ) -> Vec<crate::block::Block> {
628 let blocks: Vec<crate::block::Block> = rich_text
629 .iter()
630 .map(|r| {
631 if let notionrs::object::rich_text::RichText::Mention { mention, .. } = r {
632 if let notionrs::object::rich_text::mention::Mention::CustomEmoji {
633 custom_emoji,
634 } = mention
635 {
636 let props = crate::block::ElmInlineIconProps {
637 src: custom_emoji.url.to_string(),
638 alt: custom_emoji.name.to_string(),
639 };
640
641 let block =
642 crate::block::Block::ElmInlineIcon(crate::block::ElmInlineIcon {
643 id: custom_emoji.id.to_string(),
644 props,
645 });
646
647 return block;
648 }
649 }
650
651 let annotations = match r {
652 notionrs::object::rich_text::RichText::Text { annotations, .. } => annotations,
653 notionrs::object::rich_text::RichText::Mention { annotations, .. } => {
654 annotations
655 }
656 notionrs::object::rich_text::RichText::Equation { annotations, .. } => {
657 annotations
658 }
659 };
660
661 let plain_text = match r {
662 notionrs::object::rich_text::RichText::Text { plain_text, .. } => plain_text,
663 notionrs::object::rich_text::RichText::Mention { plain_text, .. } => plain_text,
664 notionrs::object::rich_text::RichText::Equation { plain_text, .. } => {
665 plain_text
666 }
667 };
668
669 let props = crate::block::ElmInlineTextProps {
670 text: plain_text.to_string(),
671 bold: annotations.bold,
672 italic: annotations.italic,
673 underline: annotations.underline,
674 strikethrough: annotations.strikethrough,
675 code: annotations.code,
676 color: match annotations.color {
677 notionrs::object::color::Color::Default => None,
678 notionrs::object::color::Color::Blue => Some(String::from("#6987b8")),
679 notionrs::object::color::Color::Brown => Some(String::from("#8b4c3f")),
680 notionrs::object::color::Color::Gray => Some(String::from("#868e9c")),
681 notionrs::object::color::Color::Green => Some(String::from("#59b57c")),
682 notionrs::object::color::Color::Orange => Some(String::from("#bf7e71")),
683 notionrs::object::color::Color::Pink => Some(String::from("#c9699e")),
684 notionrs::object::color::Color::Purple => Some(String::from("#9771bd")),
685 notionrs::object::color::Color::Red => Some(String::from("#b36472")),
686 notionrs::object::color::Color::Yellow => Some(String::from("#b8a36e")),
687 _ => None,
688 },
689 };
690 crate::block::Block::ElmInlineText(crate::block::ElmInlineText { props })
691 })
692 .collect();
693
694 blocks
695 }
696}