use std::iter::Peekable;
use std::ops::Range;
use crate::{CommonMarkCache, CommonMarkOptions};
use egui::{self, Id, Pos2, TextStyle, Ui};
use crate::List;
use egui_commonmark_backend_extended::elements::{
blockquote, footnote, footnote_start, heading_end_spacing, heading_start_spacing, newline,
paragraph_end_spacing, rule, soft_break, ImmutableCheckbox,
};
use egui_commonmark_backend_extended::misc::*;
use egui_commonmark_backend_extended::pulldown::*;
use pulldown_cmark::{CowStr, HeadingLevel};
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum HighlightKind {
None,
Match,
Active,
}
impl HighlightKind {
fn background_color(self, ui: &Ui) -> Option<egui::Color32> {
let dark = ui.style().visuals.dark_mode;
match self {
HighlightKind::None => None,
HighlightKind::Match => Some(if dark {
egui::Color32::from_rgb(102, 92, 46)
} else {
egui::Color32::from_rgb(255, 229, 127)
}),
HighlightKind::Active => Some(if dark {
egui::Color32::from_rgb(156, 107, 26)
} else {
egui::Color32::from_rgb(255, 167, 38)
}),
}
}
}
fn compute_highlight_segments(
text: &str,
span: &Range<usize>,
ranges: &[Range<usize>],
active: Option<&Range<usize>>,
) -> Vec<(String, HighlightKind)> {
let mut overlaps: Vec<(usize, usize, HighlightKind)> = Vec::new();
for r in ranges {
let start = r.start.max(span.start);
let end = r.end.min(span.end);
if start >= end {
continue;
}
let local_start = start - span.start;
let local_end = end - span.start;
let is_active = active.map(|a| a == r).unwrap_or(false);
let kind = if is_active {
HighlightKind::Active
} else {
HighlightKind::Match
};
overlaps.push((local_start, local_end, kind));
}
if overlaps.is_empty() {
return vec![(text.to_string(), HighlightKind::None)];
}
let mut segments = Vec::new();
let mut cursor = 0;
for (start, end, kind) in overlaps {
if start > cursor
&& text.is_char_boundary(cursor)
&& text.is_char_boundary(start)
{
segments.push((text[cursor..start].to_string(), HighlightKind::None));
}
if text.is_char_boundary(start) && text.is_char_boundary(end) {
segments.push((text[start..end].to_string(), kind));
}
cursor = end;
}
if cursor < text.len() && text.is_char_boundary(cursor) {
segments.push((text[cursor..].to_string(), HighlightKind::None));
}
segments
}
fn inline_code_wrap_segments(text: &str) -> Vec<String> {
const MAX_SEGMENT_CHARS: usize = 56;
if text.chars().count() <= MAX_SEGMENT_CHARS {
return vec![text.to_owned()];
}
let mut segments = Vec::new();
let mut current = String::with_capacity(MAX_SEGMENT_CHARS * 4);
let mut current_len = 0;
for ch in text.chars() {
current.push(ch);
current_len += 1;
if current_len >= MAX_SEGMENT_CHARS {
segments.push(std::mem::take(&mut current));
current_len = 0;
}
}
if !current.is_empty() {
segments.push(current);
}
segments
}
fn cell_visual_lines(cell: &[(pulldown_cmark::Event, Range<usize>)]) -> usize {
let mut max_lines = 1usize;
for (event, _) in cell {
if let pulldown_cmark::Event::Code(text) = event {
let chunks = inline_code_wrap_segments(text).len();
if chunks > max_lines {
max_lines = chunks;
}
}
}
max_lines
}
fn html_cell_visual_lines(cell: &str) -> usize {
let explicit_lines = cell.lines().count().max(1);
let wrap_est = cell.len().saturating_sub(1) / 60;
explicit_lines.saturating_add(wrap_est).max(1)
}
fn forward_shift_wheel_to_horizontal_scroll<R>(
ui: &Ui,
out: &mut egui::containers::scroll_area::ScrollAreaOutput<R>,
) {
if !ui.rect_contains_pointer(out.inner_rect) {
return;
}
if !ui.ctx().input(|i| i.modifiers.shift) {
return;
}
let dy = ui.ctx().input(|i| i.smooth_scroll_delta.y);
if dy.abs() < 0.1 {
return;
}
let max_x = (out.content_size.x - out.inner_rect.width()).max(0.0);
if max_x <= 0.0 {
return;
}
let at_left = out.state.offset.x <= 0.0 && dy > 0.0;
let at_right = out.state.offset.x >= max_x && dy < 0.0;
if at_left || at_right {
return;
}
let new_x = (out.state.offset.x - dy).clamp(0.0, max_x);
if (new_x - out.state.offset.x).abs() > f32::EPSILON {
out.state.offset.x = new_x;
out.state.store(ui.ctx(), out.id);
ui.ctx().input_mut(|i| i.smooth_scroll_delta.y = 0.0);
ui.ctx().request_repaint();
}
}
struct Newline {
should_not_start_newline_forced: bool,
should_start_newline: bool,
should_end_newline: bool,
should_end_newline_forced: bool,
}
impl Default for Newline {
fn default() -> Self {
Self {
should_not_start_newline_forced: true,
should_start_newline: true,
should_end_newline: true,
should_end_newline_forced: true,
}
}
}
impl Newline {
pub fn can_insert_end(&self) -> bool {
self.should_end_newline && self.should_end_newline_forced
}
pub fn can_insert_start(&self) -> bool {
self.should_start_newline && !self.should_not_start_newline_forced
}
pub fn try_insert_start(&self, ui: &mut Ui) {
if self.can_insert_start() {
newline(ui);
}
}
pub fn try_insert_end(&self, ui: &mut Ui) {
if self.can_insert_end() {
newline(ui);
}
}
}
#[derive(Default)]
struct DefinitionList {
is_first_item: bool,
is_def_list_def: bool,
}
pub struct CommonMarkViewerInternal {
curr_table: usize,
curr_code_block: usize,
text_style: Style,
list: List,
link: Option<Link>,
image: Option<Image>,
line: Newline,
code_block: Option<CodeBlock>,
html_block: String,
is_list_item: bool,
def_list: DefinitionList,
is_table: bool,
is_blockquote: bool,
checkbox_events: Vec<CheckboxClickEvent>,
current_heading_y: Option<f32>,
current_heading_text: String,
current_heading_rich_texts: Vec<egui::RichText>,
heading_occurrence_counts: std::collections::HashMap<String, usize>,
}
pub(crate) struct CheckboxClickEvent {
pub(crate) checked: bool,
pub(crate) span: Range<usize>,
}
impl CommonMarkViewerInternal {
pub fn new() -> Self {
Self {
curr_table: 0,
curr_code_block: 0,
text_style: Style::default(),
list: List::default(),
link: None,
image: None,
line: Newline::default(),
is_list_item: false,
def_list: Default::default(),
code_block: None,
html_block: String::new(),
is_table: false,
is_blockquote: false,
checkbox_events: Vec::new(),
current_heading_y: None,
current_heading_text: String::new(),
current_heading_rich_texts: Vec::new(),
heading_occurrence_counts: std::collections::HashMap::new(),
}
}
}
fn parser_options_math(is_math_enabled: bool) -> pulldown_cmark::Options {
if is_math_enabled {
parser_options() | pulldown_cmark::Options::ENABLE_MATH
} else {
parser_options()
}
}
fn compute_layout_signature(ui: &egui::Ui, options: &CommonMarkOptions) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(ui.available_width().round() as i32).hash(&mut h);
((ui.text_style_height(&egui::TextStyle::Body) * 10.0).round() as i32).hash(&mut h);
((ui.text_style_height(&egui::TextStyle::Monospace) * 10.0).round() as i32).hash(&mut h);
ui.style().visuals.dark_mode.hash(&mut h);
options.default_width.hash(&mut h);
options.indentation_spaces.hash(&mut h);
h.finish()
}
const CONTENT_H_DRIFT_THRESHOLD: f32 = 1024.0;
fn is_block_end_tag(tag: &pulldown_cmark::TagEnd) -> bool {
use pulldown_cmark::TagEnd;
matches!(
tag,
TagEnd::Paragraph
| TagEnd::Heading(_)
| TagEnd::BlockQuote(_)
| TagEnd::CodeBlock
| TagEnd::List(_)
| TagEnd::Item
| TagEnd::FootnoteDefinition
| TagEnd::Table
| TagEnd::HtmlBlock
| TagEnd::MetadataBlock(_)
| TagEnd::DefinitionList
| TagEnd::DefinitionListTitle
| TagEnd::DefinitionListDefinition
)
}
fn is_likely_currency(tex: &str) -> bool {
let trimmed = tex.trim();
if trimmed.is_empty() {
return false;
}
let has_backslash_cmd = trimmed.contains('\\');
if has_backslash_cmd {
return false;
}
let has_braces = trimmed.contains('{') || trimmed.contains('}');
if has_braces {
return false;
}
if trimmed.contains(|c: char| matches!(c, '=' | '<' | '>' | '(' | ')' | '[' | ']')) {
return false;
}
let mut leading = trimmed.chars();
if matches!(leading.next(), Some('+' | '-'))
&& leading.next().is_some_and(|c| c.is_ascii_digit())
{
return false;
}
if trimmed.len() <= 3 && trimmed.chars().all(|c| c.is_ascii_alphabetic()) {
return false;
}
if trimmed.chars().all(|c| c.is_ascii_digit() || c == '.' || c == ',')
&& trimmed.chars().any(|c| c.is_ascii_digit())
{
return false;
}
let has_sub_super = trimmed.contains('^') || trimmed.contains('_');
if has_sub_super {
let word_count = trimmed.split_whitespace().count();
if word_count > 5 {
return true; }
return false;
}
true
}
impl CommonMarkViewerInternal {
fn hash_content(text: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
pub(crate) fn show(
&mut self,
ui: &mut egui::Ui,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
text: &str,
split_points_id: Option<Id>,
) -> (egui::InnerResponse<()>, Vec<CheckboxClickEvent>) {
let max_width = options.max_width(ui);
let layout = egui::Layout::left_to_right(egui::Align::BOTTOM).with_main_wrap(true);
let content_hash = Self::hash_content(text);
if cache.get_cached_events(content_hash).is_none() {
let math_enabled = options.math_fn.is_some() || cfg!(feature = "math");
let owned_events: Vec<(pulldown_cmark::Event<'static>, Range<usize>)> =
pulldown_cmark::Parser::new_ext(text, parser_options_math(math_enabled))
.into_offset_iter()
.map(|(event, range)| (event.into_static(), range))
.collect();
cache.set_cached_events(content_hash, owned_events);
}
let re = ui.allocate_ui_with_layout(egui::vec2(max_width, 0.0), layout, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
let height = ui.text_style_height(&TextStyle::Body);
ui.set_row_height(height);
let events_data = cache.get_cached_events(content_hash)
.expect("events just cached")
.to_vec();
let mut events = events_data
.into_iter()
.enumerate()
.peekable();
while let Some((index, (e, src_span))) = events.next() {
let start_position = ui.next_widget_position();
let is_block_end = matches!(
&e,
pulldown_cmark::Event::End(end) if is_block_end_tag(end)
);
if events.peek().is_none() {
self.line.should_end_newline_forced = false;
}
self.process_event(ui, &mut events, e, src_span, cache, options, max_width);
let safe_for_split = is_block_end
&& !self.list.is_inside_a_list()
&& !self.is_table
&& !self.is_blockquote;
if let Some(source_id) = split_points_id {
if safe_for_split {
let scroll_cache = scroll_cache(cache, &source_id);
let end_position = ui.next_widget_position();
let split_point_exists = scroll_cache
.split_points
.iter()
.any(|(i, _, _)| *i == index);
if !split_point_exists {
scroll_cache
.split_points
.push((index, start_position, end_position));
}
}
}
if index == 0 {
self.line.should_not_start_newline_forced = false;
}
}
if let Some(source_id) = split_points_id {
scroll_cache(cache, &source_id).page_size =
Some(ui.next_widget_position().to_vec2());
}
});
(re, std::mem::take(&mut self.checkbox_events))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn show_scrollable(
&mut self,
source_id: Id,
ui: &mut egui::Ui,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
text: &str,
content_version: Option<u64>,
pending_scroll_offset: Option<f32>,
scroll_source: Option<egui::scroll_area::ScrollSource>,
) -> egui::scroll_area::ScrollAreaOutput<()> {
let available_size = ui.available_size();
let scroll_id = source_id.with("_scroll_area");
let layout_sig = compute_layout_signature(ui, options);
let version = content_version.unwrap_or_else(|| Self::hash_content(text));
let mut content_changed = false;
{
let sc = scroll_cache(cache, &source_id);
if sc.events.is_empty() || sc.content_version != version {
content_changed = true;
let math_enabled =
options.math_fn.is_some() || cfg!(feature = "math");
sc.events = pulldown_cmark::Parser::new_ext(
text,
parser_options_math(math_enabled),
)
.into_offset_iter()
.map(|(e, r)| (e.into_static(), r))
.collect();
sc.content_version = version;
sc.page_size = None;
sc.split_points.clear();
}
if sc.layout_signature != layout_sig {
sc.layout_signature = layout_sig;
sc.page_size = None;
sc.split_points.clear();
sc.available_size = available_size;
}
if pending_scroll_offset.is_some() {
sc.page_size = None;
}
if sc.bootstrap_content_h > 0.0
&& (sc.last_content_h - sc.bootstrap_content_h).abs() > CONTENT_H_DRIFT_THRESHOLD
{
sc.page_size = None;
sc.split_points.clear();
}
}
if content_changed {
cache.clear_header_positions();
}
let make_scroll_area = || {
let mut sa = egui::ScrollArea::vertical()
.id_salt(scroll_id)
.auto_shrink([false, true]);
if let Some(offset) = pending_scroll_offset {
sa = sa.vertical_scroll_offset(offset);
}
if let Some(src) = scroll_source {
sa = sa.scroll_source(src);
}
sa
};
{
let out = make_scroll_area().show(ui, |ui| {
cache.set_scroll_offset(pending_scroll_offset.unwrap_or(0.0));
self.show(ui, cache, options, text, Some(source_id));
});
let sc = scroll_cache(cache, &source_id);
sc.available_size = available_size;
sc.last_content_h = out.content_size.y;
sc.bootstrap_content_h = out.content_size.y;
return out;
}
#[allow(unreachable_code)]
let page_size_opt = scroll_cache(cache, &source_id).page_size;
#[allow(unreachable_code)]
let Some(page_size) = page_size_opt else {
unreachable!()
};
let num_rows = scroll_cache(cache, &source_id).events.len();
let out = make_scroll_area()
.show_viewport(ui, |ui, viewport| {
ui.set_height(page_size.y);
cache.set_scroll_offset(viewport.min.y);
let layout = egui::Layout::left_to_right(egui::Align::BOTTOM).with_main_wrap(true);
let max_width = options.max_width(ui);
ui.allocate_ui_with_layout(egui::vec2(max_width, 0.0), layout, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
let scroll_cache = scroll_cache(cache, &source_id);
let above = scroll_cache
.split_points
.partition_point(|(_, _, end)| end.y < viewport.min.y);
let (first_event_index, _, first_end_position) = if above >= 2 {
scroll_cache.split_points[above - 2]
} else {
(0, Pos2::ZERO, Pos2::ZERO)
};
let below = scroll_cache
.split_points
.partition_point(|(_, start, _)| start.y <= viewport.max.y);
let last_event_index = scroll_cache
.split_points
.get(below + 1)
.map(|(index, _, _)| *index)
.unwrap_or(num_rows);
let range_end = last_event_index.min(scroll_cache.events.len());
let events_range: Vec<(pulldown_cmark::Event<'static>, Range<usize>)> =
if first_event_index < range_end {
scroll_cache.events[first_event_index..range_end].to_vec()
} else {
Vec::new()
};
let last_sp_y_used = scroll_cache
.split_points
.get(below + 1)
.map(|p| p.2.y)
.unwrap_or(0.0);
eprintln!(
"[SKIP] vp=[{:.0},{:.0}] evt=[{},{}]/{} sp_y=[{:.0},{:.0}] a={} b={}",
viewport.min.y, viewport.max.y,
first_event_index, last_event_index, num_rows,
first_end_position.y, last_sp_y_used,
above, below
);
ui.allocate_space(egui::vec2(0.0, first_end_position.y));
let mut events = events_range
.into_iter()
.enumerate()
.map(|(offset, ev)| (offset + first_event_index, ev))
.peekable();
while let Some((i, (e, src_span))) = events.next() {
if events.peek().is_none() {
self.line.should_end_newline_forced = false;
}
self.process_event(ui, &mut events, e, src_span, cache, options, max_width);
if i == 0 {
self.line.should_not_start_newline_forced = false;
}
}
});
});
let real_max_scroll = (page_size.y - out.inner_rect.height()).max(0.0);
let clamped = out.state.offset.y > real_max_scroll;
if clamped {
let mut state = out.state;
state.offset.y = real_max_scroll;
state.store(ui.ctx(), out.id);
ui.ctx().request_repaint();
}
let _ = clamped;
out
}
#[allow(clippy::too_many_arguments)]
fn process_event<'e>(
&mut self,
ui: &mut Ui,
events: &mut Peekable<impl Iterator<Item = EventIteratorItem<'e>>>,
event: pulldown_cmark::Event,
src_span: Range<usize>,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
max_width: f32,
) {
self.event(ui, event, src_span, cache, options, max_width);
self.def_list_def_wrapping(events, max_width, cache, options, ui);
self.item_list_wrapping(events, max_width, cache, options, ui);
self.table(events, cache, options, ui, max_width);
self.blockquote(events, max_width, cache, options, ui);
}
fn def_list_def_wrapping<'e>(
&mut self,
events: &mut Peekable<impl Iterator<Item = EventIteratorItem<'e>>>,
max_width: f32,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
ui: &mut Ui,
) {
if self.def_list.is_def_list_def {
self.def_list.is_def_list_def = false;
let item_events = delayed_events(events, |tag| {
matches!(tag, pulldown_cmark::TagEnd::DefinitionListDefinition)
});
let mut events_iter = item_events.into_iter().enumerate().peekable();
self.line.try_insert_start(ui);
self.line.should_start_newline = false;
if let Some((_, (e, src_span))) = events_iter.next() {
self.process_event(ui, &mut events_iter, e, src_span, cache, options, max_width);
}
ui.label(" ".repeat(options.indentation_spaces));
self.line.should_start_newline = true;
self.line.should_end_newline = false;
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,
);
}
});
self.line.should_end_newline = true;
if !matches!(
events.peek(),
Some((
_,
(
pulldown_cmark::Event::End(pulldown_cmark::TagEnd::DefinitionList),
_
)
))
) {
self.line.try_insert_end(ui);
}
}
}
fn item_list_wrapping<'e>(
&mut self,
events: &mut impl Iterator<Item = EventIteratorItem<'e>>,
max_width: f32,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
ui: &mut Ui,
) {
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().peekable();
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,
);
}
});
}
}
fn blockquote<'e>(
&mut self,
events: &mut Peekable<impl Iterator<Item = EventIteratorItem<'e>>>,
max_width: f32,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
ui: &mut Ui,
) {
if self.is_blockquote {
let mut collected_events = delayed_events(events, |tag| {
matches!(tag, pulldown_cmark::TagEnd::BlockQuote(_))
});
self.line.try_insert_start(ui);
self.line.should_not_start_newline_forced = false;
if let Some(alert) = parse_alerts(&options.alerts, &mut collected_events) {
egui_commonmark_backend_extended::alert_ui(alert, ui, |ui| {
for (event, src_span) in collected_events {
self.event(ui, event, src_span, cache, options, max_width);
}
})
} 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.text_style.quote = false;
});
}
if events.peek().is_none() {
self.line.should_end_newline_forced = false;
}
self.line.try_insert_end(ui);
self.is_blockquote = false;
}
}
fn table<'e>(
&mut self,
events: &mut Peekable<impl Iterator<Item = EventIteratorItem<'e>>>,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
ui: &mut Ui,
max_width: f32,
) {
if self.is_table {
self.line.try_insert_start(ui);
let id = ui.id().with("_table").with(self.curr_table);
self.curr_table += 1;
let Table { header, rows } = parse_table(events);
let rows: Vec<_> = rows.into_iter().filter(|r| !r.is_empty()).collect();
let num_cols = if !header.is_empty() {
header.len()
} else {
rows.first().map(|r| r.len()).unwrap_or(0)
};
let line_h = ui.text_style_height(&egui::TextStyle::Body);
if num_cols == 0 {
self.is_table = false;
if events.peek().is_none() {
self.line.should_end_newline_forced = false;
}
self.line.try_insert_end(ui);
return;
}
let cell_h = line_h * 1.5;
let header_lines = header
.iter()
.map(|c| cell_visual_lines(c))
.max()
.unwrap_or(1);
let header_h = cell_h * header_lines as f32;
let body_heights: Vec<f32> = rows
.iter()
.map(|row| {
let max_lines = row
.iter()
.map(|c| cell_visual_lines(c))
.max()
.unwrap_or(1);
cell_h * max_lines as f32
})
.collect();
let mut scroll_out = egui::ScrollArea::horizontal()
.id_salt(id.with("_scroll"))
.max_width(max_width)
.auto_shrink([false, true])
.show(ui, |ui| {
ui.vertical(|ui| {
egui::Frame::group(ui.style()).show(ui, |ui| {
let table = egui_extras::TableBuilder::new(ui)
.id_salt(id)
.striped(true)
.resizable(true)
.vscroll(false)
.auto_shrink([false, true])
.min_scrolled_height(0.0)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.columns(
egui_extras::Column::auto().resizable(true).at_least(40.0),
num_cols,
)
.header(header_h, |mut row| {
for col in header {
row.col(|ui| {
let col_w = ui.available_width();
for (e, src_span) in col {
let tmp_start = std::mem::replace(
&mut self.line.should_start_newline,
false,
);
let tmp_end = std::mem::replace(
&mut self.line.should_end_newline,
false,
);
self.event(
ui, e, src_span, cache, options, col_w,
);
self.line.should_start_newline = tmp_start;
self.line.should_end_newline = tmp_end;
}
});
}
});
table.body(|mut body| {
for (row_idx, row) in rows.into_iter().enumerate() {
let h = body_heights
.get(row_idx)
.copied()
.unwrap_or(cell_h);
body.row(h, |mut row_ui| {
for col in row {
row_ui.col(|ui| {
let col_w = ui.available_width();
for (e, src_span) in col {
let tmp_start = std::mem::replace(
&mut self.line.should_start_newline,
false,
);
let tmp_end = std::mem::replace(
&mut self.line.should_end_newline,
false,
);
self.event(
ui, e, src_span, cache, options, col_w,
);
self.line.should_start_newline = tmp_start;
self.line.should_end_newline = tmp_end;
}
});
}
});
}
});
});
});
});
forward_shift_wheel_to_horizontal_scroll(ui, &mut scroll_out);
self.is_table = false;
if events.peek().is_none() {
self.line.should_end_newline_forced = false;
}
self.line.try_insert_end(ui);
}
}
fn event(
&mut self,
ui: &mut Ui,
event: pulldown_cmark::Event,
src_span: Range<usize>,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
max_width: f32,
) {
match event {
pulldown_cmark::Event::Start(tag) => self.start_tag(ui, tag, options),
pulldown_cmark::Event::End(tag) => self.end_tag(ui, tag, cache, options, max_width),
pulldown_cmark::Event::Text(text) => {
self.event_text_with_highlights(text, &src_span, cache, ui, options);
}
pulldown_cmark::Event::Code(text) => {
self.text_style.code = true;
let segments = inline_code_wrap_segments(&text);
let wrap = segments.len() > 1;
let interior_span = if !wrap && src_span.len() >= text.len() {
let delim_total = src_span.len() - text.len();
if delim_total > 0 && delim_total % 2 == 0 {
let bt = delim_total / 2;
Some((src_span.start + bt)..(src_span.end - bt))
} else {
None
}
} else {
None
};
for segment in segments {
if let Some(ref span) = interior_span {
self.event_text_with_highlights(
segment.into(),
span,
cache,
ui,
options,
);
} else {
self.event_text(segment.into(), ui, options);
}
if wrap {
ui.end_row();
}
}
self.text_style.code = false;
}
pulldown_cmark::Event::InlineHtml(text) => {
self.event_text(text, ui, options);
}
pulldown_cmark::Event::Html(text) => {
self.html_block.push_str(&text);
}
pulldown_cmark::Event::FootnoteReference(footnote) => {
footnote_start(ui, &footnote);
}
pulldown_cmark::Event::SoftBreak => {
soft_break(ui);
}
pulldown_cmark::Event::HardBreak => newline(ui),
pulldown_cmark::Event::Rule => {
self.line.try_insert_start(ui);
rule(ui, self.line.can_insert_end());
}
pulldown_cmark::Event::TaskListMarker(mut checkbox) => {
if options.mutable {
if ui
.add(egui::Checkbox::without_text(&mut checkbox))
.clicked()
{
self.checkbox_events.push(CheckboxClickEvent {
checked: checkbox,
span: src_span,
});
}
} else {
ui.add(ImmutableCheckbox::without_text(&mut checkbox));
}
}
pulldown_cmark::Event::InlineMath(tex) => {
if is_likely_currency(&tex) {
let text: CowStr = format!("${tex}").into();
self.event_text(text, ui, options);
} else {
#[cfg(feature = "math")]
{
crate::render_math(ui, cache, &tex, true);
}
#[cfg(not(feature = "math"))]
if let Some(math_fn) = options.math_fn {
math_fn(ui, &tex, true);
}
}
}
pulldown_cmark::Event::DisplayMath(tex) => {
newline(ui);
#[cfg(feature = "math")]
{
crate::render_math(ui, cache, &tex, false);
}
#[cfg(not(feature = "math"))]
if let Some(math_fn) = options.math_fn {
math_fn(ui, &tex, false);
}
newline(ui);
}
}
}
fn event_text(&mut self, text: CowStr, ui: &mut Ui, options: &CommonMarkOptions) {
self.emit_text(text, HighlightKind::None, ui, options);
}
fn emit_text(
&mut self,
text: CowStr,
hl: HighlightKind,
ui: &mut Ui,
options: &CommonMarkOptions,
) {
let bg = hl.background_color(ui);
let mut rich_text = if bg.is_some() && self.text_style.code {
let mut t = egui::RichText::new(text.as_ref())
.text_style(egui::TextStyle::Monospace);
if self.text_style.strong {
t = t.strong();
}
if self.text_style.emphasis {
t = t.italics();
}
if self.text_style.strikethrough {
t = t.strikethrough();
}
if self.text_style.quote {
t = t.weak();
}
t
} else {
self.text_style
.to_richtext_with_typography(ui, &text, Some(&options.typography))
};
if let Some(bg) = bg {
rich_text = rich_text.background_color(bg);
}
if let Some(image) = &mut self.image {
image.alt_text.push(rich_text);
} else if let Some(block) = &mut self.code_block {
block.content.push_str(&text);
} else if let Some(link) = &mut self.link {
link.text.push(rich_text);
} else if self.text_style.heading.is_some() {
self.current_heading_text.push_str(&text);
self.current_heading_rich_texts.push(rich_text);
} else {
ui.label(rich_text);
}
}
fn event_text_with_highlights(
&mut self,
text: CowStr,
span: &Range<usize>,
cache: &mut CommonMarkCache,
ui: &mut Ui,
options: &CommonMarkOptions,
) {
let ranges = cache.search_ranges();
if ranges.is_empty() || text.len() != span.len() {
self.event_text(text, ui, options);
return;
}
let segments =
compute_highlight_segments(&text, span, ranges, cache.active_search_range());
for (segment_text, hl) in segments {
if hl == HighlightKind::Active {
let vy = ui.cursor().top();
cache.record_active_search_y_viewport(vy);
}
self.emit_text(segment_text.into(), hl, ui, options);
}
}
fn start_tag(&mut self, ui: &mut Ui, tag: pulldown_cmark::Tag, options: &CommonMarkOptions) {
match tag {
pulldown_cmark::Tag::Paragraph => {
self.line.try_insert_start(ui);
}
pulldown_cmark::Tag::Heading { level, .. } => {
ui.end_row();
self.current_heading_y = Some(ui.cursor().top());
self.current_heading_text.clear();
heading_start_spacing(ui, &options.typography);
self.text_style.heading = Some(match level {
HeadingLevel::H1 => 0,
HeadingLevel::H2 => 1,
HeadingLevel::H3 => 2,
HeadingLevel::H4 => 3,
HeadingLevel::H5 => 4,
HeadingLevel::H6 => 5,
});
}
pulldown_cmark::Tag::BlockQuote(_) => {
self.is_blockquote = true;
}
pulldown_cmark::Tag::CodeBlock(c) => {
match c {
pulldown_cmark::CodeBlockKind::Fenced(lang) => {
self.code_block = Some(crate::CodeBlock {
lang: Some(lang.to_string()),
content: "".to_string(),
});
}
pulldown_cmark::CodeBlockKind::Indented => {
self.code_block = Some(crate::CodeBlock {
lang: None,
content: "".to_string(),
});
}
}
self.line.try_insert_start(ui);
}
pulldown_cmark::Tag::List(point) => {
if !self.list.is_inside_a_list() && self.line.can_insert_start() {
newline(ui);
}
if let Some(number) = point {
self.list.start_level_with_number(number);
} else {
self.list.start_level_without_number();
}
self.line.should_start_newline = false;
self.line.should_end_newline = false;
}
pulldown_cmark::Tag::Item => {
self.is_list_item = true;
self.list.start_item(ui, options);
}
pulldown_cmark::Tag::FootnoteDefinition(note) => {
self.line.try_insert_start(ui);
self.line.should_start_newline = false;
self.line.should_end_newline = false;
footnote(ui, ¬e);
}
pulldown_cmark::Tag::Table(_) => {
self.is_table = true;
}
pulldown_cmark::Tag::TableHead => {}
pulldown_cmark::Tag::TableRow => {}
pulldown_cmark::Tag::TableCell => {}
pulldown_cmark::Tag::Emphasis => {
self.text_style.emphasis = true;
}
pulldown_cmark::Tag::Strong => {
self.text_style.strong = true;
}
pulldown_cmark::Tag::Strikethrough => {
self.text_style.strikethrough = true;
}
pulldown_cmark::Tag::Link { dest_url, .. } => {
self.link = Some(crate::Link {
destination: dest_url.to_string(),
text: Vec::new(),
});
}
pulldown_cmark::Tag::Image { dest_url, .. } => {
self.image = Some(crate::Image::new(&dest_url, options));
}
pulldown_cmark::Tag::HtmlBlock => {
self.line.try_insert_start(ui);
}
pulldown_cmark::Tag::MetadataBlock(_) => {}
pulldown_cmark::Tag::DefinitionList => {
self.line.try_insert_start(ui);
self.def_list.is_first_item = true;
}
pulldown_cmark::Tag::DefinitionListTitle => {
if !self.def_list.is_first_item {
self.line.try_insert_start(ui)
} else {
self.def_list.is_first_item = false;
}
}
pulldown_cmark::Tag::DefinitionListDefinition => {
self.def_list.is_def_list_def = true;
}
pulldown_cmark::Tag::Superscript | pulldown_cmark::Tag::Subscript => {}
}
}
fn end_tag(
&mut self,
ui: &mut Ui,
tag: pulldown_cmark::TagEnd,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
max_width: f32,
) {
match tag {
pulldown_cmark::TagEnd::Paragraph => {
self.line.try_insert_end(ui);
paragraph_end_spacing(ui, &options.typography);
}
pulldown_cmark::TagEnd::Heading { .. } => {
if !self.current_heading_rich_texts.is_empty() {
let available = ui.available_rect_before_wrap();
let left_edge = ui.min_rect().left();
let heading_rect = egui::Rect::from_min_size(
egui::pos2(left_edge, available.top()),
egui::vec2(available.width() + (available.left() - left_edge), available.height()),
);
let rich_texts = std::mem::take(&mut self.current_heading_rich_texts);
ui.allocate_ui_at_rect(heading_rect, |ui| {
for rt in rich_texts {
ui.label(rt);
}
});
}
if let Some(y) = self.current_heading_y.take() {
if !self.current_heading_text.is_empty() {
let normalized = self.current_heading_text.trim().to_lowercase();
let nth = self
.heading_occurrence_counts
.entry(normalized.clone())
.or_insert(0);
let key = if *nth == 0 {
normalized.clone()
} else {
format!("{normalized}#{nth}")
};
*nth += 1;
let content_y = y - ui.min_rect().top();
cache.record_header_content_y(&key, content_y);
}
}
self.current_heading_text.clear();
heading_end_spacing(ui, &options.typography);
self.line.try_insert_end(ui);
self.text_style.heading = None;
}
pulldown_cmark::TagEnd::BlockQuote(_) => {}
pulldown_cmark::TagEnd::CodeBlock => {
self.end_code_block(ui, cache, options, max_width);
}
pulldown_cmark::TagEnd::List(_) => {
if self.list.is_last_level() {
self.line.should_start_newline = true;
self.line.should_end_newline = true;
}
self.list.end_level(ui, self.line.can_insert_end());
if !self.list.is_inside_a_list() {
self.list = List::default();
}
}
pulldown_cmark::TagEnd::Item => {}
pulldown_cmark::TagEnd::FootnoteDefinition => {
self.line.should_start_newline = true;
self.line.should_end_newline = true;
self.line.try_insert_end(ui);
}
pulldown_cmark::TagEnd::Table => {}
pulldown_cmark::TagEnd::TableHead => {}
pulldown_cmark::TagEnd::TableRow => {}
pulldown_cmark::TagEnd::TableCell => {
ui.label(" ");
}
pulldown_cmark::TagEnd::Emphasis => {
self.text_style.emphasis = false;
}
pulldown_cmark::TagEnd::Strong => {
self.text_style.strong = false;
}
pulldown_cmark::TagEnd::Strikethrough => {
self.text_style.strikethrough = false;
}
pulldown_cmark::TagEnd::Link => {
if let Some(link) = self.link.take() {
link.end(ui, cache);
}
}
pulldown_cmark::TagEnd::Image => {
if let Some(image) = self.image.take() {
image.end(ui, cache, options);
}
}
pulldown_cmark::TagEnd::HtmlBlock => {
if !self.html_block.is_empty() {
if let Some(table) = egui_commonmark_backend_extended::html_table::parse_html_table(&self.html_block) {
self.render_html_table(ui, &table, options, max_width);
} else if let Some(html_fn) = options.html_fn {
html_fn(ui, &self.html_block);
} else {
let text: pulldown_cmark::CowStr = std::mem::take(&mut self.html_block).into();
self.event_text(text, ui, options);
}
self.html_block.clear();
}
}
pulldown_cmark::TagEnd::MetadataBlock(_) => {}
pulldown_cmark::TagEnd::DefinitionList => self.line.try_insert_end(ui),
pulldown_cmark::TagEnd::DefinitionListTitle
| pulldown_cmark::TagEnd::DefinitionListDefinition => {}
pulldown_cmark::TagEnd::Superscript | pulldown_cmark::TagEnd::Subscript => {}
}
}
fn end_code_block(
&mut self,
ui: &mut Ui,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
max_width: f32,
) {
if let Some(block) = self.code_block.take() {
let id = ui.id().with("_code_block").with(self.curr_code_block);
self.curr_code_block += 1;
block.end(ui, cache, options, max_width, id);
self.line.try_insert_end(ui);
}
}
fn render_html_table(
&mut self,
ui: &mut Ui,
table: &egui_commonmark_backend_extended::html_table::HtmlTable,
options: &CommonMarkOptions,
max_width: f32,
) {
let id = ui.id().with("_html_table").with(self.curr_table);
self.curr_table += 1;
let num_cols = table
.header
.first()
.or(table.rows.first())
.map(|r| r.len())
.unwrap_or(0);
let line_h = ui.text_style_height(&egui::TextStyle::Body);
let cell_h = line_h * 1.5;
if num_cols == 0 {
self.line.try_insert_end(ui);
return;
}
let row_height_for = |cells: &[String]| -> f32 {
let max_lines = cells
.iter()
.map(|c| html_cell_visual_lines(c))
.max()
.unwrap_or(1);
cell_h * max_lines as f32
};
let header_h = table
.header
.first()
.map(|row| row_height_for(row))
.unwrap_or(cell_h);
let extra_header_heights: Vec<f32> = table
.header
.iter()
.skip(1)
.map(|row| row_height_for(row))
.collect();
let body_heights: Vec<f32> = table.rows.iter().map(|row| row_height_for(row)).collect();
let mut scroll_out = egui::ScrollArea::horizontal()
.id_salt(id.with("_scroll"))
.max_width(max_width)
.auto_shrink([false, true])
.show(ui, |ui| {
ui.vertical(|ui| {
egui::Frame::group(ui.style()).show(ui, |ui| {
let builder = egui_extras::TableBuilder::new(ui)
.id_salt(id)
.striped(true)
.resizable(true)
.vscroll(false)
.auto_shrink([false, true])
.min_scrolled_height(0.0)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.columns(
egui_extras::Column::auto().resizable(true).at_least(40.0),
num_cols,
);
let render_cell_strong = |ui: &mut Ui, cell: &str| {
egui::Frame::NONE
.inner_margin(egui::Margin::symmetric(8, 4))
.show(ui, |ui| {
ui.strong(cell);
});
};
if let Some(first_header) = table.header.first() {
builder
.header(header_h, |mut row| {
for cell in first_header {
row.col(|ui| render_cell_strong(ui, cell));
}
})
.body(|mut body| {
for (idx, extra) in table.header.iter().skip(1).enumerate() {
let h = extra_header_heights
.get(idx)
.copied()
.unwrap_or(cell_h);
body.row(h, |mut row_ui| {
for cell in extra {
row_ui.col(|ui| render_cell_strong(ui, cell));
}
});
}
for (row_idx, row) in table.rows.iter().enumerate() {
let h = body_heights
.get(row_idx)
.copied()
.unwrap_or(cell_h);
body.row(h, |mut row_ui| {
for cell in row {
row_ui.col(|ui| {
egui::Frame::NONE
.inner_margin(egui::Margin::symmetric(
8, 4,
))
.show(ui, |ui| {
let rich_text = self
.text_style
.to_richtext_with_typography(
ui,
cell,
Some(&options.typography),
);
ui.label(rich_text);
});
});
}
});
}
});
} else {
builder.body(|mut body| {
for (row_idx, row) in table.rows.iter().enumerate() {
let h = body_heights
.get(row_idx)
.copied()
.unwrap_or(cell_h);
body.row(h, |mut row_ui| {
for cell in row {
row_ui.col(|ui| {
egui::Frame::NONE
.inner_margin(egui::Margin::symmetric(8, 4))
.show(ui, |ui| {
let rich_text = self
.text_style
.to_richtext_with_typography(
ui,
cell,
Some(&options.typography),
);
ui.label(rich_text);
});
});
}
});
}
});
}
});
});
});
forward_shift_wheel_to_horizontal_scroll(ui, &mut scroll_out);
self.line.try_insert_end(ui);
}
}