gorbie_commonmark/lib.rs
1//! A commonmark viewer for egui
2//!
3//! # Example
4//!
5//! ```
6//! # use gorbie_commonmark::*;
7//! # use egui::__run_test_ui;
8//! let markdown =
9//! r"# Hello world
10//!
11//! * A list
12//! * [ ] Checkbox
13//! ";
14//!
15//! # __run_test_ui(|ui| {
16//! let mut cache = CommonMarkCache::default();
17//! CommonMarkViewer::new().show(ui, &mut cache, markdown);
18//! # });
19//!
20//! ```
21//!
22//! Remember to opt into the image formats you want to use!
23//!
24//! ```toml
25//! image = { version = "0.25", default-features = false, features = ["png"] }
26//! ```
27//! # FAQ
28//!
29//! ## URL is not displayed when hovering over a link
30//!
31//! By default egui does not show urls when you hover hyperlinks. To enable it,
32//! you can do the following before calling any ui related functions:
33//!
34//! ```
35//! # use egui::__run_test_ui;
36//! # __run_test_ui(|ui| {
37//! ui.style_mut().url_in_tooltip = true;
38//! # });
39//! ```
40//!
41//!
42//! # Compile time evaluation of markdown
43//!
44//! If you want to embed markdown directly the binary then you can enable the `macros` feature.
45//! This will do the parsing of the markdown at compile time and output egui widgets.
46//!
47//! ## Example
48//!
49//! ```rust,ignore
50//! use gorbie_commonmark::{CommonMarkCache, commonmark};
51//! # egui::__run_test_ui(|ui| {
52//! let mut cache = CommonMarkCache::default();
53//! let _response = commonmark!(ui, &mut cache, "# ATX Heading Level 1");
54//! # });
55//! ```
56//!
57//! Alternatively you can embed a file
58//!
59//!
60//! ## Example
61//!
62//! ```rust,ignore
63//! use gorbie_commonmark::{CommonMarkCache, commonmark_str};
64//! # egui::__run_test_ui(|ui| {
65//! let mut cache = CommonMarkCache::default();
66//! commonmark_str!(ui, &mut cache, "content.md");
67//! # });
68//! ```
69//!
70//! For more information check out the documentation for
71//! [gorbie_commonmark_macros](https://docs.rs/gorbie-commonmark-macros)
72#![cfg_attr(feature = "document-features", doc = "# Features")]
73#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
74
75use egui::{self, Id};
76
77mod parsers;
78
79pub use gorbie_commonmark_backend::RenderHtmlFn;
80pub use gorbie_commonmark_backend::RenderMathFn;
81pub use gorbie_commonmark_backend::alerts::{Alert, AlertBundle};
82pub use gorbie_commonmark_backend::misc::CommonMarkCache;
83
84#[cfg(feature = "macros")]
85pub use gorbie_commonmark_macros::*;
86
87#[cfg(feature = "macros")]
88// Do not rely on this directly!
89#[doc(hidden)]
90pub use gorbie_commonmark_backend;
91
92use gorbie_commonmark_backend::*;
93
94#[derive(Debug, Default)]
95pub struct CommonMarkViewer<'f> {
96 options: CommonMarkOptions<'f>,
97}
98
99impl<'f> CommonMarkViewer<'f> {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 /// The amount of spaces a bullet point is indented. By default this is 4
105 /// spaces.
106 pub fn indentation_spaces(mut self, spaces: usize) -> Self {
107 self.options.indentation_spaces = spaces;
108 self
109 }
110
111 /// The maximum size images are allowed to be. They will be scaled down if
112 /// they are larger
113 pub fn max_image_width(mut self, width: Option<usize>) -> Self {
114 self.options.max_image_width = width;
115 self
116 }
117
118 /// The default width of the ui. This is only respected if this is larger than
119 /// the [`max_image_width`](Self::max_image_width)
120 pub fn default_width(mut self, width: Option<usize>) -> Self {
121 self.options.default_width = width;
122 self
123 }
124
125 /// Show alt text when hovering over images. By default this is enabled.
126 pub fn show_alt_text_on_hover(mut self, show: bool) -> Self {
127 self.options.show_alt_text_on_hover = show;
128 self
129 }
130
131 /// Allows changing the default implicit `file://` uri scheme.
132 /// This does nothing if [`explicit_image_uri_scheme`](`Self::explicit_image_uri_scheme`) is enabled
133 ///
134 /// # Example
135 /// ```
136 /// # use gorbie_commonmark::CommonMarkViewer;
137 /// CommonMarkViewer::new().default_implicit_uri_scheme("https://example.org/");
138 /// ```
139 pub fn default_implicit_uri_scheme<S: Into<String>>(mut self, scheme: S) -> Self {
140 self.options.default_implicit_uri_scheme = scheme.into();
141 self
142 }
143
144 /// By default any image without a uri scheme such as `foo://` is assumed to
145 /// be of the type `file://`. This assumption can sometimes be wrong or be done
146 /// incorrectly, so if you want to always be explicit with the scheme then set
147 /// this to `true`
148 pub fn explicit_image_uri_scheme(mut self, use_explicit: bool) -> Self {
149 self.options.use_explicit_uri_scheme = use_explicit;
150 self
151 }
152
153 #[cfg(feature = "better_syntax_highlighting")]
154 /// Set the syntax theme to be used inside code blocks in light mode
155 pub fn syntax_theme_light<S: Into<String>>(mut self, theme: S) -> Self {
156 self.options.theme_light = theme.into();
157 self
158 }
159
160 #[cfg(feature = "better_syntax_highlighting")]
161 /// Set the syntax theme to be used inside code blocks in dark mode
162 pub fn syntax_theme_dark<S: Into<String>>(mut self, theme: S) -> Self {
163 self.options.theme_dark = theme.into();
164 self
165 }
166
167 /// Specify what kind of alerts are supported. This can also be used to localize alerts.
168 ///
169 /// By default [github flavoured markdown style alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts)
170 /// are used
171 pub fn alerts(mut self, alerts: AlertBundle) -> Self {
172 self.options.alerts = alerts;
173 self
174 }
175
176 /// Allows rendering math. This has to be done manually as you might want a different
177 /// implementation for the web and native.
178 ///
179 /// The example is template code for rendering a svg image. Make sure to enable the
180 /// `egui_extras/svg` feature for the result to show up.
181 ///
182 /// ## Example
183 ///
184 /// ```
185 /// # use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
186 /// # use gorbie_commonmark::CommonMarkViewer;
187 /// let mut math_images = Rc::new(RefCell::new(HashMap::new()));
188 /// CommonMarkViewer::new()
189 /// .render_math_fn(Some(&move |ui, math, inline| {
190 /// let mut map = math_images.borrow_mut();
191 /// let svg = map
192 /// .entry(math.to_string())
193 /// .or_insert_with(|| {
194 /// if inline {
195 /// // render as inline
196 /// // dummy data for the example
197 /// Arc::new([0])
198 /// } else {
199 /// Arc::new([0])
200 /// }
201 /// });
202 ///
203 /// let uri = format!("{}.svg", egui::Id::from(math.to_string()).value());
204 /// ui.add(
205 /// egui::Image::new(egui::ImageSource::Bytes {
206 /// uri: uri.into(),
207 /// bytes: egui::load::Bytes::Shared(svg.clone()),
208 /// })
209 /// .fit_to_original_size(1.0),
210 /// );
211 /// }));
212 /// ```
213 pub fn render_math_fn(mut self, func: Option<&'f RenderMathFn>) -> Self {
214 self.options.math_fn = func;
215 self
216 }
217
218 /// Allows custom handling of html. Enabling this will disable plain text rendering
219 /// of html blocks. Nodes are included in the provided text
220 pub fn render_html_fn(mut self, func: Option<&'f RenderHtmlFn>) -> Self {
221 self.options.html_fn = func;
222 self
223 }
224
225 /// Shows rendered markdown
226 pub fn show(
227 self,
228 ui: &mut egui::Ui,
229 cache: &mut CommonMarkCache,
230 text: &str,
231 ) -> egui::InnerResponse<()> {
232 gorbie_commonmark_backend::prepare_show(cache, ui.ctx());
233
234 let (response, _) = parsers::pulldown::CommonMarkViewerInternal::new().show(
235 ui,
236 cache,
237 &self.options,
238 text,
239 None,
240 );
241
242 response
243 }
244
245 /// Shows rendered markdown, and allows the rendered ui to mutate the source text.
246 ///
247 /// The only currently implemented mutation is allowing checkboxes to be toggled through the ui.
248 pub fn show_mut(
249 mut self,
250 ui: &mut egui::Ui,
251 cache: &mut CommonMarkCache,
252 text: &mut String,
253 ) -> egui::InnerResponse<()> {
254 self.options.mutable = true;
255 gorbie_commonmark_backend::prepare_show(cache, ui.ctx());
256
257 let (mut inner_response, checkmark_events) =
258 parsers::pulldown::CommonMarkViewerInternal::new().show(
259 ui,
260 cache,
261 &self.options,
262 text,
263 None,
264 );
265
266 // Update source text for checkmarks that were clicked
267 for ev in checkmark_events {
268 if ev.checked {
269 text.replace_range(ev.span, "[x]")
270 } else {
271 text.replace_range(ev.span, "[ ]")
272 }
273
274 inner_response.response.mark_changed();
275 }
276
277 inner_response
278 }
279
280 /// Shows markdown inside a [`ScrollArea`].
281 /// This function is much more performant than just calling [`show`] inside a [`ScrollArea`],
282 /// because it only renders elements that are visible.
283 ///
284 /// # Caveat
285 ///
286 /// This assumes that the markdown is static. If it does change, you have to clear the cache
287 /// by using [`clear_scrollable_with_id`](CommonMarkCache::clear_scrollable_with_id) or
288 /// [`clear_scrollable`](CommonMarkCache::clear_scrollable). If the content changes every frame,
289 /// it's faster to call [`show`] directly.
290 ///
291 /// [`ScrollArea`]: egui::ScrollArea
292 /// [`show`]: crate::CommonMarkViewer::show
293 #[doc(hidden)] // Buggy in scenarios more complex than the example application
294 #[cfg(feature = "pulldown_cmark")]
295 pub fn show_scrollable(
296 self,
297 source_id: impl std::hash::Hash,
298 ui: &mut egui::Ui,
299 cache: &mut CommonMarkCache,
300 text: &str,
301 ) {
302 gorbie_commonmark_backend::prepare_show(cache, ui.ctx());
303 parsers::pulldown::CommonMarkViewerInternal::new().show_scrollable(
304 Id::new(source_id),
305 ui,
306 cache,
307 &self.options,
308 text,
309 );
310 }
311}
312
313pub(crate) struct ListLevel {
314 current_number: Option<u64>,
315}
316
317#[derive(Default)]
318pub(crate) struct List {
319 items: Vec<ListLevel>,
320 has_list_begun: bool,
321}
322
323impl List {
324 pub fn start_level_with_number(&mut self, start_number: u64) {
325 self.items.push(ListLevel {
326 current_number: Some(start_number),
327 });
328 }
329
330 pub fn start_level_without_number(&mut self) {
331 self.items.push(ListLevel {
332 current_number: None,
333 });
334 }
335
336 pub fn is_inside_a_list(&self) -> bool {
337 !self.items.is_empty()
338 }
339
340 pub fn is_last_level(&self) -> bool {
341 self.items.len() == 1
342 }
343
344 pub fn start_item(&mut self, ui: &mut egui::Ui, options: &CommonMarkOptions) {
345 // To ensure that newlines are only inserted within the list and not before it
346 if self.has_list_begun {
347 newline(ui);
348 } else {
349 self.has_list_begun = true;
350 }
351
352 let len = self.items.len();
353 if let Some(item) = self.items.last_mut() {
354 ui.label(" ".repeat((len - 1) * options.indentation_spaces));
355
356 if let Some(number) = &mut item.current_number {
357 number_point(ui, &number.to_string());
358 *number += 1;
359 } else if len > 1 {
360 bullet_point_hollow(ui);
361 } else {
362 bullet_point(ui);
363 }
364 } else {
365 unreachable!();
366 }
367
368 ui.add_space(4.0);
369 }
370
371 pub fn end_level(&mut self, ui: &mut egui::Ui, insert_newline: bool) {
372 self.items.pop();
373
374 if self.items.is_empty() && insert_newline {
375 newline(ui);
376 }
377 }
378}