elmethis_notion/
client.rs

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