1mod lang;
33mod scan;
34
35use std::ops::Range;
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39#[non_exhaustive]
40pub enum Kind {
41 Plain,
43 Keyword,
45 Type,
47 Function,
49 String,
51 Comment,
53 Number,
55 Constant,
57 Punctuation,
59 Operator,
61 Attribute,
63 Property,
65 Variable,
67 Tag,
69 Heading,
71 Emphasis,
73 Link,
75 Inserted,
77 Deleted,
79 Meta,
81}
82
83impl Kind {
84 pub const ALL: [Kind; 20] = [
86 Kind::Plain,
87 Kind::Keyword,
88 Kind::Type,
89 Kind::Function,
90 Kind::String,
91 Kind::Comment,
92 Kind::Number,
93 Kind::Constant,
94 Kind::Punctuation,
95 Kind::Operator,
96 Kind::Attribute,
97 Kind::Property,
98 Kind::Variable,
99 Kind::Tag,
100 Kind::Heading,
101 Kind::Emphasis,
102 Kind::Link,
103 Kind::Inserted,
104 Kind::Deleted,
105 Kind::Meta,
106 ];
107
108 pub fn name(self) -> &'static str {
110 match self {
111 Kind::Plain => "plain",
112 Kind::Keyword => "keyword",
113 Kind::Type => "type",
114 Kind::Function => "function",
115 Kind::String => "string",
116 Kind::Comment => "comment",
117 Kind::Number => "number",
118 Kind::Constant => "constant",
119 Kind::Punctuation => "punctuation",
120 Kind::Operator => "operator",
121 Kind::Attribute => "attribute",
122 Kind::Property => "property",
123 Kind::Variable => "variable",
124 Kind::Tag => "tag",
125 Kind::Heading => "heading",
126 Kind::Emphasis => "emphasis",
127 Kind::Link => "link",
128 Kind::Inserted => "inserted",
129 Kind::Deleted => "deleted",
130 Kind::Meta => "meta",
131 }
132 }
133}
134
135pub type Span = (Range<usize>, Kind);
138
139#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
143pub struct State {
144 pub(crate) mode: u8,
145 pub(crate) n: u16,
146 pub(crate) h: u32,
147}
148
149impl State {
150 pub const START: State = State { mode: 0, n: 0, h: 0 };
152
153 pub fn to_bits(self) -> u64 {
155 u64::from(self.mode) | (u64::from(self.n) << 8) | (u64::from(self.h) << 32)
156 }
157
158 pub fn from_bits(bits: u64) -> State {
160 State { mode: (bits & 0xff) as u8, n: ((bits >> 8) & 0xffff) as u16, h: (bits >> 32) as u32 }
161 }
162
163 pub(crate) fn pack24(self) -> u32 {
166 u32::from(self.mode) | (u32::from(self.n) << 8)
167 }
168
169 pub(crate) fn unpack24(v: u32) -> State {
170 State { mode: (v & 0xff) as u8, n: ((v >> 8) & 0xffff) as u16, h: 0 }
171 }
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
176#[non_exhaustive]
177pub enum Lang {
178 Rust,
179 TypeScript,
180 JavaScript,
181 Go,
182 C,
183 Cpp,
184 Java,
185 CSharp,
186 Python,
187 Shell,
188 Sql,
189 Json,
190 Toml,
191 Yaml,
192 Html,
193 Css,
194 Markdown,
195 Diff,
196}
197
198const ALIASES: &[(&str, Lang)] = &[
200 ("rust", Lang::Rust),
201 ("rs", Lang::Rust),
202 ("typescript", Lang::TypeScript),
203 ("ts", Lang::TypeScript),
204 ("tsx", Lang::TypeScript),
205 ("mts", Lang::TypeScript),
206 ("cts", Lang::TypeScript),
207 ("javascript", Lang::JavaScript),
208 ("js", Lang::JavaScript),
209 ("jsx", Lang::JavaScript),
210 ("mjs", Lang::JavaScript),
211 ("cjs", Lang::JavaScript),
212 ("go", Lang::Go),
213 ("golang", Lang::Go),
214 ("c", Lang::C),
215 ("h", Lang::C),
216 ("cpp", Lang::Cpp),
217 ("c++", Lang::Cpp),
218 ("cc", Lang::Cpp),
219 ("cxx", Lang::Cpp),
220 ("hpp", Lang::Cpp),
221 ("hh", Lang::Cpp),
222 ("hxx", Lang::Cpp),
223 ("java", Lang::Java),
224 ("csharp", Lang::CSharp),
225 ("c#", Lang::CSharp),
226 ("cs", Lang::CSharp),
227 ("python", Lang::Python),
228 ("py", Lang::Python),
229 ("pyi", Lang::Python),
230 ("shell", Lang::Shell),
231 ("sh", Lang::Shell),
232 ("bash", Lang::Shell),
233 ("zsh", Lang::Shell),
234 ("console", Lang::Shell),
235 ("sql", Lang::Sql),
236 ("json", Lang::Json),
237 ("jsonc", Lang::Json),
238 ("json5", Lang::Json),
239 ("toml", Lang::Toml),
240 ("yaml", Lang::Yaml),
241 ("yml", Lang::Yaml),
242 ("html", Lang::Html),
243 ("htm", Lang::Html),
244 ("xml", Lang::Html),
245 ("svg", Lang::Html),
246 ("xhtml", Lang::Html),
247 ("vue", Lang::Html),
248 ("css", Lang::Css),
249 ("scss", Lang::Css),
250 ("less", Lang::Css),
251 ("markdown", Lang::Markdown),
252 ("md", Lang::Markdown),
253 ("mdx", Lang::Markdown),
254 ("diff", Lang::Diff),
255 ("patch", Lang::Diff),
256];
257
258const FILE_NAMES: &[(&str, Lang)] = &[
260 (".bashrc", Lang::Shell),
261 (".zshrc", Lang::Shell),
262 (".profile", Lang::Shell),
263 (".bash_profile", Lang::Shell),
264 ("Cargo.lock", Lang::Toml),
265 ("PKGBUILD", Lang::Shell),
266 (".gitconfig", Lang::Toml),
267];
268
269impl Lang {
270 pub const ALL: [Lang; 18] = [
272 Lang::Rust,
273 Lang::TypeScript,
274 Lang::JavaScript,
275 Lang::Go,
276 Lang::C,
277 Lang::Cpp,
278 Lang::Java,
279 Lang::CSharp,
280 Lang::Python,
281 Lang::Shell,
282 Lang::Sql,
283 Lang::Json,
284 Lang::Toml,
285 Lang::Yaml,
286 Lang::Html,
287 Lang::Css,
288 Lang::Markdown,
289 Lang::Diff,
290 ];
291
292 pub fn name(self) -> &'static str {
294 match self {
295 Lang::Rust => "rust",
296 Lang::TypeScript => "typescript",
297 Lang::JavaScript => "javascript",
298 Lang::Go => "go",
299 Lang::C => "c",
300 Lang::Cpp => "cpp",
301 Lang::Java => "java",
302 Lang::CSharp => "csharp",
303 Lang::Python => "python",
304 Lang::Shell => "shell",
305 Lang::Sql => "sql",
306 Lang::Json => "json",
307 Lang::Toml => "toml",
308 Lang::Yaml => "yaml",
309 Lang::Html => "html",
310 Lang::Css => "css",
311 Lang::Markdown => "markdown",
312 Lang::Diff => "diff",
313 }
314 }
315
316 pub fn from_name(name: &str) -> Option<Lang> {
319 let name = name.trim();
320 ALIASES.iter().find(|(alias, _)| alias.eq_ignore_ascii_case(name)).map(|(_, lang)| *lang)
321 }
322
323 pub fn for_path(path: &str) -> Option<Lang> {
326 let file = path.rsplit(['/', '\\']).next().unwrap_or(path);
327 if let Some((_, lang)) = FILE_NAMES.iter().find(|(name, _)| *name == file) {
328 return Some(*lang);
329 }
330 let (stem, ext) = file.rsplit_once('.')?;
331 if stem.is_empty() {
332 return None;
333 }
334 Lang::from_name(ext)
335 }
336}
337
338pub fn lang_for_path(path: &str) -> Option<&'static str> {
340 Lang::for_path(path).map(Lang::name)
341}
342
343pub fn highlight(lang: Option<&str>, text: &str) -> Vec<Span> {
349 let mut out = Vec::new();
350 highlight_into(lang.and_then(Lang::from_name), text, &mut out);
351 out
352}
353
354pub fn highlight_into(lang: Option<Lang>, text: &str, out: &mut Vec<Span>) {
358 if text.is_empty() {
359 return;
360 }
361 let Some(lang) = lang else {
362 out.push((0..text.len(), Kind::Plain));
363 return;
364 };
365 let mut state = State::START;
366 let mut base = 0usize;
367 for line in text.split_inclusive('\n') {
368 state = line_into(lang, line, state, base, out);
369 base = base.saturating_add(line.len());
370 }
371}
372
373pub fn highlight_line(lang: Lang, line: &str, state: State, out: &mut Vec<Span>) -> State {
376 line_into(lang, line, state, 0, out)
377}
378
379fn line_into(lang: Lang, line: &str, state: State, base: usize, out: &mut Vec<Span>) -> State {
380 let body = line.strip_suffix('\n').unwrap_or(line);
381 let body = body.strip_suffix('\r').unwrap_or(body);
382 let mut o = scan::Out::new(out, line, base);
383 let next = lang::lex(lang, body, state, &mut o);
384 o.finish();
385 next
386}
387
388pub trait Highlighter {
391 fn supports(&self, lang: &str) -> bool;
393
394 fn highlight_line(&self, lang: &str, line: &str, state: State, out: &mut Vec<Span>) -> State;
398
399 fn highlight(&self, lang: &str, text: &str, out: &mut Vec<Span>) {
401 let mut state = State::START;
402 let mut base = 0usize;
403 let mut line_spans = Vec::new();
404 for line in text.split_inclusive('\n') {
405 line_spans.clear();
406 state = self.highlight_line(lang, line, state, &mut line_spans);
407 for (range, kind) in line_spans.drain(..) {
408 let range = range.start.saturating_add(base)..range.end.saturating_add(base);
409 match out.last_mut() {
410 Some((last, k)) if *k == kind && last.end == range.start => last.end = range.end,
411 _ => out.push((range, kind)),
412 }
413 }
414 base = base.saturating_add(line.len());
415 }
416 }
417}
418
419#[derive(Clone, Copy, Debug, Default)]
421pub struct Builtin;
422
423impl Highlighter for Builtin {
424 fn supports(&self, lang: &str) -> bool {
425 Lang::from_name(lang).is_some()
426 }
427
428 fn highlight_line(&self, lang: &str, line: &str, state: State, out: &mut Vec<Span>) -> State {
429 match Lang::from_name(lang) {
430 Some(l) => highlight_line(l, line, state, out),
431 None => {
432 if !line.is_empty() {
433 out.push((0..line.len(), Kind::Plain));
434 }
435 state
436 }
437 }
438 }
439
440 fn highlight(&self, lang: &str, text: &str, out: &mut Vec<Span>) {
441 highlight_into(Lang::from_name(lang), text, out);
442 }
443}