okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petview: see code as highlighted images for a final review pass0d0b12c

main
3.3 KiB99 linesraw
1//! Syntax colors for a whole file, looked up by byte offset.
2
3use arborium::Highlighter;
4use arborium_highlight::{FlatToken, spans_to_flat_tokens};
5use arborium_theme::highlights;
6use arborium_theme::theme::{Color, Theme};
7
8/// Which grammar colored the file, or why none did.
9pub enum Language {
10    Highlighted(String),
11    /// Named after the file, but not one of the grammars compiled in.
12    Unsupported(String),
13    /// The grammar exists but could not highlight this file.
14    Failed(String),
15    Unknown,
16}
17
18pub struct Colors {
19    tokens: Vec<(FlatToken, Color)>,
20    pub foreground: Color,
21    pub background: Color,
22    pub language: Language,
23}
24
25impl Colors {
26    /// Highlights all of `source`, so a range starting inside a comment or a
27    /// string is colored as it would be in the whole file.
28    pub fn new(source: &str, language: Option<&str>, theme: &Theme) -> Colors {
29        let foreground = theme.foreground.unwrap_or(Color::new(0, 0, 0));
30        let background = theme.background.unwrap_or(Color::new(255, 255, 255));
31        let plain = |language| Colors {
32            tokens: Vec::new(),
33            foreground,
34            background,
35            language,
36        };
37        let Some(name) = language else {
38            return plain(Language::Unknown);
39        };
40        let spans = match Highlighter::new().highlight_spans(name, source) {
41            Ok(spans) => spans,
42            Err(arborium::Error::UnsupportedLanguage { .. }) => {
43                return plain(Language::Unsupported(name.to_string()));
44            }
45            Err(error) => return plain(Language::Failed(error.to_string())),
46        };
47        let tokens = spans_to_flat_tokens(source, spans)
48            .into_iter()
49            .filter_map(|token| tag_color(theme, token.tag).map(|color| (token, color)))
50            .collect();
51        Colors {
52            tokens,
53            foreground,
54            background,
55            language: Language::Highlighted(name.to_string()),
56        }
57    }
58
59    /// The token covering each byte from `from` on, for a caller walking forward.
60    pub fn cursor(&self, from: usize) -> Cursor<'_> {
61        let index = self
62            .tokens
63            .partition_point(|(token, _)| (token.end as usize) <= from);
64        Cursor {
65            colors: self,
66            index,
67        }
68    }
69}
70
71pub struct Cursor<'a> {
72    colors: &'a Colors,
73    index: usize,
74}
75
76impl Cursor<'_> {
77    /// The color at byte `offset`; offsets must not decrease between calls.
78    pub fn at(&mut self, offset: usize) -> Color {
79        let tokens = &self.colors.tokens;
80        while self.index < tokens.len() && (tokens[self.index].0.end as usize) <= offset {
81            self.index += 1;
82        }
83        match tokens.get(self.index) {
84            Some((token, color)) if token.start as usize <= offset => *color,
85            _ => self.colors.foreground,
86        }
87    }
88}
89
90/// A child tag with no color of its own takes its parent's, as the theme's CSS would.
91fn tag_color(theme: &Theme, tag: &str) -> Option<Color> {
92    let index = (0..highlights::COUNT).find(|&index| highlights::tag(index) == Some(tag))?;
93    theme.style(index).and_then(|style| style.fg).or_else(|| {
94        let parent = highlights::HIGHLIGHTS[index].parent_tag;
95        (!parent.is_empty())
96            .then(|| tag_color(theme, parent))
97            .flatten()
98    })
99}