elmethis_notion/
client.rs

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                    // title
57
58                    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                    // description
76
77                    let description = document
78                        .select(&scraper::Selector::parse("meta[name='description']")?)
79                        .next()
80                        .map(|element| element.value().attr("content").unwrap().to_string());
81
82                    if let Some(description) = description {
83                        props.description = Some(description);
84                    }
85
86                    let og_description_selector =
87                        scraper::Selector::parse("meta[property='og:description']")?;
88
89                    if let Some(element) = document.select(&og_description_selector).next() {
90                        if let Some(content) = element.value().attr("content") {
91                            props.description = Some(content.to_string());
92                        }
93                    }
94
95                    let og_image_selector = scraper::Selector::parse("meta[property='og:image']")?;
96
97                    if let Some(element) = document.select(&og_image_selector).next() {
98                        if let Some(content) = element.value().attr("content") {
99                            props.image = Some(content.to_string());
100                        }
101                    }
102
103                    let block = crate::block::Block::ElmBookmark(crate::block::ElmBookmark {
104                        props,
105                        id: block.id,
106                    });
107
108                    blocks.push(block);
109                }
110                notionrs::object::block::Block::Breadcrumb { breadcrumb: _ } => {}
111                notionrs::object::block::Block::BulletedListItem { bulleted_list_item } => {
112                    let mut list_item_children: Vec<crate::block::Block> = Vec::new();
113
114                    let rich_text_block =
115                        Client::convert_rich_text(bulleted_list_item.rich_text).await?;
116                    list_item_children.extend(rich_text_block);
117
118                    if block.has_children {
119                        let list_item_children_blocks = self.convert_block(&block.id).await?;
120                        list_item_children.extend(list_item_children_blocks);
121                    }
122
123                    let list_item_block =
124                        crate::block::Block::ElmListItem(crate::block::ElmListItem {
125                            children: list_item_children,
126                            id: block.id.clone(),
127                        });
128
129                    let last_item = blocks.last_mut();
130
131                    match last_item {
132                        Some(crate::block::Block::ElmBulletedList(elm_bulleted_list)) => {
133                            elm_bulleted_list.children.push(list_item_block);
134                        }
135                        Some(_) | None => {
136                            let new_ul = vec![list_item_block];
137                            blocks.push(crate::block::Block::ElmBulletedList(
138                                crate::block::ElmBulletedList {
139                                    children: new_ul,
140                                    id: block.id,
141                                },
142                            ));
143                        }
144                    };
145                }
146                notionrs::object::block::Block::Callout { callout } => {
147                    let mut children: Vec<crate::block::Block> = Vec::new();
148                    let text_blocks = Client::convert_rich_text(callout.rich_text).await?;
149                    let children_blocks = self.convert_block(&block.id).await?;
150                    children.extend(text_blocks);
151                    children.extend(children_blocks);
152
153                    let r#type = match callout.color {
154                        notionrs::object::color::Color::Default
155                        | notionrs::object::color::Color::DefaultBackground
156                        | notionrs::object::color::Color::Blue
157                        | notionrs::object::color::Color::BlueBackground
158                        | notionrs::object::color::Color::Gray
159                        | notionrs::object::color::Color::GrayBackground => "note",
160                        notionrs::object::color::Color::Green
161                        | notionrs::object::color::Color::GreenBackground => "tip",
162                        notionrs::object::color::Color::Purple
163                        | notionrs::object::color::Color::PurpleBackground => "important",
164                        notionrs::object::color::Color::Yellow
165                        | notionrs::object::color::Color::YellowBackground
166                        | notionrs::object::color::Color::Orange
167                        | notionrs::object::color::Color::OrangeBackground
168                        | notionrs::object::color::Color::Brown
169                        | notionrs::object::color::Color::BrownBackground => "warning",
170                        notionrs::object::color::Color::Red
171                        | notionrs::object::color::Color::RedBackground
172                        | notionrs::object::color::Color::Pink
173                        | notionrs::object::color::Color::PinkBackground => "caution",
174                    }
175                    .to_string();
176
177                    let props = crate::block::ElmCalloutProps { r#type };
178
179                    let callout_block = crate::block::Block::ElmCallout(crate::block::ElmCallout {
180                        props,
181                        children,
182                        id: block.id,
183                    });
184                    blocks.push(callout_block);
185                }
186                notionrs::object::block::Block::ChildDatabase { child_database: _ } => {}
187                notionrs::object::block::Block::ChildPage { child_page: _ } => {}
188                notionrs::object::block::Block::Code { code } => {
189                    let language = code.language.to_string();
190
191                    let caption = code
192                        .caption
193                        .iter()
194                        .map(|t| t.to_string())
195                        .collect::<String>();
196
197                    let props = crate::block::ElmCodeBlockProps {
198                        code: code
199                            .rich_text
200                            .iter()
201                            .map(|t| t.to_string())
202                            .collect::<String>(),
203                        language: language.to_string(),
204                        caption: if caption.is_empty() {
205                            language.to_string()
206                        } else {
207                            caption
208                        },
209                        margin: "2rem".to_string(),
210                    };
211
212                    blocks.push(crate::block::Block::ElmCodeBlock(
213                        crate::block::ElmCodeBlock {
214                            props,
215                            id: block.id,
216                        },
217                    ));
218                }
219                notionrs::object::block::Block::ColumnList { column_list: _ } => {
220                    let children = self.convert_block(&block.id).await?;
221                    let block = crate::block::Block::ElmColumnList(crate::block::ElmColumnList {
222                        children,
223                        id: block.id,
224                    });
225                    blocks.push(block);
226                }
227                notionrs::object::block::Block::Column { column: _ } => {
228                    let children = self.convert_block(&block.id).await?;
229                    let block = crate::block::Block::ElmColumn(crate::block::ElmColumn {
230                        children,
231                        id: block.id,
232                    });
233                    blocks.push(block);
234                }
235                notionrs::object::block::Block::Divider { divider: _ } => {
236                    blocks.push(crate::block::Block::ElmDivider(crate::block::ElmDivider {
237                        props: crate::block::ElmDividerProps {
238                            margin: "2rem".to_string(),
239                        },
240                        id: block.id,
241                    }));
242                }
243                notionrs::object::block::Block::Embed { embed: _ } => {}
244                notionrs::object::block::Block::Equation { equation } => {
245                    let props = crate::block::ElmKatexProps {
246                        expression: equation.expression,
247                        block: true,
248                    };
249
250                    let block = crate::block::Block::ElmKatex(crate::block::ElmKatex { props });
251
252                    blocks.push(block);
253                }
254                notionrs::object::block::Block::File { file } => {
255                    let (name, src) = match file {
256                        notionrs::object::file::File::External(f) => (f.name, f.external.url),
257                        notionrs::object::file::File::Uploaded(f) => (f.name, f.file.url),
258                    };
259
260                    let props = crate::block::ElmFileProps {
261                        name,
262                        src,
263                        margin: "2rem".to_string(),
264                    };
265
266                    let block = crate::block::Block::ElmFile(crate::block::ElmFile {
267                        props,
268                        id: block.id,
269                    });
270
271                    blocks.push(block);
272                }
273                notionrs::object::block::Block::Heading1 { heading_1 } => {
274                    let heading = heading_1;
275                    if heading.is_toggleable {
276                        let summary = heading
277                            .rich_text
278                            .iter()
279                            .map(|t| t.to_string())
280                            .collect::<String>();
281
282                        let props = crate::block::ElmToggleProps {
283                            summary,
284                            margin: "2rem".to_string(),
285                        };
286
287                        let children = self.convert_block(&block.id).await?;
288
289                        let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
290                            props,
291                            children,
292                            id: block.id,
293                        });
294
295                        blocks.push(block);
296                    } else {
297                        let props = crate::block::ElmHeadingProps {
298                            text: heading
299                                .rich_text
300                                .iter()
301                                .map(|t| t.to_string())
302                                .collect::<String>(),
303                        };
304
305                        let block = crate::block::Block::ElmHeading1(crate::block::ElmHeading1 {
306                            props,
307                            id: block.id,
308                        });
309
310                        blocks.push(block);
311                    }
312                }
313                notionrs::object::block::Block::Heading2 { heading_2 } => {
314                    let heading = heading_2;
315                    if heading.is_toggleable {
316                        let summary = heading
317                            .rich_text
318                            .iter()
319                            .map(|t| t.to_string())
320                            .collect::<String>();
321
322                        let props = crate::block::ElmToggleProps {
323                            summary,
324                            margin: "2rem".to_string(),
325                        };
326
327                        let children = self.convert_block(&block.id).await?;
328
329                        let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
330                            props,
331                            children,
332                            id: block.id,
333                        });
334
335                        blocks.push(block);
336                    } else {
337                        let props = crate::block::ElmHeadingProps {
338                            text: heading
339                                .rich_text
340                                .iter()
341                                .map(|t| t.to_string())
342                                .collect::<String>(),
343                        };
344
345                        let block = crate::block::Block::ElmHeading2(crate::block::ElmHeading2 {
346                            props,
347                            id: block.id,
348                        });
349
350                        blocks.push(block);
351                    }
352                }
353                notionrs::object::block::Block::Heading3 { heading_3 } => {
354                    let heading = heading_3;
355                    if heading.is_toggleable {
356                        let summary = heading
357                            .rich_text
358                            .iter()
359                            .map(|t| t.to_string())
360                            .collect::<String>();
361
362                        let props = crate::block::ElmToggleProps {
363                            summary,
364                            margin: "2rem".to_string(),
365                        };
366
367                        let children = self.convert_block(&block.id).await?;
368
369                        let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
370                            props,
371                            children,
372                            id: block.id,
373                        });
374
375                        blocks.push(block);
376                    } else {
377                        let props = crate::block::ElmHeadingProps {
378                            text: heading
379                                .rich_text
380                                .iter()
381                                .map(|t| t.to_string())
382                                .collect::<String>(),
383                        };
384
385                        let block = crate::block::Block::ElmHeading3(crate::block::ElmHeading3 {
386                            props,
387                            id: block.id,
388                        });
389
390                        blocks.push(block);
391                    }
392                }
393                notionrs::object::block::Block::Image { image } => {
394                    let (src, alt) = match image {
395                        notionrs::object::file::File::External(f) => (
396                            f.external.url,
397                            f.caption
398                                .map(|rich_text| {
399                                    rich_text.iter().map(|t| t.to_string()).collect::<String>()
400                                })
401                                .filter(|s| !s.trim().is_empty()),
402                        ),
403                        notionrs::object::file::File::Uploaded(f) => (
404                            f.file.url,
405                            f.caption
406                                .map(|rich_text| {
407                                    rich_text.iter().map(|t| t.to_string()).collect::<String>()
408                                })
409                                .filter(|s| !s.trim().is_empty()),
410                        ),
411                    };
412
413                    let props = crate::block::ElmBlockImageProps {
414                        src: src.clone(),
415                        alt,
416                        enable_modal: true,
417                    };
418
419                    let image_block =
420                        crate::block::Block::ElmBlockImage(crate::block::ElmBlockImage {
421                            props,
422                            id: block.id.clone(),
423                        });
424
425                    blocks.push(image_block);
426                }
427                notionrs::object::block::Block::LinkPreview { link_preview: _ } => {}
428                notionrs::object::block::Block::NumberedListItem { numbered_list_item } => {
429                    let mut list_item_children: Vec<crate::block::Block> = Vec::new();
430
431                    let rich_text_block =
432                        Client::convert_rich_text(numbered_list_item.rich_text).await?;
433                    list_item_children.extend(rich_text_block);
434
435                    if block.has_children {
436                        let list_item_children_blocks = self.convert_block(&block.id).await?;
437                        list_item_children.extend(list_item_children_blocks);
438                    }
439
440                    let list_item_block =
441                        crate::block::Block::ElmListItem(crate::block::ElmListItem {
442                            children: list_item_children,
443                            id: block.id.clone(),
444                        });
445
446                    let last_item = blocks.last_mut();
447
448                    match last_item {
449                        Some(crate::block::Block::ElmNumberedList(elm_numbered_list)) => {
450                            elm_numbered_list.children.push(list_item_block);
451                        }
452                        Some(_) | None => {
453                            let new_ol = vec![list_item_block];
454                            blocks.push(crate::block::Block::ElmNumberedList(
455                                crate::block::ElmNumberedList {
456                                    children: new_ol,
457                                    id: block.id,
458                                },
459                            ));
460                        }
461                    };
462                }
463                notionrs::object::block::Block::Paragraph { paragraph } => {
464                    let block = crate::block::Block::ElmParagraph(crate::block::ElmParagraph {
465                        children: Client::convert_rich_text(paragraph.rich_text).await?,
466                        id: block.id,
467                    });
468
469                    blocks.push(block);
470                }
471                notionrs::object::block::Block::Pdf { pdf: _ } => {}
472                notionrs::object::block::Block::Quote { quote } => {
473                    let mut children = Vec::new();
474
475                    let inline_text_block = Client::convert_rich_text(quote.rich_text).await?;
476
477                    let children_block = self.convert_block(&block.id).await?;
478
479                    children.extend(inline_text_block);
480                    children.extend(children_block);
481
482                    let block = crate::block::Block::ElmBlockQuote(crate::block::ElmBlockQuote {
483                        children,
484                        id: block.id,
485                    });
486
487                    blocks.push(block);
488                }
489                notionrs::object::block::Block::SyncedBlock { synced_block: _ } => {
490                    let children = self.convert_block(&block.id).await?;
491                    blocks.extend(children);
492                }
493                notionrs::object::block::Block::TableOfContents {
494                    table_of_contents: _,
495                } => {}
496                notionrs::object::block::Block::Table { table: _ } => {
497                    let rows = self
498                        .notionrs_client
499                        .get_block_children_all()
500                        .block_id(block.id)
501                        .send()
502                        .await?;
503
504                    if let Some((header_row, body_rows)) = rows.split_first() {
505                        let table_header_block =
506                            if let notionrs::object::block::Block::TableRow { table_row } =
507                                &header_row.block
508                            {
509                                let cells_blocks = table_row
510                                    .cells
511                                    .iter()
512                                    .map(|cell| {
513                                        crate::block::Block::ElmTableCell(
514                                            crate::block::ElmTableCell {
515                                                props: crate::block::ElmTableCellProps {
516                                                    has_header: true,
517                                                    text: cell
518                                                        .iter()
519                                                        .map(|t| t.to_string())
520                                                        .collect::<String>(),
521                                                },
522                                            },
523                                        )
524                                    })
525                                    .collect::<Vec<crate::block::Block>>();
526
527                                let table_row_block =
528                                    crate::block::Block::ElmTableRow(crate::block::ElmTableRow {
529                                        children: cells_blocks,
530                                    });
531
532                                crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
533                                    children: vec![table_row_block],
534                                })
535                            } else {
536                                crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
537                                    children: vec![],
538                                })
539                            };
540
541                        let table_body_row_blocks =
542                            body_rows.iter().filter_map(|row| match &row.block {
543                                notionrs::object::block::Block::TableRow { table_row } => {
544                                    let cells_blocks = table_row
545                                        .cells
546                                        .iter()
547                                        .map(|cell| {
548                                            crate::block::Block::ElmTableCell(
549                                                crate::block::ElmTableCell {
550                                                    props: crate::block::ElmTableCellProps {
551                                                        has_header: false,
552                                                        text: cell
553                                                            .iter()
554                                                            .map(|t| t.to_string())
555                                                            .collect::<String>(),
556                                                    },
557                                                },
558                                            )
559                                        })
560                                        .collect::<Vec<crate::block::Block>>();
561
562                                    Some(crate::block::Block::ElmTableRow(
563                                        crate::block::ElmTableRow {
564                                            children: cells_blocks,
565                                        },
566                                    ))
567                                }
568                                _ => None,
569                            });
570
571                        let table_body_block =
572                            crate::block::Block::ElmTableBody(crate::block::ElmTableBody {
573                                children: table_body_row_blocks.collect(),
574                            });
575
576                        let table_block = crate::block::Block::ElmTable(crate::block::ElmTable {
577                            children: vec![table_header_block, table_body_block],
578                        });
579
580                        blocks.push(table_block);
581                    }
582                }
583                notionrs::object::block::Block::TableRow { table_row: _ } => {}
584                notionrs::object::block::Block::Template { template: _ } => {}
585                notionrs::object::block::Block::ToDo { to_do } => {
586                    let props = crate::block::ElmCheckboxProps {
587                        label: to_do
588                            .rich_text
589                            .iter()
590                            .map(|t| t.to_string())
591                            .collect::<String>(),
592                    };
593
594                    let block = crate::block::Block::ElmCheckbox(crate::block::ElmCheckbox {
595                        props,
596                        id: block.id,
597                    });
598
599                    blocks.push(block);
600                }
601                notionrs::object::block::Block::Toggle { toggle } => {
602                    let summary = toggle
603                        .rich_text
604                        .iter()
605                        .map(|t| t.to_string())
606                        .collect::<String>();
607
608                    let props = crate::block::ElmToggleProps {
609                        summary,
610                        margin: "2rem".to_string(),
611                    };
612
613                    let children = self.convert_block(&block.id).await?;
614
615                    let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
616                        props,
617                        children,
618                        id: block.id,
619                    });
620
621                    blocks.push(block);
622                }
623                notionrs::object::block::Block::Video { video: _ } => {}
624                
625                notionrs::object::block::Block::Unsupported => {}
626
627                #[allow(unreachable_patterns)]
628                _ => {}
629            };
630        }
631
632        Ok(blocks)
633    }
634
635    pub async fn convert_rich_text(
636        rich_text: Vec<notionrs::object::rich_text::RichText>,
637    ) -> Result<Vec<crate::block::Block>, crate::error::Error> {
638        let mut blocks: Vec<crate::block::Block> = Vec::new();
639
640        for r in rich_text {
641            let block: Result<crate::block::Block, crate::error::Error> = match r {
642                notionrs::object::rich_text::RichText::Mention { mention, .. } => match mention {
643                    notionrs::object::rich_text::mention::Mention::LinkMention { link_mention } => {
644                        let href = link_mention.href.as_str();
645
646                        let mut props = crate::block::ElmInlineLinkProps {
647                            href: link_mention.href.to_string(),
648                            text: link_mention.to_string(),
649                            favicon: None,
650                        };
651
652                        let response = reqwest::Client::new()
653                            .get(href)
654                            .header("user-agent", "Rust - reqwest")
655                            .send()
656                            .await?
657                            .text()
658                            .await?;
659
660                        let document = scraper::Html::parse_document(&response);
661
662                        let title = document
663                            .select(&scraper::Selector::parse("title")?)
664                            .next()
665                            .map(|element| element.text().collect::<String>());
666
667                        let og_title_selector =
668                            scraper::Selector::parse("meta[property='og:title']")?;
669
670                        if let Some(element) = document.select(&og_title_selector).next() {
671                            if let Some(content) = element.value().attr("content") {
672                                props.text = content.to_string();
673                            }
674                        }
675
676                        if let Some(title) = title {
677                            props.text = title;
678                        }
679
680                        let favicon_selector = scraper::Selector::parse(r#"link[rel="icon"]"#)?;
681
682                        props.favicon = document
683                            .select(&favicon_selector)
684                            .next()
685                            .and_then(|f| f.value().attr("href").map(String::from))
686                            .and_then(|favicon_href| {
687                                if favicon_href.starts_with("http://")
688                                    || favicon_href.starts_with("https://")
689                                {
690                                    Some(favicon_href)
691                                } else {
692                                    url::Url::parse(&href)
693                                        .and_then(|s| s.join(&favicon_href))
694                                        .map(|s| s.to_string())
695                                        .ok()
696                                }
697                            });
698
699                        Ok(crate::block::Block::ElmInlineLink(
700                            crate::block::ElmInlineLink { props },
701                        ))
702                    }
703                    notionrs::object::rich_text::mention::Mention::User { user: _ } => continue,
704                    notionrs::object::rich_text::mention::Mention::Date { date: _ } => continue,
705                    notionrs::object::rich_text::mention::Mention::LinkPreview {
706                        link_preview: _,
707                    } => {
708                        continue;
709                    }
710                    notionrs::object::rich_text::mention::Mention::TemplateMention {
711                        template_mention: _,
712                    } => continue,
713                    notionrs::object::rich_text::mention::Mention::Page { page: _ } => continue,
714                    notionrs::object::rich_text::mention::Mention::Database { database: _ } => {
715                        continue;
716                    }
717                    notionrs::object::rich_text::mention::Mention::CustomEmoji { custom_emoji } => {
718                        let props = crate::block::ElmInlineIconProps {
719                            src: custom_emoji.url.to_string(),
720                            alt: custom_emoji.name.to_string(),
721                        };
722
723                        let block =
724                            crate::block::Block::ElmInlineIcon(crate::block::ElmInlineIcon {
725                                id: custom_emoji.id.to_string(),
726                                props,
727                            });
728
729                        Ok(block)
730                    }
731                },
732                notionrs::object::rich_text::RichText::Text {
733                    text: _text,
734                    annotations,
735                    plain_text,
736                    href: _href,
737                } => {
738                    let props = crate::block::ElmInlineTextProps {
739                        text: plain_text.to_string(),
740                        bold: annotations.bold,
741                        italic: annotations.italic,
742                        underline: annotations.underline,
743                        strikethrough: annotations.strikethrough,
744                        code: annotations.code,
745                        color: match annotations.color {
746                            notionrs::object::color::Color::Default => None,
747                            notionrs::object::color::Color::Blue => Some(String::from("#6987b8")),
748                            notionrs::object::color::Color::Brown => Some(String::from("#8b4c3f")),
749                            notionrs::object::color::Color::Gray => Some(String::from("#868e9c")),
750                            notionrs::object::color::Color::Green => Some(String::from("#59b57c")),
751                            notionrs::object::color::Color::Orange => Some(String::from("#bf7e71")),
752                            notionrs::object::color::Color::Pink => Some(String::from("#c9699e")),
753                            notionrs::object::color::Color::Purple => Some(String::from("#9771bd")),
754                            notionrs::object::color::Color::Red => Some(String::from("#b36472")),
755                            notionrs::object::color::Color::Yellow => Some(String::from("#b8a36e")),
756                            _ => None,
757                        },
758                    };
759
760                    Ok(crate::block::Block::ElmInlineText(
761                        crate::block::ElmInlineText { props },
762                    ))
763                }
764                notionrs::object::rich_text::RichText::Equation {
765                    equation,
766                    annotations: _annotations,
767                    plain_text: _plain_text,
768                    href: _href,
769                } => {
770                    let props = crate::block::ElmKatexProps {
771                        block: false,
772                        expression: equation.expression,
773                    };
774
775                    let block = crate::block::Block::ElmKatex(crate::block::ElmKatex { props });
776
777                    Ok(block)
778                }
779            };
780
781            blocks.push(block.unwrap());
782        }
783
784        Ok(blocks)
785    }
786}