This commit is contained in:
2025-06-12 20:23:52 +02:00
parent 6f591627be
commit 27728fc431
28 changed files with 6730 additions and 0 deletions

View File

@ -0,0 +1,243 @@
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<CCursorRange>,
) -> 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<CCursorRange>,
) -> 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()
}
}