128 lines
3.2 KiB
Rust
128 lines
3.2 KiB
Rust
use egui::text::{CCursorRange, LayoutJob};
|
|
|
|
use crate::markdown::Heading;
|
|
|
|
use super::{Item, Style, parse};
|
|
|
|
/// Highlight markdown, caching previous output to save CPU.
|
|
#[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_markdown(egui_style, code, cursor);
|
|
}
|
|
self.output.clone()
|
|
}
|
|
}
|
|
|
|
pub fn highlight_markdown(
|
|
egui_style: &egui::Style,
|
|
text: &str,
|
|
|
|
// TODO: hide special characters where cursor isn't
|
|
_cursor: Option<CCursorRange>,
|
|
) -> LayoutJob {
|
|
let mut job = LayoutJob::default();
|
|
|
|
let code_style = Style {
|
|
code: true,
|
|
..Default::default()
|
|
};
|
|
|
|
for item in parse(text) {
|
|
match item {
|
|
Item::Text { span, style } => {
|
|
job.append(&span, 0.0, format_from_style(egui_style, &style));
|
|
}
|
|
Item::CodeBlock {
|
|
all,
|
|
language: _, // TODO
|
|
code: _, // TODO
|
|
} => {
|
|
job.append(&all, 100.0, format_from_style(egui_style, &code_style));
|
|
}
|
|
}
|
|
}
|
|
|
|
job
|
|
}
|
|
|
|
fn format_from_style(egui_style: &egui::Style, style: &Style) -> egui::text::TextFormat {
|
|
use egui::{Align, Color32, Stroke, TextStyle};
|
|
|
|
let color = if style.code {
|
|
egui_style.visuals.strong_text_color() * Color32::GREEN
|
|
} else if style.strong || style.heading.is_some() {
|
|
egui_style.visuals.strong_text_color()
|
|
} else if style.quoted {
|
|
egui_style.visuals.weak_text_color()
|
|
} else {
|
|
egui_style.visuals.text_color()
|
|
};
|
|
|
|
let text_style = if let Some(heading) = style.heading {
|
|
match heading {
|
|
Heading::H1 => TextStyle::Name("H1".into()),
|
|
Heading::H2 => TextStyle::Name("H2".into()),
|
|
Heading::H3 => TextStyle::Name("H3".into()),
|
|
Heading::H4 => TextStyle::Name("H4".into()),
|
|
Heading::H5 => TextStyle::Name("H5".into()),
|
|
Heading::H6 => TextStyle::Name("H6".into()),
|
|
}
|
|
} else if style.code {
|
|
TextStyle::Monospace
|
|
} else if style.small | style.raised {
|
|
TextStyle::Small
|
|
} else {
|
|
TextStyle::Body
|
|
};
|
|
|
|
let background = if style.code {
|
|
egui_style.visuals.code_bg_color
|
|
} else {
|
|
Color32::TRANSPARENT
|
|
};
|
|
|
|
let underline = if style.underline {
|
|
Stroke::new(1.0, color)
|
|
} else {
|
|
Stroke::NONE
|
|
};
|
|
|
|
let strikethrough = if style.strikethrough {
|
|
Stroke::new(1.0, color)
|
|
} else {
|
|
Stroke::NONE
|
|
};
|
|
|
|
let valign = if style.raised {
|
|
Align::TOP
|
|
} else {
|
|
Align::BOTTOM
|
|
};
|
|
|
|
egui::text::TextFormat {
|
|
font_id: text_style.resolve(egui_style),
|
|
color,
|
|
background,
|
|
italics: style.italics,
|
|
underline,
|
|
strikethrough,
|
|
valign,
|
|
..Default::default()
|
|
}
|
|
}
|