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 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 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}