csskit_highlight/
highlight.rs

1use 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
8/// Miette highlighter for CSS syntax highlighting
9pub 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		// Find the byte offset of this line in the source
46		let line_offset: usize = self.source.lines().take(self.current_line).map(|l| l.len() + 1).sum();
47		self.current_line += 1;
48
49		// Lex just this line
50		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			// Get the color for this token based on its global position
59			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			// Add any whitespace before this token
65			if start > last_end {
66				result.push(Style::default().style(&line[last_end..start]));
67			}
68
69			// Add the colored token - use csskit_highlight color if available
70			let token_str = &line[start..end];
71
72			// Check for background color decoration first (for color values)
73			let style = if let Some(highlight) = color
74				&& let SemanticDecoration::BackgroundColor(bg) = highlight.decoration()
75			{
76				use chromashift::{Named, Srgb, WcagColorContrast};
77
78				// Choose contrasting foreground color
79				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				// Convert to Srgb to get RGB components
86				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				// Use the theme to get the owo-colors style
96				self.theme.get_owo_style(highlight.kind(), highlight.modifier())
97			} else {
98				// Fallback: just return unstyled
99				Style::default()
100			};
101
102			result.push(style.style(token_str));
103			last_end = end;
104		}
105
106		// Add any remaining text
107		if last_end < line.len() {
108			result.push(Style::default().style(&line[last_end..]));
109		}
110
111		result
112	}
113}