rustdown_md/render/
mod.rs1#![forbid(unsafe_code)]
2mod blocks;
8mod height;
9mod layout;
10mod lists;
11mod table;
12mod text;
13
14#[cfg(test)]
15mod tests;
16
17use crate::parse::{Block, parse_markdown_into};
18use crate::style::MarkdownStyle;
19
20use blocks::{render_block, render_blocks};
21pub use height::bytecount_newlines;
22use height::estimate_block_height;
23use layout::RenderContext;
24
25const PREVIEW_WHEEL_SCROLL_MULTIPLIER: f32 = 1.15;
26
27#[derive(Default)]
31pub struct MarkdownCache {
32 text_hash: u64,
33 text_len: usize,
34 text_ptr: usize,
35 pub blocks: Vec<Block>,
36 pub heights: Vec<f32>,
38 pub cum_y: Vec<f32>,
40 pub total_height: f32,
42 height_body_size: f32,
44 height_wrap_width: f32,
46 pub last_scroll_y: f32,
48 heading_block_indices: Vec<usize>,
50}
51
52impl MarkdownCache {
53 pub fn clear(&mut self) {
55 self.text_hash = 0;
56 self.text_len = 0;
57 self.text_ptr = 0;
58 self.blocks.clear();
59 self.heights.clear();
60 self.cum_y.clear();
61 self.total_height = 0.0;
62 self.height_body_size = 0.0;
63 self.height_wrap_width = 0.0;
64 self.last_scroll_y = 0.0;
65 self.heading_block_indices.clear();
66 }
67
68 pub fn ensure_parsed(&mut self, source: &str) {
69 let ptr = source.as_ptr() as usize;
72 let len = source.len();
73 if ptr == self.text_ptr && len == self.text_len {
74 return;
75 }
76
77 let hash = simple_hash(source);
79 self.text_ptr = ptr;
80 self.text_len = len;
81
82 if self.text_hash == hash {
83 return;
84 }
85
86 self.text_hash = hash;
88 self.blocks.clear();
89 parse_markdown_into(source, &mut self.blocks);
90 self.heights.clear();
91 self.cum_y.clear();
92 self.total_height = 0.0;
93
94 self.heading_block_indices.clear();
96 for (idx, block) in self.blocks.iter().enumerate() {
97 if let Block::Heading { text, .. } = block
98 && !text.text.is_empty()
99 {
100 self.heading_block_indices.push(idx);
101 }
102 }
103 }
104
105 pub fn ensure_heights(&mut self, body_size: f32, wrap_width: f32, style: &MarkdownStyle) {
106 let size_bits = body_size.to_bits();
107 let width_bits = wrap_width.to_bits();
108 if !self.heights.is_empty()
109 && self.height_body_size.to_bits() == size_bits
110 && self.height_wrap_width.to_bits() == width_bits
111 {
112 return;
113 }
114 self.height_body_size = body_size;
115 self.height_wrap_width = wrap_width;
116 let n = self.blocks.len();
117 self.heights.resize(n, 0.0);
118 self.cum_y.resize(n, 0.0);
119 let mut acc = 0.0_f32;
120 for (i, block) in self.blocks.iter().enumerate() {
121 self.cum_y[i] = acc;
122 let h = estimate_block_height(block, body_size, wrap_width, style);
123 self.heights[i] = h;
124 acc += h;
125 }
126 self.total_height = acc;
127 }
128
129 fn recompute_cum_y(&mut self) {
131 let mut acc = 0.0_f32;
132 for (cum, h) in self.cum_y.iter_mut().zip(&self.heights) {
133 *cum = acc;
134 acc += h;
135 }
136 self.total_height = acc;
137 }
138
139 #[must_use]
146 pub fn heading_y(&self, ordinal: usize) -> Option<f32> {
147 let block_idx = *self.heading_block_indices.get(ordinal)?;
148 self.cum_y.get(block_idx).copied()
149 }
150}
151
152pub struct MarkdownViewer {
156 id_salt: &'static str,
157}
158
159impl MarkdownViewer {
160 #[must_use]
161 pub const fn new(id_salt: &'static str) -> Self {
162 Self { id_salt }
163 }
164
165 pub fn show_scrollable(
172 &self,
173 ui: &mut egui::Ui,
174 cache: &mut MarkdownCache,
175 style: &MarkdownStyle,
176 source: &str,
177 scroll_to_y: Option<f32>,
178 ) {
179 cache.ensure_parsed(source);
180
181 let body_size = ui.text_style_height(&egui::TextStyle::Body);
182 let wrap_width = ui.available_width();
183 cache.ensure_heights(body_size, wrap_width, style);
184
185 if cache.blocks.is_empty() {
186 return;
187 }
188
189 let mut scroll_area = egui::ScrollArea::vertical()
190 .id_salt(self.id_salt)
191 .auto_shrink([false, false])
192 .wheel_scroll_multiplier(egui::vec2(1.0, PREVIEW_WHEEL_SCROLL_MULTIPLIER));
193
194 if let Some(y) = scroll_to_y
195 && y.is_finite()
196 {
197 scroll_area = scroll_area.vertical_scroll_offset(y);
198 }
199
200 let mut heights_changed = false;
202
203 scroll_area.show_viewport(ui, |ui, viewport| {
204 cache.last_scroll_y = viewport.min.y;
206
207 ui.set_min_height(cache.total_height);
209
210 let vis_top = viewport.min.y;
211 let vis_bottom = viewport.max.y;
212
213 let first = match cache
215 .cum_y
216 .binary_search_by(|y| y.partial_cmp(&vis_top).unwrap_or(std::cmp::Ordering::Equal))
217 {
218 Ok(i) => i,
219 Err(i) => i.saturating_sub(1),
220 };
221
222 if first > 0 {
224 let skip_h = cache.cum_y[first];
225 ui.add_space(skip_h);
226 }
227
228 let mut idx = first;
230 while idx < cache.blocks.len() {
231 let block_y = cache.cum_y[idx];
232 if block_y > vis_bottom {
233 break;
234 }
235
236 let rendered = ui.scope(|ui| {
241 render_block(ui, &cache.blocks[idx], style, RenderContext::root(ui));
242 });
243 let actual_h = rendered.response.rect.height();
244
245 if actual_h > 0.0 && (cache.heights[idx] - actual_h).abs() > 2.0 {
246 cache.heights[idx] = actual_h;
247 heights_changed = true;
248 }
249
250 idx += 1;
251 }
252
253 if idx < cache.blocks.len() {
255 let remaining = cache.total_height - cache.cum_y[idx];
256 if remaining > 0.0 {
257 ui.add_space(remaining);
258 }
259 }
260 });
261
262 if heights_changed {
266 cache.recompute_cum_y();
267 }
268 }
269
270 pub fn show(
272 &self,
273 ui: &mut egui::Ui,
274 cache: &mut MarkdownCache,
275 style: &MarkdownStyle,
276 source: &str,
277 ) {
278 cache.ensure_parsed(source);
279 render_blocks(ui, &cache.blocks, style, RenderContext::root(ui));
280 }
281}
282
283#[inline]
284pub(crate) fn simple_hash(s: &str) -> u64 {
285 const BASIS: u64 = 0xcbf2_9ce4_8422_2325;
287 const PRIME: u64 = 0x0100_0000_01b3;
288
289 let bytes = s.as_bytes();
290 let (chunks, remainder) = bytes.as_chunks::<8>();
291 let mut h: u64 = BASIS;
292
293 for chunk in chunks {
294 let word = u64::from_le_bytes(*chunk);
295 h ^= word;
296 h = h.wrapping_mul(PRIME);
297 }
298
299 for &b in remainder {
300 h ^= u64::from(b);
301 h = h.wrapping_mul(PRIME);
302 }
303 h
304}