Skip to main content

iris_reader/renderer/markdown/
model.rs

1use pulldown_cmark::Alignment;
2
3use crate::{
4    renderer::{HeadingId, NodeId},
5    search::{SearchMatch, SearchMatcher},
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum AlertKind {
10    Note,
11    Tip,
12    Important,
13    Warning,
14    Caution,
15}
16
17#[derive(Debug, Clone)]
18pub enum Inline {
19    Text(String),
20    Strong(Vec<Inline>),
21    Emphasis(Vec<Inline>),
22    Strikethrough(Vec<Inline>),
23    Code(String),
24    Link {
25        text: Vec<Inline>,
26        destination: String,
27    },
28    Image {
29        alt: Vec<Inline>,
30        source: String,
31    },
32    InlineMath(String),
33    RawHtml(String),
34    SoftBreak,
35    HardBreak,
36}
37
38impl Inline {
39    pub fn plain_text(items: &[Inline]) -> String {
40        let mut output = String::new();
41        for item in items {
42            match item {
43                Inline::Text(text) | Inline::Code(text) | Inline::InlineMath(text) => {
44                    output.push_str(text)
45                }
46                Inline::RawHtml(_) => {}
47                Inline::Strong(inner) | Inline::Emphasis(inner) | Inline::Strikethrough(inner) => {
48                    output.push_str(&Inline::plain_text(inner))
49                }
50                Inline::Link { text, .. } => output.push_str(&Inline::plain_text(text)),
51                Inline::Image { alt, .. } => output.push_str(&Inline::plain_text(alt)),
52                Inline::SoftBreak => output.push(' '),
53                Inline::HardBreak => output.push('\n'),
54            }
55        }
56        output
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct Heading {
62    pub id: HeadingId,
63    pub level: u8,
64    pub slug: String,
65    pub content: Vec<Inline>,
66}
67
68#[derive(Debug, Clone)]
69pub struct ListItem {
70    pub id: NodeId,
71    pub checked: Option<bool>,
72    pub content: Vec<Inline>,
73    pub blocks: Vec<Block>,
74}
75
76#[derive(Debug, Clone)]
77pub struct Table {
78    pub id: NodeId,
79    pub alignments: Vec<Alignment>,
80    pub header: Vec<Vec<Inline>>,
81    pub rows: Vec<Vec<Vec<Inline>>>,
82}
83
84#[derive(Debug, Clone)]
85pub enum Block {
86    Paragraph {
87        id: NodeId,
88        content: Vec<Inline>,
89    },
90    Heading(Heading),
91    BlockQuote {
92        id: NodeId,
93        kind: Option<AlertKind>,
94        blocks: Vec<Block>,
95    },
96    CodeBlock {
97        id: NodeId,
98        language: Option<String>,
99        code: String,
100    },
101    List {
102        ordered_start: Option<u64>,
103        items: Vec<ListItem>,
104    },
105    Table(Table),
106    HorizontalRule {
107        id: NodeId,
108    },
109    DisplayMath {
110        id: NodeId,
111        content: String,
112    },
113    Html {
114        id: NodeId,
115        content: String,
116    },
117}
118
119#[derive(Debug, Clone, Default)]
120pub struct Document {
121    pub blocks: Vec<Block>,
122}
123
124impl Document {
125    pub fn search(&self, matcher: &SearchMatcher) -> Vec<SearchMatch> {
126        let mut matches = Vec::new();
127        search_blocks(&self.blocks, matcher, &mut matches);
128        matches
129    }
130}
131
132fn search_blocks(blocks: &[Block], matcher: &SearchMatcher, matches: &mut Vec<SearchMatch>) {
133    for block in blocks {
134        match block {
135            Block::Heading(heading) => {
136                push_text_matches(
137                    heading.id,
138                    &Inline::plain_text(&heading.content),
139                    matcher,
140                    matches,
141                );
142            }
143            Block::Paragraph { id, content } => {
144                push_text_matches(*id, &Inline::plain_text(content), matcher, matches);
145            }
146            Block::CodeBlock { id, code, .. } | Block::DisplayMath { id, content: code } => {
147                push_text_matches(*id, code, matcher, matches);
148            }
149            Block::Html { id, content } => {
150                push_text_matches(*id, &super::html::plain_text(content), matcher, matches);
151            }
152            Block::BlockQuote { blocks, .. } => {
153                search_blocks(blocks, matcher, matches);
154            }
155            Block::List { items, .. } => {
156                for item in items {
157                    if !item.content.is_empty() {
158                        push_text_matches(
159                            item.id,
160                            &Inline::plain_text(&item.content),
161                            matcher,
162                            matches,
163                        );
164                    }
165                    search_blocks(&item.blocks, matcher, matches);
166                }
167            }
168            Block::Table(table) => {
169                let mut text = String::new();
170                for cell in &table.header {
171                    if !text.is_empty() {
172                        text.push(' ');
173                    }
174                    text.push_str(&Inline::plain_text(cell));
175                }
176                for row in &table.rows {
177                    for cell in row {
178                        if !text.is_empty() {
179                            text.push(' ');
180                        }
181                        text.push_str(&Inline::plain_text(cell));
182                    }
183                }
184                push_text_matches(table.id, &text, matcher, matches);
185            }
186            Block::HorizontalRule { .. } => {}
187        }
188    }
189}
190
191fn push_text_matches(
192    node_id: NodeId,
193    text: &str,
194    matcher: &SearchMatcher,
195    output: &mut Vec<SearchMatch>,
196) {
197    for occurrence in 0..matcher.count(text) {
198        output.push(SearchMatch {
199            node_id,
200            occurrence,
201        });
202    }
203}