egui_demo_lib/easy_mark/
easy_mark_highlighter.rs

1use crate::easy_mark::easy_mark_parser;
2
3/// Highlight easymark, memoizing previous output to save CPU.
4///
5/// In practice, the highlighter is fast enough not to need any caching.
6#[derive(Default)]
7pub struct MemoizedEasymarkHighlighter {
8    style: egui::Style,
9    code: String,
10    output: egui::text::LayoutJob,
11}
12
13impl MemoizedEasymarkHighlighter {
14    pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob {
15        if (&self.style, self.code.as_str()) != (egui_style, code) {
16            self.style = egui_style.clone();
17            code.clone_into(&mut self.code);
18            self.output = highlight_easymark(egui_style, code);
19        }
20        self.output.clone()
21    }
22}
23
24pub fn highlight_easymark(egui_style: &egui::Style, mut text: &str) -> egui::text::LayoutJob {
25    let mut job = egui::text::LayoutJob::default();
26    let mut style = easy_mark_parser::Style::default();
27    let mut start_of_line = true;
28
29    while !text.is_empty() {
30        if start_of_line && text.starts_with("```") {
31            let end = text.find("\n```").map_or_else(|| text.len(), |i| i + 4);
32            job.append(
33                &text[..end],
34                0.0,
35                format_from_style(
36                    egui_style,
37                    &easy_mark_parser::Style {
38                        code: true,
39                        ..Default::default()
40                    },
41                ),
42            );
43            text = &text[end..];
44            style = Default::default();
45            continue;
46        }
47
48        if text.starts_with('`') {
49            style.code = true;
50            let end = text[1..]
51                .find(&['`', '\n'][..])
52                .map_or_else(|| text.len(), |i| i + 2);
53            job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
54            text = &text[end..];
55            style.code = false;
56            continue;
57        }
58
59        let mut skip;
60
61        if text.starts_with('\\') && text.len() >= 2 {
62            skip = 2;
63        } else if start_of_line && text.starts_with(' ') {
64            // we don't preview indentation, because it is confusing
65            skip = 1;
66        } else if start_of_line && text.starts_with("# ") {
67            style.heading = true;
68            skip = 2;
69        } else if start_of_line && text.starts_with("> ") {
70            style.quoted = true;
71            skip = 2;
72            // we don't preview indentation, because it is confusing
73        } else if start_of_line && text.starts_with("- ") {
74            skip = 2;
75            // we don't preview indentation, because it is confusing
76        } else if text.starts_with('*') {
77            skip = 1;
78            if style.strong {
79                // Include the character that is ending this style:
80                job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
81                text = &text[skip..];
82                skip = 0;
83            }
84            style.strong ^= true;
85        } else if text.starts_with('$') {
86            skip = 1;
87            if style.small {
88                // Include the character that is ending this style:
89                job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
90                text = &text[skip..];
91                skip = 0;
92            }
93            style.small ^= true;
94        } else if text.starts_with('^') {
95            skip = 1;
96            if style.raised {
97                // Include the character that is ending this style:
98                job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
99                text = &text[skip..];
100                skip = 0;
101            }
102            style.raised ^= true;
103        } else {
104            skip = 0;
105        }
106        // Note: we don't preview underline, strikethrough and italics because it confuses things.
107
108        // Swallow everything up to the next special character:
109        let line_end = text[skip..]
110            .find('\n')
111            .map_or_else(|| text.len(), |i| skip + i + 1);
112        let end = text[skip..]
113            .find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..])
114            .map_or_else(|| text.len(), |i| (skip + i).max(1));
115
116        if line_end <= end {
117            job.append(
118                &text[..line_end],
119                0.0,
120                format_from_style(egui_style, &style),
121            );
122            text = &text[line_end..];
123            start_of_line = true;
124            style = Default::default();
125        } else {
126            job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
127            text = &text[end..];
128            start_of_line = false;
129        }
130    }
131
132    job
133}
134
135fn format_from_style(
136    egui_style: &egui::Style,
137    emark_style: &easy_mark_parser::Style,
138) -> egui::text::TextFormat {
139    use egui::{Align, Color32, Stroke, TextStyle};
140
141    let color = if emark_style.strong || emark_style.heading {
142        egui_style.visuals.strong_text_color()
143    } else if emark_style.quoted {
144        egui_style.visuals.weak_text_color()
145    } else {
146        egui_style.visuals.text_color()
147    };
148
149    let text_style = if emark_style.heading {
150        TextStyle::Heading
151    } else if emark_style.code {
152        TextStyle::Monospace
153    } else if emark_style.small | emark_style.raised {
154        TextStyle::Small
155    } else {
156        TextStyle::Body
157    };
158
159    let background = if emark_style.code {
160        egui_style.visuals.code_bg_color
161    } else {
162        Color32::TRANSPARENT
163    };
164
165    let underline = if emark_style.underline {
166        Stroke::new(1.0, color)
167    } else {
168        Stroke::NONE
169    };
170
171    let strikethrough = if emark_style.strikethrough {
172        Stroke::new(1.0, color)
173    } else {
174        Stroke::NONE
175    };
176
177    let valign = if emark_style.raised {
178        Align::TOP
179    } else {
180        Align::BOTTOM
181    };
182
183    egui::text::TextFormat {
184        font_id: text_style.resolve(egui_style),
185        color,
186        background,
187        italics: emark_style.italics,
188        underline,
189        strikethrough,
190        valign,
191        ..Default::default()
192    }
193}