1use crate::exporters::Exporter;
2use core::range::Range;
3use oak_core::{
4 TokenType,
5 language::{ElementRole, Language, TokenRole, UniversalElementRole, UniversalTokenRole},
6 tree::{RedLeaf, RedNode, RedTree},
7 visitor::Visitor,
8};
9use serde::{Deserialize, Serialize};
10use std::{
11 borrow::Cow,
12 collections::HashMap,
13 string::{String, ToString},
14 vec::Vec,
15};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct HighlightStyle {
23 pub color: Option<String>,
25 pub background_color: Option<String>,
27 pub bold: bool,
29 pub italic: bool,
31 pub underline: bool,
33}
34
35impl Default for HighlightStyle {
36 fn default() -> Self {
37 Self { color: None, background_color: None, bold: false, italic: false, underline: false }
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct HighlightTheme {
44 pub name: String,
46 pub styles: HashMap<String, HighlightStyle>,
49}
50
51impl Default for HighlightTheme {
52 fn default() -> Self {
53 let mut styles = HashMap::new();
54
55 styles.insert("keyword".to_string(), HighlightStyle { color: Some("#0000FF".to_string()), bold: true, ..Default::default() });
57 styles.insert("keyword.operator".to_string(), HighlightStyle { color: Some("#800080".to_string()), ..Default::default() });
58 styles.insert("variable.other".to_string(), HighlightStyle { color: Some("#001080".to_string()), ..Default::default() });
59 styles.insert("constant".to_string(), HighlightStyle { color: Some("#098658".to_string()), ..Default::default() });
60 styles.insert("constant.character.escape".to_string(), HighlightStyle { color: Some("#FF6600".to_string()), ..Default::default() });
61 styles.insert("punctuation".to_string(), HighlightStyle { color: Some("#000080".to_string()), ..Default::default() });
62 styles.insert("comment".to_string(), HighlightStyle { color: Some("#808080".to_string()), italic: true, ..Default::default() });
63 styles.insert("punctuation.whitespace".to_string(), HighlightStyle::default());
64
65 styles.insert("entity.name.function".to_string(), HighlightStyle { color: Some("#795E26".to_string()), bold: true, ..Default::default() });
67 styles.insert("entity.name.type".to_string(), HighlightStyle { color: Some("#267F99".to_string()), ..Default::default() });
68 styles.insert("variable.other.declaration".to_string(), HighlightStyle { color: Some("#795E26".to_string()), ..Default::default() });
69 styles.insert("comment.block.documentation".to_string(), HighlightStyle { color: Some("#008000".to_string()), italic: true, ..Default::default() });
70 styles.insert("meta.preprocessor".to_string(), HighlightStyle { color: Some("#AF00DB".to_string()), ..Default::default() });
71 styles.insert("entity.other.attribute-name".to_string(), HighlightStyle { color: Some("#AF00DB".to_string()), ..Default::default() });
72 styles.insert("entity.other.attribute-name.key".to_string(), HighlightStyle { color: Some("#001080".to_string()), ..Default::default() });
73
74 styles.insert("invalid".to_string(), HighlightStyle { color: Some("#FF0000".to_string()), background_color: Some("#FFCCCC".to_string()), ..Default::default() });
76 styles.insert("none".to_string(), HighlightStyle::default());
77
78 Self { name: "default".to_string(), styles }
79 }
80}
81
82impl HighlightTheme {
83 pub fn resolve_style(&self, scope: &str) -> HighlightStyle {
86 let mut current_scope = scope;
87 while !current_scope.is_empty() {
88 if let Some(style) = self.styles.get(current_scope) {
89 return style.clone();
90 }
91 if let Some(pos) = current_scope.rfind('.') {
92 current_scope = ¤t_scope[..pos];
93 }
94 else {
95 break;
96 }
97 }
98 self.styles.get("none").cloned().unwrap_or_default()
99 }
100
101 pub fn resolve_styles(&self, scopes: &[String]) -> HighlightStyle {
104 let mut best_style = None;
105 let mut best_depth = -1;
106
107 for scope in scopes {
108 let mut current_scope = scope.as_str();
109 let mut depth = (current_scope.split('.').count()) as i32;
111
112 while !current_scope.is_empty() {
113 if let Some(style) = self.styles.get(current_scope) {
114 if depth > best_depth {
115 best_depth = depth;
116 best_style = Some(style.clone());
117 }
118 break; }
120 if let Some(pos) = current_scope.rfind('.') {
121 current_scope = ¤t_scope[..pos];
122 depth -= 1;
123 }
124 else {
125 break;
126 }
127 }
128 }
129
130 best_style.unwrap_or_else(|| self.styles.get("none").cloned().unwrap_or_default())
131 }
132
133 pub fn get_token_style(&self, role: oak_core::UniversalTokenRole) -> HighlightStyle {
134 use oak_core::TokenRole;
135 self.resolve_style(role.name())
136 }
137
138 pub fn get_element_style(&self, role: oak_core::UniversalElementRole) -> HighlightStyle {
139 use oak_core::ElementRole;
140 self.resolve_style(role.name())
141 }
142}
143
144fn get_token_scopes<R: TokenRole>(role: R, language: &str, category: oak_core::language::LanguageCategory) -> Vec<String> {
146 let specific_name = role.name();
147 let universal_role = role.universal();
148 let universal_name = universal_role.name();
149 let category_prefix = match category {
150 oak_core::language::LanguageCategory::Markup => "markup",
151 oak_core::language::LanguageCategory::Config => "config",
152 oak_core::language::LanguageCategory::Programming => "source",
153 oak_core::language::LanguageCategory::Dsl => "dsl",
154 _ => "source",
155 };
156
157 let mut scopes = Vec::new();
158
159 scopes.push(format!("{}.{}", specific_name, language));
161
162 if specific_name != universal_name {
164 scopes.push(specific_name.to_string());
165 }
166
167 scopes.push(format!("{}.{}", category_prefix, universal_name));
169
170 scopes.push(universal_name.to_string());
172
173 scopes.push(format!("{}.{}", category_prefix, language));
175
176 scopes
177}
178
179fn get_element_scopes<R: ElementRole>(role: R, language: &str, category: oak_core::language::LanguageCategory) -> Vec<String> {
181 let specific_name = role.name();
182 let universal_role = role.universal();
183 let universal_name = universal_role.name();
184 let category_prefix = match category {
185 oak_core::language::LanguageCategory::Markup => "markup",
186 oak_core::language::LanguageCategory::Config => "config",
187 oak_core::language::LanguageCategory::Programming => "source",
188 oak_core::language::LanguageCategory::Dsl => "dsl",
189 _ => "source",
190 };
191
192 let mut scopes = Vec::new();
193
194 scopes.push(format!("{}.{}", specific_name, language));
196
197 if specific_name != universal_name {
199 scopes.push(specific_name.to_string());
200 }
201
202 scopes.push(format!("{}.{}", category_prefix, universal_name));
204
205 scopes.push(universal_name.to_string());
207
208 scopes.push(format!("{}.{}", category_prefix, language));
210
211 scopes
212}
213
214pub trait ScopeProvider {
216 fn scopes(&self, language: &str, category: oak_core::language::LanguageCategory) -> Vec<String>;
217}
218
219impl ScopeProvider for UniversalTokenRole {
220 fn scopes(&self, language: &str, category: oak_core::language::LanguageCategory) -> Vec<String> {
221 get_token_scopes(*self, language, category)
222 }
223}
224
225impl ScopeProvider for UniversalElementRole {
226 fn scopes(&self, language: &str, category: oak_core::language::LanguageCategory) -> Vec<String> {
227 get_element_scopes(*self, language, category)
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
233pub struct HighlightSpan {
234 pub start: usize,
235 pub end: usize,
236}
237
238impl From<Range<usize>> for HighlightSpan {
239 fn from(range: Range<usize>) -> Self {
240 Self { start: range.start, end: range.end }
241 }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct HighlightSegment<'a> {
249 pub span: HighlightSpan,
251 pub style: HighlightStyle,
253 pub text: Cow<'a, str>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct HighlightResult<'a> {
260 pub segments: Vec<HighlightSegment<'a>>,
261 pub source: Cow<'a, str>,
262}
263
264pub struct HighlightVisitor<'a, 't> {
265 pub theme: &'t HighlightTheme,
266 pub segments: Vec<HighlightSegment<'a>>,
267 pub source: &'a str,
268}
269
270impl<'a, 't, 'tree, L: Language> Visitor<'tree, L> for HighlightVisitor<'a, 't> {
271 fn visit_node(&mut self, node: RedNode<'tree, L>) {
272 for child in node.children() {
275 match child {
276 RedTree::Node(n) => <Self as Visitor<L>>::visit_node(self, n),
277 RedTree::Leaf(t) => <Self as Visitor<L>>::visit_token(self, t),
278 }
279 }
280 }
281
282 fn visit_token(&mut self, token: RedLeaf<L>) {
283 let scopes = get_token_scopes(token.kind.role(), L::NAME, L::CATEGORY);
285 let style = self.theme.resolve_styles(&scopes);
286
287 let text = &self.source[token.span.start..token.span.end];
288
289 self.segments.push(HighlightSegment { span: HighlightSpan { start: token.span.start, end: token.span.end }, style, text: Cow::Borrowed(text) });
290 }
291}
292
293pub trait Highlighter {
298 fn highlight<'a>(&self, source: &'a str, language: &str, theme: crate::themes::Theme) -> oak_core::errors::ParseResult<HighlightResult<'a>>;
300}
301
302impl Highlighter for OakHighlighter {
303 fn highlight<'a>(&self, source: &'a str, language: &str, theme: crate::themes::Theme) -> oak_core::errors::ParseResult<HighlightResult<'a>> {
304 self.highlight(source, language, theme)
305 }
306}
307
308pub struct OakHighlighter {
320 pub theme: HighlightTheme,
321}
322
323impl Default for OakHighlighter {
324 fn default() -> Self {
325 Self { theme: HighlightTheme::default() }
326 }
327}
328
329impl OakHighlighter {
330 pub fn new() -> Self {
331 Self::default()
332 }
333
334 pub fn with_theme(mut self, theme: HighlightTheme) -> Self {
335 self.theme = theme;
336 self
337 }
338
339 pub fn theme(mut self, theme: crate::themes::Theme) -> Self {
341 self.theme = theme.get_theme();
342 self
343 }
344
345 pub fn highlight<'a>(&self, source: &'a str, _language: &str, theme: crate::themes::Theme) -> oak_core::errors::ParseResult<HighlightResult<'a>> {
347 let theme_config = theme.get_theme();
348
349 let segments = vec![HighlightSegment { span: Range { start: 0, end: source.len() }.into(), style: theme_config.resolve_style("none"), text: Cow::Borrowed(source) }];
353
354 Ok(HighlightResult { segments, source: Cow::Borrowed(source) })
355 }
356
357 pub fn highlight_with_language<'a, L: Language + Send + Sync + 'static, P: oak_core::parser::Parser<L>, LX: oak_core::Lexer<L>>(
358 &self,
359 source: &'a str,
360 theme: crate::themes::Theme,
361 parser: &P,
362 _lexer: &LX,
363 ) -> oak_core::errors::ParseResult<HighlightResult<'a>> {
364 let theme_config = theme.get_theme();
365 let source_text = oak_core::source::SourceText::new(source.to_string());
366 let mut cache = oak_core::parser::session::ParseSession::<L>::new(1024);
367 let parse_result = parser.parse(&source_text, &[], &mut cache);
368
369 let mut visitor = HighlightVisitor { theme: &theme_config, segments: Vec::new(), source };
370
371 let root_node = parse_result.result.map_err(|e| e)?;
372 let red_root = RedNode::new(root_node, 0);
373
374 <HighlightVisitor<'a, '_> as Visitor<L>>::visit_node(&mut visitor, red_root);
375
376 Ok(HighlightResult { segments: visitor.segments, source: Cow::Borrowed(source) })
377 }
378
379 pub fn highlight_format(&self, source: &str, language: &str, theme: crate::themes::Theme, format: crate::exporters::ExportFormat) -> oak_core::errors::ParseResult<String> {
381 let result = self.highlight(source, language, theme)?;
382
383 let content = match format {
384 crate::exporters::ExportFormat::Html => crate::exporters::HtmlExporter::new(true, true).export(&result),
385 crate::exporters::ExportFormat::Json => crate::exporters::JsonExporter { pretty: true }.export(&result),
386 crate::exporters::ExportFormat::Ansi => crate::exporters::AnsiExporter.export(&result),
387 crate::exporters::ExportFormat::Css => crate::exporters::CssExporter.export(&result),
388 _ => {
389 return Err(oak_core::errors::OakError::unsupported_format(format!("{format:?}")));
390 }
391 };
392
393 Ok(content)
394 }
395}