use egui::text::{CCursorRange, LayoutJob}; use crate::easy_mark::easy_mark_parser; /// Highlight easymark, memoizing previous output to save CPU. /// /// In practice, the highlighter is fast enough not to need any caching. #[derive(Default)] pub struct MemoizedHighlighter { style: egui::Style, code: String, output: LayoutJob, } impl MemoizedHighlighter { pub fn highlight( &mut self, egui_style: &egui::Style, code: &str, cursor: Option, ) -> LayoutJob { if (&self.style, self.code.as_str()) != (egui_style, code) { self.style = egui_style.clone(); code.clone_into(&mut self.code); self.output = highlight_easymark(egui_style, code, cursor); } self.output.clone() } } pub fn highlight_easymark( egui_style: &egui::Style, mut text: &str, // TODO: hide special characters where cursor isn't _cursor: Option, ) -> LayoutJob { let mut job = LayoutJob::default(); let mut style = easy_mark_parser::Style::default(); let mut start_of_line = true; const CODE_INDENT: f32 = 10.0; while !text.is_empty() { if start_of_line && text.starts_with("```") { let astyle = format_from_style( egui_style, &easy_mark_parser::Style { code: true, ..Default::default() }, ); // Render the initial backticks as spaces text = &text[3..]; job.append(" ", CODE_INDENT, astyle.clone()); match text.find("\n```") { Some(n) => { for line in text[..n + 1].lines() { job.append(line, CODE_INDENT, astyle.clone()); job.append("\n", 0.0, astyle.clone()); } // Render the final backticks as spaces job.append(" ", CODE_INDENT, astyle); text = &text[n + 4..]; } None => { job.append(text, 0.0, astyle.clone()); text = ""; } }; style = Default::default(); continue; } if text.starts_with('`') { style.code = true; let end = text[1..] .find(&['`', '\n'][..]) .map_or_else(|| text.len(), |i| i + 2); job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); text = &text[end..]; style.code = false; continue; } let skip; // zero-width space let _zws = "\u{200b}"; let mut apply_basic_style = |text: &mut &str, style: &mut easy_mark_parser::Style, access: fn(&mut easy_mark_parser::Style) -> &mut bool| { let skip = if *access(style) { // Include the character that is ending this style: job.append(&text[..1], 0.0, format_from_style(egui_style, style)); *text = &text[1..]; 0 } else { 1 }; *access(style) ^= true; skip }; if text.starts_with('*') { skip = apply_basic_style(&mut text, &mut style, |style| &mut style.strong); } else if text.starts_with('/') { skip = apply_basic_style(&mut text, &mut style, |style| &mut style.italics); } else if text.starts_with('_') { skip = apply_basic_style(&mut text, &mut style, |style| &mut style.underline); } else if text.starts_with('$') { skip = apply_basic_style(&mut text, &mut style, |style| &mut style.small); } else if text.starts_with('~') { skip = apply_basic_style(&mut text, &mut style, |style| &mut style.strikethrough); } else if text.starts_with('^') { skip = apply_basic_style(&mut text, &mut style, |style| &mut style.raised); } else if text.starts_with('\\') && text.len() >= 2 { skip = 2; } else if start_of_line && text.starts_with(' ') { // we don't preview indentation, because it is confusing skip = 1; } else if start_of_line && text.starts_with("###### ") { style.heading = true; skip = 7; } else if start_of_line && text.starts_with("##### ") { style.heading = true; skip = 6; } else if start_of_line && text.starts_with("#### ") { style.heading = true; skip = 5; } else if start_of_line && text.starts_with("### ") { style.heading = true; skip = 4; } else if start_of_line && text.starts_with("## ") { style.heading = true; skip = 3; } else if start_of_line && text.starts_with("# ") { style.heading = true; skip = 2; } else if start_of_line && text.starts_with("> ") { style.quoted = true; skip = 2; // we don't preview indentation, because it is confusing } else if start_of_line && text.trim_start().starts_with("- ") { job.append("• ", 0.0, format_from_style(egui_style, &style)); text = &text[2..]; skip = 0; // we don't preview indentation, because it is confusing } else { skip = 0; } // Note: we don't preview underline, strikethrough and italics because it confuses things. // Swallow everything up to the next special character: let line_end = text[skip..] .find('\n') .map_or_else(|| text.len(), |i| (skip + i + 1)); let end = text[skip..] .find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..]) .map_or_else(|| text.len(), |i| (skip + i).max(1)); if line_end <= end { job.append( &text[..line_end], 0.0, format_from_style(egui_style, &style), ); text = &text[line_end..]; start_of_line = true; style = Default::default(); } else { job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); text = &text[end..]; start_of_line = false; } } job } fn format_from_style( egui_style: &egui::Style, emark_style: &easy_mark_parser::Style, ) -> egui::text::TextFormat { use egui::{Align, Color32, Stroke, TextStyle}; let color = if emark_style.strong || emark_style.heading { egui_style.visuals.strong_text_color() } else if emark_style.quoted { egui_style.visuals.weak_text_color() } else { egui_style.visuals.text_color() }; let text_style = if emark_style.heading { TextStyle::Heading } else if emark_style.code { TextStyle::Monospace } else if emark_style.small | emark_style.raised { TextStyle::Small } else { TextStyle::Body }; let background = if emark_style.code { egui_style.visuals.code_bg_color } else { Color32::TRANSPARENT }; let underline = if emark_style.underline { Stroke::new(1.0, color) } else { Stroke::NONE }; let strikethrough = if emark_style.strikethrough { Stroke::new(1.0, color) } else { Stroke::NONE }; let valign = if emark_style.raised { Align::TOP } else { Align::BOTTOM }; egui::text::TextFormat { font_id: text_style.resolve(egui_style), color, background, italics: emark_style.italics, underline, strikethrough, valign, ..Default::default() } }