This commit is contained in:
2025-07-07 00:08:36 +02:00
parent 138df11710
commit 9511ae8176
8 changed files with 419 additions and 10 deletions

View File

@ -1,6 +1,12 @@
use egui::text::{CCursorRange, LayoutJob};
use crate::easy_mark::easy_mark_parser;
use crate::{
easy_mark::easy_mark_parser,
markdown::{
span::Span,
tokenizer::{Token, TokenKind, tokenize},
},
};
/// Highlight easymark, memoizing previous output to save CPU.
///
@ -29,6 +35,131 @@ impl MemoizedHighlighter {
}
pub fn highlight_easymark(
egui_style: &egui::Style,
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 prev = TokenKind::Newline;
let tokens: Vec<_> = tokenize(text).collect();
let mut tokens = &tokens[..];
const CODE_INDENT: f32 = 10.0;
while !tokens.is_empty() {
let token = tokens.first().unwrap();
tokens = &tokens[1..];
let start_of_line = prev == TokenKind::Newline;
prev = token.kind;
match token.kind {
TokenKind::CodeBlock if start_of_line => {
let astyle = format_from_style(
egui_style,
&easy_mark_parser::Style {
code: true,
..Default::default()
},
);
let span = collect_until(
token,
&mut tokens,
series([TokenKind::Newline, TokenKind::CodeBlock]),
);
job.append(&*span, CODE_INDENT, astyle.clone());
style = Default::default();
continue;
}
TokenKind::Newline => style = easy_mark_parser::Style::default(),
TokenKind::Strong => style.strong ^= true,
TokenKind::Italic => style.italics ^= true,
TokenKind::Strikethrough => style.strikethrough ^= true,
TokenKind::Heading(_h) if start_of_line => style.heading = true,
TokenKind::Quote if start_of_line => style.quoted = true,
TokenKind::CodeBlock | TokenKind::Mono => {
style.code = true;
let span = collect_until(
token,
&mut tokens,
any_of([TokenKind::Mono, TokenKind::CodeBlock, TokenKind::Newline]),
);
job.append(&*span, 0.0, format_from_style(egui_style, &style));
style.code = false;
continue;
}
TokenKind::Heading(..) | TokenKind::Quote | TokenKind::Text => {}
}
job.append(&token.span, 0.0, format_from_style(egui_style, &style));
}
job
}
fn series<'a, const N: usize>(of: [TokenKind; N]) -> impl FnMut(&[Token<'a>; N]) -> bool {
move |token| {
of.iter()
.zip(token)
.all(|(kind, token)| kind == &token.kind)
}
}
fn any_of<'a, const N: usize>(these: [TokenKind; N]) -> impl FnMut(&[Token<'a>; 1]) -> bool {
move |[token]| these.contains(&token.kind)
}
/// Collect all tokens up to and including `pattern`, and merge them into a signle span.
///
/// `N` determines how many specific and consecutive tokens we are looking for.
/// i.e. if we were looking for a [TokenKind::Newline] followed by a [TokenKind::Quote], `N`
/// would equal `2`.
///
/// `pattern` is a function that accepts an array of `N` tokens and returns `true` if they match,
/// i.e. if we should stop collecting. [any_of] and [series] can help to construct this function.
///
/// The collected tokens will be split off the head of the slice referred to by `tokens`.
///
/// # Panic
/// Panics if `tokens` does not contain only consecutive adjacent spans.
fn collect_until<'a, const N: usize>(
token: &Token<'a>,
tokens: &mut &[Token<'a>],
pattern: impl FnMut(&[Token<'a>; N]) -> bool,
) -> Span<'a>
where
for<'b> &'b [Token<'a>; N]: TryFrom<&'b [Token<'a>]>,
{
let mut windows = tokens
.windows(N)
.map(|slice| <&[Token<'a>; N]>::try_from(slice).ok().unwrap());
let split_at = match windows.position(pattern) {
Some(i) => i + N,
None => tokens.len(), // consume everything
};
let (consume, keep) = tokens.split_at(split_at);
*tokens = keep;
consume
.iter()
.fold(token.span.clone(), |span: Span<'_>, token| {
span.try_merge(&token.span).unwrap()
})
}
pub fn highlight_easymark_old(
egui_style: &egui::Style,
mut text: &str,