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}