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::elements::*;
use egui_commonmark_backend::misc::*;
use egui_commonmark_backend::pulldown::*;
use pulldown_cmark::{CowStr, HeadingLevel};
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,
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>,
}
pub(crate) struct CheckboxClickEvent {
pub(crate) checked: bool,
pub(crate) span: Range<usize>,
}
impl CommonMarkViewerInternal {
pub fn new() -> Self {
Self {
curr_table: 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(),
}
}
}
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()
}
}
impl CommonMarkViewerInternal {
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 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 mut events = pulldown_cmark::Parser::new_ext(
text,
parser_options_math(options.math_fn.is_some()),
)
.into_offset_iter()
.enumerate()
.peekable();
while let Some((index, (e, src_span))) = events.next() {
let start_position = ui.next_widget_position();
let is_element_end = matches!(e, pulldown_cmark::Event::End(_));
let should_add_split_point = self.list.is_inside_a_list() && is_element_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);
if let Some(source_id) = split_points_id
&& should_add_split_point
{
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))
}
pub(crate) fn show_scrollable(
&mut self,
source_id: Id,
ui: &mut egui::Ui,
cache: &mut CommonMarkCache,
options: &CommonMarkOptions,
text: &str,
) {
let available_size = ui.available_size();
let scroll_id = source_id.with("_scroll_area");
let Some(page_size) = scroll_cache(cache, &source_id).page_size else {
egui::ScrollArea::vertical()
.id_salt(scroll_id)
.auto_shrink([false, true])
.show(ui, |ui| {
self.show(ui, cache, options, text, Some(source_id));
});
scroll_cache(cache, &source_id).available_size = available_size;
return;
};
let events =
pulldown_cmark::Parser::new_ext(text, parser_options_math(options.math_fn.is_some()))
.into_offset_iter()
.collect::<Vec<_>>();
let num_rows = events.len();
egui::ScrollArea::vertical()
.id_salt(scroll_id)
.auto_shrink([false, true])
.show_viewport(ui, |ui, viewport| {
ui.set_height(page_size.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 (first_event_index, _, first_end_position) = scroll_cache
.split_points
.iter()
.filter(|(_, _, end_position)| end_position.y < viewport.min.y)
.nth_back(1)
.copied()
.unwrap_or((0, Pos2::ZERO, Pos2::ZERO));
let last_event_index = scroll_cache
.split_points
.iter()
.filter(|(_, start_position, _)| start_position.y > viewport.max.y)
.nth(1)
.map(|(index, _, _)| *index)
.unwrap_or(num_rows);
ui.allocate_space(first_end_position.to_vec2());
let mut events = events
.into_iter()
.enumerate()
.skip(first_event_index)
.take(last_event_index - first_event_index)
.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 scroll_cache = scroll_cache(cache, &source_id);
if available_size != scroll_cache.available_size {
scroll_cache.available_size = available_size;
scroll_cache.page_size = None;
scroll_cache.split_points.clear();
}
}
#[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::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;
egui::Frame::group(ui.style()).show(ui, |ui| {
let Table { header, rows } = parse_table(events);
egui::Grid::new(id).striped(true).show(ui, |ui| {
for col in header {
ui.horizontal(|ui| {
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, max_width);
self.line.should_start_newline = tmp_start;
self.line.should_end_newline = tmp_end;
}
});
}
ui.end_row();
for row in rows {
for col in row {
ui.horizontal(|ui| {
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, max_width);
self.line.should_start_newline = tmp_start;
self.line.should_end_newline = tmp_end;
}
});
}
ui.end_row();
}
});
});
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(text, ui);
}
pulldown_cmark::Event::Code(text) => {
self.text_style.code = true;
self.event_text(text, ui);
self.text_style.code = false;
}
pulldown_cmark::Event::InlineHtml(text) => {
self.event_text(text, ui);
}
pulldown_cmark::Event::Html(text) => {
if options.html_fn.is_some() {
self.html_block.push_str(&text);
} else {
self.event_text(text, ui);
}
}
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 let Some(math_fn) = options.math_fn {
math_fn(ui, &tex, true);
}
}
pulldown_cmark::Event::DisplayMath(tex) => {
if let Some(math_fn) = options.math_fn {
math_fn(ui, &tex, false);
}
}
}
}
fn event_text(&mut self, text: CowStr, ui: &mut Ui) {
let rich_text = self.text_style.to_richtext(ui, &text);
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 {
ui.label(rich_text);
}
}
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, .. } => {
newline(ui);
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);
}
pulldown_cmark::TagEnd::Heading { .. } => {
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, options);
}
}
pulldown_cmark::TagEnd::HtmlBlock => {
if let Some(html_fn) = options.html_fn {
html_fn(ui, &self.html_block);
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() {
block.end(ui, cache, options, max_width);
self.line.try_insert_end(ui);
}
}
}