csskit_highlight/
highlight.rs1use crate::{AnsiTheme, DefaultAnsiTheme, SemanticDecoration, TokenHighlighter};
2use css_ast::{CssAtomSet, StyleSheet, Visitable};
3use css_lexer::{Lexer, SourceOffset, Span};
4use miette::SpanContents;
5use miette::highlighters::{Highlighter, HighlighterState};
6use owo_colors::Style;
7
8pub struct CssHighlighter {
10 source: String,
11 token_colors: TokenHighlighter,
12}
13
14impl CssHighlighter {
15 pub fn new(source: String, stylesheet: &StyleSheet) -> Self {
16 let mut token_colors = TokenHighlighter::new();
17 let _ = stylesheet.accept(&mut token_colors);
18 Self { source, token_colors }
19 }
20}
21
22impl Highlighter for CssHighlighter {
23 fn start_highlighter_state<'h>(&'h self, source: &dyn SpanContents<'_>) -> Box<dyn HighlighterState + 'h> {
24 Box::new(CssHighlighterState::new(&self.source, &self.token_colors, source.line()))
25 }
26}
27
28struct CssHighlighterState<'a> {
29 source: &'a str,
30 token_colors: &'a TokenHighlighter,
31 current_line: usize,
32 theme: DefaultAnsiTheme,
33}
34
35impl<'a> CssHighlighterState<'a> {
36 fn new(source: &'a str, token_colors: &'a TokenHighlighter, current_line: usize) -> Self {
37 Self { source, token_colors, theme: DefaultAnsiTheme, current_line }
38 }
39}
40
41impl<'a> HighlighterState for CssHighlighterState<'a> {
42 fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<owo_colors::Styled<&'s str>> {
43 let mut result = Vec::new();
44
45 let line_offset: usize = self.source.lines().take(self.current_line).map(|l| l.len() + 1).sum();
47 self.current_line += 1;
48
49 let lexer = Lexer::new(&CssAtomSet::ATOMS, line);
51 let mut last_end = 0;
52
53 for cursor in lexer {
54 let start = cursor.offset().0 as usize;
55 let token_str = cursor.str_slice(line);
56 let end = start + token_str.len();
57
58 let global_offset = line_offset + start;
60 let span =
61 Span::new(SourceOffset(global_offset as u32), SourceOffset((global_offset + token_str.len()) as u32));
62 let color = self.token_colors.get(span);
63
64 if start > last_end {
66 result.push(Style::default().style(&line[last_end..start]));
67 }
68
69 let token_str = &line[start..end];
71
72 let style = if let Some(highlight) = color
74 && let SemanticDecoration::BackgroundColor(bg) = highlight.decoration()
75 {
76 use chromashift::{Named, Srgb, WcagColorContrast};
77
78 let fg = if bg.wcag_contrast_ratio(Named::White) > bg.wcag_contrast_ratio(Named::Black) {
80 Named::White
81 } else {
82 Named::Black
83 };
84
85 let [bg_red, bg_green, bg_blue] = Srgb::from(bg).to_u8();
87 let [fg_red, fg_green, fg_blue] = Srgb::from(fg).to_u8();
88
89 Style::new().truecolor(fg_red, fg_green, fg_blue).on_truecolor(bg_red, bg_green, bg_blue)
90 } else if let Some(highlight) = color {
91 self.theme.get_owo_style(highlight.kind(), highlight.modifier())
93 } else {
94 Style::default()
96 };
97
98 result.push(style.style(token_str));
99 last_end = end;
100 }
101
102 if last_end < line.len() {
104 result.push(Style::default().style(&line[last_end..]));
105 }
106
107 result
108 }
109}