elmethis_notion/
client.rs

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