1 use std::path::Path;
2
3 // all syntect imports are explicitly qualified, but their paths are shortened for convenience
4 mod syntect {
5 pub(super) use syntect::{
6 highlighting::{
7 Color, HighlightIterator, HighlightState, Highlighter, Style, Theme, ThemeSet,
8 },
9 parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
10 };
11 }
12
13 use owo_colors::{Rgb, Style, Styled};
14
15 use crate::{
16 highlighters::{Highlighter, HighlighterState},
17 SpanContents,
18 };
19
20 use super::BlankHighlighterState;
21
22 /// Highlights miette [SourceCode] with the [syntect](https://docs.rs/syntect/latest/syntect/) highlighting crate.
23 ///
24 /// Currently only 24-bit truecolor output is supported due to syntect themes
25 /// representing color as RGBA.
26 #[derive(Debug, Clone)]
27 pub struct SyntectHighlighter {
28 theme: syntect::Theme,
29 syntax_set: syntect::SyntaxSet,
30 use_bg_color: bool,
31 }
32
33 impl Default for SyntectHighlighter {
default() -> Self34 fn default() -> Self {
35 let theme_set = syntect::ThemeSet::load_defaults();
36 let theme = theme_set.themes["base16-ocean.dark"].clone();
37 Self::new_themed(theme, false)
38 }
39 }
40
41 impl Highlighter for SyntectHighlighter {
start_highlighter_state<'h>( &'h self, source: &dyn SpanContents<'_>, ) -> Box<dyn HighlighterState + 'h>42 fn start_highlighter_state<'h>(
43 &'h self,
44 source: &dyn SpanContents<'_>,
45 ) -> Box<dyn HighlighterState + 'h> {
46 if let Some(syntax) = self.detect_syntax(source) {
47 let highlighter = syntect::Highlighter::new(&self.theme);
48 let parse_state = syntect::ParseState::new(syntax);
49 let highlight_state =
50 syntect::HighlightState::new(&highlighter, syntect::ScopeStack::new());
51 Box::new(SyntectHighlighterState {
52 syntax_set: &self.syntax_set,
53 highlighter,
54 parse_state,
55 highlight_state,
56 use_bg_color: self.use_bg_color,
57 })
58 } else {
59 Box::new(BlankHighlighterState)
60 }
61 }
62 }
63
64 impl SyntectHighlighter {
65 /// Create a syntect highlighter with the given theme and syntax set.
new(syntax_set: syntect::SyntaxSet, theme: syntect::Theme, use_bg_color: bool) -> Self66 pub fn new(syntax_set: syntect::SyntaxSet, theme: syntect::Theme, use_bg_color: bool) -> Self {
67 Self {
68 theme,
69 syntax_set,
70 use_bg_color,
71 }
72 }
73
74 /// Create a syntect highlighter with the given theme and the default syntax set.
new_themed(theme: syntect::Theme, use_bg_color: bool) -> Self75 pub fn new_themed(theme: syntect::Theme, use_bg_color: bool) -> Self {
76 Self::new(
77 syntect::SyntaxSet::load_defaults_nonewlines(),
78 theme,
79 use_bg_color,
80 )
81 }
82
83 /// Determine syntect SyntaxReference to use for given SourceCode
detect_syntax(&self, contents: &dyn SpanContents<'_>) -> Option<&syntect::SyntaxReference>84 fn detect_syntax(&self, contents: &dyn SpanContents<'_>) -> Option<&syntect::SyntaxReference> {
85 // use language if given
86 if let Some(language) = contents.language() {
87 return self.syntax_set.find_syntax_by_name(language);
88 }
89 // otherwise try to use any file extension provided in the name
90 if let Some(name) = contents.name() {
91 if let Some(ext) = Path::new(name).extension() {
92 return self
93 .syntax_set
94 .find_syntax_by_extension(ext.to_string_lossy().as_ref());
95 }
96 }
97 // finally, attempt to guess syntax based on first line
98 return self.syntax_set.find_syntax_by_first_line(
99 &std::str::from_utf8(contents.data())
100 .ok()?
101 .split('\n')
102 .next()?,
103 );
104 }
105 }
106
107 /// Stateful highlighting iterator for [SyntectHighlighter]
108 #[derive(Debug)]
109 pub(crate) struct SyntectHighlighterState<'h> {
110 syntax_set: &'h syntect::SyntaxSet,
111 highlighter: syntect::Highlighter<'h>,
112 parse_state: syntect::ParseState,
113 highlight_state: syntect::HighlightState,
114 use_bg_color: bool,
115 }
116
117 impl<'h> HighlighterState for SyntectHighlighterState<'h> {
highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>>118 fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>> {
119 if let Ok(ops) = self.parse_state.parse_line(line, &self.syntax_set) {
120 let use_bg_color = self.use_bg_color;
121 syntect::HighlightIterator::new(
122 &mut self.highlight_state,
123 &ops,
124 line,
125 &mut self.highlighter,
126 )
127 .map(|(style, str)| (convert_style(style, use_bg_color).style(str)))
128 .collect()
129 } else {
130 vec![Style::default().style(line)]
131 }
132 }
133 }
134
135 /// Convert syntect [syntect::Style] into owo_colors [Style] */
136 #[inline]
convert_style(syntect_style: syntect::Style, use_bg_color: bool) -> Style137 fn convert_style(syntect_style: syntect::Style, use_bg_color: bool) -> Style {
138 if use_bg_color {
139 let fg = blend_fg_color(syntect_style);
140 let bg = convert_color(syntect_style.background);
141 Style::new().color(fg).on_color(bg)
142 } else {
143 let fg = convert_color(syntect_style.foreground);
144 Style::new().color(fg)
145 }
146 }
147
148 /// Blend foreground RGB into background RGB according to alpha channel
149 #[inline]
blend_fg_color(syntect_style: syntect::Style) -> Rgb150 fn blend_fg_color(syntect_style: syntect::Style) -> Rgb {
151 let fg = syntect_style.foreground;
152 if fg.a == 0xff {
153 return convert_color(fg);
154 }
155 let bg = syntect_style.background;
156 let ratio = fg.a as u32;
157 let r = (fg.r as u32 * ratio + bg.r as u32 * (255 - ratio)) / 255;
158 let g = (fg.g as u32 * ratio + bg.g as u32 * (255 - ratio)) / 255;
159 let b = (fg.b as u32 * ratio + bg.b as u32 * (255 - ratio)) / 255;
160 Rgb(r as u8, g as u8, b as u8)
161 }
162
163 /// Convert syntect color into owo color.
164 ///
165 /// Note: ignores alpha channel. use [`blend_fg_color`] if you need that
166 ///
167 #[inline]
convert_color(color: syntect::Color) -> Rgb168 fn convert_color(color: syntect::Color) -> Rgb {
169 Rgb(color.r, color.g, color.b)
170 }
171