diff --git a/CHANGELOG.md b/CHANGELOG.md index a99f9a5..27767a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,29 @@ ## Unreleased +## 0.15.0 - 2024-04-02 + ### Added - Replace copy icon with checkmark when clicking copy button in code blocks ([#42](https://github.com/lampsitter/egui_commonmark/pull/42) by [@zeozeozeo](https://github.com/zeozeozeo)) - - Interactive tasklists with `CommonMarkViewer::show_mut` ([#40](https://github.com/lampsitter/egui_commonmark/pull/40) by [@crumblingstatue](https://github.com/crumblingstatue)) ### Changed +- MSRV bumped to 1.74 due to pulldown_cmark - Alerts are case-insensitive +- More spacing between list indicator and elements ([#46](https://github.com/lampsitter/egui_commonmark/pull/46)) + +### Fixed +- Lists align text when wrapping instead of wrapping at the beginning of the next + line ([#46](https://github.com/lampsitter/egui_commonmark/pull/46)) +- Code blocks won't insert a newline when in lists +- In certain scenarios there was no newline after lists +- Copy button for code blocks show the correct cursor again on hover (regression + after egui 0.27) ## 0.14.0 - 2024-03-26 diff --git a/Cargo.toml b/Cargo.toml index 9981051..a290900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "egui_commonmark" authors = ["Erlend Walstad"] -version = "0.14.0" +version = "0.15.0" edition = "2021" description = "Commonmark viewer for egui" keywords = ["commonmark", "egui"] @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/lampsitter/egui_commonmark" documentation = "https://docs.rs/egui_commonmark" include = ["**/*.rs", "LICENSE-MIT", "LICENSE-APACHE", "Cargo.toml"] -rust-version = "1.72" # Follow egui +rust-version = "1.74" # Required by pulldown_cmark [dependencies] egui_extras = "0.27" @@ -24,10 +24,10 @@ syntect = { version = "5.0.0", optional = true, default-features = false, featur document-features = { version = "0.2", optional = true } comrak = { version = "0.20.0", default-features = false, optional = true } -pulldown-cmark = { version = "0.10", default-features = false, optional = true } +pulldown-cmark = { git = "https://github.com/pulldown-cmark/pulldown-cmark.git", branch = "branch_0.11", default-features = false, optional = true } [features] -default = ["load-images", "pulldown_cmark"] +default = ["load-images", "pulldown_cmark", "math"] pulldown_cmark = ["dep:pulldown-cmark"] comrak = ["dep:comrak"] @@ -44,9 +44,14 @@ svg = ["egui_extras/svg"] ## Images with urls will be downloaded and displayed fetch = ["egui_extras/http"] +## Enable pulldown_cmark's math parser +math = [] + [dev-dependencies] eframe = { version = "0.27", default-features = false, features = ["default_fonts", "glow"] } -image = { version = "0.24", default-features = false, features = ["png"] } +image = { version = "0.25", default-features = false, features = ["png"] } +egui_extras = { version = "0.27", features = ["svg"] } # required for math rendering +mathjax_svg = "3.1" # required for LaTeX -> SVG [package.metadata.docs.rs] features = ["better_syntax_highlighting", "document-features"] diff --git a/README.md b/README.md index 03ff3ea..b82b3fd 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Github's markdown syntax: tables, strikethrough, tasklists and footnotes. In Cargo.toml: ```toml -egui_commonmark = "0.14" +egui_commonmark = "0.15" # Specify what image formats you want to use image = { version = "0.24", default-features = false, features = ["png"] } ``` diff --git a/examples/book.rs b/examples/book.rs index 1023ff0..6f75253 100644 --- a/examples/book.rs +++ b/examples/book.rs @@ -7,7 +7,9 @@ //! Shows a simple way to use the crate to implement a book like view. use eframe::egui; +use egui::ImageSource; use egui_commonmark::*; +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; struct Page { name: String, @@ -18,6 +20,18 @@ struct App { cache: CommonMarkCache, curr_tab: Option, pages: Vec, + math_to_svg: Rc>>>, +} + +fn render_math(math: &str, inline: bool) -> Arc<[u8]> { + println!("rendering math: {math}"); + if inline { + mathjax_svg::convert_to_svg_inline(math).unwrap() + } else { + mathjax_svg::convert_to_svg(math).unwrap() + } + .into_bytes() + .into() } impl App { @@ -47,12 +61,28 @@ impl App { fn content_panel(&mut self, ui: &mut egui::Ui) { egui::ScrollArea::vertical().show(ui, |ui| { // Add a frame with margin to prevent the content from hugging the sidepanel + let math_to_svg = self.math_to_svg.clone(); egui::Frame::none() .inner_margin(egui::Margin::symmetric(5.0, 0.0)) .show(ui, |ui| { CommonMarkViewer::new("viewer") .default_width(Some(200)) .max_image_width(Some(512)) + .render_math_fn(Some(&move |ui, math, inline| { + let mut map = math_to_svg.borrow_mut(); + let svg = map + .entry(math.to_string()) + .or_insert_with(|| render_math(math, inline)); + + let uri = format!("{}.svg", egui::Id::from(math.to_string()).value()); + ui.add( + egui::Image::new(ImageSource::Bytes { + uri: uri.into(), + bytes: egui::load::Bytes::Shared(svg.clone()), + }) + .fit_to_original_size(1.0), + ); + })) .show( ui, &mut self.cache, @@ -121,14 +151,19 @@ fn main() { content: include_str!("markdown/code-blocks.md").to_owned(), }, Page { - name: "Block Quotes ".to_owned(), + name: "Block Quotes".to_owned(), content: include_str!("markdown/blockquotes.md").to_owned(), }, Page { - name: "Tables ".to_owned(), + name: "Tables".to_owned(), content: include_str!("markdown/tables.md").to_owned(), }, + Page { + name: "Math".to_owned(), + content: include_str!("markdown/math.md").to_owned(), + }, ], + math_to_svg: Rc::new(RefCell::new(HashMap::new())), }) }), ); diff --git a/examples/markdown/blockquotes.md b/examples/markdown/blockquotes.md index 072b8d6..9b4f9dc 100644 --- a/examples/markdown/blockquotes.md +++ b/examples/markdown/blockquotes.md @@ -36,7 +36,7 @@ will be displayed as: > tip alert - + > [!imporTant] > important alert diff --git a/examples/markdown/code-blocks.md b/examples/markdown/code-blocks.md index c1980e7..91e0f22 100644 --- a/examples/markdown/code-blocks.md +++ b/examples/markdown/code-blocks.md @@ -20,3 +20,10 @@ default. It will therefore fallback to default highlighting. egui_commonmark = "0.10" image = { version = "0.24", default-features = false, features = ["png"] } ``` + +- ```rs + let x = 3.14; + ``` +- Code blocks can be in lists too :) + +More content... diff --git a/examples/markdown/lists.md b/examples/markdown/lists.md index d328e70..30e22db 100644 --- a/examples/markdown/lists.md +++ b/examples/markdown/lists.md @@ -19,4 +19,10 @@ 1. Lorem ipsum dolor sit amet, consectetur __adipiscing elit, sed__ do eiusmod tempor incididunt _ut_ labore ~~et~~ dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + 2. Lorem ipsum dolor sit amet, consectetur __adipiscing elit, sed__ do + eiusmod tempor incididunt _ut_ labore ~~et~~ dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation + - Lorem ipsum dolor sit amet, consectetur __adipiscing elit, sed__ do + eiusmod tempor incididunt _ut_ labore ~~et~~ dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation diff --git a/examples/markdown/math.md b/examples/markdown/math.md new file mode 100644 index 0000000..0062573 --- /dev/null +++ b/examples/markdown/math.md @@ -0,0 +1,60 @@ +When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are +$$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ + +- Block equations: + + $$ \frac{\partial \rho}{\partial t} + \nabla \cdot \vec{j} = 0 \,. \label{eq:continuity} $$ + +- Stokes' theorem: + +$$ \begin{equation} + \int_{\partial\Omega} \omega = \int_{\Omega} \mathrm{d}\omega \,. + \label{eq:stokes} + \end{equation} $$ + +- Maxwell's equations: + +$$ +\begin{align} + \nabla \cdot \vec{E} & = \rho \nonumber \\ + \nabla \cdot \vec{B} & = 0 \nonumber \\ + \nabla \times \vec{E} & = - \frac{\partial \vec{B}}{\partial t} \label{eq:maxwell} \\ + \nabla \times \vec{B} & = \vec{j} + \frac{\partial \vec{E}}{\partial t} \nonumber \,. +\end{align} +$$ + +- L'Hôpital's rule + +$$ +\begin{align} + \lim_{x\to 0}{\frac{e^x-1}{2x}} + \overset{\left[\frac{0}{0}\right]}{\underset{\mathrm{H}}{=}} + \lim_{x\to 0}{\frac{e^x}{2}}={\frac{1}{2}} +\end{align} +$$ + +- More stuff + +$$ +\begin{align} + z = \overbrace{ + \underbrace{x}_\text{real} + i + \underbrace{y}_\text{imaginary} + }^\text{complex number} +\end{align} +$$ + +- Multiline subscripts: $$ +\begin{align} + \prod_{\substack{ + 1\le i \le n \\ + 1\le j \le m}} + M_{i,j} +\end{align} +$$ + +--- + +## Edge cases + +Cheese is $10.40 + $0.20 tax diff --git a/rust-toolchain b/rust-toolchain index be838d8..9e137c1 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,3 +1,3 @@ [toolchain] -channel = "1.72.0" +channel = "1.74.0" components = ["rustfmt", "clippy"] diff --git a/src/elements.rs b/src/elements.rs index 51fe993..04c07f8 100644 --- a/src/elements.rs +++ b/src/elements.rs @@ -41,7 +41,7 @@ pub(crate) fn number_point(ui: &mut Ui, number: &str) { ui.painter().text( rect.right_center(), egui::Align2::RIGHT_CENTER, - format!("{number}. "), + format!("{number}."), TextStyle::Body.resolve(ui.style()), ui.visuals().strong_text_color(), ); @@ -120,16 +120,25 @@ pub fn code_block<'t>( let persistent_id = ui.make_persistent_id(output.response.id); let copied_icon = ui.memory_mut(|m| *m.data.get_temp_mut_or_default::(persistent_id)); - let copy_button = ui.put( - egui::Rect { - min: position, - max: position, - }, - egui::Button::new(if copied_icon { "✔" } else { "🗐" }) - .small() - .frame(false) - .fill(egui::Color32::TRANSPARENT), - ); + let copy_button = ui + .put( + egui::Rect { + min: position, + max: position, + }, + egui::Button::new(if copied_icon { "✔" } else { "🗐" }) + .small() + .frame(false) + .fill(egui::Color32::TRANSPARENT), + ) + // workaround for a regression after egui 0.27 where the edit cursor was shown even when + // hovering over the button. We try interact_cursor first to allow the cursor to be + // overriden + .on_hover_cursor( + ui.visuals() + .interact_cursor + .unwrap_or(egui::CursorIcon::Default), + ); // Update icon state in persistent memory if copied_icon && !copy_button.hovered() { @@ -251,6 +260,8 @@ impl List { } else { unreachable!(); } + + ui.add_space(4.0); } pub fn end_level(&mut self, ui: &mut Ui) { @@ -263,7 +274,6 @@ impl List { } pub(crate) fn blockquote(ui: &mut Ui, accent: egui::Color32, add_contents: impl FnOnce(&mut Ui)) { - newline(ui); let start = ui.painter().add(egui::Shape::Noop); let response = egui::Frame::none() // offset the frame so that we can use the space for the horizontal line and other stuff diff --git a/src/lib.rs b/src/lib.rs index df527f6..97ec99f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -302,20 +302,29 @@ impl CommonMarkOptions { } } -#[derive(Debug)] -pub struct CommonMarkViewer { +/// Takes [`egui::Ui`], the math text to be rendered and whether it is inline +type RenderMathFn = dyn Fn(&mut Ui, &str, bool); + +pub struct CommonMarkViewer<'a> { source_id: Id, options: CommonMarkOptions, + math_fn: Option<&'a RenderMathFn>, } -impl CommonMarkViewer { +impl<'a> CommonMarkViewer<'a> { pub fn new(source_id: impl std::hash::Hash) -> Self { Self { source_id: Id::new(source_id), options: CommonMarkOptions::default(), + math_fn: None, } } + pub fn render_math_fn(mut self, math_fn: Option<&'a RenderMathFn>) -> Self { + self.math_fn = math_fn; + self + } + /// The amount of spaces a bullet point is indented. By default this is 4 /// spaces. pub fn indentation_spaces(mut self, spaces: usize) -> Self { @@ -413,6 +422,7 @@ impl CommonMarkViewer { &self.options, text, false, + self.math_fn, ); #[cfg(feature = "comrak")] @@ -442,7 +452,7 @@ impl CommonMarkViewer { let (response, checkmark_events) = parsers::pulldown::CommonMarkViewerInternal::new( self.source_id, ) - .show(ui, cache, &self.options, text, false); + .show(ui, cache, &self.options, text, false, self.math_fn); // Update source text for checkmarks that were clicked for ev in checkmark_events { @@ -478,6 +488,7 @@ impl CommonMarkViewer { cache, &self.options, text, + self.math_fn, ); } } @@ -655,8 +666,6 @@ impl FencedCodeBlock { elements::code_block(ui, max_width, &self.content, &mut layout); }); - - elements::newline(ui); } } diff --git a/src/parsers/comrak.rs b/src/parsers/comrak.rs index 307f17a..f3cddfc 100644 --- a/src/parsers/comrak.rs +++ b/src/parsers/comrak.rs @@ -77,13 +77,18 @@ impl CommonMarkViewerInternal { NodeValue::FrontMatter(_front_matter) => {} NodeValue::BlockQuote => { + if self.should_insert_newline { + newline(ui) + } blockquote(ui, ui.visuals().weak_text_color(), |ui| { self.text_style.quote = true; self.render(ui, cache, options, max_width, c); self.text_style.quote = false; }); - newline(ui); + if self.should_insert_newline { + newline(ui); + } } NodeValue::List(list) => { @@ -96,7 +101,7 @@ impl CommonMarkViewerInternal { self.render(ui, cache, options, max_width, c); self.list.end_level(ui); - if self.list.is_inside_a_list() { + if !self.list.is_inside_a_list() { self.should_insert_newline = true; } } @@ -119,7 +124,9 @@ impl CommonMarkViewerInternal { content: code_block.literal.to_string(), }); - newline(ui); + if self.should_insert_newline { + newline(ui); + } } self.text_style.code = true; @@ -127,6 +134,10 @@ impl CommonMarkViewerInternal { if let Some(block) = self.fenced_code_block.take() { block.end(ui, cache, options, max_width); + + if self.should_insert_newline { + newline(ui); + } } self.text_style.code = false; } @@ -139,7 +150,9 @@ impl CommonMarkViewerInternal { // we deliberately do not set it to false after this } - self.render(ui, cache, options, max_width, c); + ui.horizontal_wrapped(|ui| { + self.render(ui, cache, options, max_width, c); + }); // To end the inlines if self.should_insert_newline { diff --git a/src/parsers/pulldown.rs b/src/parsers/pulldown.rs index 1d96392..d138d17 100644 --- a/src/parsers/pulldown.rs +++ b/src/parsers/pulldown.rs @@ -2,7 +2,7 @@ use std::ops::Range; -use crate::{elements::*, Alert, AlertBundle}; +use crate::{elements::*, Alert, AlertBundle, RenderMathFn}; use crate::{CommonMarkCache, CommonMarkOptions}; use egui::{self, Id, Pos2, TextStyle, Ui, Vec2}; @@ -16,10 +16,12 @@ pub struct ScrollableCache { split_points: Vec<(usize, Pos2, Pos2)>, } +pub type EventIteratorItem<'e> = (usize, (pulldown_cmark::Event<'e>, Range)); + /// Parse events until a desired end tag is reached or no more events are found. /// This is needed for multiple events that must be rendered inside a single widget fn delayed_events<'e>( - events: &mut impl Iterator, Range))>, + events: &mut impl Iterator>, end_at: pulldown_cmark::TagEnd, ) -> Vec<(pulldown_cmark::Event<'e>, Range)> { let mut curr_event = events.next(); @@ -40,6 +42,30 @@ fn delayed_events<'e>( } } +fn delayed_events_list_item<'e>( + events: &mut impl Iterator>, +) -> Vec<(pulldown_cmark::Event<'e>, Range)> { + let mut curr_event = events.next(); + let mut total_events = Vec::new(); + loop { + if let Some(event) = curr_event.take() { + total_events.push(event.1.clone()); + if let (_, (pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Item), _range)) = event { + return total_events; + } + + if let (_, (pulldown_cmark::Event::Start(pulldown_cmark::Tag::List(_)), _range)) = event + { + return total_events; + } + } else { + return total_events; + } + + curr_event = events.next(); + } +} + type Column<'e> = Vec<(pulldown_cmark::Event<'e>, Range)>; type Row<'e> = Vec>; @@ -74,9 +100,7 @@ fn parse_row<'e>( row } -fn parse_table<'e>( - events: &mut impl Iterator, Range))>, -) -> Table<'e> { +fn parse_table<'e>(events: &mut impl Iterator>) -> Table<'e> { let mut all_events = delayed_events(events, pulldown_cmark::TagEnd::Table) .into_iter() .peekable(); @@ -162,6 +186,32 @@ fn parse_alerts<'a>( } /// Supported pulldown_cmark options +#[inline] +#[cfg(feature = "math")] +fn parser_options() -> Options { + Options::ENABLE_TABLES + | Options::ENABLE_TASKLISTS + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_FOOTNOTES + | Options::ENABLE_MATH +} + +/// Supported pulldown_cmark options +#[inline] +#[cfg(not(feature = "math"))] +#[inline] +#[cfg(feature = "math")] +fn parser_options() -> Options { + Options::ENABLE_TABLES + | Options::ENABLE_TASKLISTS + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_FOOTNOTES + | Options::ENABLE_MATH +} + +/// Supported pulldown_cmark options +#[inline] +#[cfg(not(feature = "math"))] fn parser_options() -> Options { Options::ENABLE_TABLES | Options::ENABLE_TASKLISTS @@ -178,6 +228,7 @@ pub struct CommonMarkViewerInternal { image: Option, should_insert_newline: bool, fenced_code_block: Option, + is_list_item: bool, is_table: bool, is_blockquote: bool, checkbox_events: Vec, @@ -198,6 +249,7 @@ impl CommonMarkViewerInternal { link: None, image: None, should_insert_newline: true, + is_list_item: false, fenced_code_block: None, is_table: false, is_blockquote: false, @@ -215,6 +267,7 @@ impl CommonMarkViewerInternal { options: &CommonMarkOptions, text: &str, populate_split_points: bool, + math_fn: Option<&RenderMathFn>, ) -> (egui::InnerResponse<()>, Vec) { let max_width = options.max_width(ui); let layout = egui::Layout::left_to_right(egui::Align::BOTTOM).with_main_wrap(true); @@ -233,7 +286,16 @@ impl CommonMarkViewerInternal { let is_element_end = matches!(e, pulldown_cmark::Event::End(_)); let should_add_split_point = self.list.is_inside_a_list() && is_element_end; - self.process_event(ui, &mut events, e, src_span, cache, options, max_width); + self.process_event( + ui, + &mut events, + e, + src_span, + cache, + options, + max_width, + math_fn, + ); if populate_split_points && should_add_split_point { let scroll_cache = cache.scroll(&self.source_id); @@ -263,6 +325,7 @@ impl CommonMarkViewerInternal { cache: &mut CommonMarkCache, options: &CommonMarkOptions, text: &str, + math_fn: Option<&RenderMathFn>, ) { let available_size = ui.available_size(); let scroll_id = self.source_id.with("_scroll_area"); @@ -272,7 +335,7 @@ impl CommonMarkViewerInternal { .id_source(scroll_id) .auto_shrink([false, true]) .show(ui, |ui| { - self.show(ui, cache, options, text, true); + self.show(ui, cache, options, text, true, math_fn); }); // Prevent repopulating points twice at startup cache.scroll(&self.source_id).available_size = available_size; @@ -328,7 +391,16 @@ impl CommonMarkViewerInternal { .take(last_event_index - first_event_index); while let Some((_, (e, src_span))) = events.next() { - self.process_event(ui, &mut events, e, src_span, cache, options, max_width); + self.process_event( + ui, + &mut events, + e, + src_span, + cache, + options, + max_width, + math_fn, + ); } }); }); @@ -341,51 +413,99 @@ impl CommonMarkViewerInternal { scroll_cache.split_points.clear(); } } + #[allow(clippy::too_many_arguments)] fn process_event<'e>( &mut self, ui: &mut Ui, - events: &mut impl Iterator, Range))>, + events: &mut impl Iterator>, event: pulldown_cmark::Event, src_span: Range, cache: &mut CommonMarkCache, options: &CommonMarkOptions, max_width: f32, + math_fn: Option<&RenderMathFn>, ) { - self.event(ui, event, src_span, cache, options, max_width); - self.fenced_code_block(events, max_width, cache, options, ui); - self.table(events, cache, options, ui, max_width); - self.blockquote(events, max_width, cache, options, ui); + self.event(ui, event, src_span, cache, options, max_width, math_fn); + + self.item_list_wrapping(events, max_width, cache, options, ui, math_fn); + self.fenced_code_block(events, max_width, cache, options, ui, math_fn); + self.table(events, cache, options, ui, max_width, math_fn); + self.blockquote(events, max_width, cache, options, ui, math_fn); + } + + fn item_list_wrapping<'e>( + &mut self, + events: &mut impl Iterator>, + max_width: f32, + cache: &mut CommonMarkCache, + options: &CommonMarkOptions, + ui: &mut Ui, + math_fn: Option<&RenderMathFn>, + ) { + if self.is_list_item { + self.is_list_item = false; + + let item_events = delayed_events_list_item(events); + let mut events_iter = item_events.into_iter().enumerate(); + + // Required to ensure that the content of the list item is aligned with + // the * or - when wrapping + ui.horizontal_wrapped(|ui| { + while let Some((_, (e, src_span))) = events_iter.next() { + self.process_event( + ui, + &mut events_iter, + e, + src_span, + cache, + options, + max_width, + math_fn, + ); + } + }); + } + self.fenced_code_block(events, max_width, cache, options, ui, math_fn); + self.table(events, cache, options, ui, max_width, math_fn); + self.blockquote(events, max_width, cache, options, ui, math_fn); } fn blockquote<'e>( &mut self, - events: &mut impl Iterator, Range))>, + events: &mut impl Iterator>, max_width: f32, cache: &mut CommonMarkCache, options: &CommonMarkOptions, ui: &mut Ui, + math_fn: Option<&RenderMathFn>, ) { if self.is_blockquote { let mut collected_events = delayed_events(events, pulldown_cmark::TagEnd::BlockQuote); + if self.should_insert_newline { + newline(ui) + } + if let Some(alert) = parse_alerts(&options.alerts, &mut collected_events) { alert.ui(ui, |ui| { for (event, src_span) in collected_events.into_iter() { - self.event(ui, event, src_span, cache, options, max_width); + self.event(ui, event, src_span, cache, options, max_width, math_fn); } }) } else { blockquote(ui, ui.visuals().weak_text_color(), |ui| { self.text_style.quote = true; for (event, src_span) in collected_events { - self.event(ui, event, src_span, cache, options, max_width); + self.event(ui, event, src_span, cache, options, max_width, math_fn); } self.text_style.quote = false; }); } - newline(ui); + if self.should_insert_newline { + newline(ui); + } self.is_blockquote = false; } @@ -393,15 +513,16 @@ impl CommonMarkViewerInternal { fn fenced_code_block<'e>( &mut self, - events: &mut impl Iterator, Range))>, + events: &mut impl Iterator>, max_width: f32, cache: &mut CommonMarkCache, options: &CommonMarkOptions, ui: &mut Ui, + math_fn: Option<&RenderMathFn>, ) { while self.fenced_code_block.is_some() { if let Some((_, (e, src_span))) = events.next() { - self.event(ui, e, src_span, cache, options, max_width); + self.event(ui, e, src_span, cache, options, max_width, math_fn); } else { break; } @@ -410,11 +531,12 @@ impl CommonMarkViewerInternal { fn table<'e>( &mut self, - events: &mut impl Iterator, Range))>, + events: &mut impl Iterator>, cache: &mut CommonMarkCache, options: &CommonMarkOptions, ui: &mut Ui, max_width: f32, + math_fn: Option<&RenderMathFn>, ) { if self.is_table { newline(ui); @@ -430,7 +552,7 @@ impl CommonMarkViewerInternal { ui.horizontal(|ui| { for (e, src_span) in col { self.should_insert_newline = false; - self.event(ui, e, src_span, cache, options, max_width); + self.event(ui, e, src_span, cache, options, max_width, math_fn); } }); } @@ -442,7 +564,7 @@ impl CommonMarkViewerInternal { ui.horizontal(|ui| { for (e, src_span) in col { self.should_insert_newline = false; - self.event(ui, e, src_span, cache, options, max_width); + self.event(ui, e, src_span, cache, options, max_width, math_fn); } }); } @@ -458,6 +580,7 @@ impl CommonMarkViewerInternal { } } + #[allow(clippy::too_many_arguments)] fn event( &mut self, ui: &mut Ui, @@ -466,6 +589,7 @@ impl CommonMarkViewerInternal { cache: &mut CommonMarkCache, options: &CommonMarkOptions, max_width: f32, + math_fn: Option<&RenderMathFn>, ) { match event { pulldown_cmark::Event::Start(tag) => self.start_tag(ui, tag, options), @@ -508,6 +632,16 @@ impl CommonMarkViewerInternal { ui.add(ImmutableCheckbox::without_text(&mut checkbox)); } } + pulldown_cmark::Event::InlineMath(tex) => { + if let Some(math_fn) = math_fn { + math_fn(ui, &tex, true); + } + } + pulldown_cmark::Event::DisplayMath(tex) => { + if let Some(math_fn) = math_fn { + math_fn(ui, &tex, false); + } + } } } @@ -543,7 +677,7 @@ impl CommonMarkViewerInternal { HeadingLevel::H6 => 5, }); } - pulldown_cmark::Tag::BlockQuote => { + pulldown_cmark::Tag::BlockQuote(_) => { self.is_blockquote = true; } pulldown_cmark::Tag::CodeBlock(c) => { @@ -553,7 +687,9 @@ impl CommonMarkViewerInternal { content: "".to_string(), }); - newline(ui); + if self.should_insert_newline { + newline(ui); + } } self.text_style.code = true; @@ -566,6 +702,7 @@ impl CommonMarkViewerInternal { } } pulldown_cmark::Tag::Item => { + self.is_list_item = true; self.should_insert_newline = false; self.list.start_item(ui, options); } @@ -625,7 +762,7 @@ impl CommonMarkViewerInternal { pulldown_cmark::TagEnd::List(_) => { self.list.end_level(ui); - if self.list.is_inside_a_list() { + if !self.list.is_inside_a_list() { self.should_insert_newline = true; } } @@ -672,6 +809,9 @@ impl CommonMarkViewerInternal { if let Some(block) = self.fenced_code_block.take() { block.end(ui, cache, options, max_width); self.text_style.code = false; + if self.should_insert_newline { + newline(ui); + } } } }