iris_reader/renderer/markdown/
model.rs1use 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}