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 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_srgb: Srgb = bg.into();
87 let fg_srgb: Srgb = fg.into();
88
89 Style::new().truecolor(fg_srgb.red, fg_srgb.green, fg_srgb.blue).on_truecolor(
90 bg_srgb.red,
91 bg_srgb.green,
92 bg_srgb.blue,
93 )
94 } else if let Some(highlight) = color {
95 self.theme.get_owo_style(highlight.kind(), highlight.modifier())
97 } else {
98 Style::default()
100 };
101
102 result.push(style.style(token_str));
103 last_end = end;
104 }
105
106 if last_end < line.len() {
108 result.push(Style::default().style(&line[last_end..]));
109 }
110
111 result
112 }
113}